mirror of
https://github.com/PenumbraOS/sdk.git
synced 2026-02-03 17:26:48 -06:00
Support for updating app settings
This commit is contained in:
parent
3273b05719
commit
7af56c4f5d
@ -2,12 +2,19 @@ import { useSettings } from "./hooks/useSettings";
|
||||
import ConnectionStatus from "./components/ConnectionStatus";
|
||||
import SystemStatus from "./components/SystemStatus";
|
||||
import SystemSettings from "./components/SystemSettings";
|
||||
import AppSettings from "./components/AppSettings";
|
||||
import { AppSettings } from "./components/AppSettings";
|
||||
import MABLStatus from "./components/MABLStatus";
|
||||
import ESimSettings from "./components/ESimSettings";
|
||||
|
||||
function App() {
|
||||
const { loading, error, connected, executeAction, actionResults, executionStatus } = useSettings();
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
connected,
|
||||
executeAction,
|
||||
actionResults,
|
||||
executionStatus,
|
||||
} = useSettings();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -36,7 +43,11 @@ function App() {
|
||||
<SystemSettings />
|
||||
<AppSettings />
|
||||
<MABLStatus />
|
||||
<ESimSettings onExecuteAction={executeAction} actionResults={actionResults} executionStatus={executionStatus} />
|
||||
<ESimSettings
|
||||
onExecuteAction={executeAction}
|
||||
actionResults={actionResults}
|
||||
executionStatus={executionStatus}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
LogEntry,
|
||||
ExecutionStatus,
|
||||
} from "../types/settings";
|
||||
import TextInput from "./TextInput";
|
||||
|
||||
interface ActionButtonProps {
|
||||
appId: string;
|
||||
@ -74,11 +75,10 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
handleParameterChange(param.name, parseFloat(e.target.value) || 0),
|
||||
},
|
||||
string: {
|
||||
type: "text" as const,
|
||||
type: "string" as const,
|
||||
value: String(currentValue),
|
||||
placeholder: param.description,
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handleParameterChange(param.name, e.target.value),
|
||||
onChange: (value: string) => handleParameterChange(param.name, value),
|
||||
},
|
||||
};
|
||||
|
||||
@ -96,7 +96,15 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
{param.name}
|
||||
{param.required && <span className="required">*</span>}
|
||||
</span>
|
||||
<input {...inputConfig} />
|
||||
{inputConfig.type === "string" ? (
|
||||
<TextInput
|
||||
value={inputConfig.value}
|
||||
onChange={inputConfig.onChange}
|
||||
placeholder={inputConfig.placeholder}
|
||||
/>
|
||||
) : (
|
||||
<input {...inputConfig} />
|
||||
)}
|
||||
{param.description && (
|
||||
<span className="parameter-description">{param.description}</span>
|
||||
)}
|
||||
|
||||
@ -1,104 +1,105 @@
|
||||
import React from 'react';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import Slider from './Slider';
|
||||
import React from "react";
|
||||
import { useSettings } from "../hooks/useSettings";
|
||||
import ToggleSwitch from "./ToggleSwitch";
|
||||
import Slider from "./Slider";
|
||||
import TextInput from "./TextInput";
|
||||
|
||||
const AppSettings: React.FC = () => {
|
||||
const { allSettings, updateAppSetting } = useSettings();
|
||||
export const AppSettings: React.FC = () => {
|
||||
const { allSettings } = useSettings();
|
||||
|
||||
// Extract app settings (anything that's not 'system')
|
||||
const appSettingsEntries = Object.entries(allSettings).filter(
|
||||
([key]) => key !== 'system'
|
||||
([key]) => key !== "system"
|
||||
);
|
||||
|
||||
if (appSettingsEntries.length === 0) {
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h2>App Settings</h2>
|
||||
<p style={{ color: '#7f8c8d', fontStyle: 'italic' }}>
|
||||
No app settings registered yet. Apps will appear here when they register settings through the SDK.
|
||||
<p style={{ color: "#7f8c8d", fontStyle: "italic" }}>
|
||||
No app settings registered yet. Apps will appear here when they
|
||||
register settings through the SDK.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleAppSettingChange = (fullKey: string, settingKey: string, value: unknown) => {
|
||||
// Parse the full key to get appId and category
|
||||
const parts = fullKey.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const appId = parts[0];
|
||||
const category = parts.slice(1).join('.');
|
||||
updateAppSetting(appId, category, settingKey, value);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSettingControl = (fullKey: string, settingKey: string, value: unknown) => {
|
||||
// Try to determine the setting type from the value
|
||||
if (typeof value === 'boolean') {
|
||||
// Boolean setting
|
||||
return (
|
||||
<ToggleSwitch
|
||||
enabled={value}
|
||||
onChange={(enabled) => handleAppSettingChange(fullKey, settingKey, enabled)}
|
||||
/>
|
||||
);
|
||||
} else if (typeof value === 'number') {
|
||||
// Numeric setting - treat as slider with reasonable defaults
|
||||
const max = Math.max(100, value * 2); // Reasonable max
|
||||
return (
|
||||
<div className="setting-control">
|
||||
<Slider
|
||||
value={value}
|
||||
min={0}
|
||||
max={max}
|
||||
onChange={(newValue) => handleAppSettingChange(fullKey, settingKey, newValue)}
|
||||
/>
|
||||
<span className="status-display">{value}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// String setting or unknown - just display for now
|
||||
return (
|
||||
<span className="status-display" style={{ maxWidth: '200px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{String(value)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h2>App Settings</h2>
|
||||
|
||||
{appSettingsEntries.map(([fullKey, settings]) => {
|
||||
const appName = fullKey.split('.')[0];
|
||||
const categoryName = fullKey.split('.').slice(1).join('.');
|
||||
|
||||
return (
|
||||
<div key={fullKey} style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{
|
||||
color: '#34495e',
|
||||
fontSize: '1rem',
|
||||
marginBottom: '12px',
|
||||
borderBottom: '1px solid #ecf0f1',
|
||||
paddingBottom: '4px'
|
||||
}}>
|
||||
{appName} - {categoryName}
|
||||
</h3>
|
||||
|
||||
{Object.entries(settings).map(([settingKey, value]) => (
|
||||
<div key={settingKey} className="setting-item">
|
||||
<span className="setting-label">{settingKey}</span>
|
||||
{appSettingsEntries.map(([appId, settings]) => (
|
||||
<div key={appId} style={{ marginBottom: "20px" }}>
|
||||
<h3
|
||||
style={{
|
||||
color: "#34495e",
|
||||
fontSize: "1rem",
|
||||
marginBottom: "12px",
|
||||
borderBottom: "1px solid #ecf0f1",
|
||||
paddingBottom: "4px",
|
||||
}}
|
||||
>
|
||||
{appId}
|
||||
</h3>
|
||||
{Object.entries(settings).map(([category, categoryContents]) => {
|
||||
return Object.entries(
|
||||
categoryContents as Record<string, unknown>
|
||||
).map(([property, value]) => (
|
||||
<div key={`${category}.${property}`} className="setting-item">
|
||||
<span className="setting-label">{property}</span>
|
||||
<div className="setting-control">
|
||||
{renderSettingControl(fullKey, settingKey, value)}
|
||||
<SettingControlView
|
||||
appId={appId}
|
||||
category={category}
|
||||
property={property}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
));
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppSettings;
|
||||
const SettingControlView: React.FC<{
|
||||
appId: string;
|
||||
category: string;
|
||||
property: string;
|
||||
value: unknown;
|
||||
}> = ({ appId, category, property, value }) => {
|
||||
const { updateAppSetting } = useSettings();
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return (
|
||||
<ToggleSwitch
|
||||
enabled={value}
|
||||
onChange={(enabled) =>
|
||||
updateAppSetting(appId, category, property, enabled)
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (typeof value === "number") {
|
||||
return (
|
||||
<div className="setting-control">
|
||||
<Slider
|
||||
value={value}
|
||||
onChange={(newValue) =>
|
||||
updateAppSetting(appId, category, property, newValue)
|
||||
}
|
||||
/>
|
||||
<span className="status-display">{value}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TextInput
|
||||
value={String(value)}
|
||||
onChange={(newValue) =>
|
||||
updateAppSetting(appId, category, property, newValue)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface SliderProps {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
onChange: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Slider: React.FC<SliderProps> = ({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
onChange,
|
||||
disabled = false
|
||||
const Slider: React.FC<SliderProps> = ({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!disabled) {
|
||||
@ -38,4 +38,4 @@ const Slider: React.FC<SliderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
export default Slider;
|
||||
|
||||
47
bridge-settings/react-app/src/components/TextInput.tsx
Normal file
47
bridge-settings/react-app/src/components/TextInput.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
interface TextInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const TextInput: React.FC<TextInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "",
|
||||
disabled = false,
|
||||
style = {},
|
||||
}) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
const defaultStyle: React.CSSProperties = {
|
||||
padding: "4px 8px",
|
||||
border: "1px solid #bdc3c7",
|
||||
borderRadius: "3px",
|
||||
fontSize: "0.9rem",
|
||||
minWidth: "120px",
|
||||
maxWidth: "200px",
|
||||
outline: "none",
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
style={defaultStyle}
|
||||
onFocus={(e) => (e.target.style.borderColor = "#3498db")}
|
||||
onBlur={(e) => (e.target.style.borderColor = "#bdc3c7")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextInput;
|
||||
@ -127,18 +127,15 @@ export function useSettings() {
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
const updateSystemSetting = (key: string, value: unknown) => {
|
||||
updateSetting("system", key, value);
|
||||
};
|
||||
const updateSystemSetting = (key: string, value: unknown) =>
|
||||
updateSetting("system", "main", key, value);
|
||||
|
||||
const updateAppSetting = (
|
||||
appId: string,
|
||||
category: string,
|
||||
key: string,
|
||||
value: unknown
|
||||
) => {
|
||||
updateSetting(`${appId}.${category}`, key, value);
|
||||
};
|
||||
) => updateSetting(appId, category, key, value);
|
||||
|
||||
const getSystemSettings = () => {
|
||||
return allSettings.system || {};
|
||||
|
||||
@ -118,9 +118,15 @@ export class WebSocketService {
|
||||
}
|
||||
}
|
||||
|
||||
updateSetting(category: string, key: string, value: unknown): void {
|
||||
updateSetting(
|
||||
appId: string,
|
||||
category: string,
|
||||
key: string,
|
||||
value: unknown
|
||||
): void {
|
||||
this.send({
|
||||
type: "updateSetting",
|
||||
appId,
|
||||
category,
|
||||
key,
|
||||
value,
|
||||
@ -134,7 +140,11 @@ export class WebSocketService {
|
||||
});
|
||||
}
|
||||
|
||||
executeAction(appId: string, action: string, params: Record<string, unknown>): void {
|
||||
executeAction(
|
||||
appId: string,
|
||||
action: string,
|
||||
params: Record<string, unknown>
|
||||
): void {
|
||||
this.send({
|
||||
type: "executeAction",
|
||||
appId,
|
||||
|
||||
@ -1,25 +1,56 @@
|
||||
// Message types for WebSocket communication with Kotlin backend
|
||||
export type SettingsMessage =
|
||||
| {
|
||||
type: "updateSetting";
|
||||
appId: string;
|
||||
category: string;
|
||||
key: string;
|
||||
value: unknown;
|
||||
}
|
||||
| { type: "registerForUpdates"; categories: string[] }
|
||||
| { type: "getAllSettings" }
|
||||
| {
|
||||
type: "executeAction";
|
||||
appId: string;
|
||||
action: string;
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SettingsMessage =
|
||||
| { type: 'updateSetting'; category: string; key: string; value: unknown }
|
||||
| { type: 'registerForUpdates'; categories: string[] }
|
||||
| { type: 'getAllSettings' }
|
||||
| { type: 'executeAction'; appId: string; action: string; params: Record<string, unknown> };
|
||||
|
||||
export type StatusMessage =
|
||||
| { type: 'settingChanged'; category: string; key: string; value: unknown }
|
||||
| { type: 'statusUpdate'; statusType: string; data: Record<string, unknown> }
|
||||
| { type: 'allSettings'; settings: Record<string, Record<string, unknown>> }
|
||||
| { type: 'appStatusUpdate'; appId: string; component: string; data: Record<string, unknown> }
|
||||
| { type: 'appEvent'; appId: string; eventType: string; payload: Record<string, unknown> }
|
||||
| { type: 'actionResult'; appId: string; action: string; success: boolean; message?: string; data?: Record<string, unknown>; logs?: LogEntry[] }
|
||||
| { type: 'actionsRegistered'; appId: string; actions: Record<string, ActionDefinition> }
|
||||
| { type: 'error'; message: string };
|
||||
export type StatusMessage =
|
||||
| { type: "settingChanged"; category: string; key: string; value: unknown }
|
||||
| { type: "statusUpdate"; statusType: string; data: Record<string, unknown> }
|
||||
| { type: "allSettings"; settings: Record<string, Record<string, unknown>> }
|
||||
| {
|
||||
type: "appStatusUpdate";
|
||||
appId: string;
|
||||
component: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: "appEvent";
|
||||
appId: string;
|
||||
eventType: string;
|
||||
payload: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: "actionResult";
|
||||
appId: string;
|
||||
action: string;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: Record<string, unknown>;
|
||||
logs?: LogEntry[];
|
||||
}
|
||||
| {
|
||||
type: "actionsRegistered";
|
||||
appId: string;
|
||||
actions: Record<string, ActionDefinition>;
|
||||
}
|
||||
| { type: "error"; message: string };
|
||||
|
||||
export interface Setting {
|
||||
key: string;
|
||||
value: unknown;
|
||||
type: 'boolean' | 'integer' | 'string' | 'float' | 'action';
|
||||
type: "boolean" | "integer" | "string" | "float" | "action";
|
||||
defaultValue: unknown;
|
||||
validation?: {
|
||||
min?: number;
|
||||
@ -38,7 +69,7 @@ export interface ActionDefinition {
|
||||
|
||||
export interface ActionParameter {
|
||||
name: string;
|
||||
type: 'boolean' | 'integer' | 'string' | 'float';
|
||||
type: "boolean" | "integer" | "string" | "float";
|
||||
required: boolean;
|
||||
defaultValue?: unknown;
|
||||
description?: string;
|
||||
@ -46,7 +77,7 @@ export interface ActionParameter {
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
level: 'INFO' | 'WARNING' | 'ERROR' | 'DEBUG';
|
||||
level: "INFO" | "WARNING" | "ERROR" | "DEBUG";
|
||||
message: string;
|
||||
}
|
||||
|
||||
@ -98,4 +129,4 @@ export interface ConnectionState {
|
||||
connected: boolean;
|
||||
connecting: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,24 +48,24 @@ class EsimSettingsProvider(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getActionDefinitions(): Map<String, ActionDefinition> {
|
||||
override fun getActionDefinitions(): Map<String, LocalActionDefinition> {
|
||||
return mapOf(
|
||||
"getProfiles" to ActionDefinition(
|
||||
"getProfiles" to LocalActionDefinition(
|
||||
key = "getProfiles",
|
||||
displayText = "List eSIM Profiles",
|
||||
description = "Retrieve all eSIM profiles on the device"
|
||||
),
|
||||
"getActiveProfile" to ActionDefinition(
|
||||
"getActiveProfile" to LocalActionDefinition(
|
||||
key = "getActiveProfile",
|
||||
displayText = "Get Active Profile",
|
||||
description = "Get the currently active eSIM profile"
|
||||
),
|
||||
"getEid" to ActionDefinition(
|
||||
"getEid" to LocalActionDefinition(
|
||||
key = "getEid",
|
||||
displayText = "Get Device EID",
|
||||
description = "Retrieve the device's embedded identity document (EID)"
|
||||
),
|
||||
"enableProfile" to ActionDefinition(
|
||||
"enableProfile" to LocalActionDefinition(
|
||||
key = "enableProfile",
|
||||
displayText = "Enable Profile",
|
||||
parameters = listOf(
|
||||
@ -78,7 +78,7 @@ class EsimSettingsProvider(
|
||||
),
|
||||
description = "Enable an eSIM profile by ICCID"
|
||||
),
|
||||
"disableProfile" to ActionDefinition(
|
||||
"disableProfile" to LocalActionDefinition(
|
||||
key = "disableProfile",
|
||||
displayText = "Disable Profile",
|
||||
parameters = listOf(
|
||||
@ -91,7 +91,7 @@ class EsimSettingsProvider(
|
||||
),
|
||||
description = "Disable an eSIM profile by ICCID"
|
||||
),
|
||||
"deleteProfile" to ActionDefinition(
|
||||
"deleteProfile" to LocalActionDefinition(
|
||||
key = "deleteProfile",
|
||||
displayText = "Delete Profile",
|
||||
parameters = listOf(
|
||||
@ -104,7 +104,7 @@ class EsimSettingsProvider(
|
||||
),
|
||||
description = "Permanently delete an eSIM profile"
|
||||
),
|
||||
"setNickname" to ActionDefinition(
|
||||
"setNickname" to LocalActionDefinition(
|
||||
key = "setNickname",
|
||||
displayText = "Set Profile Nickname",
|
||||
parameters = listOf(
|
||||
@ -123,7 +123,7 @@ class EsimSettingsProvider(
|
||||
),
|
||||
description = "Set a custom nickname for an eSIM profile"
|
||||
),
|
||||
"downloadProfile" to ActionDefinition(
|
||||
"downloadProfile" to LocalActionDefinition(
|
||||
key = "downloadProfile",
|
||||
displayText = "Download Profile",
|
||||
parameters = listOf(
|
||||
@ -136,7 +136,7 @@ class EsimSettingsProvider(
|
||||
),
|
||||
description = "Download a new eSIM profile from activation code"
|
||||
),
|
||||
"downloadAndEnableProfile" to ActionDefinition(
|
||||
"downloadAndEnableProfile" to LocalActionDefinition(
|
||||
key = "downloadAndEnableProfile",
|
||||
displayText = "Download & Enable Profile",
|
||||
parameters = listOf(
|
||||
|
||||
@ -123,8 +123,11 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
override fun sendAppStatusUpdate(appId: String, component: String, payload: Map<*, *>) {
|
||||
providerScope.launch {
|
||||
try {
|
||||
val convertedPayload = convertMapPayload(payload)
|
||||
settingsRegistry.sendAppStatusUpdate(appId, component, convertedPayload)
|
||||
settingsRegistry.sendAppStatusUpdate(
|
||||
appId,
|
||||
component,
|
||||
payload as Map<String, Any>
|
||||
)
|
||||
Log.d(TAG, "Sent app status update: $appId.$component")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error sending app status update: $appId.$component", e)
|
||||
@ -135,8 +138,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
override fun sendAppEvent(appId: String, eventType: String, payload: Map<*, *>) {
|
||||
providerScope.launch {
|
||||
try {
|
||||
val convertedPayload = convertMapPayload(payload)
|
||||
settingsRegistry.sendAppEvent(appId, eventType, convertedPayload)
|
||||
settingsRegistry.sendAppEvent(appId, eventType, payload as Map<String, Any>)
|
||||
Log.d(TAG, "Sent app event: $appId.$eventType")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error sending app event: $appId.$eventType", e)
|
||||
@ -147,8 +149,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
override fun executeAction(appId: String, action: String, params: Map<*, *>) {
|
||||
providerScope.launch {
|
||||
try {
|
||||
val convertedParams = convertMapPayload(params)
|
||||
settingsRegistry.executeAction(appId, action, convertedParams)
|
||||
settingsRegistry.executeAction(appId, action, params as Map<String, Any>)
|
||||
Log.i(TAG, "Executed action: $appId.$action")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error executing action: $appId.$action", e)
|
||||
@ -156,19 +157,24 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
}
|
||||
}
|
||||
|
||||
override fun executeActionWithCallback(appId: String, action: String, params: Map<*, *>, callback: ISettingsCallback) {
|
||||
override fun executeActionWithCallback(
|
||||
appId: String,
|
||||
action: String,
|
||||
params: Map<*, *>,
|
||||
callback: ISettingsCallback
|
||||
) {
|
||||
providerScope.launch {
|
||||
try {
|
||||
val convertedParams = convertMapPayload(params)
|
||||
val result = settingsRegistry.executeAction(appId, action, convertedParams)
|
||||
val result =
|
||||
settingsRegistry.executeAction(appId, action, params as Map<String, Any>)
|
||||
Log.i(TAG, "Executed action with callback: $appId.$action")
|
||||
|
||||
|
||||
safeCallback(TAG) {
|
||||
callback.onActionResult(
|
||||
appId,
|
||||
action,
|
||||
result.success,
|
||||
result.message ?: "",
|
||||
appId,
|
||||
action,
|
||||
result.success,
|
||||
result.message ?: "",
|
||||
result.data ?: emptyMap<String, Any>()
|
||||
)
|
||||
}
|
||||
@ -232,10 +238,6 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertMapPayload(payload: Map<*, *>): Map<String, Any> {
|
||||
return payload.mapKeys { it.key.toString() }.mapValues { it.value ?: "" }
|
||||
}
|
||||
|
||||
private fun notifySettingsChanged(allSettings: Map<String, Any>) {
|
||||
// This would be implemented to notify specific callbacks about relevant changes
|
||||
// For now, we'll skip detailed change detection
|
||||
@ -328,7 +330,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
com.penumbraos.bridge.types.AppActionInfo().apply {
|
||||
this.appId = appId
|
||||
this.actions = actionDefinitions
|
||||
@ -339,7 +341,7 @@ class SettingsProvider(private val settingsRegistry: SettingsRegistry) : ISettin
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getSystemSettingDescription(key: String): String {
|
||||
return when (key) {
|
||||
"audio.volume" -> "Audio volume level (0-100)"
|
||||
|
||||
@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
@ -47,67 +46,14 @@ enum class LogLevel {
|
||||
|
||||
interface SettingsActionProvider {
|
||||
suspend fun executeAction(action: String, params: Map<String, Any>): ActionResult
|
||||
fun getActionDefinitions(): Map<String, ActionDefinition>
|
||||
fun getActionDefinitions(): Map<String, LocalActionDefinition>
|
||||
}
|
||||
|
||||
data class ActionDefinition(
|
||||
val key: String,
|
||||
val displayText: String,
|
||||
val parameters: List<ActionParameter> = emptyList(),
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
data class ActionParameter(
|
||||
val name: String,
|
||||
val type: SettingType,
|
||||
val required: Boolean = true,
|
||||
val defaultValue: Any? = null,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
data class SettingDefinition(
|
||||
val key: String,
|
||||
val type: SettingType,
|
||||
val defaultValue: Any,
|
||||
val validation: SettingValidation? = null
|
||||
)
|
||||
|
||||
enum class SettingType {
|
||||
BOOLEAN, INTEGER, STRING, FLOAT, ACTION
|
||||
}
|
||||
|
||||
data class SettingValidation(
|
||||
val min: Number? = null,
|
||||
val max: Number? = null,
|
||||
val allowedValues: List<Any>? = null,
|
||||
val regex: String? = null
|
||||
)
|
||||
|
||||
data class AppSettingsCategory(
|
||||
val appId: String,
|
||||
val category: String,
|
||||
val definitions: Map<String, SettingDefinition>,
|
||||
val values: MutableMap<String, Any> = mutableMapOf()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedSettings(
|
||||
val systemSettings: Map<String, String> = emptyMap(),
|
||||
val appSettings: Map<String, Map<String, Map<String, JsonElement>>> = emptyMap()
|
||||
)
|
||||
|
||||
data class ExecutingAction(
|
||||
val providerId: String,
|
||||
val actionName: String,
|
||||
val params: Map<String, Any>,
|
||||
val startTime: Long
|
||||
)
|
||||
|
||||
class SettingsRegistry(private val context: Context, val shellClient: ShellClient) {
|
||||
private val appSettings = ConcurrentHashMap<String, MutableMap<String, AppSettingsCategory>>()
|
||||
private val systemSettings = ConcurrentHashMap<String, Any>()
|
||||
private val actionProviders = ConcurrentHashMap<String, SettingsActionProvider>()
|
||||
|
||||
|
||||
// Execution state tracking
|
||||
@Volatile
|
||||
private var currentExecutingAction: ExecutingAction? = null
|
||||
@ -117,14 +63,14 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
companion object {
|
||||
private const val EXECUTION_TIMEOUT_MS = 30000L // 30 seconds
|
||||
}
|
||||
|
||||
|
||||
private fun startExecutionTimeout(appId: String, action: String) {
|
||||
clearState()
|
||||
|
||||
|
||||
executionTimeoutRunnable = Runnable {
|
||||
Log.w(TAG, "Action execution timeout: $appId.$action")
|
||||
currentExecutingAction = null
|
||||
|
||||
|
||||
// Broadcast timeout error
|
||||
registryScope.launch {
|
||||
sendAppEvent(
|
||||
@ -132,20 +78,22 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
"action" to action,
|
||||
"success" to false,
|
||||
"message" to "Action timed out after ${EXECUTION_TIMEOUT_MS / 1000} seconds",
|
||||
"logs" to listOf(mapOf<String, Any>(
|
||||
"timestamp" to System.currentTimeMillis(),
|
||||
"level" to "ERROR",
|
||||
"message" to "Action execution timeout"
|
||||
))
|
||||
"logs" to listOf(
|
||||
mapOf<String, Any>(
|
||||
"timestamp" to System.currentTimeMillis(),
|
||||
"level" to "ERROR",
|
||||
"message" to "Action execution timeout"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
executionTimeoutHandler.postDelayed(executionTimeoutRunnable!!, EXECUTION_TIMEOUT_MS)
|
||||
Log.d(TAG, "Started execution timeout for: $appId.$action")
|
||||
}
|
||||
|
||||
|
||||
private fun clearState() {
|
||||
currentExecutingAction = null
|
||||
executionTimeoutRunnable?.let {
|
||||
@ -165,8 +113,8 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
// Reference to web server for broadcasting (set by SettingsService)
|
||||
private var webServer: SettingsWebServer? = null
|
||||
|
||||
private val _settingsFlow = MutableStateFlow<Map<String, Any>>(emptyMap())
|
||||
val settingsFlow: StateFlow<Map<String, Any>> = _settingsFlow.asStateFlow()
|
||||
private val _settingsFlow = MutableStateFlow<Map<String, Map<String, Any>>>(emptyMap())
|
||||
val settingsFlow: StateFlow<Map<String, Map<String, Any>>> = _settingsFlow.asStateFlow()
|
||||
|
||||
@SuppressLint("SdCardPath")
|
||||
private val settingsFile = File("/sdcard/penumbra/etc/settings.json")
|
||||
@ -437,8 +385,8 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
return systemSettings.toMap()
|
||||
}
|
||||
|
||||
fun getAllSettings(): Map<String, Any> {
|
||||
val result = mutableMapOf<String, Any>()
|
||||
fun getAllSettings(): Map<String, Map<String, Any>> {
|
||||
val result = mutableMapOf<String, Map<String, Any>>()
|
||||
|
||||
// Add system settings
|
||||
result["system"] = systemSettings.toMap()
|
||||
@ -451,7 +399,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
}
|
||||
result[appId] = appData
|
||||
}
|
||||
|
||||
|
||||
// Add current execution status
|
||||
getCurrentExecutionStatus()?.let { executionStatus ->
|
||||
result["executionStatus"] = executionStatus
|
||||
@ -555,7 +503,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
// Broadcast available actions to web UI
|
||||
registryScope.launch {
|
||||
val actions = provider.getActionDefinitions()
|
||||
sendAppEvent(appId, "actionsRegistered", mapOf<String, Any>("actions" to actions))
|
||||
sendAppEvent(appId, "actionsRegistered", mapOf("actions" to actions))
|
||||
}
|
||||
}
|
||||
|
||||
@ -570,7 +518,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
params: Map<String, Any>
|
||||
): ActionResult {
|
||||
Log.i(TAG, "Executing action: $appId.$action with params: $params")
|
||||
|
||||
|
||||
currentExecutingAction = ExecutingAction(appId, action, params, System.currentTimeMillis())
|
||||
startExecutionTimeout(appId, action)
|
||||
|
||||
@ -596,7 +544,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
"logs" to (errorResult.logs ?: emptyList())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
clearState()
|
||||
|
||||
return errorResult
|
||||
@ -616,7 +564,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
"logs" to (result.logs ?: emptyList())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
clearState()
|
||||
|
||||
result
|
||||
@ -637,7 +585,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
"logs" to (errorResult.logs ?: emptyList())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
clearState()
|
||||
|
||||
errorResult
|
||||
@ -647,7 +595,7 @@ class SettingsRegistry(private val context: Context, val shellClient: ShellClien
|
||||
fun getActionProvider(appId: String): SettingsActionProvider? {
|
||||
return actionProviders[appId]
|
||||
}
|
||||
|
||||
|
||||
fun getCurrentExecutionStatus(): Map<String, Any>? {
|
||||
return currentExecutingAction?.let { action ->
|
||||
mapOf(
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
package com.penumbraos.bridge_settings
|
||||
|
||||
import android.util.Log
|
||||
import com.penumbraos.bridge_settings.json.toJsonElement
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
@ -32,12 +33,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
@ -52,7 +53,12 @@ private const val TAG = "SettingsWebServer"
|
||||
sealed class SettingsMessage {
|
||||
@Serializable
|
||||
@SerialName("updateSetting")
|
||||
data class UpdateSetting(val category: String, val key: String, val value: JsonElement) :
|
||||
data class UpdateSetting(
|
||||
val appId: String,
|
||||
val category: String,
|
||||
val key: String,
|
||||
val value: JsonElement
|
||||
) :
|
||||
SettingsMessage()
|
||||
|
||||
@Serializable
|
||||
@ -86,14 +92,16 @@ sealed class StatusMessage {
|
||||
|
||||
@Serializable
|
||||
@SerialName("allSettings")
|
||||
data class AllSettings(val settings: Map<String, Map<String, JsonElement>>) : StatusMessage()
|
||||
data class AllSettings(val settings: JsonElement) :
|
||||
StatusMessage()
|
||||
|
||||
@Serializable
|
||||
@SerialName("appStatusUpdate")
|
||||
data class AppStatusUpdate(
|
||||
val appId: String,
|
||||
val component: String,
|
||||
val data: Map<String, JsonElement>
|
||||
@Contextual
|
||||
val data: JsonElement
|
||||
) : StatusMessage()
|
||||
|
||||
@Serializable
|
||||
@ -101,7 +109,8 @@ sealed class StatusMessage {
|
||||
data class AppEvent(
|
||||
val appId: String,
|
||||
val eventType: String,
|
||||
val payload: Map<String, JsonElement>
|
||||
@Contextual
|
||||
val payload: JsonElement
|
||||
) : StatusMessage()
|
||||
|
||||
@Serializable
|
||||
@ -321,7 +330,7 @@ class SettingsWebServer(
|
||||
val allSettings = settingsRegistry.getAllSettings()
|
||||
sendToSession(
|
||||
session,
|
||||
StatusMessage.AllSettings(convertToJsonCompatibleMap(allSettings))
|
||||
StatusMessage.AllSettings(allSettings.toJsonElement())
|
||||
)
|
||||
|
||||
for (frame in session.incoming) {
|
||||
@ -358,7 +367,7 @@ class SettingsWebServer(
|
||||
val allSettings = settingsRegistry.getAllSettings()
|
||||
sendToSession(
|
||||
session,
|
||||
StatusMessage.AllSettings(convertToJsonCompatibleMap(allSettings))
|
||||
StatusMessage.AllSettings(allSettings.toJsonElement())
|
||||
)
|
||||
}
|
||||
|
||||
@ -367,12 +376,15 @@ class SettingsWebServer(
|
||||
primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.doubleOrNull
|
||||
?: primitive.content
|
||||
}
|
||||
val success = if (message.category == "system") {
|
||||
val success = if (message.appId == "system") {
|
||||
settingsRegistry.updateSystemSetting(message.key, convertedValue)
|
||||
} else {
|
||||
// For app settings, we'd need to parse the category to get appId
|
||||
// This is simplified - in a real implementation, the message format would include appId
|
||||
false
|
||||
settingsRegistry.updateAppSetting(
|
||||
message.appId,
|
||||
message.category,
|
||||
message.key,
|
||||
convertedValue
|
||||
)
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
@ -389,7 +401,7 @@ class SettingsWebServer(
|
||||
is SettingsMessage.ExecuteAction -> {
|
||||
try {
|
||||
Log.i(TAG, "Executing action: ${message.appId}.${message.action}")
|
||||
|
||||
|
||||
// Convert JsonElement parameters to Map<String, Any>
|
||||
val params = message.params.mapValues { (_, value) ->
|
||||
value.jsonPrimitive.let { primitive ->
|
||||
@ -412,8 +424,8 @@ class SettingsWebServer(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun broadcastSettingsUpdate(allSettings: Map<String, Any>) {
|
||||
val message = StatusMessage.AllSettings(convertToJsonCompatibleMap(allSettings))
|
||||
private suspend fun broadcastSettingsUpdate(allSettings: Map<String, Map<String, Any>>) {
|
||||
val message = StatusMessage.AllSettings(allSettings.toJsonElement())
|
||||
broadcast(message)
|
||||
}
|
||||
|
||||
@ -425,7 +437,7 @@ class SettingsWebServer(
|
||||
val message = StatusMessage.AppStatusUpdate(
|
||||
appId = appId,
|
||||
component = component,
|
||||
data = convertValuesToJsonElements(payload)
|
||||
data = payload.toJsonElement()
|
||||
)
|
||||
broadcast(message)
|
||||
Log.d(TAG, "Broadcasted app status update: $appId.$component")
|
||||
@ -435,7 +447,7 @@ class SettingsWebServer(
|
||||
val message = StatusMessage.AppEvent(
|
||||
appId = appId,
|
||||
eventType = eventType,
|
||||
payload = convertValuesToJsonElements(payload)
|
||||
payload = payload.toJsonElement()
|
||||
)
|
||||
broadcast(message)
|
||||
Log.d(TAG, "Broadcasted app event: $appId.$eventType")
|
||||
@ -465,53 +477,6 @@ class SettingsWebServer(
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertValuesToJsonElements(data: Map<String, Any?>): Map<String, JsonElement> {
|
||||
return data.mapValues { (_, value) ->
|
||||
when (value) {
|
||||
null -> JsonPrimitive(null)
|
||||
is Boolean -> JsonPrimitive(value)
|
||||
is Number -> JsonPrimitive(value)
|
||||
is String -> JsonPrimitive(value)
|
||||
is List<*> -> {
|
||||
try {
|
||||
JsonPrimitive(Json.encodeToString(value))
|
||||
} catch (_: Exception) {
|
||||
JsonPrimitive(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
is Map<*, *> -> {
|
||||
try {
|
||||
JsonPrimitive(Json.encodeToString(value))
|
||||
} catch (_: Exception) {
|
||||
JsonPrimitive(value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
else -> JsonPrimitive(value.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertToJsonCompatibleMap(input: Map<String, Any>): Map<String, Map<String, JsonElement>> {
|
||||
return input.mapValues { (_, value) ->
|
||||
when (value) {
|
||||
is Map<*, *> -> convertValuesToJsonElements(
|
||||
value.mapKeys { it.key.toString() }.mapValues { it.value ?: "" }
|
||||
)
|
||||
|
||||
else -> mapOf(
|
||||
"value" to when (value) {
|
||||
is Boolean -> JsonPrimitive(value)
|
||||
is Number -> JsonPrimitive(value)
|
||||
is String -> JsonPrimitive(value)
|
||||
else -> JsonPrimitive(value.toString())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getResourceFromApk(resourcePath: String): InputStream? {
|
||||
return try {
|
||||
this::class.java.classLoader?.getResourceAsStream(resourcePath)
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
@file:OptIn(ExperimentalUnsignedTypes::class, ExperimentalSerializationApi::class)
|
||||
|
||||
package com.penumbraos.bridge_settings.json
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.serializerOrNull
|
||||
|
||||
internal fun Any?.toJsonElement(): JsonElement {
|
||||
val serializer = this?.let { Json.serializersModule.serializerOrNull(this::class.java) }
|
||||
|
||||
return when {
|
||||
this == null -> JsonNull
|
||||
serializer != null -> Json.encodeToJsonElement(serializer, this)
|
||||
this is Map<*, *> -> toJsonElement()
|
||||
this is Array<*> -> toJsonElement()
|
||||
this is BooleanArray -> toJsonElement()
|
||||
this is ByteArray -> toJsonElement()
|
||||
this is CharArray -> toJsonElement()
|
||||
this is ShortArray -> toJsonElement()
|
||||
this is IntArray -> toJsonElement()
|
||||
this is LongArray -> toJsonElement()
|
||||
this is FloatArray -> toJsonElement()
|
||||
this is DoubleArray -> toJsonElement()
|
||||
this is UByteArray -> toJsonElement()
|
||||
this is UShortArray -> toJsonElement()
|
||||
this is UIntArray -> toJsonElement()
|
||||
this is ULongArray -> toJsonElement()
|
||||
this is Collection<*> -> toJsonElement()
|
||||
this is Boolean -> JsonPrimitive(this)
|
||||
this is Number -> JsonPrimitive(this)
|
||||
this is String -> JsonPrimitive(this)
|
||||
this is Enum<*> -> JsonPrimitive(this.name)
|
||||
this is Pair<*, *> -> JsonObject(
|
||||
mapOf(
|
||||
"first" to first.toJsonElement(),
|
||||
"second" to second.toJsonElement(),
|
||||
)
|
||||
)
|
||||
|
||||
this is Triple<*, *, *> -> JsonObject(
|
||||
mapOf(
|
||||
"first" to first.toJsonElement(),
|
||||
"second" to second.toJsonElement(),
|
||||
"third" to third.toJsonElement(),
|
||||
)
|
||||
)
|
||||
|
||||
else -> error("Can't serialize '$this' as it is of an unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Map<*, *>.toJsonElement(): JsonElement {
|
||||
return buildJsonObject {
|
||||
forEach { (key, value) ->
|
||||
if (key !is String)
|
||||
error("Only string keys are supported for maps")
|
||||
|
||||
put(key, value.toJsonElement())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Collection<*>.toJsonElement(): JsonElement = buildJsonArray {
|
||||
forEach { element ->
|
||||
add(element.toJsonElement())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Array<*>.toJsonElement(): JsonElement = buildJsonArray {
|
||||
forEach { element ->
|
||||
add(element.toJsonElement())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BooleanArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun ByteArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun CharArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it.toString())) } }
|
||||
|
||||
internal fun ShortArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun IntArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun LongArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun FloatArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun DoubleArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun UByteArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun UShortArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun UIntArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
|
||||
internal fun ULongArray.toJsonElement(): JsonElement =
|
||||
buildJsonArray { forEach { add(JsonPrimitive(it)) } }
|
||||
@ -0,0 +1,121 @@
|
||||
package com.penumbraos.bridge_settings
|
||||
|
||||
import com.penumbraos.bridge_settings.json.toJsonElement
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.double
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
|
||||
object AnyAsJsonElementSerializer : KSerializer<Any> {
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Any")
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Any) {
|
||||
val jsonElement = value.toJsonElement()
|
||||
encoder.encodeSerializableValue(JsonElement.serializer(), jsonElement)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Any {
|
||||
val jsonElement = decoder.decodeSerializableValue(JsonElement.serializer())
|
||||
return when {
|
||||
jsonElement.jsonPrimitive.isString -> jsonElement.jsonPrimitive.content
|
||||
else -> jsonElement.jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object NumberAsJsonElementSerializer : KSerializer<Number> {
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Number")
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Number) {
|
||||
val jsonElement = value.toJsonElement()
|
||||
encoder.encodeSerializableValue(JsonElement.serializer(), jsonElement)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Number {
|
||||
val jsonElement = decoder.decodeSerializableValue(JsonElement.serializer())
|
||||
val primitive = jsonElement.jsonPrimitive
|
||||
return when {
|
||||
primitive.intOrNull != null -> primitive.int
|
||||
primitive.longOrNull != null -> primitive.long
|
||||
primitive.floatOrNull != null -> primitive.float
|
||||
primitive.doubleOrNull != null -> primitive.double
|
||||
else -> primitive.content.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LocalActionDefinition(
|
||||
val key: String,
|
||||
val displayText: String,
|
||||
val parameters: List<ActionParameter> = emptyList(),
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ActionParameter(
|
||||
val name: String,
|
||||
val type: SettingType,
|
||||
val required: Boolean = true,
|
||||
@Serializable(with = AnyAsJsonElementSerializer::class)
|
||||
val defaultValue: Any? = null,
|
||||
val description: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SettingDefinition(
|
||||
val key: String,
|
||||
val type: SettingType,
|
||||
@Serializable(with = AnyAsJsonElementSerializer::class)
|
||||
val defaultValue: Any,
|
||||
val validation: SettingValidation? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class SettingType {
|
||||
BOOLEAN, INTEGER, STRING, FLOAT, ACTION
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SettingValidation(
|
||||
@Serializable(with = NumberAsJsonElementSerializer::class)
|
||||
val min: Number? = null,
|
||||
@Serializable(with = NumberAsJsonElementSerializer::class)
|
||||
val max: Number? = null,
|
||||
val allowedValues: List<@Serializable(with = AnyAsJsonElementSerializer::class) Any>? = null,
|
||||
val regex: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AppSettingsCategory(
|
||||
val appId: String,
|
||||
val category: String,
|
||||
val definitions: Map<String, SettingDefinition>,
|
||||
val values: MutableMap<String, @Serializable(with = AnyAsJsonElementSerializer::class) Any> = mutableMapOf()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedSettings(
|
||||
val systemSettings: Map<String, String> = emptyMap(),
|
||||
val appSettings: Map<String, Map<String, Map<String, JsonElement>>> = emptyMap()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExecutingAction(
|
||||
val providerId: String,
|
||||
val actionName: String,
|
||||
val params: Map<String, @Serializable(with = AnyAsJsonElementSerializer::class) Any>,
|
||||
val startTime: Long
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user