mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-07 05:32:52 +08:00
✨ Feature(custom): add several new features for manage bucket page
This commit is contained in:
@@ -2,37 +2,42 @@
|
||||
|
||||
### 🚀 性能优化
|
||||
|
||||
- 减少了60-70%的闲置内存占用和20%的打开窗口时内存占用
|
||||
- 减少了 **60-70%** 的闲置内存占用和 **20%** 的打开窗口时内存占用
|
||||
- 优化了多个页面的加载速度和浏览性能
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
- 新功能
|
||||
- 现在支持关闭GPU加速,解决部分兼容性问题
|
||||
- 新增高级动画设置,开启后可获得更好的UI体验
|
||||
- windows下新增便携模式,无需安装即可运行,数据存储在程序目录下的`data`文件夹中,支持自动更新
|
||||
- Linux下新增`rpm`安装包
|
||||
- 管理页面新增图床编辑卡片页面,避免了之前多配置切换时的混乱
|
||||
#### ⚙️ 核心功能
|
||||
|
||||
- UI
|
||||
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫等风格
|
||||
- 重新设计了管理功能的全部页面
|
||||
- 重构了几乎全部页面,优化了数十项UI细节问题,整体风格更加统一
|
||||
- 相册页面多项优化,支持显示已选择图片数量,匹配的url列表和记忆过滤器打开状态
|
||||
- 优化了管理文件浏览页面侧边栏名字的显示,现在在超出宽度时会滚动显示完整名称
|
||||
- 插件页面现在可以浏览所有插件列表,查看详情和安装
|
||||
- 新增教学引导页面,首次运行时会自动弹出
|
||||
- 现在支持手动关闭 GPU 加速,解决部分硬件兼容性导致的黑屏或闪烁问题。
|
||||
- 新增高级动画设置,开启后可获得更佳的 UI 交互体验。
|
||||
- Windows 便携模式,无需安装运行,数据存储在程序目录下的 `data` 文件夹中,且支持自动更新。
|
||||
- Linux 新增 `rpm` 安装包。
|
||||
- 新增图床编辑卡片页面,解决多配置切换时的混乱问题。
|
||||
- 文件浏览页面新增列表模式支持。
|
||||
|
||||
- 其它
|
||||
- 原管理功能重命名为`云端`,更符合实际功能
|
||||
- 现在重置图床后不再自动返回上一页面
|
||||
#### 🎨 UI 界面
|
||||
|
||||
- 接入主题仓库 [PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub),支持自定义下载。
|
||||
- 提供 12 个内置主题(如 bilibili、二次元、极夜紫等风格)。
|
||||
- 重新设计了管理功能的全部页面,重构了几乎所有业务页面,优化数十项 UI 细节。
|
||||
- 优化相册页面卡片样式,边界更清晰,提升选择框视觉效果。
|
||||
- 优化多个页面在窄屏下的显示,避免内容溢出。
|
||||
- 文件浏览侧边栏名称超出宽度时支持滚动显示完整名称。
|
||||
- 相册页面支持显示已选数量、匹配的 URL 列表和记忆过滤器状态。
|
||||
- 支持浏览完整插件列表、查看详情及一键安装。
|
||||
- 新增新手引导页面,首次运行自动弹出。
|
||||
|
||||
#### 📝 其它
|
||||
|
||||
- 原“管理”功能重命名为 **“云端”**,更符合实际功能定义。
|
||||
- 重置图床后不再自动返回上一页面。
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
- 修复了管理页面中排序下拉框显示异常的问题
|
||||
- 修复了管理页面图床列表没有正确为当前选中图床高亮的问题
|
||||
- 修复了暗色模式下任务页面的显示问题
|
||||
- 修复了图床设置页面设置为默认图床按钮状态没有及时更新的问题
|
||||
- 修复了预处理设置页面,图床水印独立设置的按钮状态没有及时更新的问题
|
||||
- 修复了部分页面底部元素被遮挡的问题
|
||||
- 修复了管理页面中排序下拉框显示异常的问题。
|
||||
- 修复了管理页面图床列表未正确高亮当前选中项的问题。
|
||||
- 修复了暗色模式下任务页面的显示异常。
|
||||
- 修复了图床设置页面“设置为默认图床”按钮状态更新不及时的问题。
|
||||
- 修复了预处理设置页面中,图床水印独立设置按钮状态不同步的问题。
|
||||
- 修复了部分页面底部元素被遮挡的问题。
|
||||
|
||||
@@ -2,24 +2,42 @@
|
||||
|
||||
### 🚀 Performance Optimization
|
||||
|
||||
- Reduced idle memory usage by 60-70% and memory usage when opening windows by 20%
|
||||
- Reduced **60-70%** idle memory usage and **20%** memory usage when opening windows.
|
||||
- Optimized loading speed and browsing performance for multiple pages
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- 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 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
|
||||
- Now supports disabling GPU acceleration to resolve some compatibility issues
|
||||
- Added advanced animation settings for a better UI experience
|
||||
- Optimized the loading speed of multiple pages and browsing performance
|
||||
#### ⚙️ Core Features
|
||||
|
||||
- Now supports manually disabling GPU acceleration to resolve black screen or flickering issues caused by some hardware compatibility.
|
||||
- Added advanced animation settings for a better UI interaction experience.
|
||||
- Windows portable mode, no installation required, data is stored in the `data` folder in the program directory, and supports automatic updates.
|
||||
- Added `rpm` installation package for Linux.
|
||||
- Added image hosting editing card page to solve confusion when switching multiple configurations.
|
||||
- Added list mode support for the file browsing page.
|
||||
|
||||
#### 🎨 UI Interface
|
||||
|
||||
- Integrated theme repository [PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub), supporting custom downloads.
|
||||
- Provided 12 built-in themes (such as bilibili, 二次元, 极夜紫 styles).
|
||||
- Redesigned all pages of the management feature, refactored almost all business pages, and optimized dozens of UI details.
|
||||
- Optimized album page card styles, clearer boundaries, and improved selection box visual effects.
|
||||
- Optimized the display of multiple pages on narrow screens to avoid content overflow.
|
||||
- Supported scrolling to display the full name when the file browsing sidebar name exceeds the width.
|
||||
- The album page supports displaying the number of selected items, matching URL lists, and remembering filter states.
|
||||
- Supports browsing the complete plugin list, viewing details, and one-click installation.
|
||||
- Added a new user guide page that automatically pops up on the first run.
|
||||
|
||||
#### 📝 Others
|
||||
|
||||
- The original "Management" feature has been renamed to **"Cloud"** to better reflect its actual functionality.
|
||||
- After resetting the image hosting, it no longer automatically returns to the previous page.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Fixed an issue where the sort dropdown in the management page displayed abnormally
|
||||
- Fixed an issue where the task page displayed incorrectly in dark mode
|
||||
- Fixed an issue where the default image bed button status on the image bed settings page did not update in time
|
||||
- Fixed an issue where the button status for independent watermark settings on the image bed watermark settings page did not update in time
|
||||
- Fixed the issue of abnormal display of the sorting dropdown box on the management page.
|
||||
- Fixed the issue where the image hosting list on the management page did not correctly highlight the currently selected item.
|
||||
- Fixed the display issue of the task page in dark mode.
|
||||
- Fixed the issue where the "Set as Default Image Hosting" button status on the image hosting settings page was not updated in a timely manner.
|
||||
- Fixed the issue where the independent watermark setting button status on the image hosting was not synchronized in the preprocessing settings page.
|
||||
- Fixed the issue where bottom elements of some pages were obscured.
|
||||
|
||||
@@ -53,9 +53,7 @@ const buildMiniPageMenu = () => {
|
||||
label: $t('HIDE_MINI_WINDOW'),
|
||||
click() {
|
||||
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)
|
||||
console.log('hide mini window', miniWindow)
|
||||
miniWindow?.close()
|
||||
console.log('mini window closed')
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div v-if="isLoading" class="loading-placeholder">
|
||||
<div class="loading-spinner" />
|
||||
<div class="relative flex h-full w-full items-center justify-center p-0">
|
||||
<div v-if="isLoading" class="flex h-full w-full items-center justify-center">
|
||||
<div class="h-[34px] w-[34px] animate-spin rounded-full border-3 border-t-3 border-border border-t-accent" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="isShowThumbnail && item.isImage ? base64Image : `./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
|
||||
alt=""
|
||||
class="image"
|
||||
class="h-full w-full object-contain"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img v-else :src="`./assets/icons/${getFileIconPath(item.fileName ?? '')}`" alt="" class="image" />
|
||||
<img
|
||||
v-else
|
||||
:src="`./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
|
||||
alt=""
|
||||
class="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,49 +65,3 @@ onBeforeMount(async () => {
|
||||
await createBase64Image()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: var(--radius-round);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div v-if="isLoading" class="loading-placeholder">
|
||||
<div class="loading-spinner" />
|
||||
<div class="relative flex h-full w-full items-center justify-center p-0">
|
||||
<div v-if="isLoading" class="flex h-full w-full items-center justify-center">
|
||||
<div class="h-[34px] w-[34px] animate-spin rounded-full border-3 border-t-3 border-border border-t-accent" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="imageSource"
|
||||
alt=""
|
||||
class="image"
|
||||
class="h-full w-full object-contain"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img v-else :src="iconPath" alt="" class="image" />
|
||||
<img v-else :src="iconPath" alt="" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -76,49 +76,3 @@ watch(() => [props.url, props.item], getUrl, { deep: true })
|
||||
|
||||
onMounted(getUrl)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: var(--radius-round);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
418
src/renderer/components/ImagePreview.vue
Normal file
418
src/renderer/components/ImagePreview.vue
Normal file
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<transition name="modal">
|
||||
<div
|
||||
v-if="gallerySliderControl.visible"
|
||||
class="image-preview-modal fixed inset-0 z-1000 flex items-center justify-center outline-none"
|
||||
tabindex="0"
|
||||
@click.stop
|
||||
@wheel="handleImageWheel"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" :class="{ 'advanced-animation': enableAdvancedAnimation }" />
|
||||
<div class="relative max-h-[90vh] max-w-[90vw] overflow-hidden rounded-xl bg-surface shadow-lg">
|
||||
<button
|
||||
class="absolute top-4 right-4 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-danger bg-danger/70 text-white hover:bg-danger hover:text-white"
|
||||
@click="handleClose"
|
||||
>
|
||||
<XIcon :size="24" />
|
||||
</button>
|
||||
|
||||
<!-- Zoom controls -->
|
||||
<div class="absolute top-4 left-4 z-10 flex items-center gap-2 rounded-lg bg-black/70 p-2">
|
||||
<button class="zoom-btn" :disabled="imagePreviewState.scale <= 0.1" @click="zoomOut">
|
||||
<span>-</span>
|
||||
</button>
|
||||
<span class="min-w-[50px] text-center text-sm font-medium text-white"
|
||||
>{{ Math.round(imagePreviewState.scale * 100) }}%</span
|
||||
>
|
||||
<button class="zoom-btn" :disabled="imagePreviewState.scale >= 5" @click="zoomIn">
|
||||
<span>+</span>
|
||||
</button>
|
||||
<button class="zoom-btn reset-btn" @click="resetImageTransform">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="relative flex items-center">
|
||||
<button class="nav-button prev" :disabled="gallerySliderControl.index === 0" @click.stop="navigateImage(-1)">
|
||||
<ChevronLeftIcon :size="24" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="relative flex h-[80vh] w-[90vw] items-center justify-center overflow-hidden bg-black select-none active:cursor-grab!"
|
||||
@mousedown="handleImageMouseDown"
|
||||
@mousemove="handleImageMouseMove"
|
||||
@mouseup="handleImageMouseUp"
|
||||
@mouseleave="handleImageMouseUp"
|
||||
@touchstart="handleImageTouchStart"
|
||||
@touchmove="handleImageTouchMove"
|
||||
@touchend="handleImageTouchEnd"
|
||||
>
|
||||
<img
|
||||
ref="previewImageRef"
|
||||
:src="currentPreviewImage?.src"
|
||||
:alt="currentPreviewImage?.intro"
|
||||
class="block h-auto max-h-none w-auto max-w-none origin-center object-contain"
|
||||
:style="imageTransformStyle"
|
||||
@load="onPreviewImageLoad"
|
||||
@dragstart.prevent
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="nav-button next"
|
||||
:disabled="gallerySliderControl.index === filterList.length - 1"
|
||||
@click.stop="navigateImage(1)"
|
||||
>
|
||||
<ChevronRightIcon :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border border-border-secondary px-6 py-4">
|
||||
<h3 class="m-0 mr-4 flex-1 overflow-hidden text-base font-semibold text-ellipsis text-main">
|
||||
{{ currentPreviewImage?.intro }}
|
||||
</h3>
|
||||
<div class="mr-4 text-sm font-semibold whitespace-nowrap text-main">
|
||||
{{ gallerySliderControl.index + 1 }} / {{ filterList.length }}
|
||||
</div>
|
||||
<div class="text-center text-xs font-medium text-main">
|
||||
{{ t('pages.gallery.previewHelp') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronLeftIcon, ChevronRightIcon, XIcon } from 'lucide-vue-next'
|
||||
import { computed, nextTick, onMounted, reactive, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { getConfig } from '@/utils/dataSender'
|
||||
|
||||
const gallerySliderControl = defineModel<{
|
||||
visible: boolean
|
||||
index: number
|
||||
}>('gallerySliderControl', { required: true })
|
||||
|
||||
const { filterList, isAlwaysForceReload } = defineProps<{
|
||||
filterList: {
|
||||
src: string
|
||||
imgUrl?: string
|
||||
galleryPath?: string
|
||||
intro?: string
|
||||
}[]
|
||||
isAlwaysForceReload: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const previewImageRef = useTemplateRef('previewImageRef')
|
||||
const enableAdvancedAnimation = ref(false)
|
||||
const imagePreviewState = reactive({
|
||||
scale: 1,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startTranslateX: 0,
|
||||
startTranslateY: 0,
|
||||
isSwipeMode: false,
|
||||
swipeStartX: 0,
|
||||
swipeThreshold: 100,
|
||||
})
|
||||
|
||||
const imageTransformStyle = computed(() => {
|
||||
// Check if image overflows the viewport
|
||||
const imageElement = previewImageRef.value
|
||||
let isDraggable = false
|
||||
|
||||
if (imageElement && imageElement.naturalWidth && imageElement.naturalHeight) {
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (viewerElement) {
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const currentImageWidth = imageElement.naturalWidth * imagePreviewState.scale
|
||||
const currentImageHeight = imageElement.naturalHeight * imagePreviewState.scale
|
||||
isDraggable = currentImageWidth > viewerRect.width + 1 || currentImageHeight > viewerRect.height + 1
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `translate(${imagePreviewState.translateX}px, ${imagePreviewState.translateY}px) scale(${imagePreviewState.scale})`,
|
||||
cursor: imagePreviewState.isDragging ? 'grabbing' : isDraggable ? 'grab' : 'default',
|
||||
transition: 'none',
|
||||
}
|
||||
})
|
||||
|
||||
const currentPreviewImage = computed(() => {
|
||||
const item = filterList[gallerySliderControl.value.index]
|
||||
if (!item) return null
|
||||
const cacheBustedItem = { ...item }
|
||||
if (isAlwaysForceReload) {
|
||||
if (cacheBustedItem.imgUrl) {
|
||||
cacheBustedItem.imgUrl = addCacheBustParam(cacheBustedItem.imgUrl)
|
||||
}
|
||||
if (cacheBustedItem.galleryPath) {
|
||||
cacheBustedItem.galleryPath = addCacheBustParam(cacheBustedItem.galleryPath)
|
||||
}
|
||||
}
|
||||
const src = cacheBustedItem.src || cacheBustedItem.galleryPath || cacheBustedItem.imgUrl || ''
|
||||
cacheBustedItem.src = isAlwaysForceReload ? addCacheBustParam(src) : src
|
||||
return cacheBustedItem
|
||||
})
|
||||
|
||||
function handleImageWheel(event: WheelEvent) {
|
||||
event.preventDefault()
|
||||
const delta = event.deltaY > 0 ? -1 : 1
|
||||
const zoomFactor = 1.1
|
||||
const newScale =
|
||||
delta > 0 ? Math.min(imagePreviewState.scale * zoomFactor, 5) : Math.max(imagePreviewState.scale / zoomFactor, 0.1)
|
||||
|
||||
zoomToScale(newScale)
|
||||
}
|
||||
|
||||
function zoomToScale(newScale: number) {
|
||||
const oldScale = imagePreviewState.scale
|
||||
imagePreviewState.scale = newScale
|
||||
|
||||
const optimalScale = calculateOptimalScale()
|
||||
if (newScale <= optimalScale) {
|
||||
imagePreviewState.translateX = 0
|
||||
imagePreviewState.translateY = 0
|
||||
} else {
|
||||
const scaleDiff = newScale / oldScale
|
||||
imagePreviewState.translateX *= scaleDiff
|
||||
imagePreviewState.translateY *= scaleDiff
|
||||
}
|
||||
}
|
||||
|
||||
function calculateOptimalScale(): number {
|
||||
const imageElement = previewImageRef.value
|
||||
if (!imageElement) {
|
||||
return 1
|
||||
}
|
||||
if (!imageElement.naturalWidth || !imageElement.naturalHeight) {
|
||||
return 1
|
||||
}
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (!viewerElement) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const viewerWidth = viewerRect.width
|
||||
const viewerHeight = viewerRect.height
|
||||
|
||||
const imageWidth = imageElement.naturalWidth
|
||||
const imageHeight = imageElement.naturalHeight
|
||||
const scaleX = viewerWidth / imageWidth
|
||||
const scaleY = viewerHeight / imageHeight
|
||||
const optimalScale = Math.min(scaleX, scaleY, 1)
|
||||
|
||||
return optimalScale
|
||||
}
|
||||
|
||||
function navigateImage(direction: number) {
|
||||
const newIndex = gallerySliderControl.value.index + direction
|
||||
if (newIndex >= 0 && newIndex < filterList.length) {
|
||||
gallerySliderControl.value.index = newIndex
|
||||
resetImageTransform()
|
||||
}
|
||||
}
|
||||
|
||||
function resetImageTransform() {
|
||||
const optimalScale = calculateOptimalScale()
|
||||
imagePreviewState.scale = optimalScale
|
||||
imagePreviewState.translateX = 0
|
||||
imagePreviewState.translateY = 0
|
||||
imagePreviewState.isDragging = false
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault()
|
||||
navigateImage(-1)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
event.preventDefault()
|
||||
navigateImage(1)
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
handleClose()
|
||||
break
|
||||
case '=':
|
||||
case '+':
|
||||
event.preventDefault()
|
||||
zoomIn()
|
||||
break
|
||||
case '-':
|
||||
event.preventDefault()
|
||||
zoomOut()
|
||||
break
|
||||
case '0':
|
||||
event.preventDefault()
|
||||
resetImageTransform()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
const newScale = Math.max(imagePreviewState.scale / 1.2, 0.1)
|
||||
zoomToScale(newScale)
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
zoomToScale(Math.min(imagePreviewState.scale * 1.2, 5))
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
gallerySliderControl.value.index = 0
|
||||
gallerySliderControl.value.visible = false
|
||||
resetImageTransform()
|
||||
}
|
||||
|
||||
function handleImageMouseDown(event: MouseEvent) {
|
||||
const imageElement = previewImageRef.value
|
||||
let isImageLargerThanViewer = false
|
||||
|
||||
if (imageElement && imageElement.naturalWidth && imageElement.naturalHeight) {
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (viewerElement) {
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const currentImageWidth = imageElement.naturalWidth * imagePreviewState.scale
|
||||
const currentImageHeight = imageElement.naturalHeight * imagePreviewState.scale
|
||||
isImageLargerThanViewer = currentImageWidth > viewerRect.width + 1 || currentImageHeight > viewerRect.height + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!isImageLargerThanViewer) {
|
||||
imagePreviewState.isSwipeMode = true
|
||||
imagePreviewState.swipeStartX = event.clientX
|
||||
} else {
|
||||
imagePreviewState.isDragging = true
|
||||
imagePreviewState.startX = event.clientX
|
||||
imagePreviewState.startY = event.clientY
|
||||
imagePreviewState.startTranslateX = imagePreviewState.translateX
|
||||
imagePreviewState.startTranslateY = imagePreviewState.translateY
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleImageMouseMove(event: MouseEvent) {
|
||||
if (imagePreviewState.isDragging) {
|
||||
const deltaX = event.clientX - imagePreviewState.startX
|
||||
const deltaY = event.clientY - imagePreviewState.startY
|
||||
imagePreviewState.translateX = imagePreviewState.startTranslateX + deltaX
|
||||
imagePreviewState.translateY = imagePreviewState.startTranslateY + deltaY
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageMouseUp(event: MouseEvent) {
|
||||
if (imagePreviewState.isSwipeMode) {
|
||||
const deltaX = event.clientX - imagePreviewState.swipeStartX
|
||||
if (Math.abs(deltaX) > imagePreviewState.swipeThreshold) {
|
||||
if (deltaX > 0) {
|
||||
navigateImage(-1)
|
||||
} else {
|
||||
navigateImage(1)
|
||||
}
|
||||
}
|
||||
imagePreviewState.isSwipeMode = false
|
||||
}
|
||||
imagePreviewState.isDragging = false
|
||||
}
|
||||
|
||||
function handleImageTouchStart(event: TouchEvent) {
|
||||
const touch = event.touches[0]
|
||||
const imageElement = previewImageRef.value
|
||||
let isImageLargerThanViewer = false
|
||||
|
||||
if (imageElement && imageElement.naturalWidth && imageElement.naturalHeight) {
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (viewerElement) {
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const currentImageWidth = imageElement.naturalWidth * imagePreviewState.scale
|
||||
const currentImageHeight = imageElement.naturalHeight * imagePreviewState.scale
|
||||
isImageLargerThanViewer = currentImageWidth > viewerRect.width + 1 || currentImageHeight > viewerRect.height + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!isImageLargerThanViewer) {
|
||||
imagePreviewState.isSwipeMode = true
|
||||
imagePreviewState.swipeStartX = touch.clientX
|
||||
} else {
|
||||
imagePreviewState.isDragging = true
|
||||
imagePreviewState.startX = touch.clientX
|
||||
imagePreviewState.startY = touch.clientY
|
||||
imagePreviewState.startTranslateX = imagePreviewState.translateX
|
||||
imagePreviewState.startTranslateY = imagePreviewState.translateY
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleImageTouchMove(event: TouchEvent) {
|
||||
if (imagePreviewState.isDragging) {
|
||||
const touch = event.touches[0]
|
||||
const deltaX = touch.clientX - imagePreviewState.startX
|
||||
const deltaY = touch.clientY - imagePreviewState.startY
|
||||
imagePreviewState.translateX = imagePreviewState.startTranslateX + deltaX
|
||||
imagePreviewState.translateY = imagePreviewState.startTranslateY + deltaY
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleImageTouchEnd(event: TouchEvent) {
|
||||
if (imagePreviewState.isSwipeMode && event.changedTouches.length > 0) {
|
||||
const touch = event.changedTouches[0]
|
||||
const deltaX = touch.clientX - imagePreviewState.swipeStartX
|
||||
if (Math.abs(deltaX) > imagePreviewState.swipeThreshold) {
|
||||
if (deltaX > 0) {
|
||||
navigateImage(-1)
|
||||
} else {
|
||||
navigateImage(1)
|
||||
}
|
||||
}
|
||||
imagePreviewState.isSwipeMode = false
|
||||
}
|
||||
imagePreviewState.isDragging = false
|
||||
}
|
||||
|
||||
function onPreviewImageLoad() {
|
||||
nextTick(() => {
|
||||
resetImageTransform()
|
||||
})
|
||||
}
|
||||
|
||||
const addCacheBustParam = (url: string | undefined) => {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (!(url.startsWith('http://') || url.startsWith('https://'))) {
|
||||
return url
|
||||
}
|
||||
try {
|
||||
const separator = url.includes('?') ? '&' : '?'
|
||||
return `${url}${separator}cbplist=${new Date().getTime()}`
|
||||
} catch (_e) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
async function initConf() {
|
||||
const settingConfig = await getConfig<any>('settings')
|
||||
enableAdvancedAnimation.value = settingConfig.enableAdvancedAnimation || false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initConf()
|
||||
resetImageTransform()
|
||||
nextTick(() => {
|
||||
const modal = document.querySelector('.image-preview-modal') as HTMLElement
|
||||
if (modal) {
|
||||
modal.focus()
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -977,7 +977,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label class="title-text">{{ $t('pages.settings.upload.availablePlaceholders') }}</label>
|
||||
<placeholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
|
||||
<PlaceholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1014,7 +1014,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import customRadioOption from '@/components/common/CustomRadioOption.vue'
|
||||
import customRange from '@/components/common/CustomRange.vue'
|
||||
import customSwitch from '@/components/common/CustomSwitch.vue'
|
||||
import placeholderTable from '@/components/common/PlaceholderTable.vue'
|
||||
import PlaceholderTable from '@/components/common/PlaceholderTable.vue'
|
||||
import PerPicbedSetting from '@/components/PerPicbedSetting.vue'
|
||||
import { getRawData } from '@/utils/common'
|
||||
import { configPaths } from '@/utils/configPaths'
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="image-container">
|
||||
<div v-if="isLoading" class="loading-placeholder">
|
||||
<div class="loading-spinner" />
|
||||
<div class="relative flex h-full w-full items-center justify-center p-0">
|
||||
<div v-if="isLoading" class="flex h-full w-full items-center justify-center">
|
||||
<div class="h-[34px] w-[34px] animate-spin rounded-full border-3 border-t-3 border-border border-t-accent" />
|
||||
</div>
|
||||
<img
|
||||
v-else-if="!hasError"
|
||||
:src="imageSource"
|
||||
alt=""
|
||||
class="image"
|
||||
class="h-full w-full object-contain"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<img v-else :src="iconPath" alt="" class="image" />
|
||||
<img v-else :src="iconPath" alt="" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,49 +104,3 @@ watch(() => [props.url, props.item], fetchImage, { deep: true })
|
||||
|
||||
onMounted(fetchImage)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 2px solid #e4e7ed;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: var(--radius-round);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</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"
|
||||
class="multiselect-dropdown fixed z-10000 mt-[2px] max-h-[150px] min-w-[185px] overflow-y-auto rounded-md border border-border-secondary bg-bg-tertiary px-2 py-1.5 text-main shadow-lg"
|
||||
>
|
||||
<label
|
||||
v-for="item in allList"
|
||||
|
||||
@@ -542,7 +542,7 @@
|
||||
"refresh": "Refresh",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
"savedConfigs": "Enter Saved Cloud",
|
||||
"savedConfigs": "Saved Cloud",
|
||||
"selectPlaceholder": "Please select",
|
||||
"tips": "Tips",
|
||||
"title": "Cloud Management",
|
||||
|
||||
@@ -542,7 +542,7 @@
|
||||
"refresh": "刷新",
|
||||
"reset": "重置",
|
||||
"save": "保存",
|
||||
"savedConfigs": "进入已配置云端",
|
||||
"savedConfigs": "已配置云端",
|
||||
"selectPlaceholder": "请选择",
|
||||
"tips": "提示",
|
||||
"title": "云端管理",
|
||||
|
||||
@@ -542,7 +542,7 @@
|
||||
"refresh": "重新整理",
|
||||
"reset": "重設",
|
||||
"save": "儲存",
|
||||
"savedConfigs": "進入已配置雲端",
|
||||
"savedConfigs": "已配置雲端",
|
||||
"selectPlaceholder": "請選擇",
|
||||
"tips": "提示",
|
||||
"title": "雲端存儲管理",
|
||||
|
||||
@@ -178,4 +178,16 @@
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@
|
||||
>
|
||||
<div class="flex h-full w-full">
|
||||
<div
|
||||
class="flex min-h-0 max-w-[400px] min-w-[120px] flex-col border-r-2 border-r-border transition-all duration-100 ease-out"
|
||||
class="flex min-h-0 max-w-[400px] min-w-[40px] flex-col border-r-2 border-r-border transition-all duration-100 ease-out"
|
||||
:style="{ width: sidebarWidth + 'px' }"
|
||||
>
|
||||
<div class="shrink-0 border-b-2 border-b-border-secondary p-2">
|
||||
|
||||
24
src/renderer/manage/pages/components/FileInfo.vue
Normal file
24
src/renderer/manage/pages/components/FileInfo.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-row flex-wrap items-center gap-2 p-0">
|
||||
<div class="ml-2 flex items-center gap-2 p-1">
|
||||
<FileIcon class="h-[13px] w-[13px] text-accent" />
|
||||
<span class="text-xs font-semibold text-secondary">{{
|
||||
`${t('pages.manage.bucket.fileNum', { num: currentPageFilesInfo.length })}`
|
||||
}}</span>
|
||||
<HardDriveIcon class="h-[13px] w-[13px] text-accent" />
|
||||
<span class="text-xs font-semibold text-secondary">{{
|
||||
`${t('pages.manage.bucket.pageFileSize', { size: calculateAllFileSize })}`
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FileIcon, HardDriveIcon } from 'lucide-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const { currentPageFilesInfo, calculateAllFileSize } = defineProps<{
|
||||
currentPageFilesInfo: any[]
|
||||
calculateAllFileSize: string
|
||||
}>()
|
||||
</script>
|
||||
39
src/renderer/manage/pages/components/IconButton.vue
Normal file
39
src/renderer/manage/pages/components/IconButton.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="group relative inline-block">
|
||||
<button
|
||||
class="relative flex cursor-pointer items-center justify-center gap-1 rounded-md p-2 text-xs font-medium transition-all duration-150 ease-apple hover:-translate-y-px disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:class="{
|
||||
'border-none bg-accent text-white hover:bg-accent-hover': type === 'primary',
|
||||
'border-[1.5px] border-border text-main hover:bg-accent/50 hover:text-white': type === 'secondary',
|
||||
'border-none bg-error/80 text-white hover:bg-error': type === 'danger',
|
||||
}"
|
||||
:disabled="disabled"
|
||||
@click="emit('click')"
|
||||
>
|
||||
<component :is="icon" v-if="icon" class="h-[16px] w-[16px]" />
|
||||
<span v-if="title">{{ title }}</span>
|
||||
<span
|
||||
v-if="tips"
|
||||
class="invisible absolute top-[125%] left-1/2 z-10 w-max max-w-[200px] translate-x-[-50%] rounded-md border border-border bg-bg-tertiary p-2 text-center text-xs text-main opacity-0 shadow-md transition-opacity duration-300 group-hover:visible group-hover:opacity-100"
|
||||
>{{ tips }}</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const {
|
||||
icon = null,
|
||||
title = '',
|
||||
tips = '',
|
||||
type,
|
||||
disabled = false,
|
||||
} = defineProps<{
|
||||
icon?: any
|
||||
title?: string
|
||||
tips?: string
|
||||
disabled?: boolean
|
||||
type: 'primary' | 'secondary' | 'danger'
|
||||
}>()
|
||||
const emit = defineEmits<(e: 'click') => void>()
|
||||
</script>
|
||||
@@ -1,14 +1,7 @@
|
||||
/* BucketPage Styles */
|
||||
@import "tailwindcss" reference;
|
||||
@import "../../../assets/css/theme.css" reference;
|
||||
@import "../../../assets/css/utilities.css" reference;
|
||||
|
||||
.bucket-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
padding: 0.25rem;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Header Card */
|
||||
.bucket-card {
|
||||
@@ -197,8 +190,7 @@
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@apply w-[16px] h-[16px];
|
||||
}
|
||||
|
||||
.search-input {
|
||||
@@ -546,29 +538,9 @@
|
||||
}
|
||||
|
||||
.file-action-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.375rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--color-text-secondary);
|
||||
background: var(--color-background-secondary);
|
||||
transition: var(--transition-fast);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-action-button:hover {
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-surface);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.file-action-button.danger:hover {
|
||||
color: var(--color-error);
|
||||
background: rgb(239 68 68 / 10%);
|
||||
@apply flex justify-center items-center border-none rounded-sm p-1.5 w-[32px] h-[32px] text-secondary bg-bg-secondary transition-all duration-fast ease-apple cursor-pointer;
|
||||
@apply hover:bg-accent/10 hover:text-main hover:-translate-y-px;
|
||||
@apply hover:[.danger]:text-error hover:[.danger]:bg-error/10;
|
||||
}
|
||||
|
||||
.file-checkbox {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="relative z-1 no-scrollbar flex h-full w-full flex-col items-center justify-start gap-4 overflow-auto rounded-xl border-none p-4 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-2 shadow-md max-md:items-stretch max-md:p-5"
|
||||
class="flex w-full items-center justify-between gap-4 rounded-2xl border border-border-secondary px-6 py-0 shadow-md max-md:items-stretch max-md:p-5"
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-4 p-1">
|
||||
<ImagesIcon :size="24" class="text-accent" />
|
||||
@@ -205,21 +205,21 @@
|
||||
v-else
|
||||
:key="componentKey"
|
||||
ref="virtualScrollerRef"
|
||||
:items="filterList"
|
||||
:view-mode="viewMode"
|
||||
class="virtual-gallery-scroller min-h-0 w-full flex-1 p-3"
|
||||
:items="filterList"
|
||||
:item-height="300"
|
||||
:grid-breakpoints="effectiveGridBreakpoints"
|
||||
key-field="key"
|
||||
>
|
||||
<template #default="{ item, index }">
|
||||
<div
|
||||
class="group/image m-0 box-border flex h-[calc(100%-8px)] w-full cursor-pointer flex-col overflow-hidden rounded-lg border border-border-secondary transition-all duration-fast ease-apple hover:-translate-y-[2px] hover:border-border hover:shadow-md [.selected]:border-2 [.selected]:border-accent [.selected]:shadow-md"
|
||||
class="group/image m-0 box-border flex h-[calc(100%-8px)] w-full cursor-pointer flex-col overflow-hidden rounded-lg border-2 border-border shadow-sm transition-all duration-fast ease-apple hover:-translate-y-[2px] hover:border-accent hover:shadow-md [.selected]:border-2 [.selected]:border-accent [.selected]:shadow-md"
|
||||
:class="{ selected: choosedList[item.id || ''] }"
|
||||
@click="handleChooseImage(!choosedList[item.id || ''], index)"
|
||||
>
|
||||
<div
|
||||
class="relative flex aspect-auto min-h-0 flex-1 items-center justify-center overflow-hidden"
|
||||
class="relative mb-2 flex aspect-auto min-h-0 flex-1 items-center justify-center overflow-hidden border-b border-dashed border-b-accent/40"
|
||||
@click.stop="zoomImage(index)"
|
||||
>
|
||||
<img
|
||||
@@ -245,16 +245,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex min-h-[80px] shrink-0 flex-col justify-between p-3">
|
||||
<div class="flex shrink-0 flex-col justify-between">
|
||||
<div
|
||||
class="mb-3 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-main"
|
||||
class="mb-1.5 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap text-main"
|
||||
:title="(item.fileName || '').toString().length > 30 ? item.fileName || '' : ''"
|
||||
>
|
||||
{{ formatFileName(item.fileName || '') }}
|
||||
<div class="text-center">{{ formatFileName(item.fileName || '') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
<div class="mr-2 flex items-center justify-between">
|
||||
<div class="flex flex-1 justify-center gap-2">
|
||||
<button :title="t('pages.gallery.copy')" class="icon-button copy-icon" @click.stop="copy(item)">
|
||||
<ClipboardIcon :size="16" />
|
||||
</button>
|
||||
@@ -282,7 +282,7 @@
|
||||
@change="e => handleChooseImage((e.target as HTMLInputElement).checked, index)"
|
||||
/>
|
||||
<span
|
||||
class="relative inline-block h-[16px] w-[16px] rounded-sm border-2 border-border transition-all duration-fast ease-apple peer-checked:border-accent-hover peer-checked:bg-accent peer-checked:after:absolute peer-checked:after:top-[-2px] peer-checked:after:left-px peer-checked:after:text-[12px] peer-checked:after:font-bold peer-checked:after:text-white peer-checked:after:content-['✓']"
|
||||
class="relative inline-block h-[16px] w-[16px] rounded-sm border-2 border-accent/50 transition-all duration-fast ease-apple peer-checked:border-accent-hover peer-checked:bg-accent peer-checked:after:absolute peer-checked:after:top-[-2px] peer-checked:after:left-px peer-checked:after:text-[12px] peer-checked:after:font-bold peer-checked:after:text-white peer-checked:after:content-['✓']"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
@@ -293,92 +293,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Custom Image Preview Modal -->
|
||||
<transition name="modal">
|
||||
<div
|
||||
v-if="gallerySliderControl.visible"
|
||||
class="image-preview-modal fixed inset-0 z-1000 flex items-center justify-center outline-none"
|
||||
tabindex="0"
|
||||
@click.stop
|
||||
@wheel="handleImageWheel"
|
||||
@keydown="handleKeydown"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" :class="{ 'advanced-animation': enableAdvancedAnimation }" />
|
||||
<div class="relative max-h-[90vh] max-w-[90vw] overflow-hidden rounded-xl bg-surface shadow-lg">
|
||||
<button
|
||||
class="absolute top-4 right-4 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-danger bg-danger/70 text-white hover:bg-danger hover:text-white"
|
||||
@click="handleClose"
|
||||
>
|
||||
<XIcon :size="24" />
|
||||
</button>
|
||||
|
||||
<!-- Zoom controls -->
|
||||
<div class="absolute top-4 left-4 z-10 flex items-center gap-2 rounded-lg bg-black/70 p-2">
|
||||
<button class="zoom-btn" :disabled="imagePreviewState.scale <= 0.1" @click="zoomOut">
|
||||
<span>-</span>
|
||||
</button>
|
||||
<span class="min-w-[50px] text-center text-sm font-medium text-white"
|
||||
>{{ Math.round(imagePreviewState.scale * 100) }}%</span
|
||||
>
|
||||
<button class="zoom-btn" :disabled="imagePreviewState.scale >= 5" @click="zoomIn">
|
||||
<span>+</span>
|
||||
</button>
|
||||
<button class="zoom-btn reset-btn" @click="resetImageTransform">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="relative flex items-center">
|
||||
<button
|
||||
class="nav-button prev"
|
||||
:disabled="gallerySliderControl.index === 0"
|
||||
@click.stop="navigateImage(-1)"
|
||||
>
|
||||
<ChevronLeftIcon :size="24" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="relative flex h-[80vh] w-[90vw] items-center justify-center overflow-hidden bg-black select-none active:cursor-grab!"
|
||||
@mousedown="handleImageMouseDown"
|
||||
@mousemove="handleImageMouseMove"
|
||||
@mouseup="handleImageMouseUp"
|
||||
@mouseleave="handleImageMouseUp"
|
||||
@touchstart="handleImageTouchStart"
|
||||
@touchmove="handleImageTouchMove"
|
||||
@touchend="handleImageTouchEnd"
|
||||
>
|
||||
<img
|
||||
ref="previewImageRef"
|
||||
:src="currentPreviewImage?.src"
|
||||
:alt="currentPreviewImage?.intro"
|
||||
class="block h-auto max-h-none w-auto max-w-none origin-center object-contain"
|
||||
:style="imageTransformStyle"
|
||||
@load="onPreviewImageLoad"
|
||||
@dragstart.prevent
|
||||
@contextmenu.prevent
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="nav-button next"
|
||||
:disabled="gallerySliderControl.index === filterList.length - 1"
|
||||
@click.stop="navigateImage(1)"
|
||||
>
|
||||
<ChevronRightIcon :size="24" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border border-border-secondary px-6 py-4">
|
||||
<h3 class="m-0 mr-4 flex-1 overflow-hidden text-base font-semibold text-ellipsis text-main">
|
||||
{{ currentPreviewImage?.intro }}
|
||||
</h3>
|
||||
<div class="mr-4 text-sm font-semibold whitespace-nowrap text-main">
|
||||
{{ gallerySliderControl.index + 1 }} / {{ filterList.length }}
|
||||
</div>
|
||||
<div class="text-center text-xs font-medium text-main">
|
||||
{{ t('pages.gallery.previewHelp') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<ImagePreview
|
||||
v-model:gallery-slider-control="gallerySliderControl"
|
||||
:filter-list="filterList"
|
||||
:is-always-force-reload="isAlwaysForceReload"
|
||||
/>
|
||||
|
||||
<!-- Edit URL Modal -->
|
||||
<transition name="modal">
|
||||
@@ -410,7 +329,7 @@
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="mb-6 last:mb-0">
|
||||
<label class="form-label">
|
||||
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-main">
|
||||
{{ t('pages.gallery.regexPattern', { matched: matchedCount || 0 }) }}
|
||||
</label>
|
||||
<input
|
||||
@@ -441,7 +360,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-6 last:mb-0">
|
||||
<label class="form-label">
|
||||
<label class="mb-2 flex items-center gap-2 text-sm font-medium text-main">
|
||||
{{ t('pages.gallery.replacedWith') }}
|
||||
<button
|
||||
class="flex h-[20px] w-[20px] cursor-pointer items-center justify-around rounded-full border-none bg-accent text-white transition-all duration-fast ease-apple hover:bg-accent-hover"
|
||||
@@ -456,60 +375,7 @@
|
||||
<!-- Format Info Panel -->
|
||||
<div v-if="showFormatInfo" class="mb-6 last:mb-0">
|
||||
<label>{{ t('pages.settings.upload.availablePlaceholders') }}</label>
|
||||
<div
|
||||
class="mt-3 max-h-[400px] overflow-y-auto rounded-lg border border-border bg-bg-tertiary p-0 shadow-sm"
|
||||
>
|
||||
<div class="border-b border-b-border last:border-b-0">
|
||||
<div class="category-title">
|
||||
{{ t('pages.settings.upload.placeholder.categoryTime') }}
|
||||
</div>
|
||||
<div class="placeholder-grid">
|
||||
<div
|
||||
v-for="item in advancedRenameList.categoryTime"
|
||||
:key="item.value"
|
||||
class="placeholder-item"
|
||||
@click="copyPlaceholder(item.value)"
|
||||
>
|
||||
<code>{{ item.value }}</code>
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-category">
|
||||
<div class="category-title">
|
||||
{{ t('pages.settings.upload.placeholder.categoryHash') }}
|
||||
</div>
|
||||
<div class="placeholder-grid">
|
||||
<div
|
||||
v-for="item in advancedRenameList.categoryHash"
|
||||
:key="item.value"
|
||||
class="placeholder-item"
|
||||
@click="copyPlaceholder(item.value)"
|
||||
>
|
||||
<code>{{ item.value }}</code>
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="placeholder-category">
|
||||
<div class="category-title">
|
||||
{{ t('pages.settings.upload.placeholder.categoryFile') }}
|
||||
</div>
|
||||
<div class="placeholder-grid">
|
||||
<div
|
||||
v-for="item in advancedRenameList.categoryFile"
|
||||
:key="item.value"
|
||||
class="placeholder-item"
|
||||
@click="copyPlaceholder(item.value)"
|
||||
>
|
||||
<code>{{ item.value }}</code>
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PlaceholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
@@ -526,8 +392,6 @@ import { useStorage } from '@vueuse/core'
|
||||
import {
|
||||
CheckSquareIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
ClipboardIcon,
|
||||
EditIcon,
|
||||
@@ -560,7 +424,9 @@ import ALLApi from '@/apis/allApi'
|
||||
import CustomButton from '@/components/common/CustomButton.vue'
|
||||
import CustomModal from '@/components/common/CustomModal.vue'
|
||||
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||
import PlaceholderTable from '@/components/common/PlaceholderTable.vue'
|
||||
import SingleSelect from '@/components/common/SingleSelect.vue'
|
||||
import ImagePreview from '@/components/ImagePreview.vue'
|
||||
import VirtualScroller from '@/components/VirtualScroller.vue'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
import { usePicBed } from '@/hooks/useGlobal'
|
||||
@@ -586,7 +452,6 @@ const { picBedG } = usePicBed()
|
||||
|
||||
const images = ref<ImgInfo[]>([])
|
||||
const virtualScrollerRef = useTemplateRef('virtualScrollerRef')
|
||||
const previewImageRef = useTemplateRef('previewImageRef')
|
||||
const dialogVisible = ref(false)
|
||||
const imgInfo = reactive({
|
||||
id: '',
|
||||
@@ -630,22 +495,15 @@ const userGridColumns = useStorage<number>('galleryGridColumns', 4)
|
||||
const imageLoadStates = reactive<Record<string, boolean>>({})
|
||||
const imageErrorStates = reactive<Record<string, boolean>>({})
|
||||
|
||||
const imagePreviewState = reactive({
|
||||
scale: 1,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startTranslateX: 0,
|
||||
startTranslateY: 0,
|
||||
isSwipeMode: false,
|
||||
swipeStartX: 0,
|
||||
swipeThreshold: 100,
|
||||
})
|
||||
|
||||
const pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
||||
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
||||
|
||||
const advancedRenameTitleList = computed(() => ({
|
||||
categoryTime: t('pages.settings.upload.placeholder.categoryTime'),
|
||||
categoryHash: t('pages.settings.upload.placeholder.categoryHash'),
|
||||
categoryFile: t('pages.settings.upload.placeholder.categoryFile'),
|
||||
}))
|
||||
|
||||
const advancedRenameList = {
|
||||
categoryTime: [
|
||||
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
|
||||
@@ -711,45 +569,6 @@ const selectedCount = computed(() => {
|
||||
return Object.values(choosedList).filter(v => v).length
|
||||
})
|
||||
|
||||
const currentPreviewImage = computed(() => {
|
||||
const item = filterList.value[gallerySliderControl.index]
|
||||
if (!item) return null
|
||||
const cacheBustedItem = { ...item }
|
||||
if (isAlwaysForceReload.value) {
|
||||
if (cacheBustedItem.imgUrl) {
|
||||
cacheBustedItem.imgUrl = addCacheBustParam(cacheBustedItem.imgUrl)
|
||||
}
|
||||
if (cacheBustedItem.galleryPath) {
|
||||
cacheBustedItem.galleryPath = addCacheBustParam(cacheBustedItem.galleryPath)
|
||||
}
|
||||
}
|
||||
const src = cacheBustedItem.src || cacheBustedItem.galleryPath || cacheBustedItem.imgUrl || ''
|
||||
cacheBustedItem.src = isAlwaysForceReload.value ? addCacheBustParam(src) : src
|
||||
return cacheBustedItem
|
||||
})
|
||||
|
||||
const imageTransformStyle = computed(() => {
|
||||
// Check if image overflows the viewport
|
||||
const imageElement = previewImageRef.value
|
||||
let isDraggable = false
|
||||
|
||||
if (imageElement && imageElement.naturalWidth && imageElement.naturalHeight) {
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (viewerElement) {
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const currentImageWidth = imageElement.naturalWidth * imagePreviewState.scale
|
||||
const currentImageHeight = imageElement.naturalHeight * imagePreviewState.scale
|
||||
isDraggable = currentImageWidth > viewerRect.width + 1 || currentImageHeight > viewerRect.height + 1
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `translate(${imagePreviewState.translateX}px, ${imagePreviewState.translateY}px) scale(${imagePreviewState.scale})`,
|
||||
cursor: imagePreviewState.isDragging ? 'grabbing' : isDraggable ? 'grab' : 'default',
|
||||
transition: 'none',
|
||||
}
|
||||
})
|
||||
|
||||
const dateRange = computed({
|
||||
get: () => {
|
||||
if (dateRangeStart.value && dateRangeEnd.value) {
|
||||
@@ -812,11 +631,6 @@ watch(searchTextURL, newVal => {
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function copyPlaceholder(placeholder: string) {
|
||||
window.electron.clipboard.writeText(String(placeholder))
|
||||
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
|
||||
}
|
||||
|
||||
function onImageLoad(id: string) {
|
||||
imageLoadStates[id] = true
|
||||
}
|
||||
@@ -826,224 +640,6 @@ function onImageError(id: string) {
|
||||
imageErrorStates[id] = true
|
||||
}
|
||||
|
||||
function onPreviewImageLoad() {
|
||||
nextTick(() => {
|
||||
resetImageTransform()
|
||||
})
|
||||
}
|
||||
|
||||
function navigateImage(direction: number) {
|
||||
const newIndex = gallerySliderControl.index + direction
|
||||
if (newIndex >= 0 && newIndex < filterList.value.length) {
|
||||
gallerySliderControl.index = newIndex
|
||||
resetImageTransform()
|
||||
}
|
||||
}
|
||||
|
||||
function resetImageTransform() {
|
||||
const optimalScale = calculateOptimalScale()
|
||||
imagePreviewState.scale = optimalScale
|
||||
imagePreviewState.translateX = 0
|
||||
imagePreviewState.translateY = 0
|
||||
imagePreviewState.isDragging = false
|
||||
}
|
||||
|
||||
function calculateOptimalScale(): number {
|
||||
const imageElement = previewImageRef.value
|
||||
if (!imageElement) {
|
||||
return 1
|
||||
}
|
||||
if (!imageElement.naturalWidth || !imageElement.naturalHeight) {
|
||||
return 1
|
||||
}
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (!viewerElement) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const viewerWidth = viewerRect.width
|
||||
const viewerHeight = viewerRect.height
|
||||
|
||||
const imageWidth = imageElement.naturalWidth
|
||||
const imageHeight = imageElement.naturalHeight
|
||||
const scaleX = viewerWidth / imageWidth
|
||||
const scaleY = viewerHeight / imageHeight
|
||||
const optimalScale = Math.min(scaleX, scaleY, 1)
|
||||
|
||||
return optimalScale
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
zoomToScale(Math.min(imagePreviewState.scale * 1.2, 5))
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
const newScale = Math.max(imagePreviewState.scale / 1.2, 0.1)
|
||||
zoomToScale(newScale)
|
||||
}
|
||||
|
||||
function zoomToScale(newScale: number) {
|
||||
const oldScale = imagePreviewState.scale
|
||||
imagePreviewState.scale = newScale
|
||||
|
||||
const optimalScale = calculateOptimalScale()
|
||||
if (newScale <= optimalScale) {
|
||||
imagePreviewState.translateX = 0
|
||||
imagePreviewState.translateY = 0
|
||||
} else {
|
||||
const scaleDiff = newScale / oldScale
|
||||
imagePreviewState.translateX *= scaleDiff
|
||||
imagePreviewState.translateY *= scaleDiff
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageWheel(event: WheelEvent) {
|
||||
event.preventDefault()
|
||||
const delta = event.deltaY > 0 ? -1 : 1
|
||||
const zoomFactor = 1.1
|
||||
const newScale =
|
||||
delta > 0 ? Math.min(imagePreviewState.scale * zoomFactor, 5) : Math.max(imagePreviewState.scale / zoomFactor, 0.1)
|
||||
|
||||
zoomToScale(newScale)
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault()
|
||||
navigateImage(-1)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
event.preventDefault()
|
||||
navigateImage(1)
|
||||
break
|
||||
case 'Escape':
|
||||
event.preventDefault()
|
||||
handleClose()
|
||||
break
|
||||
case '=':
|
||||
case '+':
|
||||
event.preventDefault()
|
||||
zoomIn()
|
||||
break
|
||||
case '-':
|
||||
event.preventDefault()
|
||||
zoomOut()
|
||||
break
|
||||
case '0':
|
||||
event.preventDefault()
|
||||
resetImageTransform()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageMouseDown(event: MouseEvent) {
|
||||
const imageElement = previewImageRef.value
|
||||
let isImageLargerThanViewer = false
|
||||
|
||||
if (imageElement && imageElement.naturalWidth && imageElement.naturalHeight) {
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (viewerElement) {
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const currentImageWidth = imageElement.naturalWidth * imagePreviewState.scale
|
||||
const currentImageHeight = imageElement.naturalHeight * imagePreviewState.scale
|
||||
isImageLargerThanViewer = currentImageWidth > viewerRect.width + 1 || currentImageHeight > viewerRect.height + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!isImageLargerThanViewer) {
|
||||
imagePreviewState.isSwipeMode = true
|
||||
imagePreviewState.swipeStartX = event.clientX
|
||||
} else {
|
||||
imagePreviewState.isDragging = true
|
||||
imagePreviewState.startX = event.clientX
|
||||
imagePreviewState.startY = event.clientY
|
||||
imagePreviewState.startTranslateX = imagePreviewState.translateX
|
||||
imagePreviewState.startTranslateY = imagePreviewState.translateY
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleImageMouseMove(event: MouseEvent) {
|
||||
if (imagePreviewState.isDragging) {
|
||||
const deltaX = event.clientX - imagePreviewState.startX
|
||||
const deltaY = event.clientY - imagePreviewState.startY
|
||||
imagePreviewState.translateX = imagePreviewState.startTranslateX + deltaX
|
||||
imagePreviewState.translateY = imagePreviewState.startTranslateY + deltaY
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageMouseUp(event: MouseEvent) {
|
||||
if (imagePreviewState.isSwipeMode) {
|
||||
const deltaX = event.clientX - imagePreviewState.swipeStartX
|
||||
if (Math.abs(deltaX) > imagePreviewState.swipeThreshold) {
|
||||
if (deltaX > 0) {
|
||||
navigateImage(-1)
|
||||
} else {
|
||||
navigateImage(1)
|
||||
}
|
||||
}
|
||||
imagePreviewState.isSwipeMode = false
|
||||
}
|
||||
imagePreviewState.isDragging = false
|
||||
}
|
||||
|
||||
function handleImageTouchStart(event: TouchEvent) {
|
||||
const touch = event.touches[0]
|
||||
const imageElement = previewImageRef.value
|
||||
let isImageLargerThanViewer = false
|
||||
|
||||
if (imageElement && imageElement.naturalWidth && imageElement.naturalHeight) {
|
||||
const viewerElement = imageElement.parentElement
|
||||
if (viewerElement) {
|
||||
const viewerRect = viewerElement.getBoundingClientRect()
|
||||
const currentImageWidth = imageElement.naturalWidth * imagePreviewState.scale
|
||||
const currentImageHeight = imageElement.naturalHeight * imagePreviewState.scale
|
||||
isImageLargerThanViewer = currentImageWidth > viewerRect.width + 1 || currentImageHeight > viewerRect.height + 1
|
||||
}
|
||||
}
|
||||
|
||||
if (!isImageLargerThanViewer) {
|
||||
imagePreviewState.isSwipeMode = true
|
||||
imagePreviewState.swipeStartX = touch.clientX
|
||||
} else {
|
||||
imagePreviewState.isDragging = true
|
||||
imagePreviewState.startX = touch.clientX
|
||||
imagePreviewState.startY = touch.clientY
|
||||
imagePreviewState.startTranslateX = imagePreviewState.translateX
|
||||
imagePreviewState.startTranslateY = imagePreviewState.translateY
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleImageTouchMove(event: TouchEvent) {
|
||||
if (imagePreviewState.isDragging) {
|
||||
const touch = event.touches[0]
|
||||
const deltaX = touch.clientX - imagePreviewState.startX
|
||||
const deltaY = touch.clientY - imagePreviewState.startY
|
||||
imagePreviewState.translateX = imagePreviewState.startTranslateX + deltaX
|
||||
imagePreviewState.translateY = imagePreviewState.startTranslateY + deltaY
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
function handleImageTouchEnd(event: TouchEvent) {
|
||||
if (imagePreviewState.isSwipeMode && event.changedTouches.length > 0) {
|
||||
const touch = event.changedTouches[0]
|
||||
const deltaX = touch.clientX - imagePreviewState.swipeStartX
|
||||
if (Math.abs(deltaX) > imagePreviewState.swipeThreshold) {
|
||||
if (deltaX > 0) {
|
||||
navigateImage(-1)
|
||||
} else {
|
||||
navigateImage(1)
|
||||
}
|
||||
}
|
||||
imagePreviewState.isSwipeMode = false
|
||||
}
|
||||
imagePreviewState.isDragging = false
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid'
|
||||
}
|
||||
@@ -1214,20 +810,6 @@ function clearChoosedList() {
|
||||
function zoomImage(index: number) {
|
||||
gallerySliderControl.index = index
|
||||
gallerySliderControl.visible = true
|
||||
resetImageTransform()
|
||||
|
||||
nextTick(() => {
|
||||
const modal = document.querySelector('.image-preview-modal') as HTMLElement
|
||||
if (modal) {
|
||||
modal.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
gallerySliderControl.index = 0
|
||||
gallerySliderControl.visible = false
|
||||
resetImageTransform()
|
||||
}
|
||||
|
||||
async function copy(item: ImgInfo) {
|
||||
|
||||
@@ -314,6 +314,7 @@
|
||||
<CustomSwitch
|
||||
v-model="formOfSetting.autoImport"
|
||||
small
|
||||
no-border
|
||||
:title="t('pages.settings.upload.autoImportInManage')"
|
||||
:description="t('pages.settings.upload.autoImportInManageHint')"
|
||||
/>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
>
|
||||
<div class="flex max-w-[calc(100%-300px)] flex-1 flex-wrap items-center gap-2 max-md:order-1">
|
||||
<button
|
||||
class="provider-button group/provider flex w-auto min-w-[150px] shrink-0 cursor-pointer items-center gap-3 rounded-lg border border-border-secondary bg-bg-secondary px-4 py-2 font-[inherit] duration-fast ease-standard hover:-translate-y-px hover:border-accent-hover/70 hover:bg-surface hover:shadow-sm focus-visible:focus-ring max-xs:w-full max-xs:min-w-[100px]"
|
||||
class="provider-button group/provider flex w-auto min-w-[150px] shrink-0 cursor-pointer items-center gap-3 rounded-lg bg-bg-secondary px-4 py-2 font-[inherit] shadow-sm duration-fast ease-standard hover:-translate-y-px hover:border-accent-hover/70 hover:bg-surface hover:shadow-sm focus-visible:focus-ring max-xs:w-full max-xs:min-w-[100px]"
|
||||
:title="t('pages.upload.uploadViewHint')"
|
||||
@click="handlePicBedNameClick(picBedName)"
|
||||
>
|
||||
@@ -47,7 +47,7 @@
|
||||
<button
|
||||
v-for="picbedType in favoritePicbeds"
|
||||
:key="picbedType.id"
|
||||
class="group/badge relative flex w-[85px] shrink-0 cursor-pointer items-center gap-2 overflow-hidden rounded-md border border-border-secondary bg-bg-secondary pt-1.5 pr-2 pb-1.5 pl-3 text-xs font-medium whitespace-nowrap text-secondary transition-all duration-fast ease-standard select-none hover:-translate-y-px hover:border-accent-hover hover:bg-bg-tertiary hover:text-accent-hover [.is-active]:border-[0.1rem] [.is-active]:border-accent-hover [.is-active]:font-semibold [.show-delete]:pr-2"
|
||||
class="group/badge relative flex w-[85px] shrink-0 cursor-pointer items-center gap-2 overflow-hidden rounded-md bg-bg-secondary pt-1.5 pr-2 pb-1.5 pl-3 text-xs font-medium whitespace-nowrap text-secondary shadow-sm transition-all duration-fast ease-standard select-none hover:-translate-y-px hover:border-accent-hover hover:bg-bg-tertiary hover:text-accent-hover [.is-active]:border-[0.1rem] [.is-active]:border-accent-hover [.is-active]:font-semibold [.show-delete]:pr-2"
|
||||
:class="{ 'is-active': isCurrentPicbed(picbedType), 'show-delete': longPressedBadge === picbedType.id }"
|
||||
:title="t('pages.upload.longPressToRemoveFromFavorites') + getPicbedName(picbedType)"
|
||||
@click="handleBadgeClick(picbedType)"
|
||||
@@ -82,7 +82,7 @@
|
||||
</transition-group>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3 max-md:order-2 max-md:justify-stretch">
|
||||
<div class="inline-flex overflow-hidden rounded-md border border-border-secondary bg-bg-secondary">
|
||||
<div class="inline-flex overflow-hidden rounded-md bg-bg-secondary shadow-sm">
|
||||
<button
|
||||
class="segmented-button"
|
||||
:title="t('pages.upload.imageProcessNameSingle')"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
@apply flex flex-col gap-3 flex-1 min-w-[140px];
|
||||
@apply flex flex-col gap-1 flex-1 min-w-[140px];
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
@@ -61,32 +61,6 @@
|
||||
@apply [.prev]:left-4 [.next]:right-4;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
@apply fixed inset-0 z-1000 flex justify-center items-center bg-[rgb(0_0_0_/50%)];
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
@apply overflow-auto rounded-xl w-[90%] max-w-[700px] max-h-[90vh] bg-bg-tertiary shadow-lg;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
@apply flex justify-between items-center border-b border-border-secondary p-6;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@apply flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-border bg-surface-elevated text-secondary transition-all duration-fast ease-apple;
|
||||
@apply hover:scale-105 hover:border-danger hover:bg-danger hover:text-white focus-visible:focus-ring;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@apply flex justify-end border-t border-border-secondary p-6 gap-3;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@apply flex items-center mb-2 text-sm font-medium text-main gap-2;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@apply border border-border rounded-md p-3 w-full text-sm text-main bg-bg-secondary transition-all duration-fast ease-apple box-border;
|
||||
@apply focus:border-accent focus:outline-none focus:shadow-md;
|
||||
|
||||
Reference in New Issue
Block a user