Phase 1: pick a device folder via SAF (persisted grant, no new permission), browse its nested subfolders/tracks as a 5th Android Auto root folder (hidden until configured), and play tracks through the existing pipeline (EQ, art rotation, cold-start-safe source). No metadata/sort/filter/shuffle yet -- filename is the title, generic rotating art is the placeholder; deferred to a follow-up phase. Adds a new pluriwave/file_actions native method (listAudioChildren) and an onActivityResult override in MainActivity for the SAF folder picker -- both static-review-only, no Android build available here.
130 lines
4.3 KiB
Dart
130 lines
4.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'app.dart';
|
|
import 'servicios/musica_local_auto.dart';
|
|
import 'servicios/navegacion_auto.dart';
|
|
import 'servicios/servicio_audio.dart';
|
|
import 'servicios/servicio_audio_session.dart';
|
|
import 'tema/pluriwave_tokens.dart';
|
|
|
|
const _anchoMinimoLandscape = 600.0;
|
|
|
|
/// Branded monochrome status-bar icon, replacing the default full-color
|
|
/// launcher silhouette fallback.
|
|
const androidNotificationIconResource = 'drawable/ic_stat_pluriwave';
|
|
|
|
/// S5-R8: media notification accent uses the brand color, not the M3
|
|
/// default purple. Top-level const so tests can assert it.
|
|
const configuracionAudioService = AudioServiceConfig(
|
|
androidNotificationChannelId: 'es.freetimelab.pluriwave.audio',
|
|
androidNotificationChannelName: 'PluriWave Radio',
|
|
androidNotificationOngoing: true,
|
|
androidStopForegroundOnPause: true,
|
|
notificationColor: PluriWaveTokens.brand,
|
|
androidNotificationIcon: androidNotificationIconResource,
|
|
);
|
|
|
|
Future<void> main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await _aplicarPoliticaOrientacion();
|
|
|
|
// S3-R4: single SharedPreferences instance resolved once at startup and
|
|
// injected into every state/service below.
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
final handler = await AudioService.init(
|
|
builder: () => PluriWaveAudioHandler(),
|
|
config: configuracionAudioService,
|
|
);
|
|
registrarHandler(handler);
|
|
|
|
// Android Auto browse source (Design "getChildren data source, cold-start
|
|
// safe") — registered before EstadoRadio builds so a headless Auto bind
|
|
// (main() runs but the lazily-created Provider tree may never build) can
|
|
// still serve favourites/custom stations from local reads.
|
|
final fuenteAuto = FuenteEmisorasAutoLocal();
|
|
registrarFuenteNavegacion(fuenteAuto);
|
|
|
|
// Local-music browse source (Design "getChildren data source
|
|
// registration"), same injectable-prefs DI convention as every other
|
|
// startup service — required so `_fuenteMusicaLocalGlobal` is ever
|
|
// non-null; without this registration the local-music root would stay
|
|
// permanently hidden (`hayCarpetaConfigurada()` has no fuente to ask).
|
|
registrarFuenteMusicaLocal(FuenteMusicaLocalAutoImpl(prefs: prefs));
|
|
|
|
// S3-R1: audio focus — phone calls / transient losses pause or duck the
|
|
// radio; headphones unplugged pauses it.
|
|
final sesionAudio = ServicioAudioSession(objetivo: handler);
|
|
unawaited(sesionAudio.configurar());
|
|
|
|
runApp(
|
|
_OrientacionResponsiveApp(
|
|
child: PluriWaveApp(prefs: prefs, fuenteAuto: fuenteAuto),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _aplicarPoliticaOrientacion([ui.Display? display]) async {
|
|
final vista =
|
|
WidgetsBinding.instance.platformDispatcher.views.isNotEmpty
|
|
? WidgetsBinding.instance.platformDispatcher.views.first
|
|
: null;
|
|
final displayActivo = display ?? vista?.display;
|
|
if (displayActivo == null) return;
|
|
|
|
final anchoLogico = displayActivo.size.width / displayActivo.devicePixelRatio;
|
|
if (anchoLogico < _anchoMinimoLandscape) {
|
|
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
|
return;
|
|
}
|
|
|
|
await SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
|
}
|
|
|
|
class _OrientacionResponsiveApp extends StatefulWidget {
|
|
const _OrientacionResponsiveApp({required this.child});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
State<_OrientacionResponsiveApp> createState() =>
|
|
_OrientacionResponsiveAppState();
|
|
}
|
|
|
|
class _OrientacionResponsiveAppState extends State<_OrientacionResponsiveApp>
|
|
with WidgetsBindingObserver {
|
|
ui.Display? _display;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
_display = View.maybeOf(context)?.display;
|
|
unawaited(_aplicarPoliticaOrientacion(_display));
|
|
}
|
|
|
|
@override
|
|
void didChangeMetrics() {
|
|
unawaited(_aplicarPoliticaOrientacion(_display));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => widget.child;
|
|
}
|