Skip to content

Commit

Permalink
chore: refactor to MainPageIsPlayingIcon (#984)
Browse files Browse the repository at this point in the history
  • Loading branch information
Feichtmeier authored Oct 26, 2024
1 parent ace16e8 commit 18b3030
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 218 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
},
);
}
}
12 changes: 8 additions & 4 deletions lib/app/view/master_detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -204,23 +205,26 @@ List<MasterItem> 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,
),
MasterItem(
titleBuilder: (context) => Text(context.l10n.radio),
pageBuilder: (_) => const RadioPage(),
iconBuilder: (selected) => RadioPageIcon(
iconBuilder: (selected) => MainPageIcon(
audioType: AudioType.radio,
selected: selected,
),
pageId: kRadioPageId,
),
MasterItem(
titleBuilder: (context) => Text(context.l10n.podcasts),
pageBuilder: (_) => const PodcastsPage(),
iconBuilder: (selected) => PodcastsPageIcon(
iconBuilder: (selected) => MainPageIcon(
audioType: AudioType.podcast,
selected: selected,
),
pageId: kPodcastsPageId,
Expand Down
221 changes: 221 additions & 0 deletions lib/common/view/audio_signal_indicator.dart
Original file line number Diff line number Diff line change
@@ -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<ActiveAudioSignalIndicator> createState() =>
_ActiveAudioSignalIndicatorState();
}

class _ActiveAudioSignalIndicatorState extends State<ActiveAudioSignalIndicator>
with TickerProviderStateMixin {
static const _delayInMills = [770, 290, 280, 740];
static const _durationInMills = [1260, 430, 1010, 730];
static final TweenSequence<double> _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<AnimationController> _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<double> 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<double> 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<double> 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;
}
}
Loading

0 comments on commit 18b3030

Please sign in to comment.