Skip to content

Commit 95f0a4b

Browse files
committed
style: beautify the toolbar of option tabs (#487)
1 parent 6a27a8d commit 95f0a4b

File tree

7 files changed

+100
-125
lines changed

7 files changed

+100
-125
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@rsdoctor/rspack-plugin": "^1.1.4",
3535
"@rspack/cli": "^1.3.15",
3636
"@rspack/core": "^1.3.15",
37-
"@swc/core": "^1.12.4",
37+
"@swc/core": "^1.12.5",
3838
"@swc/jest": "^0.2.38",
3939
"@types/chrome": "0.0.326",
4040
"@types/decompress": "^4.2.7",
Lines changed: 80 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,94 @@
11
import { t } from "@app/locale"
22
import { Download, Refresh, Upload } from "@element-plus/icons-vue"
3-
import { ElIcon, ElMessage, ElMessageBox, ElTabPane, ElTabs, TabPaneName } from "element-plus"
3+
import Flex from "@pages/components/Flex"
4+
import { ElLink, ElMessage, ElMessageBox, ElTabPane, ElTabs, ElTooltip, TabPaneName } from "element-plus"
45
import { defineComponent, h, ref, useSlots } from "vue"
56
import { useRouter } from "vue-router"
67
import ContentContainer from "../common/ContentContainer"
78
import { CATE_LABELS, changeQuery, type OptionCategory, parseQuery } from "./common"
8-
import { exportSettings, importSettings, createFileInput } from "./export-import"
9+
import { createFileInput, exportSettings, importSettings } from "./export-import"
910

10-
const resetButtonName = "reset"
11-
const exportButtonName = "export"
12-
const importButtonName = "import"
11+
const TOOLBAR_NAME = "toolbar"
1312

14-
const _default = defineComponent({
15-
emits: {
16-
reset: (_cate: OptionCategory, _callback: () => void) => Promise.resolve(true),
17-
},
18-
setup: (_, ctx) => {
19-
const tab = ref(parseQuery() || 'appearance')
20-
const router = useRouter()
13+
type TooltipProps = {
14+
onReset?: NoArgCallback
15+
}
2116

22-
const handleBeforeLeave = async (activeName: TabPaneName, oldActiveName: TabPaneName): Promise<boolean> => {
23-
if (activeName === resetButtonName) {
24-
const cate: OptionCategory = oldActiveName as OptionCategory
25-
await new Promise<void>(res => ctx.emit('reset', cate, res))
26-
ElMessage.success(t(msg => msg.option.resetSuccess))
27-
return Promise.reject()
28-
} else if (activeName === exportButtonName) {
29-
try {
30-
await exportSettings()
31-
ElMessage.success(t(msg => msg.option.exportSuccess))
32-
} catch (error) {
33-
ElMessage.error('Export failed: ' + (error as Error).message)
34-
}
35-
return Promise.reject()
36-
} else if (activeName === importButtonName) {
37-
try {
38-
const fileContent = await createFileInput()
39-
if (fileContent === null) {
40-
// User cancelled file selection, don't show any message
41-
return Promise.reject()
42-
}
17+
const Toolbar = defineComponent<TooltipProps>(props => {
18+
const handleExport = async () => {
19+
try {
20+
await exportSettings()
21+
ElMessage.success(t(msg => msg.option.exportSuccess))
22+
} catch (error) {
23+
ElMessage.error('Export failed: ' + (error as Error).message)
24+
}
25+
}
26+
27+
const handleImport = async () => {
28+
try {
29+
const fileContent = await createFileInput()
30+
// User cancelled, don't show error message
31+
if (!fileContent) return
32+
await importSettings(fileContent)
33+
ElMessageBox({
34+
message: t(msg => msg.option.importConfirm),
35+
type: "success",
36+
confirmButtonText: t(msg => msg.option.reloadButton),
37+
closeOnPressEscape: false,
38+
closeOnClickModal: false
39+
}).then(() => {
40+
window.location.reload()
41+
}).catch(() => {/* do nothing */ })
42+
} catch (error) {
43+
ElMessage.error(t(msg => msg.option.importError))
44+
}
45+
}
46+
47+
return () => (
48+
<Flex align="center" gap={10} onClick={ev => ev.stopPropagation()}>
49+
<ElTooltip content={t(msg => msg.option.exportButton)}>
50+
<ElLink
51+
icon={Download}
52+
underline="never"
53+
onClick={handleExport}
54+
/>
55+
</ElTooltip>
56+
<ElTooltip content={t(msg => msg.option.importButton)}>
57+
<ElLink
58+
icon={Upload}
59+
underline="never"
60+
onClick={handleImport}
61+
/>
62+
</ElTooltip>
63+
<ElLink
64+
icon={Refresh}
65+
underline="never"
66+
onClick={() => props.onReset?.()}
67+
>
68+
{t(msg => msg.option.resetButton)}
69+
</ElLink>
70+
</Flex>
71+
)
72+
}, {
73+
props: ['onReset']
74+
})
4375

44-
await importSettings(fileContent)
45-
ElMessageBox({
46-
message: t(msg => msg.option.importConfirm),
47-
type: "success",
48-
confirmButtonText: t(msg => msg.option.reloadButton),
49-
closeOnPressEscape: false,
50-
closeOnClickModal: false
51-
}).then(() => {
52-
window.location.reload()
53-
}).catch(() => {/* do nothing */ })
54-
} catch (error) {
55-
ElMessage.error(t(msg => msg.option.importError))
56-
}
76+
type Props = { onReset: (cate: OptionCategory) => Promise<void> | void }
77+
78+
const _default = defineComponent<Props>(
79+
props => {
80+
const tab = ref(parseQuery() ?? 'appearance')
81+
const router = useRouter()
82+
const handleReset = () => props.onReset?.(tab.value)
83+
84+
const handleBeforeLeave = (activeName: TabPaneName): Promise<boolean> => {
85+
if (activeName === TOOLBAR_NAME) {
86+
// do nothing, and never happen
5787
return Promise.reject()
5888
}
5989
// Change the query of current route
6090
changeQuery(activeName as OptionCategory, router)
61-
return true
91+
return Promise.resolve(true)
6292
}
6393
return () => (
6494
<ContentContainer>
@@ -74,46 +104,12 @@ const _default = defineComponent({
74104
</ElTabPane>
75105
))}
76106
<ElTabPane
77-
name={resetButtonName}
78-
v-slots={{
79-
label: () => (
80-
<div>
81-
<ElIcon>
82-
<Refresh />
83-
</ElIcon>
84-
{t(msg => msg.option.resetButton)}
85-
</div>
86-
)
87-
}}
88-
/>
89-
<ElTabPane
90-
name={exportButtonName}
91-
v-slots={{
92-
label: () => (
93-
<div title={t(msg => msg.option.exportButton)}>
94-
<ElIcon>
95-
<Download />
96-
</ElIcon>
97-
</div>
98-
)
99-
}}
100-
/>
101-
<ElTabPane
102-
name={importButtonName}
103-
v-slots={{
104-
label: () => (
105-
<div title={t(msg => msg.option.importButton)}>
106-
<ElIcon>
107-
<Upload />
108-
</ElIcon>
109-
</div>
110-
)
111-
}}
107+
name={TOOLBAR_NAME}
108+
v-slots={{ label: () => <Toolbar onReset={handleReset} /> }}
112109
/>
113110
</ElTabs>
114111
</ContentContainer>
115112
)
116-
}
117-
})
113+
}, { props: ['onReset'] })
118114

119115
export default _default

src/pages/app/components/Option/export-import.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
*/
66

77
import optionHolder from "@service/components/option-holder"
8-
import { exportJson } from "@util/file"
9-
import { deserialize } from "@util/file"
10-
import { defaultOption } from "@util/constant/option"
8+
import { deserialize, exportJson } from "@util/file"
9+
import { mergeObject } from '@util/lang'
1110

1211
export interface ExportedSettings {
1312
version: string
@@ -23,7 +22,7 @@ export async function exportSettings(): Promise<void> {
2322
const exportData: ExportedSettings = {
2423
version: '1.0',
2524
timestamp: Date.now(),
26-
settings
25+
settings,
2726
}
2827

2928
const fileName = `timer-settings-${new Date().toISOString().split('T')[0]}`
@@ -53,27 +52,9 @@ export async function importSettings(jsonString: string): Promise<void> {
5352
async function validateAndMergeSettings(importedSettings: Partial<timer.option.AllOption>): Promise<timer.option.AllOption> {
5453
// Get current user settings as defaults instead of default options
5554
const defaults = await optionHolder.get()
56-
57-
// Merge imported settings with defaults, giving preference to imported values
58-
const mergedSettings: timer.option.AllOption = {
59-
...defaults,
60-
...importedSettings
61-
}
62-
63-
// Ensure critical nested objects exist
64-
if (importedSettings.backupAuths) {
65-
mergedSettings.backupAuths = { ...defaults.backupAuths, ...importedSettings.backupAuths }
66-
}
67-
68-
if (importedSettings.backupLogin) {
69-
mergedSettings.backupLogin = { ...defaults.backupLogin, ...importedSettings.backupLogin }
70-
}
71-
72-
if (importedSettings.backupExts) {
73-
mergedSettings.backupExts = { ...defaults.backupExts, ...importedSettings.backupExts }
74-
}
75-
76-
return mergedSettings
55+
// Delete client name
56+
delete importedSettings['clientName']
57+
return mergeObject(defaults, importedSettings)
7758
}
7859

7960
/**

src/pages/app/components/Option/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@ const _default = defineComponent(() => {
3939
backup: () => <BackupOption ref={paneRefMap.backup} />,
4040
}
4141

42-
const handleReset = async (cate: OptionCategory, callback: () => void) => {
43-
await paneRefMap[cate]?.value?.reset?.()
44-
callback?.()
45-
}
42+
const handleReset = (cate: OptionCategory) => paneRefMap[cate]?.value?.reset?.()
4643

4744
return () => mediaSize.value <= MediaSize.sm
4845
? <Select v-slots={slots} />

src/pages/app/components/Option/style.sass

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,9 @@
99
// Keep the reset button top-right
1010
.el-tabs__nav
1111
float: none !important
12-
#tab-reset, #tab-export, #tab-import
12+
#tab-toolbar
1313
position: absolute
14-
// Optimize the buttons
15-
.el-icon
16-
top: 2px
17-
padding-inline-end: 4px
18-
#tab-reset
1914
right: 0px
20-
#tab-export
21-
right: 76px
22-
#tab-import
23-
right: 112px
2415
.el-tabs__item
2516
font-size: 16px
2617
height: 43px

src/util/file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function exportCsv(titleAndData: any[][], fileName: string) {
2626
* @since 0.0.7
2727
*/
2828
export function exportJson(data: any, fileName: string) {
29-
const jsonStr = JSON.stringify(data)
29+
const jsonStr = JSON.stringify(data, null, 4)
3030
var blob = new Blob([jsonStr], { type: 'text/json' })
3131

3232
exportBlob(blob, fileName + '.json')

src/util/lang.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,14 @@ export const deepCopy = <T = any | null | undefined>(obj: T): T => {
2323
}
2424
})
2525
return deep as T
26+
}
27+
28+
export const mergeObject = <T extends Record<string, any>>(defaults: T, newVal: Partial<T>): T => {
29+
Object.entries(newVal).forEach(([k, v]) => {
30+
if (typeof v === 'object' && !!v && !Array.isArray(v)) {
31+
(defaults as any)[k] = mergeObject(defaults[k], v as Record<string, any>)
32+
}
33+
(defaults as any)[k] = v
34+
})
35+
return defaults
2636
}

0 commit comments

Comments
 (0)