mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-05-08 16:24:36 +00:00
feat: add connect server support
This commit is contained in:
parent
ee97aedcfc
commit
c399baa5ab
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -66,5 +66,11 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true />
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>To allow other devices on the network control playback of Spotube securely.</string>
|
||||
<key>NSBonjourServices</key>
|
||||
<array>
|
||||
<string>_spotube._tcp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
@ -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<Spotube> {
|
||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
ref.read(connectServerProvider);
|
||||
|
||||
useDisableBatteryOptimizations();
|
||||
useInitSysTray(ref);
|
||||
useDeepLinking(ref);
|
||||
|
||||
15
lib/models/connect/connect.dart
Normal file
15
lib/models/connect/connect.dart
Normal file
@ -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';
|
||||
216
lib/models/connect/connect.freezed.dart
Normal file
216
lib/models/connect/connect.freezed.dart
Normal file
@ -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>(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<String, dynamic> json) {
|
||||
return _WebSocketLoadEventData.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$WebSocketLoadEventData {
|
||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||
List<Track> get tracks => throw _privateConstructorUsedError;
|
||||
String? get collectionId => throw _privateConstructorUsedError;
|
||||
int? get initialIndex => throw _privateConstructorUsedError;
|
||||
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$WebSocketLoadEventDataCopyWith<WebSocketLoadEventData> 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<Track> 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<Track>,
|
||||
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<Track> 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<Track>,
|
||||
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<Track> tracks,
|
||||
this.collectionId,
|
||||
this.initialIndex})
|
||||
: _tracks = tracks;
|
||||
|
||||
factory _$WebSocketLoadEventDataImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$WebSocketLoadEventDataImplFromJson(json);
|
||||
|
||||
final List<Track> _tracks;
|
||||
@override
|
||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||
List<Track> 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<String, dynamic> toJson() {
|
||||
return _$$WebSocketLoadEventDataImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _WebSocketLoadEventData implements WebSocketLoadEventData {
|
||||
factory _WebSocketLoadEventData(
|
||||
{@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||
required final List<Track> tracks,
|
||||
final String? collectionId,
|
||||
final int? initialIndex}) = _$WebSocketLoadEventDataImpl;
|
||||
|
||||
factory _WebSocketLoadEventData.fromJson(Map<String, dynamic> json) =
|
||||
_$WebSocketLoadEventDataImpl.fromJson;
|
||||
|
||||
@override
|
||||
@JsonKey(name: 'tracks', toJson: _tracksJson)
|
||||
List<Track> get tracks;
|
||||
@override
|
||||
String? get collectionId;
|
||||
@override
|
||||
int? get initialIndex;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$WebSocketLoadEventDataImplCopyWith<_$WebSocketLoadEventDataImpl>
|
||||
get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
25
lib/models/connect/connect.g.dart
Normal file
25
lib/models/connect/connect.g.dart
Normal file
@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'connect.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$WebSocketLoadEventDataImpl _$$WebSocketLoadEventDataImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$WebSocketLoadEventDataImpl(
|
||||
tracks: (json['tracks'] as List<dynamic>)
|
||||
.map((e) => Track.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
collectionId: json['collectionId'] as String?,
|
||||
initialIndex: json['initialIndex'] as int?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$WebSocketLoadEventDataImplToJson(
|
||||
_$WebSocketLoadEventDataImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'tracks': _tracksJson(instance.tracks),
|
||||
'collectionId': instance.collectionId,
|
||||
'initialIndex': instance.initialIndex,
|
||||
};
|
||||
27
lib/models/connect/load.dart
Normal file
27
lib/models/connect/load.dart
Normal file
@ -0,0 +1,27 @@
|
||||
part of 'connect.dart';
|
||||
|
||||
List<Map<String, dynamic>> _tracksJson(List<Track> tracks) {
|
||||
return tracks.map((e) => e.toJson()).toList();
|
||||
}
|
||||
|
||||
@freezed
|
||||
class WebSocketLoadEventData with _$WebSocketLoadEventData {
|
||||
factory WebSocketLoadEventData({
|
||||
@JsonKey(name: 'tracks', toJson: _tracksJson) required List<Track> tracks,
|
||||
String? collectionId,
|
||||
int? initialIndex,
|
||||
}) = _WebSocketLoadEventData;
|
||||
|
||||
factory WebSocketLoadEventData.fromJson(Map<String, dynamic> json) =>
|
||||
_$WebSocketLoadEventDataFromJson(json);
|
||||
}
|
||||
|
||||
class WebSocketLoadEvent extends WebSocketEvent<WebSocketLoadEventData> {
|
||||
WebSocketLoadEvent(WebSocketLoadEventData data) : super(WsEvent.load, data);
|
||||
|
||||
factory WebSocketLoadEvent.fromJson(Map<String, dynamic> json) {
|
||||
return WebSocketLoadEvent(
|
||||
WebSocketLoadEventData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
191
lib/models/connect/ws_event.dart
Normal file
191
lib/models/connect/ws_event.dart
Normal file
@ -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<T> = FutureOr<void> Function(T event);
|
||||
|
||||
class WebSocketEvent<T> {
|
||||
final WsEvent type;
|
||||
final T data;
|
||||
|
||||
WebSocketEvent(this.type, this.data);
|
||||
|
||||
factory WebSocketEvent.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
T Function(dynamic) fromJson,
|
||||
) {
|
||||
return WebSocketEvent(
|
||||
WsEvent.fromString(json["type"]),
|
||||
fromJson(json["data"]),
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() {
|
||||
return jsonEncode({
|
||||
"type": type.name,
|
||||
"data": data,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onPosition(
|
||||
EventCallback<WebSocketPositionEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.position) {
|
||||
await callback(
|
||||
WebSocketPositionEvent.fromJson(data as Map<String, dynamic>));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPlaying(
|
||||
EventCallback<WebSocketPlayingEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.playing) {
|
||||
await callback(WebSocketPlayingEvent(data as bool));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onResume(
|
||||
EventCallback<WebSocketResumeEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.resume) {
|
||||
await callback(WebSocketResumeEvent());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPause(
|
||||
EventCallback<WebSocketPauseEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.pause) {
|
||||
await callback(WebSocketPauseEvent());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onStop(
|
||||
EventCallback<WebSocketStopEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.stop) {
|
||||
await callback(WebSocketStopEvent());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onLoad(
|
||||
EventCallback<WebSocketLoadEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.load) {
|
||||
await callback(WebSocketLoadEvent.fromJson(data as Map<String, dynamic>));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onNext(
|
||||
EventCallback<WebSocketNextEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.next) {
|
||||
await callback(WebSocketNextEvent());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onPrevious(
|
||||
EventCallback<WebSocketPreviousEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.previous) {
|
||||
await callback(WebSocketPreviousEvent());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onJump(
|
||||
EventCallback<WebSocketJumpEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.jump) {
|
||||
await callback(WebSocketJumpEvent(data as int));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onError(
|
||||
EventCallback<WebSocketErrorEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.error) {
|
||||
await callback(WebSocketErrorEvent(data as String));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onQueue(
|
||||
EventCallback<WebSocketQueueEvent> callback,
|
||||
) async {
|
||||
if (type == WsEvent.queue) {
|
||||
await callback(
|
||||
WebSocketQueueEvent.fromJson(data as Map<String, dynamic>));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WebSocketPositionEvent extends WebSocketEvent<Duration> {
|
||||
WebSocketPositionEvent(Duration data) : super(WsEvent.position, data);
|
||||
|
||||
WebSocketPositionEvent.fromJson(Map<String, dynamic> 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<bool> {
|
||||
WebSocketPlayingEvent(bool data) : super(WsEvent.playing, data);
|
||||
}
|
||||
|
||||
class WebSocketResumeEvent extends WebSocketEvent<void> {
|
||||
WebSocketResumeEvent() : super(WsEvent.resume, null);
|
||||
}
|
||||
|
||||
class WebSocketPauseEvent extends WebSocketEvent<void> {
|
||||
WebSocketPauseEvent() : super(WsEvent.pause, null);
|
||||
}
|
||||
|
||||
class WebSocketStopEvent extends WebSocketEvent<void> {
|
||||
WebSocketStopEvent() : super(WsEvent.stop, null);
|
||||
}
|
||||
|
||||
class WebSocketNextEvent extends WebSocketEvent<void> {
|
||||
WebSocketNextEvent() : super(WsEvent.next, null);
|
||||
}
|
||||
|
||||
class WebSocketPreviousEvent extends WebSocketEvent<void> {
|
||||
WebSocketPreviousEvent() : super(WsEvent.previous, null);
|
||||
}
|
||||
|
||||
class WebSocketJumpEvent extends WebSocketEvent<int> {
|
||||
WebSocketJumpEvent(int data) : super(WsEvent.jump, data);
|
||||
}
|
||||
|
||||
class WebSocketErrorEvent extends WebSocketEvent<String> {
|
||||
WebSocketErrorEvent(String data) : super(WsEvent.error, data);
|
||||
}
|
||||
|
||||
class WebSocketQueueEvent extends WebSocketEvent<ProxyPlaylist> {
|
||||
WebSocketQueueEvent(ProxyPlaylist data) : super(WsEvent.queue, data);
|
||||
|
||||
factory WebSocketQueueEvent.fromJson(Map<String, dynamic> json) =>
|
||||
WebSocketQueueEvent(
|
||||
ProxyPlaylist.fromJsonRaw(json["data"] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
152
lib/provider/connect/server.dart
Normal file
152
lib/provider/connect/server.dart
Normal file
@ -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 = <StreamSubscription>[];
|
||||
|
||||
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;
|
||||
});
|
||||
@ -27,6 +27,16 @@ class ProxyPlaylist {
|
||||
);
|
||||
}
|
||||
|
||||
factory ProxyPlaylist.fromJsonRaw(Map<String, dynamic> json) => ProxyPlaylist(
|
||||
json['tracks'] == null
|
||||
? <Track>{}
|
||||
: (json['tracks'] as List).map((t) => Track.fromJson(t)).toSet(),
|
||||
json['active'] as int?,
|
||||
json['collections'] == null
|
||||
? {}
|
||||
: (json['collections'] as List).toSet().cast<String>(),
|
||||
);
|
||||
|
||||
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<String, dynamic> _makeAppropriateTrackJson(Track track) {
|
||||
return switch (track.runtimeType) {
|
||||
LocalTrack => track.toJson(),
|
||||
SourcedTrack => track.toJson(),
|
||||
LocalTrack() => track.toJson(),
|
||||
SourcedTrack() => track.toJson(),
|
||||
_ => track.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -127,6 +127,10 @@ class UserPreferencesNotifier extends PersistedStateNotifier<UserPreferences> {
|
||||
state = state.copyWith(endlessPlayback: endless);
|
||||
}
|
||||
|
||||
void setEnableConnect(bool enable) {
|
||||
state = state.copyWith(enableConnect: enable);
|
||||
}
|
||||
|
||||
Future<String> _getDefaultDownloadDirectory() async {
|
||||
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
|
||||
|
||||
|
||||
@ -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<String, dynamic> json) =>
|
||||
_$UserPreferencesFromJson(json);
|
||||
|
||||
@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
|
||||
|
||||
@ -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<String, dynamic> _$$UserPreferencesImplToJson(
|
||||
@ -87,6 +88,7 @@ Map<String, dynamic> _$$UserPreferencesImplToJson(
|
||||
'downloadMusicCodec': _$SourceCodecsEnumMap[instance.downloadMusicCodec]!,
|
||||
'discordPresence': instance.discordPresence,
|
||||
'endlessPlayback': instance.endlessPlayback,
|
||||
'enableConnect': instance.enableConnect,
|
||||
};
|
||||
|
||||
const _$SourceQualitiesEnumMap = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -13,6 +13,9 @@ requires:
|
||||
- libsecret
|
||||
- libnotify
|
||||
- xdg-user-dirs
|
||||
- avahi
|
||||
- mdns-scan
|
||||
- nss-mdns
|
||||
|
||||
display_name: Spotube
|
||||
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
78
pubspec.lock
78
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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <bonsoir_windows/bonsoir_windows_plugin_c_api.h>
|
||||
#include <dart_discord_rpc/dart_discord_rpc_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
@ -23,6 +24,8 @@
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
AppLinksPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
||||
BonsoirWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi"));
|
||||
DartDiscordRpcPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DartDiscordRpcPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
bonsoir_windows
|
||||
dart_discord_rpc
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
|
||||
Loading…
Reference in New Issue
Block a user