From c399baa5abf8ea1694f81d1eefeab286e0dab7d9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 24 Mar 2024 17:03:41 +0600 Subject: [PATCH] feat: add connect server support --- CONTRIBUTION.md | 8 +- ios/Podfile | 2 +- ios/Runner/Info.plist | 6 + lib/collections/spotube_icons.dart | 1 + lib/l10n/app_en.arb | 4 +- lib/main.dart | 3 + lib/models/connect/connect.dart | 15 ++ lib/models/connect/connect.freezed.dart | 216 ++++++++++++++++++ lib/models/connect/connect.g.dart | 25 ++ lib/models/connect/load.dart | 27 +++ lib/models/connect/ws_event.dart | 191 ++++++++++++++++ lib/pages/settings/sections/playback.dart | 7 + lib/provider/connect/server.dart | 152 ++++++++++++ .../proxy_playlist/proxy_playlist.dart | 14 +- .../user_preferences_provider.dart | 4 + .../user_preferences_state.dart | 1 + .../user_preferences_state.freezed.dart | 37 ++- .../user_preferences_state.g.dart | 2 + linux/packaging/deb/make_config.yaml | 5 + linux/packaging/rpm/make_config.yaml | 3 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile | 2 +- macos/Podfile.lock | 9 +- macos/Runner.xcodeproj/project.pbxproj | 6 +- pubspec.lock | 78 ++++++- pubspec.yaml | 5 + untranslated_messages.json | 99 +++++++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 29 files changed, 900 insertions(+), 28 deletions(-) create mode 100644 lib/models/connect/connect.dart create mode 100644 lib/models/connect/connect.freezed.dart create mode 100644 lib/models/connect/connect.g.dart create mode 100644 lib/models/connect/load.dart create mode 100644 lib/models/connect/ws_event.dart create mode 100644 lib/provider/connect/server.dart diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 13996cea..e859f9e6 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -25,7 +25,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents - [Before Submitting an Enhancement](#before-submitting-an-enhancement) - [How Do I Submit a Good Enhancement Suggestion?](#how-do-i-submit-a-good-enhancement-suggestion) - [Your First Code Contribution](#your-first-code-contribution) - - [Submit translations](#submit-translations) + - [Submit Translations](#submit-translations) ## Code of Conduct @@ -123,16 +123,16 @@ Do the following: - Install Development dependencies in linux - Debian (>=12/Bookworm)/Ubuntu ```bash - $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev + $ apt-get install mpv libmpv-dev libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp25 libsecret-1-dev libjsoncpp-dev libnotify-bin libnotify-dev avahi-daemon avahi-discover avahi-utils libnss-mdns mdns-scan ``` - Use `libjsoncpp1` instead of `libjsoncpp25` (for Ubuntu < 22.04) - Arch/Manjaro ```bash - yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify + yay -S mpv libappindicator-gtk3 libsecret jsoncpp libnotify avahi nss-mdns mdns-scan ``` - Fedora ```bash - dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel + dnf install mpv mpv-devel libappindicator-gtk3 libappindicator-gtk3-devel libsecret libsecret-devel jsoncpp jsoncpp-devel libnotify libnotify-devel avahi mdns-scan nss-mdns ``` - Clone the Repo - Create a `.env` in root of the project following the `.env.example` template diff --git a/ios/Podfile b/ios/Podfile index bc3dcaa6..7235f482 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8e103cfa..ffd511a4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -66,5 +66,11 @@ UIViewControllerBasedStatusBarAppearance + NSLocalNetworkUsageDescription + To allow other devices on the network control playback of Spotube securely. + NSBonjourServices + + _spotube._tcp + \ No newline at end of file diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 98c8ad45..8d497388 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -116,4 +116,5 @@ abstract class SpotubeIcons { static const openCollective = SimpleIcons.opencollective; static const anonymous = FeatherIcons.user; static const history = FeatherIcons.clock; + static const connect = FeatherIcons.link; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8257eac9..9fdcbc1b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -313,5 +313,7 @@ "help_project_grow_description": "Spotube is an open-source project. You can help this project grow by contributing to the project, reporting bugs, or suggesting new features.", "contribute_on_github": "Contribute on GitHub", "donate_on_open_collective": "Donate on Open Collective", - "browse_anonymously": "Browse Anonymously" + "browse_anonymously": "Browse Anonymously", + "enable_connect": "Enable Connect", + "enable_connect_description": "Control Spotube from other devices" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 5c100fd3..65e21d51 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,7 @@ import 'package:spotube/l10n/l10n.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/models/source_match.dart'; +import 'package:spotube/provider/connect/server.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; @@ -180,6 +181,8 @@ class SpotubeState extends ConsumerState { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); + ref.read(connectServerProvider); + useDisableBatteryOptimizations(); useInitSysTray(ref); useDeepLinking(ref); diff --git a/lib/models/connect/connect.dart b/lib/models/connect/connect.dart new file mode 100644 index 00000000..4d6920ac --- /dev/null +++ b/lib/models/connect/connect.dart @@ -0,0 +1,15 @@ +library connect; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/extensions/track.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; + +part 'connect.freezed.dart'; +part 'connect.g.dart'; + +part 'ws_event.dart'; +part 'load.dart'; diff --git a/lib/models/connect/connect.freezed.dart b/lib/models/connect/connect.freezed.dart new file mode 100644 index 00000000..dcbd783d --- /dev/null +++ b/lib/models/connect/connect.freezed.dart @@ -0,0 +1,216 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'connect.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +WebSocketLoadEventData _$WebSocketLoadEventDataFromJson( + Map json) { + return _WebSocketLoadEventData.fromJson(json); +} + +/// @nodoc +mixin _$WebSocketLoadEventData { + @JsonKey(name: 'tracks', toJson: _tracksJson) + List get tracks => throw _privateConstructorUsedError; + String? get collectionId => throw _privateConstructorUsedError; + int? get initialIndex => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $WebSocketLoadEventDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $WebSocketLoadEventDataCopyWith<$Res> { + factory $WebSocketLoadEventDataCopyWith(WebSocketLoadEventData value, + $Res Function(WebSocketLoadEventData) then) = + _$WebSocketLoadEventDataCopyWithImpl<$Res, WebSocketLoadEventData>; + @useResult + $Res call( + {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + String? collectionId, + int? initialIndex}); +} + +/// @nodoc +class _$WebSocketLoadEventDataCopyWithImpl<$Res, + $Val extends WebSocketLoadEventData> + implements $WebSocketLoadEventDataCopyWith<$Res> { + _$WebSocketLoadEventDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tracks = null, + Object? collectionId = freezed, + Object? initialIndex = freezed, + }) { + return _then(_value.copyWith( + tracks: null == tracks + ? _value.tracks + : tracks // ignore: cast_nullable_to_non_nullable + as List, + collectionId: freezed == collectionId + ? _value.collectionId + : collectionId // ignore: cast_nullable_to_non_nullable + as String?, + initialIndex: freezed == initialIndex + ? _value.initialIndex + : initialIndex // ignore: cast_nullable_to_non_nullable + as int?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$WebSocketLoadEventDataImplCopyWith<$Res> + implements $WebSocketLoadEventDataCopyWith<$Res> { + factory _$$WebSocketLoadEventDataImplCopyWith( + _$WebSocketLoadEventDataImpl value, + $Res Function(_$WebSocketLoadEventDataImpl) then) = + __$$WebSocketLoadEventDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'tracks', toJson: _tracksJson) List tracks, + String? collectionId, + int? initialIndex}); +} + +/// @nodoc +class __$$WebSocketLoadEventDataImplCopyWithImpl<$Res> + extends _$WebSocketLoadEventDataCopyWithImpl<$Res, + _$WebSocketLoadEventDataImpl> + implements _$$WebSocketLoadEventDataImplCopyWith<$Res> { + __$$WebSocketLoadEventDataImplCopyWithImpl( + _$WebSocketLoadEventDataImpl _value, + $Res Function(_$WebSocketLoadEventDataImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tracks = null, + Object? collectionId = freezed, + Object? initialIndex = freezed, + }) { + return _then(_$WebSocketLoadEventDataImpl( + tracks: null == tracks + ? _value._tracks + : tracks // ignore: cast_nullable_to_non_nullable + as List, + collectionId: freezed == collectionId + ? _value.collectionId + : collectionId // ignore: cast_nullable_to_non_nullable + as String?, + initialIndex: freezed == initialIndex + ? _value.initialIndex + : initialIndex // ignore: cast_nullable_to_non_nullable + as int?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$WebSocketLoadEventDataImpl implements _WebSocketLoadEventData { + _$WebSocketLoadEventDataImpl( + {@JsonKey(name: 'tracks', toJson: _tracksJson) + required final List tracks, + this.collectionId, + this.initialIndex}) + : _tracks = tracks; + + factory _$WebSocketLoadEventDataImpl.fromJson(Map json) => + _$$WebSocketLoadEventDataImplFromJson(json); + + final List _tracks; + @override + @JsonKey(name: 'tracks', toJson: _tracksJson) + List get tracks { + if (_tracks is EqualUnmodifiableListView) return _tracks; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tracks); + } + + @override + final String? collectionId; + @override + final int? initialIndex; + + @override + String toString() { + return 'WebSocketLoadEventData(tracks: $tracks, collectionId: $collectionId, initialIndex: $initialIndex)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$WebSocketLoadEventDataImpl && + const DeepCollectionEquality().equals(other._tracks, _tracks) && + (identical(other.collectionId, collectionId) || + other.collectionId == collectionId) && + (identical(other.initialIndex, initialIndex) || + other.initialIndex == initialIndex)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(_tracks), collectionId, initialIndex); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl> + get copyWith => __$$WebSocketLoadEventDataImplCopyWithImpl< + _$WebSocketLoadEventDataImpl>(this, _$identity); + + @override + Map toJson() { + return _$$WebSocketLoadEventDataImplToJson( + this, + ); + } +} + +abstract class _WebSocketLoadEventData implements WebSocketLoadEventData { + factory _WebSocketLoadEventData( + {@JsonKey(name: 'tracks', toJson: _tracksJson) + required final List tracks, + final String? collectionId, + final int? initialIndex}) = _$WebSocketLoadEventDataImpl; + + factory _WebSocketLoadEventData.fromJson(Map json) = + _$WebSocketLoadEventDataImpl.fromJson; + + @override + @JsonKey(name: 'tracks', toJson: _tracksJson) + List get tracks; + @override + String? get collectionId; + @override + int? get initialIndex; + @override + @JsonKey(ignore: true) + _$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/connect/connect.g.dart b/lib/models/connect/connect.g.dart new file mode 100644 index 00000000..f636e035 --- /dev/null +++ b/lib/models/connect/connect.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'connect.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$WebSocketLoadEventDataImpl _$$WebSocketLoadEventDataImplFromJson( + Map json) => + _$WebSocketLoadEventDataImpl( + tracks: (json['tracks'] as List) + .map((e) => Track.fromJson(e as Map)) + .toList(), + collectionId: json['collectionId'] as String?, + initialIndex: json['initialIndex'] as int?, + ); + +Map _$$WebSocketLoadEventDataImplToJson( + _$WebSocketLoadEventDataImpl instance) => + { + 'tracks': _tracksJson(instance.tracks), + 'collectionId': instance.collectionId, + 'initialIndex': instance.initialIndex, + }; diff --git a/lib/models/connect/load.dart b/lib/models/connect/load.dart new file mode 100644 index 00000000..d750cddd --- /dev/null +++ b/lib/models/connect/load.dart @@ -0,0 +1,27 @@ +part of 'connect.dart'; + +List> _tracksJson(List tracks) { + return tracks.map((e) => e.toJson()).toList(); +} + +@freezed +class WebSocketLoadEventData with _$WebSocketLoadEventData { + factory WebSocketLoadEventData({ + @JsonKey(name: 'tracks', toJson: _tracksJson) required List tracks, + String? collectionId, + int? initialIndex, + }) = _WebSocketLoadEventData; + + factory WebSocketLoadEventData.fromJson(Map json) => + _$WebSocketLoadEventDataFromJson(json); +} + +class WebSocketLoadEvent extends WebSocketEvent { + WebSocketLoadEvent(WebSocketLoadEventData data) : super(WsEvent.load, data); + + factory WebSocketLoadEvent.fromJson(Map json) { + return WebSocketLoadEvent( + WebSocketLoadEventData.fromJson(json['data'] as Map), + ); + } +} diff --git a/lib/models/connect/ws_event.dart b/lib/models/connect/ws_event.dart new file mode 100644 index 00000000..7f72acb9 --- /dev/null +++ b/lib/models/connect/ws_event.dart @@ -0,0 +1,191 @@ +part of 'connect.dart'; + +enum WsEvent { + error, + queue, + position, + playing, + resume, + pause, + load, + next, + previous, + jump, + stop; + + static WsEvent fromString(String value) { + return WsEvent.values.firstWhere((e) => e.name == value); + } +} + +typedef EventCallback = FutureOr Function(T event); + +class WebSocketEvent { + final WsEvent type; + final T data; + + WebSocketEvent(this.type, this.data); + + factory WebSocketEvent.fromJson( + Map json, + T Function(dynamic) fromJson, + ) { + return WebSocketEvent( + WsEvent.fromString(json["type"]), + fromJson(json["data"]), + ); + } + + String toJson() { + return jsonEncode({ + "type": type.name, + "data": data, + }); + } + + Future onPosition( + EventCallback callback, + ) async { + if (type == WsEvent.position) { + await callback( + WebSocketPositionEvent.fromJson(data as Map)); + } + } + + Future onPlaying( + EventCallback callback, + ) async { + if (type == WsEvent.playing) { + await callback(WebSocketPlayingEvent(data as bool)); + } + } + + Future onResume( + EventCallback callback, + ) async { + if (type == WsEvent.resume) { + await callback(WebSocketResumeEvent()); + } + } + + Future onPause( + EventCallback callback, + ) async { + if (type == WsEvent.pause) { + await callback(WebSocketPauseEvent()); + } + } + + Future onStop( + EventCallback callback, + ) async { + if (type == WsEvent.stop) { + await callback(WebSocketStopEvent()); + } + } + + Future onLoad( + EventCallback callback, + ) async { + if (type == WsEvent.load) { + await callback(WebSocketLoadEvent.fromJson(data as Map)); + } + } + + Future onNext( + EventCallback callback, + ) async { + if (type == WsEvent.next) { + await callback(WebSocketNextEvent()); + } + } + + Future onPrevious( + EventCallback callback, + ) async { + if (type == WsEvent.previous) { + await callback(WebSocketPreviousEvent()); + } + } + + Future onJump( + EventCallback callback, + ) async { + if (type == WsEvent.jump) { + await callback(WebSocketJumpEvent(data as int)); + } + } + + Future onError( + EventCallback callback, + ) async { + if (type == WsEvent.error) { + await callback(WebSocketErrorEvent(data as String)); + } + } + + Future onQueue( + EventCallback callback, + ) async { + if (type == WsEvent.queue) { + await callback( + WebSocketQueueEvent.fromJson(data as Map)); + } + } +} + +class WebSocketPositionEvent extends WebSocketEvent { + WebSocketPositionEvent(Duration data) : super(WsEvent.position, data); + + WebSocketPositionEvent.fromJson(Map json) + : super(WsEvent.position, Duration(seconds: json["data"] as int)); + + @override + String toJson() { + return jsonEncode({ + "type": type.name, + "data": data.inSeconds, + }); + } +} + +class WebSocketPlayingEvent extends WebSocketEvent { + WebSocketPlayingEvent(bool data) : super(WsEvent.playing, data); +} + +class WebSocketResumeEvent extends WebSocketEvent { + WebSocketResumeEvent() : super(WsEvent.resume, null); +} + +class WebSocketPauseEvent extends WebSocketEvent { + WebSocketPauseEvent() : super(WsEvent.pause, null); +} + +class WebSocketStopEvent extends WebSocketEvent { + WebSocketStopEvent() : super(WsEvent.stop, null); +} + +class WebSocketNextEvent extends WebSocketEvent { + WebSocketNextEvent() : super(WsEvent.next, null); +} + +class WebSocketPreviousEvent extends WebSocketEvent { + WebSocketPreviousEvent() : super(WsEvent.previous, null); +} + +class WebSocketJumpEvent extends WebSocketEvent { + WebSocketJumpEvent(int data) : super(WsEvent.jump, data); +} + +class WebSocketErrorEvent extends WebSocketEvent { + WebSocketErrorEvent(String data) : super(WsEvent.error, data); +} + +class WebSocketQueueEvent extends WebSocketEvent { + WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data); + + factory WebSocketQueueEvent.fromJson(Map json) => + WebSocketQueueEvent( + ProxyPlaylist.fromJsonRaw(json["data"] as Map), + ); +} diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index b3f0d897..e023cc60 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -227,6 +227,13 @@ class SettingsPlaybackSection extends HookConsumerWidget { value: preferences.endlessPlayback, onChanged: preferencesNotifier.setEndlessPlayback, ), + SwitchListTile( + title: Text(context.l10n.enable_connect), + subtitle: Text(context.l10n.enable_connect_description), + secondary: const Icon(SpotubeIcons.connect), + value: preferences.enableConnect, + onChanged: preferencesNotifier.setEnableConnect, + ), ], ); } diff --git a/lib/provider/connect/server.dart b/lib/provider/connect/server.dart new file mode 100644 index 00000000..aa9cb15f --- /dev/null +++ b/lib/provider/connect/server.dart @@ -0,0 +1,152 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:catcher_2/catcher_2.dart'; +import 'package:shelf/shelf_io.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shelf_router/shelf_router.dart'; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:spotube/models/connect/connect.dart'; +import 'package:spotube/models/logger.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:bonsoir/bonsoir.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +final logger = getLogger('ConnectServer'); + +final connectServerProvider = FutureProvider((ref) async { + final enabled = + ref.watch(userPreferencesProvider.select((s) => s.enableConnect)); + final playbackNotifier = ref.read(ProxyPlaylistNotifier.notifier); + + if (!enabled) { + return null; + } + + final app = Router(); + + final subscriptions = []; + + final websocket = webSocketHandler( + (WebSocketChannel channel, String? protocol) { + ref.listen( + ProxyPlaylistNotifier.provider, + (previous, next) { + channel.sink.add( + WebSocketQueueEvent(next).toJson(), + ); + }, + ); + + subscriptions.addAll([ + audioPlayer.positionStream.listen( + (position) { + channel.sink.add( + WebSocketPositionEvent(position).toJson(), + ); + }, + ), + audioPlayer.playingStream.listen( + (playing) { + channel.sink.add( + WebSocketEvent(WsEvent.playing, playing).toJson(), + ); + }, + ), + channel.stream.listen( + (message) { + try { + final event = WebSocketEvent.fromJson( + jsonDecode(message), + (data) => data, + ); + + event.onLoad((event) async { + await playbackNotifier.load( + event.data.tracks, + autoPlay: true, + initialIndex: event.data.initialIndex ?? 0, + ); + + if (event.data.collectionId != null) { + playbackNotifier.addCollection(event.data.collectionId!); + } + }); + + event.onPause((event) async { + await audioPlayer.pause(); + }); + + event.onResume((event) async { + await audioPlayer.resume(); + }); + + event.onStop((event) async { + await audioPlayer.stop(); + }); + + event.onNext((event) async { + await playbackNotifier.next(); + }); + + event.onPrevious((event) async { + await playbackNotifier.previous(); + }); + + event.onJump((event) async { + await playbackNotifier.jumpTo(event.data); + }); + } catch (e, stackTrace) { + Catcher2.reportCheckedError(e, stackTrace); + channel.sink.add(WebSocketErrorEvent(e.toString()).toJson()); + } + }, + onDone: () { + logger.i('Connection closed'); + }, + ), + ]); + }, + ); + + final port = Random().nextInt(17000) + 3000; + + final server = await serve( + (request) { + if (request.url.path.startsWith('ws')) { + return websocket(request); + } + return app(request); + }, + InternetAddress.loopbackIPv4, + port, + ); + + logger.i('Server running on http://${server.address.host}:${server.port}'); + + final service = BonsoirService( + name: 'Spotube', + type: '_spotube._tcp', + port: port, + ); + + final broadcast = BonsoirBroadcast(service: service); + + await broadcast.ready; + await broadcast.start(); + + ref.onDispose(() async { + logger.i('Stopping server'); + for (final subscription in subscriptions) { + await subscription.cancel(); + } + await broadcast.stop(); + await server.close(); + }); + + return app; +}); diff --git a/lib/provider/proxy_playlist/proxy_playlist.dart b/lib/provider/proxy_playlist/proxy_playlist.dart index 026b3403..efc818ed 100644 --- a/lib/provider/proxy_playlist/proxy_playlist.dart +++ b/lib/provider/proxy_playlist/proxy_playlist.dart @@ -27,6 +27,16 @@ class ProxyPlaylist { ); } + factory ProxyPlaylist.fromJsonRaw(Map json) => ProxyPlaylist( + json['tracks'] == null + ? {} + : (json['tracks'] as List).map((t) => Track.fromJson(t)).toSet(), + json['active'] as int?, + json['collections'] == null + ? {} + : (json['collections'] as List).toSet().cast(), + ); + Track? get activeTrack => active == null || active == -1 ? null : tracks.elementAtOrNull(active!); @@ -62,8 +72,8 @@ class ProxyPlaylist { /// Otherwise default super.toJson() is used static Map _makeAppropriateTrackJson(Track track) { return switch (track.runtimeType) { - LocalTrack => track.toJson(), - SourcedTrack => track.toJson(), + LocalTrack() => track.toJson(), + SourcedTrack() => track.toJson(), _ => track.toJson(), }; } diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 875f36cc..42b38746 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -127,6 +127,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier { state = state.copyWith(endlessPlayback: endless); } + void setEnableConnect(bool enable) { + state = state.copyWith(enableConnect: enable); + } + Future _getDefaultDownloadDirectory() async { if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; diff --git a/lib/provider/user_preferences/user_preferences_state.dart b/lib/provider/user_preferences/user_preferences_state.dart index cf6c0597..e35c73b5 100644 --- a/lib/provider/user_preferences/user_preferences_state.dart +++ b/lib/provider/user_preferences/user_preferences_state.dart @@ -91,6 +91,7 @@ class UserPreferences with _$UserPreferences { @Default(SourceCodecs.m4a) SourceCodecs downloadMusicCodec, @Default(true) bool discordPresence, @Default(true) bool endlessPlayback, + @Default(false) bool enableConnect, }) = _UserPreferences; factory UserPreferences.fromJson(Map json) => _$UserPreferencesFromJson(json); diff --git a/lib/provider/user_preferences/user_preferences_state.freezed.dart b/lib/provider/user_preferences/user_preferences_state.freezed.dart index 4d08d1a9..a5b076bb 100644 --- a/lib/provider/user_preferences/user_preferences_state.freezed.dart +++ b/lib/provider/user_preferences/user_preferences_state.freezed.dart @@ -50,6 +50,7 @@ mixin _$UserPreferences { SourceCodecs get downloadMusicCodec => throw _privateConstructorUsedError; bool get discordPresence => throw _privateConstructorUsedError; bool get endlessPlayback => throw _privateConstructorUsedError; + bool get enableConnect => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -93,7 +94,8 @@ abstract class $UserPreferencesCopyWith<$Res> { SourceCodecs streamMusicCodec, SourceCodecs downloadMusicCodec, bool discordPresence, - bool endlessPlayback}); + bool endlessPlayback, + bool enableConnect}); } /// @nodoc @@ -131,6 +133,7 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> Object? downloadMusicCodec = null, Object? discordPresence = null, Object? endlessPlayback = null, + Object? enableConnect = null, }) { return _then(_value.copyWith( audioQuality: null == audioQuality @@ -221,6 +224,10 @@ class _$UserPreferencesCopyWithImpl<$Res, $Val extends UserPreferences> ? _value.endlessPlayback : endlessPlayback // ignore: cast_nullable_to_non_nullable as bool, + enableConnect: null == enableConnect + ? _value.enableConnect + : enableConnect // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -263,7 +270,8 @@ abstract class _$$UserPreferencesImplCopyWith<$Res> SourceCodecs streamMusicCodec, SourceCodecs downloadMusicCodec, bool discordPresence, - bool endlessPlayback}); + bool endlessPlayback, + bool enableConnect}); } /// @nodoc @@ -299,6 +307,7 @@ class __$$UserPreferencesImplCopyWithImpl<$Res> Object? downloadMusicCodec = null, Object? discordPresence = null, Object? endlessPlayback = null, + Object? enableConnect = null, }) { return _then(_$UserPreferencesImpl( audioQuality: null == audioQuality @@ -389,6 +398,10 @@ class __$$UserPreferencesImplCopyWithImpl<$Res> ? _value.endlessPlayback : endlessPlayback // ignore: cast_nullable_to_non_nullable as bool, + enableConnect: null == enableConnect + ? _value.enableConnect + : enableConnect // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -426,7 +439,8 @@ class _$UserPreferencesImpl implements _UserPreferences { this.streamMusicCodec = SourceCodecs.weba, this.downloadMusicCodec = SourceCodecs.m4a, this.discordPresence = true, - this.endlessPlayback = true}); + this.endlessPlayback = true, + this.enableConnect = false}); factory _$UserPreferencesImpl.fromJson(Map json) => _$$UserPreferencesImplFromJson(json); @@ -503,10 +517,13 @@ class _$UserPreferencesImpl implements _UserPreferences { @override @JsonKey() final bool endlessPlayback; + @override + @JsonKey() + final bool enableConnect; @override String toString() { - return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback)'; + return 'UserPreferences(audioQuality: $audioQuality, albumColorSync: $albumColorSync, amoledDarkTheme: $amoledDarkTheme, checkUpdate: $checkUpdate, normalizeAudio: $normalizeAudio, showSystemTrayIcon: $showSystemTrayIcon, skipNonMusic: $skipNonMusic, systemTitleBar: $systemTitleBar, closeBehavior: $closeBehavior, accentColorScheme: $accentColorScheme, layoutMode: $layoutMode, locale: $locale, recommendationMarket: $recommendationMarket, searchMode: $searchMode, downloadLocation: $downloadLocation, pipedInstance: $pipedInstance, themeMode: $themeMode, audioSource: $audioSource, streamMusicCodec: $streamMusicCodec, downloadMusicCodec: $downloadMusicCodec, discordPresence: $discordPresence, endlessPlayback: $endlessPlayback, enableConnect: $enableConnect)'; } @override @@ -556,7 +573,9 @@ class _$UserPreferencesImpl implements _UserPreferences { (identical(other.discordPresence, discordPresence) || other.discordPresence == discordPresence) && (identical(other.endlessPlayback, endlessPlayback) || - other.endlessPlayback == endlessPlayback)); + other.endlessPlayback == endlessPlayback) && + (identical(other.enableConnect, enableConnect) || + other.enableConnect == enableConnect)); } @JsonKey(ignore: true) @@ -584,7 +603,8 @@ class _$UserPreferencesImpl implements _UserPreferences { streamMusicCodec, downloadMusicCodec, discordPresence, - endlessPlayback + endlessPlayback, + enableConnect ]); @JsonKey(ignore: true) @@ -633,7 +653,8 @@ abstract class _UserPreferences implements UserPreferences { final SourceCodecs streamMusicCodec, final SourceCodecs downloadMusicCodec, final bool discordPresence, - final bool endlessPlayback}) = _$UserPreferencesImpl; + final bool endlessPlayback, + final bool enableConnect}) = _$UserPreferencesImpl; factory _UserPreferences.fromJson(Map json) = _$UserPreferencesImpl.fromJson; @@ -691,6 +712,8 @@ abstract class _UserPreferences implements UserPreferences { @override bool get endlessPlayback; @override + bool get enableConnect; + @override @JsonKey(ignore: true) _$$UserPreferencesImplCopyWith<_$UserPreferencesImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/provider/user_preferences/user_preferences_state.g.dart b/lib/provider/user_preferences/user_preferences_state.g.dart index ce488247..8bdd12cc 100644 --- a/lib/provider/user_preferences/user_preferences_state.g.dart +++ b/lib/provider/user_preferences/user_preferences_state.g.dart @@ -59,6 +59,7 @@ _$UserPreferencesImpl _$$UserPreferencesImplFromJson( SourceCodecs.m4a, discordPresence: json['discordPresence'] as bool? ?? true, endlessPlayback: json['endlessPlayback'] as bool? ?? true, + enableConnect: json['enableConnect'] as bool? ?? false, ); Map _$$UserPreferencesImplToJson( @@ -87,6 +88,7 @@ Map _$$UserPreferencesImplToJson( 'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!, 'discordPresence': instance.discordPresence, 'endlessPlayback': instance.endlessPlayback, + 'enableConnect': instance.enableConnect, }; const _$SourceQualitiesEnumMap = { diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index f4c279b4..95777f56 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -18,6 +18,11 @@ dependencies: - libjsoncpp25 - libmpv1 | libmpv2 - xdg-user-dirs + - avahi-daemon + - avahi-discover + - avahi-utils + - libnss-mdns + - mdns-scan essential: false icon: assets/spotube-logo.png diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml index 1f952d0e..12b4473e 100644 --- a/linux/packaging/rpm/make_config.yaml +++ b/linux/packaging/rpm/make_config.yaml @@ -13,6 +13,9 @@ requires: - libsecret - libnotify - xdg-user-dirs + - avahi + - mdns-scan + - nss-mdns display_name: Spotube diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a7965e14..7bc841c6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import app_links import audio_service import audio_session +import bonsoir_darwin import device_info_plus import file_selector_macos import flutter_secure_storage_macos @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) diff --git a/macos/Podfile b/macos/Podfile index 049abe29..9ec46f8c 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 566e8196..68a46ae4 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,6 +5,9 @@ PODS: - FlutterMacOS - audio_session (0.0.1): - FlutterMacOS + - bonsoir_darwin (0.0.1): + - Flutter + - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS - file_selector_macos (0.0.1): @@ -50,6 +53,7 @@ DEPENDENCIES: - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`) - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) + - bonsoir_darwin (from `Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) @@ -80,6 +84,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos audio_session: :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos + bonsoir_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos file_selector_macos: @@ -121,6 +127,7 @@ SPEC CHECKSUMS: app_links: 4481ed4d71f384b0c3ae5016f4633aa73d32ff67 audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9 audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72 + bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea @@ -141,6 +148,6 @@ SPEC CHECKSUMS: window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 -PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 +PODFILE CHECKSUM: 0d3963a09fc94f580682bd88480486da345dc3f0 COCOAPODS: 1.15.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index a2dd74c4..bf5d70cf 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -436,7 +436,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -567,7 +567,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -592,7 +592,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/pubspec.lock b/pubspec.lock index bbf4faeb..adf30b4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,6 +177,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + bonsoir: + dependency: "direct main" + description: + name: bonsoir + sha256: "9703ca3ce201c7ab6cd278ae5a530a125959687f59c2b97822f88a8db5bef106" + url: "https://pub.dev" + source: hosted + version: "5.1.9" + bonsoir_android: + dependency: transitive + description: + name: bonsoir_android + sha256: "19583ae34a5e5743fa2c16619e4ec699b35ae5e6cece59b99b1cf21c1b4ed618" + url: "https://pub.dev" + source: hosted + version: "5.1.4" + bonsoir_darwin: + dependency: transitive + description: + name: bonsoir_darwin + sha256: "985c4c38b4cbfa57ed5870e724a7e17aa080ee7f49d03b43e6d08781511505c6" + url: "https://pub.dev" + source: hosted + version: "5.1.2" + bonsoir_linux: + dependency: transitive + description: + name: bonsoir_linux + sha256: "65554b20bc169c68c311eb31fab46ccdd8ee3d3dd89a2d57c338f4cbf6ceb00d" + url: "https://pub.dev" + source: hosted + version: "5.1.2" + bonsoir_platform_interface: + dependency: transitive + description: + name: bonsoir_platform_interface + sha256: "4ee898bec0b5a63f04f82b06da9896ae8475f32a33b6fa395bea56399daeb9f0" + url: "https://pub.dev" + source: hosted + version: "5.1.2" + bonsoir_windows: + dependency: transitive + description: + name: bonsoir_windows + sha256: abbc90b73ac39e823b0c127da43b91d8906dcc530fc0cec4e169cf0d8c4404b1 + url: "https://pub.dev" + source: hosted + version: "5.1.4" boolean_selector: dependency: transitive description: @@ -478,10 +526,10 @@ packages: dependency: "direct main" description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" device_frame: dependency: transitive description: @@ -1146,6 +1194,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + http_methods: + dependency: transitive + description: + name: http_methods + sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566" + url: "https://pub.dev" + source: hosted + version: "1.1.1" http_multi_server: dependency: transitive description: @@ -1882,13 +1938,21 @@ packages: source: hosted version: "2.3.2" shelf: - dependency: transitive + dependency: "direct main" description: name: shelf sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_router: + dependency: "direct main" + description: + name: shelf_router + sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864 + url: "https://pub.dev" + source: hosted + version: "1.1.4" shelf_static: dependency: transitive description: @@ -1898,7 +1962,7 @@ packages: source: hosted version: "1.1.2" shelf_web_socket: - dependency: transitive + dependency: "direct main" description: name: shelf_web_socket sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" @@ -2311,13 +2375,13 @@ packages: source: hosted version: "0.5.0" web_socket_channel: - dependency: transitive + dependency: "direct main" description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" webdriver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ef8401bc..9e869164 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -123,6 +123,11 @@ dependencies: flutter_broadcasts: ^0.4.0 freezed_annotation: ^2.4.1 spotify: ^0.13.3 + bonsoir: ^5.1.9 + shelf: ^1.4.1 + shelf_router: ^1.1.4 + shelf_web_socket: ^1.0.4 + web_socket_channel: ^2.4.4 dev_dependencies: build_runner: ^2.3.2 diff --git a/untranslated_messages.json b/untranslated_messages.json index 4275f461..26eb5d26 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,6 +1,103 @@ { + "ar": [ + "enable_connect", + "enable_connect_description" + ], + + "bn": [ + "enable_connect", + "enable_connect_description" + ], + + "ca": [ + "enable_connect", + "enable_connect_description" + ], + + "de": [ + "enable_connect", + "enable_connect_description" + ], + + "es": [ + "enable_connect", + "enable_connect_description" + ], + + "fa": [ + "enable_connect", + "enable_connect_description" + ], + + "fr": [ + "enable_connect", + "enable_connect_description" + ], + + "hi": [ + "enable_connect", + "enable_connect_description" + ], + + "it": [ + "enable_connect", + "enable_connect_description" + ], + + "ja": [ + "enable_connect", + "enable_connect_description" + ], + + "ko": [ + "enable_connect", + "enable_connect_description" + ], + + "ne": [ + "enable_connect", + "enable_connect_description" + ], + + "nl": [ + "enable_connect", + "enable_connect_description" + ], + + "pl": [ + "enable_connect", + "enable_connect_description" + ], + + "pt": [ + "enable_connect", + "enable_connect_description" + ], + + "ru": [ + "enable_connect", + "enable_connect_description" + ], + + "tr": [ + "enable_connect", + "enable_connect_description" + ], + + "uk": [ + "enable_connect", + "enable_connect_description" + ], + "vi": [ "friends", - "no_lyrics_available" + "no_lyrics_available", + "enable_connect", + "enable_connect_description" + ], + + "zh": [ + "enable_connect", + "enable_connect_description" ] } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index fcf9927e..d8a9db29 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -23,6 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + BonsoirWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi")); DartDiscordRpcPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DartDiscordRpcPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0fe6e076..90292744 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + bonsoir_windows dart_discord_rpc file_selector_windows flutter_secure_storage_windows