improve settings desing

This commit is contained in:
SrGooglo 2024-09-11 01:14:24 +00:00
parent 1e23aad5b4
commit 4a38afb45a
16 changed files with 395 additions and 193 deletions

View File

@ -1,20 +1,20 @@
{ {
"app": { "app": {
"label": "Application Settings" "label": "Application"
}, },
"basic": { "basic": {
"label": "Basic Settings" "label": "Basic"
}, },
"security": { "security": {
"label": "Security Settings" "label": "Security"
}, },
"privacy": { "privacy": {
"label": "Privacy Settings" "label": "Privacy"
}, },
"advanced": { "advanced": {
"label": "Advanced Settings" "label": "Advanced"
}, },
"other": { "other": {
"label": "Other Settings" "label": "Other"
} }
} }

View File

@ -0,0 +1,70 @@
import React from "react"
import * as antd from "antd"
import Image from "@components/Image"
import UserBadges from "@components/UserBadges"
import { Icons, createIconRender } from "@components/Icons"
import ContrastYIQ from "@utils/contrastYIQ"
import "./index.less"
const UserShareBadge = (props) => {
const { user } = props
const [loading, setLoading] = React.useState(true)
const [contrastColor, setContrastColor] = React.useState(null)
async function initialize(params) {
setLoading(true)
const contrastYIQ = await ContrastYIQ.fromUrl(user.cover)
setContrastColor(contrastYIQ)
setLoading(false)
}
React.useEffect(() => {
initialize()
}, [])
if (loading) {
return <div className="user-share-badge">
<antd.Skeleton active />
</div>
}
return <div
className="user-share-badge"
style={{
backgroundImage: `url("${user.cover}")`,
color: contrastColor
}}
>
<div className="user-share-badge-info">
<div className="user-share-badge-avatar">
<Image
src={user.avatar}
/>
</div>
<div className="user-share-badge-username">
<h1>
{user.public_name || user.username}
{user.verified && <Icons.verifiedBadge />}
</h1>
<span>
@{user.username}
</span>
</div>
{
user.badges?.length > 0 && <UserBadges user_id={user._id} />
}
</div>
</div>
}
export default UserShareBadge

View File

@ -0,0 +1,60 @@
.user-share-badge {
width: 400px;
height: 240px;
border-radius: 24px;
// background-color: rgb(255, 96, 100);
// background-image: radial-gradient(circle, currentcolor 0%, transparent 110%);
outline: 2px solid var(--border-color);
padding: 20px;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
h1,
h2,
h3,
h4,
h5,
h6,
p,
span {
color: currentColor;
margin: 0;
}
.user-share-badge-info {
display: flex;
flex-direction: column;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
background-color: rgba(var(--bg_color_4), 0.4);
width: fit-content;
padding: 10px;
border-radius: 12px;
.user-share-badge-avatar {
display: flex;
flex-direction: column;
img {
border-radius: 12px;
}
}
.user-share-badge-username {
display: flex;
flex-direction: column;
font-size: 1.3rem;
}
}
}

View File

@ -360,6 +360,14 @@ export default class SettingItemComponent extends React.PureComponent {
})) }))
} }
computeSwitchEnablerDefault = () => {
if (typeof this.props.setting.switchDefault === "function") {
return this.props.setting.switchDefault()
}
return this.props.setting.switchDefault
}
render() { render() {
if (!this.props.setting) { if (!this.props.setting) {
console.error(`Item [${this.props.setting.id}] has no an setting!`) console.error(`Item [${this.props.setting.id}] has no an setting!`)
@ -428,8 +436,9 @@ export default class SettingItemComponent extends React.PureComponent {
{(t) => t(this.props.setting.title ?? this.props.setting.id)} {(t) => t(this.props.setting.title ?? this.props.setting.id)}
</Translation> </Translation>
</h1> </h1>
{ {
this.props.setting.experimental && <antd.Tag> Experimental </antd.Tag> this.props.setting.experimental && <antd.Tag>Experimental</antd.Tag>
} }
</div> </div>
<div className="setting_item_header_description"> <div className="setting_item_header_description">
@ -441,45 +450,50 @@ export default class SettingItemComponent extends React.PureComponent {
</div> </div>
</div> </div>
{ <div className="setting_item_header_actions">
this.props.setting.extraActions && <div className="setting_item_header_actions"> {
{ this.props.setting.extraActions && this.props.setting.extraActions.map((action, index) => {
this.props.setting.extraActions.map((action, index) => { if (typeof action === "function") {
if (typeof action === "function") { return React.createElement(action, {
return React.createElement(action, { ctx: {
ctx: { updateCurrentValue: (updateValue) => this.setState({
updateCurrentValue: (updateValue) => this.setState({ value: updateValue
value: updateValue }),
}), getCurrentValue: () => this.state.value,
getCurrentValue: () => this.state.value, currentValue: this.state.value,
currentValue: this.state.value, dispatchUpdate: this.dispatchUpdate,
dispatchUpdate: this.dispatchUpdate, onUpdateItem: this.onUpdateItem,
onUpdateItem: this.onUpdateItem, processedCtx: this.props.ctx
processedCtx: this.props.ctx },
}, })
}) }
}
const handleOnClick = () => { const handleOnClick = () => {
if (action.onClick) { if (action.onClick) {
action.onClick(finalProps.ctx) action.onClick(finalProps.ctx)
}
} }
}
return <antd.Button return <antd.Button
key={action.id} key={action.id}
id={action.id} id={action.id}
onClick={handleOnClick} onClick={handleOnClick}
icon={action.icon && createIconRender(action.icon)} icon={action.icon && createIconRender(action.icon)}
type={action.type ?? "round"} type={action.type ?? "round"}
disabled={this.props.setting.disabled} disabled={this.props.setting.disabled}
> >
{action.title} {action.title}
</antd.Button> </antd.Button>
}) })
} }
</div>
} {
typeof this.props.setting.onEnabledChange === "function" && <antd.Switch
defaultChecked={this.computeSwitchEnablerDefault()}
onChange={this.props.setting.onEnabledChange}
/>
}
</div>
</div> </div>
<div className="setting_item_content"> <div className="setting_item_content">

View File

@ -45,12 +45,13 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
color: var(--background-color-contrast); color: var(--background-color-contrast);
gap: 5px;
h1 { h1 {
font-size: 1rem; font-size: 1rem;
margin: 0; margin: 0;

View File

@ -55,7 +55,7 @@ const generateMenuItems = () => {
return { return {
key: item.id, key: item.id,
type: "item", type: "item",
label: <div {...item.props}> label: <div {...item.props} className="menu-item-content">
{createIconRender(item.icon ?? "Settings")} {createIconRender(item.icon ?? "Settings")}
{item.label} {item.label}
</div>, </div>,
@ -126,7 +126,9 @@ export default () => {
if (app.layout.tools_bar) { if (app.layout.tools_bar) {
app.layout.tools_bar.toggleVisibility(false) app.layout.tools_bar.toggleVisibility(false)
} }
}, [activeKey])
React.useEffect(() => {
return () => { return () => {
if (app.layout.tools_bar) { if (app.layout.tools_bar) {
app.layout.tools_bar.toggleVisibility(true) app.layout.tools_bar.toggleVisibility(true)

View File

@ -29,6 +29,43 @@
padding: 0 30px; padding: 0 30px;
gap: 3px;
.ant-menu-item-group-list {
display: flex;
flex-direction: column;
width: 100%;
gap: 3px;
}
.menu-item-content {
display: flex;
flex-direction: row;
align-items: center;
height: fit-content;
gap: 10px;
svg {
margin: 0 !important;
}
}
.ant-menu-item {
padding: 0 !important;
padding-inline: 20px !important;
margin: 0;
border-radius: 24px;
.ant-menu-title-content {
height: fit-content;
}
}
.ant-menu-item-danger { .ant-menu-item-danger {
.ant-menu-title-content { .ant-menu-title-content {
svg { svg {
@ -59,7 +96,7 @@
border-radius: 12px; border-radius: 12px;
padding: 20px; padding: 15px;
gap: 15px; gap: 15px;

View File

@ -78,6 +78,10 @@ export default {
width: "100%" width: "100%"
}, },
options: [ options: [
{
label: "Noto Sans",
value: "'Noto Sans', sans-serif"
},
{ {
label: "Inter (Default)", label: "Inter (Default)",
value: "'Inter', sans-serif" value: "'Inter', sans-serif"
@ -117,7 +121,7 @@ export default {
}, },
storaged: false, storaged: false,
}, },
{ {
id: "style.backgroundImage", id: "style.backgroundImage",
group: "aspect", group: "aspect",

View File

@ -88,41 +88,75 @@ export default (props) => {
}, [selectedPreset]) }, [selectedPreset])
return <> return <>
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: "5px",
}}
>
<Icons.MdList
style={{
margin: "0"
}}
/>
<Select
style={{
width: "50%"
}}
value={selectedPreset}
options={options}
onChange={(key) => {
if (key === "new") {
handleCreateNewPreset()
} else {
setSelectedPreset(key)
}
}}
/>
<Button
onClick={handleDeletePreset}
icon={<Icons.MdDelete />}
disabled={selectedPreset === "default"}
/>
</div>
<Sliders <Sliders
{...props} {...props}
/> />
<div
style={{
display: "inline-flex",
flexDirection: "column",
alignItems: "center",
gap: "5px",
width: "100%",
backgroundColor: "rgba(var(--bg_color_3), 0.5)",
padding: "5px 10px",
borderRadius: "12px",
}}
>
<div
style={{
display: "inline-flex",
flexDirection: "row",
alignItems: "center",
gap: "5px",
width: "100%",
}}
>
<Icons.MdList
style={{
margin: "0"
}}
/>
<h4
style={{
margin: "0",
}}
>
Preset
</h4>
</div>
<div
style={{
display: "inline-flex",
flexDirection: "row",
alignItems: "center",
gap: "5px",
width: "100%",
}}
>
<Select
style={{
width: "50%"
}}
value={selectedPreset}
options={options}
onChange={(key) => {
if (key === "new") {
handleCreateNewPreset()
} else {
setSelectedPreset(key)
}
}}
/>
<Button
onClick={handleDeletePreset}
icon={<Icons.MdDelete />}
disabled={selectedPreset === "default"}
/>
</div>
</div>
</> </>
} }

View File

@ -7,14 +7,14 @@ import WidgetItemPreview from "@components/WidgetItemPreview"
import "./index.less" import "./index.less"
export default class WidgetsView extends React.Component { export default class WidgetsManager extends React.Component {
state = { state = {
loadedWidgets: this.props.ctx.currentValue ?? [], loadedWidgets: app.cores.widgets.getInstalled() ?? [],
} }
render() { render() {
return <div className="widgets_load"> return <div className="widgets-manager">
<div className="widgets_load_list"> <div className="widgets-manager-list">
{ {
Array.isArray(this.state.loadedWidgets) && this.state.loadedWidgets.map((manifest) => { Array.isArray(this.state.loadedWidgets) && this.state.loadedWidgets.map((manifest) => {
return <React.Fragment> return <React.Fragment>
@ -52,7 +52,7 @@ export default class WidgetsView extends React.Component {
icon={<Icons.Plus />} icon={<Icons.Plus />}
onClick={openWidgetsBrowserModal} onClick={openWidgetsBrowserModal}
> >
Add widget Install more
</antd.Button> </antd.Button>
</div> </div>
</div> </div>

View File

@ -1,16 +1,16 @@
.widgets_load { .widgets-manager {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
.widgets_load_list { .widgets-manager-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
.widget_load_list_item { .widgets-manager-list-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -22,14 +22,14 @@
border: 1px var(--border-color) solid; border: 1px var(--border-color) solid;
border-radius: 12px; border-radius: 12px;
.widget_load_list_item_info { .widgets-manager-list-item-info {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 20px; gap: 20px;
} }
.widget_load_list_item_icon { .widgets-manager-list-item-icon {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -47,7 +47,6 @@
object-fit: contain; object-fit: contain;
} }
} }
} }
} }

View File

@ -7,17 +7,35 @@ export default {
group: "app", group: "app",
settings: [ settings: [
{ {
id: "player.allowVolumeOver100", id: "player.gain",
title: "Allow volume over 100%", title: "Gain",
icon: "MdGraphicEq",
group: "general", group: "general",
icon: "MdHearing", description: "Adjust gain for audio output",
description: "Allow volume amplification over 100% (may cause distortion)", component: "Slider",
component: "Switch", props: {
storaged: true, min: 1,
max: 2,
step: 0.1,
marks: {
1: "Normal",
1.5: "+50%",
2: "+100%"
}
},
defaultValue: () => {
return app.cores.player.gain.values().gain
},
onUpdate: (value) => {
app.cores.player.gain.modifyValues({
gain: value
})
},
storaged: false,
}, },
{ {
id: "player.sample_rate", id: "player.sample_rate",
title: "Sample rate", title: "Sample Rate",
icon: "MdHearing", icon: "MdHearing",
group: "general", group: "general",
description: "Internal sample rate for audio output", description: "Internal sample rate for audio output",
@ -52,54 +70,9 @@ export default {
}, },
storaged: false, storaged: false,
}, },
{
id: "player.crossfade",
title: "Crossfade",
icon: "MdSwapHoriz",
group: "general",
description: "Enable crossfade between tracks",
component: "Slider",
props: {
min: 0,
max: 10,
step: 0.1,
marks: {
0: "Off",
1: "1s",
2: "2s",
3: "3s",
4: "4s",
5: "5s",
6: "6s",
7: "7s",
8: "8s",
9: "9s",
10: "10s",
}
},
storaged: true,
disabled: true,
},
{
id: "player.compressor",
title: "Compression",
icon: "MdGraphicEq",
group: "general",
description: "Enable compression for audio output",
component: "Switch",
experimental: true,
beforeSave: (value) => {
if (value) {
app.cores.player.compressor.attach()
} else {
app.cores.player.compressor.detach()
}
},
storaged: true,
},
{ {
id: "player.compressor.values", id: "player.compressor.values",
title: "Compression adjustment", title: "Compression",
icon: "Sliders", icon: "Sliders",
group: "general", group: "general",
description: "Adjust compression values (Warning: may cause distortion when changing values)", description: "Adjust compression values (Warning: may cause distortion when changing values)",
@ -108,6 +81,18 @@ export default {
"player.compressor": true "player.compressor": true
}, },
component: loadable(() => import("./items/player.compressor")), component: loadable(() => import("./items/player.compressor")),
switchDefault: () => {
return app.cores.settings.get("player.compressor")
},
onEnabledChange: (enabled) => {
if (enabled === true) {
app.cores.settings.set("player.compressor", true)
app.cores.player.compressor.attach()
} else {
app.cores.settings.set("player.compressor", false)
app.cores.player.compressor.detach()
}
},
props: { props: {
valueFormat: (value) => `${value}dB`, valueFormat: (value) => `${value}dB`,
sliders: [ sliders: [
@ -149,7 +134,7 @@ export default {
extraActions: [ extraActions: [
{ {
id: "reset", id: "reset",
title: "Reset", title: "Default",
icon: "MdRefresh", icon: "MdRefresh",
onClick: async (ctx) => { onClick: async (ctx) => {
const values = await app.cores.player.compressor.resetDefaultValues() const values = await app.cores.player.compressor.resetDefaultValues()
@ -165,33 +150,7 @@ export default {
}, },
storaged: false, storaged: false,
}, },
{
id: "player.gain",
title: "Gain",
icon: "MdGraphicEq",
group: "general",
description: "Adjust gain for audio output",
component: "Slider",
props: {
min: 1,
max: 2,
step: 0.1,
marks: {
1: "Off",
1.5: "50%",
2: "100%"
}
},
defaultValue: () => {
return app.cores.player.gain.values().gain
},
onUpdate: (value) => {
app.cores.player.gain.modifyValues({
gain: value
})
},
storaged: false,
},
{ {
id: "player.eq", id: "player.eq",
title: "Equalizer", title: "Equalizer",
@ -211,7 +170,9 @@ export default {
} }
}, },
], ],
usePadding: false, dependsOn: {
"player.equalizer": true
},
props: { props: {
valueFormat: (value) => `${value}dB`, valueFormat: (value) => `${value}dB`,
marks: [ marks: [

View File

@ -1,8 +1,14 @@
import { Switch } from "antd"
import SlidersWithPresets from "../../../components/slidersWithPresets" import SlidersWithPresets from "../../../components/slidersWithPresets"
export default (props) => { export default (props) => {
return <SlidersWithPresets return <SlidersWithPresets
{...props} {...props}
controller={app.cores.player.compressor} controller={app.cores.player.compressor}
extraHeaderItems={[
<Switch
onChange={props.onEnabledChange}
/>
]}
/> />
} }

View File

@ -3,15 +3,16 @@ import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import NFCModel from "comty.js/models/nfc" import NFCModel from "comty.js/models/nfc"
import StepsContext from "./context"
import { Icons } from "@components/Icons" import { Icons } from "@components/Icons"
import UserShareBadge from "@components/UserShareBadge"
import CheckRegister from "./steps/check_register" import CheckRegister from "./steps/check_register"
import DataEditor from "./steps/data_editor" import DataEditor from "./steps/data_editor"
import TagWritter from "./steps/tag_writter" import TagWritter from "./steps/tag_writter"
import Success from "./steps/success" import Success from "./steps/success"
import StepsContext from "./context"
import "./index.less" import "./index.less"
const RegisterNewTagSteps = [ const RegisterNewTagSteps = [
@ -306,14 +307,29 @@ const TapShareRender = () => {
<Icons.MdSpoke /> Registered Tags <Icons.MdSpoke /> Registered Tags
</h1> </h1>
</div> </div>
{ {
app.cores.nfc.scanning && <span className="tip"> app.cores.nfc.scanning && <span className="tip">
<Icons.MdInfo /> You can quickly edit your tags by tapping them. <Icons.MdInfo /> You can quickly edit your tags by tapping them.
</span> </span>
} }
<OwnTags /> <OwnTags />
</div> </div>
<div className="tap-share-field">
<div className="tap-share-field_header">
<h1>
<Icons.MdBadge /> Your Badge
</h1>
</div>
<UserShareBadge
user={app.userData}
editMode
/>
</div>
{ {
app.isMobile && <antd.Button app.isMobile && <antd.Button
type="primary" type="primary"

View File

@ -1,26 +1,23 @@
import loadable from "@loadable/component" import React from "react"
import WidgetsManager from "../components/widgetsManager"
export default { export default {
id: "widgets", id: "widgets",
icon: "List", icon: "List",
label: "Widgets", label: "Widgets",
group: "app", group: "app",
settings: [ render: () => {
{ React.useEffect(() => {
id: "widgets.urls", if (app.layout.tools_bar) {
title: "Widgets", app.layout.tools_bar.toggleVisibility(true)
group: "general", }
icon: "List", }, [])
component: loadable(() => import("../components/widgetsView")),
defaultValue: () => {
if (typeof app.cores.widgets === "undefined") {
return []
}
return app.cores.widgets.getInstalled() return <div>
}, <h1>Widgets</h1>
reloadValueOnUpdateEvent: "widgets:update",
storaged: false, <WidgetsManager />
} </div>
] },
} }

View File

@ -1,6 +1,7 @@
/* Selectable fonts for users */ /* Selectable fonts for users */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Varela+Round&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
/* Required secondary fonts */ /* Required secondary fonts */
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap'); @import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap');