Compare commits
18 Commits
3.2.0+132
...
be414891ec
Author | SHA1 | Date | |
---|---|---|---|
be414891ec | |||
787876ab6a | |||
8578cde620
|
|||
14d55d45a8
|
|||
724391584e
|
|||
3a5e45808a | |||
488055955c
|
|||
|
313ebc64cc | ||
|
1ed8b1d0c1 | ||
4af816d931 | |||
1c058a4323 | |||
461ed1fcda | |||
5363afa558
|
|||
f0d2737da8
|
|||
1f2f80aa3e
|
|||
240a872e65
|
|||
c1ec6f0849 | |||
ab42686d4d |
@@ -181,7 +181,7 @@ class IslandApp extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
useEffect(() {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,7 @@ sealed class GeoIpLocation with _$GeoIpLocation {
|
||||
sealed class SnAuthChallenge with _$SnAuthChallenge {
|
||||
const factory SnAuthChallenge({
|
||||
required String id,
|
||||
required DateTime expiredAt,
|
||||
required DateTime? expiredAt,
|
||||
required int stepRemain,
|
||||
required int stepTotal,
|
||||
required int failedAttempts,
|
||||
@@ -57,7 +57,7 @@ sealed class SnAuthSession with _$SnAuthSession {
|
||||
required String id,
|
||||
required String? label,
|
||||
required DateTime lastGrantedAt,
|
||||
required DateTime expiredAt,
|
||||
required DateTime? expiredAt,
|
||||
required String accountId,
|
||||
required String challengeId,
|
||||
required SnAuthChallenge challenge,
|
||||
|
@@ -541,7 +541,7 @@ as String,
|
||||
/// @nodoc
|
||||
mixin _$SnAuthChallenge {
|
||||
|
||||
String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; DateTime? get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAuthChallenge
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -574,7 +574,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res> {
|
||||
factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -591,11 +591,11 @@ class _$SnAuthChallengeCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthChallenge
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
|
||||
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
@@ -704,7 +704,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthChallenge() when $default != null:
|
||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -725,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthChallenge():
|
||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
@@ -742,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthChallenge() when $default != null:
|
||||
return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -761,7 +761,7 @@ class _SnAuthChallenge implements SnAuthChallenge {
|
||||
factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final DateTime expiredAt;
|
||||
@override final DateTime? expiredAt;
|
||||
@override final int stepRemain;
|
||||
@override final int stepTotal;
|
||||
@override final int failedAttempts;
|
||||
@@ -829,7 +829,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge
|
||||
factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, DateTime? expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -846,11 +846,11 @@ class __$SnAuthChallengeCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthChallenge
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = freezed,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAuthChallenge(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as String,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable
|
||||
as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable
|
||||
as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable
|
||||
as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
@@ -888,7 +888,7 @@ $GeoIpLocationCopyWith<$Res>? get location {
|
||||
/// @nodoc
|
||||
mixin _$SnAuthSession {
|
||||
|
||||
String get id; String? get label; DateTime get lastGrantedAt; DateTime get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; String? get label; DateTime get lastGrantedAt; DateTime? get expiredAt; String get accountId; String get challengeId; SnAuthChallenge get challenge; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAuthSession
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -921,7 +921,7 @@ abstract mixin class $SnAuthSessionCopyWith<$Res> {
|
||||
factory $SnAuthSessionCopyWith(SnAuthSession value, $Res Function(SnAuthSession) _then) = _$SnAuthSessionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -938,13 +938,13 @@ class _$SnAuthSessionCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthSession
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
|
||||
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
@@ -1041,7 +1041,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthSession() when $default != null:
|
||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -1062,7 +1062,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthSession():
|
||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
@@ -1079,7 +1079,7 @@ return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.a
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAuthSession() when $default != null:
|
||||
return $default(_that.id,_that.label,_that.lastGrantedAt,_that.expiredAt,_that.accountId,_that.challengeId,_that.challenge,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
@@ -1100,7 +1100,7 @@ class _SnAuthSession implements SnAuthSession {
|
||||
@override final String id;
|
||||
@override final String? label;
|
||||
@override final DateTime lastGrantedAt;
|
||||
@override final DateTime expiredAt;
|
||||
@override final DateTime? expiredAt;
|
||||
@override final String accountId;
|
||||
@override final String challengeId;
|
||||
@override final SnAuthChallenge challenge;
|
||||
@@ -1141,7 +1141,7 @@ abstract mixin class _$SnAuthSessionCopyWith<$Res> implements $SnAuthSessionCopy
|
||||
factory _$SnAuthSessionCopyWith(_SnAuthSession value, $Res Function(_SnAuthSession) _then) = __$SnAuthSessionCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String? label, DateTime lastGrantedAt, DateTime? expiredAt, String accountId, String challengeId, SnAuthChallenge challenge, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@@ -1158,13 +1158,13 @@ class __$SnAuthSessionCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAuthSession
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = null,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = freezed,Object? lastGrantedAt = null,Object? expiredAt = freezed,Object? accountId = null,Object? challengeId = null,Object? challenge = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAuthSession(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable
|
||||
as String?,lastGrantedAt: null == lastGrantedAt ? _self.lastGrantedAt : lastGrantedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challengeId: null == challengeId ? _self.challengeId : challengeId // ignore: cast_nullable_to_non_nullable
|
||||
as String,challenge: null == challenge ? _self.challenge : challenge // ignore: cast_nullable_to_non_nullable
|
||||
as SnAuthChallenge,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
|
@@ -34,7 +34,10 @@ Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
|
||||
_SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
|
||||
_SnAuthChallenge(
|
||||
id: json['id'] as String,
|
||||
expiredAt: DateTime.parse(json['expired_at'] as String),
|
||||
expiredAt:
|
||||
json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
stepRemain: (json['step_remain'] as num).toInt(),
|
||||
stepTotal: (json['step_total'] as num).toInt(),
|
||||
failedAttempts: (json['failed_attempts'] as num).toInt(),
|
||||
@@ -66,7 +69,7 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'expired_at': instance.expiredAt.toIso8601String(),
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'step_remain': instance.stepRemain,
|
||||
'step_total': instance.stepTotal,
|
||||
'failed_attempts': instance.failedAttempts,
|
||||
@@ -89,7 +92,10 @@ _SnAuthSession _$SnAuthSessionFromJson(Map<String, dynamic> json) =>
|
||||
id: json['id'] as String,
|
||||
label: json['label'] as String?,
|
||||
lastGrantedAt: DateTime.parse(json['last_granted_at'] as String),
|
||||
expiredAt: DateTime.parse(json['expired_at'] as String),
|
||||
expiredAt:
|
||||
json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
accountId: json['account_id'] as String,
|
||||
challengeId: json['challenge_id'] as String,
|
||||
challenge: SnAuthChallenge.fromJson(
|
||||
@@ -108,7 +114,7 @@ Map<String, dynamic> _$SnAuthSessionToJson(_SnAuthSession instance) =>
|
||||
'id': instance.id,
|
||||
'label': instance.label,
|
||||
'last_granted_at': instance.lastGrantedAt.toIso8601String(),
|
||||
'expired_at': instance.expiredAt.toIso8601String(),
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'account_id': instance.accountId,
|
||||
'challenge_id': instance.challengeId,
|
||||
'challenge': instance.challenge.toJson(),
|
||||
|
@@ -9,7 +9,11 @@ import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
// Conditional imports for IPC server - use web stubs on web platform
|
||||
import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart';
|
||||
import 'ipc_server.windows.dart' if (dart.library.html) 'ipc_server.web.dart';
|
||||
import 'ipc_server.unix.dart' if (dart.library.html) 'ipc_server.web.dart';
|
||||
|
||||
const String kRpcLogPrefix = 'arRPC.websocket';
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
@@ -43,14 +47,14 @@ class IpcErrorCodes {
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
|
||||
class ActivityRpcServer {
|
||||
static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
|
||||
Map<String, Function>
|
||||
handlers; // {connection: (socket), message: (socket, data), close: (socket)}
|
||||
HttpServer? _httpServer;
|
||||
ServerSocket? _ipcServer;
|
||||
IpcServer? _ipcServer;
|
||||
final List<WebSocketChannel> _wsSockets = [];
|
||||
final List<_IpcSocketWrapper> _ipcSockets = [];
|
||||
|
||||
ActivityRpcServer(this.handlers);
|
||||
|
||||
@@ -58,109 +62,20 @@ class ActivityRpcServer {
|
||||
handlers = newHandlers;
|
||||
}
|
||||
|
||||
// Encode IPC packet
|
||||
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
|
||||
final jsonData = jsonEncode(data);
|
||||
final dataBytes = utf8.encode(jsonData);
|
||||
final dataSize = dataBytes.length;
|
||||
|
||||
final buffer = ByteData(8 + dataSize);
|
||||
buffer.setInt32(0, type, Endian.little);
|
||||
buffer.setInt32(4, dataSize, Endian.little);
|
||||
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
|
||||
|
||||
return buffer.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<String> _getMacOsSystemTmpDir() async {
|
||||
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
// Find available IPC socket path
|
||||
Future<String> _findAvailableIpcPath() async {
|
||||
// Build list of directories to try, with macOS-specific handling
|
||||
final baseDirs = <String>[];
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final macTempDir = await _getMacOsSystemTmpDir();
|
||||
if (macTempDir.isNotEmpty) {
|
||||
baseDirs.add(macTempDir);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to get macOS system temp dir: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add other standard directories
|
||||
final otherDirs = [
|
||||
Platform.environment['XDG_RUNTIME_DIR'], // User runtime directory
|
||||
Platform.environment['TMPDIR'], // App container temp (fallback)
|
||||
Platform.environment['TMP'],
|
||||
Platform.environment['TEMP'],
|
||||
'/tmp', // System temp directory - most compatible
|
||||
];
|
||||
|
||||
baseDirs.addAll(
|
||||
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
|
||||
);
|
||||
|
||||
for (final baseDir in baseDirs) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final socketPath = path.join(baseDir, '$kIpcBasePath-$i');
|
||||
try {
|
||||
final socket = await ServerSocket.bind(
|
||||
InternetAddress(socketPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
socket.close();
|
||||
// Clean up the test socket
|
||||
try {
|
||||
await File(socketPath).delete();
|
||||
} catch (_) {}
|
||||
developer.log(
|
||||
'IPC socket will be created at: $socketPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
return socketPath;
|
||||
} catch (e) {
|
||||
// Path not available, try next
|
||||
if (i == 0) {
|
||||
// Log only for the first attempt per directory
|
||||
developer.log(
|
||||
'IPC path $socketPath not available: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception(
|
||||
'No available IPC socket paths found in any temp directory',
|
||||
);
|
||||
}
|
||||
|
||||
// Start the WebSocket server
|
||||
// Start the server
|
||||
Future<void> start() async {
|
||||
int port = portRange[0];
|
||||
bool wsSuccess = false;
|
||||
|
||||
// Start WebSocket server
|
||||
while (port <= portRange[1]) {
|
||||
developer.log('trying port $port', name: kRpcLogPrefix);
|
||||
developer.log('Trying port $port', name: kRpcLogPrefix);
|
||||
try {
|
||||
// Start HTTP server
|
||||
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
|
||||
developer.log('listening on $port', name: kRpcLogPrefix);
|
||||
developer.log('Listening on $port', name: kRpcLogPrefix);
|
||||
|
||||
// Handle WebSocket upgrades
|
||||
shelf_io.serveRequests(_httpServer!, (Request request) async {
|
||||
developer.log('new request', name: kRpcLogPrefix);
|
||||
developer.log('New request', name: kRpcLogPrefix);
|
||||
if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
|
||||
final handler = webSocketHandler((WebSocketChannel channel, _) {
|
||||
_wsSockets.add(channel);
|
||||
@@ -169,7 +84,7 @@ class ActivityRpcServer {
|
||||
return handler(request);
|
||||
}
|
||||
developer.log(
|
||||
'new request disposed due to not websocket',
|
||||
'New request disposed due to not websocket',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
return Response.notFound('Not a WebSocket request');
|
||||
@@ -178,12 +93,12 @@ class ActivityRpcServer {
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e is SocketException && e.osError?.errorCode == 98) {
|
||||
// EADDRINUSE
|
||||
developer.log('$port in use!', name: kRpcLogPrefix);
|
||||
} else {
|
||||
developer.log('http error: $e', name: kRpcLogPrefix);
|
||||
developer.log('HTTP error: $e', name: kRpcLogPrefix);
|
||||
}
|
||||
port++;
|
||||
await Future.delayed(Duration(milliseconds: 100)); // Add delay
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,27 +108,28 @@ class ActivityRpcServer {
|
||||
);
|
||||
}
|
||||
|
||||
// Start IPC server (skip on macOS due to sandboxing)
|
||||
final shouldStartIpc = !Platform.isMacOS;
|
||||
// Start IPC server
|
||||
final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
|
||||
if (shouldStartIpc) {
|
||||
try {
|
||||
final ipcPath = await _findAvailableIpcPath();
|
||||
_ipcServer = await ServerSocket.bind(
|
||||
InternetAddress(ipcPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
|
||||
if (Platform.isWindows) {
|
||||
_ipcServer = WindowsIpcServer();
|
||||
} else {
|
||||
_ipcServer = UnixIpcServer();
|
||||
}
|
||||
|
||||
_ipcServer!.listen((Socket socket) {
|
||||
_onIpcConnection(socket);
|
||||
});
|
||||
// Set up IPC handlers
|
||||
_ipcServer!.handlePacket = (socket, packet, _) {
|
||||
_handleIpcPacket(socket, packet);
|
||||
};
|
||||
|
||||
await _ipcServer!.start();
|
||||
} catch (e) {
|
||||
developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
|
||||
// Continue without IPC if it fails
|
||||
}
|
||||
} else {
|
||||
developer.log(
|
||||
'IPC server disabled on macOS in production mode due to sandboxing',
|
||||
'IPC server disabled on macOS or web in production mode',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
@@ -223,24 +139,23 @@ class ActivityRpcServer {
|
||||
Future<void> stop() async {
|
||||
// Stop WebSocket server
|
||||
for (var socket in _wsSockets) {
|
||||
await socket.sink.close();
|
||||
try {
|
||||
await socket.sink.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
|
||||
}
|
||||
}
|
||||
_wsSockets.clear();
|
||||
await _httpServer?.close();
|
||||
await _httpServer?.close(force: true);
|
||||
|
||||
// Stop IPC server
|
||||
for (var socket in _ipcSockets) {
|
||||
socket.close();
|
||||
}
|
||||
_ipcSockets.clear();
|
||||
await _ipcServer?.close();
|
||||
await _ipcServer?.stop();
|
||||
|
||||
developer.log('servers stopped', name: kRpcLogPrefix);
|
||||
developer.log('Servers stopped', name: kRpcLogPrefix);
|
||||
}
|
||||
|
||||
// Handle new WebSocket connection
|
||||
void _onWsConnection(WebSocketChannel socket, Request request) {
|
||||
// Parse query parameters
|
||||
final uri = request.url;
|
||||
final params = uri.queryParameters;
|
||||
final ver = int.tryParse(params['v'] ?? '1') ?? 1;
|
||||
@@ -249,43 +164,38 @@ class ActivityRpcServer {
|
||||
final origin = request.headers['origin'] ?? '';
|
||||
|
||||
developer.log(
|
||||
'new WS connection! origin: $origin, params: $params',
|
||||
'New WS connection! origin: $origin, params: $params',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
|
||||
// Validate origin
|
||||
if (origin.isNotEmpty &&
|
||||
![
|
||||
'https://discord.com',
|
||||
'https://ptb.discord.com',
|
||||
'https://canary.discord.com',
|
||||
].contains(origin)) {
|
||||
developer.log('disallowed origin: $origin', name: kRpcLogPrefix);
|
||||
developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate encoding
|
||||
if (encoding != 'json') {
|
||||
developer.log(
|
||||
'unsupported encoding requested: $encoding',
|
||||
'Unsupported encoding requested: $encoding',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate version
|
||||
if (ver != 1) {
|
||||
developer.log('unsupported version requested: $ver', name: kRpcLogPrefix);
|
||||
developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store client info on socket
|
||||
final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
|
||||
|
||||
// Set up event listeners
|
||||
socket.stream.listen(
|
||||
(data) => _onWsMessage(socketWithMeta, data),
|
||||
onError: (e) {
|
||||
@@ -298,36 +208,27 @@ class ActivityRpcServer {
|
||||
},
|
||||
);
|
||||
|
||||
// Notify handler of new connection
|
||||
handlers['connection']?.call(socketWithMeta);
|
||||
}
|
||||
|
||||
// Handle new IPC connection
|
||||
void _onIpcConnection(Socket socket) {
|
||||
developer.log('new IPC connection!', name: kRpcIpcLogPrefix);
|
||||
|
||||
final socketWrapper = _IpcSocketWrapper(socket);
|
||||
_ipcSockets.add(socketWrapper);
|
||||
|
||||
// Set up event listeners
|
||||
socket.listen(
|
||||
(data) => _onIpcData(socketWrapper, data),
|
||||
onError: (e) {
|
||||
developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.close();
|
||||
},
|
||||
onDone: () {
|
||||
developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
|
||||
handlers['close']?.call(socketWrapper);
|
||||
_ipcSockets.remove(socketWrapper);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket message
|
||||
void _onWsMessage(_WsSocketWrapper socket, dynamic data) {
|
||||
Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
|
||||
if (data is! String) {
|
||||
developer.log(
|
||||
'Invalid WebSocket message: not a string',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final jsonData = jsonDecode(data as String);
|
||||
final jsonData = await compute(jsonDecode, data);
|
||||
if (jsonData is! Map<String, dynamic>) {
|
||||
developer.log(
|
||||
'Invalid WebSocket message: not a JSON object',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
return;
|
||||
}
|
||||
developer.log('WS message: $jsonData', name: kRpcLogPrefix);
|
||||
handlers['message']?.call(socket, jsonData);
|
||||
} catch (e) {
|
||||
@@ -335,22 +236,8 @@ class ActivityRpcServer {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming IPC data
|
||||
void _onIpcData(_IpcSocketWrapper socket, List<int> data) {
|
||||
try {
|
||||
socket.addData(data);
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
_handleIpcPacket(socket, packet);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IPC packet
|
||||
void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) {
|
||||
void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
|
||||
switch (packet.type) {
|
||||
case IpcTypes.ping:
|
||||
developer.log('IPC ping received', name: kRpcIpcLogPrefix);
|
||||
@@ -359,7 +246,6 @@ class ActivityRpcServer {
|
||||
|
||||
case IpcTypes.pong:
|
||||
developer.log('IPC pong received', name: kRpcIpcLogPrefix);
|
||||
// Handle pong if needed
|
||||
break;
|
||||
|
||||
case IpcTypes.handshake:
|
||||
@@ -388,13 +274,12 @@ class ActivityRpcServer {
|
||||
}
|
||||
|
||||
// Handle IPC handshake
|
||||
void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||
void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
||||
|
||||
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||
final clientId = params['client_id']?.toString() ?? '';
|
||||
|
||||
// Validate version
|
||||
if (ver != 1) {
|
||||
developer.log(
|
||||
'IPC unsupported version requested: $ver',
|
||||
@@ -404,7 +289,6 @@ class ActivityRpcServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate client ID
|
||||
if (clientId.isEmpty) {
|
||||
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||
@@ -413,7 +297,6 @@ class ActivityRpcServer {
|
||||
|
||||
socket.clientId = clientId;
|
||||
|
||||
// Notify handler of new connection
|
||||
handlers['connection']?.call(socket);
|
||||
}
|
||||
}
|
||||
@@ -432,74 +315,6 @@ class _WsSocketWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
// IPC wrapper
|
||||
class _IpcSocketWrapper {
|
||||
final Socket socket;
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
final List<int> _buffer = [];
|
||||
|
||||
_IpcSocketWrapper(this.socket);
|
||||
|
||||
void addData(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
void sendPong(dynamic data) {
|
||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
void close() {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
void closeWithCode(int code, [String message = '']) {
|
||||
final closeData = {'code': code, 'message': message};
|
||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
||||
socket.add(packet);
|
||||
socket.close();
|
||||
}
|
||||
|
||||
List<_IpcPacket> readPackets() {
|
||||
final packets = <_IpcPacket>[];
|
||||
|
||||
while (_buffer.length >= 8) {
|
||||
final buffer = Uint8List.fromList(_buffer);
|
||||
final byteData = ByteData.view(buffer.buffer);
|
||||
|
||||
final type = byteData.getInt32(0, Endian.little);
|
||||
final dataSize = byteData.getInt32(4, Endian.little);
|
||||
|
||||
if (_buffer.length < 8 + dataSize) break;
|
||||
|
||||
final dataBytes = _buffer.sublist(8, 8 + dataSize);
|
||||
final jsonStr = utf8.decode(dataBytes);
|
||||
final jsonData = jsonDecode(jsonStr);
|
||||
|
||||
packets.add(_IpcPacket(type, jsonData));
|
||||
|
||||
_buffer.removeRange(0, 8 + dataSize);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class _IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
_IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
// State management for server status and activities
|
||||
class ServerState {
|
||||
final String status;
|
||||
@@ -522,7 +337,6 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||
: super(ServerState(status: 'Server not started'));
|
||||
|
||||
Future<void> start() async {
|
||||
// Only start server on desktop platforms
|
||||
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
|
||||
try {
|
||||
await server.start();
|
||||
@@ -554,9 +368,8 @@ final rpcServerStateProvider =
|
||||
final clientId =
|
||||
socket is _WsSocketWrapper
|
||||
? socket.clientId
|
||||
: (socket as _IpcSocketWrapper).clientId;
|
||||
: (socket as IpcSocketWrapper).clientId;
|
||||
notifier.updateStatus('Client connected (ID: $clientId)');
|
||||
// Send READY event
|
||||
socket.send({
|
||||
'cmd': 'DISPATCH',
|
||||
'data': {
|
||||
@@ -575,7 +388,7 @@ final rpcServerStateProvider =
|
||||
},
|
||||
},
|
||||
'evt': 'READY',
|
||||
'nonce': '12345', // Should be dynamic
|
||||
'nonce': '12345',
|
||||
});
|
||||
},
|
||||
'message': (socket, dynamic data) async {
|
||||
@@ -583,7 +396,6 @@ final rpcServerStateProvider =
|
||||
notifier.addActivity(
|
||||
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
|
||||
);
|
||||
// Call setRemoteActivityStatus
|
||||
final label = data['args']['activity']['details'] ?? 'Unknown';
|
||||
final appId = socket.clientId;
|
||||
try {
|
||||
@@ -594,7 +406,6 @@ final rpcServerStateProvider =
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
}
|
||||
// Echo back success
|
||||
socket.send({
|
||||
'cmd': 'SET_ACTIVITY',
|
||||
'data': data['args']['activity'],
|
118
lib/pods/activity/ipc_server.dart
Normal file
118
lib/pods/activity/ipc_server.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
// Abstract base class for IPC server
|
||||
abstract class IpcServer {
|
||||
final List<IpcSocketWrapper> _sockets = [];
|
||||
|
||||
// Encode IPC packet
|
||||
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
|
||||
final jsonData = jsonEncode(data);
|
||||
final dataBytes = utf8.encode(jsonData);
|
||||
final dataSize = dataBytes.length;
|
||||
|
||||
final buffer = ByteData(8 + dataSize);
|
||||
buffer.setInt32(0, type, Endian.little);
|
||||
buffer.setInt32(4, dataSize, Endian.little);
|
||||
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
|
||||
|
||||
return buffer.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Future<void> start();
|
||||
Future<void> stop();
|
||||
|
||||
void addSocket(IpcSocketWrapper socket) {
|
||||
_sockets.add(socket);
|
||||
}
|
||||
|
||||
void removeSocket(IpcSocketWrapper socket) {
|
||||
_sockets.remove(socket);
|
||||
}
|
||||
|
||||
List<IpcSocketWrapper> get sockets => _sockets;
|
||||
|
||||
void Function(
|
||||
IpcSocketWrapper socket,
|
||||
IpcPacket packet,
|
||||
Map<String, Function> handlers,
|
||||
)?
|
||||
handlePacket;
|
||||
}
|
||||
|
||||
// Abstract base class for IPC socket wrapper
|
||||
abstract class IpcSocketWrapper {
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
final List<int> _buffer = [];
|
||||
|
||||
void addData(List<int> data) {
|
||||
_buffer.addAll(data);
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> msg);
|
||||
void sendPong(dynamic data);
|
||||
void close();
|
||||
void closeWithCode(int code, [String message = '']);
|
||||
|
||||
List<IpcPacket> readPackets() {
|
||||
final packets = <IpcPacket>[];
|
||||
|
||||
while (_buffer.length >= 8) {
|
||||
final buffer = Uint8List.fromList(_buffer);
|
||||
final byteData = ByteData.view(buffer.buffer);
|
||||
|
||||
final type = byteData.getInt32(0, Endian.little);
|
||||
final dataSize = byteData.getInt32(4, Endian.little);
|
||||
|
||||
if (_buffer.length < 8 + dataSize) break;
|
||||
|
||||
final dataBytes = _buffer.sublist(8, 8 + dataSize);
|
||||
final jsonStr = utf8.decode(dataBytes);
|
||||
final jsonData = jsonDecode(jsonStr);
|
||||
|
||||
packets.add(IpcPacket(type, jsonData));
|
||||
|
||||
_buffer.removeRange(0, 8 + dataSize);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
}
|
172
lib/pods/activity/ipc_server.unix.dart
Normal file
172
lib/pods/activity/ipc_server.unix.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'ipc_server.dart';
|
||||
|
||||
class UnixIpcServer extends IpcServer {
|
||||
ServerSocket? _ipcServer;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
final ipcPath = await _findAvailableIpcPath();
|
||||
_ipcServer = await ServerSocket.bind(
|
||||
InternetAddress(ipcPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
|
||||
|
||||
_ipcServer!.listen((Socket socket) {
|
||||
_onIpcConnection(socket);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
for (var socket in sockets) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
|
||||
}
|
||||
}
|
||||
sockets.clear();
|
||||
await _ipcServer?.close();
|
||||
}
|
||||
|
||||
// Handle new IPC connection
|
||||
void _onIpcConnection(Socket socket) {
|
||||
developer.log('New IPC connection!', name: kRpcIpcLogPrefix);
|
||||
|
||||
final socketWrapper = UnixIpcSocketWrapper(socket);
|
||||
addSocket(socketWrapper);
|
||||
|
||||
socket.listen(
|
||||
(data) => _onIpcData(socketWrapper, data),
|
||||
onError: (e) {
|
||||
developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.close();
|
||||
},
|
||||
onDone: () {
|
||||
developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
|
||||
removeSocket(socketWrapper);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handle incoming IPC data
|
||||
void _onIpcData(UnixIpcSocketWrapper socket, List<int> data) {
|
||||
try {
|
||||
socket.addData(data);
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
handlePacket?.call(socket, packet, {});
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getMacOsSystemTmpDir() async {
|
||||
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
|
||||
return (result.stdout as String).trim();
|
||||
}
|
||||
|
||||
// Find available IPC socket path
|
||||
Future<String> _findAvailableIpcPath() async {
|
||||
// Build list of directories to try, with macOS-specific handling
|
||||
final baseDirs = <String>[];
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final macTempDir = await _getMacOsSystemTmpDir();
|
||||
if (macTempDir.isNotEmpty) {
|
||||
baseDirs.add(macTempDir);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to get macOS system temp dir: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add other standard directories
|
||||
final otherDirs = [
|
||||
Platform.environment['XDG_RUNTIME_DIR'],
|
||||
Platform.environment['TMPDIR'],
|
||||
Platform.environment['TMP'],
|
||||
Platform.environment['TEMP'],
|
||||
'/tmp',
|
||||
];
|
||||
|
||||
baseDirs.addAll(
|
||||
otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
|
||||
);
|
||||
|
||||
for (final baseDir in baseDirs) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final socketPath = path.join(baseDir, 'discord-ipc-$i');
|
||||
try {
|
||||
final socket = await ServerSocket.bind(
|
||||
InternetAddress(socketPath, type: InternetAddressType.unix),
|
||||
0,
|
||||
);
|
||||
socket.close();
|
||||
try {
|
||||
await File(socketPath).delete();
|
||||
} catch (_) {}
|
||||
developer.log(
|
||||
'IPC socket will be created at: $socketPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
return socketPath;
|
||||
} catch (e) {
|
||||
if (i == 0) {
|
||||
developer.log(
|
||||
'IPC path $socketPath not available: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception(
|
||||
'No available IPC socket paths found in any temp directory',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnixIpcSocketWrapper extends IpcSocketWrapper {
|
||||
final Socket socket;
|
||||
|
||||
UnixIpcSocketWrapper(this.socket);
|
||||
|
||||
@override
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendPong(dynamic data) {
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
socket.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void closeWithCode(int code, [String message = '']) {
|
||||
final closeData = {'code': code, 'message': message};
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
||||
socket.add(packet);
|
||||
socket.close();
|
||||
}
|
||||
}
|
63
lib/pods/activity/ipc_server.web.dart
Normal file
63
lib/pods/activity/ipc_server.web.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
// Stub implementation for web platform
|
||||
// This file provides empty implementations to avoid import errors on web
|
||||
|
||||
// IPC Packet Types
|
||||
class IpcTypes {
|
||||
static const int handshake = 0;
|
||||
static const int frame = 1;
|
||||
static const int close = 2;
|
||||
static const int ping = 3;
|
||||
static const int pong = 4;
|
||||
}
|
||||
|
||||
// IPC Close Codes
|
||||
class IpcCloseCodes {
|
||||
static const int closeNormal = 1000;
|
||||
static const int closeUnsupported = 1003;
|
||||
static const int closeAbnormal = 1006;
|
||||
}
|
||||
|
||||
// IPC Error Codes
|
||||
class IpcErrorCodes {
|
||||
static const int invalidClientId = 4000;
|
||||
static const int invalidOrigin = 4001;
|
||||
static const int rateLimited = 4002;
|
||||
static const int tokenRevoked = 4003;
|
||||
static const int invalidVersion = 4004;
|
||||
static const int invalidEncoding = 4005;
|
||||
}
|
||||
|
||||
// IPC Packet structure
|
||||
class IpcPacket {
|
||||
final int type;
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
IpcPacket(this.type, this.data);
|
||||
}
|
||||
|
||||
class IpcServer {
|
||||
Future<void> start() async {}
|
||||
Future<void> stop() async {}
|
||||
void Function(dynamic, dynamic, dynamic)? handlePacket;
|
||||
void addSocket(dynamic socket) {}
|
||||
void removeSocket(dynamic socket) {}
|
||||
List<dynamic> get sockets => [];
|
||||
}
|
||||
|
||||
class IpcSocketWrapper {
|
||||
String clientId = '';
|
||||
bool handshook = false;
|
||||
|
||||
void addData(List<int> data) {}
|
||||
void send(Map<String, dynamic> msg) {}
|
||||
void sendPong(dynamic data) {}
|
||||
void close() {}
|
||||
void closeWithCode(int code, [String message = '']) {}
|
||||
List<dynamic> readPackets() => [];
|
||||
}
|
||||
|
||||
class WindowsIpcServer extends IpcServer {}
|
||||
|
||||
class UnixIpcServer extends IpcServer {}
|
||||
|
||||
class WindowsIpcSocketWrapper extends IpcSocketWrapper {}
|
243
lib/pods/activity/ipc_server.windows.dart
Normal file
243
lib/pods/activity/ipc_server.windows.dart
Normal file
@@ -0,0 +1,243 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
import 'ipc_server.dart';
|
||||
|
||||
class WindowsIpcServer extends IpcServer {
|
||||
int? _pipeHandle;
|
||||
Timer? _ipcTimer;
|
||||
|
||||
@override
|
||||
Future<void> start() async {
|
||||
final pipeName = r'\\.\pipe\discord-ipc-0'.toNativeUtf16();
|
||||
try {
|
||||
_pipeHandle = CreateNamedPipe(
|
||||
pipeName,
|
||||
PIPE_ACCESS_DUPLEX,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
4096, // Output buffer size
|
||||
4096, // Input buffer size
|
||||
0, // Default timeout
|
||||
nullptr, // Security attributes
|
||||
);
|
||||
|
||||
if (_pipeHandle == INVALID_HANDLE_VALUE) {
|
||||
final error = GetLastError();
|
||||
throw Exception('Failed to create named pipe: error code $error');
|
||||
}
|
||||
|
||||
developer.log(
|
||||
r'IPC named pipe created at \\.\pipe\discord-ipc-0',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
|
||||
// Start listening for connections in a separate isolate
|
||||
_listenWindowsIpc();
|
||||
} finally {
|
||||
free(pipeName);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
for (var socket in sockets) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
|
||||
}
|
||||
}
|
||||
sockets.clear();
|
||||
|
||||
if (_pipeHandle != null) {
|
||||
try {
|
||||
CloseHandle(_pipeHandle!);
|
||||
} catch (e) {
|
||||
developer.log('Error closing named pipe: $e', name: kRpcIpcLogPrefix);
|
||||
}
|
||||
_pipeHandle = null;
|
||||
}
|
||||
_ipcTimer?.cancel();
|
||||
}
|
||||
|
||||
// Listen for Windows IPC connections in an isolate
|
||||
void _listenWindowsIpc() async {
|
||||
final receivePort = ReceivePort();
|
||||
await Isolate.spawn(_windowsIpcIsolate, receivePort.sendPort);
|
||||
|
||||
receivePort.listen((message) {
|
||||
developer.log(message.toString(), name: kRpcIpcLogPrefix);
|
||||
if (message is int) {
|
||||
final socketWrapper = WindowsIpcSocketWrapper(message);
|
||||
addSocket(socketWrapper);
|
||||
developer.log(
|
||||
'New IPC connection on named pipe',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
_handleWindowsIpcData(socketWrapper);
|
||||
start(); // Create new pipe for next connection
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void _windowsIpcIsolate(SendPort sendPort) {
|
||||
while (true) {
|
||||
final pipeHandle = CreateNamedPipe(
|
||||
r'\\.\pipe\discord-ipc-0'.toNativeUtf16(),
|
||||
PIPE_ACCESS_DUPLEX,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
PIPE_UNLIMITED_INSTANCES,
|
||||
4096,
|
||||
4096,
|
||||
0,
|
||||
nullptr,
|
||||
);
|
||||
if (pipeHandle == INVALID_HANDLE_VALUE) {
|
||||
developer.log(
|
||||
'Failed to create named pipe: ${GetLastError()}',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
break;
|
||||
}
|
||||
final connected = ConnectNamedPipe(pipeHandle, nullptr);
|
||||
if (connected != 0 || GetLastError() == ERROR_PIPE_CONNECTED) {
|
||||
sendPort.send(pipeHandle);
|
||||
}
|
||||
// Avoid tight loop
|
||||
sleep(Duration(milliseconds: 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Windows IPC data
|
||||
void _handleWindowsIpcData(WindowsIpcSocketWrapper socket) async {
|
||||
final startTime = DateTime.now();
|
||||
final buffer = malloc.allocate<BYTE>(4096);
|
||||
final bytesRead = malloc.allocate<DWORD>(4);
|
||||
try {
|
||||
while (socket.pipeHandle != null) {
|
||||
final readStart = DateTime.now();
|
||||
final success = ReadFile(
|
||||
socket.pipeHandle!,
|
||||
buffer.cast(),
|
||||
4096,
|
||||
bytesRead,
|
||||
nullptr,
|
||||
);
|
||||
final readDuration =
|
||||
DateTime.now().difference(readStart).inMicroseconds;
|
||||
developer.log(
|
||||
'ReadFile took $readDuration microseconds',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
|
||||
if (success == FALSE && GetLastError() != ERROR_MORE_DATA) {
|
||||
developer.log(
|
||||
'IPC read error: ${GetLastError()}',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
socket.close();
|
||||
break;
|
||||
}
|
||||
|
||||
final data = buffer.asTypedList(0);
|
||||
socket.addData(data);
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
handlePacket?.call(socket, packet, {});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
} finally {
|
||||
malloc.free(buffer);
|
||||
malloc.free(bytesRead);
|
||||
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
|
||||
developer.log(
|
||||
'handleWindowsIpcData took $totalDuration microseconds',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WindowsIpcSocketWrapper extends IpcSocketWrapper {
|
||||
final int? pipeHandle;
|
||||
|
||||
WindowsIpcSocketWrapper(this.pipeHandle);
|
||||
|
||||
@override
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
final buffer = malloc.allocate<BYTE>(packet.length);
|
||||
buffer.asTypedList(packet.length).setAll(0, packet);
|
||||
final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
|
||||
try {
|
||||
WriteFile(
|
||||
pipeHandle!,
|
||||
buffer.cast(),
|
||||
packet.length,
|
||||
bytesWritten,
|
||||
nullptr,
|
||||
);
|
||||
} finally {
|
||||
malloc.free(buffer);
|
||||
malloc.free(bytesWritten);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void sendPong(dynamic data) {
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
||||
final buffer = malloc.allocate<BYTE>(packet.length);
|
||||
buffer.asTypedList(packet.length).setAll(0, packet);
|
||||
final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
|
||||
try {
|
||||
WriteFile(
|
||||
pipeHandle!,
|
||||
buffer.cast(),
|
||||
packet.length,
|
||||
bytesWritten,
|
||||
nullptr,
|
||||
);
|
||||
} finally {
|
||||
malloc.free(buffer);
|
||||
malloc.free(bytesWritten);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
if (pipeHandle != null) {
|
||||
CloseHandle(pipeHandle!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void closeWithCode(int code, [String message = '']) {
|
||||
final closeData = {'code': code, 'message': message};
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
||||
final buffer = malloc.allocate<BYTE>(packet.length);
|
||||
buffer.asTypedList(packet.length).setAll(0, packet);
|
||||
final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
|
||||
try {
|
||||
WriteFile(
|
||||
pipeHandle!,
|
||||
buffer.cast(),
|
||||
packet.length,
|
||||
bytesWritten,
|
||||
nullptr,
|
||||
);
|
||||
} finally {
|
||||
malloc.free(buffer);
|
||||
malloc.free(bytesWritten);
|
||||
}
|
||||
CloseHandle(pipeHandle!);
|
||||
}
|
||||
}
|
@@ -89,8 +89,7 @@ bool get _supportsAnalytics =>
|
||||
kIsWeb ||
|
||||
Platform.isAndroid ||
|
||||
Platform.isIOS ||
|
||||
Platform.isMacOS ||
|
||||
Platform.isWindows;
|
||||
Platform.isMacOS;
|
||||
|
||||
// Provider for the router
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
|
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
@@ -48,11 +48,12 @@ class TrayService {
|
||||
void handleAction(MenuItem item) {
|
||||
switch (item.key) {
|
||||
case 'show_window':
|
||||
if (appWindow.isVisible) {
|
||||
appWindow.restore();
|
||||
} else {
|
||||
appWindow.show();
|
||||
}
|
||||
() async {
|
||||
appWindow.show();
|
||||
appWindow.restore();
|
||||
await Future.delayed(const Duration(milliseconds: 32));
|
||||
appWindow.show();
|
||||
}();
|
||||
break;
|
||||
case 'exit_app':
|
||||
appWindow.close();
|
||||
|
@@ -1,230 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
||||
_appLifecycleState = state;
|
||||
}
|
||||
// Conditional imports based on platform
|
||||
import 'notify.windows.dart' as windows_notify;
|
||||
import 'notify.universal.dart' as universal_notify;
|
||||
|
||||
// Platform-specific delegation
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsIOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsMacOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
|
||||
const WindowsInitializationSettings initializationSettingsWindows =
|
||||
WindowsInitializationSettings(
|
||||
appName: 'Island',
|
||||
appUserModelId: 'dev.solsynth.solian',
|
||||
guid: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsIOS,
|
||||
macOS: initializationSettingsMacOS,
|
||||
linux: initializationSettingsLinux,
|
||||
windows: initializationSettingsWindows,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
||||
final payload = response.payload;
|
||||
if (payload != null) {
|
||||
if (payload.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(payload);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
|
||||
);
|
||||
}
|
||||
|
||||
class LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
final void Function(AppLifecycleState) onAppLifecycleChanged;
|
||||
|
||||
LifecycleEventHandler({required this.onAppLifecycleChanged});
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onAppLifecycleChanged(state);
|
||||
if (Platform.isWindows) {
|
||||
return windows_notify.initializeLocalNotifications();
|
||||
} else {
|
||||
return universal_notify.initializeLocalNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
StreamSubscription setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) async {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||
// App is focused, show in-app notification
|
||||
log(
|
||||
'[Notification] Showing in-app notification: ${notification.title}',
|
||||
);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top:
|
||||
(!kIsWeb &&
|
||||
(Platform.isMacOS ||
|
||||
Platform.isWindows ||
|
||||
Platform.isLinux))
|
||||
? 28
|
||||
// ignore: use_build_context_synchronously
|
||||
: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// App is in background, show system notification (only on supported platforms)
|
||||
if (!kIsWeb && !Platform.isIOS) {
|
||||
log(
|
||||
'[Notification] Showing system notification: ${notification.title}',
|
||||
);
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'channel_id',
|
||||
'channel_name',
|
||||
channelDescription: 'channel_description',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker',
|
||||
);
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
notification.title,
|
||||
notification.content,
|
||||
notificationDetails,
|
||||
payload: notification.meta['action_uri'] as String?,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (Platform.isWindows) {
|
||||
return windows_notify.setupNotificationListener(context, ref);
|
||||
} else {
|
||||
return universal_notify.setupNotificationListener(context, ref);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(
|
||||
Dio apiClient, {
|
||||
bool detailedErrors = false,
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
String? deviceToken;
|
||||
if (kIsWeb) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken(
|
||||
vapidKey:
|
||||
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
.listen((fcmToken) {
|
||||
_putTokenToRemote(apiClient, fcmToken, 1);
|
||||
})
|
||||
.onError((err) {
|
||||
log("Failed to get firebase cloud messaging push token: $err");
|
||||
});
|
||||
|
||||
if (deviceToken != null) {
|
||||
_putTokenToRemote(
|
||||
if (Platform.isWindows) {
|
||||
return windows_notify.subscribePushNotification(
|
||||
apiClient,
|
||||
deviceToken,
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
||||
detailedErrors: detailedErrors,
|
||||
);
|
||||
} else {
|
||||
return universal_notify.subscribePushNotification(
|
||||
apiClient,
|
||||
detailedErrors: detailedErrors,
|
||||
);
|
||||
} else if (detailedErrors) {
|
||||
throw Exception("Failed to get device token for push notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _putTokenToRemote(
|
||||
Dio apiClient,
|
||||
String token,
|
||||
int provider,
|
||||
) async {
|
||||
await apiClient.put(
|
||||
"/pusher/notifications/subscription",
|
||||
data: {"provider": provider, "device_token": token},
|
||||
);
|
||||
}
|
||||
|
232
lib/services/notify.universal.dart
Normal file
232
lib/services/notify.universal.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
||||
_appLifecycleState = state;
|
||||
}
|
||||
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsIOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const DarwinInitializationSettings initializationSettingsMacOS =
|
||||
DarwinInitializationSettings();
|
||||
|
||||
const LinuxInitializationSettings initializationSettingsLinux =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
|
||||
const WindowsInitializationSettings initializationSettingsWindows =
|
||||
WindowsInitializationSettings(
|
||||
appName: 'Island',
|
||||
appUserModelId: 'dev.solsynth.solian',
|
||||
guid: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
const InitializationSettings initializationSettings = InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
iOS: initializationSettingsIOS,
|
||||
macOS: initializationSettingsMacOS,
|
||||
linux: initializationSettingsLinux,
|
||||
windows: initializationSettingsWindows,
|
||||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {
|
||||
final payload = response.payload;
|
||||
if (payload != null) {
|
||||
if (payload.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(payload);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
|
||||
);
|
||||
}
|
||||
|
||||
class LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
final void Function(AppLifecycleState) onAppLifecycleChanged;
|
||||
|
||||
LifecycleEventHandler({required this.onAppLifecycleChanged});
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onAppLifecycleChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) async {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||
// App is focused, show in-app notification
|
||||
log(
|
||||
'[Notification] Showing in-app notification: ${notification.title}',
|
||||
);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top:
|
||||
(!kIsWeb &&
|
||||
(Platform.isMacOS ||
|
||||
Platform.isWindows ||
|
||||
Platform.isLinux))
|
||||
? 28
|
||||
// ignore: use_build_context_synchronously
|
||||
: MediaQuery.of(context).padding.top + 16,
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// App is in background, show system notification (only on supported platforms)
|
||||
if (!kIsWeb && !Platform.isIOS) {
|
||||
log(
|
||||
'[Notification] Showing system notification: ${notification.title}',
|
||||
);
|
||||
|
||||
// Use flutter_local_notifications for universal platforms
|
||||
const AndroidNotificationDetails androidNotificationDetails =
|
||||
AndroidNotificationDetails(
|
||||
'channel_id',
|
||||
'channel_name',
|
||||
channelDescription: 'channel_description',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
ticker: 'ticker',
|
||||
);
|
||||
const NotificationDetails notificationDetails = NotificationDetails(
|
||||
android: androidNotificationDetails,
|
||||
);
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
notification.title,
|
||||
notification.content,
|
||||
notificationDetails,
|
||||
payload: notification.meta['action_uri'] as String?,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
'[Notification] Skipping system notification for unsupported platform: ${notification.title}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(
|
||||
Dio apiClient, {
|
||||
bool detailedErrors = false,
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
String? deviceToken;
|
||||
if (kIsWeb) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken(
|
||||
vapidKey:
|
||||
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
.listen((fcmToken) {
|
||||
_putTokenToRemote(apiClient, fcmToken, 1);
|
||||
})
|
||||
.onError((err) {
|
||||
log("Failed to get firebase cloud messaging push token: $err");
|
||||
});
|
||||
|
||||
if (deviceToken != null) {
|
||||
_putTokenToRemote(
|
||||
apiClient,
|
||||
deviceToken,
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
||||
);
|
||||
} else if (detailedErrors) {
|
||||
throw Exception("Failed to get device token for push notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _putTokenToRemote(
|
||||
Dio apiClient,
|
||||
String token,
|
||||
int provider,
|
||||
) async {
|
||||
await apiClient.put(
|
||||
"/pusher/notifications/subscription",
|
||||
data: {"provider": provider, "device_token": token},
|
||||
);
|
||||
}
|
176
lib/services/notify.windows.dart
Normal file
176
lib/services/notify.windows.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:windows_notification/windows_notification.dart'
|
||||
as windows_notification;
|
||||
import 'package:windows_notification/notification_message.dart';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
// Windows notification instance
|
||||
windows_notification.WindowsNotification? windowsNotification;
|
||||
|
||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
||||
|
||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
||||
_appLifecycleState = state;
|
||||
}
|
||||
|
||||
Future<void> initializeLocalNotifications() async {
|
||||
// Initialize Windows notification for Windows platform
|
||||
windowsNotification = windows_notification.WindowsNotification(
|
||||
applicationId: 'dev.solsynth.solian',
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged),
|
||||
);
|
||||
}
|
||||
|
||||
class LifecycleEventHandler extends WidgetsBindingObserver {
|
||||
final void Function(AppLifecycleState) onAppLifecycleChanged;
|
||||
|
||||
LifecycleEventHandler({required this.onAppLifecycleChanged});
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
onAppLifecycleChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) async {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
if (_appLifecycleState == AppLifecycleState.resumed) {
|
||||
// App is focused, show in-app notification
|
||||
log(
|
||||
'[Notification] Showing in-app notification: ${notification.title}',
|
||||
);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: NotificationCard(notification: notification),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
rootNavigatorKey.currentContext?.push(
|
||||
notification.meta['action_uri'],
|
||||
);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 28, // Windows specific padding
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// App is in background, show Windows system notification
|
||||
log(
|
||||
'[Notification] Showing Windows system notification: ${notification.title}',
|
||||
);
|
||||
|
||||
if (windowsNotification != null) {
|
||||
// Use Windows notification for Windows platform
|
||||
final notificationMessage = NotificationMessage.fromPluginTemplate(
|
||||
DateTime.now().millisecondsSinceEpoch.toString(), // unique id
|
||||
notification.title,
|
||||
notification.content,
|
||||
launch: notification.meta['action_uri'] as String?,
|
||||
);
|
||||
await windowsNotification!.showNotificationPluginTemplate(
|
||||
notificationMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(
|
||||
Dio apiClient, {
|
||||
bool detailedErrors = false,
|
||||
}) async {
|
||||
if (!kIsWeb && Platform.isLinux) {
|
||||
return;
|
||||
}
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
String? deviceToken;
|
||||
if (kIsWeb) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken(
|
||||
vapidKey:
|
||||
"BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU",
|
||||
);
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceToken = await FirebaseMessaging.instance.getToken();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceToken = await FirebaseMessaging.instance.getAPNSToken();
|
||||
}
|
||||
|
||||
FirebaseMessaging.instance.onTokenRefresh
|
||||
.listen((fcmToken) {
|
||||
_putTokenToRemote(apiClient, fcmToken, 1);
|
||||
})
|
||||
.onError((err) {
|
||||
log("Failed to get firebase cloud messaging push token: $err");
|
||||
});
|
||||
|
||||
if (deviceToken != null) {
|
||||
_putTokenToRemote(
|
||||
apiClient,
|
||||
deviceToken,
|
||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
||||
);
|
||||
} else if (detailedErrors) {
|
||||
throw Exception("Failed to get device token for push notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _putTokenToRemote(
|
||||
Dio apiClient,
|
||||
String token,
|
||||
int provider,
|
||||
) async {
|
||||
await apiClient.put(
|
||||
"/pusher/notifications/subscription",
|
||||
data: {"provider": provider, "device_token": token},
|
||||
);
|
||||
}
|
@@ -3,7 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/activity_rpc.dart';
|
||||
import 'package:island/pods/activity/activity_rpc.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/screens/tray_manager.dart';
|
||||
import 'package:island/services/notify.dart';
|
||||
|
@@ -9,6 +9,11 @@ String _parseRemoteError(DioException err) {
|
||||
String? message;
|
||||
if (err.response?.data is String) {
|
||||
message = err.response?.data;
|
||||
} else if (err.response?.data?['message'] != null) {
|
||||
message = <String?>[
|
||||
err.response?.data?['message']?.toString(),
|
||||
err.response?.data?['detail']?.toString(),
|
||||
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
||||
} else if (err.response?.data?['errors'] != null) {
|
||||
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
||||
message = errors.values
|
||||
|
@@ -8,17 +8,26 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
String _parseRemoteError(DioException err) {
|
||||
log('${err.requestOptions.method} ${err.requestOptions.uri} ${err.message}');
|
||||
if (err.response?.data is String) return err.response?.data;
|
||||
if (err.response?.data?['errors'] != null) {
|
||||
String? message;
|
||||
if (err.response?.data is String) {
|
||||
message = err.response?.data;
|
||||
} else if (err.response?.data?['message'] != null) {
|
||||
message = <String?>[
|
||||
err.response?.data?['message']?.toString(),
|
||||
err.response?.data?['detail']?.toString(),
|
||||
].where((e) => e != null).cast<String>().map((e) => e.trim()).join('\n');
|
||||
} else if (err.response?.data?['errors'] != null) {
|
||||
final errors = err.response?.data['errors'] as Map<String, dynamic>;
|
||||
return errors.values
|
||||
message = errors.values
|
||||
.map(
|
||||
(ele) =>
|
||||
(ele as List<dynamic>).map((ele) => ele.toString()).join('\n'),
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
return err.message ?? err.toString();
|
||||
if (message == null || message.isEmpty) message = err.response?.statusMessage;
|
||||
message ??= err.message;
|
||||
return message ?? err.toString();
|
||||
}
|
||||
|
||||
void showErrorAlert(dynamic err) async {
|
||||
|
@@ -7,7 +7,7 @@ part of 'post_award_history_sheet.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$postAwardListNotifierHash() =>
|
||||
r'492ae59a5dbbfb5c98f863f036023193b6e08668';
|
||||
r'834d08f90ef352a2dfb0192455c75b1620e859c2';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
26
pubspec.lock
26
pubspec.lock
@@ -546,7 +546,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.3.3"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
@@ -1281,10 +1281,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
||||
sha256: a45bef33deb24839a51fb85a4d9e504ead2b1ad1c4779d02d09bf6a8857cdd52
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
version: "0.8.13+2"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1449,10 +1449,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
||||
sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.52"
|
||||
version: "1.0.53"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2576,10 +2576,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
||||
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.18"
|
||||
version: "6.3.20"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2765,7 +2765,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: win32
|
||||
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||
@@ -2780,6 +2780,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
windows_notification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: windows_notification
|
||||
sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2806,4 +2814,4 @@ packages:
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.32.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
@@ -76,7 +76,7 @@ dependencies:
|
||||
file_picker: ^10.3.2
|
||||
riverpod_annotation: ^2.6.1
|
||||
image_picker_platform_interface: ^2.11.0
|
||||
image_picker_android: ^0.8.13+1
|
||||
image_picker_android: ^0.8.13+2
|
||||
super_context_menu: ^0.9.1
|
||||
modal_bottom_sheet: ^3.0.0
|
||||
firebase_messaging: ^16.0.1
|
||||
@@ -147,6 +147,9 @@ dependencies:
|
||||
slide_countdown: ^2.0.2
|
||||
shelf: ^1.4.2
|
||||
shelf_web_socket: ^3.0.0
|
||||
windows_notification: ^1.3.0
|
||||
win32: ^5.14.0
|
||||
ffi: ^2.1.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
; ==================================================
|
||||
#define AppVersion "3.2.0"
|
||||
#define BuildNumber "124"
|
||||
#define BuildNumber "132"
|
||||
; ==================================================
|
||||
|
||||
#define FullVersion AppVersion + "." + BuildNumber
|
||||
|
@@ -31,6 +31,7 @@
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <volume_controller/volume_controller_plugin_c_api.h>
|
||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||
@@ -83,4 +84,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
VolumeControllerPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
||||
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
volume_controller
|
||||
windows_notification
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
Reference in New Issue
Block a user