Skip to content

Commit 87de78f

Browse files
committed
NEW! (wip) All settings view!
1 parent 63627d1 commit 87de78f

File tree

3 files changed

+362
-0
lines changed

3 files changed

+362
-0
lines changed

src/react/AllSettingsEditor.tsx

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
import { useSnapshot } from 'valtio'
2+
import { useState, useMemo, useEffect, useRef } from 'react'
3+
import { titleCase } from 'title-case'
4+
import { noCase } from 'change-case'
5+
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
6+
import { options, disabledSettings } from '../optionsStorage'
7+
import { hideCurrentModal, showModal } from '../globalState'
8+
import Screen from './Screen'
9+
import Button from './Button'
10+
import Input from './Input'
11+
import { useIsModalActive } from './utilsApp'
12+
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
13+
14+
export const showAllSettingsEditor = () => {
15+
showModal({ reactType: 'all-settings-editor' })
16+
}
17+
18+
export default () => {
19+
const isModalActive = useIsModalActive('all-settings-editor')
20+
const optionsSnapshot = useSnapshot(options)
21+
const disabledSettingsSnapshot = useSnapshot(disabledSettings)
22+
const [searchTerm, setSearchTerm] = useState('')
23+
const [editingKey, setEditingKey] = useState<string | null>(null)
24+
const [editingValue, setEditingValue] = useState<string>('')
25+
const searchInputRef = useRef<HTMLInputElement>(null)
26+
const dropdownRef = useRef<HTMLDivElement>(null)
27+
28+
const filteredOptions = useMemo(() => {
29+
if (!isModalActive) return []
30+
const search = searchTerm.toLowerCase()
31+
return Object.entries(optionsSnapshot)
32+
.filter(([key]) => {
33+
const keyLower = key.toLowerCase()
34+
const titleKey = titleCase(noCase(key)).toLowerCase()
35+
return keyLower.includes(search) || titleKey.includes(search)
36+
})
37+
.sort(([a], [b]) => a.localeCompare(b))
38+
}, [optionsSnapshot, searchTerm, isModalActive])
39+
40+
useEffect(() => {
41+
if (isModalActive && searchInputRef.current && !isMobile()) {
42+
// Focus search input on open (but not on mobile to avoid keyboard popup)
43+
searchInputRef.current.focus()
44+
}
45+
}, [isModalActive])
46+
47+
// Close dropdown when clicking outside
48+
useEffect(() => {
49+
const handleClickOutside = (event: MouseEvent) => {
50+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
51+
setEditingKey(null)
52+
}
53+
}
54+
if (editingKey) {
55+
document.addEventListener('mousedown', handleClickOutside)
56+
return () => document.removeEventListener('mousedown', handleClickOutside)
57+
}
58+
}, [editingKey])
59+
60+
if (!isModalActive) return null
61+
62+
const handleValueClick = (key: string, value: any, event: React.MouseEvent) => {
63+
if (disabledSettingsSnapshot.value.has(key)) return
64+
event.stopPropagation()
65+
setEditingKey(key)
66+
setEditingValue(String(value))
67+
}
68+
69+
const handleValueChange = (key: string, newValue: any) => {
70+
try {
71+
if (typeof optionsSnapshot[key] === 'boolean') {
72+
options[key] = newValue === 'true' || newValue === true
73+
} else if (typeof optionsSnapshot[key] === 'number') {
74+
const num = parseFloat(String(newValue))
75+
if (!isNaN(num)) {
76+
options[key] = num
77+
}
78+
} else {
79+
options[key] = newValue
80+
}
81+
setEditingKey(null)
82+
} catch (err) {
83+
console.error('Failed to update option:', err)
84+
}
85+
}
86+
87+
const OptionRow = ({ optionKey, value }: { optionKey: string, value: any }) => {
88+
const valueButtonRef = useRef<HTMLDivElement>(null)
89+
const isDisabled = disabledSettingsSnapshot.value.has(optionKey)
90+
const isEditing = editingKey === optionKey
91+
const isBoolean = typeof value === 'boolean'
92+
const isNumber = typeof value === 'number'
93+
const isString = typeof value === 'string'
94+
95+
const renderValueEditor = () => {
96+
if (!isEditing) return null
97+
98+
const dropdownStyle: React.CSSProperties = {
99+
position: 'fixed',
100+
backgroundColor: '#2a2a2a',
101+
border: '1px solid #555',
102+
borderRadius: '4px',
103+
padding: '4px',
104+
zIndex: 10_000,
105+
minWidth: '120px',
106+
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.3)',
107+
}
108+
109+
if (valueButtonRef.current) {
110+
const rect = valueButtonRef.current.getBoundingClientRect()
111+
dropdownStyle.left = `${rect.left}px`
112+
dropdownStyle.top = `${rect.bottom + 4}px`
113+
}
114+
115+
return (
116+
<div ref={dropdownRef} style={dropdownStyle}>
117+
{isBoolean ? (
118+
<>
119+
<div
120+
style={{
121+
padding: '6px 12px',
122+
cursor: 'pointer',
123+
backgroundColor: '#333',
124+
borderRadius: '2px',
125+
marginBottom: '4px',
126+
}}
127+
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#444' }}
128+
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#333' }}
129+
onClick={() => handleValueChange(optionKey, 'true')}
130+
>
131+
true
132+
</div>
133+
<div
134+
style={{
135+
padding: '6px 12px',
136+
cursor: 'pointer',
137+
backgroundColor: '#333',
138+
borderRadius: '2px',
139+
}}
140+
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#444' }}
141+
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#333' }}
142+
onClick={() => handleValueChange(optionKey, 'false')}
143+
>
144+
false
145+
</div>
146+
</>
147+
) : isString ? (
148+
<div style={{ padding: '4px' }}>
149+
<Input
150+
defaultValue={String(value)}
151+
autoFocus
152+
rootStyles={{ width: '100%' }}
153+
onKeyDown={(e) => {
154+
if (e.key === 'Enter') {
155+
handleValueChange(optionKey, e.currentTarget.value)
156+
} else if (e.key === 'Escape') {
157+
setEditingKey(null)
158+
}
159+
}}
160+
onBlur={() => {
161+
// Small delay to allow dropdown clicks to register
162+
setTimeout(() => {
163+
if (editingKey === optionKey) {
164+
handleValueChange(optionKey, editingValue)
165+
}
166+
}, 200)
167+
}}
168+
onChange={(e) => setEditingValue(e.target.value)}
169+
/>
170+
</div>
171+
) : isNumber ? (
172+
<div style={{ padding: '4px' }}>
173+
<Input
174+
type="number"
175+
defaultValue={String(value)}
176+
autoFocus
177+
rootStyles={{ width: '100%' }}
178+
onKeyDown={(e) => {
179+
if (e.key === 'Enter') {
180+
handleValueChange(optionKey, e.currentTarget.value)
181+
} else if (e.key === 'Escape') {
182+
setEditingKey(null)
183+
}
184+
}}
185+
onBlur={() => {
186+
setTimeout(() => {
187+
if (editingKey === optionKey) {
188+
handleValueChange(optionKey, editingValue)
189+
}
190+
}, 200)
191+
}}
192+
onChange={(e) => setEditingValue(e.target.value)}
193+
/>
194+
</div>
195+
) : (
196+
<div style={{ padding: '8px', color: '#999', fontSize: '12px' }}>
197+
Complex type (object/array)
198+
</div>
199+
)}
200+
</div>
201+
)
202+
}
203+
204+
return (
205+
<div
206+
style={{
207+
display: 'flex',
208+
alignItems: 'center',
209+
gap: '8px',
210+
padding: '8px 12px',
211+
backgroundColor: isEditing ? '#333' : isDisabled ? '#1f1f1f' : '#2a2a2a',
212+
borderRadius: '4px',
213+
cursor: isDisabled ? 'not-allowed' : 'pointer',
214+
position: 'relative',
215+
// Mobile-friendly touch targets
216+
minHeight: '44px',
217+
}}
218+
onMouseEnter={(e) => {
219+
if (!isDisabled && !isEditing) {
220+
e.currentTarget.style.backgroundColor = '#333'
221+
}
222+
}}
223+
onMouseLeave={(e) => {
224+
if (!isDisabled && !isEditing) {
225+
e.currentTarget.style.backgroundColor = '#2a2a2a'
226+
}
227+
}}
228+
>
229+
<div
230+
style={{
231+
flex: 1,
232+
minWidth: 0, // Allow text to truncate on mobile
233+
fontSize: '13px',
234+
color: isDisabled ? '#666' : '#fff',
235+
fontWeight: 500,
236+
wordBreak: 'break-word',
237+
}}
238+
>
239+
{titleCase(noCase(optionKey))}
240+
</div>
241+
<div
242+
ref={valueButtonRef}
243+
style={{
244+
fontSize: '12px',
245+
color: typeof value === 'boolean' ? (value ? '#4caf50' : '#f44336') : '#aaa',
246+
padding: '4px 8px',
247+
backgroundColor: isEditing ? '#444' : '#1a1a1a',
248+
borderRadius: '3px',
249+
whiteSpace: 'nowrap',
250+
cursor: isDisabled ? 'not-allowed' : 'pointer',
251+
border: isEditing ? '1px solid #555' : '1px solid transparent',
252+
userSelect: 'none',
253+
// Mobile-friendly touch target
254+
minWidth: '60px',
255+
textAlign: 'center',
256+
}}
257+
onClick={(e) => handleValueClick(optionKey, value, e)}
258+
onTouchStart={(e) => {
259+
// Prevent double-tap zoom on mobile
260+
if (e.touches.length > 1) {
261+
e.preventDefault()
262+
}
263+
}}
264+
>
265+
{typeof value === 'boolean' ? String(value)
266+
: typeof value === 'object' ? JSON.stringify(value).slice(0, 30) + (JSON.stringify(value).length > 30 ? '...' : '')
267+
: String(value)}
268+
</div>
269+
{renderValueEditor()}
270+
</div>
271+
)
272+
}
273+
274+
return (
275+
<Screen title="All Settings" backdrop>
276+
<div style={{
277+
display: 'flex',
278+
flexDirection: 'column',
279+
height: '100%',
280+
width: '100%',
281+
maxHeight: 'calc(100vh - 100px)',
282+
gap: '8px',
283+
}}>
284+
{/* Search input */}
285+
<div style={{
286+
position: 'sticky',
287+
top: 0,
288+
zIndex: 100,
289+
backgroundColor: '#1a1a1a',
290+
padding: '8px 0',
291+
}}>
292+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
293+
<PixelartIcon iconName={pixelartIcons['search']} styles={{ width: '16px', height: '16px', flexShrink: 0 }} />
294+
<Input
295+
ref={searchInputRef}
296+
placeholder="Search options..."
297+
value={searchTerm}
298+
onChange={(e) => setSearchTerm(e.target.value)}
299+
rootStyles={{
300+
flex: 1,
301+
minWidth: 0, // Allow flexbox to shrink properly on mobile
302+
}}
303+
/>
304+
{searchTerm && (
305+
<Button
306+
icon={pixelartIcons['close']}
307+
onClick={() => setSearchTerm('')}
308+
style={{ width: '20px', flexShrink: 0 }}
309+
/>
310+
)}
311+
</div>
312+
<div style={{ fontSize: '11px', color: '#888', marginTop: '4px' }}>
313+
{filteredOptions.length} option{(filteredOptions.length === 1) ? '' : 's'} found
314+
</div>
315+
</div>
316+
317+
{/* Options list */}
318+
<div style={{
319+
overflowY: 'auto',
320+
flex: 1,
321+
display: 'flex',
322+
flexDirection: 'column',
323+
gap: '2px',
324+
paddingRight: '4px',
325+
// Mobile-friendly scrolling
326+
WebkitOverflowScrolling: 'touch',
327+
}}>
328+
{filteredOptions.length === 0 ? (
329+
<div style={{
330+
padding: '20px',
331+
textAlign: 'center',
332+
color: '#888',
333+
fontSize: '14px',
334+
}}>
335+
No options found matching "{searchTerm}"
336+
</div>
337+
) : (
338+
filteredOptions.map(([key, value]) => (
339+
<OptionRow key={key} optionKey={key} value={value} />
340+
))
341+
)}
342+
</div>
343+
344+
{/* Close button */}
345+
<div style={{
346+
position: 'sticky',
347+
bottom: 0,
348+
paddingTop: '8px',
349+
backgroundColor: '#1a1a1a',
350+
}}>
351+
<Button onClick={hideCurrentModal} style={{ width: '100%' }}>
352+
Close
353+
</Button>
354+
</div>
355+
</div>
356+
</Screen>
357+
)
358+
}

src/react/OptionsItems.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Screen from './Screen'
1111
import { showOptionsModal } from './SelectOption'
1212
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
1313
import { reconnectReload } from './AppStatusProvider'
14+
import { showAllSettingsEditor } from './AllSettingsEditor'
1415

1516
type GeneralItem<T extends string | number | boolean> = {
1617
id?: string
@@ -239,6 +240,7 @@ export default ({ items, title, backButtonAction }: Props) => {
239240
<div style={{ position: 'fixed', marginLeft: '-30px', display: 'flex', flexDirection: 'column', gap: 1, }}>
240241
<Button icon={pixelartIcons['close']} onClick={hideAllModals} style={{ color: '#ff5d5d', }} />
241242
<Button icon={pixelartIcons['chevron-left']} onClick={backButtonAction} style={{ color: 'yellow', }} />
243+
<Button icon={pixelartIcons['search']} onClick={showAllSettingsEditor} style={{ color: '#4caf50', }} title="Search all settings" />
242244
</div>
243245

244246
{items.map((element, i) => {

src/reactUi.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import FireRenderer from './react/FireRenderer'
7070
import MonacoEditor from './react/MonacoEditor'
7171
import OverlayModelViewer from './react/OverlayModelViewer'
7272
import CornerIndicatorStats from './react/CornerIndicatorStats'
73+
import AllSettingsEditor from './react/AllSettingsEditor'
7374

7475
const isFirefox = ua.getBrowser().name === 'Firefox'
7576
if (isFirefox) {
@@ -255,6 +256,7 @@ const App = () => {
255256
<ModsPage />
256257
<SelectOption />
257258
<CreditsAboutModal />
259+
<AllSettingsEditor />
258260
<NoModalFoundProvider />
259261
</RobustPortal>
260262
<RobustPortal to={document.body}>

0 commit comments

Comments
 (0)