From 18b303073731bd3bc3e6627b8cf3ef929fa17e6e Mon Sep 17 00:00:00 2001 From: Frederik Feichtmeier Date: Sat, 26 Oct 2024 22:32:25 +0200 Subject: [PATCH] chore: refactor to MainPageIsPlayingIcon (#984) --- .../view/main_page_icon.dart} | 24 +- lib/app/view/master_detail_page.dart | 12 +- lib/common/view/audio_signal_indicator.dart | 221 ++++++++++++++++++ .../view/audio_type_is_playing_indicator.dart | 123 ---------- lib/local_audio/view/local_audio_page.dart | 38 --- lib/podcasts/view/podcasts_page.dart | 46 ---- 6 files changed, 246 insertions(+), 218 deletions(-) rename lib/{radio/view/radio_page_icon.dart => app/view/main_page_icon.dart} (52%) create mode 100644 lib/common/view/audio_signal_indicator.dart delete mode 100644 lib/common/view/audio_type_is_playing_indicator.dart diff --git a/lib/radio/view/radio_page_icon.dart b/lib/app/view/main_page_icon.dart similarity index 52% rename from lib/radio/view/radio_page_icon.dart rename to lib/app/view/main_page_icon.dart index 99a0d48fe..1e18a3608 100644 --- a/lib/radio/view/radio_page_icon.dart +++ b/lib/app/view/main_page_icon.dart @@ -2,31 +2,34 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import '../../common/data/audio.dart'; -import '../../common/view/audio_type_is_playing_indicator.dart'; +import '../../common/view/audio_signal_indicator.dart'; import '../../common/view/icons.dart'; import '../../constants.dart'; import '../../extensions/build_context_x.dart'; import '../../player/player_model.dart'; import '../../settings/settings_model.dart'; -class RadioPageIcon extends StatelessWidget with WatchItMixin { - const RadioPageIcon({ +class MainPageIcon extends StatelessWidget with WatchItMixin { + const MainPageIcon({ super.key, required this.selected, + required this.audioType, }); final bool selected; + final AudioType audioType; @override Widget build(BuildContext context) { - final audioType = watchPropertyValue((PlayerModel m) => m.audio?.audioType); + final currentAudioType = + watchPropertyValue((PlayerModel m) => m.audio?.audioType); final isPlaying = watchPropertyValue((PlayerModel m) => m.isPlaying); final useMoreAnimations = watchPropertyValue((SettingsModel m) => m.useMoreAnimations); - if (useMoreAnimations && audioType == AudioType.radio) { + if (useMoreAnimations && currentAudioType == audioType) { if (isPlaying) { - return const AudioTypeIsPlayingIndicator(thickness: 1); + return const ActiveAudioSignalIndicator(thickness: 1); } else { return Icon( Iconz.playFilled, @@ -37,7 +40,14 @@ class RadioPageIcon extends StatelessWidget with WatchItMixin { return Padding( padding: kMainPageIconPadding, - child: selected ? Icon(Iconz.radioFilled) : Icon(Iconz.radio), + child: switch (audioType) { + AudioType.local => + selected ? Icon(Iconz.localAudioFilled) : Icon(Iconz.localAudio), + AudioType.radio => + selected ? Icon(Iconz.radioFilled) : Icon(Iconz.radio), + AudioType.podcast => + selected ? Icon(Iconz.podcastFilled) : Icon(Iconz.podcast), + }, ); } } diff --git a/lib/app/view/master_detail_page.dart b/lib/app/view/master_detail_page.dart index d0974c5c9..6a6bfbd1d 100644 --- a/lib/app/view/master_detail_page.dart +++ b/lib/app/view/master_detail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; import 'package:yaru/yaru.dart'; +import '../../common/data/audio.dart'; import '../../common/view/back_gesture.dart'; import '../../common/view/global_keys.dart'; import '../../common/view/header_bar.dart'; @@ -25,11 +26,11 @@ import '../../podcasts/view/podcast_page_side_bar_icon.dart'; import '../../podcasts/view/podcast_page_title.dart'; import '../../podcasts/view/podcasts_page.dart'; import '../../radio/view/radio_page.dart'; -import '../../radio/view/radio_page_icon.dart'; import '../../radio/view/station_page.dart'; import '../../radio/view/station_page_icon.dart'; import '../../search/view/search_page.dart'; import '../../settings/view/settings_tile.dart'; +import 'main_page_icon.dart'; import 'master_tile.dart'; class MasterDetailPage extends StatelessWidget with WatchItMixin { @@ -204,7 +205,8 @@ List createMasterItems({required LibraryModel libraryModel}) { MasterItem( titleBuilder: (context) => Text(context.l10n.local), pageBuilder: (_) => const LocalAudioPage(), - iconBuilder: (selected) => LocalAudioPageIcon( + iconBuilder: (selected) => MainPageIcon( + audioType: AudioType.local, selected: selected, ), pageId: kLocalAudioPageId, @@ -212,7 +214,8 @@ List createMasterItems({required LibraryModel libraryModel}) { MasterItem( titleBuilder: (context) => Text(context.l10n.radio), pageBuilder: (_) => const RadioPage(), - iconBuilder: (selected) => RadioPageIcon( + iconBuilder: (selected) => MainPageIcon( + audioType: AudioType.radio, selected: selected, ), pageId: kRadioPageId, @@ -220,7 +223,8 @@ List createMasterItems({required LibraryModel libraryModel}) { MasterItem( titleBuilder: (context) => Text(context.l10n.podcasts), pageBuilder: (_) => const PodcastsPage(), - iconBuilder: (selected) => PodcastsPageIcon( + iconBuilder: (selected) => MainPageIcon( + audioType: AudioType.podcast, selected: selected, ), pageId: kPodcastsPageId, diff --git a/lib/common/view/audio_signal_indicator.dart b/lib/common/view/audio_signal_indicator.dart new file mode 100644 index 000000000..0c30ad536 --- /dev/null +++ b/lib/common/view/audio_signal_indicator.dart @@ -0,0 +1,221 @@ +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'theme.dart'; + +class ActiveAudioSignalIndicator extends StatefulWidget { + final Color? color; + final double thickness; + + const ActiveAudioSignalIndicator({ + this.color, + this.thickness = 2.0, + super.key, + }); + + @override + State createState() => + _ActiveAudioSignalIndicatorState(); +} + +class _ActiveAudioSignalIndicatorState extends State + with TickerProviderStateMixin { + static const _delayInMills = [770, 290, 280, 740]; + static const _durationInMills = [1260, 430, 1010, 730]; + static final TweenSequence _seq = TweenSequence([ + TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.5), weight: 1), + TweenSequenceItem(tween: Tween(begin: 0.5, end: 1.0), weight: 1), + ]); + + final List _controllers = []; + + @override + void initState() { + super.initState(); + for (int i = 0; i < 4; i++) { + _controllers.add( + AnimationController( + value: _delayInMills[i] / _durationInMills[i], + vsync: this, + duration: Duration(milliseconds: _durationInMills[i]), + ), + ); + _controllers[i].repeat(); + } + } + + @override + void dispose() { + for (final AnimationController e in _controllers) { + e.dispose(); + } + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: Padding( + padding: const EdgeInsets.only(left: 3, right: 1), + child: SizedBox( + width: iconSize - 2, + height: iconSize, + child: AnimatedBuilder( + animation: Listenable.merge(_controllers), + builder: (context, _) { + return CustomPaint( + painter: _ActiveAudioSignalPainter( + animations: _controllers + .map(_seq.animate) + .map((e) => e.value) + .toList(), + color: widget.color ?? Theme.of(context).colorScheme.primary, + thickness: widget.thickness, + ), + ); + }, + ), + ), + ), + ); + } +} + +class _ActiveAudioSignalPainter extends CustomPainter { + final List animations; + final Color? color; + final double thickness; + + const _ActiveAudioSignalPainter({ + required this.animations, + this.color, + this.thickness = 2.0, + }); + + @override + void paint(Canvas canvas, Size size) { + for (int i = 0; i < animations.length; i++) { + final double dx = (size.width - thickness * animations.length) / + (animations.length - 1) * + i + + thickness / 2; + + canvas.drawLine( + Offset(dx, 0.5 * animations[i] * size.height), + Offset(dx, size.height - 0.5 * animations[i] * size.height), + Paint() + ..color = color ?? Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = thickness + ..strokeCap = StrokeCap.round, + ); + } + } + + @override + bool shouldRepaint(covariant _ActiveAudioSignalPainter old) { + return !listEquals(animations, old.animations) || + color != old.color || + thickness != old.thickness; + } +} + +// Code by @HrX03 +const _kTargetCanvasSize = 24.0; +const _kBarsStartOffsets = [0.2, 0.8, 0.4, 0.6]; +const _kDefaultIndicatorThickness = 2.0; + +class AudioSignalIndicator extends StatelessWidget { + const AudioSignalIndicator({ + required this.progress, + this.color, + this.size = _kTargetCanvasSize, + super.key, + }); + + final Animation progress; + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + final values = + _kBarsStartOffsets.map((e) => _transform(progress.value, e)).toList(); + + return RepaintBoundary( + child: SizedBox.square( + dimension: size, + child: AnimatedBuilder( + animation: progress, + builder: (context, _) { + return CustomPaint( + painter: _AudioSignalPainter( + values: values, + size: size, + color: color ?? Theme.of(context).colorScheme.onSurface, + thickness: _kDefaultIndicatorThickness, + ), + size: Size.square(size), + ); + }, + ), + ), + ); + } +} + +double _transform(double v, [double offset = 0]) { + return math.acos(math.cos((v + offset / 2) * 2 * math.pi)) / math.pi; +} + +class _AudioSignalPainter extends CustomPainter { + const _AudioSignalPainter({ + required this.values, + this.size = 24.0, + this.color, + this.thickness = 2.0, + }); + + final List values; + final double size; + final Color? color; + final double thickness; + + @override + void paint(Canvas canvas, Size size) { + canvas.scale(this.size / _kTargetCanvasSize); + + final inbetweenSpace = + (_kTargetCanvasSize - thickness) / (values.length - 1); + + for (var i = 0; i < values.length; i++) { + final dx = (inbetweenSpace * i) + thickness / 2; + const hh = _kTargetCanvasSize / 2; + final fraction = values[i]; + + canvas.drawLine( + Offset(dx, hh - (fraction * hh)), + Offset(dx, hh + (fraction * hh)), + getBarPaint(), + ); + } + } + + Paint getBarPaint() { + return Paint() + ..color = color ?? Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = thickness + ..strokeCap = StrokeCap.round; + } + + @override + bool shouldRepaint(covariant _AudioSignalPainter old) { + return !listEquals(values, old.values) || + color != old.color || + thickness != old.thickness; + } +} diff --git a/lib/common/view/audio_type_is_playing_indicator.dart b/lib/common/view/audio_type_is_playing_indicator.dart deleted file mode 100644 index 2b2468bfd..000000000 --- a/lib/common/view/audio_type_is_playing_indicator.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'theme.dart'; - -// Code by @HrX03 -class AudioTypeIsPlayingIndicator extends StatefulWidget { - final Color? color; - final double thickness; - - const AudioTypeIsPlayingIndicator({ - this.color, - this.thickness = 2.0, - super.key, - }); - - @override - State createState() => - _AudioTypeIsPlayingIndicatorState(); -} - -class _AudioTypeIsPlayingIndicatorState - extends State with TickerProviderStateMixin { - static const _delayInMills = [770, 290, 280, 740]; - static const _durationInMills = [1260, 430, 1010, 730]; - static final TweenSequence _seq = TweenSequence([ - TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.5), weight: 1), - TweenSequenceItem(tween: Tween(begin: 0.5, end: 1.0), weight: 1), - ]); - - final List _controllers = []; - - @override - void initState() { - super.initState(); - for (int i = 0; i < 4; i++) { - _controllers.add( - AnimationController( - value: _delayInMills[i] / _durationInMills[i], - vsync: this, - duration: Duration(milliseconds: _durationInMills[i]), - ), - ); - _controllers[i].repeat(); - } - } - - @override - void dispose() { - for (final AnimationController e in _controllers) { - e.dispose(); - } - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: Padding( - padding: const EdgeInsets.only(left: 3, right: 1), - child: SizedBox( - width: iconSize - 2, - height: iconSize, - child: AnimatedBuilder( - animation: Listenable.merge(_controllers), - builder: (context, _) { - return CustomPaint( - painter: _MusicIndicatorPainter( - animations: _controllers - .map(_seq.animate) - .map((e) => e.value) - .toList(), - color: widget.color ?? Theme.of(context).colorScheme.primary, - thickness: widget.thickness, - ), - ); - }, - ), - ), - ), - ); - } -} - -class _MusicIndicatorPainter extends CustomPainter { - final List animations; - final Color? color; - final double thickness; - - const _MusicIndicatorPainter({ - required this.animations, - this.color, - this.thickness = 2.0, - }); - - @override - void paint(Canvas canvas, Size size) { - for (int i = 0; i < animations.length; i++) { - final double dx = (size.width - thickness * animations.length) / - (animations.length - 1) * - i + - thickness / 2; - - canvas.drawLine( - Offset(dx, 0.5 * animations[i] * size.height), - Offset(dx, size.height - 0.5 * animations[i] * size.height), - Paint() - ..color = color ?? Colors.black - ..style = PaintingStyle.stroke - ..strokeWidth = thickness - ..strokeCap = StrokeCap.round, - ); - } - } - - @override - bool shouldRepaint(covariant _MusicIndicatorPainter old) { - return !listEquals(animations, old.animations) || - color != old.color || - thickness != old.thickness; - } -} diff --git a/lib/local_audio/view/local_audio_page.dart b/lib/local_audio/view/local_audio_page.dart index 9659f753b..0e8b413ae 100644 --- a/lib/local_audio/view/local_audio_page.dart +++ b/lib/local_audio/view/local_audio_page.dart @@ -5,21 +5,16 @@ import 'package:yaru/yaru.dart'; import '../../common/data/audio.dart'; import '../../common/view/adaptive_container.dart'; -import '../../common/view/audio_type_is_playing_indicator.dart'; import '../../common/view/common_widgets.dart'; import '../../common/view/header_bar.dart'; -import '../../common/view/icons.dart'; import '../../common/view/search_button.dart'; import '../../common/view/sliver_filter_app_bar.dart'; import '../../common/view/theme.dart'; import '../../constants.dart'; -import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; -import '../../player/player_model.dart'; import '../../search/search_model.dart'; import '../../search/search_type.dart'; -import '../../settings/settings_model.dart'; import '../../settings/view/settings_dialog.dart'; import '../local_audio_model.dart'; import 'failed_imports_content.dart'; @@ -134,36 +129,3 @@ class _LocalAudioPageState extends State { ); } } - -class LocalAudioPageIcon extends StatelessWidget with WatchItMixin { - const LocalAudioPageIcon({ - super.key, - required this.selected, - }); - - final bool selected; - - @override - Widget build(BuildContext context) { - final audioType = watchPropertyValue((PlayerModel m) => m.audio?.audioType); - final isPlaying = watchPropertyValue((PlayerModel m) => m.isPlaying); - final useMoreAnimations = - watchPropertyValue((SettingsModel m) => m.useMoreAnimations); - - if (useMoreAnimations && audioType == AudioType.local) { - if (isPlaying) { - return const AudioTypeIsPlayingIndicator(thickness: 1); - } else { - return Icon( - Iconz.playFilled, - color: context.colorScheme.primary, - ); - } - } - - return Padding( - padding: kMainPageIconPadding, - child: selected ? Icon(Iconz.localAudioFilled) : Icon(Iconz.localAudio), - ); - } -} diff --git a/lib/podcasts/view/podcasts_page.dart b/lib/podcasts/view/podcasts_page.dart index 47fdc0489..6225949f7 100644 --- a/lib/podcasts/view/podcasts_page.dart +++ b/lib/podcasts/view/podcasts_page.dart @@ -3,20 +3,14 @@ import 'package:watch_it/watch_it.dart'; import 'package:yaru/theme.dart'; import '../../common/data/audio.dart'; -import '../../common/view/audio_type_is_playing_indicator.dart'; import '../../common/view/header_bar.dart'; -import '../../common/view/icons.dart'; -import '../../common/view/progress.dart'; import '../../common/view/search_button.dart'; import '../../common/view/theme.dart'; import '../../constants.dart'; -import '../../extensions/build_context_x.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; -import '../../player/player_model.dart'; import '../../search/search_model.dart'; import '../../search/search_type.dart'; -import '../../settings/settings_model.dart'; import '../podcast_model.dart'; import 'podcasts_collection_body.dart'; @@ -68,43 +62,3 @@ class _PodcastsPageState extends State { ); } } - -class PodcastsPageIcon extends StatelessWidget with WatchItMixin { - const PodcastsPageIcon({ - super.key, - required this.selected, - }); - - final bool selected; - - @override - Widget build(BuildContext context) { - final theme = context.theme; - final audioType = watchPropertyValue((PlayerModel m) => m.audio?.audioType); - final checkingForUpdates = - watchPropertyValue((PodcastModel m) => m.checkingForUpdates); - final isPlaying = watchPropertyValue((PlayerModel m) => m.isPlaying); - final useMoreAnimations = - watchPropertyValue((SettingsModel m) => m.useMoreAnimations); - - if (useMoreAnimations && audioType == AudioType.podcast && isPlaying) { - return const AudioTypeIsPlayingIndicator(thickness: 1); - } - - if (checkingForUpdates) { - return const SideBarProgress(); - } - - if (audioType == AudioType.podcast) { - return Icon( - Iconz.playFilled, - color: theme.colorScheme.primary, - ); - } - - return Padding( - padding: kMainPageIconPadding, - child: selected ? Icon(Iconz.podcastFilled) : Icon(Iconz.podcast), - ); - } -}