spotube/lib/provider/connect/connect.dart
2025-04-27 22:39:16 +06:00

230 lines
6.3 KiB
Dart

import 'dart:convert';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/connect/connect.dart';
import 'package:spotube/provider/connect/clients.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
final playingProvider = StateProvider<bool>(
(ref) => false,
);
final positionProvider = StateProvider<Duration>(
(ref) => Duration.zero,
);
final durationProvider = StateProvider<Duration>(
(ref) => Duration.zero,
);
final shuffleProvider = StateProvider<bool>(
(ref) => false,
);
final loopModeProvider = StateProvider<PlaylistMode>(
(ref) => PlaylistMode.none,
);
final queueProvider = StateProvider<AudioPlayerState>(
(ref) => AudioPlayerState(
playing: audioPlayer.isPlaying,
loopMode: audioPlayer.loopMode,
shuffled: audioPlayer.isShuffled,
playlist: audioPlayer.playlist,
collections: [],
),
);
final volumeProvider = StateProvider<double>(
(ref) => 1.0,
);
typedef ConnectState = ({WebSocketChannel channel, Stream stream});
class ConnectNotifier extends AsyncNotifier<ConnectState?> {
@override
build() async {
try {
final connectClients = await ref.watch(connectClientsProvider.future);
if (connectClients.resolvedService == null) return null;
final service = connectClients.resolvedService!;
AppLogger.log.t(
'♾️ Connecting to ${service.name}: ws://${service.host}:${service.port}/ws',
);
final channel = WebSocketChannel.connect(
Uri.parse('ws://${service.host}:${service.port}/ws'),
);
await channel.ready;
AppLogger.log.t(
'✅ Connected to ${service.name}: ws://${service.host}:${service.port}/ws',
);
final stream = channel.stream.asBroadcastStream();
final subscription = stream.listen(
(message) {
final event =
WebSocketEvent.fromJson(jsonDecode(message), (data) => data);
event.onQueue((event) {
ref.read(queueProvider.notifier).state = event.data;
});
event.onPlaying((event) {
ref.read(playingProvider.notifier).state = event.data;
});
event.onPosition((event) {
ref.read(positionProvider.notifier).state = event.data;
});
event.onDuration((event) {
ref.read(durationProvider.notifier).state = event.data;
});
event.onShuffle((event) {
ref.read(shuffleProvider.notifier).state = event.data;
});
event.onLoop((event) {
ref.read(loopModeProvider.notifier).state = event.data;
});
event.onVolume((event) {
ref.read(volumeProvider.notifier).state = event.data;
});
event.onError((event) {
if (event.data == "Connection denied") {
ref.read(connectClientsProvider.notifier).clearResolvedService();
if (rootNavigatorKey.currentContext?.mounted == true) {
final theme = Theme.of(rootNavigatorKey.currentContext!);
showToast(
context: rootNavigatorKey.currentContext!,
location: ToastLocation.topRight,
dismissible: true,
builder: (context, overlay) {
return SurfaceCard(
fillColor: theme.colorScheme.destructive,
filled: true,
child: Basic(
leading: const Icon(SpotubeIcons.error),
title: Text(
context.l10n.connection_request_denied,
style: theme.typography.normal.copyWith(
color: theme.colorScheme.destructiveForeground,
),
),
leadingAlignment: Alignment.center,
),
);
},
);
}
}
});
},
onError: (error) {
AppLogger.reportError(error, StackTrace.current);
},
);
ref.onDispose(() {
subscription.cancel();
channel.sink.close(status.goingAway);
});
return (channel: channel, stream: stream);
} catch (e, stack) {
AppLogger.reportError(e, stack);
rethrow;
}
}
Future<void> emit(Object message) async {
if (state.value == null) return;
state.value?.channel.sink.add(
message is String ? message : (message as dynamic).toJson(),
);
}
Future<void> resume() async {
emit(WebSocketResumeEvent());
}
Future<void> pause() async {
emit(WebSocketPauseEvent());
}
Future<void> stop() async {
emit(WebSocketStopEvent());
}
Future<void> jumpTo(int position) async {
emit(WebSocketJumpEvent(position));
}
Future<void> load(WebSocketLoadEventData data) async {
emit(WebSocketLoadEvent(data));
}
Future<void> next() async {
emit(WebSocketNextEvent());
}
Future<void> previous() async {
emit(WebSocketPreviousEvent());
}
Future<void> seek(Duration position) async {
emit(WebSocketSeekEvent(position));
}
Future<void> setShuffle(bool value) async {
emit(WebSocketShuffleEvent(value));
}
Future<void> setLoopMode(PlaylistMode value) async {
emit(WebSocketLoopEvent(value));
}
Future<void> addTrack(Track data) async {
emit(WebSocketAddTrackEvent(data));
}
Future<void> removeTrack(String data) async {
emit(WebSocketRemoveTrackEvent(data));
}
Future<void> reorder(ReorderData data) async {
emit(WebSocketReorderEvent(data));
}
Future<void> setVolume(double value) async {
emit(WebSocketVolumeEvent(value));
}
}
final connectProvider = AsyncNotifierProvider<ConnectNotifier, ConnectState?>(
() => ConnectNotifier(),
);