feat(quality): harden lint rules and add quality-gate tests
This commit is contained in:
@@ -49,9 +49,8 @@ class EstadoIdioma extends ChangeNotifier {
|
||||
final partes = value.split('_');
|
||||
final languageCode = partes.first;
|
||||
if (languageCode.isEmpty) return null;
|
||||
final countryCode = partes.length > 1 && partes[1].isNotEmpty
|
||||
? partes[1]
|
||||
: null;
|
||||
final countryCode =
|
||||
partes.length > 1 && partes[1].isNotEmpty ? partes[1] : null;
|
||||
return Locale.fromSubtags(
|
||||
languageCode: languageCode,
|
||||
countryCode: countryCode,
|
||||
|
||||
@@ -136,7 +136,11 @@ class Emisora {
|
||||
/// Lista de géneros/tags como lista limpia.
|
||||
List<String> get generos {
|
||||
if (tags == null || tags!.isEmpty) return [];
|
||||
return tags!.split(',').map((t) => t.trim()).where((t) => t.isNotEmpty).toList();
|
||||
return tags!
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.where((t) => t.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static String? _nonEmpty(String? s) =>
|
||||
@@ -148,7 +152,9 @@ class Emisora {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Emisora && runtimeType == other.runtimeType && uuid == other.uuid;
|
||||
other is Emisora &&
|
||||
runtimeType == other.runtimeType &&
|
||||
uuid == other.uuid;
|
||||
|
||||
@override
|
||||
int get hashCode => uuid.hashCode;
|
||||
|
||||
@@ -5,21 +5,47 @@ class PresetEcualizador {
|
||||
final List<double> bandas; // 5 valores entre -12.0 y +12.0 dB
|
||||
|
||||
const PresetEcualizador({required this.nombre, required this.bandas})
|
||||
: assert(bandas.length == 5);
|
||||
: assert(bandas.length == 5);
|
||||
|
||||
static final flat = PresetEcualizador(nombre: 'Flat', bandas: [0.0, 0.0, 0.0, 0.0, 0.0]);
|
||||
static final rock = PresetEcualizador(nombre: 'Rock', bandas: [2.0, 1.0, -1.0, 2.0, 3.0]);
|
||||
static final pop = PresetEcualizador(nombre: 'Pop', bandas: [1.0, 1.5, 0.5, 1.0, 1.5]);
|
||||
static final bassBoost = PresetEcualizador(nombre: 'Bass Boost', bandas: [5.0, 3.0, -1.0, 0.5, 0.0]);
|
||||
static final jazz = PresetEcualizador(nombre: 'Jazz', bandas: [3.0, -1.0, -1.5, 2.0, 4.0]);
|
||||
static final voz = PresetEcualizador(nombre: 'Voz', bandas: [-2.0, -1.0, 2.0, 3.0, 1.0]);
|
||||
static final flat = PresetEcualizador(
|
||||
nombre: 'Flat',
|
||||
bandas: [0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
);
|
||||
static final rock = PresetEcualizador(
|
||||
nombre: 'Rock',
|
||||
bandas: [2.0, 1.0, -1.0, 2.0, 3.0],
|
||||
);
|
||||
static final pop = PresetEcualizador(
|
||||
nombre: 'Pop',
|
||||
bandas: [1.0, 1.5, 0.5, 1.0, 1.5],
|
||||
);
|
||||
static final bassBoost = PresetEcualizador(
|
||||
nombre: 'Bass Boost',
|
||||
bandas: [5.0, 3.0, -1.0, 0.5, 0.0],
|
||||
);
|
||||
static final jazz = PresetEcualizador(
|
||||
nombre: 'Jazz',
|
||||
bandas: [3.0, -1.0, -1.5, 2.0, 4.0],
|
||||
);
|
||||
static final voz = PresetEcualizador(
|
||||
nombre: 'Voz',
|
||||
bandas: [-2.0, -1.0, 2.0, 3.0, 1.0],
|
||||
);
|
||||
|
||||
static final presets = [flat, rock, pop, bassBoost, jazz, voz];
|
||||
|
||||
factory PresetEcualizador.desdeJson(Map<String, dynamic> json) {
|
||||
final raw = (json['bandas'] as List?)?.map((e) => (e as num).toDouble()).toList() ?? <double>[];
|
||||
final bandas = List<double>.generate(5, (i) => i < raw.length ? raw[i] : 0.0);
|
||||
return PresetEcualizador(nombre: json['nombre'] as String? ?? 'Personalizado', bandas: bandas);
|
||||
final raw =
|
||||
(json['bandas'] as List?)?.map((e) => (e as num).toDouble()).toList() ??
|
||||
<double>[];
|
||||
final bandas = List<double>.generate(
|
||||
5,
|
||||
(i) => i < raw.length ? raw[i] : 0.0,
|
||||
);
|
||||
return PresetEcualizador(
|
||||
nombre: json['nombre'] as String? ?? 'Personalizado',
|
||||
bandas: bandas,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {'nombre': nombre, 'bandas': bandas};
|
||||
|
||||
@@ -1226,11 +1226,10 @@ class _SeccionBackup extends StatelessWidget {
|
||||
if (result == null || result.files.single.path == null) return;
|
||||
|
||||
final file = File(result.files.single.path!);
|
||||
final contenido = await file.readAsString();
|
||||
if (!context.mounted) return;
|
||||
// Parsing is owned by ServicioExportImport (S4-R4): null = malformed.
|
||||
final json = context.read<EstadoRadio>().parsearConfigJson(
|
||||
await file.readAsString(),
|
||||
);
|
||||
final json = context.read<EstadoRadio>().parsearConfigJson(contenido);
|
||||
if (json == null) {
|
||||
throw const FormatException('invalid backup file');
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ class ServicioFavoritos {
|
||||
DatabaseFactory? databaseFactory,
|
||||
Future<String> Function()? databasePathProvider,
|
||||
String? databaseName,
|
||||
}) : _databaseFactory = databaseFactory,
|
||||
_databasePathProvider = databasePathProvider ?? getDatabasesPath,
|
||||
_databaseName = databaseName ?? _dbName;
|
||||
}) : _databaseFactory = databaseFactory,
|
||||
_databasePathProvider = databasePathProvider ?? getDatabasesPath,
|
||||
_databaseName = databaseName ?? _dbName;
|
||||
|
||||
final DatabaseFactory? _databaseFactory;
|
||||
final Future<String> Function() _databasePathProvider;
|
||||
@@ -175,7 +175,8 @@ class ServicioFavoritos {
|
||||
Future<GrupoFavoritos> crearGrupo(String nombre) async {
|
||||
final db = await _database;
|
||||
final normalizado = _normalizarNombreGrupo(nombre);
|
||||
final maxOrden = Sqflite.firstIntValue(
|
||||
final maxOrden =
|
||||
Sqflite.firstIntValue(
|
||||
await db.rawQuery('SELECT MAX(orden) FROM grupos_favoritos'),
|
||||
) ??
|
||||
0;
|
||||
@@ -232,7 +233,8 @@ class ServicioFavoritos {
|
||||
|
||||
Future<void> asignarGrupo(String uuid, String grupoId) async {
|
||||
final db = await _database;
|
||||
final existe = Sqflite.firstIntValue(
|
||||
final existe =
|
||||
Sqflite.firstIntValue(
|
||||
await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM grupos_favoritos WHERE id = ?',
|
||||
[grupoId],
|
||||
@@ -250,7 +252,8 @@ class ServicioFavoritos {
|
||||
|
||||
Future<void> agregar(Emisora emisora) async {
|
||||
final db = await _database;
|
||||
final maxOrden = Sqflite.firstIntValue(
|
||||
final maxOrden =
|
||||
Sqflite.firstIntValue(
|
||||
await db.rawQuery('SELECT MAX(orden) FROM favoritos'),
|
||||
) ??
|
||||
-1;
|
||||
@@ -273,10 +276,9 @@ class ServicioFavoritos {
|
||||
Future<bool> esFavorito(String uuid) async {
|
||||
final db = await _database;
|
||||
final count = Sqflite.firstIntValue(
|
||||
await db.rawQuery(
|
||||
'SELECT COUNT(*) FROM favoritos WHERE uuid = ?',
|
||||
[uuid],
|
||||
),
|
||||
await db.rawQuery('SELECT COUNT(*) FROM favoritos WHERE uuid = ?', [
|
||||
uuid,
|
||||
]),
|
||||
);
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ class ServicioTimer {
|
||||
_controller.add(_tiempoRestante);
|
||||
}
|
||||
|
||||
|
||||
// if (restante <= Duration.zero) {
|
||||
// _tiempoRestante = Duration.zero;
|
||||
// _controller.add(_tiempoRestante);
|
||||
@@ -104,7 +103,9 @@ class ServicioTimer {
|
||||
if (paso >= pasos) {
|
||||
_fadeTicker?.cancel();
|
||||
await _audio.detener();
|
||||
await _audio.setVolumen(volumenInicial); // restaurar volumen para próxima vez
|
||||
await _audio.setVolumen(
|
||||
volumenInicial,
|
||||
); // restaurar volumen para próxima vez
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@ class PluriWaveMotion extends ThemeExtension<PluriWaveMotion> {
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<PluriWaveMotion> lerp(covariant ThemeExtension<PluriWaveMotion>? other, double t) {
|
||||
ThemeExtension<PluriWaveMotion> lerp(
|
||||
covariant ThemeExtension<PluriWaveMotion>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! PluriWaveMotion) return this;
|
||||
return t < 0.5 ? this : other;
|
||||
}
|
||||
|
||||
@@ -22,11 +22,10 @@ abstract final class PluriWaveTheme {
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
scaffoldBackgroundColor: tokens.deepViolet,
|
||||
textTheme: GoogleFonts.plusJakartaSansTextTheme(ThemeData.dark().textTheme),
|
||||
extensions: const <ThemeExtension<dynamic>>[
|
||||
tokens,
|
||||
PluriWaveMotion.dark,
|
||||
],
|
||||
textTheme: GoogleFonts.plusJakartaSansTextTheme(
|
||||
ThemeData.dark().textTheme,
|
||||
),
|
||||
extensions: const <ThemeExtension<dynamic>>[tokens, PluriWaveMotion.dark],
|
||||
appBarTheme: const AppBarTheme(
|
||||
centerTitle: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -39,7 +38,10 @@ abstract final class PluriWaveTheme {
|
||||
indicatorColor: tokens.electricMagenta.withValues(alpha: 0.18),
|
||||
labelTextStyle: WidgetStateProperty.resolveWith(
|
||||
(states) => TextStyle(
|
||||
fontWeight: states.contains(WidgetState.selected) ? FontWeight.w800 : FontWeight.w600,
|
||||
fontWeight:
|
||||
states.contains(WidgetState.selected)
|
||||
? FontWeight.w800
|
||||
: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
@@ -62,7 +64,9 @@ abstract final class PluriWaveTheme {
|
||||
}
|
||||
|
||||
extension PluriWaveThemeContextX on BuildContext {
|
||||
PluriWaveTokens get pluriTokens => Theme.of(this).extension<PluriWaveTokens>() ?? PluriWaveTokens.dark;
|
||||
PluriWaveTokens get pluriTokens =>
|
||||
Theme.of(this).extension<PluriWaveTokens>() ?? PluriWaveTokens.dark;
|
||||
|
||||
PluriWaveMotion get pluriMotion => Theme.of(this).extension<PluriWaveMotion>() ?? PluriWaveMotion.dark;
|
||||
PluriWaveMotion get pluriMotion =>
|
||||
Theme.of(this).extension<PluriWaveMotion>() ?? PluriWaveMotion.dark;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
@@ -9,7 +9,11 @@ class EcualizadorWidget extends StatefulWidget {
|
||||
final PresetEcualizador preset;
|
||||
final void Function(PresetEcualizador) onCambio;
|
||||
|
||||
const EcualizadorWidget({super.key, required this.preset, required this.onCambio});
|
||||
const EcualizadorWidget({
|
||||
super.key,
|
||||
required this.preset,
|
||||
required this.onCambio,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EcualizadorWidget> createState() => _EcualizadorWidgetState();
|
||||
@@ -35,7 +39,9 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
||||
|
||||
void _actualizarBanda(int index, double valor) {
|
||||
setState(() => _bandas[index] = valor);
|
||||
widget.onCambio(PresetEcualizador(nombre: 'Personalizado', bandas: List.from(_bandas)));
|
||||
widget.onCambio(
|
||||
PresetEcualizador(nombre: 'Personalizado', bandas: List.from(_bandas)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -52,11 +58,20 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(l10n.equalizerTitle, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700)),
|
||||
Text(
|
||||
l10n.equalizerTitle,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Chip(
|
||||
label: Text(_nombrePreset(l10n, widget.preset.nombre), style: theme.textTheme.labelMedium),
|
||||
backgroundColor: theme.colorScheme.secondaryContainer.withValues(alpha: 0.75),
|
||||
label: Text(
|
||||
_nombrePreset(l10n, widget.preset.nombre),
|
||||
style: theme.textTheme.labelMedium,
|
||||
),
|
||||
backgroundColor: theme.colorScheme.secondaryContainer
|
||||
.withValues(alpha: 0.75),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -68,11 +83,18 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
||||
for (int i = 0; i < 5; i++)
|
||||
Expanded(
|
||||
child: Card(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.35,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 4,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -80,7 +102,9 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
||||
child: Semantics(
|
||||
slider: true,
|
||||
label: l10n.equalizerBandLabel(_etiquetas[i]),
|
||||
value: l10n.equalizerBandValue(_bandas[i].toStringAsFixed(1)),
|
||||
value: l10n.equalizerBandValue(
|
||||
_bandas[i].toStringAsFixed(1),
|
||||
),
|
||||
child: RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Slider(
|
||||
@@ -93,10 +117,15 @@ class _EcualizadorWidgetState extends State<EcualizadorWidget> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Text('${_bandas[i].toStringAsFixed(1)}dB', style: theme.textTheme.labelSmall),
|
||||
Text(
|
||||
'${_bandas[i].toStringAsFixed(1)}dB',
|
||||
style: theme.textTheme.labelSmall,
|
||||
),
|
||||
Text(
|
||||
_etiquetas[i],
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
@@ -142,17 +171,19 @@ class PresetsEcualizadorWidget extends StatelessWidget {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 6,
|
||||
children: PresetEcualizador.presets.map((p) {
|
||||
final selected = p.nombre == presetActual.nombre;
|
||||
return ChoiceChip(
|
||||
label: Text(_nombrePreset(l10n, p.nombre)),
|
||||
selected: selected,
|
||||
showCheckmark: false,
|
||||
selectedColor: theme.colorScheme.primaryContainer,
|
||||
backgroundColor: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.32),
|
||||
onSelected: (_) => onSeleccionar(p),
|
||||
);
|
||||
}).toList(),
|
||||
children:
|
||||
PresetEcualizador.presets.map((p) {
|
||||
final selected = p.nombre == presetActual.nombre;
|
||||
return ChoiceChip(
|
||||
label: Text(_nombrePreset(l10n, p.nombre)),
|
||||
selected: selected,
|
||||
showCheckmark: false,
|
||||
selectedColor: theme.colorScheme.primaryContainer,
|
||||
backgroundColor: theme.colorScheme.surfaceContainerHighest
|
||||
.withValues(alpha: 0.32),
|
||||
onSelected: (_) => onSeleccionar(p),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import 'pluri_glass_surface.dart';
|
||||
@@ -72,29 +72,32 @@ class _PluriNavButton extends StatelessWidget {
|
||||
margin: EdgeInsets.symmetric(horizontal: selected ? 3 : 2),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
gradient: selected
|
||||
? LinearGradient(
|
||||
colors: [
|
||||
t.electricMagenta.withValues(alpha: 0.32),
|
||||
t.warmCoral.withValues(alpha: 0.18),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
gradient:
|
||||
selected
|
||||
? LinearGradient(
|
||||
colors: [
|
||||
t.electricMagenta.withValues(alpha: 0.32),
|
||||
t.warmCoral.withValues(alpha: 0.18),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? Colors.white.withValues(alpha: 0.22)
|
||||
: Colors.white.withValues(alpha: 0.06),
|
||||
color:
|
||||
selected
|
||||
? Colors.white.withValues(alpha: 0.22)
|
||||
: Colors.white.withValues(alpha: 0.06),
|
||||
),
|
||||
boxShadow: selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: t.glowColor.withValues(alpha: 0.36),
|
||||
blurRadius: 24,
|
||||
spreadRadius: -6,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
boxShadow:
|
||||
selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: t.glowColor.withValues(alpha: 0.36),
|
||||
blurRadius: 24,
|
||||
spreadRadius: -6,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
]
|
||||
: const [],
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
@@ -113,30 +116,34 @@ class _PluriNavButton extends StatelessWidget {
|
||||
curve: Curves.easeOutBack,
|
||||
child: PluriIcon(
|
||||
glyph: item.glyph,
|
||||
variant: selected
|
||||
? PluriIconVariant.activeGlow
|
||||
: PluriIconVariant.filled,
|
||||
variant:
|
||||
selected
|
||||
? PluriIconVariant.activeGlow
|
||||
: PluriIconVariant.filled,
|
||||
size: selected ? 42 : 34,
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: context.pluriMotion.quick,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: selected
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: foreground,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
child:
|
||||
selected
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
item.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(
|
||||
color: foreground,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: -0.2,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -63,7 +63,8 @@ class PluriIcon extends StatelessWidget {
|
||||
: icon;
|
||||
|
||||
return Semantics(
|
||||
label: semanticLabel ?? _fallbackLabel(AppLocalizations.of(context), glyph),
|
||||
label:
|
||||
semanticLabel ?? _fallbackLabel(AppLocalizations.of(context), glyph),
|
||||
image: true,
|
||||
child: ExcludeSemantics(child: child),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user