save more battery now, few other changes idr

This commit is contained in:
Pato05 2024-01-28 12:02:59 +01:00
parent faec2af805
commit 8679cf844f
No known key found for this signature in database
GPG Key ID: ED4C6F9C3D574FB6
15 changed files with 447 additions and 289 deletions

View File

@ -146,37 +146,40 @@ class DeezerAPI {
//Wrapper so it can be globally awaited
Future<bool> authorize() async => _authorizing ??= rawAuthorize();
//Login with email FROM DEEMIX-JS
Future<String> getArlByEmail(String email, String password) async {
//Get MD5 of password
final md5Password = md5.convert(utf8.encode(password)).toString();
final hash = md5
.convert(utf8
.encode([CLIENT_ID, email, md5Password, CLIENT_SECRET].join('')))
.toString();
//Get access token
// String url =
// "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json";
// http.Response response = await http.get(Uri.parse(url));
// String? accessToken = jsonDecode(response.body)["access_token"];
final res = await dio.get('https://api.deezer.com/auth/token',
queryParameters: {
'app_id': CLIENT_ID,
'login': email,
'password': md5Password,
'hash': hash
},
options: Options(responseType: ResponseType.json));
print(res.data);
final accessToken = res.data['access_token'] as String?;
if (accessToken == null) {
throw Exception('login failed, access token is null');
}
// NOT WORKING ANYMORE.
// this didn't last very long now, did it?
print(accessToken);
return getArlByAccessToken(accessToken);
}
// //Login with email FROM DEEMIX-JS
// Future<String> getArlByEmail(String email, String password) async {
// //Get MD5 of password
// final md5Password = md5.convert(utf8.encode(password)).toString();
// final hash = md5
// .convert(utf8
// .encode([CLIENT_ID, email, md5Password, CLIENT_SECRET].join('')))
// .toString();
// //Get access token
// // String url =
// // "https://tv.deezer.com/smarttv/8caf9315c1740316053348a24d25afc7/user_auth.php?login=$email&password=$md5password&device=panasonic&output=json";
// // http.Response response = await http.get(Uri.parse(url));
// // String? accessToken = jsonDecode(response.body)["access_token"];
// final res = await dio.get('https://api.deezer.com/auth/token',
// queryParameters: {
// 'app_id': CLIENT_ID,
// 'login': email,
// 'password': md5Password,
// 'hash': hash
// },
// options: Options(responseType: ResponseType.json));
// print(res.data);
// final accessToken = res.data['access_token'] as String?;
// if (accessToken == null) {
// throw Exception('login failed, access token is null');
// }
//
// print(accessToken);
//
// return getArlByAccessToken(accessToken);
// }
// FROM DEEMIX-JS
Future<String> getArlByAccessToken(String accessToken) async {

View File

@ -14,15 +14,20 @@ class Paths {
final target =
await Directory(path.join(home, '.local', 'share', 'freezer'))
.create();
if (kDebugMode) {
return (await Directory(path.join(target.path, 'debug')).create())
.path;
}
return target.path;
}
return path.dirname(Platform.resolvedExecutable);
case TargetPlatform.windows:
final String? localAppData = Platform.environment['LOCALAPPDATA'];
if (localAppData != null) {
final target = await Directory(path.join(localAppData, 'Freezer')).create();
return target.path;
}
final String? localAppData = Platform.environment['LOCALAPPDATA'];
if (localAppData != null) {
final target =
await Directory(path.join(localAppData, 'Freezer')).create();
return target.path;
}
String? home = Platform.environment['USERPROFILE'];
if (home == null) {
final drive = Platform.environment['HOMEDRIVE'];
@ -35,7 +40,12 @@ class Paths {
}
final target =
await Directory(path.join(home, 'AppData', 'Local', 'Freezer')).create();
await Directory(path.join(home, 'AppData', 'Local', 'Freezer'))
.create();
if (kDebugMode) {
return (await Directory(path.join(target.path, 'debug')).create())
.path;
}
return target.path;
default:
return (await getApplicationDocumentsDirectory()).path;

View File

@ -171,8 +171,8 @@ class AudioPlayerTask extends BaseAudioHandler {
);
if (initArgs.ignoreInterruptions) {
session.interruptionEventStream.listen((_) {});
session.becomingNoisyEventStream.listen((_) {});
//session.interruptionEventStream.listen((_) {});
//session.becomingNoisyEventStream.listen((_) {});
}
//Update track index

View File

@ -39,6 +39,13 @@ class PlayerHelper {
final _bufferPositionSubject = BehaviorSubject<Duration>();
ValueStream<Duration> get bufferPosition => _bufferPositionSubject.stream;
final _playingSubject = BehaviorSubject<bool>();
ValueStream<bool> get playing => _playingSubject.stream;
final _processingStateSubject = BehaviorSubject<AudioProcessingState>();
ValueStream<AudioProcessingState> get processingState =>
_processingStateSubject.stream;
/// Find queue index by id
///
/// The function gets more expensive the longer the queue is and the further the element is from the beginning.
@ -60,13 +67,13 @@ class PlayerHelper {
builder: () => AudioPlayerTask(initArgs),
config: AudioServiceConfig(
notificationColor: settings.primaryColor,
androidStopForegroundOnPause: false,
androidNotificationOngoing: false,
androidStopForegroundOnPause: true,
androidNotificationOngoing: true,
androidNotificationClickStartsActivity: true,
androidNotificationChannelDescription: 'Freezer',
androidNotificationChannelName: 'Freezer',
androidNotificationIcon: 'drawable/ic_logo',
preloadArtwork: false,
preloadArtwork: true,
),
cacheManager: cacheManager,
);
@ -81,8 +88,6 @@ class PlayerHelper {
Logger('PlayerHelper').fine("event received: ${event['action']}");
switch (event['action']) {
case 'onLoad':
//After audio_service is loaded, load queue, set quality
await settings.updateAudioServiceQuality();
break;
case 'onRestore':
//Load queueSource from isolate
@ -140,6 +145,21 @@ class PlayerHelper {
cache.history.add(Track.fromMediaItem(mediaItem));
cache.save();
});
_playbackStateStreamSubscription =
audioHandler.playbackState.listen((playbackState) {
if (!_processingStateSubject.hasValue ||
_processingStateSubject.value != playbackState.processingState) {
_processingStateSubject.add(playbackState.processingState);
}
print(
'now ${playbackState.playing}, previous ${_playingSubject.valueOrNull}');
if (!_playingSubject.hasValue ||
_playingSubject.value != playbackState.playing) {
print('added!');
_playingSubject.add(playbackState.playing);
}
});
//Start audio_service
// await startService(); it is already ready, there is no need to start it
@ -178,16 +198,15 @@ class PlayerHelper {
}
//Executed before exit
Future onExit() async {
Future stop() async {
_customEventSubscription.cancel();
_playbackStateStreamSubscription.cancel();
_mediaItemSubscription.cancel();
_started = false;
}
//Replace queue, play specified track id
Future<void> _loadQueuePlay(List<MediaItem> queue, int? index) async {
await settings.updateAudioServiceQuality();
if (index != null) {
await audioHandler.customAction('setIndex', {'index': index});
}
@ -270,7 +289,6 @@ class PlayerHelper {
//Load and play
// await startService(); // audioservice is ready
await settings.updateAudioServiceQuality();
await setQueueSource(queueSource);
await audioHandler.customAction('setIndex', {'index': index});
await audioHandler.updateQueue(queue);

View File

@ -113,7 +113,7 @@ void main() async {
DefaultCacheManager.key,
// cache aggressively
stalePeriod: const Duration(days: 30),
maxNrOfCacheObjects: 1000,
maxNrOfCacheObjects: 5000,
));
// cacheManager = HiveCacheManager(
// boxName: 'freezer-images', boxPath: await Paths.cacheDir());
@ -142,32 +142,32 @@ class FreezerApp extends StatefulWidget {
State<FreezerApp> createState() => _FreezerAppState();
}
class _FreezerAppState extends State<FreezerApp> {
late StreamSubscription _playbackStateSub;
class _FreezerAppState extends State<FreezerApp> with WidgetsBindingObserver {
@override
void initState() {
_initStateAsync();
WidgetsBinding.instance.addObserver(this);
super.initState();
}
Future<void> _initStateAsync() async {
_playbackStateChanged(audioHandler.playbackState.value);
_playbackStateSub =
audioHandler.playbackState.listen(_playbackStateChanged);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
playerHelper.stop();
break;
case AppLifecycleState.resumed:
playerHelper.start();
break;
Future<void> _playbackStateChanged(PlaybackState playbackState) async {
if (playbackState.processingState == AudioProcessingState.idle ||
playbackState.processingState == AudioProcessingState.error) {
// TODO: reconnect maybe?
return;
default:
print('lifecycle: $state');
}
}
@override
void dispose() {
_playbackStateSub.cancel();
WidgetsBinding.instance.removeObserver(this);
playerHelper.stop();
super.dispose();
}
@ -191,13 +191,35 @@ class _FreezerAppState extends State<FreezerApp> {
builder: (context, child) =>
DynamicColorBuilder(builder: (lightScheme, darkScheme) {
final lightTheme = settings.materialYouAccent
? ThemeData(colorScheme: lightScheme, useMaterial3: true)
? ThemeData(
colorScheme: lightScheme,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
)
: settings.themeData;
final darkTheme = settings.materialYouAccent
? ThemeData(
colorScheme: darkScheme,
useMaterial3: true,
brightness: Brightness.dark)
brightness: Brightness.dark,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
)
: null;
return MaterialApp(
title: 'Freezer',
@ -269,13 +291,13 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
}
Future _logOut() async {
await deezerAPI.logout();
await settings.save();
await Cache.wipe();
setState(() {
settings.arl = null;
settings.offlineMode = false;
});
await deezerAPI.logout();
await settings.save();
await Cache.wipe();
}
@override
@ -672,18 +694,17 @@ class MainScreenState extends State<MainScreen>
onKey: _handleKey,
child: LayoutBuilder(builder: (context, constraints) {
// check if we're able to display the desktop layout
if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
final isLandscape = constraints.maxWidth > constraints.maxHeight;
isDesktop = isLandscape &&
constraints.maxWidth >= 1100 &&
constraints.maxHeight >= 600;
}
final isLandscape = constraints.maxWidth > constraints.maxHeight;
isDesktop = isLandscape &&
constraints.maxWidth >= 1100 &&
constraints.maxHeight >= 600;
return FancyScaffold(
key: _fancyScaffoldKey,
navigationRail: _buildNavigationRail(isDesktop),
bottomNavigationBar: buildBottomBar(isDesktop),
bottomPanel: Builder(
builder: (context) => PlayerBar(
backgroundColor: Theme.of(context).cardColor,
focusNode: playerBarFocusNode,
onTap: FancyScaffold.of(context)!.openPanel,
shouldHaveHero: false,

View File

@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/api/player/audio_handler.dart';
@ -170,6 +171,9 @@ class Settings {
NavigationRailAppearance navigationRailAppearance =
NavigationRailAppearance.expand_on_hover;
@HiveField(49, defaultValue: true)
bool enableMaterial3PlayButton = true;
static LazyBox<Settings>? __box;
static Future<LazyBox<Settings>> get _box async =>
__box ??= await Hive.openLazyBox<Settings>('settings');
@ -267,12 +271,6 @@ class Settings {
downloadManager.updateServiceSettings();
}
Future<void> updateAudioServiceQuality() async {
//Send wifi & mobile quality to audio service isolate
await audioHandler.customAction('updateQuality',
{'mobileQuality': mobileQuality, 'wifiQuality': wifiQuality});
}
// MaterialColor get _primarySwatch =>
// MaterialColor(primaryColor.value, <int, Color>{
// 50: primaryColor.withOpacity(.1),
@ -319,6 +317,15 @@ class Settings {
sliderTheme: _sliderTheme,
bottomAppBarTheme: const BottomAppBarTheme(color: Color(0xfff5f5f5)),
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.dark,
systemNavigationBarIconBrightness: Brightness.dark,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
),
Themes.Dark: ThemeData(
textTheme: textTheme,
@ -331,6 +338,15 @@ class Settings {
),
sliderTheme: _sliderTheme,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
),
Themes.Deezer: ThemeData(
textTheme: textTheme,
@ -349,6 +365,15 @@ class Settings {
const BottomSheetThemeData(backgroundColor: deezerBottom),
cardColor: deezerBg,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
),
Themes.Black: ThemeData(
textTheme: textTheme,
@ -370,6 +395,15 @@ class Settings {
BottomSheetThemeData(backgroundColor: _elevation1Black),
cardColor: _elevation1Black,
useMaterial3: true,
appBarTheme: const AppBarTheme(
systemOverlayStyle: SystemUiOverlayStyle(
statusBarBrightness: Brightness.light,
statusBarIconBrightness: Brightness.light,
systemNavigationBarIconBrightness: Brightness.light,
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
)),
)
};

View File

@ -1,5 +1,6 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:freezer/api/deezer.dart';
import 'package:freezer/api/definitions.dart';
import 'package:freezer/api/player/audio_handler.dart';
@ -54,15 +55,19 @@ class HomeScreen extends StatelessWidget {
PointerDeviceKind.trackpad
},
),
child: SafeArea(
child: Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, _) => [
SliverPersistentHeader(
delegate: _SearchHeaderDelegate(), floating: true)
],
body: const HomePageWidget(cacheable: true),
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: Theme.of(context).appBarTheme.systemOverlayStyle ??
const SystemUiOverlayStyle(),
child: SafeArea(
child: Scaffold(
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (context, _) => [
SliverPersistentHeader(
delegate: _SearchHeaderDelegate(), floating: true)
],
body: const HomePageWidget(cacheable: true),
),
),
),
),

View File

@ -209,19 +209,19 @@ class _LoginWidgetState extends State<LoginWidget> {
style: const TextStyle(fontSize: 16.0),
),
const SizedBox(height: 16.0),
//Email login dialog
ElevatedButton(
child: Text(
'Login using email'.i18n,
),
onPressed: () {
showDialog(
context: context,
builder: (context) =>
EmailLogin(_update));
},
),
const SizedBox(height: 2.0),
//Email login dialog (Not working anymore)
// ElevatedButton(
// child: Text(
// 'Login using email'.i18n,
// ),
// onPressed: () {
// showDialog(
// context: context,
// builder: (context) =>
// EmailLogin(_update));
// },
// ),
// const SizedBox(height: 2.0),
// only supported on android
if (Platform.isAndroid)
@ -345,7 +345,7 @@ class LoginBrowser extends StatelessWidget {
controller.evaluateJavascript(
source: 'window.location.href = "/open_app"');
}
print('scheme ${uri.scheme}, host: ${uri.host}');
print('uri $uri');
//Parse arl from url
if (uri.scheme == 'intent' && uri.host == 'deezer.page.link') {
try {
@ -367,107 +367,109 @@ class LoginBrowser extends StatelessWidget {
}
}
class EmailLogin extends StatefulWidget {
final Function callback;
const EmailLogin(this.callback, {Key? key}) : super(key: key);
// email login is removed cuz not working = USELESS
@override
State<EmailLogin> createState() => _EmailLoginState();
}
class _EmailLoginState extends State<EmailLogin> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _loading = false;
Future _login() async {
setState(() => _loading = true);
//Try logging in
String? arl;
String? exception;
try {
arl = await deezerAPI.getArlByEmail(
_emailController.text, _passwordController.text);
} catch (e, st) {
exception = e.toString();
print(e);
print(st);
}
setState(() => _loading = false);
//Success
if (arl != null) {
settings.arl = arl;
Navigator.of(context).pop();
widget.callback();
return;
}
//Error
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Error logging in!".i18n),
content: Text(
"Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"),
actions: [
TextButton(
child: Text('Dismiss'.i18n),
onPressed: () {
Navigator.of(context).pop();
},
)
],
));
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Email Login'.i18n),
content: AutofillGroup(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
enabled: !_loading,
decoration: InputDecoration(labelText: 'Email'.i18n),
controller: _emailController,
autofillHints: const [
AutofillHints.email,
],
),
const SizedBox(height: 8.0),
TextFormField(
enabled: !_loading,
obscureText: true,
decoration: InputDecoration(labelText: "Password".i18n),
controller: _passwordController,
autofillHints: const [
AutofillHints.password,
],
)
],
),
),
actions: [
TextButton(
onPressed: _loading
? null
: () async {
if (_emailController.text.isNotEmpty &&
_passwordController.text.isNotEmpty) {
await _login();
} else {
ScaffoldMessenger.of(context)
.snack("Missing email or password!".i18n);
}
},
child: _loading
? const CircularProgressIndicator()
: const Text('Login'),
)
],
);
}
}
//class EmailLogin extends StatefulWidget {
// final Function callback;
// const EmailLogin(this.callback, {Key? key}) : super(key: key);
//
// @override
// State<EmailLogin> createState() => _EmailLoginState();
//}
//
//class _EmailLoginState extends State<EmailLogin> {
// final _emailController = TextEditingController();
// final _passwordController = TextEditingController();
// bool _loading = false;
//
// Future _login() async {
// setState(() => _loading = true);
// //Try logging in
// String? arl;
// String? exception;
// try {
// arl = await deezerAPI.getArlByEmail(
// _emailController.text, _passwordController.text);
// } catch (e, st) {
// exception = e.toString();
// print(e);
// print(st);
// }
// setState(() => _loading = false);
//
// //Success
// if (arl != null) {
// settings.arl = arl;
// Navigator.of(context).pop();
// widget.callback();
// return;
// }
//
// //Error
// showDialog(
// context: context,
// builder: (context) => AlertDialog(
// title: Text("Error logging in!".i18n),
// content: Text(
// "Error logging in using email, please check your credentials.\nError: ${exception ?? 'Unknown'}"),
// actions: [
// TextButton(
// child: Text('Dismiss'.i18n),
// onPressed: () {
// Navigator.of(context).pop();
// },
// )
// ],
// ));
// }
//
// @override
// Widget build(BuildContext context) {
// return AlertDialog(
// title: Text('Email Login'.i18n),
// content: AutofillGroup(
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// TextFormField(
// enabled: !_loading,
// decoration: InputDecoration(labelText: 'Email'.i18n),
// controller: _emailController,
// autofillHints: const [
// AutofillHints.email,
// ],
// ),
// const SizedBox(height: 8.0),
// TextFormField(
// enabled: !_loading,
// obscureText: true,
// decoration: InputDecoration(labelText: "Password".i18n),
// controller: _passwordController,
// autofillHints: const [
// AutofillHints.password,
// ],
// )
// ],
// ),
// ),
// actions: [
// TextButton(
// onPressed: _loading
// ? null
// : () async {
// if (_emailController.text.isNotEmpty &&
// _passwordController.text.isNotEmpty) {
// await _login();
// } else {
// ScaffoldMessenger.of(context)
// .snack("Missing email or password!".i18n);
// }
// },
// child: _loading
// ? const CircularProgressIndicator()
// : const Text('Login'),
// )
// ],
// );
// }
// }

View File

@ -45,12 +45,14 @@ class LyricsWidget extends StatefulWidget {
State<LyricsWidget> createState() => _LyricsWidgetState();
}
class _LyricsWidgetState extends State<LyricsWidget> {
class _LyricsWidgetState extends State<LyricsWidget>
with WidgetsBindingObserver {
late StreamSubscription _mediaItemSub;
late StreamSubscription _playbackStateSub;
int? _currentIndex = -1;
Duration _nextOffset = Duration.zero;
Duration _currentOffset = Duration.zero;
String? _currentTrackId;
final ScrollController _controller = ScrollController();
final double height = 90;
BoxConstraints? _widgetConstraints;
@ -64,8 +66,12 @@ class _LyricsWidgetState extends State<LyricsWidget> {
bool _syncedLyrics = false;
Future<void> _loadForId(String trackId) async {
if (_currentTrackId == trackId) return;
_currentTrackId = trackId;
print('cancelling req?');
// cancel current request, if applicable
_lyricsCancelToken?.cancel();
_currentIndex = -1;
_currentOffset = Duration.zero;
_nextOffset = Duration.zero;
@ -83,6 +89,7 @@ class _LyricsWidgetState extends State<LyricsWidget> {
_lyricsCancelToken = CancelToken();
final lyrics =
await deezerAPI.lyrics(trackId, cancelToken: _lyricsCancelToken);
_syncedLyrics = lyrics.sync;
if (!mounted) return;
setState(() {
@ -95,10 +102,14 @@ class _LyricsWidgetState extends State<LyricsWidget> {
} on DioException catch (e) {
if (e.type != DioExceptionType.cancel) rethrow;
} catch (e) {
_currentTrackId = null;
if (!mounted) return;
setState(() {
_error = e;
});
} finally {
_lyricsCancelToken =
null; // dispose of cancel token after lyrics are fetched.
}
}
@ -115,8 +126,6 @@ class _LyricsWidgetState extends State<LyricsWidget> {
scrollTo = minScroll - widgetHeight / 2 + height / 2;
}
print(
'${height * _currentIndex!}, ${MediaQuery.of(context).size.height / 2}');
if (scrollTo < 0.0) scrollTo = 0.0;
if (scrollTo > _controller.position.maxScrollExtent) {
scrollTo = _controller.position.maxScrollExtent;
@ -152,28 +161,49 @@ class _LyricsWidgetState extends State<LyricsWidget> {
_scrollToLyric();
}
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_playbackStateSub = AudioService.position.listen(_updatePosition);
});
void _makeSubscriptions() {
_playbackStateSub = AudioService.position.listen(_updatePosition);
/// Track change = ~exit~ reload lyrics
/// Track change = reload new lyrics
_mediaItemSub = audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return;
if (_controller.hasClients) _controller.jumpTo(0.0);
_loadForId(mediaItem.id);
});
}
@override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
//Enable visualizer
// if (settings.lyricsVisualizer) playerHelper.startVisualizer();
_makeSubscriptions();
});
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
_mediaItemSub.cancel();
_playbackStateSub.cancel();
break;
case AppLifecycleState.resumed:
_makeSubscriptions();
break;
default:
break;
}
}
@override
void dispose() {
_mediaItemSub.cancel();
_playbackStateSub.cancel();
WidgetsBinding.instance.removeObserver(this);
//Stop visualizer
// if (settings.lyricsVisualizer) playerHelper.stopVisualizer();
super.dispose();

View File

@ -148,28 +148,27 @@ class MenuSheet {
showModalBottomSheet(
isScrollControlled: false, // true,
context: context,
useSafeArea: true,
builder: (BuildContext context) {
return SafeArea(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: (MediaQuery.of(context).orientation ==
Orientation.landscape)
? 220
: 350,
),
child: SingleChildScrollView(
child: Column(
children: options
.map((option) => ListTile(
title: option.label,
leading: option.icon,
onTap: () {
option.onTap.call();
Navigator.pop(context);
},
))
.toList(growable: false)),
),
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight:
(MediaQuery.of(context).orientation == Orientation.landscape)
? 220
: 350,
),
child: SingleChildScrollView(
child: Column(
children: options
.map((option) => ListTile(
title: option.label,
leading: option.icon,
onTap: () {
option.onTap.call();
Navigator.pop(context);
},
))
.toList(growable: false)),
),
);
});
@ -197,13 +196,13 @@ class MenuSheet {
return DraggableScrollableSheet(
initialChildSize: 0.5,
minChildSize: 0.45,
maxChildSize: 0.75,
builder: (context, scrollController) => SafeArea(
child: Material(
type: MaterialType.card,
clipBehavior: Clip.antiAlias,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(20.0)),
maxChildSize: 0.95,
builder: (context, scrollController) => Material(
type: MaterialType.card,
clipBehavior: Clip.antiAlias,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(20.0)),
child: SafeArea(
child: CustomScrollView(
controller: scrollController,
slivers: [

View File

@ -69,6 +69,7 @@ class PlayerBar extends StatelessWidget {
}
final currentMediaItem = snapshot.data!;
final image = CachedImage(
rounded: true,
width: 50,
height: 50,
url: currentMediaItem.extras!['thumb'] ??
@ -185,6 +186,7 @@ class PrevNextButton extends StatelessWidget {
class PlayPauseButton extends StatefulWidget {
final double size;
final bool filled;
final bool material3;
final Color? iconColor;
/// The color of the card if [filled] is true
@ -193,6 +195,7 @@ class PlayPauseButton extends StatefulWidget {
this.size, {
Key? key,
this.filled = false,
this.material3 = true,
this.color,
this.iconColor,
}) : super(key: key);
@ -205,7 +208,8 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
late StreamSubscription _subscription;
late StreamSubscription _stateSubscription;
late StreamSubscription _playingSubscription;
late bool _canPlay = audioHandler.playbackState.value.processingState ==
AudioProcessingState.ready ||
audioHandler.playbackState.value.processingState ==
@ -217,26 +221,30 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
vsync: this, duration: const Duration(milliseconds: 200));
_animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
_subscription = audioHandler.playbackState.listen((playbackState) {
if (playbackState.processingState == AudioProcessingState.ready ||
_stateSubscription = playerHelper.processingState.listen((processingState) {
print('yes!');
if (processingState == AudioProcessingState.ready ||
audioHandler.playbackState.value.processingState ==
AudioProcessingState.idle) {
if (playbackState.playing) {
_controller.forward();
} else {
_controller.reverse();
}
if (!_canPlay) setState(() => _canPlay = true);
return;
}
setState(() => _canPlay = false);
});
_playingSubscription = playerHelper.playing.listen((playing) {
if (playing) {
_controller.forward();
} else {
_controller.reverse();
}
});
super.initState();
}
@override
void dispose() {
_subscription.cancel();
_stateSubscription.cancel();
_playingSubscription.cancel();
_controller.dispose();
super.dispose();
}
@ -261,7 +269,7 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
: 'Play'.i18n,
);
if (!widget.filled) {
return IconButton(
child = IconButton(
color: widget.iconColor,
icon: icon,
iconSize: widget.size,
@ -270,7 +278,7 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
child = Material(
type: MaterialType.transparency,
child: InkWell(
customBorder: const CircleBorder(),
customBorder: widget.material3 ? null : const CircleBorder(),
onTap: _playPause,
child: IconTheme.merge(
child: Center(child: icon),
@ -290,16 +298,33 @@ class _PlayPauseButtonState extends State<PlayPauseButton>
break;
}
}
if (widget.filled) {
return SizedBox.square(
dimension: widget.size,
child: AnimatedContainer(
duration: const Duration(seconds: 1),
decoration:
BoxDecoration(shape: BoxShape.circle, color: widget.color),
child: child));
} else {
return SizedBox.square(dimension: widget.size, child: child);
if (widget.material3 && widget.filled) {
return StreamBuilder<bool>(
stream: playerHelper.playing,
builder: (context, snapshot) {
return AnimatedContainer(
clipBehavior: Clip.antiAlias,
width: widget.size,
height: widget.size,
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
borderRadius: snapshot.data == true
? BorderRadius.circular(24.0)
: BorderRadius.circular(36.0),
color: widget.color),
child: child);
});
}
if (widget.filled) {
return AnimatedContainer(
width: widget.size,
height: widget.size,
duration: const Duration(seconds: 1),
decoration:
BoxDecoration(shape: BoxShape.circle, color: widget.color),
child: child);
}
return SizedBox.square(dimension: widget.size * 2, child: child);
}
}

View File

@ -61,6 +61,7 @@ class BackgroundProvider extends ChangeNotifier {
@override
void addListener(VoidCallback listener) {
print('[PROVIDER] listener added $hasListeners');
_mediaItemSub ??= audioHandler.mediaItem.listen((mediaItem) {
if (mediaItem == null) return;
_updateColor(mediaItem);
@ -71,14 +72,12 @@ class BackgroundProvider extends ChangeNotifier {
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
if (!hasListeners && _mediaItemSub != null) {
_mediaItemSub!.cancel();
_mediaItemSub = null;
}
print('[PROVIDER] listener removed! hasListeners? $hasListeners');
}
@override
void dispose() {
print('[PROVIDER] DISPOSED');
_isDisposed = true;
_mediaItemSub?.cancel();
super.dispose();
@ -243,7 +242,7 @@ class PlayerScreenHorizontal extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
padding: const EdgeInsets.fromLTRB(0, 0, 8, 0),
child: PlayerScreenTopRow(
textSize: 24.sp,
iconSize: 36.sp,
@ -251,19 +250,19 @@ class PlayerScreenHorizontal extends StatelessWidget {
short: true),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: PlayerTextSubtext(textSize: 35.sp),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SeekBar(textSize: 24.sp),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: PlaybackControls(46.sp),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: BottomBarControls(size: 30.sp),
)
],
@ -457,6 +456,7 @@ class _FitOrScrollTextState extends State<FitOrScrollText> {
style: widget.style,
blankSpace: 32.0,
startPadding: 0.0,
numberOfRounds: 2,
accelerationDuration: const Duration(seconds: 1),
pauseAfterRound: const Duration(seconds: 2),
crossAxisAlignment: CrossAxisAlignment.end,
@ -491,7 +491,9 @@ class PlayerTextSubtext extends StatelessWidget {
text: currentMediaItem.displayTitle!,
maxLines: 1,
style: TextStyle(
fontSize: textSize, fontWeight: FontWeight.bold)),
fontSize: textSize,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis)),
),
// child: currentMediaItem.displayTitle!.length >= 26
// ? Marquee(
@ -552,6 +554,12 @@ class QualityInfoWidget extends StatelessWidget {
stream: playerHelper.streamInfo.map<String>(_getQualityStringFromInfo),
builder: (context, snapshot) {
return TextButton(
// style: ButtonStyle(
// elevation: MaterialStatePropertyAll(0.5),
// padding: MaterialStatePropertyAll(
// EdgeInsets.symmetric(horizontal: 16, vertical: 4)),
// foregroundColor: MaterialStatePropertyAll(
// Theme.of(context).colorScheme.onSurface)),
child: Text(snapshot.data ?? '',
style: textSize == null ? null : TextStyle(fontSize: textSize)),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
@ -795,6 +803,7 @@ class PlaybackControls extends StatelessWidget {
: darken(provider.dominantColor!);
return PlayPauseButton(size * 2.25,
filled: true,
material3: settings.enableMaterial3PlayButton,
color: color,
iconColor: Color.lerp(
(ThemeData.estimateBrightnessForColor(color) ==
@ -1172,6 +1181,7 @@ class BottomBarControls extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
QualityInfoWidget(textSize: size * 0.75),
const Expanded(child: SizedBox()),
if (!desktopMode)
IconButton(
iconSize: size,

View File

@ -18,15 +18,6 @@ class QueueScreen extends StatelessWidget {
return Scaffold(
appBar: AppBar(
title: Text('Queue'.i18n),
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: Theme.of(context).scaffoldBackgroundColor,
systemNavigationBarDividerColor: Color(
Theme.of(context).scaffoldBackgroundColor.value - 0x00111111),
systemNavigationBarIconBrightness: Brightness.light,
),
// actions: <Widget>[
// IconButton(
// icon: Icon(

View File

@ -349,13 +349,21 @@ class _AppearanceSettingsState extends State<AppearanceSettings> {
}),
),
SwitchListTile(
title: const Text('Enable filled play button'),
title: Text('Enable filled play button'.i18n),
secondary: const Icon(Icons.play_circle),
value: settings.enableFilledPlayButton,
onChanged: (bool v) {
setState(() => settings.enableFilledPlayButton = v);
settings.save();
}),
SwitchListTile(
title: Text('Material 3 play button'.i18n),
secondary: const Icon(Icons.play_circle_outline),
value: settings.enableMaterial3PlayButton,
onChanged: (bool v) {
setState(() => settings.enableMaterial3PlayButton = v);
settings.save();
}),
SwitchListTile(
title: Text('Visualizer'.i18n),
subtitle: Text(
@ -724,15 +732,12 @@ class _QualityPickerState extends State<QualityPicker> {
switch (widget.field) {
case 'mobile_wifi':
settings.mobileQuality = settings.wifiQuality = _quality;
settings.updateAudioServiceQuality();
break;
case 'mobile':
settings.mobileQuality = _quality;
settings.updateAudioServiceQuality();
break;
case 'wifi':
settings.wifiQuality = _quality;
settings.updateAudioServiceQuality();
break;
case 'download':
settings.downloadQuality = _quality;
@ -1818,6 +1823,10 @@ class _CreditsScreenState extends State<CreditsScreen> {
title: Text('Pato05'),
subtitle: Text('Current Developer - best of all'),
),
const ListTile(
title: Text('iDrinkCoffee'),
subtitle: Text('idk, he\'s romanian'),
),
const ListTile(
title: Text('exttex'),
subtitle: Text('Ex-Developer'),

View File

@ -269,6 +269,7 @@ class PlaylistTile extends StatelessWidget {
leading: CachedImage(
url: playlist!.image!.thumb,
width: 48,
rounded: true,
),
onTap: onTap,
onLongPress: normalizeSecondary(onSecondary),