diff --git a/config/bootstrap.php b/config/bootstrap.php index 5302ec2..1d58fef 100644 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -66,6 +66,8 @@ function confirm_setup(): void { validate_storage_subdirs(); validate_tables(); validate_table_contents(); + migrate_db(); + migrate_tick_files(); } // Make sure the storage/ directory exists and is writable @@ -114,6 +116,38 @@ function validate_storage_subdirs(): void { } } +function migrate_tick_files() { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(TICKS_DIR, RecursiveDirectoryIterator::SKIP_DOTS) + ); + + foreach ($files as $file) { + if ($file->isFile() && str_ends_with($file->getFilename(), '.txt')) { + migrate_tick_file($file->getPathname()); + } + } +} + +function migrate_tick_file($filepath) { + $lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $modified = false; + + foreach ($lines as &$line) { + $fields = explode('|', $line); + if (count($fields) === 2) { + // Convert id|text to id|emoji|text + $line = $fields[0] . '||' . $fields[1]; + $modified = true; + } + } + + if ($modified) { + file_put_contents($filepath, implode("\n", $lines) . "\n"); + // TODO: log properly + //echo "Migrated: " . basename($filepath) . "\n"; + } +} + function get_db(): PDO { try { // SQLite will just create this if it doesn't exist. @@ -132,6 +166,93 @@ function get_db(): PDO { return $db; } +// The database version will just be an int +// stored as PRAGMA user_version. It will +// correspond to the most recent migration file applied to the db. +function get_db_version(): int { + $db = get_db(); + + return $db->query("PRAGMA user_version")->fetchColumn() ?? 0; +} + +function migration_number_from_file(string $filename): int { + $basename = basename($filename, '.sql'); + $parts = explode('_', $basename); + return (int) $parts[0]; +} + +function set_db_version(int $newVersion): void { + $currentVersion = get_db_version(); + + if ($newVersion <= $currentVersion){ + throw new SetupException( + "New version ($newVersion) must be greater than current version ($currentVersion)", + 'db_migration' + ); + } + + $db = get_db(); + $db->exec("PRAGMA user_version = $newVersion"); +} + +function get_pending_migrations(): array { + $currentVersion = get_db_version(); + $files = glob(DATA_DIR . '/migrations/*.sql'); + + $pending = []; + foreach ($files as $file) { + $version = migration_number_from_file($file); + if ($version > $currentVersion) { + $pending[$version] = $file; + } + } + + ksort($pending); + return $pending; +} + +function migrate_db(): void { + $migrations = get_pending_migrations(); + + if (empty($migrations)) { + # TODO: log + return; + } + + $db = get_db(); + $db->beginTransaction(); + + try { + foreach ($migrations as $version => $file) { + $filename = basename($file); + // TODO: log properly + + $sql = file_get_contents($file); + if ($sql === false) { + throw new Exception("Could not read migration file: $file"); + } + + // Execute the migration SQL + $db->exec($sql); + } + + // Update db version + $db->commit(); + set_db_version($version); + //TODO: log properly + //echo "All migrations completed successfully.\n"; + + } catch (Exception $e) { + $db->rollBack(); + throw new SetupException( + "Migration failed: $filename", + 'db_migration', + 0, + $e + ); + } +} + function create_tables(): void { $db = get_db(); diff --git a/public/css/default.css b/public/css/default.css index dd550fe..7ea9c62 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -72,6 +72,16 @@ select { box-sizing: border-box; } +input[type="checkbox"] { + width: auto; + height: 2rem; + aspect-ratio: 1; + margin: 0; + cursor: pointer; + justify-self: start; + align-self: center; +} + /* A bit of custom styling for the file input */ input[type="file"] { border-style: dashed; @@ -429,6 +439,13 @@ time { - Once the width exceeds that (e.g. desktops), it will convert to horizontal alignment */ @media (min-width: 600px) { + input[type="checkbox"] { + height: 100%; + /*grid-column: 2;*/ + justify-self: start; + align-self: center; + } + label { text-align: right; padding-top: 10px; diff --git a/src/Controller/AdminController/AdminController.php b/src/Controller/AdminController/AdminController.php index 7bbd735..0ed2182 100644 --- a/src/Controller/AdminController/AdminController.php +++ b/src/Controller/AdminController/AdminController.php @@ -52,22 +52,24 @@ class AdminController extends Controller { if ($_SERVER['REQUEST_METHOD'] === 'POST') { $errors = []; - // UserModel profile - $username = trim($_POST['username'] ?? ''); - $displayName = trim($_POST['display_name'] ?? ''); - $about = trim($_POST['about'] ?? ''); - $website = trim($_POST['website'] ?? ''); + // User profile + $username = trim($_POST['username'] ?? ''); + $displayName = trim($_POST['display_name'] ?? ''); + $about = trim($_POST['about'] ?? ''); + $website = trim($_POST['website'] ?? ''); // Site settings - $siteTitle = trim($_POST['site_title']) ?? ''; - $siteDescription = trim($_POST['site_description']) ?? ''; - $baseUrl = trim($_POST['base_url'] ?? ''); - $basePath = trim($_POST['base_path'] ?? '/'); - $itemsPerPage = (int) ($_POST['items_per_page'] ?? 25); + $siteTitle = trim($_POST['site_title']) ?? ''; + $siteDescription = trim($_POST['site_description']) ?? ''; + $baseUrl = trim($_POST['base_url'] ?? ''); + $basePath = trim($_POST['base_path'] ?? '/'); + $itemsPerPage = (int) ($_POST['items_per_page'] ?? 25); + $strictAccessibility = isset($_POST['strict_accessibility']); + $showTickMood = isset($_POST['strict_accessibility']); // Password - $password = $_POST['password'] ?? ''; - $confirmPassword = $_POST['confirm_password'] ?? ''; + $password = $_POST['password'] ?? ''; + $confirmPassword = $_POST['confirm_password'] ?? ''; // Validate user profile if (!$username) { @@ -112,6 +114,8 @@ class AdminController extends Controller { $config->baseUrl = $baseUrl; $config->basePath = $basePath; $config->itemsPerPage = $itemsPerPage; + $config->strictAccessibility = $strictAccessibility; + $config->showTickMood = $showTickMood; // Save site settings and reload config from database // TODO - raise and handle exception on failure diff --git a/src/Model/ConfigModel/ConfigModel.php b/src/Model/ConfigModel/ConfigModel.php index 4735d70..c7fe667 100644 --- a/src/Model/ConfigModel/ConfigModel.php +++ b/src/Model/ConfigModel/ConfigModel.php @@ -8,6 +8,8 @@ class ConfigModel { public int $itemsPerPage = 25; public string $timezone = 'relative'; public ?int $cssId = null; + public bool $strictAccessibility = true; + public bool $showTickMood = true; // load config from sqlite database public static function load(): self { @@ -17,7 +19,16 @@ class ConfigModel { $c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath; global $db; - $stmt = $db->query("SELECT site_title, site_description, base_url, base_path, items_per_page, css_id FROM settings WHERE id=1"); + $stmt = $db->query("SELECT site_title, + site_description, + base_url, + base_path, + items_per_page, + css_id, + strict_accessibility, + show_tick_mood + FROM settings WHERE id=1"); + $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) { @@ -26,7 +37,8 @@ class ConfigModel { $c->baseUrl = $row['base_url']; $c->basePath = $row['base_path']; $c->itemsPerPage = (int) $row['items_per_page']; - $c->cssId = (int) $row['css_id']; + $c->strictAccessibility = (bool) $row['strict_accessibility']; + $c->showTickMood = (bool) $row['show_tick_mood']; } return $c; @@ -49,11 +61,39 @@ class ConfigModel { $settingsCount = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn(); if ($settingsCount === 0){ - $stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page, css_id) VALUES (1, ?, ?, ?, ?, ?, ?)"); + $stmt = $db->prepare("INSERT INTO settings ( + id, + site_title, + site_description, + base_url, + base_path, + items_per_page, + css_id, + strict_accessibility, + show_tick_mood + ) + VALUES (1, ?, ?, ?, ?, ?, ?)"); } else { - $stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=?, css_id=? WHERE id=1"); + $stmt = $db->prepare("UPDATE settings SET + site_title=?, + site_description=?, + base_url=?, + base_path=?, + items_per_page=?, + css_id=?, + strict_accessibility=?, + show_tick_mood=? + WHERE id=1"); } - $stmt->execute([$this->siteTitle, $this->siteDescription, $this->baseUrl, $this->basePath, $this->itemsPerPage, $this->cssId]); + $stmt->execute([$this->siteTitle, + $this->siteDescription, + $this->baseUrl, + $this->basePath, + $this->itemsPerPage, + $this->cssId, + $this->strictAccessibility, + $this->showTickMood + ]); return self::load(); } diff --git a/src/Model/TickModel/TickModel.php b/src/Model/TickModel/TickModel.php index d8b44a7..ab472a8 100644 --- a/src/Model/TickModel/TickModel.php +++ b/src/Model/TickModel/TickModel.php @@ -34,9 +34,7 @@ class TickModel { // Ticks are pipe-delimited: timestamp|text // But just in case a tick contains a pipe, only split on the first one that occurs - $tickParts = explode('|', $line, 2); - $time = $tickParts[0]; - $tick = $tickParts[1]; + list($time, $emoji, $tick) = explode('|', $line, 3); // Build the timestamp from the date and time // Ticks are always stored in UTC @@ -90,10 +88,13 @@ class TickModel { $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { if (str_starts_with($line, $timestamp)) { - $tick = explode('|', $line)[1]; + echo $line; + exit; + list($time, $emoji, $tick) = explode('|', $line, 3); return [ 'tickTime' => $tickTime, + 'emoji' => $emoji, 'tick' => $tick, 'config' => ConfigModel::load(), ]; diff --git a/storage/db/migrations/001_add_accessibility_setting.sql b/storage/db/migrations/001_add_accessibility_setting.sql new file mode 100755 index 0000000..38035c8 --- /dev/null +++ b/storage/db/migrations/001_add_accessibility_setting.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings +ADD COLUMN strict_accessibility BOOLEAN DEFAULT TRUE; diff --git a/storage/db/migrations/002_add_show_tick_mood_setting.sql b/storage/db/migrations/002_add_show_tick_mood_setting.sql new file mode 100755 index 0000000..f6c5541 --- /dev/null +++ b/storage/db/migrations/002_add_show_tick_mood_setting.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings +ADD COLUMN show_tick_mood BOOLEAN DEFAULT TRUE; diff --git a/storage/db/tkr.sqlite.bak b/storage/db/tkr.sqlite.bak new file mode 100755 index 0000000..e677bd9 Binary files /dev/null and b/storage/db/tkr.sqlite.bak differ diff --git a/templates/partials/admin.php b/templates/partials/admin.php index e5ef650..c8bb6f0 100644 --- a/templates/partials/admin.php +++ b/templates/partials/admin.php @@ -57,6 +57,18 @@ name="items_per_page" value="= $config->itemsPerPage ?>" min="1" max="50" required> + + strictAccessibility): ?> checked > + + showTickMood): ?> checked >