Feature(custom): rewrite titlebar and navigation UI

This commit is contained in:
Kuingsmile
2025-08-01 22:09:20 +08:00
parent 70396c3e5c
commit 11dd2e7238
20 changed files with 1692 additions and 596 deletions

15
.vscode/settings.json vendored
View File

@@ -8,10 +8,17 @@
"githubPullRequests.ignoredPullRequestBranches": [
"dev"
],
"[go]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@@ -44,6 +44,7 @@
"@aws-sdk/s3-request-presigner": "^3.857.0",
"@electron-toolkit/preload": "^3.0.2",
"@element-plus/icons-vue": "^2.3.1",
"@headlessui/vue": "^1.7.23",
"@highlightjs/vue-plugin": "^2.1.2",
"@nodelib/fs.walk": "^3.0.1",
"@octokit/rest": "^22.0.0",
@@ -67,6 +68,7 @@
"hpagent": "^1.2.0",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.535.0",
"marked": "^16.1.1",
"mime-types": "^2.1.35",
"mitt": "^3.0.1",

View File

@@ -8,7 +8,7 @@ import windowManager from 'apis/app/window/windowManager'
import dayjs from 'dayjs'
import { BrowserWindow, clipboard, ipcMain, IpcMainEvent, Notification, WebContents } from 'electron'
import fs from 'fs-extra'
import { IPicGo } from 'piclist'
import type { IPicGo } from 'piclist'
import writeFile from 'write-file-atomic'
import { GET_RENAME_FILE_NAME, RENAME_FILE_NAME } from '#/events/constants'

View File

@@ -2,21 +2,21 @@ const isDevelopment = process.env.NODE_ENV !== 'production'
export const MANUAL_WINDOW_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000#documents'
: 'index.html#documents'
? 'http://localhost:3000/documents'
: 'index.html/documents'
export const MINI_WINDOW_URL = isDevelopment
? 'http://localhost:3000#mini-page'
: 'index.html#mini-page'
? 'http://localhost:3000/mini-page'
: 'index.html/mini-page'
export const RENAME_WINDOW_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000#rename-page'
: 'index.html#rename-page'
? 'http://localhost:3000/rename-page'
: 'index.html/rename-page'
export const SETTING_WINDOW_URL = isDevelopment
? 'http://localhost:3000#main-page/upload'
: 'index.html#main-page/upload'
? 'http://localhost:3000/main-page/upload'
: 'index.html/main-page/upload'
export const TRAY_WINDOW_URL = isDevelopment ? 'http://localhost:3000' : 'index.html'
@@ -24,5 +24,5 @@ console.log(TRAY_WINDOW_URL)
export const TOOLBOX_WINDOW_URL =
process.env.NODE_ENV === 'development'
? 'http://localhost:3000#toolbox-page'
: 'index.html#toolbox-page'
? 'http://localhost:3000/toolbox-page'
: 'index.html/toolbox-page'

View File

@@ -24,8 +24,6 @@ import {
const windowList = new Map<IWindowList, IWindowListItem>()
const handleWindowParams = (windowURL: string) => windowURL
const getDefaultWindowSizes = (): { width: number; height: number } => {
const [mainWindowWidth, mainWindowHeight] = db.get([
configPaths.settings.mainWindowWidth,
@@ -92,8 +90,8 @@ const settingWindowOptions = {
fullscreenable: true,
resizable: true,
title: 'PicList',
vibrancy: 'ultra-dark',
transparent: true,
transparent: false,
backgroundColor: '#ebeef5',
titleBarStyle: 'hidden',
webPreferences: {
sandbox: false,
@@ -108,10 +106,7 @@ const settingWindowOptions = {
} as IBrowserWindowOptions
if (process.platform !== 'darwin') {
settingWindowOptions.show = false
settingWindowOptions.frame = false
settingWindowOptions.backgroundColor = '#3f3c37'
settingWindowOptions.transparent = false
settingWindowOptions.icon = '../../../../../resources/logo.png'
}
@@ -196,7 +191,7 @@ windowList.set(IWindowList.TRAY_WINDOW, {
multiple: false,
options: () => trayWindowOptions,
callback (window) {
window.loadURL(handleWindowParams(TRAY_WINDOW_URL))
window.loadURL(TRAY_WINDOW_URL)
window.on('blur', () => {
window.hide()
})
@@ -208,7 +203,7 @@ windowList.set(IWindowList.MANUAL_WINDOW, {
multiple: false,
options: () => manualWindowOptions,
callback (window) {
window.loadURL(handleWindowParams(MANUAL_WINDOW_URL))
window.loadURL(MANUAL_WINDOW_URL)
window.focus()
}
})
@@ -218,7 +213,7 @@ windowList.set(IWindowList.SETTING_WINDOW, {
multiple: false,
options: () => settingWindowOptions,
callback (window, windowManager) {
window.loadURL(handleWindowParams(SETTING_WINDOW_URL))
window.loadURL(SETTING_WINDOW_URL)
window.webContents.openDevTools({ mode: 'detach' })
window.on('closed', () => {
bus.emit(TOGGLE_SHORTKEY_MODIFIED_MODE, false)
@@ -238,7 +233,7 @@ windowList.set(IWindowList.MINI_WINDOW, {
multiple: false,
options: () => miniWindowOptions,
callback (window) {
window.loadURL(handleWindowParams(MINI_WINDOW_URL))
window.loadURL(MINI_WINDOW_URL)
}
})
@@ -247,7 +242,7 @@ windowList.set(IWindowList.RENAME_WINDOW, {
multiple: true,
options: () => renameWindowOptions,
async callback (window, windowManager) {
window.loadURL(handleWindowParams(RENAME_WINDOW_URL))
window.loadURL(RENAME_WINDOW_URL)
const currentWindow = windowManager.getAvailableWindow(true)
if (currentWindow && currentWindow.isVisible()) {
const { x, y, width, height } = currentWindow.getBounds()

View File

@@ -9,15 +9,18 @@
<script lang="ts" setup>
import type { IConfig } from 'piclist'
import { onBeforeMount } from 'vue'
import { onBeforeMount, onMounted } from 'vue'
import { useATagClick } from '@/hooks/useATagClick'
import { useStore } from '@/hooks/useStore'
import { getConfig } from '@/utils/dataSender'
import { pageReloadCount } from '@/utils/global'
import { useAppStore } from './hooks/appStore'
useATagClick()
const store = useStore()
const appStore = useAppStore()
onBeforeMount(async () => {
const config = await getConfig<IConfig>()
@@ -25,6 +28,15 @@ onBeforeMount(async () => {
store?.setDefaultPicBed(config?.picBed?.uploader || config?.picBed?.current || 'smms')
}
})
onMounted(async () => {
try {
appStore.init()
} catch (error) {
console.error('Failed to load settings:', error)
}
})
</script>
<script lang="ts">

View File

@@ -0,0 +1,746 @@
<template>
<nav class="navigation">
<div class="title-bar">
<div class="app-title">
<div class="app-text">
{{ $t('app.title') }}
</div>
<div class="app-version">
v{{ version }}
</div>
</div>
</div>
<div class="theme-section">
<ThemeSwitcher />
</div>
<div class="nav-menu">
<router-link
v-for="item in navigationItems"
:key="item.path"
:to="item.path"
class="nav-item"
:title="`${item.name}`"
>
<div class="nav-icon-container">
<component
:is="item.icon"
:size="18"
/>
</div>
<span class="nav-label">{{ item.name }}</span>
</router-link>
<Disclosure
v-slot="{ open }"
as="div"
class="nav-submenu"
>
<DisclosureButton class="nav-item submenu-trigger">
<div class="nav-icon-container">
<DatabaseIcon :size="18" />
</div>
<span class="nav-label">{{ $t('navigation.picbed') }}</span>
<ChevronDownIcon
:size="16"
class="submenu-arrow"
:class="{ 'rotate-180': open }"
/>
</DisclosureButton>
<DisclosurePanel class="submenu-panel">
<router-link
v-for="item in visiblePicBeds"
:key="item.type"
:to="{ name: routerConfig.UPLOADER_CONFIG_PAGE, params: { type: item.type } }"
class="submenu-item"
>
<span>{{ item.name }}</span>
</router-link>
</DisclosurePanel>
</Disclosure>
</div>
<div class="sidebar-footer">
<button
class="footer-button"
:title="$t('navigation.moreOptions')"
@click="openMenu"
>
<BadgeInfoIcon :size="20" />
</button>
</div>
</nav>
<TransitionRoot
appear
:show="qrcodeVisible"
as="template"
>
<Dialog
as="div"
class="qr-dialog"
@close="qrcodeVisible = false"
>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="dialog-overlay" />
</TransitionChild>
<div class="dialog-container">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel class="dialog-panel">
<DialogTitle class="dialog-title">
{{ $t('navigation.picBedQrCode') }}
</DialogTitle>
<div class="dialog-content">
<div class="form-group">
<label class="form-label">{{ $t('navigation.choosePicBed') }}</label>
<Listbox
v-model="choosedPicBedForQRCode"
multiple
>
<div class="listbox-container">
<ListboxButton class="listbox-button">
<span
v-if="choosedPicBedForQRCode.length === 0"
class="placeholder"
>
{{ $t('navigation.selectPicBeds') }}
</span>
<span
v-else
class="selected-count"
>
{{ choosedPicBedForQRCode.length }} {{ $t('navigation.selected') }}
</span>
<ChevronDownIcon
:size="16"
class="listbox-arrow"
/>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions class="listbox-options">
<ListboxOption
v-for="picbed in picBedGlobal"
:key="picbed.type"
v-slot="{ active, selected }"
:value="picbed.type"
>
<li
class="listbox-option"
:class="{ active, selected }"
>
<span>{{ picbed.name }}</span>
<CheckIcon
v-if="selected"
:size="16"
/>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
<button
v-if="choosedPicBedForQRCode.length > 0"
class="copy-button"
@click="handleCopyPicBedConfig"
>
<CopyIcon :size="16" />
{{ $t('navigation.copyPicBedConfig') }}
</button>
</div>
<div
v-if="choosedPicBedForQRCode.length > 0"
class="qr-container"
>
<qrcode-vue
:size="280"
:value="picBedConfigString"
class="qr-code"
/>
</div>
</div>
<div class="dialog-actions">
<button
class="cancel-button"
@click="qrcodeVisible = false"
>
{{ $t('navigation.close') }}
</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import {
Dialog,
DialogPanel,
DialogTitle,
Disclosure,
DisclosureButton,
DisclosurePanel,
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
TransitionChild,
TransitionRoot
} from '@headlessui/vue'
import { ElMessage as $message } from 'element-plus'
import { pick } from 'lodash-es'
import { BadgeInfoIcon, CheckIcon, ChevronDownIcon, CopyIcon, DatabaseIcon, FolderIcon, PieChartIcon, PlugIcon, Settings, UploadIcon } from 'lucide-vue-next'
import QrcodeVue from 'qrcode.vue'
import pkg from 'root/package.json'
import { SHOW_MAIN_PAGE_QRCODE } from 'root/src/universal/events/constants'
import { computed, nextTick, onBeforeMount, reactive, Ref, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import * as config from '@/router/config'
import { getConfig } from '@/utils/dataSender'
import { picBedGlobal, updatePicBedGlobal } from '@/utils/global'
import { IRPCActionType } from '#/types/enum'
import ThemeSwitcher from './ui/ThemeSwitcher.vue'
const version = ref(pkg.version)
const { t } = useI18n()
const routerConfig = reactive(config)
const qrcodeVisible = ref(false)
const choosedPicBedForQRCode: Ref<string[]> = ref([])
const picBedConfigString = ref('')
watch(
() => choosedPicBedForQRCode,
val => {
if (val.value.length > 0) {
nextTick(async () => {
const picBedConfig = await getConfig('picBed')
const config = pick(picBedConfig, ...choosedPicBedForQRCode.value)
picBedConfigString.value = JSON.stringify(config)
})
}
},
{ deep: true }
)
const visiblePicBeds = computed(() =>
picBedGlobal.value.filter(item => item.visible)
)
const qrCodeHandler = () => {
qrcodeVisible.value = true
}
function openMenu () {
window.electron.sendRPC(IRPCActionType.SHOW_MAIN_PAGE_MENU)
}
function handleCopyPicBedConfig () {
window.electron.clipboard.writeText(picBedConfigString.value)
$message.success(t('COPY_PICBED_CONFIG_SUCCEED'))
}
const navigationItems = computed(() => [
{ name: t('navigation.upload'), path: '/main-page/upload', icon: UploadIcon },
{ name: t('navigation.manage'), path: '/main-page/manage-login-page', icon: PieChartIcon },
{ name: t('navigation.gallery'), path: '/main-page/gallery', icon: FolderIcon },
{ name: t('navigation.settings'), path: '/main-page/settings', icon: Settings },
{
name: t('navigation.plugins'),
path: '/main-page/plugins',
icon: PlugIcon
}
])
onBeforeMount(() => {
updatePicBedGlobal()
window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
})
</script>
<style scoped>
.navigation {
display: flex;
flex-direction: column;
width: 150px;
height: 100vh;
background: var(--color-background-secondary);
border-right: 1px solid rgb(229 231 235);
overflow: hidden;
}
:root.dark .navigation,
:root.auto.dark .navigation {
background: var(--color-background-secondary);
border-right-color: var(--color-background-secondary);
}
.title-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 1.25rem 1rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-background-secondary);
}
:root.dark .title-bar,
:root.auto.dark .title-bar {
border-bottom-color: var(--color-border);
background: var(--color-background-secondary);
}
.app-title {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.app-text {
font-size: 16px;
font-weight: 700;
color: var(--color-text-primary);
letter-spacing: -0.025em;
}
.app-version {
font-size: 10px;
font-weight: 500;
color: var(--color-text-secondary);
background: var(--color-surface-elevated);
padding: 3px 8px;
border-radius: 12px;
border: 1px solid var(--color-border);
}
.theme-section {
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
:root.dark .theme-section,
:root.auto.dark .theme-section {
border-bottom-color: var(--color-border);
}
.nav-menu {
flex: 1;
padding: 1rem 0;
overflow-y: auto;
min-height: 0;
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: rgb(75 85 99);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
:root.dark .nav-item,
:root.auto.dark .nav-item {
color: rgb(209 213 219);
}
.nav-item:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
}
:root.dark .nav-item:hover,
:root.auto.dark .nav-item:hover {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
.nav-item.router-link-active {
background: rgb(239 246 255);
color: rgb(99 102 241);
border-right: 3px solid rgb(99 102 241);
}
:root.dark .nav-item.router-link-active,
:root.auto.dark .nav-item.router-link-active {
background: rgb(30 58 138 / 0.2);
color: rgb(129 140 248);
border-right-color: rgb(129 140 248);
}
.nav-icon-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.sidebar-footer {
padding: 12px;
border-top: 1px solid var(--color-border);
}
.footer-button {
cursor: pointer;
position: fixed;
bottom: 4px;
left: 4px;
color: var(--color-text-secondary);
background: transparent;
border: none;
padding: 8px;
border-radius: 6px;
}
.footer-button:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.nav-submenu {
margin-top: 4px;
justify-content: center;
position: relative;
}
.submenu-trigger {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
color: rgb(75 85 99);
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
background: transparent;
border: none;
width: 100%;
cursor: pointer;
position: relative;
}
:root.dark .submenu-trigger,
:root.auto.dark .submenu-trigger {
color: rgb(209 213 219);
}
.submenu-trigger:hover {
background: rgb(243 244 246);
color: rgb(17 24 39);
}
:root.dark .submenu-trigger:hover,
:root.auto.dark .submenu-trigger:hover {
background: rgb(55 65 81);
color: rgb(243 244 246);
}
.submenu-trigger .nav-icon-container {
flex-shrink: 0;
}
.submenu-trigger span {
flex-shrink: 0;
}
.submenu-arrow {
position: absolute;
right: 1rem;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.rotate-180 {
transform: rotate(180deg);
}
.submenu-panel {
margin-top: 2px;
padding-left: 2.75rem;
display: flex;
flex-direction: column;
gap: 4px;
}
.submenu-item {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 0.8125rem;
font-weight: 500;
border-radius: 6px;
transition: all 0.2s ease;
}
.submenu-item:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.submenu-item.router-link-active {
background: rgb(239 246 255);
color: rgb(99 102 241);
}
:root.dark .submenu-item.router-link-active,
:root.auto.dark .submenu-item.router-link-active {
background: rgb(30 58 138 / 0.2);
color: rgb(129 140 248);
}
.qr-dialog {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.dialog-container {
position: fixed;
inset: 0;
z-index: 50;
overflow-y: auto;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
padding: 16px;
}
.dialog-panel {
width: 100%;
max-width: 500px;
background: var(--color-surface);
border-radius: 16px;
border: 1px solid var(--color-border);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.dialog-title {
padding: 20px 24px 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
}
.dialog-content {
padding: 20px 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
}
.listbox-container {
position: relative;
}
.listbox-button {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 16px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
font-size: 14px;
color: var(--color-text-primary);
cursor: pointer;
transition: var(--transition);
}
.listbox-button:hover {
border-color: var(--color-accent);
}
.placeholder {
color: var(--color-text-secondary);
}
.selected-count {
color: var(--color-text-primary);
}
.listbox-arrow {
color: var(--color-text-secondary);
}
.listbox-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10;
margin-top: 4px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
box-shadow: var(--shadow-md);
max-height: 200px;
overflow-y: auto;
}
.listbox-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
font-size: 14px;
color: var(--color-text-primary);
cursor: pointer;
transition: var(--transition);
}
.listbox-option.active {
background: var(--color-surface-elevated);
}
.listbox-option.selected {
background: var(--color-accent);
color: white;
}
.copy-button {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 10px 16px;
background: var(--color-accent);
color: white;
border: none;
border-radius: var(--border-radius);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
}
.copy-button:hover {
background: var(--color-accent-hover);
}
.qr-container {
display: flex;
justify-content: center;
padding: 20px 0;
}
.qr-code {
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.dialog-actions {
padding: 0 24px 20px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.cancel-button {
padding: 10px 20px;
background: var(--color-surface-elevated);
color: var(--color-text-primary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
font-size: 14px;
cursor: pointer;
transition: var(--transition);
}
.cancel-button:hover {
background: var(--color-border);
}
/* Responsive Design */
@media (max-width: 768px) {
.sidebar {
width: 60px;
}
.nav-label {
display: none;
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
</style>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { Monitor, Moon, Sun } from 'lucide-vue-next'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/hooks/appStore'
const { t } = useI18n()
const appStore = useAppStore()
const currentTheme = computed(() => appStore.settings.app.theme || 'light')
const themeOptions = computed(() => [
{
value: 'light',
label: t('settings.theme.light'),
icon: Sun,
description: t('settings.theme.lightDesc')
},
{
value: 'dark',
label: t('settings.theme.dark'),
icon: Moon,
description: t('settings.theme.darkDesc')
},
{
value: 'auto',
label: t('settings.theme.auto'),
icon: Monitor,
description: t('settings.theme.autoDesc')
}
])
const currentThemeOption = computed(
() => themeOptions.value.find(option => option.value === currentTheme.value) || themeOptions.value[0]
)
const toggleTheme = () => {
appStore.toggleTheme()
}
</script>
<template>
<div class="theme-switcher">
<button
class="theme-toggle-btn"
:title="t('settings.theme.toggle')"
@click="toggleTheme"
>
<component
:is="currentThemeOption.icon"
:size="18"
/>
<span class="theme-label">{{ currentThemeOption.label }}</span>
</button>
</div>
</template>
<style scoped>
.theme-switcher {
position: relative;
display: flex;
align-items: center;
}
.theme-toggle-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-secondary);
border-radius: var(--radius-md);
cursor: pointer;
font-size: 0.875rem;
}
.theme-toggle-btn:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.theme-label {
font-weight: 500;
}
/* Mobile responsive */
@media (max-width: 768px) {
.theme-label {
display: none;
}
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<div
class="title-bar"
data-drag-region
>
<div class="title-bar-content">
<div class="title-left">
<div class="app-icon">
<img
src="/roundLogo.png"
alt="App Icon"
width="20"
height="20"
>
</div>
</div>
<div class="title-center">
<!-- Progress bar in title bar -->
<div
v-if="isShowprogress"
class="progress-container"
>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${progress}%` }"
/>
</div>
<span class="progress-text">{{ progress }}%</span>
</div>
</div>
<div
class="title-right"
>
<div class="window-controls">
<button
class="control-button pin-button"
:class="{ active: isAlwaysOnTop }"
:title="$t('titleBar.alwaysOnTop')"
@click="setAlwaysOnTop"
>
<PinIcon
:color="isAlwaysOnTop ? '#CE6769' : '#000'"
:size="14"
/>
</button>
<button
class="control-button minimize-button"
:title="$t('titleBar.minimize')"
@click="minimizeWindow"
>
<MinusIcon :size="14" />
</button>
<button
class="control-button mini-button"
:title="$t('titleBar.miniWindow')"
@click="openMiniWindow"
>
<ShrinkIcon :size="14" />
</button>
<button
class="control-button close-button"
:title="$t('titleBar.close')"
@click="closeWindow"
>
<XIcon :size="14" />
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { IpcRendererEvent } from 'electron'
import { MinusIcon, PinIcon, ShrinkIcon, XIcon } from 'lucide-vue-next'
import { onBeforeMount, onBeforeUnmount, ref } from 'vue'
import { IRPCActionType } from '#/types/enum'
const isShowprogress = ref(false)
const progress = ref(0)
const isAlwaysOnTop = ref(false)
function setAlwaysOnTop () {
isAlwaysOnTop.value = !isAlwaysOnTop.value
window.electron.sendRPC(IRPCActionType.MAIN_WINDOW_ON_TOP)
}
function minimizeWindow () {
window.electron.sendRPC(IRPCActionType.MINIMIZE_WINDOW)
}
function openMiniWindow () {
window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
}
function closeWindow () {
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
}
const uploadProcessHandler = (_event: IpcRendererEvent, data: { progress: number }) => {
isShowprogress.value = data.progress !== 100 && data.progress !== 0
progress.value = data.progress
}
onBeforeMount(() => {
window.electron.ipcRendererOn('updateProgress', uploadProcessHandler)
})
onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener('updateProgress', uploadProcessHandler)
})
</script>
<style scoped>
.title-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 32px;
background: var(--color-background-secondary);
border-bottom: 1px solid var(--color-border);
z-index: 1000;
-webkit-app-region: drag;
}
.title-bar-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 16px;
}
.title-left {
display: flex;
align-items: center;
gap: 8px;
-webkit-app-region: no-drag;
}
.app-icon {
display: flex;
align-items: center;
color: var(--color-accent);
}
.app-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.app-version {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-border);
padding: 2px 6px;
border-radius: 4px;
}
.title-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
-webkit-app-region: no-drag;
}
.progress-container {
display: flex;
align-items: center;
gap: 8px;
max-width: 200px;
}
.progress-bar {
flex: 1;
height: 4px;
background: var(--color-border);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-success);
border-radius: 2px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 11px;
color: var(--color-text-secondary);
min-width: 35px;
}
.title-right {
display: flex;
align-items: center;
-webkit-app-region: no-drag;
}
.window-controls {
display: flex;
align-items: center;
gap: 8px;
}
.control-button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 20px;
border: none;
background: transparent;
border-radius: 4px;
color: var(--color-text-secondary);
cursor: pointer;
transition: var(--transition);
}
.control-button:hover {
background: var(--color-surface-elevated);
color: var(--color-text-primary);
}
.pin-button.active {
color: var(--color-accent);
background: var(--color-accent)20;
}
.close-button:hover {
background: var(--color-danger);
color: white;
}
</style>

View File

@@ -0,0 +1,83 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { IStringKeyMap } from '#/types/types'
export const useAppStore = defineStore('app', () => {
const settings = ref<IStringKeyMap>({
app: {
theme: 'light'
}
})
const loading = ref(false)
const error = ref<string | undefined>()
function clearError () {
error.value = undefined
}
const loadSettings = () => {
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
settings.value.app.theme = savedTheme
}
applyTheme(settings.value.app.theme || 'light')
}
function applyTheme (theme: string) {
const root = document.documentElement
root.classList.remove('light', 'dark', 'auto')
if (theme === 'auto') {
root.classList.add('auto')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
root.classList.add(prefersDark ? 'dark' : 'light')
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
mediaQuery.addEventListener('change', e => {
if (settings.value.app.theme === 'auto') {
root.classList.remove('light', 'dark')
root.classList.add(e.matches ? 'dark' : 'light')
}
})
} else {
root.classList.add(theme)
}
}
function setTheme (theme: 'light' | 'dark' | 'auto') {
settings.value.app.theme = theme
localStorage.setItem('theme', theme)
applyTheme(theme)
}
function toggleTheme () {
const currentTheme = settings.value.app.theme || 'light'
const themes = ['light', 'dark', 'auto'] as const
const currentIndex = themes.indexOf(currentTheme as any)
const nextTheme = themes[(currentIndex + 1) % themes.length]
setTheme(nextTheme)
}
function init () {
try {
loadSettings()
} catch (err) {
console.error('Application initialization failed:', err)
throw err
}
}
return {
init,
loadSettings,
settings,
loading,
error,
clearError,
setTheme,
toggleTheme,
applyTheme
}
})

View File

@@ -1,4 +1,39 @@
{
"app": {
"title": "PicList"
},
"titleBar": {
"alwaysOnTop": "Always On Top",
"close": "Close",
"minimize": "Minimize",
"miniWindow": "Mini Window"
},
"navigation": {
"upload": "Upload",
"manage": "Manage",
"gallery": "Gallery",
"settings": "Settings",
"plugins": "Plugins",
"picbed": "PicBed",
"close": "Close",
"copyPicBedConfig": "Copy PicBed Config",
"selected": "Selected",
"moreOptions": "More Options",
"picBedQrCode": "PicBed QR Code",
"choosePicBed": "Choose PicBed",
"selectPicBeds": "Select PicBeds"
},
"settings": {
"theme": {
"light": "Light",
"dark": "Dark",
"auto": "Auto",
"lightDesc": "Light Mode",
"darkDesc": "Dark Mode",
"autoDesc": "Auto Mode",
"toggle": "Toggle Theme"
}
},
"OPEN_MAIN_WINDOW": "Open Main Window",
"OPERATION_SUCCEED": "Operation Succeed",
"QUICK_UPLOAD": "Quick Upload",
@@ -21,7 +56,6 @@
"MANUAL_PAGE_OPEN_BY_BROWSER": "Browser",
"MANUAL_PAGE_OPEN_BY_BUILD_IN": "Built-in Window",
"MANUAL_PAGE_OPEN_SETTING_TIP": "Select the way to open the manual",
"UPLOAD_AREA": "Upload",
"UPLOAD_VIEW_HINT": "Click to open picbeds settings",
"MANAGE_PAGE": "Manage",
"GALLERY": "Gallery",
@@ -432,8 +466,8 @@
"MANAGE_CONSTANT_GITHUB_PROXY_TIPS": "If the access speed is slow, you can try configuring a proxy",
"MANAGE_CONSTANT_GITHUB_PAGING_DESC": "Enable pagination",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_DESC": "CDN acceleration domain name - Optional",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_PLACEHOLDER": "Support using {username}, {repo}, {branch}, and {path} as replacement placeholders",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_TIPS": "For example: https://cdn.staticaly.com/gh/{username}/{repo}@{branch}/{path}",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_PLACEHOLDER": "Support using {'{'}username{'}'}, {'{'}repo{'}'}, {'{'}branch{'}'}, and {'{'}path{'}'} as replacement placeholders",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_TIPS": "eg. https://cdn.staticaly.com/gh/{'{'}username{'}'}/{'{'}repo{'}'}{'@'}{'{'}branch{'}'}/{'{'}path{'}'}",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_RULE_MESSAGE_A": "The acceleration domain name must start with http:// or https://",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_RULE_MESSAGE_B": "The braces in the acceleration domain name must appear in pairs",
"MANAGE_CONSTANT_GITHUB_EXPLAIN": "There is an hourly limit for API calls, and uploading files larger than 100M is not supported",

View File

@@ -1,4 +1,40 @@
{
"app": {
"title": "PicList"
},
"titleBar": {
"alwaysOnTop": "置顶",
"close": "关闭",
"minimize": "最小化",
"miniWindow": "迷你窗口"
},
"navigation": {
"upload": "上传",
"manage": "管理",
"gallery": "相册",
"settings": "设置",
"plugins": "插件",
"manual": "手册",
"picbed": "图床",
"close": "关闭",
"copyPicBedConfig": "复制图床设置",
"selected": "已选中",
"moreOptions": "更多选项",
"picBedQrCode": "图床配置二维码",
"choosePicBed": "选择图床",
"selectPicBeds": "请选择图床"
},
"settings": {
"theme": {
"light": "浅色",
"dark": "深色",
"auto": "自动",
"lightDesc": "明亮模式",
"darkDesc": "黑暗模式",
"autoDesc": "自动模式",
"toggle": "切换主题"
}
},
"OPEN_MAIN_WINDOW": "打开主窗口",
"OPERATION_SUCCEED": "操作成功",
"QUICK_UPLOAD": "快捷上传",
@@ -21,7 +57,6 @@
"MANUAL_PAGE_OPEN_BY_BROWSER": "浏览器",
"MANUAL_PAGE_OPEN_BY_BUILD_IN": "内置窗口",
"MANUAL_PAGE_OPEN_SETTING_TIP": "选择手册打开方式",
"UPLOAD_AREA": "上传",
"UPLOAD_VIEW_HINT": "点击打开图床设置",
"MANAGE_PAGE": "管理",
"GALLERY": "相册",
@@ -432,8 +467,8 @@
"MANAGE_CONSTANT_GITHUB_PROXY_TIPS": "如果访问速度较慢,可以尝试配置代理",
"MANAGE_CONSTANT_GITHUB_PAGING_DESC": "是否开启分页",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_DESC": "CDN加速域名-可选",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_PLACEHOLDER": "支持使用{username}、{repo}、{branch}和{path}作为替换占位符,用于适配不同仓库和分支",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_TIPS": "例如: https://cdn.staticaly.com/gh/{username}/{repo}@{branch}/{path}",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_PLACEHOLDER": "支持使用{'{'}username{'}'}、{'{'}repo{'}'}、{'{'}branch{'}'}和{'{'}path{'}'}作为替换占位符,用于适配不同仓库和分支",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_TIPS": "例如`https://cdn.staticaly.com/gh/{'{'}username{'}'}/{'{'}repo{'}'}{'@'}{'{'}branch{'}'}/{'{'}path{'}'}`",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_RULE_MESSAGE_A": "加速域名请以http://或https://开头",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_RULE_MESSAGE_B": "加速域名中的大括号必须成对出现",
"MANAGE_CONSTANT_GITHUB_EXPLAIN": "API调用有每小时上限此外不支持上传超过100M的文件",

View File

@@ -1,4 +1,40 @@
{
"app": {
"title": "PicList"
},
"titleBar": {
"alwaysOnTop": "置頂",
"close": "關閉",
"minimize": "最小化",
"miniWindow": "迷你視窗"
},
"navigation": {
"upload": "上傳",
"manage": "管理",
"gallery": "相簿",
"settings": "設定",
"plugins": "插件",
"manual": "手冊",
"picbed": "圖床",
"close": "關閉",
"copyPicBedConfig": "複製圖床設定",
"selected": "已選中",
"moreOptions": "更多選項",
"picBedQrCode": "圖床配置 QRCODE",
"choosePicBed": "選擇圖床",
"selectPicBeds": "請選擇圖床"
},
"settings": {
"theme": {
"light": "淺色",
"dark": "深色",
"auto": "自動",
"lightDesc": "明亮模式",
"darkDesc": "黑暗模式",
"autoDesc": "自動模式",
"toggle": "切換主題"
}
},
"OPEN_MAIN_WINDOW": "打開主視窗",
"OPERATION_SUCCEED": "操作成功",
"QUICK_UPLOAD": "快速上傳",
@@ -21,7 +57,6 @@
"MANUAL_PAGE_OPEN_BY_BROWSER": "瀏覽器",
"MANUAL_PAGE_OPEN_BY_BUILD_IN": "內置窗口",
"MANUAL_PAGE_OPEN_SETTING_TIP": "選擇打開手冊方式",
"UPLOAD_AREA": "上傳",
"UPLOAD_VIEW_HINT": "點擊打開圖床設定",
"MANAGE_PAGE": "管理",
"GALLERY": "相簿",
@@ -432,8 +467,8 @@
"MANAGE_CONSTANT_GITHUB_PROXY_TIPS": "如果訪問速度較慢,可以嘗試配置代理",
"MANAGE_CONSTANT_GITHUB_PAGING_DESC": "是否開啟分頁",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_DESC": "CDN加速域名-可選",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_PLACEHOLDER": "支持使用{username}、{repo}、{branch}和{path}作為替換占位符,用於適配不同倉庫和分支",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_TIPS": "例如: https://cdn.staticaly.com/gh/{username}/{repo}@{branch}/{path}",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_PLACEHOLDER": "支持使用{'{'}username{'}'}、{'{'}repo{'}'}、{'{'}branch{'}'}和{'{'}path{'}'}作為替換占位符,用於適配不同倉庫和分支",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_TIPS": "例如`https://cdn.staticaly.com/gh/{'{'}username{'}'}/{'{'}repo{'}'}{'@'}{'{'}branch{'}'}/{'{'}path{'}'}`",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_RULE_MESSAGE_A": "加速域名請以http://或https://開頭",
"MANAGE_CONSTANT_GITHUB_CUSTOM_URL_RULE_MESSAGE_B": "加速域名中的大括號必須成對出現",
"MANAGE_CONSTANT_GITHUB_EXPLAIN": "API調用有每小時上限此外不支持上傳超過100M的文件",

View File

@@ -1,519 +1,297 @@
<template>
<div id="main-page">
<div class="fake-title-bar">
<div class="fake-title-bar__title">
PicList - {{ version }}
</div>
<div
v-if="osGlobal !== 'darwin'"
class="handle-bar"
id="main"
class="app-container"
>
<el-icon
class="minus"
:color="isAlwaysOnTop ? '#409EFF' : '#fff'"
size="20"
style="margin-right: 10px"
@click="setAlwaysOnTop"
>
<ArrowUpBold />
</el-icon>
<el-icon
class="minus"
color="#fff"
size="20"
style="margin-right: 10px"
@click="minimizeWindow"
>
<SemiSelect />
</el-icon>
<el-icon
class="plus"
color="orange"
size="20"
style="margin-right: 10px"
@click="openMiniWindow"
>
<ArrowDownBold />
</el-icon>
<el-icon
class="close"
color="#fff"
size="20"
@click="closeWindow"
>
<CloseBold />
</el-icon>
<TitleBar />
<div class="app-background">
<div class="bg-gradient-primary" />
<div class="bg-gradient-secondary" />
</div>
</div>
<el-progress
v-if="isShowprogress"
:percentage="progress"
:stroke-width="7"
:text-inside="true"
:show-text="false"
status="success"
class="progress-bar"
/>
<el-row
style="padding-top: 22px"
class="main-content"
>
<el-col class="side-bar-menu">
<el-menu
class="picgo-sidebar"
:default-active="defaultActive"
:unique-opened="true"
@select="handleSelect"
>
<el-menu-item :index="routerConfig.UPLOAD_PAGE">
<el-icon>
<UploadFilled />
</el-icon>
<span>{{ $t('UPLOAD_AREA') }}</span>
</el-menu-item>
<el-menu-item :index="routerConfig.MANAGE_LOGIN_PAGE">
<el-icon>
<PieChart />
</el-icon>
<span>{{ $t('MANAGE_PAGE') }}</span>
</el-menu-item>
<el-menu-item :index="routerConfig.GALLERY_PAGE">
<el-icon>
<PictureFilled />
</el-icon>
<span>{{ $t('GALLERY') }}</span>
</el-menu-item>
<el-sub-menu
index="sub-menu"
:show-timeout="0"
:hide-timeout="0"
:popper-offset="0"
>
<template #title>
<el-icon>
<Menu />
</el-icon>
<span>{{ $t('PICBEDS_SETTINGS') }}</span>
</template>
<template v-for="item in picBedGlobal">
<el-menu-item
v-if="item.visible"
:key="item.type"
:index="`${routerConfig.UPLOADER_CONFIG_PAGE}-${item.type}`"
>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</el-sub-menu>
<el-menu-item :index="routerConfig.SETTING_PAGE">
<el-icon>
<Tools />
</el-icon>
<span>{{ $t('PICLIST_SETTINGS') }}</span>
</el-menu-item>
<el-menu-item :index="routerConfig.PLUGIN_PAGE">
<el-icon>
<Share />
</el-icon>
<span>{{ $t('PLUGIN_SETTINGS') }}</span>
</el-menu-item>
<el-menu-item :index="routerConfig.DocumentPage">
<el-icon>
<Link />
</el-icon>
<span>{{ $t('MANUAL') }}</span>
</el-menu-item>
</el-menu>
<el-icon
class="info-window"
@click="openMenu"
>
<InfoFilled />
</el-icon>
</el-col>
<el-col
:span="21"
:offset="3"
style="height: 100%"
class="main-wrapper"
>
<router-view v-slot="{ Component }">
<Navigation />
<main class="main-content">
<div class="content-container">
<router-view v-slot="{ Component, route }">
<transition
name="picgo-fade"
name="page"
mode="out-in"
>
<keep-alive :include="keepAlivePages">
<component :is="Component" />
<component
:is="Component"
:key="route.path"
/>
</keep-alive>
</transition>
</router-view>
</el-col>
</el-row>
<el-dialog
v-model="qrcodeVisible"
class="qrcode-dialog"
top="3vh"
width="60%"
:title="$t('PICBED_QRCODE')"
:modal-append-to-body="false"
lock-scroll
append-to-body
>
<el-form
label-position="left"
label-width="70px"
size="small"
>
<el-form-item :label="$t('CHOOSE_PICBED')">
<el-select
v-model="choosedPicBedForQRCode"
multiple
collapse-tags
:persistent="false"
teleported
>
<el-option
v-for="item in picBedGlobal"
:key="item.type"
:label="item.name"
:value="item.type"
/>
</el-select>
<el-button
v-show="choosedPicBedForQRCode.length > 0"
type="primary"
round
class="copy-picbed-config"
@click="handleCopyPicBedConfig"
>
{{ $t('COPY_PICBED_CONFIG') }}
</el-button>
</el-form-item>
</el-form>
<div class="qrcode-container">
<qrcode-vue
v-show="choosedPicBedForQRCode.length > 0"
:size="280"
:value="picBedConfigString"
/>
</div>
</el-dialog>
<input-box-dialog />
</main>
</div>
</template>
<script lang="ts" setup>
import {
ArrowDownBold,
ArrowUpBold,
CloseBold,
InfoFilled,
Link,
Menu,
PictureFilled,
PieChart,
SemiSelect,
Share,
Tools,
UploadFilled
} from '@element-plus/icons-vue'
import type { IpcRendererEvent } from 'electron'
import { ElMessage as $message, ElMessageBox } from 'element-plus'
import { pick } from 'lodash-es'
import QrcodeVue from 'qrcode.vue'
import pkg from 'root/package.json'
import { nextTick, onBeforeMount, onBeforeUnmount, reactive, Ref, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { onBeforeRouteUpdate, useRouter } from 'vue-router'
import { useRouter } from 'vue-router'
import InputBoxDialog from '@/components/InputBoxDialog.vue'
import * as config from '@/router/config'
import { getConfig, saveConfig } from '@/utils/dataSender'
import { osGlobal, picBedGlobal, updatePicBedGlobal } from '@/utils/global'
import { SHOW_MAIN_PAGE_QRCODE } from '#/events/constants'
import { II18nLanguage, IRPCActionType } from '#/types/enum'
import { configPaths, manualPageOpenType } from '#/utils/configPaths'
import Navigation from '@/components/NavigationPage.vue'
import TitleBar from '@/components/ui/TitleBar.vue'
const { t } = useI18n()
const version = ref(pkg.version)
const routerConfig = reactive(config)
const defaultActive = ref(routerConfig.UPLOAD_PAGE)
const $router = useRouter()
const qrcodeVisible = ref(false)
const picBedConfigString = ref('')
const choosedPicBedForQRCode: Ref<string[]> = ref([])
const isAlwaysOnTop = ref(false)
const keepAlivePages = $router
.getRoutes()
.filter(item => item.meta.keepAlive)
.map(item => item.name as string)
const isShowprogress = ref(false)
const progress = ref(0)
const qrCodeHandler = () => {
qrcodeVisible.value = true
}
const uploadProcessHandler = (_event: IpcRendererEvent, data: { progress: number }) => {
isShowprogress.value = data.progress !== 100 && data.progress !== 0
progress.value = data.progress
}
onBeforeMount(() => {
updatePicBedGlobal()
window.electron.ipcRendererOn(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
window.electron.ipcRendererOn('updateProgress', uploadProcessHandler)
})
watch(
() => choosedPicBedForQRCode,
val => {
if (val.value.length > 0) {
nextTick(async () => {
const picBedConfig = await getConfig('picBed')
const config = pick(picBedConfig, ...choosedPicBedForQRCode.value)
picBedConfigString.value = JSON.stringify(config)
})
}
},
{ deep: true }
)
const handleSelect = async (index: string) => {
defaultActive.value = index
if (index === routerConfig.DocumentPage) {
const manualPageOpenSetting = await getConfig<manualPageOpenType>(configPaths.settings.manualPageOpen)
const lang = (await getConfig(configPaths.settings.language)) || II18nLanguage.ZH_CN
const openManual = () => window.electron.sendRPC(IRPCActionType.OPEN_MANUAL_WINDOW)
const openExternal = () =>
window.electron.sendRPC(
IRPCActionType.OPEN_URL,
lang === II18nLanguage.ZH_CN ? 'https://piclist.cn/app.html' : 'https://piclist.cn/en/app.html'
)
if (!manualPageOpenSetting) {
ElMessageBox.confirm(t('MANUAL_PAGE_OPEN_TIP'), t('MANUAL_PAGE_OPEN_TIP_TITLE'), {
confirmButtonText: t('MANUAL_PAGE_OPEN_BY_BROWSER'),
cancelButtonText: t('MANUAL_PAGE_OPEN_BY_BUILD_IN'),
type: 'info',
center: true
})
.then(() => {
saveConfig(configPaths.settings.manualPageOpen, 'browser')
openExternal()
})
.catch(() => {
saveConfig(configPaths.settings.manualPageOpen, 'window')
openManual()
})
} else {
manualPageOpenSetting === 'window' ? openManual() : openExternal()
}
return
}
const type = index.match(routerConfig.UPLOADER_CONFIG_PAGE)
if (type === null) {
$router.push({
name: index
})
} else {
const type = index.replace(`${routerConfig.UPLOADER_CONFIG_PAGE}-`, '')
$router.push({
name: routerConfig.UPLOADER_CONFIG_PAGE,
params: {
type
}
})
}
}
function minimizeWindow () {
window.electron.sendRPC(IRPCActionType.MINIMIZE_WINDOW)
}
function closeWindow () {
window.electron.sendRPC(IRPCActionType.CLOSE_WINDOW)
}
function openMenu () {
window.electron.sendRPC(IRPCActionType.SHOW_MAIN_PAGE_MENU)
}
function openMiniWindow () {
window.electron.sendRPC(IRPCActionType.OPEN_MINI_WINDOW)
}
function handleCopyPicBedConfig () {
window.electron.clipboard.writeText(picBedConfigString.value)
$message.success(t('COPY_PICBED_CONFIG_SUCCEED'))
}
function setAlwaysOnTop () {
isAlwaysOnTop.value = !isAlwaysOnTop.value
window.electron.sendRPC(IRPCActionType.MAIN_WINDOW_ON_TOP)
}
onBeforeRouteUpdate(async to => {
if (to.params.type) {
defaultActive.value = `${routerConfig.UPLOADER_CONFIG_PAGE}-${to.params.type}`
} else {
defaultActive.value = to.name as string
}
})
onBeforeUnmount(() => {
window.electron.ipcRendererRemoveListener(SHOW_MAIN_PAGE_QRCODE, qrCodeHandler)
window.electron.ipcRendererRemoveListener('updateProgress', uploadProcessHandler)
})
</script>
<script lang="ts">
export default {
name: 'MainPage'
}
</script>
<style lang="stylus">
$darwinBg = transparentify(#172426, #000, 0.7)
.setting-list-scroll
height 800px
overflow-y auto
overflow-x hidden
margin-right 0!important
.picgo-fade
&-enter,
&-leave,
&-leave-active
opacity 0
&-enter-active,
&-leave-active
transition all 150ms linear
.view-title
color #eee
font-size 20px
text-align center
margin 10px auto
#main-page
height 100%
.qrcode-dialog
.qrcode-container
display flex
justify-content center
.el-dialog__body
padding-top 10px
.copy-picbed-config
margin-left 10px
.fake-title-bar
-webkit-app-region drag
height h = 22px
width 100%
text-align center
color #eee
font-size 12px
line-height h
position fixed
z-index 100
&.darwin
background transparent
background-image linear-gradient(
to right,
transparent 0%,
transparent 167px,
$darwinBg 167px,
$darwinBg 100%
)
.fake-title-bar__title
padding-left 167px
.handle-bar
position absolute
top 2px
right 4px
z-index 10000
-webkit-app-region no-drag
.el-icon
cursor pointer
font-size 16px
margin-left 5px
.el-icon.minus
&:hover
color #409EFF
.el-icon.close
&:hover
color #F15140
.el-icon.plus
&:hover
color #69C282
.main-wrapper
&.darwin
background $darwinBg
.side-bar-menu
position fixed
height calc(100vh - 22px)
overflow-x hidden
overflow-y auto
width 142px
.info-window
cursor pointer
position fixed
bottom 4px
left 4px
cursor poiter
color #878d99
transition .2s all ease-in-out
&:hover
color #409EFF
.el-menu
border-right none
background transparent
width 142px
&-item
color #eee
position relative
&:focus,
&:hover
color #fff
background transparent
&.is-active
color active-color = #409EFF
&:before
content ''
position absolute
width 1px
height 20px
right 0
top 18px
background active-color
.el-sub-menu__title
color #eee
&:hover
background transparent
span
color #fff
.el-sub-menu
.el-menu-item
min-width 142px
&.is-active
&:before
top 16px
.main-content
padding-top 22px
position relative
height calc(100vh - 22px)
z-index 10
.el-dialog__body
padding 20px
.support
text-align center
&-title
text-align center
color #878d99
.align-center
input
text-align center
*::-webkit-scrollbar
width 2px
height 8px
*::-webkit-scrollbar-thumb
border-radius 4px
background #6f6f6f
*::-webkit-scrollbar-track
background-color transparent
<style>
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
font-weight: 400;
--color-text-primary: #1d1d1f;
--color-text-secondary: #6e6e73;
--color-text-tertiary: #86868b;
--color-background-primary: #ffffff;
--color-background-secondary: #f5f5f7;
--color-background-tertiary: #fbfbfd;
--color-surface: rgba(255, 255, 255, 0.8);
--color-surface-elevated: rgba(255, 255, 255, 0.95);
--color-border: rgba(0, 0, 0, 0.1);
--color-border-secondary: rgba(0, 0, 0, 0.05);
--color-primary: #6366f1;
--color-primary-hover: #4f46e5;
--color-accent: #007aff;
--color-accent-hover: #0056b3;
--color-success: #34c759;
--color-warning: #ff9500;
--color-danger: #ff3b30;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08), 0 4px 6px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1), 0 10px 10px rgba(0, 0, 0, 0.04);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
--radius-2xl: 20px;
--transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1);
--transition-medium: 0.25s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 0.35s cubic-bezier(0.4, 0, 0.2, 1);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
:root.dark,
:root.auto.dark {
--color-text-primary: #f5f5f7;
--color-text-secondary: #a1a1a6;
--color-text-tertiary: #86868b;
--color-background-primary: #000000;
--color-background-secondary: #1c1c1e;
--color-background-tertiary: #2c2c2e;
--color-surface: rgba(28, 28, 30, 0.8);
--color-surface-elevated: rgba(44, 44, 46, 0.95);
--color-border: rgba(255, 255, 255, 0.1);
--color-border-secondary: rgba(255, 255, 255, 0.05);
--color-primary: #6366f1;
--color-primary-hover: #818cf8;
--color-accent: #0a84ff;
--color-accent-hover: #409cff;
}
:root.dark,
:root.auto.dark {
.nav-item {
color: var(--color-text-secondary);
}
.nav-item.active {
color: var(--color-accent);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--color-text-primary);
}
p,
span,
div {
color: inherit;
}
svg {
color: inherit;
}
input,
select,
textarea {
background: var(--color-surface);
border-color: var(--color-border);
color: var(--color-text-primary);
}
input::placeholder,
textarea::placeholder {
color: var(--color-text-tertiary);
}
button {
color: var(--color-text-primary);
border-color: var(--color-border);
}
button:hover {
background: var(--color-surface-elevated);
}
}
body {
color: var(--color-text-primary);
background-color: var(--color-background-primary);
font-family: inherit;
overflow: hidden;
}
.app-container {
position: relative;
height: 100vh;
display: flex;
overflow: hidden;
background-color: var(--color-background-primary);
padding-top: 32px;
}
.app-background {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
}
.bg-gradient-primary {
position: absolute;
top: -50%;
right: -30%;
width: 80%;
height: 80%;
background: radial-gradient(circle, rgba(0, 122, 255, 0.05) 0%, transparent 70%);
border-radius: 50%;
}
.bg-gradient-secondary {
position: absolute;
bottom: -40%;
left: -20%;
width: 60%;
height: 60%;
background: radial-gradient(circle, rgba(175, 82, 222, 0.03) 0%, transparent 70%);
border-radius: 50%;
}
.main-content {
position: relative;
z-index: 1;
flex: 1;
height: 100vh;
overflow: scroll;
background-color: var(--color-background-secondary);
scrollbar-width: none;
-ms-overflow-style: none;
}
.main-content::-webkit-scrollbar {
display: none;
}
.content-container {
height: 100%;
padding: 0.3 rem;
max-width: none;
margin: 0;
}
.page-enter-from {
opacity: 0;
transform: translateY(24px) scale(0.98);
}
.page-leave-to {
opacity: 0;
transform: translateY(-8px) scale(1.02);
}
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: 6px;
border: 3px solid var(--color-background-primary);
transition: background-color var(--transition-fast);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--color-text-tertiary);
}
::-webkit-scrollbar-corner {
background: var(--color-background-primary);
}
::selection {
background-color: rgba(0, 122, 255, 0.2);
color: var(--color-text-primary);
}
:focus {
outline: none;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>

View File

@@ -34,6 +34,7 @@ app.config.globalProperties.sendRPC = window.electron.sendRPC
app.config.globalProperties.sendToMain = window.electron.sendToMain
const i18n = createI18n<MessageSchema, 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
@@ -58,3 +59,7 @@ app.use(pinia)
app.use(hljsVuePlugin)
app.use(VueVideoPlayer)
app.mount('#app')
export {
i18n
}

View File

@@ -1,8 +1,22 @@
import { useI18n } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json'
import { IStringKeyMap } from '#/types/types'
type MessageSchema = typeof en
const { t } = useI18n()
const i18n = createI18n<MessageSchema, 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
en,
'zh-CN': zhCN,
'zh-TW': zhTW
}
})
const { t } = i18n.global
const defaultBaseRule = (name: string) => {
return [
{

View File

@@ -1,10 +1,24 @@
import { useI18n } from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import en from '@/i18n/locales/en.json'
import zhCN from '@/i18n/locales/zh-CN.json'
import zhTW from '@/i18n/locales/zh-TW.json'
import { IStringKeyMap } from '#/types/types'
import { AliyunAreaCodeName, QiniuAreaCodeName, TencentAreaCodeName } from './bucketConfigCons'
type MessageSchema = typeof en
const { t } = useI18n()
const i18n = createI18n<MessageSchema, 'en' | 'zh-CN' | 'zh-TW'>({
legacy: false,
locale: localStorage.getItem('currentLanguage') || 'zh-CN',
fallbackLocale: 'zh-CN',
messages: {
en,
'zh-CN': zhCN,
'zh-TW': zhTW
}
})
const { t } = i18n.global
export const newBucketConfig: IStringKeyMap = {
tcyun: {

View File

@@ -1,35 +0,0 @@
<template>
<webview
:src="srcUrl"
disablewebsecurity
allowpopups
autosize="on"
scrollbars="none"
style="width: 100%; height: 100%"
/>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { getConfig } from '@/utils/dataSender'
import { II18nLanguage } from '#/types/enum'
import { configPaths } from '#/utils/configPaths'
const srcUrl = ref('https://piclist.cn/app.html')
const updateUrl = async () => {
const lang = (await getConfig(configPaths.settings.language)) || II18nLanguage.ZH_CN
srcUrl.value = lang === II18nLanguage.ZH_CN ? 'https://piclist.cn/app.html' : 'https://piclist.cn/en/app.html'
}
onMounted(() => {
updateUrl()
})
</script>
<script lang="ts">
export default {
name: 'DocumentPage'
}
</script>

View File

@@ -1,110 +1,122 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import MainPage from '@/layouts/Main.vue'
import ManageBucketPage from '@/manage/pages/BucketPage.vue'
import ManageEmptyPage from '@/manage/pages/EmptyPage.vue'
import ManageLoginPage from '@/manage/pages/LogInPage.vue'
import ManageMainPage from '@/manage/pages/ManageMain.vue'
import ManageSettingPage from '@/manage/pages/ManageSetting.vue'
import GalleryPage from '@/pages/Gallery.vue'
import MiniPage from '@/pages/MiniPage.vue'
import PicBedsPage from '@/pages/picbeds/index.vue'
import SettingPage from '@/pages/PicGoSetting.vue'
import PluginPage from '@/pages/Plugin.vue'
import RenamePage from '@/pages/RenamePage.vue'
import ShortKeyPage from '@/pages/ShortKey.vue'
import Toolbox from '@/pages/Toolbox.vue'
import TrayPage from '@/pages/TrayPage.vue'
import UploadPage from '@/pages/Upload.vue'
import UploaderConfigPage from '@/pages/UploaderConfigPage.vue'
import * as config from '@/router/config'
export default createRouter({
history: createWebHashHistory(),
history: createWebHistory(),
routes: [
{
path: '/',
name: config.TRAY_PAGE,
component: () => import('@/pages/TrayPage.vue')
component: TrayPage
},
{
path: '/rename-page',
name: config.RENAME_PAGE,
component: () => import('@/pages/RenamePage.vue')
component: RenamePage
},
{
path: '/mini-page',
name: config.MINI_PAGE,
component: () => import('@/pages/MiniPage.vue')
component: MiniPage
},
{
path: '/main-page',
name: config.MAIN_PAGE,
component: () => import('@/layouts/Main.vue'),
component: MainPage,
children: [
{
path: 'upload',
component: () => import('@/pages/Upload.vue'),
component: UploadPage,
name: config.UPLOAD_PAGE
},
{
path: 'manage-main-page',
name: config.MANAGE_MAIN_PAGE,
component: () => import('@/manage/pages/ManageMain.vue'),
component: ManageMainPage,
children: [
{
path: '',
name: config.MANAGE_EMPTY_PAGE,
component: () => import('@/manage/pages/EmptyPage.vue')
component: ManageEmptyPage
},
{
path: 'manage-setting-page',
name: config.MANAGE_SETTING_PAGE,
component: () => import('@/manage/pages/ManageSetting.vue')
component: ManageSettingPage
},
{
path: 'manage-bucket-page',
name: config.MANAGE_BUCKET_PAGE,
component: () => import('@/manage/pages/BucketPage.vue')
component: ManageBucketPage
}
]
},
{
path: 'manage-login-page',
name: config.MANAGE_LOGIN_PAGE,
component: () => import('@/manage/pages/LogInPage.vue')
component: ManageLoginPage
},
{
path: 'picbeds/:type/:configId?',
name: config.PICBEDS_PAGE,
component: () => import('@/pages/picbeds/index.vue')
component: PicBedsPage
},
{
path: 'gallery',
component: () => import('@/pages/Gallery.vue'),
component: GalleryPage,
name: config.GALLERY_PAGE,
meta: {
keepAlive: true
}
},
{
path: 'setting',
path: 'settings',
name: config.SETTING_PAGE,
component: () => import('@/pages/PicGoSetting.vue')
component: SettingPage
},
{
path: 'plugin',
component: () => import('@/pages/Plugin.vue'),
path: 'plugins',
component: PluginPage,
name: config.PLUGIN_PAGE
},
{
path: 'shortKey',
component: () => import('@/pages/ShortKey.vue'),
component: ShortKeyPage,
name: config.SHORTKEY_PAGE
},
{
path: 'uploader-config-page/:type',
component: () => import('@/pages/UploaderConfigPage.vue'),
component: UploaderConfigPage,
name: config.UPLOADER_CONFIG_PAGE
}
]
},
{
path: '/documents',
component: () => import('@/pages/DocumentPage.vue'),
name: config.DocumentPage
},
{
path: '/toolbox-page',
name: config.TOOLBOX_CONFIG_PAGE,
component: () => import('@/pages/Toolbox.vue')
component: Toolbox
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
redirect: '/main-page/upload'
}
]
})

View File

@@ -1808,6 +1808,13 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
"@headlessui/vue@^1.7.23":
version "1.7.23"
resolved "https://registry.yarnpkg.com/@headlessui/vue/-/vue-1.7.23.tgz#7fe19dbeca35de9e6270c82c78c4864e6a6f7391"
integrity sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==
dependencies:
"@tanstack/vue-virtual" "^3.0.0-beta.60"
"@highlightjs/vue-plugin@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@highlightjs/vue-plugin/-/vue-plugin-2.1.2.tgz#b7deaaa03452da659a39859437ae0c4bca037600"
@@ -3207,6 +3214,18 @@
dependencies:
defer-to-connect "^2.0.1"
"@tanstack/virtual-core@3.13.12":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
integrity sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==
"@tanstack/vue-virtual@^3.0.0-beta.60":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz#a66daac9e6822ce4bcba76a3954937440697c264"
integrity sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==
dependencies:
"@tanstack/virtual-core" "3.13.12"
"@tokenizer/inflate@^0.2.7":
version "0.2.7"
resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.2.7.tgz#32dd9dfc9abe457c89b3d9b760fc0690c85a103b"
@@ -8272,6 +8291,11 @@ lru-cache@^7.14.1, lru-cache@^7.7.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
lucide-vue-next@^0.535.0:
version "0.535.0"
resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.535.0.tgz#4ba16582f6d5f5388e17b77b4c6f23430d4a8a38"
integrity sha512-B79vTrPyVgp+yfnQe0xU0OcA5+xa9fz24psR8z01NZhbmn5VoxfYMwRm5E4iSk9mCRYgyFWqtx8DMp/bydiTcg==
m3u8-parser@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-6.0.0.tgz#e9143313b44f07bb25fdea1c8aac1098d9ada192"