chore: bring flutter client, docs, deploy and tools into monorepo

This commit is contained in:
2026-07-05 03:22:24 +02:00
parent fcc81e2a74
commit 7a931d2d6f
189 changed files with 19124 additions and 0 deletions
@@ -0,0 +1,124 @@
import 'dart:async';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../tracks/domain/track.dart';
import '../data/stream_repository.dart';
final audioPlayerProvider = Provider<AudioPlayer>((ref) {
final player = AudioPlayer();
ref.onDispose(player.dispose);
return player;
});
class PlaybackState {
const PlaybackState({
this.track,
this.playing = false,
this.loading = false,
this.position = Duration.zero,
this.duration = Duration.zero,
this.error,
});
final Track? track;
final bool playing;
final bool loading;
final Duration position;
final Duration duration;
final String? error;
bool get hasContent => track != null;
PlaybackState copyWith({
Track? track,
bool? playing,
bool? loading,
Duration? position,
Duration? duration,
Object? error = _sentinel,
}) {
return PlaybackState(
track: track ?? this.track,
playing: playing ?? this.playing,
loading: loading ?? this.loading,
position: position ?? this.position,
duration: duration ?? this.duration,
error: identical(error, _sentinel) ? this.error : error as String?,
);
}
static const _sentinel = Object();
}
class PlayerController extends Notifier<PlaybackState> {
AudioPlayer get _player => ref.read(audioPlayerProvider);
@override
PlaybackState build() {
final player = ref.read(audioPlayerProvider);
final subscriptions = <StreamSubscription<dynamic>>[
player.onPlayerStateChanged.listen((value) {
final playing = value == PlayerState.playing;
state = state.copyWith(
playing: playing,
loading: playing ? false : state.loading,
);
}),
player.onPositionChanged.listen((value) {
state = state.copyWith(position: value);
}),
player.onDurationChanged.listen((value) {
state = state.copyWith(duration: value, loading: false);
}),
player.onPlayerComplete.listen((_) {
state = state.copyWith(playing: false, position: Duration.zero);
}),
];
ref.onDispose(() {
for (final subscription in subscriptions) {
subscription.cancel();
}
});
return const PlaybackState();
}
Future<void> play(Track track) async {
state = PlaybackState(track: track, loading: true);
try {
final stream = await ref.read(streamRepositoryProvider).getStream(track.id);
await _player.play(UrlSource(_withTicket(stream.manifestUrl, stream.ticket)));
} catch (error) {
state = state.copyWith(loading: false, error: _describe(error));
}
}
Future<void> togglePlay() {
return _player.state == PlayerState.playing
? _player.pause()
: _player.resume();
}
Future<void> seek(Duration position) => _player.seek(position);
Future<void> stop() async {
await _player.stop();
state = const PlaybackState();
}
String _withTicket(String url, String ticket) {
final separator = url.contains('?') ? '&' : '?';
return '$url${separator}t=${Uri.encodeComponent(ticket)}';
}
String _describe(Object error) {
if (error.toString().contains('401')) {
return 'Inicia sesión para reproducir.';
}
return 'No se pudo reproducir la pista.';
}
}
final playerControllerProvider =
NotifierProvider<PlayerController, PlaybackState>(PlayerController.new);