@ash-qw/dsh-theme-prts 0.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +24 -0
- package/README.en.md +103 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +7466 -0
- package/lib/index.js +3 -0
- package/package.json +64 -0
- package/src/assets/faction-emblems/lungmen.png +0 -0
- package/src/assets/faction-emblems/penguin-logistics.png +0 -0
- package/src/assets/faction-emblems/reunion.png +0 -0
- package/src/assets/faction-emblems/rhine-lab.png +0 -0
- package/src/assets/faction-emblems/rhodes-island.png +0 -0
- package/src/assets/prts-original-avatar.png +0 -0
- package/src/assets/rhodes-dsh.svg +6 -0
- package/src/assets/topographic-map.svg +72 -0
- package/src/assets/topographic-tech.svg +19 -0
- package/src/client/assistant-glass-adapter.js +231 -0
- package/src/client/composer-glass-adapter.js +210 -0
- package/src/client/conversation-control-adapter.js +109 -0
- package/src/client/conversation-scale-adapter.js +2243 -0
- package/src/client/facility-geometry.js +60 -0
- package/src/client/facility-vector.js +230 -0
- package/src/client/floating-glass-adapter.js +177 -0
- package/src/client/host-geometry-adapter.js +123 -0
- package/src/client/index.js +359 -0
- package/src/client/layout-stability-diagnostics.js +162 -0
- package/src/client/operations-shell-v2.js +248 -0
- package/src/client/particle-field-adapter.js +1840 -0
- package/src/client/preferences.js +239 -0
- package/src/client/rc7-adapter.js +83 -0
- package/src/client/resize-shield-adapter.js +64 -0
- package/src/client/settings-page.js +62 -0
- package/src/client/sidebar-control-adapter.js +317 -0
- package/src/client/startup-sequence.js +366 -0
- package/src/client/status-projection.js +79 -0
- package/src/client/terrain-motion-adapter.js +188 -0
- package/src/client/theme-controller-v2.js +134 -0
- package/src/client/theme-controller.js +119 -0
- package/src/client/theme-settings-workbench.js +583 -0
- package/src/client/to-bottom-adapter.js +85 -0
- package/src/client/ui-store.js +23 -0
- package/src/index.js +4 -0
- package/src/styles/conversation-scale.css +471 -0
- package/src/styles/facility-vector.css +292 -0
- package/src/styles/prts.css +1223 -0
- package/src/styles/refinement.css +429 -0
- package/src/styles/stabilization.css +161 -0
- package/src/styles/startup-sequence.css +496 -0
- package/src/styles/theme-settings-workbench.css +803 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
export const PRTS_STORAGE_KEY = 'dsh.ui.prts.v1'
|
|
2
|
+
|
|
3
|
+
export const CONVERSATION_SCALE_DISTANCE_MIN = 16
|
|
4
|
+
export const CONVERSATION_SCALE_DISTANCE_MAX = 240
|
|
5
|
+
export const CONVERSATION_SCALE_DISTANCE_STEP = 8
|
|
6
|
+
export const CONVERSATION_SCALE_DISTANCE_DEFAULT = 96
|
|
7
|
+
|
|
8
|
+
export const CONVERSATION_SCALE_FOCUS_CONTRAST_MIN = 0
|
|
9
|
+
export const CONVERSATION_SCALE_FOCUS_CONTRAST_MAX = 100
|
|
10
|
+
export const CONVERSATION_SCALE_FOCUS_CONTRAST_STEP = 10
|
|
11
|
+
export const CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT = 70
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_PREFERENCES = Object.freeze({
|
|
14
|
+
version: 6,
|
|
15
|
+
enabled: false,
|
|
16
|
+
preset: 'standard-tactical',
|
|
17
|
+
texture: 'full',
|
|
18
|
+
glass: 'standard',
|
|
19
|
+
motion: 'system',
|
|
20
|
+
bootAnimation: true,
|
|
21
|
+
conversationParticleDensity: 'sparse',
|
|
22
|
+
heroParticleDensity: 'light',
|
|
23
|
+
particlePattern: 'orthogonal',
|
|
24
|
+
conversationScaleMaxDistance: CONVERSATION_SCALE_DISTANCE_DEFAULT,
|
|
25
|
+
conversationScaleFocusContrast: CONVERSATION_SCALE_FOCUS_CONTRAST_DEFAULT,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const VISUAL_PRESETS = Object.freeze({
|
|
29
|
+
'standard-tactical': Object.freeze({
|
|
30
|
+
texture: 'full',
|
|
31
|
+
glass: 'standard',
|
|
32
|
+
motion: 'system',
|
|
33
|
+
conversationParticleDensity: 'sparse',
|
|
34
|
+
heroParticleDensity: 'light',
|
|
35
|
+
particlePattern: 'orthogonal',
|
|
36
|
+
}),
|
|
37
|
+
'clear-glass': Object.freeze({
|
|
38
|
+
texture: 'restrained',
|
|
39
|
+
glass: 'clear',
|
|
40
|
+
motion: 'system',
|
|
41
|
+
conversationParticleDensity: 'sparse',
|
|
42
|
+
heroParticleDensity: 'light',
|
|
43
|
+
particlePattern: 'orthogonal',
|
|
44
|
+
}),
|
|
45
|
+
'quiet-reading': Object.freeze({
|
|
46
|
+
texture: 'restrained',
|
|
47
|
+
glass: 'soft',
|
|
48
|
+
motion: 'reduced',
|
|
49
|
+
conversationParticleDensity: 'sparse',
|
|
50
|
+
heroParticleDensity: 'light',
|
|
51
|
+
particlePattern: 'orthogonal',
|
|
52
|
+
}),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
export const PARTICLE_DETAIL_LEVELS = Object.freeze({
|
|
57
|
+
compact: Object.freeze({ conversationParticleDensity: 'sparse', heroParticleDensity: 'light' }),
|
|
58
|
+
standard: Object.freeze({ conversationParticleDensity: 'light', heroParticleDensity: 'standard' }),
|
|
59
|
+
precise: Object.freeze({ conversationParticleDensity: 'dense', heroParticleDensity: 'ultra' }),
|
|
60
|
+
})
|
|
61
|
+
export const PREFERENCE_GROUPS = Object.freeze({
|
|
62
|
+
background: Object.freeze(['texture']),
|
|
63
|
+
particles: Object.freeze(['conversationParticleDensity', 'heroParticleDensity']),
|
|
64
|
+
material: Object.freeze(['glass']),
|
|
65
|
+
accessibility: Object.freeze(['motion', 'bootAnimation']),
|
|
66
|
+
navigation: Object.freeze(['conversationScaleMaxDistance', 'conversationScaleFocusContrast']),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const TEXTURES = new Set(['off', 'restrained', 'full'])
|
|
70
|
+
const GLASS_STRENGTHS = new Set(['off', 'soft', 'standard', 'clear'])
|
|
71
|
+
const MOTIONS = new Set(['system', 'reduced'])
|
|
72
|
+
const PARTICLE_DENSITIES = new Set(['sparse', 'light', 'standard', 'dense', 'ultra'])
|
|
73
|
+
const PRESETS = new Set([...Object.keys(VISUAL_PRESETS), 'custom'])
|
|
74
|
+
const PRESET_LINKED_KEYS = new Set(Object.keys(VISUAL_PRESETS['standard-tactical']))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
function matchedVisualPreset(value) {
|
|
78
|
+
for (const [preset, settings] of Object.entries(VISUAL_PRESETS)) {
|
|
79
|
+
if ([...PRESET_LINKED_KEYS].every(key => value[key] === settings[key])) return preset
|
|
80
|
+
}
|
|
81
|
+
return 'custom'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveParticleDetail(value) {
|
|
85
|
+
const normalized = normalizePreferences(value)
|
|
86
|
+
const exact = Object.entries(PARTICLE_DETAIL_LEVELS).find(([, settings]) => (
|
|
87
|
+
normalized.conversationParticleDensity === settings.conversationParticleDensity
|
|
88
|
+
&& normalized.heroParticleDensity === settings.heroParticleDensity
|
|
89
|
+
))
|
|
90
|
+
if (exact) return exact[0]
|
|
91
|
+
const rank = { sparse: 0, light: 1, standard: 2, dense: 3, ultra: 4 }
|
|
92
|
+
const score = ((rank[normalized.conversationParticleDensity] ?? 0) + (rank[normalized.heroParticleDensity] ?? 1)) / 2
|
|
93
|
+
return score < 1.5 ? 'compact' : score < 3 ? 'standard' : 'precise'
|
|
94
|
+
}
|
|
95
|
+
function booleanOr(value, fallback) {
|
|
96
|
+
return typeof value === 'boolean' ? value : fallback
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function enumOr(value, allowed, fallback) {
|
|
100
|
+
return allowed.has(value) ? value : fallback
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scaleDistanceOr(value, fallback) {
|
|
104
|
+
const numeric = Number(value)
|
|
105
|
+
if (!Number.isFinite(numeric)) return fallback
|
|
106
|
+
const clamped = Math.min(CONVERSATION_SCALE_DISTANCE_MAX, Math.max(CONVERSATION_SCALE_DISTANCE_MIN, numeric))
|
|
107
|
+
return Math.round(clamped / CONVERSATION_SCALE_DISTANCE_STEP) * CONVERSATION_SCALE_DISTANCE_STEP
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scaleFocusContrastOr(value, fallback) {
|
|
111
|
+
const numeric = Number(value)
|
|
112
|
+
if (!Number.isFinite(numeric)) return fallback
|
|
113
|
+
const clamped = Math.min(CONVERSATION_SCALE_FOCUS_CONTRAST_MAX, Math.max(CONVERSATION_SCALE_FOCUS_CONTRAST_MIN, numeric))
|
|
114
|
+
return Math.round(clamped / CONVERSATION_SCALE_FOCUS_CONTRAST_STEP) * CONVERSATION_SCALE_FOCUS_CONTRAST_STEP
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeGlass(input) {
|
|
118
|
+
if (input.glassEnabled === false || input.glass === 'off') return 'off'
|
|
119
|
+
if (input.glass === 'liquid') return 'clear'
|
|
120
|
+
return enumOr(input.glass, GLASS_STRENGTHS, DEFAULT_PREFERENCES.glass)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function legacyParticleDensities(input) {
|
|
124
|
+
const source = input.particleDensities !== null
|
|
125
|
+
&& typeof input.particleDensities === 'object'
|
|
126
|
+
&& !Array.isArray(input.particleDensities)
|
|
127
|
+
? input.particleDensities
|
|
128
|
+
: null
|
|
129
|
+
const selected = source?.[input.particlePattern]
|
|
130
|
+
return {
|
|
131
|
+
conversation: input.conversationParticleDensity ?? selected?.conversation,
|
|
132
|
+
hero: input.heroParticleDensity ?? selected?.hero,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function normalizePreferences(value) {
|
|
137
|
+
const input = value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
138
|
+
const migratedPreset = input.version === 1 && !Object.hasOwn(input, 'preset')
|
|
139
|
+
? 'custom'
|
|
140
|
+
: input.preset
|
|
141
|
+
const particlePreset = VISUAL_PRESETS[input.preset] ?? DEFAULT_PREFERENCES
|
|
142
|
+
const legacyDensities = legacyParticleDensities(input)
|
|
143
|
+
const normalized = {
|
|
144
|
+
version: 6,
|
|
145
|
+
enabled: booleanOr(input.enabled, DEFAULT_PREFERENCES.enabled),
|
|
146
|
+
preset: enumOr(migratedPreset, PRESETS, DEFAULT_PREFERENCES.preset),
|
|
147
|
+
texture: enumOr(input.texture, TEXTURES, DEFAULT_PREFERENCES.texture),
|
|
148
|
+
glass: normalizeGlass(input),
|
|
149
|
+
motion: input.motion === 'full' ? 'system' : enumOr(input.motion, MOTIONS, DEFAULT_PREFERENCES.motion),
|
|
150
|
+
bootAnimation: booleanOr(input.bootAnimation, DEFAULT_PREFERENCES.bootAnimation),
|
|
151
|
+
conversationParticleDensity: enumOr(legacyDensities.conversation, PARTICLE_DENSITIES, particlePreset.conversationParticleDensity),
|
|
152
|
+
heroParticleDensity: enumOr(legacyDensities.hero, PARTICLE_DENSITIES, particlePreset.heroParticleDensity),
|
|
153
|
+
particlePattern: 'orthogonal',
|
|
154
|
+
conversationScaleMaxDistance: scaleDistanceOr(
|
|
155
|
+
input.conversationScaleMaxDistance,
|
|
156
|
+
DEFAULT_PREFERENCES.conversationScaleMaxDistance,
|
|
157
|
+
),
|
|
158
|
+
conversationScaleFocusContrast: scaleFocusContrastOr(
|
|
159
|
+
input.conversationScaleFocusContrast,
|
|
160
|
+
DEFAULT_PREFERENCES.conversationScaleFocusContrast,
|
|
161
|
+
),
|
|
162
|
+
}
|
|
163
|
+
normalized.preset = matchedVisualPreset(normalized)
|
|
164
|
+
return normalized
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function applyVisualPreset(value, preset) {
|
|
168
|
+
const current = normalizePreferences(value)
|
|
169
|
+
const settings = VISUAL_PRESETS[preset]
|
|
170
|
+
if (!settings) return { ...current, preset: 'custom' }
|
|
171
|
+
return normalizePreferences({
|
|
172
|
+
...current,
|
|
173
|
+
...settings,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
export function applyParticleDetail(value, level) {
|
|
179
|
+
const current = normalizePreferences(value)
|
|
180
|
+
const settings = PARTICLE_DETAIL_LEVELS[level]
|
|
181
|
+
return settings ? normalizePreferences({ ...current, ...settings }) : current
|
|
182
|
+
}
|
|
183
|
+
export function updatePreferenceValue(value, key, nextValue) {
|
|
184
|
+
if (key === 'preset') return applyVisualPreset(value, nextValue)
|
|
185
|
+
if (key === 'particleDetail') return applyParticleDetail(value, nextValue)
|
|
186
|
+
const current = normalizePreferences(value)
|
|
187
|
+
if (key === 'conversationParticleDensity' || key === 'heroParticleDensity') {
|
|
188
|
+
const density = enumOr(nextValue, PARTICLE_DENSITIES, current[key])
|
|
189
|
+
return normalizePreferences({ ...current, [key]: density })
|
|
190
|
+
}
|
|
191
|
+
return normalizePreferences({ ...current, [key]: nextValue })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function resetPreferenceGroup(value, group) {
|
|
195
|
+
const current = normalizePreferences(value)
|
|
196
|
+
const keys = PREFERENCE_GROUPS[group]
|
|
197
|
+
if (!keys) return current
|
|
198
|
+
const reset = { ...current }
|
|
199
|
+
for (const key of keys) reset[key] = DEFAULT_PREFERENCES[key]
|
|
200
|
+
return normalizePreferences(reset)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function loadPreferences(storage) {
|
|
204
|
+
try {
|
|
205
|
+
const raw = storage?.getItem(PRTS_STORAGE_KEY)
|
|
206
|
+
if (raw === null || raw === undefined) return { ...DEFAULT_PREFERENCES }
|
|
207
|
+
const parsed = JSON.parse(raw)
|
|
208
|
+
if (![1, 2, 3, 4, 5, 6].includes(parsed?.version)) return { ...DEFAULT_PREFERENCES }
|
|
209
|
+
return normalizePreferences(parsed)
|
|
210
|
+
} catch {
|
|
211
|
+
return { ...DEFAULT_PREFERENCES }
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function persistPreferences(storage, value) {
|
|
216
|
+
const normalized = normalizePreferences(value)
|
|
217
|
+
let persisted = false
|
|
218
|
+
try {
|
|
219
|
+
if (typeof storage?.setItem === 'function') {
|
|
220
|
+
storage.setItem(PRTS_STORAGE_KEY, JSON.stringify(normalized))
|
|
221
|
+
persisted = true
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// Private browsing or storage policy can make persistence unavailable.
|
|
225
|
+
}
|
|
226
|
+
return { preferences: normalized, persisted }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function savePreferences(storage, value) {
|
|
230
|
+
return persistPreferences(storage, value).preferences
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isSafeMode(search = '') {
|
|
234
|
+
try {
|
|
235
|
+
return new URLSearchParams(search).get('prts-safe') === '1'
|
|
236
|
+
} catch {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function closestCommonContainer(root, nodes) {
|
|
2
|
+
let candidate = nodes[0]?.parentElement
|
|
3
|
+
while (candidate && candidate !== root) {
|
|
4
|
+
if (nodes.every(node => candidate === node || candidate.contains(node))) return candidate
|
|
5
|
+
candidate = candidate.parentElement
|
|
6
|
+
}
|
|
7
|
+
return root && nodes.every(node => root.contains(node)) ? root : null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveRc7Regions(document, report = () => {}) {
|
|
11
|
+
const root = document?.querySelector('[data-slot="root"]')
|
|
12
|
+
const sidebarSlot = root?.querySelector('[data-slot="sidebar"]')
|
|
13
|
+
const conversationSlot = root?.querySelector('[data-slot="conversation"]')
|
|
14
|
+
const detailsSlot = root?.querySelector('[data-slot="details"]')
|
|
15
|
+
if (!root || !sidebarSlot || !conversationSlot || !detailsSlot) {
|
|
16
|
+
report('required layout slots are unavailable', {
|
|
17
|
+
root: Boolean(root), sidebar: Boolean(sidebarSlot),
|
|
18
|
+
conversation: Boolean(conversationSlot), details: Boolean(detailsSlot),
|
|
19
|
+
})
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
const sidebar = sidebarSlot?.parentElement
|
|
23
|
+
const center = conversationSlot?.parentElement
|
|
24
|
+
const details = detailsSlot?.parentElement
|
|
25
|
+
|
|
26
|
+
if (!sidebar || !center || !details) {
|
|
27
|
+
report('required layout region containers are unavailable', {
|
|
28
|
+
sidebar: Boolean(sidebar), conversation: Boolean(center), details: Boolean(details),
|
|
29
|
+
})
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const frame = closestCommonContainer(root, [sidebar, center, details])
|
|
34
|
+
if (!frame) {
|
|
35
|
+
report('common layout container is unavailable')
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { root, frame, sidebar, center, details, sidebarSlot, conversationSlot, detailsSlot }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createRc7Adapter({ document, warn } = {}) {
|
|
43
|
+
const owned = []
|
|
44
|
+
let mounted
|
|
45
|
+
let lastDiagnostic
|
|
46
|
+
const writeWarning = typeof warn === 'function'
|
|
47
|
+
? warn
|
|
48
|
+
: (...args) => document?.defaultView?.console?.warn?.(...args)
|
|
49
|
+
|
|
50
|
+
function reportLayoutIssue(message, details) {
|
|
51
|
+
const signature = `${message}:${JSON.stringify(details ?? {})}`
|
|
52
|
+
if (signature === lastDiagnostic) return
|
|
53
|
+
lastDiagnostic = signature
|
|
54
|
+
writeWarning(`[dsh-theme-prts] ${message}`, details)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
mount() {
|
|
59
|
+
if (mounted) return mounted
|
|
60
|
+
const regions = resolveRc7Regions(document, reportLayoutIssue)
|
|
61
|
+
if (!regions) return null
|
|
62
|
+
lastDiagnostic = undefined
|
|
63
|
+
for (const [node, value] of [
|
|
64
|
+
[regions.frame, 'frame'],
|
|
65
|
+
[regions.sidebar, 'sessions'],
|
|
66
|
+
[regions.center, 'operation'],
|
|
67
|
+
[regions.details, 'details'],
|
|
68
|
+
]) {
|
|
69
|
+
owned.push([node, node.getAttribute('data-prts-region')])
|
|
70
|
+
node.setAttribute('data-prts-region', value)
|
|
71
|
+
}
|
|
72
|
+
mounted = regions
|
|
73
|
+
return mounted
|
|
74
|
+
},
|
|
75
|
+
dispose() {
|
|
76
|
+
for (const [node, previous] of owned.splice(0).reverse()) {
|
|
77
|
+
if (previous === null) node.removeAttribute('data-prts-region')
|
|
78
|
+
else node.setAttribute('data-prts-region', previous)
|
|
79
|
+
}
|
|
80
|
+
mounted = undefined
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const RESIZE_SHIELD_ATTRIBUTE = 'data-prts-resize-shield'
|
|
2
|
+
const DEFAULT_SETTLE_DELAY = 180
|
|
3
|
+
|
|
4
|
+
export function createResizeShieldAdapter({
|
|
5
|
+
window,
|
|
6
|
+
settleDelay = DEFAULT_SETTLE_DELAY,
|
|
7
|
+
} = {}) {
|
|
8
|
+
let targets = []
|
|
9
|
+
let settleTimer
|
|
10
|
+
let started = false
|
|
11
|
+
|
|
12
|
+
function setActive(active) {
|
|
13
|
+
for (const node of targets) {
|
|
14
|
+
if (!node || node.hasAttribute?.(RESIZE_SHIELD_ATTRIBUTE) === active) continue
|
|
15
|
+
node.toggleAttribute?.(RESIZE_SHIELD_ATTRIBUTE, active)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function clearTimer() {
|
|
20
|
+
if (settleTimer === undefined) return
|
|
21
|
+
window?.clearTimeout?.(settleTimer)
|
|
22
|
+
settleTimer = undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function settle() {
|
|
26
|
+
settleTimer = undefined
|
|
27
|
+
setActive(false)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function onResize() {
|
|
31
|
+
if (!started) return
|
|
32
|
+
setActive(true)
|
|
33
|
+
clearTimer()
|
|
34
|
+
settleTimer = window?.setTimeout?.(settle, settleDelay)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function dispose() {
|
|
38
|
+
clearTimer()
|
|
39
|
+
window?.removeEventListener?.('resize', onResize)
|
|
40
|
+
setActive(false)
|
|
41
|
+
targets = []
|
|
42
|
+
started = false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
start(nextTargets = []) {
|
|
47
|
+
const resolved = [...new Set(nextTargets)].filter(Boolean)
|
|
48
|
+
if (!window || !resolved.length) {
|
|
49
|
+
dispose()
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
if (started) {
|
|
53
|
+
clearTimer()
|
|
54
|
+
setActive(false)
|
|
55
|
+
window.removeEventListener?.('resize', onResize)
|
|
56
|
+
}
|
|
57
|
+
targets = resolved
|
|
58
|
+
started = true
|
|
59
|
+
window.addEventListener?.('resize', onResize, { passive: true })
|
|
60
|
+
return true
|
|
61
|
+
},
|
|
62
|
+
dispose,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function createSettingsPage(React, meta = {}) {
|
|
2
|
+
const h = React.createElement
|
|
3
|
+
|
|
4
|
+
return function PrtsPluginSettingsPage({ useStore, updatePreference }) {
|
|
5
|
+
const { preferences } = useStore(state => ({ preferences: state.preferences }))
|
|
6
|
+
const safeMode = Boolean(meta.safeMode)
|
|
7
|
+
const enabled = Boolean(preferences.enabled) && !safeMode
|
|
8
|
+
const bootAnimation = preferences.bootAnimation !== false
|
|
9
|
+
|
|
10
|
+
const settingRow = ({ key, title, description, checked, disabled = false, onChange }) => h('div', {
|
|
11
|
+
className: 'prts-plugin-settings__row',
|
|
12
|
+
key,
|
|
13
|
+
'data-prts-plugin-setting': key,
|
|
14
|
+
},
|
|
15
|
+
h('span', { className: 'prts-plugin-settings__copy' },
|
|
16
|
+
h('strong', null, title),
|
|
17
|
+
h('small', null, description),
|
|
18
|
+
),
|
|
19
|
+
h('button', {
|
|
20
|
+
type: 'button',
|
|
21
|
+
role: 'switch',
|
|
22
|
+
className: checked ? 'is-active' : '',
|
|
23
|
+
'aria-label': title,
|
|
24
|
+
'aria-checked': String(checked),
|
|
25
|
+
disabled,
|
|
26
|
+
onClick: onChange,
|
|
27
|
+
}, h('span', { 'aria-hidden': true })),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return h('section', {
|
|
31
|
+
className: 'prts-plugin-settings',
|
|
32
|
+
'data-prts-plugin-settings': '',
|
|
33
|
+
'data-prts-settings-enabled': String(enabled),
|
|
34
|
+
'aria-label': 'P.R.T.S. 插件设置',
|
|
35
|
+
},
|
|
36
|
+
h('header', { className: 'prts-plugin-settings__header' },
|
|
37
|
+
h('h3', null, 'P.R.T.S.'),
|
|
38
|
+
h('p', null, '完整外观设置位于主界面左上角的罗德岛徽记。'),
|
|
39
|
+
),
|
|
40
|
+
safeMode && h('p', { className: 'prts-plugin-settings__safe', role: 'status' },
|
|
41
|
+
'安全模式已暂停主题。移除地址中的 ?prts-safe=1 后即可恢复。',
|
|
42
|
+
),
|
|
43
|
+
h('div', { className: 'prts-plugin-settings__list' },
|
|
44
|
+
settingRow({
|
|
45
|
+
key: 'enabled',
|
|
46
|
+
title: '启用 P.R.T.S. 主题',
|
|
47
|
+
description: safeMode ? '安全模式下无法启用主题' : '控制 P.R.T.S. 外观是否加载',
|
|
48
|
+
checked: enabled,
|
|
49
|
+
disabled: safeMode,
|
|
50
|
+
onChange: () => { if (!safeMode) updatePreference('enabled', !preferences.enabled) },
|
|
51
|
+
}),
|
|
52
|
+
settingRow({
|
|
53
|
+
key: 'bootAnimation',
|
|
54
|
+
title: '播放启动动画',
|
|
55
|
+
description: '刷新或启用主题时显示 P.R.T.S. 启动动画',
|
|
56
|
+
checked: bootAnimation,
|
|
57
|
+
onChange: () => updatePreference('bootAnimation', !bootAnimation),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|