mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
430 lines
12 KiB
Dart
430 lines
12 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:auto_size_text/auto_size_text.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:text_scroll/text_scroll.dart';
|
|
|
|
import '../../cache/image_cache.dart';
|
|
import '../../models/support.dart';
|
|
import '../../services/audio_service.dart';
|
|
import '../../state/audio.dart';
|
|
import '../../state/theme.dart';
|
|
import '../context_menus.dart';
|
|
import '../gradient.dart';
|
|
import '../images.dart';
|
|
import '../now_playing_bar.dart';
|
|
|
|
class NowPlayingPage extends HookConsumerWidget {
|
|
const NowPlayingPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
|
final itemData = ref.watch(mediaItemDataProvider);
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
final scaffold = AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value: SystemUiOverlayStyle.light.copyWith(
|
|
systemNavigationBarColor: colors?.gradientLow,
|
|
statusBarColor: Colors.transparent,
|
|
),
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
itemData?.contextType.value ?? '',
|
|
style: theme.textTheme.labelMedium,
|
|
maxLines: 1,
|
|
softWrap: false,
|
|
overflow: TextOverflow.fade,
|
|
),
|
|
// Text(
|
|
// itemData?.contextTitle ?? '',
|
|
// style: theme.textTheme.titleMedium,
|
|
// maxLines: 1,
|
|
// softWrap: false,
|
|
// overflow: TextOverflow.fade,
|
|
// ),
|
|
],
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
const MediaItemGradient(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
|
child: Column(
|
|
children: const [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
child: _Art(),
|
|
),
|
|
),
|
|
SizedBox(height: 24),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
child: _TrackInfo(),
|
|
),
|
|
SizedBox(height: 8),
|
|
_Progress(),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
child: _Controls(),
|
|
),
|
|
SizedBox(height: 64),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
if (colors != null) {
|
|
return Theme(data: colors.theme, child: scaffold);
|
|
} else {
|
|
return scaffold;
|
|
}
|
|
}
|
|
}
|
|
|
|
class _Art extends HookConsumerWidget {
|
|
const _Art();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final itemData = ref.watch(mediaItemDataProvider);
|
|
final imageCache = ref.watch(imageCacheProvider);
|
|
|
|
UriCacheInfo? cacheInfo;
|
|
if (itemData?.artCache != null) {
|
|
cacheInfo = UriCacheInfo(
|
|
uri: itemData!.artCache!.fullArtUri,
|
|
cacheKey: itemData.artCache!.fullArtCacheKey,
|
|
cacheManager: imageCache,
|
|
);
|
|
}
|
|
|
|
return AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 150),
|
|
child: CardClip(
|
|
key: ValueKey(cacheInfo?.cacheKey ?? 'default'),
|
|
child: cacheInfo != null
|
|
? CardClip(
|
|
square: false,
|
|
child: UriCacheInfoImage(
|
|
// height: 300,
|
|
fit: BoxFit.contain,
|
|
placeholderStyle: PlaceholderStyle.spinner,
|
|
cache: cacheInfo,
|
|
),
|
|
)
|
|
: const PlaceholderImage(thumbnail: false),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TrackInfo extends HookConsumerWidget {
|
|
const _TrackInfo();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final item = ref.watch(mediaItemProvider).valueOrNull;
|
|
final theme = Theme.of(context);
|
|
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
ScrollableText(
|
|
item?.title ?? '',
|
|
style: theme.textTheme.headlineSmall,
|
|
speed: 50,
|
|
),
|
|
Text(
|
|
item?.artist ?? '',
|
|
style: theme.textTheme.titleMedium!,
|
|
maxLines: 1,
|
|
softWrap: false,
|
|
overflow: TextOverflow.fade,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.star_outline_rounded,
|
|
size: 36,
|
|
),
|
|
onPressed: () {},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ScrollableText extends StatelessWidget {
|
|
final String text;
|
|
final TextStyle? style;
|
|
final double speed;
|
|
|
|
const ScrollableText(
|
|
this.text, {
|
|
super.key,
|
|
this.style,
|
|
this.speed = 35,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final defaultStyle = DefaultTextStyle.of(context);
|
|
|
|
return AutoSizeText(
|
|
text,
|
|
presetFontSizes: style != null && style?.fontSize != null
|
|
? [style!.fontSize!]
|
|
: [defaultStyle.style.fontSize ?? 12],
|
|
style: style,
|
|
maxLines: 1,
|
|
// softWrap: false,
|
|
overflowReplacement: TextScroll(
|
|
'$text ',
|
|
style: style,
|
|
delayBefore: const Duration(seconds: 3),
|
|
pauseBetween: const Duration(seconds: 4),
|
|
mode: TextScrollMode.endless,
|
|
velocity: Velocity(pixelsPerSecond: Offset(speed, 0)),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Progress extends HookConsumerWidget {
|
|
const _Progress();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final colors = ref.watch(mediaItemThemeProvider).valueOrNull;
|
|
final position = ref.watch(positionProvider);
|
|
final duration = ref.watch(durationProvider);
|
|
final audio = ref.watch(audioControlProvider);
|
|
|
|
final changeValue = useState(position.toDouble());
|
|
final changing = useState(false);
|
|
|
|
return Column(
|
|
children: [
|
|
Slider(
|
|
value: changing.value ? changeValue.value : position.toDouble(),
|
|
min: 0,
|
|
max: max(duration.toDouble(), position.toDouble()),
|
|
thumbColor: colors?.theme.colorScheme.onBackground,
|
|
activeColor: colors?.theme.colorScheme.onBackground,
|
|
inactiveColor: colors?.theme.colorScheme.surface,
|
|
onChanged: (value) {
|
|
changeValue.value = value;
|
|
},
|
|
onChangeStart: (value) {
|
|
changing.value = true;
|
|
},
|
|
onChangeEnd: (value) {
|
|
changing.value = false;
|
|
audio.seek(Duration(seconds: value.toInt()));
|
|
},
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: DefaultTextStyle(
|
|
style: Theme.of(context).textTheme.titleMedium!,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(Duration(
|
|
seconds: changing.value
|
|
? changeValue.value.toInt()
|
|
: position)
|
|
.toString()
|
|
.substring(2, 7)),
|
|
Text(Duration(seconds: duration).toString().substring(2, 7)),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class RepeatButton extends HookConsumerWidget {
|
|
final double size;
|
|
|
|
const RepeatButton({
|
|
super.key,
|
|
required this.size,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final audio = ref.watch(audioControlProvider);
|
|
final repeat = ref.watch(repeatModeProvider);
|
|
|
|
IconData icon;
|
|
void Function() action;
|
|
|
|
switch (repeat) {
|
|
case AudioServiceRepeatMode.all:
|
|
case AudioServiceRepeatMode.group:
|
|
icon = Icons.repeat_on_rounded;
|
|
action = () => audio.setRepeatMode(AudioServiceRepeatMode.one);
|
|
break;
|
|
case AudioServiceRepeatMode.one:
|
|
icon = Icons.repeat_one_on_rounded;
|
|
action = () => audio.setRepeatMode(AudioServiceRepeatMode.none);
|
|
break;
|
|
default:
|
|
icon = Icons.repeat_rounded;
|
|
action = () => audio.setRepeatMode(AudioServiceRepeatMode.all);
|
|
break;
|
|
}
|
|
|
|
return IconButton(
|
|
icon: Icon(icon),
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 30,
|
|
onPressed: action,
|
|
);
|
|
}
|
|
}
|
|
|
|
class ShuffleButton extends HookConsumerWidget {
|
|
final double size;
|
|
|
|
const ShuffleButton({
|
|
super.key,
|
|
required this.size,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final audio = ref.watch(audioControlProvider);
|
|
final shuffle = ref.watch(shuffleModeProvider);
|
|
final queueMode = ref.watch(queueModeProvider).valueOrNull;
|
|
|
|
IconData icon;
|
|
void Function() action;
|
|
|
|
switch (shuffle) {
|
|
case AudioServiceShuffleMode.all:
|
|
case AudioServiceShuffleMode.group:
|
|
icon = Icons.shuffle_on_rounded;
|
|
action = () => audio.setShuffleMode(AudioServiceShuffleMode.none);
|
|
break;
|
|
default:
|
|
icon = Icons.shuffle_rounded;
|
|
action = () => audio.setShuffleMode(AudioServiceShuffleMode.all);
|
|
break;
|
|
}
|
|
|
|
return IconButton(
|
|
icon: Icon(queueMode == QueueMode.radio ? Icons.radio_rounded : icon),
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 30,
|
|
onPressed: queueMode == QueueMode.radio ? null : action,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Controls extends HookConsumerWidget {
|
|
const _Controls();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final base = ref.watch(baseThemeProvider);
|
|
final audio = ref.watch(audioControlProvider);
|
|
|
|
return IconTheme(
|
|
data: IconThemeData(color: base.theme.colorScheme.onBackground),
|
|
child: Column(
|
|
children: [
|
|
SizedBox(
|
|
height: 100,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const RepeatButton(size: 30),
|
|
IconButton(
|
|
icon: const Icon(Icons.skip_previous_rounded),
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 60,
|
|
onPressed: () => audio.skipToPrevious(),
|
|
),
|
|
const PlayPauseButton(size: 90),
|
|
IconButton(
|
|
icon: const Icon(Icons.skip_next_rounded),
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 60,
|
|
onPressed: () => audio.skipToNext(),
|
|
),
|
|
const ShuffleButton(size: 30),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 40,
|
|
child: Row(
|
|
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.queue_music_rounded),
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 30,
|
|
onPressed: () {},
|
|
),
|
|
const _MoreButton(),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _MoreButton extends HookConsumerWidget {
|
|
const _MoreButton();
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final song = ref.watch(mediaItemSongProvider).valueOrNull;
|
|
|
|
return IconButton(
|
|
icon: const Icon(Icons.more_horiz),
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 30,
|
|
onPressed: song != null
|
|
? () {
|
|
showContextMenu(
|
|
context: context,
|
|
ref: ref,
|
|
builder: (context) => BottomSheetMenu(
|
|
child: SongContextMenu(song: song),
|
|
),
|
|
);
|
|
}
|
|
: null,
|
|
);
|
|
}
|
|
}
|