chore: run dart format
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions

This commit is contained in:
Dr.Blank 2026-01-10 16:51:05 +05:30
parent a520136e01
commit e23c0b6c5f
No known key found for this signature in database
GPG key ID: BA5F87FF0560C57B
84 changed files with 1565 additions and 1945 deletions

View file

@ -23,7 +23,9 @@ Duration sumOfTracks(BookExpanded book, int? index) {
_logger.warning('Index is null or less than 0, returning 0');
return Duration.zero;
}
final total = book.tracks.sublist(0, index).fold<Duration>(
final total = book.tracks
.sublist(0, index)
.fold<Duration>(
Duration.zero,
(previousValue, element) => previousValue + element.duration,
);
@ -34,13 +36,10 @@ Duration sumOfTracks(BookExpanded book, int? index) {
/// returns the [AudioTrack] to play based on the [position] in the [book]
AudioTrack getTrackToPlay(BookExpanded book, Duration position) {
_logger.fine('Getting track to play for position: $position');
final track = book.tracks.firstWhere(
(element) {
return element.startOffset <= position &&
(element.startOffset + element.duration) >= position;
},
orElse: () => book.tracks.last,
);
final track = book.tracks.firstWhere((element) {
return element.startOffset <= position &&
(element.startOffset + element.duration) >= position;
}, orElse: () => book.tracks.last);
_logger.fine('Track to play for position: $position is $track');
return track;
}
@ -126,8 +125,12 @@ class AudiobookPlayer extends AudioPlayer {
ConcatenatingAudioSource(
useLazyPreparation: true,
children: book.tracks.map((track) {
final retrievedUri =
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
final retrievedUri = _getUri(
track,
downloadedUris,
baseUrl: baseUrl,
token: token,
);
_logger.fine(
'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
);
@ -141,7 +144,8 @@ class AudiobookPlayer extends AudioPlayer {
.formatNotificationTitle(book),
album: appSettings.notificationSettings.secondaryTitle
.formatNotificationTitle(book),
artUri: artworkUri ??
artUri:
artworkUri ??
Uri.parse(
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
),
@ -255,12 +259,9 @@ class AudiobookPlayer extends AudioPlayer {
if (_book!.chapters.isEmpty) {
return null;
}
return _book!.chapters.firstWhere(
(element) {
return element.start <= positionInBook && element.end >= positionInBook;
},
orElse: () => _book!.chapters.first,
);
return _book!.chapters.firstWhere((element) {
return element.start <= positionInBook && element.end >= positionInBook;
}, orElse: () => _book!.chapters.first);
}
}
@ -271,11 +272,9 @@ Uri _getUri(
required String token,
}) {
// check if the track is in the downloadedUris
final uri = downloadedUris?.firstWhereOrNull(
(element) {
return element.pathSegments.last == track.metadata?.filename;
},
);
final uri = downloadedUris?.firstWhereOrNull((element) {
return element.pathSegments.last == track.metadata?.filename;
});
return uri ??
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
@ -283,17 +282,14 @@ Uri _getUri(
extension FormatNotificationTitle on String {
String formatNotificationTitle(BookExpanded book) {
return replaceAllMapped(
RegExp(r'\$(\w+)'),
(match) {
final type = match.group(1);
return NotificationTitleType.values
.firstWhere((element) => element.name == type)
.extractFrom(book) ??
match.group(0) ??
'';
},
);
return replaceAllMapped(RegExp(r'\$(\w+)'), (match) {
final type = match.group(1);
return NotificationTitleType.values
.firstWhere((element) => element.name == type)
.extractFrom(book) ??
match.group(0) ??
'';
});
}
}

View file

@ -30,23 +30,28 @@ Future<void> configurePlayer() async {
androidShowNotificationBadge: false,
notificationConfigBuilder: (state) {
final controls = [
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.skipToPreviousChapter) &&
if (appSettings.notificationSettings.mediaControls.contains(
NotificationMediaControl.skipToPreviousChapter,
) &&
state.hasPrevious)
MediaControl.skipToPrevious,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.rewind))
if (appSettings.notificationSettings.mediaControls.contains(
NotificationMediaControl.rewind,
))
MediaControl.rewind,
if (state.playing) MediaControl.pause else MediaControl.play,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.fastForward))
if (appSettings.notificationSettings.mediaControls.contains(
NotificationMediaControl.fastForward,
))
MediaControl.fastForward,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.skipToNextChapter) &&
if (appSettings.notificationSettings.mediaControls.contains(
NotificationMediaControl.skipToNextChapter,
) &&
state.hasNext)
MediaControl.skipToNext,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.stop))
if (appSettings.notificationSettings.mediaControls.contains(
NotificationMediaControl.stop,
))
MediaControl.stop,
];
return NotificationConfig(

View file

@ -62,8 +62,8 @@ class AudiobookPlaylist {
this.books = const [],
currentIndex = 0,
subCurrentIndex = 0,
}) : _currentIndex = currentIndex,
_subCurrentIndex = subCurrentIndex;
}) : _currentIndex = currentIndex,
_subCurrentIndex = subCurrentIndex;
// most important method, gets the audio file to play
// this is needed as a library item is a list of audio files

View file

@ -16,10 +16,7 @@ class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer {
@override
core.AudiobookPlayer build() {
final api = ref.watch(authenticatedApiProvider);
final player = core.AudiobookPlayer(
api.token!,
api.baseUrl,
);
final player = core.AudiobookPlayer(api.token!, api.baseUrl);
ref.onDispose(player.dispose);
_logger.finer('created simple player');

View file

@ -26,11 +26,10 @@ extension on Ref {
}
@Riverpod(keepAlive: true)
Raw<ValueNotifier<double>> playerExpandProgressNotifier(
Ref ref,
) {
final ValueNotifier<double> playerExpandProgress =
ValueNotifier(playerMinHeight);
Raw<ValueNotifier<double>> playerExpandProgressNotifier(Ref ref) {
final ValueNotifier<double> playerExpandProgress = ValueNotifier(
playerMinHeight,
);
return ref.disposeAndListenChangeNotifier(playerExpandProgress);
}
@ -46,9 +45,7 @@ Raw<ValueNotifier<double>> playerExpandProgressNotifier(
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
@Riverpod(keepAlive: true)
double playerHeight(
Ref ref,
) {
double playerHeight(Ref ref) {
final playerExpandProgress = ref.watch(playerExpandProgressProvider);
// on change of the playerExpandProgress invalidate
@ -63,9 +60,7 @@ double playerHeight(
final audioBookMiniplayerController = MiniplayerController();
@Riverpod(keepAlive: true)
bool isPlayerActive(
Ref ref,
) {
bool isPlayerActive(Ref ref) {
try {
final player = ref.watch(audiobookPlayerProvider);
if (player.book != null) {

View file

@ -31,19 +31,15 @@ class AudiobookPlayer extends HookConsumerWidget {
if (currentBook == null) {
return const SizedBox.shrink();
}
final itemBeingPlayed =
ref.watch(libraryItemProvider(currentBook.libraryItemId));
final itemBeingPlayed = ref.watch(
libraryItemProvider(currentBook.libraryItemId),
);
final player = ref.watch(audiobookPlayerProvider);
final imageOfItemBeingPlayed = itemBeingPlayed.value != null
? ref.watch(
coverImageProvider(itemBeingPlayed.value!.id),
)
? ref.watch(coverImageProvider(itemBeingPlayed.value!.id))
: null;
final imgWidget = imageOfItemBeingPlayed?.value != null
? Image.memory(
imageOfItemBeingPlayed!.value!,
fit: BoxFit.cover,
)
? Image.memory(imageOfItemBeingPlayed!.value!, fit: BoxFit.cover)
: const BookCoverSkeleton();
final playPauseController = useAnimationController(
@ -65,7 +61,8 @@ class AudiobookPlayer extends HookConsumerWidget {
themeOfLibraryItemProvider(
itemBeingPlayed.value?.id,
brightness: Theme.of(context).brightness,
highContrast: appSettings.themeSettings.highContrast ||
highContrast:
appSettings.themeSettings.highContrast ||
MediaQuery.of(context).highContrast,
),
);
@ -88,8 +85,9 @@ class AudiobookPlayer extends HookConsumerWidget {
onDragDown: (percentage) async {
// preferred volume
// set volume to 0 when dragging down
await player
.setVolume(preferredVolume * (1 - percentage.clamp(0, .75)));
await player.setVolume(
preferredVolume * (1 - percentage.clamp(0, .75)),
);
},
minHeight: playerMinHeight,
// subtract the height of notches and other system UI
@ -109,17 +107,14 @@ class AudiobookPlayer extends HookConsumerWidget {
// also at this point the image should be at its max size and in the center of the player
final miniplayerPercentageDeclaration =
(maxImgSize - playerMinHeight) /
(playerMaxHeight - playerMinHeight);
(playerMaxHeight - playerMinHeight);
final bool isFormMiniplayer =
percentage < miniplayerPercentageDeclaration;
if (!isFormMiniplayer) {
// this calculation needs a refactor
var percentageExpandedPlayer = percentage
.inverseLerp(
miniplayerPercentageDeclaration,
1,
)
.inverseLerp(miniplayerPercentageDeclaration, 1)
.clamp(0.0, 1.0);
return PlayerWhenExpanded(
@ -164,37 +159,33 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
return switch (player.processingState) {
ProcessingState.loading || ProcessingState.buffering => const Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
ProcessingState.completed => IconButton(
onPressed: () async {
await player.seek(const Duration(seconds: 0));
await player.play();
},
icon: const Icon(
Icons.replay,
),
),
onPressed: () async {
await player.seek(const Duration(seconds: 0));
await player.play();
},
icon: const Icon(Icons.replay),
),
ProcessingState.ready => IconButton(
onPressed: () async {
await player.togglePlayPause();
},
iconSize: iconSize,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: playPauseController,
),
onPressed: () async {
await player.togglePlayPause();
},
iconSize: iconSize,
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: playPauseController,
),
),
ProcessingState.idle => const SizedBox.shrink(),
};
}
}
class AudiobookChapterProgressBar extends HookConsumerWidget {
const AudiobookChapterProgressBar({
super.key,
});
const AudiobookChapterProgressBar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {

View file

@ -38,10 +38,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
const lateStart = 0.4;
const earlyEnd = 1;
final earlyPercentage = percentageExpandedPlayer
.inverseLerp(
lateStart,
earlyEnd,
)
.inverseLerp(lateStart, earlyEnd)
.clamp(0.0, 1.0);
final currentChapter = ref.watch(currentPlayingChapterProvider);
final currentBookMetadata = ref.watch(currentBookMetadataProvider);
@ -49,15 +46,11 @@ class PlayerWhenExpanded extends HookConsumerWidget {
return Column(
children: [
// sized box for system status bar; not needed as not full screen
SizedBox(
height: MediaQuery.of(context).padding.top * earlyPercentage,
),
SizedBox(height: MediaQuery.of(context).padding.top * earlyPercentage),
// a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 100 * earlyPercentage,
),
constraints: BoxConstraints(maxHeight: 100 * earlyPercentage),
child: Opacity(
opacity: earlyPercentage,
child: Padding(
@ -104,10 +97,9 @@ class PlayerWhenExpanded extends HookConsumerWidget {
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.1),
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
blurRadius: 32 * earlyPercentage,
spreadRadius: 8 * earlyPercentage,
// offset: Offset(0, 16 * earlyPercentage),
@ -170,11 +162,10 @@ class PlayerWhenExpanded extends HookConsumerWidget {
currentBookMetadata?.authorName ?? '',
].join(' - '),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.7),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),

View file

@ -32,8 +32,10 @@ class PlayerWhenMinimized extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final vanishingPercentage = 1 - percentageMiniplayer;
final progress =
useStream(player.slowPositionStream, initialData: Duration.zero);
final progress = useStream(
player.slowPositionStream,
initialData: Duration.zero,
);
final bookMetaExpanded = ref.watch(currentBookMetadataProvider);
@ -61,9 +63,7 @@ class PlayerWhenMinimized extends HookConsumerWidget {
);
},
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: maxImgSize,
),
constraints: BoxConstraints(maxWidth: maxImgSize),
child: imgWidget,
),
),
@ -80,7 +80,8 @@ class PlayerWhenMinimized extends HookConsumerWidget {
// AutoScrollText(
Text(
bookMetaExpanded?.title ?? '',
maxLines: 1, overflow: TextOverflow.ellipsis,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// velocity:
// const Velocity(pixelsPerSecond: Offset(16, 0)),
style: Theme.of(context).textTheme.bodyLarge,
@ -90,11 +91,10 @@ class PlayerWhenMinimized extends HookConsumerWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
),
@ -135,7 +135,8 @@ class PlayerWhenMinimized extends HookConsumerWidget {
SizedBox(
height: barHeight,
child: LinearProgressIndicator(
value: (progress.data ?? Duration.zero).inSeconds /
value:
(progress.data ?? Duration.zero).inSeconds /
player.book!.duration.inSeconds,
color: Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,

View file

@ -4,10 +4,7 @@ import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
class AudiobookPlayerSeekButton extends HookConsumerWidget {
const AudiobookPlayerSeekButton({
super.key,
required this.isForward,
});
const AudiobookPlayerSeekButton({super.key, required this.isForward});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;

View file

@ -5,10 +5,7 @@ import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton({
super.key,
required this.isForward,
});
const AudiobookPlayerSeekChapterButton({super.key, required this.isForward});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@ -27,9 +24,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
void seekForward() {
final index = player.book!.chapters.indexOf(player.currentChapter!);
if (index < player.book!.chapters.length - 1) {
player.seek(
player.book!.chapters[index + 1].start + offset,
);
player.seek(player.book!.chapters[index + 1].start + offset);
} else {
player.seek(player.currentChapter!.end);
}
@ -37,8 +32,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
/// seek backward to the previous chapter or the start of the current chapter
void seekBackward() {
final currentPlayingChapterIndex =
player.book!.chapters.indexOf(player.currentChapter!);
final currentPlayingChapterIndex = player.book!.chapters.indexOf(
player.currentChapter!,
);
final chapterPosition =
player.positionInBook - player.currentChapter!.start;
BookChapter chapterToSeekTo;
@ -49,9 +45,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
} else {
chapterToSeekTo = player.currentChapter!;
}
player.seek(
chapterToSeekTo.start + offset,
);
player.seek(chapterToSeekTo.start + offset);
}
return IconButton(

View file

@ -15,9 +15,7 @@ import 'package:vaani/shared/extensions/duration_format.dart'
import 'package:vaani/shared/hooks.dart' show useTimer;
class ChapterSelectionButton extends HookConsumerWidget {
const ChapterSelectionButton({
super.key,
});
const ChapterSelectionButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -49,9 +47,7 @@ class ChapterSelectionButton extends HookConsumerWidget {
}
class ChapterSelectionModal extends HookConsumerWidget {
const ChapterSelectionModal({
super.key,
});
const ChapterSelectionModal({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -87,41 +83,40 @@ class ChapterSelectionModal extends HookConsumerWidget {
child: currentBook?.chapters == null
? const Text('No chapters found')
: Column(
children: currentBook!.chapters.map(
(chapter) {
final isCurrent = currentChapterIndex == chapter.id;
final isPlayed = currentChapterIndex != null &&
chapter.id < currentChapterIndex;
return ListTile(
autofocus: isCurrent,
iconColor: isPlayed && !isCurrent
? theme.disabledColor
children: currentBook!.chapters.map((chapter) {
final isCurrent = currentChapterIndex == chapter.id;
final isPlayed =
currentChapterIndex != null &&
chapter.id < currentChapterIndex;
return ListTile(
autofocus: isCurrent,
iconColor: isPlayed && !isCurrent
? theme.disabledColor
: null,
title: Text(
chapter.title,
style: isPlayed && !isCurrent
? TextStyle(color: theme.disabledColor)
: null,
title: Text(
chapter.title,
style: isPlayed && !isCurrent
? TextStyle(color: theme.disabledColor)
: null,
),
subtitle: Text(
'(${chapter.duration.smartBinaryFormat})',
style: isPlayed && !isCurrent
? TextStyle(color: theme.disabledColor)
: null,
),
trailing: isCurrent
? const PlayingIndicatorIcon()
: const Icon(Icons.play_arrow),
selected: isCurrent,
key: isCurrent ? chapterKey : null,
onTap: () {
Navigator.of(context).pop();
notifier.seek(chapter.start + 90.ms);
notifier.play();
},
);
},
).toList(),
),
subtitle: Text(
'(${chapter.duration.smartBinaryFormat})',
style: isPlayed && !isCurrent
? TextStyle(color: theme.disabledColor)
: null,
),
trailing: isCurrent
? const PlayingIndicatorIcon()
: const Icon(Icons.play_arrow),
selected: isCurrent,
key: isCurrent ? chapterKey : null,
onTap: () {
Navigator.of(context).pop();
notifier.seek(chapter.start + 90.ms);
notifier.play();
},
);
}).toList(),
),
),
),

View file

@ -10,9 +10,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
final _logger = Logger('PlayerSpeedAdjustButton');
class PlayerSpeedAdjustButton extends HookConsumerWidget {
const PlayerSpeedAdjustButton({
super.key,
});
const PlayerSpeedAdjustButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -35,21 +33,19 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget {
notifier.setSpeed(speed);
if (appSettings.playerSettings.configurePlayerForEveryBook) {
ref
.read(
bookSettingsProvider(bookId).notifier,
)
.read(bookSettingsProvider(bookId).notifier)
.update(
bookSettings.copyWith
.playerSettings(preferredDefaultSpeed: speed),
bookSettings.copyWith.playerSettings(
preferredDefaultSpeed: speed,
),
);
} else {
ref
.read(
appSettingsProvider.notifier,
)
.read(appSettingsProvider.notifier)
.update(
appSettings.copyWith
.playerSettings(preferredDefaultSpeed: speed),
appSettings.copyWith.playerSettings(
preferredDefaultSpeed: speed,
),
);
}
},

View file

@ -59,8 +59,11 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
@override
void initState() {
super.initState();
_animationParams =
List.generate(widget.barCount, _createRandomParams, growable: false);
_animationParams = List.generate(
widget.barCount,
_createRandomParams,
growable: false,
);
}
// Helper to generate random parameters for one bar's animation cycle
@ -72,10 +75,12 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
// Note: These factors represent the scale relative to the *half-height*
// if centerSymmetric is true, controlled by the alignment in scaleY.
final targetHeightFactor1 = widget.minHeightFactor +
final targetHeightFactor1 =
widget.minHeightFactor +
_random.nextDouble() *
(widget.maxHeightFactor - widget.minHeightFactor);
final targetHeightFactor2 = widget.minHeightFactor +
final targetHeightFactor2 =
widget.minHeightFactor +
_random.nextDouble() *
(widget.maxHeightFactor - widget.minHeightFactor);
@ -95,7 +100,8 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
@override
Widget build(BuildContext context) {
final color = widget.color ??
final color =
widget.color ??
IconTheme.of(context).color ??
Theme.of(context).colorScheme.primary;
@ -110,8 +116,9 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
final double maxHeight = widget.size;
// Determine the alignment for scaling based on the symmetric flag
final Alignment scaleAlignment =
widget.centerSymmetric ? Alignment.center : Alignment.bottomCenter;
final Alignment scaleAlignment = widget.centerSymmetric
? Alignment.center
: Alignment.bottomCenter;
// Determine the cross axis alignment for the Row
final CrossAxisAlignment rowAlignment = widget.centerSymmetric
@ -129,47 +136,40 @@ class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
crossAxisAlignment: rowAlignment,
// Use spaceEvenly for better distribution, especially with center alignment
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(
widget.barCount,
(index) {
final params = _animationParams[index];
// The actual bar widget that will be animated
return Container(
width: barWidth,
// Set initial height to the max potential height
// The scaleY animation will control the visible height
height: maxHeight,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(barWidth / 2),
),
)
.animate(
delay: params.initialDelay,
onPlay: (controller) => controller.repeat(
reverse: true,
),
)
// 1. Scale to targetHeightFactor1
.scaleY(
begin:
widget.minHeightFactor, // Scale factor starts near min
end: params.targetHeightFactor1,
duration: params.duration1,
curve: Curves.easeInOutCirc,
alignment: scaleAlignment, // Apply chosen alignment
)
// 2. Then scale to targetHeightFactor2
.then()
.scaleY(
end: params.targetHeightFactor2,
duration: params.duration2,
curve: Curves.easeInOutCirc,
alignment: scaleAlignment, // Apply chosen alignment
);
},
growable: false,
),
children: List.generate(widget.barCount, (index) {
final params = _animationParams[index];
// The actual bar widget that will be animated
return Container(
width: barWidth,
// Set initial height to the max potential height
// The scaleY animation will control the visible height
height: maxHeight,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(barWidth / 2),
),
)
.animate(
delay: params.initialDelay,
onPlay: (controller) => controller.repeat(reverse: true),
)
// 1. Scale to targetHeightFactor1
.scaleY(
begin: widget.minHeightFactor, // Scale factor starts near min
end: params.targetHeightFactor1,
duration: params.duration1,
curve: Curves.easeInOutCirc,
alignment: scaleAlignment, // Apply chosen alignment
)
// 2. Then scale to targetHeightFactor2
.then()
.scaleY(
end: params.targetHeightFactor2,
duration: params.duration2,
curve: Curves.easeInOutCirc,
alignment: scaleAlignment, // Apply chosen alignment
);
}, growable: false),
),
),
);

View file

@ -10,10 +10,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
const double itemExtent = 25;
class SpeedSelector extends HookConsumerWidget {
const SpeedSelector({
super.key,
required this.onSpeedSelected,
});
const SpeedSelector({super.key, required this.onSpeedSelected});
final void Function(double speed) onSpeedSelected;
@ -26,34 +23,22 @@ class SpeedSelector extends HookConsumerWidget {
final speedState = useState(currentSpeed);
// hook the onSpeedSelected function to the state
useEffect(
() {
onSpeedSelected(speedState.value);
return null;
},
[speedState.value],
);
useEffect(() {
onSpeedSelected(speedState.value);
return null;
}, [speedState.value]);
// the speed options
final minSpeed = min(
speeds.reduce(min),
playerSettings.minSpeed,
);
final maxSpeed = max(
speeds.reduce(max),
playerSettings.maxSpeed,
);
final minSpeed = min(speeds.reduce(min), playerSettings.minSpeed);
final maxSpeed = max(speeds.reduce(max), playerSettings.maxSpeed);
final speedIncrement = playerSettings.speedIncrement;
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil() + 1;
final availableSpeedsList = List.generate(
availableSpeeds,
(index) {
// need to round to 2 decimal place to avoid floating point errors
return double.parse(
(minSpeed + index * speedIncrement).toStringAsFixed(2),
);
},
);
final availableSpeedsList = List.generate(availableSpeeds, (index) {
// need to round to 2 decimal place to avoid floating point errors
return double.parse(
(minSpeed + index * speedIncrement).toStringAsFixed(2),
);
});
final scrollController = useFixedExtentScrollController(
initialItem: availableSpeedsList.indexOf(currentSpeed),
@ -107,18 +92,19 @@ class SpeedSelector extends HookConsumerWidget {
(speed) => TextButton(
style: speed == speedState.value
? TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
backgroundColor: Theme.of(
context,
).colorScheme.primaryContainer,
foregroundColor: Theme.of(
context,
).colorScheme.onPrimaryContainer,
)
// border if not selected
: TextButton.styleFrom(
side: BorderSide(
color: Theme.of(context)
.colorScheme
.primaryContainer,
color: Theme.of(
context,
).colorScheme.primaryContainer,
),
),
onPressed: () async {
@ -195,14 +181,13 @@ class SpeedWheel extends StatelessWidget {
controller: scrollController,
scrollDirection: Axis.horizontal,
itemExtent: itemExtent,
diameterRatio: 1.5, squeeze: 1.2,
diameterRatio: 1.5,
squeeze: 1.2,
// useMagnifier: true,
// magnification: 1.5,
physics: const FixedExtentScrollPhysics(),
children: availableSpeedsList
.map(
(speed) => SpeedLine(speed: speed),
)
.map((speed) => SpeedLine(speed: speed))
.toList(),
onSelectedItemChanged: (index) {
speedState.value = availableSpeedsList[index];
@ -232,10 +217,7 @@ class SpeedWheel extends StatelessWidget {
}
class SpeedLine extends StatelessWidget {
const SpeedLine({
super.key,
required this.speed,
});
const SpeedLine({super.key, required this.speed});
final double speed;
@ -250,8 +232,8 @@ class SpeedLine extends StatelessWidget {
width: speed % 0.5 == 0
? 3
: speed % 0.25 == 0
? 2
: 0.5,
? 2
: 0.5,
color: Theme.of(context).colorScheme.onSurface,
),
),