Compare commits

..

25 Commits

Author SHA1 Message Date
c7f9da0dee ⬆️ Upgrade deps and flutter 2025-09-21 02:06:34 +08:00
LittleSheep
a243cda1df 🔀 Merge pull request #178 from Texas0295/v3
[FIX] chat: fix message pagination logic
2025-09-17 22:17:47 +08:00
Texas0295
7b238f32fd [FIX] chat: fix message pagination logic
`loadInitial()` fetched 100 messages while `_page_size` was 20,
so `_has_more` turned false too early and history stopped around
10h back. `loadMore()` also used `_currentPage * _page_size`
for offset, causing duplicates.

Use `_page_size` for initial load and rely on current message
count as offset. This removes `_currentPage` entirely and lets
older messages load correctly.

Signed-off-by: Texas0295 <kimura@texas0295.top>
2025-09-17 19:36:51 +08:00
313af28d7f 👽 Update service usage 2025-09-17 00:48:01 +08:00
c64e1e208c Desktop device name, close #7 2025-09-14 19:38:01 +08:00
c9b07a9a2a 🐛 Fix errors 2025-09-14 17:38:11 +08:00
55c0e355f1 ♻️ Refactor ICP server to make it available across platform 2025-09-13 20:32:39 +08:00
be414891ec 🐛 Fix pipe path 2025-09-13 17:59:33 +08:00
787876ab6a 🗑️ Removed unused method 2025-09-13 15:57:45 +08:00
8578cde620 🐛 Fix expired at must non null 2025-09-13 15:46:07 +08:00
14d55d45a8 🐛 Fix ipc server on web 2025-09-13 15:31:05 +08:00
724391584e 🐛 Fix and optimize alert 2025-09-13 15:31:05 +08:00
3a5e45808a 🐛 Fix firebase analytics not available on windows 2025-09-13 15:29:38 +08:00
488055955c ♻️ Splitting up ipc servers in unix and windows 2025-09-13 12:59:54 +08:00
LittleSheep
313ebc64cc 🔀 Merge pull request #177 from Texas0295/v3
[FIX] tray: ensure Show Window works reliably on Linux/Wayland
2025-09-12 21:07:56 +08:00
Texas0295
1ed8b1d0c1 [FIX] tray: ensure Show Window works reliably on Linux/Wayland
Avoid relying on appWindow.isVisible, which is not trustworthy under

bitsdojo on Linux/Wayland. Instead, always run show → restore → show

sequence to guarantee window is re-mapped and raised.


Signed-off-by: Texas0295<kimura@texas0295.top>
2025-09-12 13:26:15 +08:00
4af816d931 🐛 Fix windows rpc 2025-09-11 01:15:52 +08:00
1c058a4323 ♻️ Better windows support 2025-09-11 01:06:58 +08:00
461ed1fcda ♻️ FFI windows rpc ipc implmentation 2025-09-11 00:56:26 +08:00
5363afa558 🐛 Trying to fix windows rpc ipc 2025-09-11 00:33:44 +08:00
f0d2737da8 Windows RPC IPC 2025-09-11 00:23:14 +08:00
1f2f80aa3e 🐛 Trying to fix windows notification issue 2025-09-10 23:40:19 +08:00
240a872e65 Rollback windows gha changes 2025-09-10 23:12:57 +08:00
c1ec6f0849 Merge branch 'v3' of https://github.com/Solsynth/Solian into v3 2025-09-10 22:48:54 +08:00
ab42686d4d 🔨 Trying to fix windows build issue 2025-09-10 22:48:50 +08:00
36 changed files with 1139 additions and 686 deletions

View File

@@ -181,7 +181,7 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
if (!kIsWeb && Platform.isLinux) {
if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
return null;
}

View File

@@ -14,11 +14,11 @@ sealed class AppToken with _$AppToken {
@freezed
sealed class GeoIpLocation with _$GeoIpLocation {
const factory GeoIpLocation({
required double latitude,
required double longitude,
required String countryCode,
required String country,
required String city,
required double? latitude,
required double? longitude,
required String? countryCode,
required String? country,
required String? city,
}) = _GeoIpLocation;
factory GeoIpLocation.fromJson(Map<String, dynamic> json) =>
@@ -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,

View File

@@ -272,7 +272,7 @@ as String,
/// @nodoc
mixin _$GeoIpLocation {
double get latitude; double get longitude; String get countryCode; String get country; String get city;
double? get latitude; double? get longitude; String? get countryCode; String? get country; String? get city;
/// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -305,7 +305,7 @@ abstract mixin class $GeoIpLocationCopyWith<$Res> {
factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl;
@useResult
$Res call({
double latitude, double longitude, String countryCode, String country, String city
double? latitude, double? longitude, String? countryCode, String? country, String? city
});
@@ -322,14 +322,14 @@ class _$GeoIpLocationCopyWithImpl<$Res>
/// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
return _then(_self.copyWith(
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
as String,
latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -411,7 +411,7 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double latitude, double longitude, String countryCode, String country, String city)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( double? latitude, double? longitude, String? countryCode, String? country, String? city)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _GeoIpLocation() when $default != null:
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
@@ -432,7 +432,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double latitude, double longitude, String countryCode, String country, String city) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( double? latitude, double? longitude, String? countryCode, String? country, String? city) $default,) {final _that = this;
switch (_that) {
case _GeoIpLocation():
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);}
@@ -449,7 +449,7 @@ return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double latitude, double longitude, String countryCode, String country, String city)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( double? latitude, double? longitude, String? countryCode, String? country, String? city)? $default,) {final _that = this;
switch (_that) {
case _GeoIpLocation() when $default != null:
return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _:
@@ -467,11 +467,11 @@ class _GeoIpLocation implements GeoIpLocation {
const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city});
factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json);
@override final double latitude;
@override final double longitude;
@override final String countryCode;
@override final String country;
@override final String city;
@override final double? latitude;
@override final double? longitude;
@override final String? countryCode;
@override final String? country;
@override final String? city;
/// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values.
@@ -506,7 +506,7 @@ abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopy
factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl;
@override @useResult
$Res call({
double latitude, double longitude, String countryCode, String country, String city
double? latitude, double? longitude, String? countryCode, String? country, String? city
});
@@ -523,14 +523,14 @@ class __$GeoIpLocationCopyWithImpl<$Res>
/// Create a copy of GeoIpLocation
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? latitude = freezed,Object? longitude = freezed,Object? countryCode = freezed,Object? country = freezed,Object? city = freezed,}) {
return _then(_GeoIpLocation(
latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
as String,
latitude: freezed == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable
as double?,longitude: freezed == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable
as double?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,country: freezed == country ? _self.country : country // ignore: cast_nullable_to_non_nullable
as String?,city: freezed == city ? _self.city : city // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -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

View File

@@ -15,11 +15,11 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{
_GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) =>
_GeoIpLocation(
latitude: (json['latitude'] as num).toDouble(),
longitude: (json['longitude'] as num).toDouble(),
countryCode: json['country_code'] as String,
country: json['country'] as String,
city: json['city'] as String,
latitude: (json['latitude'] as num?)?.toDouble(),
longitude: (json['longitude'] as num?)?.toDouble(),
countryCode: json['country_code'] as String?,
country: json['country'] as String?,
city: json['city'] as String?,
);
Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) =>
@@ -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(),

View File

@@ -9,7 +9,9 @@ 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';
const String kRpcLogPrefix = 'arRPC.websocket';
const String kRpcIpcLogPrefix = 'arRPC.ipc';
@@ -43,14 +45,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 64636472
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 +60,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 +82,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 +91,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 +106,24 @@ 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);
_ipcServer = MultiPlatformIpcServer();
_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 +133,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 +158,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 +202,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 +230,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 +240,6 @@ class ActivityRpcServer {
case IpcTypes.pong:
developer.log('IPC pong received', name: kRpcIpcLogPrefix);
// Handle pong if needed
break;
case IpcTypes.handshake:
@@ -388,13 +268,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 +283,6 @@ class ActivityRpcServer {
return;
}
// Validate client ID
if (clientId.isEmpty) {
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
socket.closeWithCode(IpcErrorCodes.invalidClientId);
@@ -413,7 +291,6 @@ class ActivityRpcServer {
socket.clientId = clientId;
// Notify handler of new connection
handlers['connection']?.call(socket);
}
}
@@ -432,74 +309,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 +331,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 +362,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 +382,7 @@ final rpcServerStateProvider =
},
},
'evt': 'READY',
'nonce': '12345', // Should be dynamic
'nonce': '12345',
});
},
'message': (socket, dynamic data) async {
@@ -583,7 +390,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 +400,6 @@ final rpcServerStateProvider =
name: kRpcLogPrefix,
);
}
// Echo back success
socket.send({
'cmd': 'SET_ACTIVITY',
'data': data['args']['activity'],

View File

@@ -0,0 +1,297 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:typed_data';
import 'package:dart_ipc/dart_ipc.dart';
import 'package:path/path.dart' as path;
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;
}
}
// Multiplatform IPC Server implementation using dart_ipc
class MultiPlatformIpcServer extends IpcServer {
StreamSubscription? _serverSubscription;
@override
Future<void> start() async {
try {
final ipcPath = Platform.isWindows
? r'\\.\pipe\discord-ipc-0'
: await _findAvailableUnixIpcPath();
final serverSocket = await bind(ipcPath);
developer.log(
'IPC listening at $ipcPath',
name: kRpcIpcLogPrefix,
);
_serverSubscription = serverSocket.listen((socket) {
final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
addSocket(socketWrapper);
developer.log(
'New IPC connection!',
name: kRpcIpcLogPrefix,
);
_handleIpcData(socketWrapper);
});
} catch (e) {
throw Exception('Failed to start IPC server: $e');
}
}
@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();
_serverSubscription?.cancel();
}
// Handle incoming IPC data
void _handleIpcData(MultiPlatformIpcSocketWrapper socket) {
final startTime = DateTime.now();
socket.socket.listen((data) {
final readStart = DateTime.now();
socket.addData(data);
final readDuration = DateTime.now().difference(readStart).inMicroseconds;
developer.log(
'Read data took $readDuration microseconds',
name: kRpcIpcLogPrefix,
);
final packets = socket.readPackets();
for (final packet in packets) {
handlePacket?.call(socket, packet, {});
}
}, onDone: () {
developer.log('IPC connection closed', name: kRpcIpcLogPrefix);
socket.close();
}, onError: (e) {
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
});
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
developer.log(
'_handleIpcData took $totalDuration microseconds',
name: kRpcIpcLogPrefix,
);
}
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 for Unix-like systems
Future<String> _findAvailableUnixIpcPath() 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 bind(socketPath);
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',
);
}
}
// Multiplatform IPC Socket Wrapper
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {
final dynamic socket;
MultiPlatformIpcSocketWrapper(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();
}
}

View File

@@ -0,0 +1,61 @@
// 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 MultiPlatformIpcServer extends IpcServer {}
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {}

View File

@@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final user = SnAccount.fromJson(response.data);
state = AsyncValue.data(user);
if (kIsWeb || !Platform.isLinux) {
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
FirebaseAnalytics.instance.setUserId(id: user.id);
}
} catch (error, stackTrace) {
@@ -44,7 +44,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
: 'failedToLoadUserInfoNetwork')
.tr()
.trim(),
'${error.response!.statusCode}\n${error.response?.headers}',
'${error.response?.statusCode ?? 'Network Error'}\n${error.response?.headers}',
jsonEncode(error.response?.data),
].join('\n\n'),
iconStyle: IconStyle.error,
@@ -87,7 +87,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
final prefs = _ref.read(sharedPreferencesProvider);
await prefs.remove(kTokenPairStoreKey);
_ref.invalidate(tokenProvider);
if (kIsWeb || !Platform.isLinux) {
if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) {
FirebaseAnalytics.instance.setUserId(id: null);
}
}

View File

@@ -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) {

View File

@@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/udid.native.dart';
import 'package:island/services/udid.dart' as udid;
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart';
@@ -68,7 +68,7 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
try {
final deviceInfoPlugin = DeviceInfoPlugin();
_deviceInfo = await deviceInfoPlugin.deviceInfo;
_deviceUdid = await getUdid();
_deviceUdid = await udid.getUdid();
if (mounted) {
setState(() {});
}
@@ -174,12 +174,20 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
context,
title: 'Device Information',
children: [
_buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value:
_deviceInfo?.data['name'] ?? 'unknown'.tr(),
FutureBuilder<String>(
future: udid.getDeviceName(),
builder: (context, snapshot) {
final value =
snapshot.hasData
? snapshot.data!
: 'unknown'.tr();
return _buildInfoItem(
context,
icon: Symbols.label,
label: 'aboutDeviceName'.tr(),
value: value,
);
},
),
_buildInfoItem(
context,

View File

@@ -1,9 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:animations/animations.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
@@ -42,22 +39,6 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm),
};
Future<String?> getDeviceName() async {
if (kIsWeb) return null;
String? name;
if (Platform.isIOS) {
final deviceInfo = await DeviceInfoPlugin().iosInfo;
name = deviceInfo.name;
} else if (Platform.isAndroid) {
final deviceInfo = await DeviceInfoPlugin().androidInfo;
name = deviceInfo.name;
} else if (Platform.isWindows) {
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
name = deviceInfo.computerName;
}
return name;
}
class LoginScreen extends HookConsumerWidget {
const LoginScreen({super.key});

View File

@@ -289,7 +289,6 @@ class MessagesNotifier extends _$MessagesNotifier {
bool? _withAttachments;
late final String _roomId;
int _currentPage = 0;
static const int _pageSize = 20;
bool _hasMore = true;
bool _isSyncing = false;
@@ -565,47 +564,46 @@ class MessagesNotifier extends _$MessagesNotifier {
}
}
Future<void> loadInitial() async {
developer.log('Loading initial messages', name: 'MessagesNotifier');
if (_searchQuery == null || _searchQuery!.isEmpty) {
syncMessages();
}
final messages = await _getCachedMessages(offset: 0, take: 100);
_currentPage = 0;
_hasMore = messages.length == _pageSize;
state = AsyncValue.data(messages);
Future<void> loadInitial() async {
developer.log('Loading initial messages', name: 'MessagesNotifier');
if (_searchQuery == null || _searchQuery!.isEmpty) {
syncMessages();
}
Future<void> loadMore() async {
if (!_hasMore || state is AsyncLoading) return;
developer.log('Loading more messages', name: 'MessagesNotifier');
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
try {
final currentMessages = state.value ?? [];
_currentPage++;
final newMessages = await listMessages(
offset: _currentPage * _pageSize,
take: _pageSize,
);
_hasMore = messages.length == _pageSize;
if (newMessages.isEmpty || newMessages.length < _pageSize) {
_hasMore = false;
}
state = AsyncValue.data(messages);
}
state = AsyncValue.data(
_sortMessages([...currentMessages, ...newMessages]),
);
} catch (err, stackTrace) {
developer.log(
'Error loading more messages',
name: 'MessagesNotifier',
error: err,
stackTrace: stackTrace,
);
showErrorAlert(err);
_currentPage--;
Future<void> loadMore() async {
if (!_hasMore || state is AsyncLoading) return;
developer.log('Loading more messages', name: 'MessagesNotifier');
try {
final currentMessages = state.value ?? [];
final offset = currentMessages.length;
final newMessages = await listMessages(offset: offset, take: _pageSize);
if (newMessages.isEmpty || newMessages.length < _pageSize) {
_hasMore = false;
}
state = AsyncValue.data(
_sortMessages([...currentMessages, ...newMessages]),
);
} catch (err, stackTrace) {
developer.log(
'Error loading more messages',
name: 'MessagesNotifier',
error: err,
stackTrace: stackTrace,
);
showErrorAlert(err);
}
}
Future<void> sendMessage(
String content,

View File

@@ -39,7 +39,7 @@ class NotificationUnreadCountNotifier
try {
final client = ref.read(apiClientProvider);
final response = await client.get('/pusher/notifications/count');
final response = await client.get('/ring/notifications/count');
return (response.data as num).toInt();
} catch (_) {
return 0;
@@ -89,7 +89,7 @@ class NotificationListNotifier extends _$NotificationListNotifier
final queryParams = {'offset': offset, 'take': _pageSize};
final response = await client.get(
'/pusher/notifications',
'/ring/notifications',
queryParameters: queryParams,
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
@@ -121,7 +121,7 @@ class NotificationScreen extends HookConsumerWidget {
Future<void> markAllRead() async {
showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider);
await apiClient.post('/pusher/notifications/all/read');
await apiClient.post('/ring/notifications/all/read');
if (!context.mounted) return;
hideLoadingModal(context);
ref.invalidate(notificationListNotifierProvider);

View File

@@ -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';

View File

@@ -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();

View File

@@ -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},
);
}

View 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(
"/ring/notifications/subscription",
data: {"provider": provider, "device_token": token},
);
}

View 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(
"/ring/notifications/subscription",
data: {"provider": provider, "device_token": token},
);
}

View File

@@ -14,7 +14,7 @@ Future<void> initializeTzdb() async {
}
Future<String> getMachineTz() async {
return await FlutterTimezone.getLocalTimezone();
return (await FlutterTimezone.getLocalTimezone()).identifier;
}
List<String> getAvailableTz() {

View File

@@ -13,7 +13,7 @@ Future<void> initializeTzdb() async {
}
Future<String> getMachineTz() async {
return await FlutterTimezone.getLocalTimezone();
return (await FlutterTimezone.getLocalTimezone()).identifier;
}
List<String> getAvailableTz() {

View File

@@ -1 +1,3 @@
export 'udid.native.dart' if (dart.library.html) 'udid.web.dart';
export 'udid.native.dart'
if (dart.library.html) 'udid.web.dart'
if (dart.library.io) 'udid.native.dart';

View File

@@ -1,3 +1,6 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_udid/flutter_udid.dart';
String? _cachedUdid;
@@ -9,3 +12,18 @@ Future<String> getUdid() async {
_cachedUdid = await FlutterUdid.consistentUdid;
return _cachedUdid!;
}
Future<String> getDeviceName() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
return androidInfo.device;
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
return iosInfo.name;
} else if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
return Platform.localHostname;
} else {
return 'unknown'.tr();
}
}

View File

@@ -9,3 +9,18 @@ Future<String> getUdid() async {
final hash = sha256.convert(bytes);
return hash.toString();
}
Future<String> getDeviceName() async {
final userAgent = window.navigator.userAgent;
if (userAgent.contains('Chrome') && !userAgent.contains('Edg')) {
return 'Chrome';
} else if (userAgent.contains('Firefox')) {
return 'Firefox';
} else if (userAgent.contains('Safari') && !userAgent.contains('Chrome')) {
return 'Safari';
} else if (userAgent.contains('Edg')) {
return 'Edge';
} else {
return 'Browser';
}
}

View File

@@ -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';

View File

@@ -228,7 +228,7 @@ class CheckInWidget extends HookConsumerWidget {
),
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 3,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
@@ -244,7 +244,7 @@ class CheckInWidget extends HookConsumerWidget {
loading: () => Text('checkInNone').tr().fontSize(15).bold(),
error: (err, stack) => Text('error').tr().fontSize(15).bold(),
),
),
).padding(right: 4),
IconButton.outlined(
iconSize: 16,
visualDensity: const VisualDensity(

View File

@@ -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

View File

@@ -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 {

View File

@@ -7,7 +7,7 @@ part of 'post_award_history_sheet.dart';
// **************************************************************************
String _$postAwardListNotifierHash() =>
r'492ae59a5dbbfb5c98f863f036023193b6e08668';
r'834d08f90ef352a2dfb0192455c75b1620e859c2';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -111,9 +111,9 @@ PODS:
- flutter_udid (0.0.1):
- FlutterMacOS
- SAMKeychain
- flutter_webrtc (1.1.0):
- flutter_webrtc (1.2.0):
- FlutterMacOS
- WebRTC-SDK (= 137.7151.03)
- WebRTC-SDK (= 137.7151.04)
- FlutterMacOS (1.0.0)
- gal (1.0.0):
- Flutter
@@ -175,7 +175,7 @@ PODS:
- livekit_client (2.5.0):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 137.7151.03)
- WebRTC-SDK (= 137.7151.04)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -247,7 +247,7 @@ PODS:
- FlutterMacOS
- wakelock_plus (0.0.1):
- FlutterMacOS
- WebRTC-SDK (137.7151.03)
- WebRTC-SDK (137.7151.04)
DEPENDENCIES:
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
@@ -419,17 +419,17 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: 1ce7fe9a42f085286378355a575e682edd7f114d
flutter_webrtc: 718eae22a371cd94e5d56aa4f301443ebc5bb737
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: 5a5c0f1081978542bbf9a986c7ac9bffcdb73906
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
livekit_client: 95b4a47f51f98a8be3a181c3fa251be7823dddd4
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
@@ -451,8 +451,8 @@ SPEC CHECKSUMS:
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b
WebRTC-SDK: 40d4f5ba05cadff14e4db5614aec402a633f007e
PODFILE CHECKSUM: 346bfb2deb41d4a6ebd6f6799f92188bde2d246f

View File

@@ -6,8 +6,6 @@
<array>
<string>Default</string>
</array>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>

View File

@@ -6,8 +6,6 @@
<array>
<string>Default</string>
</array>
<key>com.apple.developer.device-information.user-assigned-device-name</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.device.audio-input</key>

View File

@@ -285,10 +285,10 @@ packages:
dependency: transitive
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev"
source: hosted
version: "4.10.1"
version: "4.11.0"
collection:
dependency: "direct main"
description:
@@ -333,10 +333,10 @@ packages:
dependency: "direct main"
description:
name: croppy
sha256: "2a69059d9ec007b79d6a494854094b2e3c0a4f7ed609cf55a4805c9de9ec171d"
sha256: ca70a77cd5a981172d69382a7b43629d15d6c868475b2bbb45efce32cfc58b86
url: "https://pub.dev"
source: hosted
version: "1.3.6"
version: "1.4.0"
cross_file:
dependency: "direct main"
description:
@@ -401,6 +401,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.7.0"
dart_ipc:
dependency: "direct main"
description:
name: dart_ipc
sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dart_style:
dependency: transitive
description:
@@ -413,10 +421,10 @@ packages:
dependency: transitive
description:
name: dart_webrtc
sha256: "3bfa069a8b14a53ba506f6dd529e9b88c878ba0cc238f311051a39bf1e53d075"
sha256: "51bcda4ba5d7dd9e65a309244ce3ac0b58025e6e1f6d7442cee4cd02134ef65f"
url: "https://pub.dev"
source: hosted
version: "1.5.3+hotfix.5"
version: "1.6.0"
dbus:
dependency: transitive
description:
@@ -546,7 +554,7 @@ packages:
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
dependency: "direct main"
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
@@ -565,10 +573,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: e7e16c9d15c36330b94ca0e2ad8cb61f93cd5282d0158c09805aed13b5452f22
sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
url: "https://pub.dev"
source: hosted
version: "10.3.2"
version: "10.3.3"
file_saver:
dependency: "direct main"
description:
@@ -709,10 +717,10 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: d3f82f4a38e33ba23d05a08ff304d7d8b22d2a59a5503f20bd802966e915db89
sha256: "7ca9a40f4eb85949190e54087be8b4d6ac09dc4c54238d782a34cf1f7c011de9"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@@ -906,10 +914,10 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: a9966c850de5e445331b854fa42df96a8020066d67f125a5964cbc6556643f68
sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
url: "https://pub.dev"
source: hosted
version: "19.4.1"
version: "19.4.2"
flutter_local_notifications_linux:
dependency: transitive
description:
@@ -930,10 +938,10 @@ packages:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: ed46d7ae4ec9d19e4c8fa2badac5fe27ba87a3fe387343ce726f927af074ec98
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "1.0.3"
flutter_localizations:
dependency: transitive
description: flutter
@@ -1076,10 +1084,10 @@ packages:
dependency: "direct main"
description:
name: flutter_timezone
sha256: "13b2109ad75651faced4831bf262e32559e44aa549426eab8a597610d385d934"
sha256: ccad42fbb5d01d51d3eb281cc4428fca556cc4063c52bd9fa40f80cd93b8e649
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "5.0.0"
flutter_typeahead:
dependency: "direct main"
description:
@@ -1105,10 +1113,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: "945d0a38b90fbca8257eadb167d8fb9fa7075d9a1939fd2953c10054454d1de2"
sha256: "16ca9e30d428bae3dd32933e875c9f67c5843d1fa726c37cf1fc479eb9294549"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.2.0"
font_awesome_flutter:
dependency: transitive
description:
@@ -1177,10 +1185,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: eb059dfe59f08546e9787f895bd01652076f996bcbf485a8609ef990419ad227
sha256: b1488741c9ce37b72e026377c69a59c47378493156fc38efb5a54f6def3f92a3
url: "https://pub.dev"
source: hosted
version: "16.2.1"
version: "16.2.2"
google_fonts:
dependency: "direct main"
description:
@@ -1281,10 +1289,10 @@ packages:
dependency: "direct main"
description:
name: image_picker_android
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02"
url: "https://pub.dev"
source: hosted
version: "0.8.13+1"
version: "0.8.13+3"
image_picker_for_web:
dependency: transitive
description:
@@ -1393,10 +1401,10 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.1"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
@@ -1433,10 +1441,10 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: "011affc0fca22b2f9b0e8827219dad9948f84f2bf057980693de13039de904c7"
sha256: "4c1663c1e6ac20a743d9a46c7bc71f17e1949db99d245750c68661d554e30cd2"
url: "https://pub.dev"
source: hosted
version: "2.5.0+hotfix.3"
version: "2.5.1"
local_auth:
dependency: "direct main"
description:
@@ -1449,18 +1457,18 @@ 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:
name: local_auth_darwin
sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055"
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
version: "1.6.1"
local_auth_platform_interface:
dependency: transitive
description:
@@ -1537,10 +1545,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: "2cfd19bf1c3016b0de7298eb3d3444fcb6ef093d934deb870ceb946af89cfa58"
sha256: "9e2042673fda5dda0b77e262220b3c34cac5806a3833da85522e41bb27fbf6c0"
url: "https://pub.dev"
source: hosted
version: "4.2872.0"
version: "4.2873.0"
media_kit:
dependency: "direct main"
description:
@@ -1697,10 +1705,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "8.3.1"
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
@@ -1873,10 +1881,10 @@ packages:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
version: "1.5.2"
posix:
dependency: transitive
description:
@@ -1969,10 +1977,10 @@ packages:
dependency: transitive
description:
name: record_android
sha256: "8361a791c9a3fa5c065f0b8b5adb10f12531f8538c86b19474cf7b56ea80d426"
sha256: "854627cd78d8d66190377f98477eee06ca96ab7c9f2e662700daf33dbf7e6673"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
version: "1.4.2"
record_ios:
dependency: transitive
description:
@@ -2137,10 +2145,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1
sha256: "3424e9d5c22fd7f7590254ba09465febd6f8827c8b19a44350de4ac31d92d3a6"
url: "https://pub.dev"
source: hosted
version: "11.1.0"
version: "12.0.0"
share_plus_platform_interface:
dependency: transitive
description:
@@ -2576,10 +2584,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
sha256: "199bc33e746088546a39cc5f36bac5a278c5e53b40cb3196f99e7345fdcfae6b"
url: "https://pub.dev"
source: hosted
version: "6.3.18"
version: "6.3.22"
url_launcher_ios:
dependency: transitive
description:
@@ -2704,18 +2712,18 @@ packages:
dependency: "direct main"
description:
name: wakelock_plus
sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678
sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.4.0"
wakelock_plus_platform_interface:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
url: "https://pub.dev"
source: hosted
version: "1.2.3"
version: "1.3.0"
watcher:
dependency: transitive
description:
@@ -2765,7 +2773,7 @@ packages:
source: hosted
version: "1.3.0"
win32:
dependency: transitive
dependency: "direct main"
description:
name: win32
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
@@ -2780,6 +2788,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 +2822,4 @@ packages:
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.32.0"
flutter: ">=3.35.0"

View File

@@ -39,7 +39,7 @@ dependencies:
flutter_hooks: ^0.21.3+1
hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6
go_router: ^16.2.1
go_router: ^16.2.2
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
@@ -67,29 +67,29 @@ dependencies:
easy_localization: ^3.0.8
flutter_inappwebview: ^6.1.5
animations: ^2.0.11
package_info_plus: ^8.3.1
package_info_plus: ^9.0.0
device_info_plus: ^11.5.0
tus_client_dart:
git: https://github.com/LittleSheep2Code/tus_client.git
cross_file: ^0.3.4+2
image_picker: ^1.2.0
file_picker: ^10.3.2
file_picker: ^10.3.3
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+3
super_context_menu: ^0.9.1
modal_bottom_sheet: ^3.0.0
firebase_messaging: ^16.0.1
flutter_udid: ^4.0.0
firebase_core: ^4.1.0
web_socket_channel: ^3.0.3
material_symbols_icons: ^4.2872.0
material_symbols_icons: ^4.2873.0
drift: ^2.28.1
drift_flutter: ^0.2.6
path: ^1.9.1
collection: ^1.19.1
markdown_editor_plus: ^0.2.15
croppy: ^1.3.6
croppy: ^1.4.0
table_calendar: ^3.2.0
relative_time: ^5.0.0
dropdown_button2: ^2.3.9
@@ -103,7 +103,7 @@ dependencies:
gal: ^2.3.2
dismissible_page: ^1.0.2
super_sliver_list: ^0.4.1
livekit_client: ^2.5.0+hotfix.3
livekit_client: ^2.5.1
pasteboard: ^0.4.0
flutter_colorpicker: ^1.1.0
record: ^6.1.1
@@ -112,15 +112,15 @@ dependencies:
palette_generator: ^0.3.3+7
flutter_popup_card: ^0.0.6
timezone: ^0.10.1
flutter_timezone: ^4.1.1
fl_chart: ^1.1.0
flutter_timezone: ^5.0.0
fl_chart: ^1.1.1
sign_in_with_apple: ^7.0.1
flutter_svg: ^2.2.1
native_exif: ^0.6.2
local_auth: ^2.3.0
flutter_secure_storage: ^9.2.4
flutter_math_fork: ^0.7.4
share_plus: ^11.1.0
share_plus: ^12.0.0
receive_sharing_intent: ^1.8.1
top_snackbar_flutter: ^3.3.0
textfield_tags:
@@ -141,12 +141,16 @@ dependencies:
flutter_card_swiper: ^7.0.2
file_saver: ^0.3.1
tray_manager: ^0.5.1
flutter_webrtc: ^1.1.0
flutter_local_notifications: ^19.4.1
wakelock_plus: ^1.3.2
flutter_webrtc: ^1.2.0
flutter_local_notifications: ^19.4.2
wakelock_plus: ^1.4.0
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
dart_ipc: ^1.0.1
dev_dependencies:
flutter_test:

View File

@@ -1,6 +1,6 @@
; ==================================================
#define AppVersion "3.2.0"
#define BuildNumber "124"
#define BuildNumber "132"
; ==================================================
#define FullVersion AppVersion + "." + BuildNumber

View File

@@ -8,6 +8,7 @@
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <dart_ipc/dart_ipc_plugin_c_api.h>
#include <file_saver/file_saver_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
@@ -31,12 +32,15 @@
#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(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DartIpcPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartIpcPluginCApi"));
FileSaverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSaverPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
@@ -83,4 +87,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
WindowsNotificationPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
}

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
bitsdojo_window_windows
connectivity_plus
dart_ipc
file_saver
file_selector_windows
firebase_core
@@ -28,6 +29,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
tray_manager
url_launcher_windows
volume_controller
windows_notification
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST