From ba8d5cee094af4466cd604d138abb64c5876e597 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 1 Nov 2025 21:47:34 +0800 Subject: [PATCH] :sparkles: Refined presense activity --- assets/i18n/en-US.json | 6 +- lib/models/activity.dart | 12 +- lib/models/activity.freezed.dart | 102 +++++----- lib/models/activity.g.dart | 29 +-- lib/pods/activity/activity_rpc.dart | 214 ++++++++++----------- lib/pods/activity/activity_rpc.g.dart | 157 +++++++++++++++ lib/screens/account/profile.dart | 2 +- lib/screens/explore.dart | 14 +- lib/screens/explore.g.dart | 19 +- lib/widgets/account/activity_presence.dart | 103 ++++++---- lib/widgets/check_in.dart | 2 +- 11 files changed, 421 insertions(+), 239 deletions(-) create mode 100644 lib/pods/activity/activity_rpc.g.dart diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 611a0941..f2f48767 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1302,5 +1302,9 @@ "aiThought": "AI Thought", "aiThoughtTitle": "Let sn-chan think", "postReferenceUnavailable": "Referenced post is unavailable", - "fabLocation": "FAB Location" + "fabLocation": "FAB Location", + "activities": "Activities", + "presenceTypeGaming": "Playing", + "presenceTypeMusic": "Listening to Music", + "presenceTypeWorkout": "Working out" } diff --git a/lib/models/activity.dart b/lib/models/activity.dart index c2c409a0..897433e1 100644 --- a/lib/models/activity.dart +++ b/lib/models/activity.dart @@ -19,8 +19,8 @@ sealed class SnNotableDay with _$SnNotableDay { } @freezed -sealed class SnActivity with _$SnActivity { - const factory SnActivity({ +sealed class SnTimelineEvent with _$SnTimelineEvent { + const factory SnTimelineEvent({ required String id, required String type, required String resourceIdentifier, @@ -28,10 +28,10 @@ sealed class SnActivity with _$SnActivity { required DateTime createdAt, required DateTime updatedAt, required DateTime? deletedAt, - }) = _SnActivity; + }) = _SnTimelineEvent; - factory SnActivity.fromJson(Map json) => - _$SnActivityFromJson(json); + factory SnTimelineEvent.fromJson(Map json) => + _$SnTimelineEventFromJson(json); } @freezed @@ -79,7 +79,7 @@ sealed class SnEventCalendarEntry with _$SnEventCalendarEntry { sealed class SnPresenceActivity with _$SnPresenceActivity { const factory SnPresenceActivity({ required String id, - required String type, + required int type, required String? manualId, required String? title, required String? subtitle, diff --git a/lib/models/activity.freezed.dart b/lib/models/activity.freezed.dart index 5fdd173e..99383801 100644 --- a/lib/models/activity.freezed.dart +++ b/lib/models/activity.freezed.dart @@ -288,22 +288,22 @@ as List, /// @nodoc -mixin _$SnActivity { +mixin _$SnTimelineEvent { String get id; String get type; String get resourceIdentifier; dynamic get data; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; -/// Create a copy of SnActivity +/// Create a copy of SnTimelineEvent /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -$SnActivityCopyWith get copyWith => _$SnActivityCopyWithImpl(this as SnActivity, _$identity); +$SnTimelineEventCopyWith get copyWith => _$SnTimelineEventCopyWithImpl(this as SnTimelineEvent, _$identity); - /// Serializes this SnActivity to a JSON map. + /// Serializes this SnTimelineEvent to a JSON map. Map toJson(); @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is SnTimelineEvent&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -312,15 +312,15 @@ int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,const Dee @override String toString() { - return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnTimelineEvent(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } } /// @nodoc -abstract mixin class $SnActivityCopyWith<$Res> { - factory $SnActivityCopyWith(SnActivity value, $Res Function(SnActivity) _then) = _$SnActivityCopyWithImpl; +abstract mixin class $SnTimelineEventCopyWith<$Res> { + factory $SnTimelineEventCopyWith(SnTimelineEvent value, $Res Function(SnTimelineEvent) _then) = _$SnTimelineEventCopyWithImpl; @useResult $Res call({ String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt @@ -331,14 +331,14 @@ $Res call({ } /// @nodoc -class _$SnActivityCopyWithImpl<$Res> - implements $SnActivityCopyWith<$Res> { - _$SnActivityCopyWithImpl(this._self, this._then); +class _$SnTimelineEventCopyWithImpl<$Res> + implements $SnTimelineEventCopyWith<$Res> { + _$SnTimelineEventCopyWithImpl(this._self, this._then); - final SnActivity _self; - final $Res Function(SnActivity) _then; + final SnTimelineEvent _self; + final $Res Function(SnTimelineEvent) _then; -/// Create a copy of SnActivity +/// Create a copy of SnTimelineEvent /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { return _then(_self.copyWith( @@ -356,8 +356,8 @@ as DateTime?, } -/// Adds pattern-matching-related methods to [SnActivity]. -extension SnActivityPatterns on SnActivity { +/// Adds pattern-matching-related methods to [SnTimelineEvent]. +extension SnTimelineEventPatterns on SnTimelineEvent { /// A variant of `map` that fallback to returning `orElse`. /// /// It is equivalent to doing: @@ -370,10 +370,10 @@ extension SnActivityPatterns on SnActivity { /// } /// ``` -@optionalTypeArgs TResult maybeMap(TResult Function( _SnActivity value)? $default,{required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap(TResult Function( _SnTimelineEvent value)? $default,{required TResult orElse(),}){ final _that = this; switch (_that) { -case _SnActivity() when $default != null: +case _SnTimelineEvent() when $default != null: return $default(_that);case _: return orElse(); @@ -392,10 +392,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map(TResult Function( _SnActivity value) $default,){ +@optionalTypeArgs TResult map(TResult Function( _SnTimelineEvent value) $default,){ final _that = this; switch (_that) { -case _SnActivity(): +case _SnTimelineEvent(): return $default(_that);} } /// A variant of `map` that fallback to returning `null`. @@ -410,10 +410,10 @@ return $default(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnActivity value)? $default,){ +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SnTimelineEvent value)? $default,){ final _that = this; switch (_that) { -case _SnActivity() when $default != null: +case _SnTimelineEvent() when $default != null: return $default(_that);case _: return null; @@ -433,7 +433,7 @@ return $default(_that);case _: @optionalTypeArgs TResult maybeWhen(TResult Function( String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { -case _SnActivity() when $default != null: +case _SnTimelineEvent() when $default != null: return $default(_that.id,_that.type,_that.resourceIdentifier,_that.data,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return orElse(); @@ -454,7 +454,7 @@ return $default(_that.id,_that.type,_that.resourceIdentifier,_that.data,_that.cr @optionalTypeArgs TResult when(TResult Function( String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { -case _SnActivity(): +case _SnTimelineEvent(): return $default(_that.id,_that.type,_that.resourceIdentifier,_that.data,_that.createdAt,_that.updatedAt,_that.deletedAt);} } /// A variant of `when` that fallback to returning `null` @@ -471,7 +471,7 @@ return $default(_that.id,_that.type,_that.resourceIdentifier,_that.data,_that.cr @optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { -case _SnActivity() when $default != null: +case _SnTimelineEvent() when $default != null: return $default(_that.id,_that.type,_that.resourceIdentifier,_that.data,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return null; @@ -483,9 +483,9 @@ return $default(_that.id,_that.type,_that.resourceIdentifier,_that.data,_that.cr /// @nodoc @JsonSerializable() -class _SnActivity implements SnActivity { - const _SnActivity({required this.id, required this.type, required this.resourceIdentifier, required this.data, required this.createdAt, required this.updatedAt, required this.deletedAt}); - factory _SnActivity.fromJson(Map json) => _$SnActivityFromJson(json); +class _SnTimelineEvent implements SnTimelineEvent { + const _SnTimelineEvent({required this.id, required this.type, required this.resourceIdentifier, required this.data, required this.createdAt, required this.updatedAt, required this.deletedAt}); + factory _SnTimelineEvent.fromJson(Map json) => _$SnTimelineEventFromJson(json); @override final String id; @override final String type; @@ -495,20 +495,20 @@ class _SnActivity implements SnActivity { @override final DateTime updatedAt; @override final DateTime? deletedAt; -/// Create a copy of SnActivity +/// Create a copy of SnTimelineEvent /// with the given fields replaced by the non-null parameter values. @override @JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline') -_$SnActivityCopyWith<_SnActivity> get copyWith => __$SnActivityCopyWithImpl<_SnActivity>(this, _$identity); +_$SnTimelineEventCopyWith<_SnTimelineEvent> get copyWith => __$SnTimelineEventCopyWithImpl<_SnTimelineEvent>(this, _$identity); @override Map toJson() { - return _$SnActivityToJson(this, ); + return _$SnTimelineEventToJson(this, ); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnActivity&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnTimelineEvent&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.resourceIdentifier, resourceIdentifier) || other.resourceIdentifier == resourceIdentifier)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -517,15 +517,15 @@ int get hashCode => Object.hash(runtimeType,id,type,resourceIdentifier,const Dee @override String toString() { - return 'SnActivity(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; + return 'SnTimelineEvent(id: $id, type: $type, resourceIdentifier: $resourceIdentifier, data: $data, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; } } /// @nodoc -abstract mixin class _$SnActivityCopyWith<$Res> implements $SnActivityCopyWith<$Res> { - factory _$SnActivityCopyWith(_SnActivity value, $Res Function(_SnActivity) _then) = __$SnActivityCopyWithImpl; +abstract mixin class _$SnTimelineEventCopyWith<$Res> implements $SnTimelineEventCopyWith<$Res> { + factory _$SnTimelineEventCopyWith(_SnTimelineEvent value, $Res Function(_SnTimelineEvent) _then) = __$SnTimelineEventCopyWithImpl; @override @useResult $Res call({ String id, String type, String resourceIdentifier, dynamic data, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt @@ -536,17 +536,17 @@ $Res call({ } /// @nodoc -class __$SnActivityCopyWithImpl<$Res> - implements _$SnActivityCopyWith<$Res> { - __$SnActivityCopyWithImpl(this._self, this._then); +class __$SnTimelineEventCopyWithImpl<$Res> + implements _$SnTimelineEventCopyWith<$Res> { + __$SnTimelineEventCopyWithImpl(this._self, this._then); - final _SnActivity _self; - final $Res Function(_SnActivity) _then; + final _SnTimelineEvent _self; + final $Res Function(_SnTimelineEvent) _then; -/// Create a copy of SnActivity +/// Create a copy of SnTimelineEvent /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? resourceIdentifier = null,Object? data = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { - return _then(_SnActivity( + return _then(_SnTimelineEvent( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as String,resourceIdentifier: null == resourceIdentifier ? _self.resourceIdentifier : resourceIdentifier // ignore: cast_nullable_to_non_nullable @@ -1429,7 +1429,7 @@ $SnCheckInResultCopyWith<$Res>? get checkInResult { /// @nodoc mixin _$SnPresenceActivity { - String get id; String get type; String? get manualId; String? get title; String? get subtitle; String? get caption; Map? get meta; int get leaseMinutes; DateTime get leaseExpiresAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; + String get id; int get type; String? get manualId; String? get title; String? get subtitle; String? get caption; Map? get meta; int get leaseMinutes; DateTime get leaseExpiresAt; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; /// Create a copy of SnPresenceActivity /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -1462,7 +1462,7 @@ abstract mixin class $SnPresenceActivityCopyWith<$Res> { factory $SnPresenceActivityCopyWith(SnPresenceActivity value, $Res Function(SnPresenceActivity) _then) = _$SnPresenceActivityCopyWithImpl; @useResult $Res call({ - String id, String type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -1483,7 +1483,7 @@ class _$SnPresenceActivityCopyWithImpl<$Res> return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable -as String,manualId: freezed == manualId ? _self.manualId : manualId // ignore: cast_nullable_to_non_nullable +as int,manualId: freezed == manualId ? _self.manualId : manualId // ignore: cast_nullable_to_non_nullable as String?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as String?,subtitle: freezed == subtitle ? _self.subtitle : subtitle // ignore: cast_nullable_to_non_nullable as String?,caption: freezed == caption ? _self.caption : caption // ignore: cast_nullable_to_non_nullable @@ -1576,7 +1576,7 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _SnPresenceActivity() when $default != null: return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: @@ -1597,7 +1597,7 @@ return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_t /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String id, String type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; switch (_that) { case _SnPresenceActivity(): return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} @@ -1614,7 +1614,7 @@ return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_t /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; switch (_that) { case _SnPresenceActivity() when $default != null: return $default(_that.id,_that.type,_that.manualId,_that.title,_that.subtitle,_that.caption,_that.meta,_that.leaseMinutes,_that.leaseExpiresAt,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: @@ -1633,7 +1633,7 @@ class _SnPresenceActivity implements SnPresenceActivity { factory _SnPresenceActivity.fromJson(Map json) => _$SnPresenceActivityFromJson(json); @override final String id; -@override final String type; +@override final int type; @override final String? manualId; @override final String? title; @override final String? subtitle; @@ -1687,7 +1687,7 @@ abstract mixin class _$SnPresenceActivityCopyWith<$Res> implements $SnPresenceAc factory _$SnPresenceActivityCopyWith(_SnPresenceActivity value, $Res Function(_SnPresenceActivity) _then) = __$SnPresenceActivityCopyWithImpl; @override @useResult $Res call({ - String id, String type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, int type, String? manualId, String? title, String? subtitle, String? caption, Map? meta, int leaseMinutes, DateTime leaseExpiresAt, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt }); @@ -1708,7 +1708,7 @@ class __$SnPresenceActivityCopyWithImpl<$Res> return _then(_SnPresenceActivity( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable -as String,manualId: freezed == manualId ? _self.manualId : manualId // ignore: cast_nullable_to_non_nullable +as int,manualId: freezed == manualId ? _self.manualId : manualId // ignore: cast_nullable_to_non_nullable as String?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as String?,subtitle: freezed == subtitle ? _self.subtitle : subtitle // ignore: cast_nullable_to_non_nullable as String?,caption: freezed == caption ? _self.caption : caption // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/activity.g.dart b/lib/models/activity.g.dart index 59ed1194..02db2707 100644 --- a/lib/models/activity.g.dart +++ b/lib/models/activity.g.dart @@ -27,20 +27,21 @@ Map _$SnNotableDayToJson(_SnNotableDay instance) => 'holidays': instance.holidays, }; -_SnActivity _$SnActivityFromJson(Map json) => _SnActivity( - id: json['id'] as String, - type: json['type'] as String, - resourceIdentifier: json['resource_identifier'] as String, - data: json['data'], - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - deletedAt: - json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), -); +_SnTimelineEvent _$SnTimelineEventFromJson(Map json) => + _SnTimelineEvent( + id: json['id'] as String, + type: json['type'] as String, + resourceIdentifier: json['resource_identifier'] as String, + data: json['data'], + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: + json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + ); -Map _$SnActivityToJson(_SnActivity instance) => +Map _$SnTimelineEventToJson(_SnTimelineEvent instance) => { 'id': instance.id, 'type': instance.type, @@ -125,7 +126,7 @@ Map _$SnEventCalendarEntryToJson( _SnPresenceActivity _$SnPresenceActivityFromJson(Map json) => _SnPresenceActivity( id: json['id'] as String, - type: json['type'] as String, + type: (json['type'] as num).toInt(), manualId: json['manual_id'] as String?, title: json['title'] as String?, subtitle: json['subtitle'] as String?, diff --git a/lib/pods/activity/activity_rpc.dart b/lib/pods/activity/activity_rpc.dart index 7e587ef2..cf907c2f 100644 --- a/lib/pods/activity/activity_rpc.dart +++ b/lib/pods/activity/activity_rpc.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/models/account.dart'; import 'package:island/models/activity.dart'; import 'package:island/pods/network.dart'; import 'package:island/talker.dart'; @@ -14,11 +13,11 @@ 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'; -part 'activity_rpc.g.dart'; - // Conditional imports for IPC server - use web stubs on web platform import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart'; +part 'activity_rpc.g.dart'; + const String kRpcLogPrefix = 'arRPC.websocket'; const String kRpcIpcLogPrefix = 'arRPC.ipc'; @@ -125,7 +124,7 @@ class ActivityRpcServer { talker.log('[$kRpcLogPrefix] IPC server error: $e'); } } else { - talker.log('IPC server disabled on macOS or web in production mode'); + talker.log('IPC server disabled on macOS or web'); } } @@ -326,6 +325,8 @@ class ServerStateNotifier extends StateNotifier { ServerStateNotifier(this.server) : super(ServerState(status: 'Server not started')); + String? get currentActivityManualId => state.currentActivityManualId; + Future start() async { if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) { try { @@ -354,114 +355,107 @@ class ServerStateNotifier extends StateNotifier { } } +const kPresenseActivityLease = 5; + // Providers -final rpcServerStateProvider = - StateNotifierProvider((ref) { - final server = ActivityRpcServer({}); - final notifier = ServerStateNotifier(server); - server.updateHandlers({ - 'connection': (socket) { - final clientId = - socket is _WsSocketWrapper - ? socket.clientId - : (socket as IpcSocketWrapper).clientId; - notifier.updateStatus('Client connected (ID: $clientId)'); - socket.send({ - 'cmd': 'DISPATCH', - 'data': { - 'v': 1, - 'config': { - 'cdn_host': 'fake.cdn', - 'api_endpoint': '//fake.api', - 'environment': 'dev', - }, - 'user': { - 'id': 'fake_user_id', - 'username': 'FakeUser', - 'discriminator': '0001', - 'avatar': null, - 'bot': false, - }, - }, - 'evt': 'READY', - 'nonce': '12345', - }); - }, - 'message': (socket, dynamic data) async { - if (data['cmd'] == 'SET_ACTIVITY') { - notifier.addActivity( - 'Activity: ${data['args']['activity']['details'] ?? ''}', - ); - final label = data['args']['activity']['details'] ?? ''; - final appId = socket.clientId; - final meta = data['args']['activity']; - try { - final apiClient = ref.watch(apiClientProvider); - final currentId = notifier.state.currentActivityManualId; - final isUpdate = currentId == appId; - final activityData = { - 'type': 'Gaming', - 'manualId': appId, - 'title': label, - 'meta': meta, - 'leaseMinutes': 30, - }; - if (isUpdate) { - await apiClient.put( - '/pass/activities', - queryParameters: {'manual_id': appId}, - data: {'leaseMinutes': 30}, - ); - } else { - await apiClient.post('/pass/activities', data: activityData); - notifier.setCurrentActivityManualId(appId); - } - final now = DateTime.now(); - final status = SnAccountStatus( - id: 'local_$appId', - attitude: 0, - isOnline: true, - isInvisible: false, - isNotDisturb: false, - isCustomized: true, - label: label, - meta: meta, - clearedAt: null, - accountId: 'me', - createdAt: now, - updatedAt: now, - deletedAt: null, - ); - ref.read(currentAccountStatusProvider.notifier).setStatus(status); - } catch (e) { - talker.log('Failed to set remote activity status: $e'); - } - socket.send({ - 'cmd': 'SET_ACTIVITY', - 'data': data['args']['activity'], - 'evt': null, - 'nonce': data['nonce'], - }); - } - }, - 'close': (socket) async { - notifier.updateStatus('Client disconnected'); - final appId = socket.clientId; - try { - final apiClient = ref.watch(apiClientProvider); - await apiClient.delete( - '/pass/activities', - queryParameters: {'manual_id': appId}, - ); - notifier.setCurrentActivityManualId(null); - ref.read(currentAccountStatusProvider.notifier).clearStatus(); - } catch (e) { - talker.log('Failed to unset remote activity status: $e'); - } +final rpcServerStateProvider = StateNotifierProvider< + ServerStateNotifier, + ServerState +>((ref) { + final server = ActivityRpcServer({}); + final notifier = ServerStateNotifier(server); + server.updateHandlers({ + 'connection': (socket) { + final clientId = + socket is _WsSocketWrapper + ? socket.clientId + : (socket as IpcSocketWrapper).clientId; + notifier.updateStatus('Client connected (ID: $clientId)'); + socket.send({ + 'cmd': 'DISPATCH', + 'data': { + 'v': 1, + 'config': { + 'cdn_host': 'fake.cdn', + 'api_endpoint': '//fake.api', + 'environment': 'dev', + }, + 'user': { + 'id': 'fake_user_id', + 'username': 'FakeUser', + 'discriminator': '0001', + 'avatar': null, + 'bot': false, + }, }, + 'evt': 'READY', + 'nonce': '12345', }); - return notifier; - }); + }, + 'message': (socket, dynamic data) async { + if (data['cmd'] == 'SET_ACTIVITY') { + final activity = data['args']['activity']; + notifier.addActivity('Activity: ${activity['details'] ?? 'Untitled'}'); + final appId = activity['application_id'] ?? socket.clientId; + // https://discord.com/developers/docs/topics/rpc#setactivity-set-activity-argument-structure + final type = switch (activity['type']) { + 0 => 1, // Discord Playing -> Playing + 2 => 2, // Discord Music -> Listening + 3 => 2, // Discord Watching -> Listening + _ => 1, // Discord Competing (or null) -> Playing + }; + try { + final apiClient = ref.watch(apiClientProvider); + final currentId = notifier.currentActivityManualId; + final isUpdate = currentId == appId; + final activityData = { + 'type': type, + 'manual_id': appId, + 'title': activity['name'], + 'subtitle': activity['details'], + 'caption': activity['state'], + 'meta': activity, + 'lease_minutes': kPresenseActivityLease, + }; + if (isUpdate) { + await apiClient.put( + '/pass/activities', + queryParameters: {'manualId': appId}, + data: {'lease_minutes': kPresenseActivityLease}, + ); + } else { + await apiClient.post('/pass/activities', data: activityData); + notifier.setCurrentActivityManualId(appId); + } + } catch (e) { + talker.log('Failed to set remote activity status: $e'); + } + socket.send({ + 'cmd': 'SET_ACTIVITY', + 'data': data['args']['activity'], + 'evt': null, + 'nonce': data['nonce'], + }); + } + }, + 'close': (socket) async { + notifier.updateStatus('Client disconnected'); + final appId = socket.clientId; + try { + final apiClient = ref.watch(apiClientProvider); + await apiClient.delete( + '/pass/activities', + queryParameters: {'manualId': appId}, + ); + notifier.setCurrentActivityManualId(null); + ref.read(currentAccountStatusProvider.notifier).clearStatus(); + } catch (e) { + talker.log('Failed to unset remote activity status: $e'); + } + }, + }); + return notifier; +}); final rpcServerProvider = Provider((ref) { final notifier = ref.watch(rpcServerStateProvider.notifier); @@ -474,7 +468,7 @@ Future> presenceActivities( String uname, ) async { final apiClient = ref.watch(apiClientProvider); - final response = await apiClient.get('/pass/accounts/$uname/activities'); + final response = await apiClient.get('/pass/activities/$uname'); final data = response.data as List; return data.map((json) => SnPresenceActivity.fromJson(json)).toList(); } diff --git a/lib/pods/activity/activity_rpc.g.dart b/lib/pods/activity/activity_rpc.g.dart new file mode 100644 index 00000000..2f168352 --- /dev/null +++ b/lib/pods/activity/activity_rpc.g.dart @@ -0,0 +1,157 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'activity_rpc.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$presenceActivitiesHash() => + r'dcea3cad01b4010c0087f5281413d83a754c2a17'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [presenceActivities]. +@ProviderFor(presenceActivities) +const presenceActivitiesProvider = PresenceActivitiesFamily(); + +/// See also [presenceActivities]. +class PresenceActivitiesFamily + extends Family>> { + /// See also [presenceActivities]. + const PresenceActivitiesFamily(); + + /// See also [presenceActivities]. + PresenceActivitiesProvider call(String uname) { + return PresenceActivitiesProvider(uname); + } + + @override + PresenceActivitiesProvider getProviderOverride( + covariant PresenceActivitiesProvider provider, + ) { + return call(provider.uname); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'presenceActivitiesProvider'; +} + +/// See also [presenceActivities]. +class PresenceActivitiesProvider + extends AutoDisposeFutureProvider> { + /// See also [presenceActivities]. + PresenceActivitiesProvider(String uname) + : this._internal( + (ref) => presenceActivities(ref as PresenceActivitiesRef, uname), + from: presenceActivitiesProvider, + name: r'presenceActivitiesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$presenceActivitiesHash, + dependencies: PresenceActivitiesFamily._dependencies, + allTransitiveDependencies: + PresenceActivitiesFamily._allTransitiveDependencies, + uname: uname, + ); + + PresenceActivitiesProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.uname, + }) : super.internal(); + + final String uname; + + @override + Override overrideWith( + FutureOr> Function(PresenceActivitiesRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: PresenceActivitiesProvider._internal( + (ref) => create(ref as PresenceActivitiesRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + uname: uname, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _PresenceActivitiesProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is PresenceActivitiesProvider && other.uname == uname; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, uname.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin PresenceActivitiesRef + on AutoDisposeFutureProviderRef> { + /// The parameter `uname` of this provider. + String get uname; +} + +class _PresenceActivitiesProviderElement + extends AutoDisposeFutureProviderElement> + with PresenceActivitiesRef { + _PresenceActivitiesProviderElement(super.provider); + + @override + String get uname => (origin as PresenceActivitiesProvider).uname; +} + +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index d2bdfe54..23fd5d96 100644 --- a/lib/screens/account/profile.dart +++ b/lib/screens/account/profile.dart @@ -1013,7 +1013,7 @@ class AccountProfileScreen extends HookConsumerWidget { SliverToBoxAdapter( child: ActivityPresenceWidget( uname: name, - ).padding(horizontal: 4), + ).padding(horizontal: 8), ), ], ), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 8584443d..4529aa52 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -634,7 +634,7 @@ class _DiscoveryActivityItem extends StatelessWidget { } class _ActivityListView extends HookConsumerWidget { - final CursorPagingData data; + final CursorPagingData data; final int widgetCount; final Widget endItemView; final ActivityListNotifier activitiesNotifier; @@ -697,13 +697,13 @@ class _ActivityListView extends HookConsumerWidget { @riverpod class ActivityListNotifier extends _$ActivityListNotifier - with CursorPagingNotifierMixin { + with CursorPagingNotifierMixin { @override - Future> build(String? filter) => + Future> build(String? filter) => fetch(cursor: null); @override - Future> fetch({required String? cursor}) async { + Future> fetch({required String? cursor}) async { final client = ref.read(apiClientProvider); final take = 20; @@ -720,9 +720,9 @@ class ActivityListNotifier extends _$ActivityListNotifier queryParameters: queryParameters, ); - final List items = + final List items = (response.data as List) - .map((e) => SnActivity.fromJson(e as Map)) + .map((e) => SnTimelineEvent.fromJson(e as Map)) .toList(); final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty'; @@ -742,7 +742,7 @@ class ActivityListNotifier extends _$ActivityListNotifier ); } - void updateOne(int index, SnActivity activity) { + void updateOne(int index, SnTimelineEvent activity) { final currentState = state.valueOrNull; if (currentState == null) return; diff --git a/lib/screens/explore.g.dart b/lib/screens/explore.g.dart index e9ce4586..ade2b890 100644 --- a/lib/screens/explore.g.dart +++ b/lib/screens/explore.g.dart @@ -7,7 +7,7 @@ part of 'explore.dart'; // ************************************************************************** String _$activityListNotifierHash() => - r'167021cada54da7c8d8437eef1ffb387a92ea2e3'; + r'a3ad3242f08139bef14a2f0fab6591ce8b3cb9f0'; /// Copied from Dart SDK class _SystemHash { @@ -31,10 +31,11 @@ class _SystemHash { } abstract class _$ActivityListNotifier - extends BuildlessAutoDisposeAsyncNotifier> { + extends + BuildlessAutoDisposeAsyncNotifier> { late final String? filter; - FutureOr> build(String? filter); + FutureOr> build(String? filter); } /// See also [ActivityListNotifier]. @@ -43,7 +44,7 @@ const activityListNotifierProvider = ActivityListNotifierFamily(); /// See also [ActivityListNotifier]. class ActivityListNotifierFamily - extends Family>> { + extends Family>> { /// See also [ActivityListNotifier]. const ActivityListNotifierFamily(); @@ -79,7 +80,7 @@ class ActivityListNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl< ActivityListNotifier, - CursorPagingData + CursorPagingData > { /// See also [ActivityListNotifier]. ActivityListNotifierProvider(String? filter) @@ -110,7 +111,7 @@ class ActivityListNotifierProvider final String? filter; @override - FutureOr> runNotifierBuild( + FutureOr> runNotifierBuild( covariant ActivityListNotifier notifier, ) { return notifier.build(filter); @@ -135,7 +136,7 @@ class ActivityListNotifierProvider @override AutoDisposeAsyncNotifierProviderElement< ActivityListNotifier, - CursorPagingData + CursorPagingData > createElement() { return _ActivityListNotifierProviderElement(this); @@ -158,7 +159,7 @@ class ActivityListNotifierProvider @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element mixin ActivityListNotifierRef - on AutoDisposeAsyncNotifierProviderRef> { + on AutoDisposeAsyncNotifierProviderRef> { /// The parameter `filter` of this provider. String? get filter; } @@ -167,7 +168,7 @@ class _ActivityListNotifierProviderElement extends AutoDisposeAsyncNotifierProviderElement< ActivityListNotifier, - CursorPagingData + CursorPagingData > with ActivityListNotifierRef { _ActivityListNotifierProviderElement(super.provider); diff --git a/lib/widgets/account/activity_presence.dart b/lib/widgets/account/activity_presence.dart index f3c65357..6f0b039c 100644 --- a/lib/widgets/account/activity_presence.dart +++ b/lib/widgets/account/activity_presence.dart @@ -1,7 +1,16 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/models/activity.dart'; import 'package:island/pods/activity/activity_rpc.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:styled_widget/styled_widget.dart'; + +const kPresenseActivityTypes = [ + 'unknown', + 'presenceTypeGaming', + 'presenceTypeMusic', + 'presenceTypeWorkout', +]; class ActivityPresenceWidget extends ConsumerWidget { final String uname; @@ -13,48 +22,64 @@ class ActivityPresenceWidget extends ConsumerWidget { final activitiesAsync = ref.watch(presenceActivitiesProvider(uname)); return activitiesAsync.when( - data: (activities) => _buildActivitiesList(activities), + data: + (activities) => Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 8, + children: [ + Text( + 'activities', + ).tr().bold().padding(horizontal: 8, vertical: 4), + if (activities.isEmpty) + Row(children: [ + const Icon(Symbols.inbox), + Text('dataEmpty').tr() + ],).opacity(0.75), + ...activities.map( + (activity) => Card( + elevation: 0, + shape: RoundedRectangleBorder( + side: BorderSide(color: Colors.grey.shade300, width: 1), + borderRadius: BorderRadius.circular(8), + ), + margin: EdgeInsets.zero, + child: ListTile( + title: Text( + (activity.title?.isEmpty ?? true) + ? 'Untitled Activity' + : activity.title!, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(kPresenseActivityTypes[activity.type]).tr(), + StreamBuilder( + stream: Stream.periodic(const Duration(seconds: 1)), + builder: (context, snapshot) { + final duration = DateTime.now().difference(activity.createdAt); + final hours = duration.inHours.toString().padLeft(2, '0'); + final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0'); + final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); + return Text('$hours:$minutes:$seconds').textColor(Colors.green); + }, + ), + if (activity.subtitle?.isNotEmpty ?? false) + Text(activity.subtitle!), + if (activity.caption?.isNotEmpty ?? false) + Text(activity.caption!), + ], + ), + ), + ), + ), + ], + ).padding(all: 8), + ), loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center(child: Text('Error loading activities: $error')), ); } - - Widget _buildActivitiesList(List activities) { - if (activities.isEmpty) { - return const Center(child: Text('No active activities')); - } - - return ListView.builder( - itemCount: activities.length, - itemBuilder: (context, index) { - final activity = activities[index]; - return Card( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: ListTile( - title: Text(activity.title ?? 'Untitled Activity'), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Type: ${activity.type}'), - if (activity.subtitle != null) Text(activity.subtitle!), - if (activity.caption != null) Text(activity.caption!), - Text( - 'Expires: ${activity.leaseExpiresAt.toLocal().toString()}', - style: const TextStyle(fontSize: 12), - ), - ], - ), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - // TODO: Implement delete functionality - }, - ), - ), - ); - }, - ); - } } diff --git a/lib/widgets/check_in.dart b/lib/widgets/check_in.dart index 9cf55808..26c4fcb1 100644 --- a/lib/widgets/check_in.dart +++ b/lib/widgets/check_in.dart @@ -296,7 +296,7 @@ class CheckInWidget extends HookConsumerWidget { } class CheckInActivityWidget extends StatelessWidget { - final SnActivity item; + final SnTimelineEvent item; const CheckInActivityWidget({super.key, required this.item}); @override