diff --git a/.gitignore b/.gitignore index 59baa88..46ae637 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ _ide_helper.php Homestead.json Homestead.yaml -Thumbs.db +Thumb.db /database/database.sqlite /app-release-signed.apk* @@ -30,3 +30,13 @@ Thumbs.db # Credential files (keystores, private keys, etc.) /credentials/ + +# AI tools +.agents +.mcp.json +CLAUDE.md +AGENTS.md +GEMINI.md +.claude +.junie +.gemini diff --git a/app/Livewire/Settings.php b/app/Livewire/Settings.php index d96a60f..8d04d4e 100644 --- a/app/Livewire/Settings.php +++ b/app/Livewire/Settings.php @@ -6,22 +6,24 @@ use App\Enum\BeepLeadIn; use App\Models\Setting; -use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rules\Enum; use Illuminate\View\View; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; use Livewire\Component; -use Nbucic\AudioTts\AudioTTS; #[Layout('layouts.app')] #[Title('Settings — Interval Timer')] class Settings extends Component { public BeepLeadIn $defaultBeepLeadIn = BeepLeadIn::Three; + public string $defaultEndSound = 'triple'; + public string $soundMode = 'beep'; + public float $volume = 0.8; + public bool $keepScreenOn = true; public bool $saved = false; @@ -69,14 +71,13 @@ public function save(): void public function updateAndTest(string $soundMode): void { - Log::info('Updating settings and testing voice mode...'); $this->soundMode = $soundMode; - if ($soundMode === 'voice') { - \Nbucic\AudioTts\Facades\AudioTTS::speak('Where is Darth Vader now?', 1.0); -// $this->dispatch('playVoiceSound', reason: 'test'); - } else { - $this->dispatch('playBeepSound', sound: 'chime'); + if (app()->isLocal()) { + if ($soundMode === 'voice') { + $this->dispatch('play-TTS-Sound', text: '3, 2, 1, - GO'); + } else { + $this->dispatch('playBeepSound', sound: 'triple'); + } } - Log::info('Updating settings and testing voice mode... [DONE]'); } } diff --git a/boost.json b/boost.json new file mode 100644 index 0000000..fd45a88 --- /dev/null +++ b/boost.json @@ -0,0 +1,22 @@ +{ + "agents": [ + "junie", + "claude_code", + "gemini" + ], + "guidelines": true, + "mcp": true, + "nightwatch_mcp": false, + "packages": [ + "nbucic/audio-tts", + "nativephp/mobile" + ], + "sail": false, + "skills": [ + "laravel-best-practices", + "livewire-development", + "pest-testing", + "tailwindcss-development", + "nativephp-mobile" + ] +} diff --git a/composer.json b/composer.json index e48c9dc..64b3604 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ }, "require-dev": { "fakerphp/faker": "^1.23", + "laravel/boost": "^2.4", "laravel/pail": "^1.2.5", "laravel/pint": "^1.27", "mockery/mockery": "^1.6", @@ -29,8 +30,7 @@ }, "autoload": { "psr-4": { - "App\\": "app/", - "Database\\Seeders\\": "database/seeders/" + "App\\": "app/" } }, "autoload-dev": { @@ -52,6 +52,10 @@ "Composer\\Config::disableProcessTimeout", "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others" ], + "mobile": [ + "npm run build -- --mode=android", + "@php artisan native:run android" + ], "test": [ "@php artisan config:clear --ansi", "@php artisan test" diff --git a/composer.lock b/composer.lock index d8d846c..caab011 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bee163741fb2ea58eac28c450d281d22", + "content-hash": "28d31365cf997193a36f285d07a4b900", "packages": [ { "name": "bacon/bacon-qr-code", @@ -7215,6 +7215,145 @@ }, "time": "2025-03-19T14:43:43+00:00" }, + { + "name": "laravel/boost", + "version": "v2.4.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/boost.git", + "reference": "841d52905728cfac9f93c778a1758e740ce9a367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/boost/zipball/841d52905728cfac9f93c778a1758e740ce9a367", + "reference": "841d52905728cfac9f93c778a1758e740ce9a367", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.9", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "laravel/mcp": "^0.5.1|^0.6.0", + "laravel/prompts": "^0.3.10", + "laravel/roster": "^0.5.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.27.0", + "mockery/mockery": "^1.6.12", + "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Boost\\BoostServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Boost\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", + "homepage": "https://github.com/laravel/boost", + "keywords": [ + "ai", + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/boost/issues", + "source": "https://github.com/laravel/boost" + }, + "time": "2026-04-10T15:59:10+00:00" + }, + { + "name": "laravel/mcp", + "version": "v0.6.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/mcp.git", + "reference": "c3775e57b95d7eadb580d543689d9971ec8721f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/mcp/zipball/c3775e57b95d7eadb580d543689d9971ec8721f2", + "reference": "c3775e57b95d7eadb580d543689d9971ec8721f2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" + }, + "require-dev": { + "laravel/pint": "^1.20", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", + "phpstan/phpstan": "^2.1.27", + "rector/rector": "^2.2.4" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" + }, + "providers": [ + "Laravel\\Mcp\\Server\\McpServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Mcp\\": "src/", + "Laravel\\Mcp\\Server\\": "src/Server/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Rapidly build MCP servers for your Laravel applications.", + "homepage": "https://github.com/laravel/mcp", + "keywords": [ + "laravel", + "mcp" + ], + "support": { + "issues": "https://github.com/laravel/mcp/issues", + "source": "https://github.com/laravel/mcp" + }, + "time": "2026-04-15T08:30:42+00:00" + }, { "name": "laravel/pail", "version": "v1.2.6", @@ -7363,6 +7502,67 @@ }, "time": "2026-03-12T15:51:39+00:00" }, + { + "name": "laravel/roster", + "version": "v0.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/roster.git", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb", + "reference": "5089de7615f72f78e831590ff9d0435fed0102bb", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/routing": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/yaml": "^7.2|^8.0" + }, + "require-dev": { + "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0|^11.0", + "pestphp/pest": "^3.0|^4.1", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Roster\\RosterServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Roster\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Detect packages & approaches in use within a Laravel project", + "homepage": "https://github.com/laravel/roster", + "keywords": [ + "dev", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/roster/issues", + "source": "https://github.com/laravel/roster" + }, + "time": "2026-03-05T07:58:43+00:00" + }, { "name": "mockery/mockery", "version": "1.6.12", @@ -7508,7 +7708,7 @@ }, { "name": "nbucic/audio-tts", - "version": "dev-fix/tts-wakelock-caching", + "version": "dev-fix/timer-loading-and-keepscreenon", "dist": { "type": "path", "url": "/home/nikola/projects/private/interval-timer-nativephp/packages/nbucic/audio-tts", @@ -9840,6 +10040,81 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "symfony/yaml", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/54174ab48c0c0f9e21512b304be17f8150ccf8f1", + "reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, { "name": "ta-tikoma/phpunit-architecture-test", "version": "0.8.7", diff --git a/config/nativephp.php b/config/nativephp.php index d12250d..8982051 100644 --- a/config/nativephp.php +++ b/config/nativephp.php @@ -275,6 +275,7 @@ 'resources', 'routes', 'config', + 'database', 'public', ], diff --git a/database/migrations/2026_04_16_081129_initial_program_seed.php b/database/migrations/2026_04_16_081129_initial_program_seed.php index 869c893..b252d14 100644 --- a/database/migrations/2026_04_16_081129_initial_program_seed.php +++ b/database/migrations/2026_04_16_081129_initial_program_seed.php @@ -6,7 +6,8 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; -return new class extends Migration { +return new class extends Migration +{ /** * Run the migrations. */ @@ -37,10 +38,10 @@ public function up(): void ]); foreach ([ - ['label' => 'Warmup', 'duration' => 10, 'repetitions' => 1, 'pause' => 0, 'cooldown' => 5, 'color' => '#3b82f6'], - ['label' => 'Sprint', 'duration' => 8, 'repetitions' => 3, 'pause' => 4, 'cooldown' => 10, 'color' => '#ef4444'], - ['label' => 'Stretch', 'duration' => 8, 'repetitions' => 1, 'pause' => 0, 'cooldown' => 0, 'color' => '#22c55e'], - ] as $index => $phase) { + ['label' => 'Warmup', 'duration' => 10, 'repetitions' => 1, 'pause' => 0, 'cooldown' => 5, 'color' => '#3b82f6'], + ['label' => 'Sprint', 'duration' => 8, 'repetitions' => 3, 'pause' => 4, 'cooldown' => 10, 'color' => '#ef4444'], + ['label' => 'Stretch', 'duration' => 8, 'repetitions' => 1, 'pause' => 0, 'cooldown' => 0, 'color' => '#22c55e'], + ] as $index => $phase) { DB::table('phases')->insert( array_merge( $phase, @@ -51,8 +52,14 @@ public function up(): void ), ); } - } + DB::table('programs')->insert([ + 'id' => Str::uuid7()->toString(), + 'name' => 'Demo with no phases', + 'beep_lead_in' => 3, + 'end_sound' => 'triple', + ]); + } /** * Reverse the migrations. diff --git a/resources/js/app.js b/resources/js/app.js index 3ab345b..35c2c85 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,5 +1,6 @@ import './bootstrap'; -import { initAudio } from './audio'; +import {initAudio} from './audio'; +import audioTTS from "../../packages/nbucic/audio-tts/resources/js/audioTTS.js"; // Boot Alpine after Livewire (Livewire v3 integrates automatically) document.addEventListener('alpine:init', () => { @@ -13,35 +14,35 @@ document.addEventListener('alpine:init', () => { _wakeLock: null, init() { - this.soundMode = this.$wire.soundMode; - this.volume = this.$wire.volume; + this.soundMode = this.$wire.soundMode; + this.volume = this.$wire.volume; this.keepScreenOn = this.$wire.keepScreenOn; - this.program = this.$wire.program; - this.audio = initAudio(this.volume); + this.program = this.$wire.program; + this.audio = initAudio(this.volume); console.log('[TTS] timerAudio init: soundMode=' + this.soundMode + ', keepScreenOn=' + this.keepScreenOn - + ', AndroidTTS=' + !!(window.AndroidTTS && typeof window.AndroidTTS.speak === 'function')); + + ', AndroidTTS=' + (typeof audioTTS.speak === 'function')); // Ticker logic: poll wire.tick() every 1 000 ms when the timer is active - this.$watch('$wire.state', state => { + this.$watch('$wire.state', async state => { console.log('State changed:', state); clearInterval(this.interval); if (['PREPARE', 'RUNNING', 'PAUSE', 'COOLDOWN'].includes(state)) { this.interval = setInterval(() => this.$wire.tick(), 1000); - this._acquireWakeLock(); + await this._acquireWakeLock(); } else { - this._releaseWakeLock(); + await this._releaseWakeLock(); } }); - this.$wire.on('playBeep', ({reason}) => { - console.log('playBeep', reason); + this.$wire.on('playBeep', async ({reason}) => { + console.log(`Beep, reason: ${reason}, mode: ${this.soundMode}`); if (this.soundMode === 'voice') { const text = this.voiceText(reason); console.log('[TTS] playBeep: reason=' + reason + ', voiceText="' + text + '"'); - this.audio.speak(text); + await audioTTS.speak(text); } else if (reason === 'prepare') { this.audio.prepareBeep(); } else { @@ -66,7 +67,7 @@ document.addEventListener('alpine:init', () => { if (soundMode !== undefined) this.soundMode = soundMode; if (volume !== undefined) { this.volume = volume; - this.audio = initAudio(volume); + this.audio = initAudio(volume); } if (keepScreenOn !== undefined) this.keepScreenOn = keepScreenOn; }); @@ -74,10 +75,10 @@ document.addEventListener('alpine:init', () => { voiceText(reason) { const map = { - prepare: this.$wire?.countdownLabel ?? '', - countdown: this.$wire?.countdownLabel ?? 'Get ready', - rep_end: 'Done', - pause_end: 'Go', + prepare: this.$wire?.countdownLabel ?? '', + countdown: this.$wire?.countdownLabel ?? 'Get ready', + rep_end: 'Done', + pause_end: 'Go', cooldown_end: 'Next', }; return map[reason] ?? 'Beep'; @@ -90,7 +91,7 @@ document.addEventListener('alpine:init', () => { try { this._wakeLock = await navigator.wakeLock.request('screen'); console.log('Wake lock acquired'); - // Re-acquire automatically if the OS releases it (e.g. tab hidden then shown) + // Re-acquire automatically if the OS releases it (e.g., tab hidden then shown) this._wakeLock.addEventListener('release', () => { this._wakeLock = null; }); @@ -115,20 +116,24 @@ document.addEventListener('alpine:init', () => { volume: 0.8, init() { - console.log('settings sounds'); this.soundMode = this.$wire.soundMode; this.volume = this.$wire.volume; - this.audio = initAudio(1); + this.audio = initAudio(1); this.$wire.on('playBeepSound', ({sound}) => { - console.log('DEMO'); - debugger; + console.log('Playing beep sound'); if (sound === 'triple') { this.audio.tripleBeep(); } else { this.audio.chime(); } }); + + this.$wire.on('play-TTS-Sound', ({text}) => { + console.log('Playing ho ho ho'); + audioTTS.speak(text).then(() => { + }); + }) } })) }); diff --git a/resources/js/audio.js b/resources/js/audio.js index 683d54e..67ef860 100644 --- a/resources/js/audio.js +++ b/resources/js/audio.js @@ -1,16 +1,5 @@ -// noinspection JSUnresolvedReference - -import audioTTS from "../../packages/nbucic/audio-tts/resources/js/audioTTS.js"; - /** * Audio engine for the interval timer. - * - * soundMode = 'beep' -> Web Audio API (synthesized, no network) - * soundMode = 'voice' -> Android TTS via window.AndroidTTS bridge (TTSBridge.kt) - * Falls back to Web Speech API in browser dev. - * - * All methods are no-ops until the user has interacted with the page, - * satisfying the browser AudioContext autoplay policy. */ export function initAudio(volume = 0.8) { volume = Math.max(0, Math.min(1, volume)); @@ -18,7 +7,7 @@ export function initAudio(volume = 0.8) { function getCtx() { if (!ctx) { - ctx = new (window.AudioContext || window.webkitAudioContext)(); + ctx = new (window.AudioContext)(); } if (ctx.state === 'suspended') { ctx.resume(); @@ -28,13 +17,13 @@ export function initAudio(volume = 0.8) { /** Play a single short beep at the given frequency and duration. */ function tone(freq = 880, durationMs = 120, delayMs = 0) { - const c = getCtx(); - const osc = c.createOscillator(); - const gain = c.createGain(); - const start = c.currentTime + delayMs / 1000; - const end = start + durationMs / 1000; + const c = getCtx(); + const osc = c.createOscillator(); + const gain = c.createGain(); + const start = c.currentTime + delayMs / 1000; + const end = start + durationMs / 1000; - osc.type = 'sine'; + osc.type = 'sine'; osc.frequency.setValueAtTime(freq, start); gain.gain.setValueAtTime(volume * 0.6, start); gain.gain.exponentialRampToValueAtTime(0.001, end); @@ -46,7 +35,9 @@ export function initAudio(volume = 0.8) { } /** Single countdown beep (800 Hz, 100 ms). */ - function beep() { tone(800, 100); } + function beep() { + tone(800, 100); + } /** Prepare-phase beep -- three rapid beeps at the same tone (800 Hz, 100 ms x 3). */ function prepareBeep() { @@ -56,7 +47,9 @@ export function initAudio(volume = 0.8) { } /** Gentle single beep on user pause (600 Hz, 80 ms). */ - function pauseBeep() { tone(600, 80); } + function pauseBeep() { + tone(600, 80); + } /** * End sound: triple beep (three 880 Hz tones 150 ms apart). @@ -74,20 +67,8 @@ export function initAudio(volume = 0.8) { */ function chime() { tone(1046.5, 300, 0); // C6 - tone(880, 300, 120); // A5 - tone(698.5, 400, 240); // F5 - } - - /** - * Speak text via the Android TTS bridge (window.AndroidTTS, registered by - * TTSBridge.kt) when running inside the NativePHP WebView. - * - * Falls back to Web Speech API for browser-based development. - * Logs every decision point so the full chain is visible in both - * Android Logcat (via WebChromeClient console forwarding) and DevTools. - */ - function speak(text) { - return audioTTS.speak(text); + tone(880, 300, 120); // A5 + tone(698.5, 400, 240); // F5 } return { @@ -96,6 +77,5 @@ export function initAudio(volume = 0.8) { pauseBeep, tripleBeep, chime, - speak, }; } diff --git a/resources/views/livewire/program-editor.blade.php b/resources/views/livewire/program-editor.blade.php index 9b1cd4f..cd69113 100644 --- a/resources/views/livewire/program-editor.blade.php +++ b/resources/views/livewire/program-editor.blade.php @@ -251,19 +251,18 @@ class="w-full bg-gray-800 text-white rounded-xl px-4 py-3 {{ $this->editingIsLastPhase() ? 'text-gray-600' : 'text-gray-400' }}"> Cooldown (sec) -
+
@if($this->editingIsLastPhase()) - not counted — add another phase + The cooldown period for the last phase will not be counted in total duration @else After final rep @endif
editingIsLastPhase()) disabled @endif class="w-full rounded-xl px-4 py-3 border text-center text-lg font-bold focus:outline-none transition-colors {{ $this->editingIsLastPhase() - ? 'bg-gray-800/40 text-gray-600 border-white/5 cursor-not-allowed' + ? 'bg-gray-800/40 text-gray-600 border-white/5' : 'bg-gray-800 text-white border-white/10 focus:border-blue-500' }}"> diff --git a/version.json b/version.json index 1d8736a..9d0278d 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "1.0.10", - "version_code": 11 + "version": "1.0.11", + "version_code": 12 }