Initial commit
This commit is contained in:
92
lib/state/connection_state_bloc.dart
Normal file
92
lib/state/connection_state_bloc.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' show log;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../api/commands.dart';
|
||||
import '../api/events.dart';
|
||||
import 'player_state_bloc.dart';
|
||||
|
||||
class ConnectionStateBloc extends Bloc<ConnectionEvent, WebSocketChannel?> {
|
||||
final PlayerStateBloc playerStateBloc;
|
||||
|
||||
String? _uri;
|
||||
WebSocketChannel? _channel;
|
||||
|
||||
ConnectionStateBloc(this.playerStateBloc) : super(null) {
|
||||
|
||||
on<Connect>((event, emit) {
|
||||
if (_channel != null && _uri == event.uri) {
|
||||
log('Already connected to ${event.uri}');
|
||||
return;
|
||||
} else if (_channel != null) {
|
||||
// Clear connection, and reconnect
|
||||
state?.sink.close();
|
||||
playerStateBloc.add(const ClearPlayerState());
|
||||
emit(null);
|
||||
}
|
||||
|
||||
_uri = event.uri;
|
||||
|
||||
_channel = WebSocketChannel.connect(
|
||||
Uri.parse(event.uri),
|
||||
);
|
||||
|
||||
_channel!.stream.listen(
|
||||
(event) {
|
||||
final jsonData = jsonDecode(event as String);
|
||||
if (jsonData is Map) {
|
||||
switch (jsonData['type']) {
|
||||
case 'initial_state':
|
||||
playerStateBloc.add(
|
||||
InitialPlayerState.fromJson(jsonData['value']),
|
||||
);
|
||||
break;
|
||||
case 'event':
|
||||
final event = parseEvent(jsonData['value']);
|
||||
if (event == null) {
|
||||
log('Unknown event: ${jsonData['value']}');
|
||||
} else {
|
||||
log('Handling event: $event');
|
||||
playerStateBloc.add(event);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log('Unknown message type: ${jsonData['type']}');
|
||||
log('Message: $jsonData');
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
log('Error: $error');
|
||||
},
|
||||
onDone: () {
|
||||
add(Disconnect());
|
||||
log('Connection closed, reconnecting...');
|
||||
add(Connect(_uri!));
|
||||
},
|
||||
);
|
||||
|
||||
emit(_channel);
|
||||
});
|
||||
|
||||
on<Disconnect>((event, emit) {
|
||||
_uri = null;
|
||||
state?.sink.close(0, 'Disconnecting');
|
||||
playerStateBloc.add(const ClearPlayerState());
|
||||
emit(null);
|
||||
});
|
||||
|
||||
on<Command>((event, emit) {
|
||||
if (_channel == null) {
|
||||
log('Cannot send command when not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
_channel!.sink.add(event.toJsonString());
|
||||
});
|
||||
}
|
||||
}
|
||||
144
lib/state/player_state.dart
Normal file
144
lib/state/player_state.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class PlayerState {
|
||||
final List<Chapter> chapters;
|
||||
final List<SubtitleTrack> subtitleTracks;
|
||||
final Playlist playlist;
|
||||
final String currentTrack;
|
||||
final bool isLooping;
|
||||
final bool isMuted;
|
||||
final bool isPlaying;
|
||||
final bool isPausedForCache;
|
||||
final double? cachedTimestamp;
|
||||
final Duration duration;
|
||||
final double volume;
|
||||
final double? currentPercentPosition;
|
||||
|
||||
const PlayerState({
|
||||
required this.cachedTimestamp,
|
||||
required this.chapters,
|
||||
required this.currentPercentPosition,
|
||||
required this.currentTrack,
|
||||
required this.duration,
|
||||
required this.isLooping,
|
||||
required this.isMuted,
|
||||
required this.isPlaying,
|
||||
required this.isPausedForCache,
|
||||
required this.playlist,
|
||||
required this.subtitleTracks,
|
||||
required this.volume,
|
||||
});
|
||||
|
||||
factory PlayerState.fromJson(Map<String, dynamic> json) {
|
||||
return PlayerState(
|
||||
cachedTimestamp: json['cached_timestamp'],
|
||||
chapters: (json['chapters'] as List).map((e) => Chapter.fromJson(e)).toList(),
|
||||
currentPercentPosition: json['current_percent_pos'],
|
||||
currentTrack: json['current_track'],
|
||||
duration: Duration(milliseconds: (json['duration'] * 1000).round()),
|
||||
isLooping: json['is_looping'],
|
||||
isMuted: json['is_muted'],
|
||||
isPlaying: json['is_playing'],
|
||||
isPausedForCache: json['is_paused_for_cache'],
|
||||
playlist: (json['playlist'] as List).map((e) => PlaylistItem.fromJson(e)).toList(),
|
||||
subtitleTracks: (json['tracks'] as List).map((e) => SubtitleTrack.fromJson(e)).toList(),
|
||||
volume: json['volume'],
|
||||
);
|
||||
}
|
||||
|
||||
PlayerState copyWith({
|
||||
List<Chapter>? chapters,
|
||||
List<SubtitleTrack>? subtitleTracks,
|
||||
Playlist? playlist,
|
||||
String? currentTrack,
|
||||
bool? isLooping,
|
||||
bool? isMuted,
|
||||
bool? isPlaying,
|
||||
bool? isPausedForCache,
|
||||
double? cachedTimestamp,
|
||||
double? currentPercentPosition,
|
||||
Duration? duration,
|
||||
double? volume,
|
||||
}) {
|
||||
return PlayerState(
|
||||
cachedTimestamp: cachedTimestamp ?? this.cachedTimestamp,
|
||||
chapters: chapters ?? this.chapters,
|
||||
currentPercentPosition: currentPercentPosition ?? this.currentPercentPosition,
|
||||
currentTrack: currentTrack ?? this.currentTrack,
|
||||
duration: duration ?? this.duration,
|
||||
isLooping: isLooping ?? this.isLooping,
|
||||
isMuted: isMuted ?? this.isMuted,
|
||||
isPlaying: isPlaying ?? this.isPlaying,
|
||||
isPausedForCache: isPausedForCache ?? this.isPausedForCache,
|
||||
playlist: playlist ?? this.playlist,
|
||||
subtitleTracks: subtitleTracks ?? this.subtitleTracks,
|
||||
volume: volume ?? this.volume,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef Playlist = List<PlaylistItem>;
|
||||
|
||||
@immutable
|
||||
class PlaylistItem {
|
||||
final bool current;
|
||||
final String filename;
|
||||
final int id;
|
||||
final String? title;
|
||||
|
||||
const PlaylistItem({
|
||||
required this.current,
|
||||
required this.filename,
|
||||
required this.id,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
factory PlaylistItem.fromJson(Map<String, dynamic> json) {
|
||||
return PlaylistItem(
|
||||
current: json['current'] ?? false,
|
||||
filename: json['filename'],
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class Chapter {
|
||||
final String title;
|
||||
final double time;
|
||||
|
||||
const Chapter({
|
||||
required this.title,
|
||||
required this.time,
|
||||
});
|
||||
|
||||
factory Chapter.fromJson(Map<String, dynamic> json) {
|
||||
return Chapter(
|
||||
title: json['title'],
|
||||
time: json['time'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class SubtitleTrack {
|
||||
final int id;
|
||||
final String title;
|
||||
final String? lang;
|
||||
|
||||
const SubtitleTrack({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.lang,
|
||||
});
|
||||
|
||||
factory SubtitleTrack.fromJson(Map<String, dynamic> json) {
|
||||
return SubtitleTrack(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
lang: json['lang'],
|
||||
);
|
||||
}
|
||||
}
|
||||
78
lib/state/player_state_bloc.dart
Normal file
78
lib/state/player_state_bloc.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'dart:developer' show log;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'player_state.dart';
|
||||
import '../api/events.dart';
|
||||
|
||||
class PlayerStateBloc extends Bloc<Event, PlayerState?> {
|
||||
PlayerStateBloc() : super(null) {
|
||||
on<InitialPlayerState>((event, emit) {
|
||||
emit(event.playerState);
|
||||
});
|
||||
|
||||
on <ClearPlayerState>((event, emit) {
|
||||
emit(null);
|
||||
});
|
||||
|
||||
on<PropertyChangedEvent>((event, emit) {
|
||||
// print('Received event: $event');
|
||||
if (state == null) {
|
||||
log('Received event before initial state');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
case PlaylistChange playlistChange:
|
||||
final newState = state!.copyWith(playlist: playlistChange.playlist);
|
||||
emit(newState);
|
||||
break;
|
||||
case LoopPlaylistChange loopPlaylistChange:
|
||||
final newState =
|
||||
state!.copyWith(isLooping: loopPlaylistChange.isLooping);
|
||||
emit(newState);
|
||||
break;
|
||||
case PercentPositionChange percentPositionChange:
|
||||
final newState = state!.copyWith(
|
||||
currentPercentPosition:
|
||||
percentPositionChange.currentPercentPosition,
|
||||
);
|
||||
emit(newState);
|
||||
break;
|
||||
case VolumeChange volumeChange:
|
||||
final newState = state!.copyWith(volume: volumeChange.volume);
|
||||
emit(newState);
|
||||
break;
|
||||
case DurationChange durationChange:
|
||||
final newState = state!.copyWith(duration: durationChange.duration);
|
||||
emit(newState);
|
||||
break;
|
||||
case PauseChange pauseChange:
|
||||
final newState = state!.copyWith(isPlaying: !pauseChange.isPaused);
|
||||
emit(newState);
|
||||
break;
|
||||
case MuteChange muteChange:
|
||||
final newState = state!.copyWith(isMuted: muteChange.isMuted);
|
||||
emit(newState);
|
||||
break;
|
||||
case TrackListChange trackListChange:
|
||||
final newState =
|
||||
state!.copyWith(subtitleTracks: trackListChange.tracks);
|
||||
emit(newState);
|
||||
break;
|
||||
case DemuxerCacheStateChange demuxerCacheStateChange:
|
||||
final newState = state!.copyWith(
|
||||
cachedTimestamp: demuxerCacheStateChange.cachedTimestamp,
|
||||
);
|
||||
emit(newState);
|
||||
break;
|
||||
case PausedForCacheChange pausedForCacheChange:
|
||||
final newState = state!.copyWith(
|
||||
isPausedForCache: pausedForCacheChange.isPausedForCache,
|
||||
);
|
||||
emit(newState);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user