Feature(custom): add per picbed rename setting

This commit is contained in:
Kuingsmile
2026-01-10 17:50:56 +08:00
parent ef7b338b88
commit b148eee606
5 changed files with 449 additions and 8 deletions

View File

@@ -939,6 +939,114 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Rename Tab -->
<div v-else-if="activeTab === 'rename'" key="rename" class="tab-content">
<div class="settings-section">
<div class="form-grid">
<div class="form-group">
<label class="switch-label">
<input v-model="autoRenameComputed" type="checkbox" class="switch-input" />
<span class="switch-slider" />
<div class="switch-content">
<span class="switch-title">{{ $t('pages.imageProcess.rename.renameTimestamp') }}</span>
<span class="switch-description">YYYYMMDDHHmmssSSS</span>
</div>
</label>
</div>
<div class="form-group">
<label class="switch-label">
<input v-model="manualRenameComputed" type="checkbox" class="switch-input" />
<span class="switch-slider" />
<div class="switch-content">
<span class="switch-title">{{ $t('pages.imageProcess.rename.manualRename') }}</span>
</div>
</label>
</div>
</div>
<div class="form-group">
<label class="switch-label">
<input v-model="renameSettingsComputed.rename.enable" type="checkbox" class="switch-input" />
<span class="switch-slider" />
<div class="switch-content">
<div class="switch-title">{{ $t('pages.settings.upload.enableAdvancedRname') }}</div>
<div class="switch-description">{{ $t('pages.settings.upload.enableAdvancedRnameDesc') }}</div>
</div>
</label>
</div>
<div class="form-group rename-format-field">
<label>
<Edit :size="14" />
{{ $t('pages.settings.upload.advancedRnameFormat') }}
</label>
<input
v-model="renameSettingsComputed.rename.format"
type="text"
class="form-input"
placeholder="Ex. {Y}-{m}-{uuid}"
/>
</div>
<div class="form-group">
<label>{{ $t('pages.settings.upload.availablePlaceholders') }}</label>
<div class="placeholder-help">
<div class="placeholder-category">
<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>
</transition> </transition>
</div> </div>
</div> </div>
@@ -947,6 +1055,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
Droplets, Droplets,
Edit,
FileText, FileText,
FlipHorizontal, FlipHorizontal,
Image, Image,
@@ -968,11 +1077,13 @@ import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toR
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import PerPicbedSetting from '@/components/PerPicbedSetting.vue' import PerPicbedSetting from '@/components/PerPicbedSetting.vue'
import useMessage from '@/hooks/useMessage'
import { getRawData } from '@/utils/common' import { getRawData } from '@/utils/common'
import { configPaths } from '@/utils/configPaths' import { configPaths } from '@/utils/configPaths'
import { getConfig, saveConfig } from '@/utils/dataSender' import { getConfig, saveConfig } from '@/utils/dataSender'
const { t } = useI18n() const { t } = useI18n()
const message = useMessage()
const activeTab = ref('general') const activeTab = ref('general')
// Tab indicator animation // Tab indicator animation
@@ -1028,6 +1139,37 @@ const tabs = computed(() => [
}, },
]) ])
const advancedRenameList = computed(() => ({
categoryTime: [
{ label: t('pages.settings.upload.placeholder.year4'), value: '{Y}' },
{ label: t('pages.settings.upload.placeholder.year2'), value: '{y}' },
{ label: t('pages.settings.upload.placeholder.month'), value: '{m}' },
{ label: t('pages.settings.upload.placeholder.date'), value: '{d}' },
{ label: t('pages.settings.upload.placeholder.hour'), value: '{h}' },
{ label: t('pages.settings.upload.placeholder.minute'), value: '{i}' },
{ label: t('pages.settings.upload.placeholder.second'), value: '{s}' },
{ label: t('pages.settings.upload.placeholder.millisecond'), value: '{ms}' },
{ label: t('pages.settings.upload.placeholder.timestamp'), value: '{timestamp}' },
],
categoryHash: [
{ label: t('pages.settings.upload.placeholder.md5'), value: '{md5}' },
{ label: t('pages.settings.upload.placeholder.md5-16'), value: '{md5-16}' },
{ label: t('pages.settings.upload.placeholder.uuid'), value: '{uuid}' },
{ label: t('pages.settings.upload.placeholder.sha256'), value: '{sha256}' },
{ label: t('pages.settings.upload.placeholder.sha256-n'), value: '{sha256-n}' },
],
categoryFile: [
{ label: t('pages.settings.upload.placeholder.filename'), value: '{filename}' },
{ label: t('pages.settings.upload.placeholder.localFolder'), value: '{localFolder:n}' },
{ label: t('pages.settings.upload.placeholder.randomString'), value: '{str-n}' },
],
}))
function copyPlaceholder(placeholder: string) {
window.electron.clipboard.writeText(placeholder)
message.success(t('pages.settings.upload.copySuccess', { content: placeholder }))
}
const waterMarkPositionMap = new Map([ const waterMarkPositionMap = new Map([
['north', t('pages.imageProcess.watermark.positionOptions.top')], ['north', t('pages.imageProcess.watermark.positionOptions.top')],
['northeast', t('pages.imageProcess.watermark.positionOptions.topRight')], ['northeast', t('pages.imageProcess.watermark.positionOptions.topRight')],
@@ -1142,6 +1284,15 @@ const defaultSkipProcessSetting = {
const skipProcessForm = ref<IBuildInSkipProcessOptions>({ const skipProcessForm = ref<IBuildInSkipProcessOptions>({
...defaultSkipProcessSetting, ...defaultSkipProcessSetting,
}) })
const globalRenameSettings = ref<{
enable?: boolean
format?: string
}>({
enable: false,
format: '{filename}',
})
const globalAutoRename = ref<Undefinable<boolean>>(false)
const globalManualRename = ref<Undefinable<boolean>>(false)
const isInitialized = ref(false) const isInitialized = ref(false)
@@ -1182,11 +1333,21 @@ let singleConfigInFile = {
id: configId || '', id: configId || '',
} as IBuildInListItem } as IBuildInListItem
let compressInFile = {} as IBuildInCompressOptions let compressInFile = {} as IBuildInCompressOptions
async function initData() { async function initData() {
// global settings // global settings
compressInFile = (await getConfig<IBuildInCompressOptions>(configPaths.buildIn.compress)) || {} compressInFile = (await getConfig<IBuildInCompressOptions>(configPaths.buildIn.compress)) || {}
const watermark = (await getConfig<IBuildInWaterMarkOptions>(configPaths.buildIn.watermark)) || {} const watermark = (await getConfig<IBuildInWaterMarkOptions>(configPaths.buildIn.watermark)) || {}
const skipProcess = (await getConfig<IBuildInSkipProcessOptions>(configPaths.buildIn.skipProcess)) || {} const skipProcess = (await getConfig<IBuildInSkipProcessOptions>(configPaths.buildIn.skipProcess)) || {}
globalRenameSettings.value = (await getConfig<{
enable?: boolean
format?: string
}>(configPaths.buildIn.rename)) || {
enable: false,
format: '{filename}',
}
globalAutoRename.value = (await getConfig<boolean>(configPaths.settings.autoRename)) ?? false
globalManualRename.value = (await getConfig<boolean>(configPaths.settings.rename)) ?? false
if (compressInFile) { if (compressInFile) {
let cleanedObj = {} let cleanedObj = {}
try { try {
@@ -1251,8 +1412,6 @@ async function initData() {
enable: false, enable: false,
format: '{filename}', format: '{filename}',
} }
const globalAutoRename = (await getConfig<boolean>(configPaths.settings.rename)) || false
const globalManualRename = (await getConfig<boolean>(configPaths.settings.autoRename)) || false
if (!buildInList) { if (!buildInList) {
saveConfig(configPaths.buildIn.list, []) saveConfig(configPaths.buildIn.list, [])
buildInList = [] buildInList = []
@@ -1319,8 +1478,8 @@ async function initData() {
...globalRenameSettings, ...globalRenameSettings,
...(singleConfigInFile.rename || {}), ...(singleConfigInFile.rename || {}),
}, },
autoRename: globalAutoRename || singleConfigInFile.autoRename || false, autoRename: singleConfigInFile.autoRename ?? (globalAutoRename.value || false),
manualRename: globalManualRename || singleConfigInFile.manualRename || false, manualRename: singleConfigInFile.manualRename ?? (globalManualRename.value || false),
} }
} }
} }
@@ -1397,6 +1556,99 @@ const activeForm = computed<any>(() => {
} }
}) })
const autoRenameComputed = computed({
get() {
return configId ? singleConfigSettings.value.autoRename : globalAutoRename.value
},
set(newValue) {
if (configId) {
singleConfigSettings.value.autoRename = newValue
const shouldUpdate = newValue !== (globalAutoRename.value ?? false)
singleConfigInFile.id = configId || ''
if (shouldUpdate) {
singleConfigInFile.autoRename = newValue
UpdateBuildInList(singleConfigInFile)
} else {
if (singleConfigInFile.autoRename !== undefined) delete singleConfigInFile.autoRename
checkIfItemOnlyId(singleConfigInFile).then(async isOnlyId => {
if (isOnlyId) {
await removeItemFromBuildInList(singleConfigInFile.id)
}
})
}
} else {
globalAutoRename.value = newValue
saveConfig(configPaths.settings.autoRename, newValue)
}
},
})
const manualRenameComputed = computed({
get() {
return configId ? singleConfigSettings.value.manualRename : globalManualRename.value
},
set(newValue) {
if (configId) {
singleConfigSettings.value.manualRename = newValue
const shouldUpdate = newValue !== (globalManualRename.value ?? false)
singleConfigInFile.id = configId || ''
if (shouldUpdate) {
singleConfigInFile.manualRename = newValue
UpdateBuildInList(singleConfigInFile)
} else {
if (singleConfigInFile.manualRename !== undefined) delete singleConfigInFile.manualRename
checkIfItemOnlyId(singleConfigInFile).then(async isOnlyId => {
if (isOnlyId) {
await removeItemFromBuildInList(singleConfigInFile.id)
}
})
}
} else {
globalManualRename.value = newValue
saveConfig(configPaths.settings.rename, newValue)
}
},
})
const renameSettingsComputed = computed<any>(() => {
if (configId) {
return {
rename: singleConfigSettings.value.rename,
}
} else {
return {
rename: globalRenameSettings.value,
}
}
})
watch(
renameSettingsComputed,
newValue => {
if (configId) {
singleConfigSettings.value.rename = newValue.rename
const shouldUpdate =
newValue.rename.enable !== (globalRenameSettings.value.enable ?? false) ||
newValue.rename.format !== (globalRenameSettings.value.format ?? '{filename}')
singleConfigInFile.id = configId || ''
if (shouldUpdate) {
singleConfigInFile.rename = newValue.rename
UpdateBuildInList(singleConfigInFile)
} else {
if (singleConfigInFile.rename) delete singleConfigInFile.rename
checkIfItemOnlyId(singleConfigInFile).then(async isOnlyId => {
if (isOnlyId) {
await removeItemFromBuildInList(singleConfigInFile.id)
}
})
}
} else {
saveConfig(configPaths.buildIn.rename, toRaw(newValue.rename))
}
},
{ deep: true },
)
const convertStr = computed({ const convertStr = computed({
get() { get() {
return configId ? singleFormatConvertObj.value : formatConvertObjStr.value return configId ? singleFormatConvertObj.value : formatConvertObjStr.value
@@ -1454,11 +1706,9 @@ async function removeItemFromBuildInList(id: string) {
} }
compressWatchKeys.forEach(key => { compressWatchKeys.forEach(key => {
console.log('setting up watch for compress key:', key)
watch( watch(
() => singleConfigSettings.value.compress![key], () => singleConfigSettings.value.compress![key],
async newValue => { async newValue => {
console.log('detected change in compress key:', key, 'new value:', newValue)
const defaultValue = defaultCompressSetting[key] const defaultValue = defaultCompressSetting[key]
const perPicBedValue = compressForm.value[`${key}Map`]?.[currentPicbedName] const perPicBedValue = compressForm.value[`${key}Map`]?.[currentPicbedName]
const inheritedValue = perPicBedValue ?? compressForm.value[key] ?? defaultValue const inheritedValue = perPicBedValue ?? compressForm.value[key] ?? defaultValue

View File

@@ -457,9 +457,8 @@
} }
.switch-description { .switch-description {
font-size: 0.8rem; font-size: 0.75rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
line-height: 1.4;
} }
/* ==================== Radio Group ==================== */ /* ==================== Radio Group ==================== */
@@ -857,3 +856,168 @@ small {
:root.auto.dark small { :root.auto.dark small {
background: var(--color-surface); background: var(--color-surface);
} }
/* Placeholder Help Styles */
.placeholder-help {
overflow-y: auto;
margin-top: 0.75rem;
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 0;
max-height: 400px;
background: var(--color-background-tertiary);
box-shadow: 0 2px 8px rgb(0 0 0 / 6%);
}
.placeholder-category {
border-bottom: 1px solid var(--color-border-light);
}
.placeholder-category:last-child {
border-bottom: none;
}
.category-title {
margin: 0;
border-bottom: 1px solid var(--color-border-light);
padding: 0.875rem 1rem 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-text-secondary);
background: linear-gradient(135deg, var(--color-background-secondary) 0%, var(--color-background-tertiary) 100%);
letter-spacing: 0.02em;
}
.placeholder-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 0;
padding: 0.5rem 0;
}
.placeholder-item {
display: flex;
align-items: center;
margin: 0;
border-radius: 0;
padding: 0.5rem 1rem;
font-size: 0.875rem;
line-height: 1.4;
cursor: pointer;
}
.placeholder-item:hover {
background: rgb(var(--color-accent-rgb), 0.08);
}
.placeholder-item code {
margin-right: 0.875rem;
border: 1px solid rgb(255 255 255 / 20%);
border-radius: 8px;
padding: 0.3rem 0.6rem;
min-width: 80px;
font-size: 1rem;
font-family: 'SF Mono', Monaco, Menlo, 'Ubuntu Mono', monospace;
font-weight: 600;
text-align: center;
color: white;
background: var(--color-blue-common);
box-shadow: 0 1px 3px rgb(0 0 0 / 12%), 0 1px 2px rgb(0 0 0 / 24%);
letter-spacing: 0.02em;
flex-shrink: 0;
}
.placeholder-item span {
font-weight: 500;
color: var(--color-text-primary);
flex: 1;
}
/* Scrollbar styling for macOS feel */
.placeholder-help::-webkit-scrollbar {
width: 6px;
}
.placeholder-help::-webkit-scrollbar-track {
border-radius: 10px;
background: transparent;
}
.placeholder-help::-webkit-scrollbar-thumb {
border-radius: 10px;
background: var(--color-border);
transition: background 0.2s ease;
}
.placeholder-help::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* Rename specific styles */
.rename-toggle-card {
margin-bottom: 1rem;
border: 1px solid var(--color-border);
border-radius: 10px;
padding: 0.25rem;
background: var(--color-background-tertiary);
}
.rename-toggle-card .switch-label {
margin: 0;
border: none;
background: transparent;
}
.rename-format-field label {
display: flex;
align-items: center;
gap: 0.375rem;
}
.rename-format-field label svg {
color: var(--color-accent);
}
.section-icon.rename-icon {
background: linear-gradient(135deg, #52c41a25 0%, #52c41a35 100%);
}
/* Dark theme adjustments */
:root.dark .placeholder-help,
:root.auto.dark .placeholder-help {
border-color: var(--color-border);
background: var(--color-background-secondary);
box-shadow: 0 2px 8px rgb(0 0 0 / 20%);
}
:root.dark .category-title,
:root.auto.dark .category-title {
border-bottom-color: var(--color-border);
background: linear-gradient(135deg, var(--color-background-tertiary) 0%, var(--color-background-secondary) 100%);
}
:root.dark .placeholder-category,
:root.auto.dark .placeholder-category {
border-bottom-color: var(--color-border);
}
:root.dark .placeholder-item:hover,
:root.auto.dark .placeholder-item:hover {
background: rgb(var(--color-accent-rgb), 0.12);
}
:root.dark .placeholder-item code,
:root.auto.dark .placeholder-item code {
border-color: rgb(255 255 255 / 15%);
background: var(--color-blue-common);
}
:root.dark .rename-toggle-card,
:root.auto.dark .rename-toggle-card {
background: var(--color-background-tertiary);
}
:root.dark .section-icon.rename-icon,
:root.auto.dark .section-icon.rename-icon {
background: linear-gradient(135deg, #52c41a25 0%, #52c41a35 100%);
}

View File

@@ -108,6 +108,15 @@
"description": "Configure settings for each PicBed individually", "description": "Configure settings for each PicBed individually",
"title": "Per-PicBed Settings" "title": "Per-PicBed Settings"
}, },
"rename": {
"description": "Configure renaming rules for uploaded files",
"manualRename": "Manual Rename",
"renameCustomFormat": "Custom Rename Format",
"renameCustomFormatPlaceholder": "Please enter custom rename format",
"renameDescription": "Set renaming rules for uploaded files",
"renameTimestamp": "Timestamp Rename",
"title": "Rename Settings"
},
"renameSettings": "Rename", "renameSettings": "Rename",
"skipProcessSettings": "Skip Process", "skipProcessSettings": "Skip Process",
"subtitle-Global": "Configure settings for all PicBeds", "subtitle-Global": "Configure settings for all PicBeds",

View File

@@ -108,6 +108,15 @@
"description": "为每个图床单独配置设置", "description": "为每个图床单独配置设置",
"title": "图床独立设置" "title": "图床独立设置"
}, },
"rename": {
"description": "配置上传文件的重命名规则",
"manualRename": "手动重命名",
"renameCustomFormat": "自定义重命名格式",
"renameCustomFormatPlaceholder": "请输入自定义重命名格式",
"renameDescription": "设置上传文件的重命名规则",
"renameTimestamp": "时间戳重命名",
"title": "重命名设置"
},
"renameSettings": "重命名", "renameSettings": "重命名",
"skipProcessSettings": "文件跳过", "skipProcessSettings": "文件跳过",
"subtitle-Global": "为所有图床配置通用设置", "subtitle-Global": "为所有图床配置通用设置",

View File

@@ -108,6 +108,15 @@
"description": "為每個圖床單獨配置設置", "description": "為每個圖床單獨配置設置",
"title": "圖床獨立設置" "title": "圖床獨立設置"
}, },
"rename": {
"description": "配置上傳文件的重命名規則",
"manualRename": "手動重命名",
"renameCustomFormat": "自定義重命名格式",
"renameCustomFormatPlaceholder": "請輸入自定義重命名格式",
"renameDescription": "設置上傳文件的重命名規則",
"renameTimestamp": "時間戳重命名",
"title": "重命名設置"
},
"renameSettings": "重命名", "renameSettings": "重命名",
"skipProcessSettings": "文件跳過", "skipProcessSettings": "文件跳過",
"subtitle-Global": "為所有圖床配置設置", "subtitle-Global": "為所有圖床配置設置",