mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
v2
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user