mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-07 06:22:46 +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
|
- 现在支持手动关闭 GPU 加速,解决部分硬件兼容性导致的黑屏或闪烁问题。
|
||||||
- 新增自定义主题功能,主题仓库[PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub)
|
- 新增高级动画设置,开启后可获得更佳的 UI 交互体验。
|
||||||
- 12个内置主题供选择,如bilibili、二次元、极夜紫等风格
|
- Windows 便携模式,无需安装运行,数据存储在程序目录下的 `data` 文件夹中,且支持自动更新。
|
||||||
- 重新设计了管理功能的全部页面
|
- Linux 新增 `rpm` 安装包。
|
||||||
- 重构了几乎全部页面,优化了数十项UI细节问题,整体风格更加统一
|
- 新增图床编辑卡片页面,解决多配置切换时的混乱问题。
|
||||||
- 相册页面多项优化,支持显示已选择图片数量,匹配的url列表和记忆过滤器打开状态
|
- 文件浏览页面新增列表模式支持。
|
||||||
- 优化了管理文件浏览页面侧边栏名字的显示,现在在超出宽度时会滚动显示完整名称
|
|
||||||
- 插件页面现在可以浏览所有插件列表,查看详情和安装
|
|
||||||
- 新增教学引导页面,首次运行时会自动弹出
|
|
||||||
|
|
||||||
- 其它
|
#### 🎨 UI 界面
|
||||||
- 原管理功能重命名为`云端`,更符合实际功能
|
|
||||||
- 现在重置图床后不再自动返回上一页面
|
- 接入主题仓库 [PicList ThemeHub](https://github.com/Kuingsmile/PicList-ThemeHub),支持自定义下载。
|
||||||
|
- 提供 12 个内置主题(如 bilibili、二次元、极夜紫等风格)。
|
||||||
|
- 重新设计了管理功能的全部页面,重构了几乎所有业务页面,优化数十项 UI 细节。
|
||||||
|
- 优化相册页面卡片样式,边界更清晰,提升选择框视觉效果。
|
||||||
|
- 优化多个页面在窄屏下的显示,避免内容溢出。
|
||||||
|
- 文件浏览侧边栏名称超出宽度时支持滚动显示完整名称。
|
||||||
|
- 相册页面支持显示已选数量、匹配的 URL 列表和记忆过滤器状态。
|
||||||
|
- 支持浏览完整插件列表、查看详情及一键安装。
|
||||||
|
- 新增新手引导页面,首次运行自动弹出。
|
||||||
|
|
||||||
|
#### 📝 其它
|
||||||
|
|
||||||
|
- 原“管理”功能重命名为 **“云端”**,更符合实际功能定义。
|
||||||
|
- 重置图床后不再自动返回上一页面。
|
||||||
|
|
||||||
### 🐛 问题修复
|
### 🐛 问题修复
|
||||||
|
|
||||||
- 修复了管理页面中排序下拉框显示异常的问题
|
- 修复了管理页面中排序下拉框显示异常的问题。
|
||||||
- 修复了管理页面图床列表没有正确为当前选中图床高亮的问题
|
- 修复了管理页面图床列表未正确高亮当前选中项的问题。
|
||||||
- 修复了暗色模式下任务页面的显示问题
|
- 修复了暗色模式下任务页面的显示异常。
|
||||||
- 修复了图床设置页面设置为默认图床按钮状态没有及时更新的问题
|
- 修复了图床设置页面“设置为默认图床”按钮状态更新不及时的问题。
|
||||||
- 修复了预处理设置页面,图床水印独立设置的按钮状态没有及时更新的问题
|
- 修复了预处理设置页面中,图床水印独立设置按钮状态不同步的问题。
|
||||||
- 修复了部分页面底部元素被遮挡的问题
|
- 修复了部分页面底部元素被遮挡的问题。
|
||||||
|
|||||||
@@ -2,24 +2,42 @@
|
|||||||
|
|
||||||
### 🚀 Performance Optimization
|
### 🚀 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
|
### ✨ 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
|
#### ⚙️ Core Features
|
||||||
- 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
|
- Now supports manually disabling GPU acceleration to resolve black screen or flickering issues caused by some hardware compatibility.
|
||||||
- Refactored almost all pages, optimizing dozens of UI detail issues
|
- Added advanced animation settings for a better UI interaction experience.
|
||||||
- Multiple optimizations on the album page, supporting display of the number of selected images, matching URL list, and remembering filter open state
|
- Windows portable mode, no installation required, data is stored in the `data` folder in the program directory, and supports automatic updates.
|
||||||
- Plugin page now allows browsing of all plugin lists, viewing details, and installation
|
- Added `rpm` installation package for Linux.
|
||||||
- Added tutorial guide page, which automatically pops up on first run
|
- Added image hosting editing card page to solve confusion when switching multiple configurations.
|
||||||
- Now supports disabling GPU acceleration to resolve some compatibility issues
|
- Added list mode support for the file browsing page.
|
||||||
- Added advanced animation settings for a better UI experience
|
|
||||||
- Optimized the loading speed of multiple pages and browsing performance
|
#### 🎨 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
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
- Fixed an issue where the sort dropdown in the management page displayed abnormally
|
- Fixed the issue of abnormal display of the sorting dropdown box on the management page.
|
||||||
- Fixed an issue where the task page displayed incorrectly in dark mode
|
- Fixed the issue where the image hosting list on the management page did not correctly highlight the currently selected item.
|
||||||
- Fixed an issue where the default image bed button status on the image bed settings page did not update in time
|
- Fixed the display issue of the task page in dark mode.
|
||||||
- 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 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'),
|
label: $t('HIDE_MINI_WINDOW'),
|
||||||
click() {
|
click() {
|
||||||
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)
|
const miniWindow = windowManager.get(IWindowList.MINI_WINDOW)
|
||||||
console.log('hide mini window', miniWindow)
|
|
||||||
miniWindow?.close()
|
miniWindow?.close()
|
||||||
console.log('mini window closed')
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-container">
|
<div class="relative flex h-full w-full items-center justify-center p-0">
|
||||||
<div v-if="isLoading" class="loading-placeholder">
|
<div v-if="isLoading" class="flex h-full w-full items-center justify-center">
|
||||||
<div class="loading-spinner" />
|
<div class="h-[34px] w-[34px] animate-spin rounded-full border-3 border-t-3 border-border border-t-accent" />
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else-if="!hasError"
|
v-else-if="!hasError"
|
||||||
:src="isShowThumbnail && item.isImage ? base64Image : `./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
|
:src="isShowThumbnail && item.isImage ? base64Image : `./assets/icons/${getFileIconPath(item.fileName ?? '')}`"
|
||||||
alt=""
|
alt=""
|
||||||
class="image"
|
class="h-full w-full object-contain"
|
||||||
@load="handleImageLoad"
|
@load="handleImageLoad"
|
||||||
@error="handleImageError"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -60,49 +65,3 @@ onBeforeMount(async () => {
|
|||||||
await createBase64Image()
|
await createBase64Image()
|
||||||
})
|
})
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<div class="image-container">
|
<div class="relative flex h-full w-full items-center justify-center p-0">
|
||||||
<div v-if="isLoading" class="loading-placeholder">
|
<div v-if="isLoading" class="flex h-full w-full items-center justify-center">
|
||||||
<div class="loading-spinner" />
|
<div class="h-[34px] w-[34px] animate-spin rounded-full border-3 border-t-3 border-border border-t-accent" />
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else-if="!hasError"
|
v-else-if="!hasError"
|
||||||
:src="imageSource"
|
:src="imageSource"
|
||||||
alt=""
|
alt=""
|
||||||
class="image"
|
class="h-full w-full object-contain"
|
||||||
@load="handleImageLoad"
|
@load="handleImageLoad"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<img v-else :src="iconPath" alt="" class="image" />
|
<img v-else :src="iconPath" alt="" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,49 +76,3 @@ watch(() => [props.url, props.item], getUrl, { deep: true })
|
|||||||
|
|
||||||
onMounted(getUrl)
|
onMounted(getUrl)
|
||||||
</script>
|
</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">
|
<div class="form-group">
|
||||||
<label class="title-text">{{ $t('pages.settings.upload.availablePlaceholders') }}</label>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1014,7 +1014,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import customRadioOption from '@/components/common/CustomRadioOption.vue'
|
import customRadioOption from '@/components/common/CustomRadioOption.vue'
|
||||||
import customRange from '@/components/common/CustomRange.vue'
|
import customRange from '@/components/common/CustomRange.vue'
|
||||||
import customSwitch from '@/components/common/CustomSwitch.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 PerPicbedSetting from '@/components/PerPicbedSetting.vue'
|
||||||
import { getRawData } from '@/utils/common'
|
import { getRawData } from '@/utils/common'
|
||||||
import { configPaths } from '@/utils/configPaths'
|
import { configPaths } from '@/utils/configPaths'
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="image-container">
|
<div class="relative flex h-full w-full items-center justify-center p-0">
|
||||||
<div v-if="isLoading" class="loading-placeholder">
|
<div v-if="isLoading" class="flex h-full w-full items-center justify-center">
|
||||||
<div class="loading-spinner" />
|
<div class="h-[34px] w-[34px] animate-spin rounded-full border-3 border-t-3 border-border border-t-accent" />
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-else-if="!hasError"
|
v-else-if="!hasError"
|
||||||
:src="imageSource"
|
:src="imageSource"
|
||||||
alt=""
|
alt=""
|
||||||
class="image"
|
class="h-full w-full object-contain"
|
||||||
@load="handleImageLoad"
|
@load="handleImageLoad"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<img v-else :src="iconPath" alt="" class="image" />
|
<img v-else :src="iconPath" alt="" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -104,49 +104,3 @@ watch(() => [props.url, props.item], fetchImage, { deep: true })
|
|||||||
|
|
||||||
onMounted(fetchImage)
|
onMounted(fetchImage)
|
||||||
</script>
|
</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>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-show="dropDownOpen"
|
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
|
<label
|
||||||
v-for="item in allList"
|
v-for="item in allList"
|
||||||
|
|||||||
@@ -542,7 +542,7 @@
|
|||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"savedConfigs": "Enter Saved Cloud",
|
"savedConfigs": "Saved Cloud",
|
||||||
"selectPlaceholder": "Please select",
|
"selectPlaceholder": "Please select",
|
||||||
"tips": "Tips",
|
"tips": "Tips",
|
||||||
"title": "Cloud Management",
|
"title": "Cloud Management",
|
||||||
|
|||||||
@@ -542,7 +542,7 @@
|
|||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"reset": "重置",
|
"reset": "重置",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"savedConfigs": "进入已配置云端",
|
"savedConfigs": "已配置云端",
|
||||||
"selectPlaceholder": "请选择",
|
"selectPlaceholder": "请选择",
|
||||||
"tips": "提示",
|
"tips": "提示",
|
||||||
"title": "云端管理",
|
"title": "云端管理",
|
||||||
|
|||||||
@@ -542,7 +542,7 @@
|
|||||||
"refresh": "重新整理",
|
"refresh": "重新整理",
|
||||||
"reset": "重設",
|
"reset": "重設",
|
||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
"savedConfigs": "進入已配置雲端",
|
"savedConfigs": "已配置雲端",
|
||||||
"selectPlaceholder": "請選擇",
|
"selectPlaceholder": "請選擇",
|
||||||
"tips": "提示",
|
"tips": "提示",
|
||||||
"title": "雲端存儲管理",
|
"title": "雲端存儲管理",
|
||||||
|
|||||||
@@ -178,4 +178,16 @@
|
|||||||
transform: rotate(360deg);
|
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 h-full w-full">
|
||||||
<div
|
<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' }"
|
:style="{ width: sidebarWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<div class="shrink-0 border-b-2 border-b-border-secondary p-2">
|
<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 */
|
/* Header Card */
|
||||||
.bucket-card {
|
.bucket-card {
|
||||||
@@ -197,8 +190,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.action-icon {
|
.action-icon {
|
||||||
width: 16px;
|
@apply w-[16px] h-[16px];
|
||||||
height: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
@@ -546,29 +538,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-action-button {
|
.file-action-button {
|
||||||
display: flex;
|
@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;
|
||||||
justify-content: center;
|
@apply hover:bg-accent/10 hover:text-main hover:-translate-y-px;
|
||||||
align-items: center;
|
@apply hover:[.danger]:text-error hover:[.danger]:bg-error/10;
|
||||||
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%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-checkbox {
|
.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"
|
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
|
<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">
|
<div class="flex flex-1 items-center gap-4 p-1">
|
||||||
<ImagesIcon :size="24" class="text-accent" />
|
<ImagesIcon :size="24" class="text-accent" />
|
||||||
@@ -205,21 +205,21 @@
|
|||||||
v-else
|
v-else
|
||||||
:key="componentKey"
|
:key="componentKey"
|
||||||
ref="virtualScrollerRef"
|
ref="virtualScrollerRef"
|
||||||
|
:items="filterList"
|
||||||
:view-mode="viewMode"
|
:view-mode="viewMode"
|
||||||
class="virtual-gallery-scroller min-h-0 w-full flex-1 p-3"
|
class="virtual-gallery-scroller min-h-0 w-full flex-1 p-3"
|
||||||
:items="filterList"
|
|
||||||
:item-height="300"
|
:item-height="300"
|
||||||
:grid-breakpoints="effectiveGridBreakpoints"
|
:grid-breakpoints="effectiveGridBreakpoints"
|
||||||
key-field="key"
|
key-field="key"
|
||||||
>
|
>
|
||||||
<template #default="{ item, index }">
|
<template #default="{ item, index }">
|
||||||
<div
|
<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 || ''] }"
|
:class="{ selected: choosedList[item.id || ''] }"
|
||||||
@click="handleChooseImage(!choosedList[item.id || ''], index)"
|
@click="handleChooseImage(!choosedList[item.id || ''], index)"
|
||||||
>
|
>
|
||||||
<div
|
<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)"
|
@click.stop="zoomImage(index)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -245,16 +245,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<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 || '' : ''"
|
:title="(item.fileName || '').toString().length > 30 ? item.fileName || '' : ''"
|
||||||
>
|
>
|
||||||
{{ formatFileName(item.fileName || '') }}
|
<div class="text-center">{{ formatFileName(item.fileName || '') }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="mr-2 flex items-center justify-between">
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-1 justify-center gap-2">
|
||||||
<button :title="t('pages.gallery.copy')" class="icon-button copy-icon" @click.stop="copy(item)">
|
<button :title="t('pages.gallery.copy')" class="icon-button copy-icon" @click.stop="copy(item)">
|
||||||
<ClipboardIcon :size="16" />
|
<ClipboardIcon :size="16" />
|
||||||
</button>
|
</button>
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
@change="e => handleChooseImage((e.target as HTMLInputElement).checked, index)"
|
@change="e => handleChooseImage((e.target as HTMLInputElement).checked, index)"
|
||||||
/>
|
/>
|
||||||
<span
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,92 +293,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Custom Image Preview Modal -->
|
<!-- Custom Image Preview Modal -->
|
||||||
<transition name="modal">
|
<ImagePreview
|
||||||
<div
|
v-model:gallery-slider-control="gallerySliderControl"
|
||||||
v-if="gallerySliderControl.visible"
|
:filter-list="filterList"
|
||||||
class="image-preview-modal fixed inset-0 z-1000 flex items-center justify-center outline-none"
|
:is-always-force-reload="isAlwaysForceReload"
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- Edit URL Modal -->
|
<!-- Edit URL Modal -->
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
@@ -410,7 +329,7 @@
|
|||||||
>
|
>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div class="mb-6 last:mb-0">
|
<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 }) }}
|
{{ t('pages.gallery.regexPattern', { matched: matchedCount || 0 }) }}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -441,7 +360,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6 last:mb-0">
|
<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') }}
|
{{ t('pages.gallery.replacedWith') }}
|
||||||
<button
|
<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"
|
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 -->
|
<!-- Format Info Panel -->
|
||||||
<div v-if="showFormatInfo" class="mb-6 last:mb-0">
|
<div v-if="showFormatInfo" class="mb-6 last:mb-0">
|
||||||
<label>{{ t('pages.settings.upload.availablePlaceholders') }}</label>
|
<label>{{ t('pages.settings.upload.availablePlaceholders') }}</label>
|
||||||
<div
|
<PlaceholderTable :list="advancedRenameList" :title-list="advancedRenameTitleList" />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -526,8 +392,6 @@ import { useStorage } from '@vueuse/core'
|
|||||||
import {
|
import {
|
||||||
CheckSquareIcon,
|
CheckSquareIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
ChevronUpIcon,
|
ChevronUpIcon,
|
||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
@@ -560,7 +424,9 @@ import ALLApi from '@/apis/allApi'
|
|||||||
import CustomButton from '@/components/common/CustomButton.vue'
|
import CustomButton from '@/components/common/CustomButton.vue'
|
||||||
import CustomModal from '@/components/common/CustomModal.vue'
|
import CustomModal from '@/components/common/CustomModal.vue'
|
||||||
import MultiSelect from '@/components/common/MultiSelect.vue'
|
import MultiSelect from '@/components/common/MultiSelect.vue'
|
||||||
|
import PlaceholderTable from '@/components/common/PlaceholderTable.vue'
|
||||||
import SingleSelect from '@/components/common/SingleSelect.vue'
|
import SingleSelect from '@/components/common/SingleSelect.vue'
|
||||||
|
import ImagePreview from '@/components/ImagePreview.vue'
|
||||||
import VirtualScroller from '@/components/VirtualScroller.vue'
|
import VirtualScroller from '@/components/VirtualScroller.vue'
|
||||||
import useConfirm from '@/hooks/useConfirm'
|
import useConfirm from '@/hooks/useConfirm'
|
||||||
import { usePicBed } from '@/hooks/useGlobal'
|
import { usePicBed } from '@/hooks/useGlobal'
|
||||||
@@ -586,7 +452,6 @@ const { picBedG } = usePicBed()
|
|||||||
|
|
||||||
const images = ref<ImgInfo[]>([])
|
const images = ref<ImgInfo[]>([])
|
||||||
const virtualScrollerRef = useTemplateRef('virtualScrollerRef')
|
const virtualScrollerRef = useTemplateRef('virtualScrollerRef')
|
||||||
const previewImageRef = useTemplateRef('previewImageRef')
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const imgInfo = reactive({
|
const imgInfo = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
@@ -630,22 +495,15 @@ const userGridColumns = useStorage<number>('galleryGridColumns', 4)
|
|||||||
const imageLoadStates = reactive<Record<string, boolean>>({})
|
const imageLoadStates = reactive<Record<string, boolean>>({})
|
||||||
const imageErrorStates = 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 pasteStyleList = ['markdown', 'HTML', 'URL', 'UBB', 'Custom']
|
||||||
const shortURLList = [t('pages.gallery.shortUrl'), t('pages.gallery.longUrl')]
|
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 = {
|
const advancedRenameList = {
|
||||||
categoryTime: [
|
categoryTime: [
|
||||||
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
|
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
|
||||||
@@ -711,45 +569,6 @@ const selectedCount = computed(() => {
|
|||||||
return Object.values(choosedList).filter(v => v).length
|
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({
|
const dateRange = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
if (dateRangeStart.value && dateRangeEnd.value) {
|
if (dateRangeStart.value && dateRangeEnd.value) {
|
||||||
@@ -812,11 +631,6 @@ watch(searchTextURL, newVal => {
|
|||||||
}, 300)
|
}, 300)
|
||||||
})
|
})
|
||||||
|
|
||||||
function copyPlaceholder(placeholder: string) {
|
|
||||||
window.electron.clipboard.writeText(String(placeholder))
|
|
||||||
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImageLoad(id: string) {
|
function onImageLoad(id: string) {
|
||||||
imageLoadStates[id] = true
|
imageLoadStates[id] = true
|
||||||
}
|
}
|
||||||
@@ -826,224 +640,6 @@ function onImageError(id: string) {
|
|||||||
imageErrorStates[id] = true
|
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() {
|
function toggleViewMode() {
|
||||||
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid'
|
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid'
|
||||||
}
|
}
|
||||||
@@ -1214,20 +810,6 @@ function clearChoosedList() {
|
|||||||
function zoomImage(index: number) {
|
function zoomImage(index: number) {
|
||||||
gallerySliderControl.index = index
|
gallerySliderControl.index = index
|
||||||
gallerySliderControl.visible = true
|
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) {
|
async function copy(item: ImgInfo) {
|
||||||
|
|||||||
@@ -314,6 +314,7 @@
|
|||||||
<CustomSwitch
|
<CustomSwitch
|
||||||
v-model="formOfSetting.autoImport"
|
v-model="formOfSetting.autoImport"
|
||||||
small
|
small
|
||||||
|
no-border
|
||||||
:title="t('pages.settings.upload.autoImportInManage')"
|
:title="t('pages.settings.upload.autoImportInManage')"
|
||||||
:description="t('pages.settings.upload.autoImportInManageHint')"
|
: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">
|
<div class="flex max-w-[calc(100%-300px)] flex-1 flex-wrap items-center gap-2 max-md:order-1">
|
||||||
<button
|
<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')"
|
:title="t('pages.upload.uploadViewHint')"
|
||||||
@click="handlePicBedNameClick(picBedName)"
|
@click="handlePicBedNameClick(picBedName)"
|
||||||
>
|
>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<button
|
<button
|
||||||
v-for="picbedType in favoritePicbeds"
|
v-for="picbedType in favoritePicbeds"
|
||||||
:key="picbedType.id"
|
: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 }"
|
:class="{ 'is-active': isCurrentPicbed(picbedType), 'show-delete': longPressedBadge === picbedType.id }"
|
||||||
:title="t('pages.upload.longPressToRemoveFromFavorites') + getPicbedName(picbedType)"
|
:title="t('pages.upload.longPressToRemoveFromFavorites') + getPicbedName(picbedType)"
|
||||||
@click="handleBadgeClick(picbedType)"
|
@click="handleBadgeClick(picbedType)"
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-3 max-md:order-2 max-md:justify-stretch">
|
<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
|
<button
|
||||||
class="segmented-button"
|
class="segmented-button"
|
||||||
:title="t('pages.upload.imageProcessNameSingle')"
|
:title="t('pages.upload.imageProcessNameSingle')"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-group {
|
.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 {
|
.filter-label {
|
||||||
@@ -61,32 +61,6 @@
|
|||||||
@apply [.prev]:left-4 [.next]:right-4;
|
@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 {
|
.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 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;
|
@apply focus:border-accent focus:outline-none focus:shadow-md;
|
||||||
|
|||||||
Reference in New Issue
Block a user