feat(lyrics): keep the screen awake while viewing lyrics

This commit is contained in:
zarzet committed 2026-09-30 20:21:21 +07:00
1 parent a1cec6de51
commit 8aa8d598d8
14 files changed
+276

No files matched your search

+2
View File
@@ -1544,6 +1544,8 @@
"@sectionLyrics": {
"description": "Settings section header"
},
"lyricsKeepScreenOn": "Keep screen on while viewing lyrics",
"lyricsKeepScreenOnSubtitle": "Prevent screen timeout while lyrics are open, including when playback is paused.",
"lyricsMode": "Lyrics Mode",
"@lyricsMode": {
"description": "Setting - how to save lyrics"
+2
View File
@@ -1512,6 +1512,8 @@
"@sectionLyrics": {
"description": "Settings section header"
},
"lyricsKeepScreenOn": "Layar tetap menyala saat melihat lirik",
"lyricsKeepScreenOnSubtitle": "Cegah layar mati otomatis selama lirik terbuka, termasuk saat lagu dijeda.",
"lyricsMode": "Mode Lirik",
"@lyricsMode": {
"description": "Setting - how to save lyrics"
+4
View File
@@ -81,6 +81,7 @@ class AppSettings {
extensionVerificationBrowserMode; // 'external_first' or 'in_app_first'
final String locale;
final String lyricsMode;
final bool keepScreenOnLyrics;
final bool autoConvertDownloads;
// Re-download a Hi-Res request at LOSSLESS when the file measures as fake.
final bool redownloadFakeHiRes;
@@ -185,6 +186,7 @@ class AppSettings {
this.extensionVerificationBrowserMode = 'in_app_first',
this.locale = 'system',
this.lyricsMode = 'embed',
this.keepScreenOnLyrics = true,
this.autoConvertDownloads = false,
this.redownloadFakeHiRes = false,
this.autoConvertFormat = 'mp3',
@@ -281,6 +283,7 @@ class AppSettings {
String? extensionVerificationBrowserMode,
String? locale,
String? lyricsMode,
bool? keepScreenOnLyrics,
bool? autoConvertDownloads,
bool? redownloadFakeHiRes,
String? autoConvertFormat,
@@ -382,6 +385,7 @@ class AppSettings {
this.extensionVerificationBrowserMode,
locale: locale ?? this.locale,
lyricsMode: lyricsMode ?? this.lyricsMode,
keepScreenOnLyrics: keepScreenOnLyrics ?? this.keepScreenOnLyrics,
autoConvertDownloads: autoConvertDownloads ?? this.autoConvertDownloads,
redownloadFakeHiRes: redownloadFakeHiRes ?? this.redownloadFakeHiRes,
autoConvertFormat: autoConvertFormat ?? this.autoConvertFormat,
+2
View File
@@ -67,6 +67,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
json['extensionVerificationBrowserMode'] as String? ?? 'in_app_first',
locale: json['locale'] as String? ?? 'system',
lyricsMode: json['lyricsMode'] as String? ?? 'embed',
keepScreenOnLyrics: json['keepScreenOnLyrics'] as bool? ?? true,
autoConvertDownloads: json['autoConvertDownloads'] as bool? ?? false,
redownloadFakeHiRes: json['redownloadFakeHiRes'] as bool? ?? false,
autoConvertFormat: json['autoConvertFormat'] as String? ?? 'mp3',
@@ -164,6 +165,7 @@ Map<String, dynamic> _$AppSettingsToJson(
'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode,
'locale': instance.locale,
'lyricsMode': instance.lyricsMode,
'keepScreenOnLyrics': instance.keepScreenOnLyrics,
'autoConvertDownloads': instance.autoConvertDownloads,
'redownloadFakeHiRes': instance.redownloadFakeHiRes,
'autoConvertFormat': instance.autoConvertFormat,
+5
View File
@@ -613,6 +613,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
}
}
void setKeepScreenOnLyrics(bool enabled) {
state = state.copyWith(keepScreenOnLyrics: enabled);
_saveSettings();
}
void setLyricsMode(String mode) {
if (mode == 'embed' || mode == 'external' || mode == 'both') {
state = state.copyWith(lyricsMode: mode);
+10
View File
@@ -42,6 +42,7 @@ import 'package:spotiflac_android/widgets/lyric_supplement_transition.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/audio_output_button.dart';
import 'package:spotiflac_android/widgets/lyric_gap_indicator.dart';
import 'package:spotiflac_android/widgets/lyrics_screen_awake.dart';
import 'package:spotiflac_android/widgets/player_artwork.dart';
import 'package:spotiflac_android/widgets/player_queue_dismissible.dart';
import 'package:spotiflac_android/widgets/player_track_swipe.dart';
@@ -549,6 +550,15 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
@override
Widget build(BuildContext context) {
return LyricsScreenAwake(
visible:
_currentPage == 1 &&
ref.watch(currentMediaItemProvider).value != null,
child: _buildPlayer(context),
);
}
Widget _buildPlayer(BuildContext context) {
final mornye = context.isMornye;
final colorScheme = mornye
? MornyeTheme.fromContext(
@@ -34,6 +34,15 @@ class LyricsSettingsPage extends ConsumerWidget {
SliverToBoxAdapter(
child: SettingsGroup(
children: [
SettingsSwitchItem(
icon: Icons.light_mode_outlined,
title: context.l10n.lyricsKeepScreenOn,
subtitle: context.l10n.lyricsKeepScreenOnSubtitle,
value: settings.keepScreenOnLyrics,
onChanged: (value) => ref
.read(settingsProvider.notifier)
.setKeepScreenOnLyrics(value),
),
SettingsSwitchItem(
icon: Icons.subtitles_outlined,
title: context.l10n.optionsEmbedLyrics,
@@ -307,6 +307,12 @@ class SettingsSearchCatalog {
),
],
lyrics = [
SettingsSearchEntry(
icon: Icons.light_mode_outlined,
title: l10n.lyricsKeepScreenOn,
subtitle: l10n.lyricsKeepScreenOnSubtitle,
keywords: const ['screen timeout', 'awake', 'sleep', 'layar'],
),
SettingsSearchEntry(
icon: Icons.subtitles_outlined,
title: l10n.optionsEmbedLyrics,
+9
View File
@@ -173,6 +173,15 @@ class PlatformBridge {
static const int notificationPercentTotal = 10000;
static const _channel = MethodChannel('com.zarz.spotiflac/backend');
static Future<void> setScreenAwake(bool enabled) async {
if (defaultTargetPlatform != TargetPlatform.android &&
defaultTargetPlatform != TargetPlatform.iOS) {
return;
}
await _channel.invokeMethod<void>('setScreenAwake', {'enabled': enabled});
}
static const _jsonResultFileKey = '__json_file';
static const _backgroundJsonDecodeThresholdBytes = 128 * 1024;
static const _metadataCacheTtl = Duration(minutes: 20);
+78
View File
@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/logger.dart';
final _log = AppLogger('LyricsScreenAwake');
/// Holds only the visible lyrics screen awake, never background audio playback.
class LyricsScreenAwake extends ConsumerStatefulWidget {
const LyricsScreenAwake({
required this.visible,
required this.child,
super.key,
});
final bool visible;
final Widget child;
@override
ConsumerState<LyricsScreenAwake> createState() => _LyricsScreenAwakeState();
}
class _LyricsScreenAwakeState extends ConsumerState<LyricsScreenAwake>
with WidgetsBindingObserver {
bool _wanted = false;
bool _requested = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
void _update({bool force = false}) {
final lifecycle = WidgetsBinding.instance.lifecycleState;
final enabled =
_wanted &&
(lifecycle == null || lifecycle == AppLifecycleState.resumed);
if (!force && enabled == _requested) return;
_requested = enabled;
unawaited(_setAwake(enabled));
}
Future<void> _setAwake(bool enabled) async {
try {
await PlatformBridge.setScreenAwake(enabled);
} catch (error) {
_log.w('Could not update screen timeout: $error');
}
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Reapply after resuming: Android may have recreated the Activity/window.
_update(force: state == AppLifecycleState.resumed);
}
@override
Widget build(BuildContext context) {
final enabled = ref.watch(
settingsProvider.select((settings) => settings.keepScreenOnLyrics),
);
_wanted =
widget.visible && enabled && (ModalRoute.isCurrentOf(context) ?? true);
_update();
return widget.child;
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
if (_requested) unawaited(_setAwake(false));
super.dispose();
}
}