feat: volume boost

close #37
This commit is contained in:
edde746
2025-12-23 07:58:58 +01:00
parent e9d1055c60
commit dae1750d86
14 changed files with 263 additions and 44 deletions
+110 -2
View File
@@ -4,9 +4,9 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 7
/// Strings: 3505 (500 per locale)
/// Strings: 3533 (504 per locale)
///
/// Built on 2025-12-21 at 14:43 UTC
/// Built on 2025-12-23 at 06:54 UTC
// coverage:ignore-file
// ignore_for_file: type=lint
@@ -398,6 +398,10 @@ class _StringsSettingsEn {
String get downloadOnWifiOnlyDescription => 'Prevent downloads when on cellular data';
String get cellularDownloadBlocked =>
'Downloads are disabled on cellular data. Connect to WiFi or change the setting.';
String get maxVolume => 'Maximum Volume';
String get maxVolumeDescription => 'Allow volume boost above 100% for quiet media';
String maxVolumePercent({required Object percent}) => '${percent}%';
String get maxVolumeHint => 'Enter max volume (100-300)';
}
// Path: search
@@ -1450,6 +1454,14 @@ class _StringsSettingsDe implements _StringsSettingsEn {
@override
String get cellularDownloadBlocked =>
'Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.';
@override
String get maxVolume => 'Maximale Lautstärke';
@override
String get maxVolumeDescription => 'Lautstärke über 100% für leise Medien erlauben';
@override
String maxVolumePercent({required Object percent}) => '${percent}%';
@override
String get maxVolumeHint => 'Maximale Lautstärke eingeben (100-300)';
}
// Path: search
@@ -2902,6 +2914,14 @@ class _StringsSettingsIt implements _StringsSettingsEn {
@override
String get cellularDownloadBlocked =>
'I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l\'impostazione.';
@override
String get maxVolume => 'Volume massimo';
@override
String get maxVolumeDescription => 'Consenti volume superiore al 100% per contenuti audio bassi';
@override
String maxVolumePercent({required Object percent}) => '${percent}%';
@override
String get maxVolumeHint => 'Inserisci volume massimo (100-300)';
}
// Path: search
@@ -4347,6 +4367,14 @@ class _StringsSettingsKo implements _StringsSettingsEn {
String get downloadOnWifiOnlyDescription => '셀룰러 데이터 사용 시 다운로드 불가';
@override
String get cellularDownloadBlocked => '셀룰러 데이터에서 다운로드가 차단 되었습니다. WiFi에 연결하거나 설정을 변경하세요.';
@override
String get maxVolume => '최대 볼륨';
@override
String get maxVolumeDescription => '조용한 미디어를 위해 100% 이상의 볼륨 허용';
@override
String maxVolumePercent({required Object percent}) => '${percent}%';
@override
String get maxVolumeHint => '최대 볼륨 입력 (100-300)';
}
// Path: search
@@ -5795,6 +5823,14 @@ class _StringsSettingsNl implements _StringsSettingsEn {
@override
String get cellularDownloadBlocked =>
'Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.';
@override
String get maxVolume => 'Maximaal volume';
@override
String get maxVolumeDescription => 'Volume boven 100% toestaan voor stille media';
@override
String maxVolumePercent({required Object percent}) => '${percent}%';
@override
String get maxVolumeHint => 'Voer maximaal volume in (100-300)';
}
// Path: search
@@ -7246,6 +7282,14 @@ class _StringsSettingsSv implements _StringsSettingsEn {
@override
String get cellularDownloadBlocked =>
'Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.';
@override
String get maxVolume => 'Maximal volym';
@override
String get maxVolumeDescription => 'Tillåt volym över 100% för tyst media';
@override
String maxVolumePercent({required Object percent}) => '${percent}%';
@override
String get maxVolumeHint => 'Ange maximal volym (100-300)';
}
// Path: search
@@ -8693,6 +8737,14 @@ class _StringsSettingsZh implements _StringsSettingsEn {
String get downloadOnWifiOnlyDescription => '使用蜂窝数据时禁止下载';
@override
String get cellularDownloadBlocked => '蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。';
@override
String get maxVolume => '最大音量';
@override
String get maxVolumeDescription => '允许音量超过 100% 以适应安静的媒体';
@override
String maxVolumePercent({required Object percent}) => '${percent}%';
@override
String get maxVolumeHint => '输入最大音量 (100-300)';
}
// Path: search
@@ -9988,6 +10040,14 @@ extension on Translations {
return 'Prevent downloads when on cellular data';
case 'settings.cellularDownloadBlocked':
return 'Downloads are disabled on cellular data. Connect to WiFi or change the setting.';
case 'settings.maxVolume':
return 'Maximum Volume';
case 'settings.maxVolumeDescription':
return 'Allow volume boost above 100% for quiet media';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return 'Enter max volume (100-300)';
case 'search.hint':
return 'Search movies, shows, music...';
case 'search.tryDifferentTerm':
@@ -11000,6 +11060,14 @@ extension on _StringsDe {
return 'Downloads über mobile Daten verhindern';
case 'settings.cellularDownloadBlocked':
return 'Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.';
case 'settings.maxVolume':
return 'Maximale Lautstärke';
case 'settings.maxVolumeDescription':
return 'Lautstärke über 100% für leise Medien erlauben';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return 'Maximale Lautstärke eingeben (100-300)';
case 'search.hint':
return 'Filme, Serien, Musik suchen...';
case 'search.tryDifferentTerm':
@@ -12016,6 +12084,14 @@ extension on _StringsIt {
return 'Impedisci i download quando si utilizza la rete dati cellulare';
case 'settings.cellularDownloadBlocked':
return 'I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l\'impostazione.';
case 'settings.maxVolume':
return 'Volume massimo';
case 'settings.maxVolumeDescription':
return 'Consenti volume superiore al 100% per contenuti audio bassi';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return 'Inserisci volume massimo (100-300)';
case 'search.hint':
return 'Cerca film. spettacoli, musica...';
case 'search.tryDifferentTerm':
@@ -13032,6 +13108,14 @@ extension on _StringsKo {
return '셀룰러 데이터 사용 시 다운로드 불가';
case 'settings.cellularDownloadBlocked':
return '셀룰러 데이터에서 다운로드가 차단 되었습니다. WiFi에 연결하거나 설정을 변경하세요.';
case 'settings.maxVolume':
return '최대 볼륨';
case 'settings.maxVolumeDescription':
return '조용한 미디어를 위해 100% 이상의 볼륨 허용';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return '최대 볼륨 입력 (100-300)';
case 'search.hint':
return '영화, 시리즈, 음악 등을 검색하세요...';
case 'search.tryDifferentTerm':
@@ -14045,6 +14129,14 @@ extension on _StringsNl {
return 'Voorkom downloads bij gebruik van mobiele data';
case 'settings.cellularDownloadBlocked':
return 'Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.';
case 'settings.maxVolume':
return 'Maximaal volume';
case 'settings.maxVolumeDescription':
return 'Volume boven 100% toestaan voor stille media';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return 'Voer maximaal volume in (100-300)';
case 'search.hint':
return 'Zoek films, series, muziek...';
case 'search.tryDifferentTerm':
@@ -15061,6 +15153,14 @@ extension on _StringsSv {
return 'Förhindra nedladdningar vid användning av mobildata';
case 'settings.cellularDownloadBlocked':
return 'Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.';
case 'settings.maxVolume':
return 'Maximal volym';
case 'settings.maxVolumeDescription':
return 'Tillåt volym över 100% för tyst media';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return 'Ange maximal volym (100-300)';
case 'search.hint':
return 'Sök filmer, serier, musik...';
case 'search.tryDifferentTerm':
@@ -16076,6 +16176,14 @@ extension on _StringsZh {
return '使用蜂窝数据时禁止下载';
case 'settings.cellularDownloadBlocked':
return '蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。';
case 'settings.maxVolume':
return '最大音量';
case 'settings.maxVolumeDescription':
return '允许音量超过 100% 以适应安静的媒体';
case 'settings.maxVolumePercent':
return ({required Object percent}) => '${percent}%';
case 'settings.maxVolumeHint':
return '输入最大音量 (100-300)';
case 'search.hint':
return '搜索电影、系列、音乐...';
case 'search.tryDifferentTerm':
+5 -1
View File
@@ -142,7 +142,11 @@
"downloadLocationSelectError": "Failed to select folder",
"downloadOnWifiOnly": "Download on WiFi only",
"downloadOnWifiOnlyDescription": "Prevent downloads when on cellular data",
"cellularDownloadBlocked": "Downloads are disabled on cellular data. Connect to WiFi or change the setting."
"cellularDownloadBlocked": "Downloads are disabled on cellular data. Connect to WiFi or change the setting.",
"maxVolume": "Maximum Volume",
"maxVolumeDescription": "Allow volume boost above 100% for quiet media",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "Enter max volume (100-300)"
},
"search": {
"hint": "Search movies, shows, music...",
+5 -1
View File
@@ -143,7 +143,11 @@
"downloadLocationSelectError": "Ordnerauswahl fehlgeschlagen",
"downloadOnWifiOnly": "Nur über WLAN herunterladen",
"downloadOnWifiOnlyDescription": "Downloads über mobile Daten verhindern",
"cellularDownloadBlocked": "Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung."
"cellularDownloadBlocked": "Downloads sind über mobile Daten deaktiviert. Verbinde dich mit einem WLAN oder ändere die Einstellung.",
"maxVolume": "Maximale Lautstärke",
"maxVolumeDescription": "Lautstärke über 100% für leise Medien erlauben",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "Maximale Lautstärke eingeben (100-300)"
},
"search": {
"hint": "Filme, Serien, Musik suchen...",
+5 -1
View File
@@ -143,7 +143,11 @@
"downloadLocationSelectError": "Impossibile selezionare la cartella",
"downloadOnWifiOnly": "Scarica solo con WiFi",
"downloadOnWifiOnlyDescription": "Impedisci i download quando si utilizza la rete dati cellulare",
"cellularDownloadBlocked": "I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l'impostazione."
"cellularDownloadBlocked": "I download sono disabilitati sulla rete dati cellulare. Connettiti al WiFi o modifica l'impostazione.",
"maxVolume": "Volume massimo",
"maxVolumeDescription": "Consenti volume superiore al 100% per contenuti audio bassi",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "Inserisci volume massimo (100-300)"
},
"search": {
"hint": "Cerca film. spettacoli, musica...",
+5 -1
View File
@@ -143,7 +143,11 @@
"downloadLocationSelectError": "폴더 선택 실패",
"downloadOnWifiOnly": "WiFi 연결 시에만 다운로드",
"downloadOnWifiOnlyDescription": "셀룰러 데이터 사용 시 다운로드 불가",
"cellularDownloadBlocked": "셀룰러 데이터에서 다운로드가 차단 되었습니다. WiFi에 연결하거나 설정을 변경하세요."
"cellularDownloadBlocked": "셀룰러 데이터에서 다운로드가 차단 되었습니다. WiFi에 연결하거나 설정을 변경하세요.",
"maxVolume": "최대 볼륨",
"maxVolumeDescription": "조용한 미디어를 위해 100% 이상의 볼륨 허용",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "최대 볼륨 입력 (100-300)"
},
"search": {
"hint": "영화, 시리즈, 음악 등을 검색하세요...",
+5 -1
View File
@@ -143,7 +143,11 @@
"downloadLocationSelectError": "Kan map niet selecteren",
"downloadOnWifiOnly": "Alleen via WiFi downloaden",
"downloadOnWifiOnlyDescription": "Voorkom downloads bij gebruik van mobiele data",
"cellularDownloadBlocked": "Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling."
"cellularDownloadBlocked": "Downloads zijn uitgeschakeld bij mobiele data. Maak verbinding met WiFi of wijzig de instelling.",
"maxVolume": "Maximaal volume",
"maxVolumeDescription": "Volume boven 100% toestaan voor stille media",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "Voer maximaal volume in (100-300)"
},
"search": {
"hint": "Zoek films, series, muziek...",
+5 -1
View File
@@ -143,7 +143,11 @@
"downloadLocationSelectError": "Kunde inte välja mapp",
"downloadOnWifiOnly": "Ladda ner endast på WiFi",
"downloadOnWifiOnlyDescription": "Förhindra nedladdningar vid användning av mobildata",
"cellularDownloadBlocked": "Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen."
"cellularDownloadBlocked": "Nedladdningar är inaktiverade på mobildata. Anslut till WiFi eller ändra inställningen.",
"maxVolume": "Maximal volym",
"maxVolumeDescription": "Tillåt volym över 100% för tyst media",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "Ange maximal volym (100-300)"
},
"search": {
"hint": "Sök filmer, serier, musik...",
+5 -1
View File
@@ -143,7 +143,11 @@
"downloadLocationSelectError": "选择文件夹失败",
"downloadOnWifiOnly": "仅在 WiFi 时下载",
"downloadOnWifiOnlyDescription": "使用蜂窝数据时禁止下载",
"cellularDownloadBlocked": "蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。"
"cellularDownloadBlocked": "蜂窝数据下已禁用下载。请连接 WiFi 或更改设置。",
"maxVolume": "最大音量",
"maxVolumeDescription": "允许音量超过 100% 以适应安静的媒体",
"maxVolumePercent": "${percent}%",
"maxVolumeHint": "输入最大音量 (100-300)"
},
"search": {
"hint": "搜索电影、系列、音乐...",
+1 -3
View File
@@ -1064,9 +1064,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
Widget _buildHeroSection() {
final statusBarHeight = MediaQuery.of(context).padding.top;
final useSideNav = PlatformDetector.shouldUseSideNavigation(context);
final heroHeight = useSideNav
? MediaQuery.of(context).size.height * 0.75
: 500 + statusBarHeight;
final heroHeight = useSideNav ? MediaQuery.of(context).size.height * 0.75 : 500 + statusBarHeight;
return SliverToBoxAdapter(
child: Focus(
focusNode: _heroFocusNode,
+24
View File
@@ -60,6 +60,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
int _autoSkipDelay = 5;
bool _downloadOnWifiOnly = false;
bool _videoPlayerNavigationEnabled = false;
int _maxVolume = 100;
// Update checking state
bool _isCheckingForUpdate = false;
@@ -90,6 +91,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_autoSkipDelay = _settingsService.getAutoSkipDelay();
_downloadOnWifiOnly = _settingsService.getDownloadOnWifiOnly();
_videoPlayerNavigationEnabled = _settingsService.getVideoPlayerNavigationEnabled();
_maxVolume = _settingsService.getMaxVolume();
_isLoading = false;
});
}
@@ -283,6 +285,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _showSleepTimerDurationDialog(),
),
ListTile(
leading: const AppIcon(Symbols.volume_up_rounded, fill: 1),
title: Text(t.settings.maxVolume),
subtitle: Text(t.settings.maxVolumePercent(percent: _maxVolume.toString())),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _showMaxVolumeDialog(),
),
SwitchListTile(
secondary: const AppIcon(Symbols.bookmark_rounded, fill: 1),
title: Text(t.settings.rememberTrackSelections),
@@ -966,6 +975,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _showMaxVolumeDialog() {
_showNumericInputDialog(
title: t.settings.maxVolume,
labelText: t.settings.maxVolumeDescription,
suffixText: '%',
min: 100,
max: 300,
currentValue: _maxVolume,
onSave: (value) async {
setState(() => _maxVolume = value);
await _settingsService.setMaxVolume(value);
},
);
}
void _showKeyboardShortcutsDialog() {
if (_keyboardService == null) return;
+11 -6
View File
@@ -334,8 +334,12 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
}
}
// Apply saved volume
final savedVolume = settingsService.getVolume();
// Set max volume limit for volume boost
final maxVolume = settingsService.getMaxVolume();
await player!.setProperty('volume-max', maxVolume.toString());
// Apply saved volume (clamped to max volume)
final savedVolume = settingsService.getVolume().clamp(0.0, maxVolume.toDouble());
player!.setVolume(savedVolume);
// Notify that player is ready
@@ -1647,7 +1651,10 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
child: Center(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(color: Colors.black.withValues(alpha: 0.5), shape: BoxShape.circle),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle,
),
child: const CircularProgressIndicator(color: Colors.white, strokeWidth: 3),
),
),
@@ -1661,9 +1668,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
valueListenable: _isExiting,
builder: (context, isExiting, child) {
if (!isExiting) return const SizedBox.shrink();
return Positioned.fill(
child: Container(color: Colors.black),
);
return Positioned.fill(child: Container(color: Colors.black));
},
),
],
+4 -2
View File
@@ -14,6 +14,7 @@ class KeyboardShortcutsService {
Map<String, HotKey> _hotkeys = {}; // New HotKey objects
int _seekTimeSmall = 10; // Default, loaded from settings
int _seekTimeLarge = 30; // Default, loaded from settings
int _maxVolume = 100; // Default, loaded from settings (100-300%)
KeyboardShortcutsService._();
@@ -38,6 +39,7 @@ class KeyboardShortcutsService {
_hotkeys = await _settingsService.getKeyboardHotkeys(); // Primary method
_seekTimeSmall = _settingsService.getSeekTimeSmall();
_seekTimeLarge = _settingsService.getSeekTimeLarge();
_maxVolume = _settingsService.getMaxVolume();
}
Map<String, String> get shortcuts => Map.from(_shortcuts);
@@ -258,12 +260,12 @@ class KeyboardShortcutsService {
player.playOrPause();
break;
case 'volume_up':
final newVolume = (player.state.volume + 10).clamp(0.0, 100.0);
final newVolume = (player.state.volume + 10).clamp(0.0, _maxVolume.toDouble());
player.setVolume(newVolume);
_settingsService.setVolume(newVolume);
break;
case 'volume_down':
final newVolume = (player.state.volume - 10).clamp(0.0, 100.0);
final newVolume = (player.state.volume - 10).clamp(0.0, _maxVolume.toDouble());
player.setVolume(newVolume);
_settingsService.setVolume(newVolume);
break;
+11
View File
@@ -53,6 +53,7 @@ class SettingsService extends BaseSharedPreferencesService {
static const String _keyShowPerformanceOverlay = 'show_performance_overlay';
static const String _keyMpvConfigEntries = 'mpv_config_entries';
static const String _keyMpvConfigPresets = 'mpv_config_presets';
static const String _keyMaxVolume = 'max_volume';
SettingsService._();
@@ -222,6 +223,15 @@ class SettingsService extends BaseSharedPreferencesService {
return prefs.getDouble(_keyVolume) ?? 100.0; // Default: full volume
}
// Max Volume (100-300%, for volume boost)
Future<void> setMaxVolume(int percent) async {
await prefs.setInt(_keyMaxVolume, percent.clamp(100, 300));
}
int getMaxVolume() {
return prefs.getInt(_keyMaxVolume) ?? 100; // Default: 100% (no boost)
}
// Rotation Lock (mobile only)
Future<void> setRotationLocked(bool locked) async {
await prefs.setBool(_keyRotationLocked, locked);
@@ -907,6 +917,7 @@ class SettingsService extends BaseSharedPreferencesService {
prefs.remove(_keyAudioSyncOffset),
prefs.remove(_keySubtitleSyncOffset),
prefs.remove(_keyVolume),
prefs.remove(_keyMaxVolume),
prefs.remove(_keySubtitleFontSize),
prefs.remove(_keySubtitleTextColor),
prefs.remove(_keySubtitleBorderSize),
@@ -52,6 +52,24 @@ class _VolumeControlState extends State<VolumeControl> {
/// Volume step size for keyboard adjustment.
static const double _volumeStep = 5.0;
/// Maximum volume from settings (100-300).
int _maxVolume = 100;
@override
void initState() {
super.initState();
_loadMaxVolume();
}
Future<void> _loadMaxVolume() async {
final settings = await SettingsService.getInstance();
if (mounted) {
setState(() {
_maxVolume = settings.getMaxVolume();
});
}
}
void _enterAdjustMode() {
setState(() {
_isAdjustMode = true;
@@ -66,7 +84,7 @@ class _VolumeControlState extends State<VolumeControl> {
Future<void> _adjustVolume(double delta) async {
final currentVolume = widget.player.state.volume;
final newVolume = (currentVolume + delta).clamp(0.0, 100.0);
final newVolume = (currentVolume + delta).clamp(0.0, _maxVolume.toDouble());
widget.player.setVolume(newVolume);
final settings = await SettingsService.getInstance();
await settings.setVolume(newVolume);
@@ -178,33 +196,58 @@ class _VolumeControlState extends State<VolumeControl> {
Widget _buildVolumeSlider(double volume, bool isKeyboardMode) {
// Show visual indicator when in adjust mode with keyboard
final showAdjustIndicator = _isAdjustMode && isKeyboardMode;
final maxVolumeDouble = _maxVolume.toDouble();
// Calculate 100% marker position as fraction of slider width
// Only show marker if max volume > 100
final showMarker = _maxVolume > 100;
final markerPosition = showMarker ? (100.0 / maxVolumeDouble) : 0.0;
return SizedBox(
width: 100,
child: SliderTheme(
data: SliderThemeData(
trackHeight: showAdjustIndicator ? 4 : 3,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: showAdjustIndicator ? 8 : 6),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
),
child: Semantics(
label: t.videoControls.volumeSlider,
slider: true,
child: Slider(
value: volume,
min: 0.0,
max: 100.0,
onChanged: (value) {
widget.player.setVolume(value);
},
onChangeEnd: (value) async {
final settings = await SettingsService.getInstance();
await settings.setVolume(value);
},
activeColor: Colors.white,
inactiveColor: Colors.white.withValues(alpha: 0.3),
child: Stack(
alignment: Alignment.centerLeft,
children: [
// 100% marker (only shown if max > 100)
if (showMarker)
Positioned(
left: 100 * markerPosition - 1, // Adjust for marker width
child: Container(
width: 2,
height: 12,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(1),
),
),
),
// Volume slider
SliderTheme(
data: SliderThemeData(
trackHeight: showAdjustIndicator ? 4 : 3,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: showAdjustIndicator ? 8 : 6),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
),
child: Semantics(
label: t.videoControls.volumeSlider,
slider: true,
child: Slider(
value: volume.clamp(0.0, maxVolumeDouble),
min: 0.0,
max: maxVolumeDouble,
onChanged: (value) {
widget.player.setVolume(value);
},
onChangeEnd: (value) async {
final settings = await SettingsService.getInstance();
await settings.setVolume(value);
},
activeColor: Colors.white,
inactiveColor: Colors.white.withValues(alpha: 0.3),
),
),
),
),
],
),
);
}