feat: added sounds and fixed skip button functionality

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-02-14 19:00:08 +01:00
parent f68a5cc3a7
commit 688a6d5d36
11 changed files with 451 additions and 5 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -9,6 +9,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4"
url: "https://pub.dev"
source: hosted
version: "6.5.1"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605"
url: "https://pub.dev"
source: hosted
version: "5.2.1"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001
url: "https://pub.dev"
source: hosted
version: "4.2.1"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656"
url: "https://pub.dev"
source: hosted
version: "7.1.1"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -33,6 +89,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -41,6 +105,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -57,6 +129,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -75,6 +171,43 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hooks:
dependency: transitive
description:
name: hooks
sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
http:
dependency: transitive
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -107,6 +240,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -131,6 +272,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac"
url: "https://pub.dev"
source: hosted
version: "0.17.4"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
url: "https://pub.dev"
source: hosted
version: "9.3.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -139,6 +296,78 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
url: "https://pub.dev"
source: hosted
version: "2.2.22"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -176,6 +405,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -192,6 +429,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.9" version: "0.7.9"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
url: "https://pub.dev"
source: hosted
version: "4.5.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -208,6 +461,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.11.0 <4.0.0" dart: ">=3.11.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.38.4"

View File

@ -34,6 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
audioplayers: ^6.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -56,10 +57,8 @@ flutter:
# the material Icons class. # the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this: assets:
# assets: - assets/sounds/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images

View File

@ -0,0 +1,63 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pomodoro_app/models/pomodoro_state.dart';
import 'package:pomodoro_app/services/sound_service.dart';
void main() {
group('SoundService', () {
late List<String> playedAssets;
late SoundService service;
setUp(() {
playedAssets = [];
service = SoundService(
playCallback: (assetPath) async => playedAssets.add(assetPath),
);
});
tearDown(() {
service.dispose();
});
test('plays work_done when work ends with short break next', () async {
await service.playTransitionSound(
completedMode: PomodoroMode.work,
nextMode: PomodoroMode.shortBreak,
);
expect(playedAssets, ['work_done.wav']);
});
test('plays long_break_start when work ends with long break next',
() async {
await service.playTransitionSound(
completedMode: PomodoroMode.work,
nextMode: PomodoroMode.longBreak,
);
expect(playedAssets, ['long_break_start.wav']);
});
test('plays short_break_done when short break ends', () async {
await service.playTransitionSound(
completedMode: PomodoroMode.shortBreak,
nextMode: PomodoroMode.work,
);
expect(playedAssets, ['short_break_done.wav']);
});
test('plays long_break_done when long break ends', () async {
await service.playTransitionSound(
completedMode: PomodoroMode.longBreak,
nextMode: PomodoroMode.work,
);
expect(playedAssets, ['long_break_done.wav']);
});
test('does nothing after dispose', () async {
service.dispose();
await service.playTransitionSound(
completedMode: PomodoroMode.work,
nextMode: PomodoroMode.shortBreak,
);
expect(playedAssets, isEmpty);
});
});
}

View File

@ -0,0 +1 @@
"""Pomodoro app development tools."""

View File

@ -0,0 +1,101 @@
"""Generate distinct notification sounds for the Pomodoro app."""
from __future__ import annotations
import logging
import math
from pathlib import Path
import struct
import wave
logger = logging.getLogger(__name__)
SAMPLE_RATE = 44100
def _write_wav(path: Path, samples: list[int]) -> None:
with wave.open(str(path), "w") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(SAMPLE_RATE)
w.writeframes(struct.pack(f"<{len(samples)}h", *samples))
def _tone(freq: float, duration: float, volume: float = 0.7) -> list[int]:
n = int(SAMPLE_RATE * duration)
return [
int(volume * 32767 * math.sin(2 * math.pi * freq * i / SAMPLE_RATE))
for i in range(n)
]
def _fade(samples: list[int], fade_ms: int = 20) -> list[int]:
n = int(SAMPLE_RATE * fade_ms / 1000)
out = list(samples)
for i in range(min(n, len(out))):
out[i] = int(out[i] * i / n)
for i in range(min(n, len(out))):
out[-(i + 1)] = int(out[-(i + 1)] * i / n)
return out
def _silence(duration: float) -> list[int]:
return [0] * int(SAMPLE_RATE * duration)
def work_done(out: Path) -> None:
"""End of pomodoro: upward three-note chime (C5-E5-G5)."""
samples = (
_fade(_tone(523.25, 0.2))
+ _silence(0.05)
+ _fade(_tone(659.25, 0.2))
+ _silence(0.05)
+ _fade(_tone(783.99, 0.4))
)
_write_wav(out, samples)
def short_break_done(out: Path) -> None:
"""End of short break: two gentle pings (G5-C6)."""
samples = _fade(_tone(783.99, 0.15)) + _silence(0.08) + _fade(_tone(1046.50, 0.3))
_write_wav(out, samples)
def long_break_start(out: Path) -> None:
"""Start of long break: descending celebration (G5-E5-C5-C4 long)."""
samples = (
_fade(_tone(783.99, 0.15))
+ _silence(0.04)
+ _fade(_tone(659.25, 0.15))
+ _silence(0.04)
+ _fade(_tone(523.25, 0.15))
+ _silence(0.04)
+ _fade(_tone(261.63, 0.6, volume=0.5))
)
_write_wav(out, samples)
def long_break_done(out: Path) -> None:
"""End of long break: wake-up alarm — rapid repeated beeps."""
beep = _fade(_tone(880.0, 0.1))
gap = _silence(0.08)
samples = (beep + gap) * 4 + _fade(_tone(1046.50, 0.3))
_write_wav(out, samples)
def main() -> None:
"""Generate all notification sounds and log file sizes."""
out_dir = Path(__file__).resolve().parent.parent / "assets" / "sounds"
out_dir.mkdir(parents=True, exist_ok=True)
work_done(out_dir / "work_done.wav")
short_break_done(out_dir / "short_break_done.wav")
long_break_start(out_dir / "long_break_start.wav")
long_break_done(out_dir / "long_break_done.wav")
for f in sorted(out_dir.glob("*.wav")):
logger.info(" %s (%d bytes)", f.name, f.stat().st_size)
if __name__ == "__main__":
main()