✨ Netease backend support
This commit is contained in:
@ -71,14 +71,16 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
final customEndpoint =
|
||||
CustomSpotifyEndpoints(_auth.auth.value?.accessToken.value ?? '');
|
||||
final forYouView = await customEndpoint.getView(
|
||||
'made-for-x-hub',
|
||||
market: market,
|
||||
locale: Intl.canonicalizedLocale(locale.toString()),
|
||||
);
|
||||
_forYouView = forYouView['content']?['items'];
|
||||
if (_auth.auth.value != null) {
|
||||
final customEndpoint =
|
||||
CustomSpotifyEndpoints(_auth.auth.value?.accessToken.value ?? '');
|
||||
final forYouView = await customEndpoint.getView(
|
||||
'made-for-x-hub',
|
||||
market: market,
|
||||
locale: Intl.canonicalizedLocale(locale.toString()),
|
||||
);
|
||||
_forYouView = forYouView['content']?['items'];
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _isLoading['forYou'] = false);
|
||||
} else {
|
||||
|
@ -69,301 +69,310 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
direction: DismissiblePageDismissDirection.down,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
children: [
|
||||
Obx(
|
||||
() => LimitedBox(
|
||||
maxHeight: maxAlbumSize,
|
||||
maxWidth: maxAlbumSize,
|
||||
child: Hero(
|
||||
tag: const Key('current-active-track-album-art'),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: _albumArt != null
|
||||
? AutoCacheImage(
|
||||
_albumArt!,
|
||||
width: albumSize,
|
||||
height: albumSize,
|
||||
)
|
||||
: Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: const Center(
|
||||
child: Icon(Icons.image)),
|
||||
),
|
||||
),
|
||||
).marginSymmetric(horizontal: 24),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
children: [
|
||||
Obx(
|
||||
() => LimitedBox(
|
||||
maxHeight: maxAlbumSize,
|
||||
maxWidth: maxAlbumSize,
|
||||
child: Hero(
|
||||
tag: const Key('current-active-track-album-art'),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(16)),
|
||||
child: _albumArt != null
|
||||
? AutoCacheImage(
|
||||
_albumArt!,
|
||||
width: albumSize,
|
||||
height: albumSize,
|
||||
)
|
||||
: Container(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: const Center(
|
||||
child: Icon(Icons.image),
|
||||
),
|
||||
),
|
||||
),
|
||||
).marginSymmetric(horizontal: 24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
Obx(
|
||||
() => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
const Gap(24),
|
||||
Obx(
|
||||
() => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_playback.state.value.activeTrack?.name ??
|
||||
'Not playing',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
Text(
|
||||
_playback.state.value.activeTrack?.artists
|
||||
?.asString() ??
|
||||
'No author',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_playback.state.value.activeTrack != null &&
|
||||
_auth.auth.value != null)
|
||||
TrackHeartButton(
|
||||
key: ValueKey(
|
||||
_playback.state.value.activeTrack!.id!,
|
||||
),
|
||||
trackId: _playback.state.value.activeTrack!.id!,
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 32),
|
||||
),
|
||||
const Gap(24),
|
||||
Obx(
|
||||
() => Column(
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 2,
|
||||
trackShape: _PlayerProgressTrackShape(),
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 8,
|
||||
),
|
||||
overlayShape: SliderComponentShape.noOverlay,
|
||||
),
|
||||
child: Slider(
|
||||
secondaryTrackValue: _playback
|
||||
.durationBuffered.value.inMilliseconds
|
||||
.abs()
|
||||
.toDouble(),
|
||||
value: _draggingValue?.abs() ??
|
||||
_playback
|
||||
.durationCurrent.value.inMilliseconds
|
||||
.toDouble()
|
||||
.abs(),
|
||||
min: 0,
|
||||
max: max(
|
||||
_playback.durationCurrent.value.inMilliseconds
|
||||
.abs(),
|
||||
_playback.durationTotal.value.inMilliseconds
|
||||
.abs(),
|
||||
).toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() => _draggingValue = value);
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
audioPlayer.seek(
|
||||
Duration(milliseconds: value.toInt()),
|
||||
);
|
||||
setState(() => _draggingValue = null);
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_playback.state.value.activeTrack?.name ??
|
||||
'Not playing',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.left,
|
||||
_playback.durationCurrent.value
|
||||
.toHumanReadableString(),
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
_playback.state.value.activeTrack?.artists
|
||||
?.asString() ??
|
||||
'No author',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.left,
|
||||
_playback.durationTotal.value
|
||||
.toHumanReadableString(),
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_playback.state.value.activeTrack != null &&
|
||||
_auth.auth.value != null)
|
||||
TrackHeartButton(
|
||||
key: ValueKey(
|
||||
_playback.state.value.activeTrack!.id!,
|
||||
),
|
||||
trackId: _playback.state.value.activeTrack!.id!,
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 32),
|
||||
),
|
||||
const Gap(24),
|
||||
Obx(
|
||||
() => Column(
|
||||
).paddingSymmetric(horizontal: 8, vertical: 4),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 24),
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 2,
|
||||
trackShape: _PlayerProgressTrackShape(),
|
||||
thumbShape: const RoundSliderThumbShape(
|
||||
enabledThumbRadius: 8,
|
||||
),
|
||||
overlayShape: SliderComponentShape.noOverlay,
|
||||
),
|
||||
child: Slider(
|
||||
secondaryTrackValue: _playback
|
||||
.durationBuffered.value.inMilliseconds
|
||||
.abs()
|
||||
.toDouble(),
|
||||
value: _draggingValue?.abs() ??
|
||||
_playback.durationCurrent.value.inMilliseconds
|
||||
.toDouble()
|
||||
.abs(),
|
||||
min: 0,
|
||||
max: max(
|
||||
_playback.durationCurrent.value.inMilliseconds
|
||||
.abs(),
|
||||
_playback.durationTotal.value.inMilliseconds
|
||||
.abs(),
|
||||
).toDouble(),
|
||||
onChanged: (value) {
|
||||
setState(() => _draggingValue = value);
|
||||
},
|
||||
onChangeEnd: (value) {
|
||||
audioPlayer.seek(
|
||||
Duration(milliseconds: value.toInt()),
|
||||
);
|
||||
setState(() => _draggingValue = null);
|
||||
},
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_playback.durationCurrent.value
|
||||
.toHumanReadableString(),
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
Text(
|
||||
_playback.durationTotal.value
|
||||
.toHumanReadableString(),
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 8, vertical: 4),
|
||||
],
|
||||
).paddingSymmetric(horizontal: 24),
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamBuilder<bool>(
|
||||
stream: audioPlayer.shuffledStream,
|
||||
builder: (context, snapshot) {
|
||||
final shuffled = snapshot.data ?? false;
|
||||
return Obx(
|
||||
() => IconButton(
|
||||
icon: Icon(
|
||||
shuffled
|
||||
? Icons.shuffle_on_outlined
|
||||
: Icons.shuffle,
|
||||
StreamBuilder<bool>(
|
||||
stream: audioPlayer.shuffledStream,
|
||||
builder: (context, snapshot) {
|
||||
final shuffled = snapshot.data ?? false;
|
||||
return Obx(
|
||||
() => IconButton(
|
||||
icon: Icon(
|
||||
shuffled
|
||||
? Icons.shuffle_on_outlined
|
||||
: Icons.shuffle,
|
||||
),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: () {
|
||||
if (shuffled) {
|
||||
audioPlayer.setShuffle(false);
|
||||
} else {
|
||||
audioPlayer.setShuffle(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: () {
|
||||
if (shuffled) {
|
||||
audioPlayer.setShuffle(false);
|
||||
} else {
|
||||
audioPlayer.setShuffle(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: audioPlayer.skipToPrevious,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: IconButton.filled(
|
||||
icon: _isFetchingActiveTrack
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
!_isPlaying
|
||||
? Icons.play_arrow
|
||||
: Icons.pause,
|
||||
size: 28,
|
||||
),
|
||||
onPressed: _togglePlayState,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: const Icon(Icons.skip_next),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: audioPlayer.skipToNext,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: Icon(
|
||||
_loopMode == PlaylistMode.none
|
||||
? Icons.repeat
|
||||
: _loopMode == PlaylistMode.loop
|
||||
? Icons.repeat_on_outlined
|
||||
: Icons.repeat_one_on_outlined,
|
||||
),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: () async {
|
||||
await audioPlayer.setLoopMode(
|
||||
switch (_loopMode) {
|
||||
PlaylistMode.loop =>
|
||||
PlaylistMode.single,
|
||||
PlaylistMode.single =>
|
||||
PlaylistMode.none,
|
||||
PlaylistMode.none => PlaylistMode.loop,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.queue_music),
|
||||
label: const Text('Queue'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PlayerQueuePopup(),
|
||||
).then((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!isLargeScreen) const Gap(4),
|
||||
if (!isLargeScreen)
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: const Icon(Icons.skip_previous),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: audioPlayer.skipToPrevious,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: 56,
|
||||
height: 56,
|
||||
child: IconButton.filled(
|
||||
icon: _isFetchingActiveTrack
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
!_isPlaying
|
||||
? Icons.play_arrow
|
||||
: Icons.pause,
|
||||
size: 28,
|
||||
),
|
||||
onPressed: _togglePlayState,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: const Icon(Icons.skip_next),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: audioPlayer.skipToNext,
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => IconButton(
|
||||
icon: Icon(
|
||||
_loopMode == PlaylistMode.none
|
||||
? Icons.repeat
|
||||
: _loopMode == PlaylistMode.loop
|
||||
? Icons.repeat_on_outlined
|
||||
: Icons.repeat_one_on_outlined,
|
||||
),
|
||||
onPressed: _isFetchingActiveTrack
|
||||
? null
|
||||
: () async {
|
||||
await audioPlayer.setLoopMode(
|
||||
switch (_loopMode) {
|
||||
PlaylistMode.loop =>
|
||||
PlaylistMode.single,
|
||||
PlaylistMode.single =>
|
||||
PlaylistMode.none,
|
||||
PlaylistMode.none =>
|
||||
PlaylistMode.loop,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.lyrics),
|
||||
label: const Text('Lyrics'),
|
||||
icon: const Icon(Icons.queue_music),
|
||||
label: const Text('Queue'),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('playerLyrics');
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const PlayerQueuePopup(),
|
||||
).then((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.merge),
|
||||
label: const Text('Sources'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const SiblingTracksPopup(),
|
||||
).then((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
},
|
||||
if (!isLargeScreen) const Gap(4),
|
||||
if (!isLargeScreen)
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.lyrics),
|
||||
label: const Text('Lyrics'),
|
||||
onPressed: () {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('playerLyrics');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
Expanded(
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.merge),
|
||||
label: const Text('Sources'),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) =>
|
||||
const SiblingTracksPopup(),
|
||||
).then((_) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isLargeScreen) const Gap(24),
|
||||
if (isLargeScreen)
|
||||
const Expanded(
|
||||
child: SyncedLyrics(defaultTextZoom: 67),
|
||||
)
|
||||
],
|
||||
if (isLargeScreen) const Gap(24),
|
||||
if (isLargeScreen)
|
||||
const Expanded(
|
||||
child: SyncedLyrics(defaultTextZoom: 67),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
).marginSymmetric(horizontal: 24),
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -5,6 +6,7 @@ import 'package:rhythm_box/providers/auth.dart';
|
||||
import 'package:rhythm_box/providers/spotify.dart';
|
||||
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||
import 'package:rhythm_box/screens/auth/login.dart';
|
||||
import 'package:rhythm_box/services/database/database.dart';
|
||||
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
||||
import 'package:rhythm_box/widgets/sized_container.dart';
|
||||
|
||||
@ -101,6 +103,53 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}),
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Obx(
|
||||
() => ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.audio_file),
|
||||
title: const Text('Audio Source'),
|
||||
subtitle:
|
||||
const Text('Choose who to provide the songs you played.'),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<AudioSource>(
|
||||
isExpanded: true,
|
||||
hint: Text(
|
||||
'Select Item',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
items: AudioSource.values
|
||||
.map((AudioSource item) =>
|
||||
DropdownMenuItem<AudioSource>(
|
||||
value: item,
|
||||
child: Text(
|
||||
item.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
value: _preferences.state.value.audioSource,
|
||||
onChanged: (AudioSource? value) {
|
||||
_preferences
|
||||
.setAudioSource(value ?? AudioSource.youtube);
|
||||
},
|
||||
buttonStyleData: const ButtonStyleData(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
height: 40,
|
||||
width: 140,
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(thickness: 0.3, height: 1),
|
||||
Obx(
|
||||
() => SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
|
Reference in New Issue
Block a user