Auto commit: 2026-03-23T19:22:21.572Z
This commit is contained in:
parent
16b7d41fc5
commit
e713c1471c
19
app.php
19
app.php
@ -27,7 +27,6 @@ function ensure_recliner_schema(): void
|
|||||||
angle_deg TINYINT UNSIGNED NOT NULL,
|
angle_deg TINYINT UNSIGNED NOT NULL,
|
||||||
intensity_pct TINYINT UNSIGNED NOT NULL,
|
intensity_pct TINYINT UNSIGNED NOT NULL,
|
||||||
pattern_mode VARCHAR(20) NOT NULL,
|
pattern_mode VARCHAR(20) NOT NULL,
|
||||||
duration_ms SMALLINT UNSIGNED NOT NULL,
|
|
||||||
notes VARCHAR(255) DEFAULT NULL,
|
notes VARCHAR(255) DEFAULT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||||
@ -41,7 +40,6 @@ function default_preset(): array
|
|||||||
'angle_deg' => 112,
|
'angle_deg' => 112,
|
||||||
'intensity_pct' => 44,
|
'intensity_pct' => 44,
|
||||||
'pattern_mode' => 'continuous',
|
'pattern_mode' => 'continuous',
|
||||||
'duration_ms' => 1400,
|
|
||||||
'notes' => 'Balanced demo profile for a calm showroom preview.',
|
'notes' => 'Balanced demo profile for a calm showroom preview.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -69,13 +67,6 @@ function validate_preset_input(array $input): array
|
|||||||
$errors['intensity_pct'] = 'Intensity must be between 0% and 100%.';
|
$errors['intensity_pct'] = 'Intensity must be between 0% and 100%.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$duration = filter_var($input['duration_ms'] ?? null, FILTER_VALIDATE_INT, [
|
|
||||||
'options' => ['min_range' => 100, 'max_range' => 5000],
|
|
||||||
]);
|
|
||||||
if ($duration === false) {
|
|
||||||
$errors['duration_ms'] = 'Duration must be between 100 ms and 5000 ms.';
|
|
||||||
}
|
|
||||||
|
|
||||||
$pattern = (string) ($input['pattern_mode'] ?? 'continuous');
|
$pattern = (string) ($input['pattern_mode'] ?? 'continuous');
|
||||||
if (!in_array($pattern, RECLINER_PATTERNS, true)) {
|
if (!in_array($pattern, RECLINER_PATTERNS, true)) {
|
||||||
$errors['pattern_mode'] = 'Choose a supported vibration pattern.';
|
$errors['pattern_mode'] = 'Choose a supported vibration pattern.';
|
||||||
@ -93,7 +84,6 @@ function validate_preset_input(array $input): array
|
|||||||
'angle_deg' => $angle === false ? 0 : (int) $angle,
|
'angle_deg' => $angle === false ? 0 : (int) $angle,
|
||||||
'intensity_pct' => $intensity === false ? 0 : (int) $intensity,
|
'intensity_pct' => $intensity === false ? 0 : (int) $intensity,
|
||||||
'pattern_mode' => $pattern,
|
'pattern_mode' => $pattern,
|
||||||
'duration_ms' => $duration === false ? 1000 : (int) $duration,
|
|
||||||
'notes' => $notes,
|
'notes' => $notes,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
@ -102,15 +92,14 @@ function validate_preset_input(array $input): array
|
|||||||
function save_preset(array $data): int
|
function save_preset(array $data): int
|
||||||
{
|
{
|
||||||
$stmt = db()->prepare(
|
$stmt = db()->prepare(
|
||||||
'INSERT INTO recliner_presets (name, angle_deg, intensity_pct, pattern_mode, duration_ms, notes)
|
'INSERT INTO recliner_presets (name, angle_deg, intensity_pct, pattern_mode, notes)
|
||||||
VALUES (:name, :angle_deg, :intensity_pct, :pattern_mode, :duration_ms, :notes)'
|
VALUES (:name, :angle_deg, :intensity_pct, :pattern_mode, :notes)'
|
||||||
);
|
);
|
||||||
|
|
||||||
$stmt->bindValue(':name', $data['name'], PDO::PARAM_STR);
|
$stmt->bindValue(':name', $data['name'], PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':angle_deg', $data['angle_deg'], PDO::PARAM_INT);
|
$stmt->bindValue(':angle_deg', $data['angle_deg'], PDO::PARAM_INT);
|
||||||
$stmt->bindValue(':intensity_pct', $data['intensity_pct'], PDO::PARAM_INT);
|
$stmt->bindValue(':intensity_pct', $data['intensity_pct'], PDO::PARAM_INT);
|
||||||
$stmt->bindValue(':pattern_mode', $data['pattern_mode'], PDO::PARAM_STR);
|
$stmt->bindValue(':pattern_mode', $data['pattern_mode'], PDO::PARAM_STR);
|
||||||
$stmt->bindValue(':duration_ms', $data['duration_ms'], PDO::PARAM_INT);
|
|
||||||
$stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
$stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
@ -121,7 +110,7 @@ function get_recent_presets(int $limit = 8): array
|
|||||||
{
|
{
|
||||||
$limit = max(1, min(24, $limit));
|
$limit = max(1, min(24, $limit));
|
||||||
$stmt = db()->query(
|
$stmt = db()->query(
|
||||||
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, duration_ms, notes, created_at
|
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, notes, created_at
|
||||||
FROM recliner_presets
|
FROM recliner_presets
|
||||||
ORDER BY created_at DESC, id DESC
|
ORDER BY created_at DESC, id DESC
|
||||||
LIMIT ' . $limit
|
LIMIT ' . $limit
|
||||||
@ -133,7 +122,7 @@ function get_recent_presets(int $limit = 8): array
|
|||||||
function get_preset(int $id): ?array
|
function get_preset(int $id): ?array
|
||||||
{
|
{
|
||||||
$stmt = db()->prepare(
|
$stmt = db()->prepare(
|
||||||
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, duration_ms, notes, created_at
|
'SELECT id, name, angle_deg, intensity_pct, pattern_mode, notes, created_at
|
||||||
FROM recliner_presets
|
FROM recliner_presets
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
LIMIT 1'
|
LIMIT 1'
|
||||||
|
|||||||
@ -8,7 +8,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
angle: document.getElementById('angle_deg'),
|
angle: document.getElementById('angle_deg'),
|
||||||
intensity: document.getElementById('intensity_pct'),
|
intensity: document.getElementById('intensity_pct'),
|
||||||
pattern: document.getElementById('pattern_mode'),
|
pattern: document.getElementById('pattern_mode'),
|
||||||
duration: document.getElementById('duration_ms'),
|
|
||||||
name: document.getElementById('name'),
|
name: document.getElementById('name'),
|
||||||
notes: document.getElementById('notes')
|
notes: document.getElementById('notes')
|
||||||
};
|
};
|
||||||
@ -18,19 +17,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
angleValue: document.getElementById('angle-value'),
|
angleValue: document.getElementById('angle-value'),
|
||||||
anglePill: document.getElementById('angle-pill'),
|
anglePill: document.getElementById('angle-pill'),
|
||||||
intensityPill: document.getElementById('intensity-pill'),
|
intensityPill: document.getElementById('intensity-pill'),
|
||||||
durationPill: document.getElementById('duration-pill'),
|
|
||||||
statAngle: document.getElementById('stat-angle'),
|
statAngle: document.getElementById('stat-angle'),
|
||||||
statIntensity: document.getElementById('stat-intensity'),
|
statIntensity: document.getElementById('stat-intensity'),
|
||||||
statPattern: document.getElementById('stat-pattern'),
|
statPattern: document.getElementById('stat-pattern'),
|
||||||
statDuration: document.getElementById('stat-duration'),
|
|
||||||
saveAngle: document.getElementById('save-angle'),
|
saveAngle: document.getElementById('save-angle'),
|
||||||
saveIntensity: document.getElementById('save-intensity'),
|
saveIntensity: document.getElementById('save-intensity'),
|
||||||
savePattern: document.getElementById('save-pattern'),
|
savePattern: document.getElementById('save-pattern'),
|
||||||
saveDuration: document.getElementById('save-duration'),
|
|
||||||
summaryAngle: document.getElementById('summary-angle'),
|
summaryAngle: document.getElementById('summary-angle'),
|
||||||
summaryIntensity: document.getElementById('summary-intensity'),
|
summaryIntensity: document.getElementById('summary-intensity'),
|
||||||
summaryPattern: document.getElementById('summary-pattern'),
|
summaryPattern: document.getElementById('summary-pattern'),
|
||||||
summaryTone: document.getElementById('summary-tone'),
|
|
||||||
reclineMode: document.getElementById('recline-mode'),
|
reclineMode: document.getElementById('recline-mode'),
|
||||||
gamepadState: document.getElementById('gamepad-state'),
|
gamepadState: document.getElementById('gamepad-state'),
|
||||||
gamepadSelect: document.getElementById('gamepad-select'),
|
gamepadSelect: document.getElementById('gamepad-select'),
|
||||||
@ -40,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
let knownGamepads = [];
|
let knownGamepads = [];
|
||||||
let scanTimer = null;
|
let scanTimer = null;
|
||||||
|
let continuousVibrationInterval = null;
|
||||||
|
|
||||||
const notify = (message) => {
|
const notify = (message) => {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@ -63,8 +59,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const currentState = () => ({
|
const currentState = () => ({
|
||||||
angle: Number(controls.angle?.value || 0),
|
angle: Number(controls.angle?.value || 0),
|
||||||
intensity: Number(controls.intensity?.value || 0),
|
intensity: Number(controls.intensity?.value || 0),
|
||||||
pattern: controls.pattern?.value || 'continuous',
|
pattern: controls.pattern?.value || 'continuous'
|
||||||
duration: Number(controls.duration?.value || 1000)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateVisualization = () => {
|
const updateVisualization = () => {
|
||||||
@ -79,19 +74,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
angleValue: `${state.angle}`,
|
angleValue: `${state.angle}`,
|
||||||
anglePill: `${state.angle}°`,
|
anglePill: `${state.angle}°`,
|
||||||
intensityPill: `${state.intensity}%`,
|
intensityPill: `${state.intensity}%`,
|
||||||
durationPill: `${state.duration} ms`,
|
|
||||||
statAngle: `${state.angle}°`,
|
statAngle: `${state.angle}°`,
|
||||||
statIntensity: `${state.intensity}%`,
|
statIntensity: `${state.intensity}%`,
|
||||||
statPattern: capitalize(state.pattern),
|
statPattern: capitalize(state.pattern),
|
||||||
statDuration: `${state.duration} ms`,
|
|
||||||
saveAngle: `${state.angle}°`,
|
saveAngle: `${state.angle}°`,
|
||||||
saveIntensity: `${state.intensity}%`,
|
saveIntensity: `${state.intensity}%`,
|
||||||
savePattern: capitalize(state.pattern),
|
savePattern: capitalize(state.pattern),
|
||||||
saveDuration: `${state.duration} ms`,
|
|
||||||
summaryAngle: `${state.angle}°`,
|
summaryAngle: `${state.angle}°`,
|
||||||
summaryIntensity: `${state.intensity}%`,
|
summaryIntensity: `${state.intensity}%`,
|
||||||
summaryPattern: capitalize(state.pattern),
|
summaryPattern: capitalize(state.pattern),
|
||||||
summaryTone: tone,
|
|
||||||
reclineMode: tone
|
reclineMode: tone
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,7 +100,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (controls.angle && preset.angle_deg !== undefined) controls.angle.value = preset.angle_deg;
|
if (controls.angle && preset.angle_deg !== undefined) controls.angle.value = preset.angle_deg;
|
||||||
if (controls.intensity && preset.intensity_pct !== undefined) controls.intensity.value = preset.intensity_pct;
|
if (controls.intensity && preset.intensity_pct !== undefined) controls.intensity.value = preset.intensity_pct;
|
||||||
if (controls.pattern && preset.pattern_mode) controls.pattern.value = preset.pattern_mode;
|
if (controls.pattern && preset.pattern_mode) controls.pattern.value = preset.pattern_mode;
|
||||||
if (controls.duration && preset.duration_ms !== undefined) controls.duration.value = preset.duration_ms;
|
|
||||||
if (controls.name && preset.name !== undefined) controls.name.value = preset.name;
|
if (controls.name && preset.name !== undefined) controls.name.value = preset.name;
|
||||||
if (controls.notes && preset.notes !== undefined) controls.notes.value = preset.notes;
|
if (controls.notes && preset.notes !== undefined) controls.notes.value = preset.notes;
|
||||||
updateVisualization();
|
updateVisualization();
|
||||||
@ -211,32 +201,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { intensity, duration, pattern } = currentState();
|
const { intensity, pattern } = currentState();
|
||||||
const normalizedIntensity = Math.max(0, Math.min(1, intensity / 100));
|
const normalizedIntensity = Math.max(0, Math.min(1, intensity / 100));
|
||||||
ui.testButton.disabled = true;
|
|
||||||
ui.testButton.textContent = 'Testing…';
|
if (continuousVibrationInterval) {
|
||||||
|
window.clearInterval(continuousVibrationInterval);
|
||||||
|
continuousVibrationInterval = null;
|
||||||
|
ui.testButton.textContent = 'Test vibration (continuous)';
|
||||||
|
notify('Vibration stopped.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.testButton.textContent = 'Stop vibration';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (pattern === 'pulse') {
|
if (pattern === 'pulse') {
|
||||||
let remaining = duration;
|
const pulse = async () => {
|
||||||
while (remaining > 0) {
|
await playEffect(actuator, normalizedIntensity, 500);
|
||||||
const burst = Math.min(160, remaining);
|
};
|
||||||
await playEffect(actuator, normalizedIntensity, burst);
|
pulse();
|
||||||
remaining -= burst;
|
continuousVibrationInterval = window.setInterval(pulse, 800);
|
||||||
if (remaining > 0) {
|
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await playEffect(actuator, normalizedIntensity, duration);
|
// For continuous, we re-trigger every 100ms
|
||||||
|
await playEffect(actuator, normalizedIntensity, 150);
|
||||||
|
continuousVibrationInterval = window.setInterval(async () => {
|
||||||
|
await playEffect(actuator, normalizedIntensity, 150);
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
notify(`Running ${pattern} vibration at ${intensity}% for ${duration} ms.`);
|
notify(`Running ${pattern} vibration at ${intensity}%.`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
notify('The browser detected the controller, but vibration could not start.');
|
notify('The browser detected the controller, but vibration could not start.');
|
||||||
} finally {
|
ui.testButton.textContent = 'Test vibration (continuous)';
|
||||||
ui.testButton.textContent = 'Test vibration';
|
|
||||||
refreshGamepads();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -248,7 +244,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
angle_deg: button.dataset.angle || 0,
|
angle_deg: button.dataset.angle || 0,
|
||||||
intensity_pct: button.dataset.intensity || 0,
|
intensity_pct: button.dataset.intensity || 0,
|
||||||
pattern_mode: button.dataset.pattern || 'continuous',
|
pattern_mode: button.dataset.pattern || 'continuous',
|
||||||
duration_ms: button.dataset.duration || 1000,
|
|
||||||
notes: button.dataset.notes || ''
|
notes: button.dataset.notes || ''
|
||||||
});
|
});
|
||||||
window.location.hash = 'simulator';
|
window.location.hash = 'simulator';
|
||||||
@ -268,7 +263,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[controls.angle, controls.intensity, controls.pattern, controls.duration].forEach((element) => {
|
[controls.angle, controls.intensity, controls.pattern].forEach((element) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
element.addEventListener('input', updateVisualization);
|
element.addEventListener('input', updateVisualization);
|
||||||
element.addEventListener('change', updateVisualization);
|
element.addEventListener('change', updateVisualization);
|
||||||
@ -306,5 +301,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (scanTimer) {
|
if (scanTimer) {
|
||||||
window.clearInterval(scanTimer);
|
window.clearInterval(scanTimer);
|
||||||
}
|
}
|
||||||
|
if (continuousVibrationInterval) {
|
||||||
|
window.clearInterval(continuousVibrationInterval);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
34
index.php
34
index.php
@ -36,7 +36,6 @@ if ($presetId) {
|
|||||||
'angle_deg' => (int) $selectedPreset['angle_deg'],
|
'angle_deg' => (int) $selectedPreset['angle_deg'],
|
||||||
'intensity_pct' => (int) $selectedPreset['intensity_pct'],
|
'intensity_pct' => (int) $selectedPreset['intensity_pct'],
|
||||||
'pattern_mode' => (string) $selectedPreset['pattern_mode'],
|
'pattern_mode' => (string) $selectedPreset['pattern_mode'],
|
||||||
'duration_ms' => (int) $selectedPreset['duration_ms'],
|
|
||||||
'notes' => (string) ($selectedPreset['notes'] ?? ''),
|
'notes' => (string) ($selectedPreset['notes'] ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -125,7 +124,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
<div><span>Angle</span><strong id="summary-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
|
<div><span>Angle</span><strong id="summary-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
|
||||||
<div><span>Intensity</span><strong id="summary-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
|
<div><span>Intensity</span><strong id="summary-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
|
||||||
<div><span>Pattern</span><strong id="summary-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
|
<div><span>Pattern</span><strong id="summary-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
|
||||||
<div><span>Tone</span><strong id="summary-tone"><?= e(preset_tone((int) $formData['angle_deg'])) ?></strong></div>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-secondary mb-0 mt-3">Tip: the browser requires a user interaction before triggering vibration. Press <strong>Test vibration</strong> after connecting the controller.</p>
|
<p class="small text-secondary mb-0 mt-3">Tip: the browser requires a user interaction before triggering vibration. Press <strong>Test vibration</strong> after connecting the controller.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -164,18 +162,15 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 stat-row">
|
<div class="row g-3 stat-row">
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-4">
|
||||||
<div class="mini-stat"><span>Recline</span><strong id="stat-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
|
<div class="mini-stat"><span>Recline</span><strong id="stat-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-4">
|
||||||
<div class="mini-stat"><span>Rumble</span><strong id="stat-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
|
<div class="mini-stat"><span>Rumble</span><strong id="stat-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-3">
|
<div class="col-6 col-md-4">
|
||||||
<div class="mini-stat"><span>Pattern</span><strong id="stat-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
|
<div class="mini-stat"><span>Pattern</span><strong id="stat-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-md-3">
|
|
||||||
<div class="mini-stat"><span>Duration</span><strong id="stat-duration"><?= e((string) $formData['duration_ms']) ?> ms</strong></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -197,7 +192,7 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<label class="form-label d-block">Action</label>
|
<label class="form-label d-block">Action</label>
|
||||||
<button type="button" class="btn btn-accent w-100" id="test-vibration">Test vibration</button>
|
<button type="button" class="btn btn-accent w-100" id="test-vibration">Test vibration (continuous)</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -210,18 +205,12 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-2"><label for="intensity_pct" class="form-label mb-0">Vibration intensity</label><span class="value-pill" id="intensity-pill"><?= e((string) $formData['intensity_pct']) ?>%</span></div>
|
<div class="d-flex justify-content-between align-items-center mb-2"><label for="intensity_pct" class="form-label mb-0">Vibration intensity</label><span class="value-pill" id="intensity-pill"><?= e((string) $formData['intensity_pct']) ?>%</span></div>
|
||||||
<input type="range" class="form-range" min="0" max="100" step="1" id="intensity_pct" name="intensity_pct" value="<?= e((string) $formData['intensity_pct']) ?>">
|
<input type="range" class="form-range" min="0" max="100" step="1" id="intensity_pct" name="intensity_pct" value="<?= e((string) $formData['intensity_pct']) ?>">
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3">
|
<div class="control-group">
|
||||||
<div class="col-md-6">
|
<label for="pattern_mode" class="form-label">Pattern</label>
|
||||||
<label for="pattern_mode" class="form-label">Pattern</label>
|
<select class="form-select" id="pattern_mode" name="pattern_mode">
|
||||||
<select class="form-select" id="pattern_mode" name="pattern_mode">
|
<option value="continuous" <?= $formData['pattern_mode'] === 'continuous' ? 'selected' : '' ?>>Continuous</option>
|
||||||
<option value="continuous" <?= $formData['pattern_mode'] === 'continuous' ? 'selected' : '' ?>>Continuous</option>
|
<option value="pulse" <?= $formData['pattern_mode'] === 'pulse' ? 'selected' : '' ?>>Pulse</option>
|
||||||
<option value="pulse" <?= $formData['pattern_mode'] === 'pulse' ? 'selected' : '' ?>>Pulse</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2"><label for="duration_ms" class="form-label mb-0">Duration</label><span class="value-pill" id="duration-pill"><?= e((string) $formData['duration_ms']) ?> ms</span></div>
|
|
||||||
<input type="range" class="form-range mt-3" min="100" max="5000" step="100" id="duration_ms" name="duration_ms" value="<?= e((string) $formData['duration_ms']) ?>">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -261,7 +250,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
<div><span>Angle</span><strong id="save-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
|
<div><span>Angle</span><strong id="save-angle"><?= e((string) $formData['angle_deg']) ?>°</strong></div>
|
||||||
<div><span>Intensity</span><strong id="save-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
|
<div><span>Intensity</span><strong id="save-intensity"><?= e((string) $formData['intensity_pct']) ?>%</strong></div>
|
||||||
<div><span>Pattern</span><strong id="save-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
|
<div><span>Pattern</span><strong id="save-pattern"><?= e(ucfirst($formData['pattern_mode'])) ?></strong></div>
|
||||||
<div><span>Duration</span><strong id="save-duration"><?= e((string) $formData['duration_ms']) ?> ms</strong></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -295,7 +283,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
<div class="preset-metrics mb-3">
|
<div class="preset-metrics mb-3">
|
||||||
<span><?= e((string) $preset['intensity_pct']) ?>% intensity</span>
|
<span><?= e((string) $preset['intensity_pct']) ?>% intensity</span>
|
||||||
<span><?= e(ucfirst((string) $preset['pattern_mode'])) ?></span>
|
<span><?= e(ucfirst((string) $preset['pattern_mode'])) ?></span>
|
||||||
<span><?= e((string) $preset['duration_ms']) ?> ms</span>
|
|
||||||
</div>
|
</div>
|
||||||
<?php if (!empty($preset['notes'])): ?>
|
<?php if (!empty($preset['notes'])): ?>
|
||||||
<p class="text-secondary small mb-3"><?= e($preset['notes']) ?></p>
|
<p class="text-secondary small mb-3"><?= e($preset['notes']) ?></p>
|
||||||
@ -309,7 +296,6 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
|
|||||||
data-angle="<?= e((string) $preset['angle_deg']) ?>"
|
data-angle="<?= e((string) $preset['angle_deg']) ?>"
|
||||||
data-intensity="<?= e((string) $preset['intensity_pct']) ?>"
|
data-intensity="<?= e((string) $preset['intensity_pct']) ?>"
|
||||||
data-pattern="<?= e((string) $preset['pattern_mode']) ?>"
|
data-pattern="<?= e((string) $preset['pattern_mode']) ?>"
|
||||||
data-duration="<?= e((string) $preset['duration_ms']) ?>"
|
|
||||||
data-notes="<?= e((string) ($preset['notes'] ?? '')) ?>"
|
data-notes="<?= e((string) ($preset['notes'] ?? '')) ?>"
|
||||||
>Apply in simulator</button>
|
>Apply in simulator</button>
|
||||||
<a class="btn btn-sm btn-link text-decoration-none px-0" href="/preset.php?id=<?= e((string) $preset['id']) ?>">View detail</a>
|
<a class="btn btn-sm btn-link text-decoration-none px-0" href="/preset.php?id=<?= e((string) $preset['id']) ?>">View detail</a>
|
||||||
|
|||||||
@ -64,14 +64,13 @@ $pageDescription = $preset
|
|||||||
<span class="chip"><?= e((string) $preset['angle_deg']) ?>° angle</span>
|
<span class="chip"><?= e((string) $preset['angle_deg']) ?>° angle</span>
|
||||||
<span class="chip"><?= e((string) $preset['intensity_pct']) ?>% intensity</span>
|
<span class="chip"><?= e((string) $preset['intensity_pct']) ?>% intensity</span>
|
||||||
<span class="chip"><?= e(ucfirst((string) $preset['pattern_mode'])) ?> rumble</span>
|
<span class="chip"><?= e(ucfirst((string) $preset['pattern_mode'])) ?> rumble</span>
|
||||||
<span class="chip"><?= e((string) $preset['duration_ms']) ?> ms</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="panel inset-panel p-3 h-100">
|
<div class="panel inset-panel p-3 h-100">
|
||||||
<div class="small-label mb-3">Profile tone</div>
|
<div class="small-label mb-3">Profile tone</div>
|
||||||
<div class="h4 mb-2"><?= e(preset_tone((int) $preset['angle_deg'])) ?></div>
|
<div class="h4 mb-2"><?= e(preset_tone((int) $preset['angle_deg'])) ?></div>
|
||||||
<p class="text-secondary small mb-0">Saved on <?= e(date('F j, Y H:i', strtotime((string) $preset['created_at']))) ?> UTC.</p>
|
<p class="text-secondary small mb-0">Saved on <?= e(date('F j, Y H:i', strtotime((string) $preset['created_at']))) ?> UTC.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -85,7 +84,6 @@ $pageDescription = $preset
|
|||||||
<div><span>Recline angle</span><strong><?= e((string) $preset['angle_deg']) ?>°</strong></div>
|
<div><span>Recline angle</span><strong><?= e((string) $preset['angle_deg']) ?>°</strong></div>
|
||||||
<div><span>Vibration intensity</span><strong><?= e((string) $preset['intensity_pct']) ?>%</strong></div>
|
<div><span>Vibration intensity</span><strong><?= e((string) $preset['intensity_pct']) ?>%</strong></div>
|
||||||
<div><span>Pattern</span><strong><?= e(ucfirst((string) $preset['pattern_mode'])) ?></strong></div>
|
<div><span>Pattern</span><strong><?= e(ucfirst((string) $preset['pattern_mode'])) ?></strong></div>
|
||||||
<div><span>Duration</span><strong><?= e((string) $preset['duration_ms']) ?> ms</strong></div>
|
|
||||||
<div><span>Saved at</span><strong><?= e((string) $preset['created_at']) ?></strong></div>
|
<div><span>Saved at</span><strong><?= e((string) $preset['created_at']) ?></strong></div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="border-secondary-subtle my-4">
|
<hr class="border-secondary-subtle my-4">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user