Auto commit: 2026-03-23T19:22:21.572Z

This commit is contained in:
Flatlogic Bot 2026-03-23 19:22:21 +00:00
parent 16b7d41fc5
commit e713c1471c
4 changed files with 48 additions and 77 deletions

21
app.php
View File

@ -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'
@ -163,4 +152,4 @@ function preset_tone(int $angle): string
function e(?string $value): string function e(?string $value): string
{ {
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
} }

View File

@ -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);
}
}); });
}); });

View File

@ -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>
@ -347,4 +333,4 @@ if (isset($_GET['saved']) && ctype_digit((string) $_GET['saved'])) {
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></script>
<script src="/assets/js/main.js?v=<?= e($assetVersion) ?>" defer></script> <script src="/assets/js/main.js?v=<?= e($assetVersion) ?>" defer></script>
</body> </body>
</html> </html>

View File

@ -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">
@ -111,4 +109,4 @@ $pageDescription = $preset
</div> </div>
</main> </main>
</body> </body>
</html> </html>