A simple interval timer for Android, Linux and the browser. EasyTimer is a Flutter rewrite of Interval Timer, with large controls and high-contrast light/dark themes suited to e-ink screens. Text is black on white or white on black. Green ACTIVE and red REST labels have at least 7:1 contrast; the coloured control-button fills are preserved. Text and controls scale with the window for reading from a distance. Wide timer windows put the countdown beside a vertical control column and use the available space without a fixed size cap.
On desktop browsers, the web app opens in a phone frame with a short about panel. Full screen expands the app to fill the display; Escape returns to the frame. Phones, tablets and narrow windows open the full-page app. The page follows the system light/dark theme and uses Czech or English; the app keeps all five languages.
Set Active, Rest and Sets, then press Start. Preparation lasts five seconds. An empty or zero set count repeats indefinitely. The last settings and volume are saved locally. Volume uses whole percentages from 0 to 100. English, Czech, Catalan, Spanish and French follow the system language, with English as the fallback.
Space starts, pauses or resumes; Escape stops; Up/Down change the timer volume by 5%. Android's volume keys always control media volume. The timer does not request audio focus, so music and podcasts can keep playing.
Pausing preparation restarts its five seconds on resume. Pausing an active set
preserves its remaining time. Resuming from rest skips the rest of that break and
prepares the next set. The screen stays awake while the timer is shown, including
while paused, where the platform allows it. On Linux this uses the desktop's
Inhibit portal through wakelock_plus; support depends on the desktop portal
backend. Web wake locks are reconciled whenever the timer becomes visible,
including when its window stays unfocused.
Use Flutter 3.47.7 / Dart 3.13.5, an Android SDK for APK builds, and the standard
Flutter Linux build dependencies plus GStreamer development headers
(libgstreamer1.0-dev and libgstreamer-plugins-base1.0-dev on Debian/Ubuntu)
for desktop builds. From app/:
flutter pub get --enforce-lockfile
flutter analyze
flutter test --concurrency=4 --no-pub
flutter test --concurrency=4 --no-pub --platform chrome --dart-define=WEB_PLUGIN_TESTS=true test/web_platform_test.dart test/web_wake_lock_test.dart
flutter test --concurrency=4 --no-pub --wasm --platform chrome --dart-define=WEB_PLUGIN_TESTS=true test/web_platform_test.dart test/web_wake_lock_test.dart
./tool/build_web.sh
./tool/build_web.sh --wasm
flutter build linux --release
flutter build apk --releaseRun the isolated Linux portal test using the private-bus setup in the packaging instructions.
The web output is app/build/web/; serve it over HTTPS (localhost also works for
development). Screen Wake Lock requires a secure origin and may be denied by the
browser. WASM works without cross-origin isolation using its single-threaded
renderer, including on GitHub Pages.
Linux audio needs GStreamer with its base and good plugins
(gstreamer1.0-plugins-base and gstreamer1.0-plugins-good on Debian/Ubuntu).
Flatpak supplies these through the Freedesktop runtime and uses
--socket=pulseaudio for host audio. Audio is decoded locally; no online services,
proprietary audio SDKs or Play services are used. See
packaging instructions for releases and installation.
The committed golden tests load the bundled fonts and cover 824×1648 physical
pixels at DPR 1.5, 1.875 and 2.5, 420×760 and 640×400 desktop windows, a
1440×900 browser, and 1920×1080 / 3200×1800 fullscreen layouts at DPR 1 and 2.
Long French labels and a three-digit countdown are covered too.
Run the UI, golden and bundled font/licence checks together with
flutter test --concurrency=4 --no-pub test/widget_test.dart test/golden_test.dart test/font_manifest_test.dart.
Generated localization bindings are tracked. Regenerate them
with flutter gen-l10n after editing the ARB files; CI checks the result.
After an intentional visual change, regenerate goldens with:
flutter test --concurrency=4 test/golden_test.dart --update-goldensapp/lib/timer_engine.dart derives the current phase, set, display and sound
boundaries from a monotonic clock. Android and Linux read CLOCK_BOOTTIME
through FFI, including suspend; the web uses performance.now(). Late callbacks
do not extend a phase; cues already stale when noticed are skipped. Timer text updates on displayed
second or state changes, with a separate volume subscription.
workout_controller.dart connects the engine to preferences and the platform
backends. Timing, cue policy and playback coordination are Dart. The finish WAV's
PCM samples are tripled at load time so the three hits play as one continuous
buffer. Native playback preloads one audioplayers player per cue kind and rewinds
it after completion or Stop, so a ready cue only needs resume. A cue waits for
any rewind still in progress. Android silently primes each player once during
initialization, then pauses and rewinds it before restoring the current volume.
The media context requests no audio focus, including during priming. Device mixer
latency remains outside the app's control.
Audio errors invalidate the affected player's preparation. Later playback reloads
its source and, on Android, silently primes it again before playing a cue.
Linux reuses stable audio/alarm.wav and audio/end.wav files in the application's
cache directory. Files are published atomically and recreated after eviction.
The original bundled WAV files are unchanged. The web backend decodes the same buffers into
Web Audio and uses a GainNode, scheduling five minutes ahead and topping up on
ticks and visibility changes. It rebases future
audio after context interruption.
The shared cue policy drops expired alarms and delivers the completion sequence
once. Successful cues gate the next alarm by planned time, so delivery jitter
cannot drop a three-second rest cue. Pause, Stop and a new plan cancel queued
sounds and reset that guard. Volume changes apply immediately; gain is the square
of the slider fraction, matching the original app. Volume saves after a 250 ms
debounce; interval settings save serialized snapshots on each edit and on Start.
Linux validates its preference file before loading it. Invalid data is preserved
untouched in a .corrupt backup, and valid entries are kept in the repaired store.
Unavailable storage is retried on later saves. Only the volumePercent preference is read; older fractional volume values are not
migrated.
Native alarm eligibility is checked when a cue becomes due. Once accepted,
a cue plays even if preparation or playback starts late; plugin start latency
is not bounded. A start that returns after cancellation is stopped immediately,
but a short stale sound can still occur. On Linux,
audioplayers can wait indefinitely for a GStreamer state change. A stalled
pipeline can block Stop and the window; Dart cannot interrupt that native wait.
As in the Android original, there is no foreground service or notification in this version. A suspended device, terminated process or discarded browser tab cannot guarantee alarms. An active workout is not restored after process exit.
EasyTimer is licensed under GPL-3.0.
The original timer behaviour, layout, translations and the unmodified
hit.wav and threesecbeep.wav assets come from Interval Timer, © 2023 Matthew
Nienow and contributors, under the MIT licence. Volume labels come from Jiří
Němec's MIT-licensed fork. The generic monospace typography is supplied by
bundled Droid Sans Mono, © 2007 Google Corporation, under the Apache License
2.0. The speaker paths are Google Material Design Icons under Apache-2.0.
The web wake-lock plugin includes NoSleep.js, © Rich Tibbett, under MIT.
Fonts are never fetched at runtime. Full attribution, pinned sources and
third-party licence texts are listed in NOTICE.