889 lines
30 KiB
Plaintext
889 lines
30 KiB
Plaintext
import 'dart:io';
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../services/app_settings_service.dart';
|
||
import '../services/media_cache_settings_service.dart';
|
||
import '../services/preferred_language_service.dart';
|
||
|
||
class SettingsPage extends StatelessWidget {
|
||
const SettingsPage({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SafeArea(
|
||
child: ListView(
|
||
padding: EdgeInsets.fromLTRB(20, 22, 20, 32),
|
||
children: [
|
||
Text(
|
||
'Paramètres',
|
||
style: TextStyle(
|
||
fontSize: 28,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
SizedBox(height: 6),
|
||
Text(
|
||
'Personnalise le comportement de YouTube Personal.',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
fontSize: 13,
|
||
),
|
||
),
|
||
SizedBox(height: 24),
|
||
_SettingsSection(
|
||
title: 'Apparence',
|
||
icon: Icons.brightness_6_outlined,
|
||
child: ValueListenableBuilder<String>(
|
||
valueListenable: AppSettingsService.instance.appearanceMode,
|
||
builder: (context, appearanceMode, child) {
|
||
return _SettingsRow(
|
||
title: 'Thème de l’application',
|
||
subtitle:
|
||
'Système suit automatiquement le thème clair ou sombre configuré sur Windows ou Android.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<String>(
|
||
value: appearanceMode,
|
||
dropdownColor:
|
||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: AppSettingsService.supportedAppearanceModes
|
||
.map(
|
||
(mode) => DropdownMenuItem<String>(
|
||
value: mode,
|
||
child: Text(
|
||
AppSettingsService.appearanceModeLabel(mode),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: (value) {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
AppSettingsService.instance.setAppearanceMode(value);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
SizedBox(height: 14),
|
||
_SettingsSection(
|
||
title: 'Lecture',
|
||
icon: Icons.play_circle_outline,
|
||
child: ValueListenableBuilder<int>(
|
||
valueListenable: AppSettingsService.instance.defaultResolution,
|
||
builder: (context, resolution, child) {
|
||
return _SettingsRow(
|
||
title: 'Résolution par défaut',
|
||
subtitle:
|
||
'Utilisée à l’ouverture d’une nouvelle vidéo. La qualité reste modifiable dans le lecteur.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<int>(
|
||
value: resolution,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: AppSettingsService.supportedResolutions
|
||
.map(
|
||
(value) => DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
AppSettingsService.resolutionLabel(value),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: (value) {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
|
||
AppSettingsService.instance
|
||
.setDefaultResolution(value);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
SizedBox(height: 14),
|
||
_SettingsSection(
|
||
title: 'Recommandations',
|
||
icon: Icons.language,
|
||
child: ValueListenableBuilder<String>(
|
||
valueListenable: AppSettingsService.instance.preferredLanguage,
|
||
builder: (context, language, child) {
|
||
return _SettingsRow(
|
||
title: 'Langue préférée des vidéos',
|
||
subtitle:
|
||
'Favorise cette langue dans le Radar et dans l’ordre des résultats de recherche, sans masquer les autres langues.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<String>(
|
||
value: language,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: PreferredLanguageService.options
|
||
.map(
|
||
(option) => DropdownMenuItem<String>(
|
||
value: option.code,
|
||
child: Text(option.label),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: (value) {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
|
||
AppSettingsService.instance
|
||
.setPreferredLanguage(value);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
SizedBox(height: 14),
|
||
_SettingsSection(
|
||
title: 'Filtre contenu très jeune',
|
||
icon: Icons.child_care,
|
||
child: ValueListenableBuilder<int>(
|
||
valueListenable:
|
||
AppSettingsService.instance.youngAudienceFilterLevel,
|
||
builder: (context, level, child) {
|
||
return _SettingsRow(
|
||
title: 'Chaînes / vidéos orientées très jeune',
|
||
subtitle:
|
||
'Modéré masque surtout les contenus explicitement enfants, bébés, jouets ou comptines. Strict élargit le filtrage aux chaînes très fortement orientées jeune public. Le filtre s’applique aussi à Continuer à regarder, Continuer ce sujet et aux autres recommandations automatiques. Il se base sur les métadonnées textuelles, pas sur l’apparence des personnes.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<int>(
|
||
value: level,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: AppSettingsService
|
||
.supportedYoungAudienceFilterLevels
|
||
.map(
|
||
(value) => DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
AppSettingsService
|
||
.youngAudienceFilterLabel(value),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: (value) {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
|
||
AppSettingsService.instance
|
||
.setYoungAudienceFilterLevel(value);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
if (Platform.isWindows) ...[
|
||
SizedBox(height: 14),
|
||
_WindowsMediaCacheSettings(),
|
||
],
|
||
if (Platform.isAndroid) ...[
|
||
SizedBox(height: 14),
|
||
_AndroidMediaCacheSettings(),
|
||
SizedBox(height: 14),
|
||
_SettingsSection(
|
||
title: 'Écran Android',
|
||
icon: Icons.bedtime_outlined,
|
||
child: ValueListenableBuilder<int>(
|
||
valueListenable:
|
||
AppSettingsService.instance.pauseScreenSleepMinutes,
|
||
builder: (context, minutes, child) {
|
||
return _SettingsRow(
|
||
title: 'Mise en veille après pause',
|
||
subtitle:
|
||
'Pendant la lecture, l’écran reste allumé. À la fin de la vidéo, la mise en veille système redevient immédiatement possible. En pause, le maintien de l’écran est libéré après ce délai.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<int>(
|
||
value: minutes,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: AppSettingsService
|
||
.supportedPauseScreenSleepMinutes
|
||
.map(
|
||
(value) => DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
AppSettingsService
|
||
.pauseScreenSleepLabel(value),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: (value) {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
|
||
AppSettingsService.instance
|
||
.setPauseScreenSleepMinutes(value);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
SizedBox(height: 14),
|
||
_SettingsSection(
|
||
title: 'Navigation',
|
||
icon: Icons.dashboard_outlined,
|
||
child: Text(
|
||
'La barre du bas mobile a été simplifiée à 5 onglets : Accueil, Recherche, Abos, Bibliothèque et Réglages. Historique, Favoris et Chaînes sont maintenant regroupés dans Bibliothèque pour un rendu plus lisible.',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
height: 1.45,
|
||
fontSize: 12.5,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _WindowsMediaCacheSettings extends StatefulWidget {
|
||
const _WindowsMediaCacheSettings();
|
||
|
||
@override
|
||
State<_WindowsMediaCacheSettings> createState() =>
|
||
_WindowsMediaCacheSettingsState();
|
||
}
|
||
|
||
class _WindowsMediaCacheSettingsState
|
||
extends State<_WindowsMediaCacheSettings> {
|
||
final MediaCacheSettingsService _cache =
|
||
MediaCacheSettingsService.instance;
|
||
|
||
bool _loading = true;
|
||
String _effectivePath = '';
|
||
int _currentBytes = 0;
|
||
int _defaultBytes = 0;
|
||
int _maxGb = MediaCacheSettingsService.defaultMaxCacheGb;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_reload();
|
||
}
|
||
|
||
Future<void> _reload() async {
|
||
setState(() => _loading = true);
|
||
|
||
await _cache.ensureInitialized();
|
||
final effectivePath = await _cache.effectiveCachePath();
|
||
final currentBytes = await _cache.currentCacheSizeBytes();
|
||
final defaultBytes = await _cache.defaultCacheSizeBytes();
|
||
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_effectivePath = effectivePath;
|
||
_currentBytes = currentBytes;
|
||
_defaultBytes = defaultBytes;
|
||
_maxGb = _cache.maxCacheGb.value;
|
||
_loading = false;
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final usingCustomPath = _cache.customCachePath.value.trim().isNotEmpty;
|
||
final defaultPath = _cache.defaultCachePath;
|
||
|
||
return _SettingsSection(
|
||
title: 'Cache média Windows',
|
||
icon: Icons.storage_outlined,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Le cache accélère les relectures et les bascules HD. Il est maintenant limité automatiquement pour éviter de remplir le disque système.',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
fontSize: 12.5,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
SizedBox(height: 16),
|
||
Text(
|
||
'Emplacement',
|
||
style: TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
SizedBox(height: 6),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.surface,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||
),
|
||
child: SelectableText(
|
||
_loading ? 'Calcul...' : _effectivePath,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
fontSize: 12.5,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 10),
|
||
Wrap(
|
||
spacing: 8,
|
||
runSpacing: 8,
|
||
children: [
|
||
OutlinedButton.icon(
|
||
onPressed: _loading ? null : _changePath,
|
||
icon: Icon(Icons.folder_outlined),
|
||
label: Text('Modifier le chemin'),
|
||
),
|
||
if (usingCustomPath)
|
||
TextButton.icon(
|
||
onPressed: _loading ? null : _resetPath,
|
||
icon: Icon(Icons.restart_alt),
|
||
label: Text('Revenir au chemin par défaut'),
|
||
),
|
||
],
|
||
),
|
||
SizedBox(height: 16),
|
||
_SettingsRow(
|
||
title: 'Taille maximale',
|
||
subtitle:
|
||
'Quand la limite est dépassée, les vidéos les moins récemment utilisées sont supprimées. La vidéo active est protégée.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<int>(
|
||
value: _maxGb,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: MediaCacheSettingsService.supportedMaxCacheGb
|
||
.map(
|
||
(value) => DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
MediaCacheSettingsService.maxCacheLabel(value),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: _loading
|
||
? null
|
||
: (value) async {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
await _cache.setMaxCacheGb(value);
|
||
await _reload();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 16),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
_loading
|
||
? 'Taille actuelle : calcul...'
|
||
: 'Taille actuelle : ${MediaCacheSettingsService.formatBytesForUi(_currentBytes)}',
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
),
|
||
OutlinedButton.icon(
|
||
onPressed: _loading ? null : _clearCurrentCache,
|
||
icon: Icon(Icons.delete_sweep_outlined),
|
||
label: Text('Vider le cache'),
|
||
),
|
||
],
|
||
),
|
||
if (usingCustomPath &&
|
||
_defaultBytes > 0 &&
|
||
_effectivePath.toLowerCase() != defaultPath.toLowerCase()) ...[
|
||
SizedBox(height: 14),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Color(0x1AFF9800),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Color(0x44FF9800)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Ancien cache sur C: ${MediaCacheSettingsService.formatBytesForUi(_defaultBytes)}',
|
||
style: TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
SizedBox(height: 4),
|
||
SelectableText(
|
||
defaultPath,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
SizedBox(height: 8),
|
||
TextButton.icon(
|
||
onPressed: _clearDefaultCache,
|
||
icon: Icon(Icons.delete_outline),
|
||
label: Text('Supprimer cet ancien cache'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _changePath() async {
|
||
final initialValue = _cache.customCachePath.value.trim().isNotEmpty
|
||
? _cache.customCachePath.value
|
||
: _effectivePath;
|
||
final controller = TextEditingController(text: initialValue);
|
||
|
||
final value = await showDialog<String>(
|
||
context: context,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: Text('Emplacement du cache'),
|
||
content: SizedBox(
|
||
width: 560,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Entre un dossier disposant de suffisamment d’espace. Exemple : D:\\YouTubePersonalCache',
|
||
),
|
||
SizedBox(height: 14),
|
||
TextField(
|
||
controller: controller,
|
||
autofocus: true,
|
||
decoration: InputDecoration(
|
||
labelText: 'Chemin du cache',
|
||
border: OutlineInputBorder(),
|
||
),
|
||
),
|
||
SizedBox(height: 8),
|
||
Text(
|
||
'Le changement ne copie pas les anciens fichiers. Ils pourront être supprimés séparément.',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||
child: Text('Annuler'),
|
||
),
|
||
FilledButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(
|
||
controller.text,
|
||
),
|
||
child: Text('Utiliser ce dossier'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
|
||
controller.dispose();
|
||
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await _cache.setCustomCachePath(value);
|
||
await _reload();
|
||
} catch (error) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('Impossible d’utiliser ce dossier : $error'),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _resetPath() async {
|
||
await _cache.resetCustomCachePath();
|
||
await _reload();
|
||
}
|
||
|
||
Future<void> _clearCurrentCache() async {
|
||
final confirmed = await _confirmDelete(
|
||
'Vider le cache média ?',
|
||
'Ferme d’abord toute vidéo en cours. Les vidéos mises en cache devront être retéléchargées lors de la prochaine lecture.',
|
||
);
|
||
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
await _cache.clearCurrentCache();
|
||
await _reload();
|
||
}
|
||
|
||
Future<void> _clearDefaultCache() async {
|
||
final confirmed = await _confirmDelete(
|
||
'Supprimer l’ancien cache sur C: ?',
|
||
'Cette opération libérera l’espace utilisé par l’ancien cache média. Le nouveau cache restera intact.',
|
||
);
|
||
|
||
if (!confirmed) {
|
||
return;
|
||
}
|
||
|
||
await _cache.clearDefaultCache();
|
||
await _reload();
|
||
}
|
||
|
||
Future<bool> _confirmDelete(String title, String message) async {
|
||
final result = await showDialog<bool>(
|
||
context: context,
|
||
builder: (dialogContext) {
|
||
return AlertDialog(
|
||
title: Text(title),
|
||
content: Text(message),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||
child: Text('Annuler'),
|
||
),
|
||
FilledButton(
|
||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||
child: Text('Supprimer'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
|
||
return result == true;
|
||
}
|
||
}
|
||
|
||
class _AndroidMediaCacheSettings extends StatefulWidget {
|
||
const _AndroidMediaCacheSettings();
|
||
|
||
@override
|
||
State<_AndroidMediaCacheSettings> createState() =>
|
||
_AndroidMediaCacheSettingsState();
|
||
}
|
||
|
||
class _AndroidMediaCacheSettingsState
|
||
extends State<_AndroidMediaCacheSettings> {
|
||
final MediaCacheSettingsService _cache =
|
||
MediaCacheSettingsService.instance;
|
||
|
||
bool _loading = true;
|
||
int _bufferMb = MediaCacheSettingsService.defaultAndroidBufferMb;
|
||
int _diskMode = MediaCacheSettingsService.androidDiskCacheDisabled;
|
||
String _path = '';
|
||
int _sizeBytes = 0;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_reload();
|
||
}
|
||
|
||
Future<void> _reload() async {
|
||
if (mounted) {
|
||
setState(() => _loading = true);
|
||
}
|
||
|
||
await _cache.ensureInitialized();
|
||
final mode = _cache.androidDiskCacheMode.value;
|
||
final path = await _cache.androidCachePathForMode(mode);
|
||
final bytes = await _cache.androidCacheSizeBytes(mode);
|
||
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_bufferMb = _cache.androidBufferMb.value;
|
||
_diskMode = mode;
|
||
_path = path;
|
||
_sizeBytes = bytes;
|
||
_loading = false;
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _SettingsSection(
|
||
title: 'Cache média Android',
|
||
icon: Icons.memory_outlined,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Par défaut, Android garde le cache vidéo en RAM pour éviter de remplir le stockage. Le cache disque temporaire est optionnel et reste dans l’espace privé de l’application.',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
fontSize: 12.5,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
SizedBox(height: 16),
|
||
_SettingsRow(
|
||
title: 'Tampon mémoire vidéo',
|
||
subtitle:
|
||
'Limite le cache démuxeur de media_kit. Le changement s’applique aux nouvelles pages vidéo.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<int>(
|
||
value: _bufferMb,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: MediaCacheSettingsService.supportedAndroidBufferMb
|
||
.map(
|
||
(value) => DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
MediaCacheSettingsService.androidBufferLabel(value),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: _loading
|
||
? null
|
||
: (value) async {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
await _cache.setAndroidBufferMb(value);
|
||
await _reload();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 14),
|
||
_SettingsRow(
|
||
title: 'Cache disque temporaire',
|
||
subtitle:
|
||
'RAM uniquement est recommandé. Interne/Externe utilise le cache temporaire mpv et le supprime à la fermeture normale de la vidéo.',
|
||
trailing: DropdownButtonHideUnderline(
|
||
child: DropdownButton<int>(
|
||
value: _diskMode,
|
||
dropdownColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||
borderRadius: BorderRadius.circular(14),
|
||
items: MediaCacheSettingsService.supportedAndroidDiskCacheModes
|
||
.map(
|
||
(value) => DropdownMenuItem<int>(
|
||
value: value,
|
||
child: Text(
|
||
MediaCacheSettingsService
|
||
.androidDiskCacheModeLabel(value),
|
||
),
|
||
),
|
||
)
|
||
.toList(growable: false),
|
||
onChanged: _loading
|
||
? null
|
||
: (value) async {
|
||
if (value == null) {
|
||
return;
|
||
}
|
||
await _cache.setAndroidDiskCacheMode(value);
|
||
await _reload();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
if (_diskMode !=
|
||
MediaCacheSettingsService.androidDiskCacheDisabled) ...[
|
||
SizedBox(height: 14),
|
||
Text(
|
||
'Emplacement',
|
||
style: TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
SizedBox(height: 6),
|
||
Container(
|
||
width: double.infinity,
|
||
padding: EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.surface,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||
),
|
||
child: SelectableText(
|
||
_loading
|
||
? 'Calcul...'
|
||
: (_path.isEmpty ? 'Indisponible' : _path),
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||
fontSize: 12.5,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'Taille temporaire : ${MediaCacheSettingsService.formatBytesForUi(_sizeBytes)}',
|
||
style: TextStyle(fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
OutlinedButton.icon(
|
||
onPressed: _loading ? null : _clear,
|
||
icon: Icon(Icons.delete_sweep_outlined),
|
||
label: Text('Vider'),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
SizedBox(height: 10),
|
||
Text(
|
||
'Android ne permet pas à l’application d’écrire librement dans n’importe quel dossier sans passer par le sélecteur de stockage. Le choix Externe utilise donc le dossier externe privé de l’application, sans permission supplémentaire.',
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.62),
|
||
fontSize: 11.5,
|
||
height: 1.35,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _clear() async {
|
||
await _cache.clearAndroidMediaCache(_diskMode);
|
||
await _reload();
|
||
}
|
||
}
|
||
|
||
class _SettingsSection extends StatelessWidget {
|
||
const _SettingsSection({
|
||
required this.title,
|
||
required this.icon,
|
||
required this.child,
|
||
});
|
||
|
||
final String title;
|
||
final IconData icon;
|
||
final Widget child;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(color: Theme.of(context).colorScheme.outlineVariant),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(icon, size: 19, color: Color(0xFFFF6B63)),
|
||
SizedBox(width: 8),
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
SizedBox(height: 14),
|
||
child,
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SettingsRow extends StatelessWidget {
|
||
const _SettingsRow({
|
||
required this.title,
|
||
required this.subtitle,
|
||
required this.trailing,
|
||
});
|
||
|
||
final String title;
|
||
final String subtitle;
|
||
final Widget trailing;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final narrow = MediaQuery.sizeOf(context).width < 560;
|
||
|
||
final text = Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
SizedBox(height: 4),
|
||
Text(
|
||
subtitle,
|
||
style: TextStyle(
|
||
color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.78),
|
||
fontSize: 12,
|
||
height: 1.35,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
|
||
if (narrow) {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
text,
|
||
SizedBox(height: 12),
|
||
trailing,
|
||
],
|
||
);
|
||
}
|
||
|
||
return Row(
|
||
children: [
|
||
Expanded(child: text),
|
||
SizedBox(width: 18),
|
||
trailing,
|
||
],
|
||
);
|
||
}
|
||
}
|