From 3de73538c7f78611309b87ce55bcc14e79dc1262 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 1 Nov 2025 23:36:05 +0800 Subject: [PATCH] :bug: Activity refined --- lib/pods/activity/activity_rpc.dart | 99 ++++++++++++++++------ lib/pods/activity/ipc_server.dart | 4 + lib/screens/account/profile.dart | 2 +- lib/widgets/account/activity_presence.dart | 33 ++++++-- 4 files changed, 104 insertions(+), 34 deletions(-) diff --git a/lib/pods/activity/activity_rpc.dart b/lib/pods/activity/activity_rpc.dart index cf907c2f..ae83b0b6 100644 --- a/lib/pods/activity/activity_rpc.dart +++ b/lib/pods/activity/activity_rpc.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:dio/dio.dart' hide Response; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/activity.dart'; import 'package:island/pods/network.dart'; import 'package:island/talker.dart'; -import 'package:island/widgets/account/status.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as shelf_io; @@ -119,6 +119,11 @@ class ActivityRpcServer { _handleIpcPacket(socket, packet); }; + // Set up IPC close handler + _ipcServer!.onSocketClose = (socket) { + handlers['close']?.call(socket); + }; + await _ipcServer!.start(); } catch (e) { talker.log('[$kRpcLogPrefix] IPC server error: $e'); @@ -298,31 +303,37 @@ class ServerState { final String status; final List activities; final String? currentActivityManualId; + final Map? currentActivityData; ServerState({ required this.status, this.activities = const [], this.currentActivityManualId, + this.currentActivityData, }); ServerState copyWith({ String? status, List? activities, String? currentActivityManualId, + Map? currentActivityData, }) { return ServerState( status: status ?? this.status, activities: activities ?? this.activities, currentActivityManualId: currentActivityManualId ?? this.currentActivityManualId, + currentActivityData: currentActivityData ?? this.currentActivityData, ); } } class ServerStateNotifier extends StateNotifier { final ActivityRpcServer server; + final Dio apiClient; + Timer? _renewalTimer; - ServerStateNotifier(this.server) + ServerStateNotifier(this.apiClient, this.server) : super(ServerState(status: 'Server not started')); String? get currentActivityManualId => state.currentActivityManualId; @@ -350,8 +361,37 @@ class ServerStateNotifier extends StateNotifier { state = state.copyWith(activities: [...state.activities, activity]); } - void setCurrentActivityManualId(String? id) { - state = state.copyWith(currentActivityManualId: id); + void setCurrentActivity(String? id, Map? data) { + state = state.copyWith(currentActivityManualId: id, currentActivityData: data); + if (id != null && data != null) { + _startRenewal(); + } else { + _stopRenewal(); + } + } + + void _startRenewal() { + _renewalTimer?.cancel(); + const int renewalIntervalSeconds = kPresenseActivityLease * 60 - 30; + _renewalTimer = Timer.periodic(Duration(seconds: renewalIntervalSeconds), (timer) { + _renewActivity(); + }); + } + + void _stopRenewal() { + _renewalTimer?.cancel(); + _renewalTimer = null; + } + + Future _renewActivity() async { + if (state.currentActivityData != null) { + try { + await apiClient.post('/pass/activities', data: state.currentActivityData); + talker.log('Activity lease renewed'); + } catch (e) { + talker.log('Failed to renew activity lease: $e'); + } + } } } @@ -362,8 +402,9 @@ final rpcServerStateProvider = StateNotifierProvider< ServerStateNotifier, ServerState >((ref) { + final apiClient = ref.watch(apiClientProvider); final server = ActivityRpcServer({}); - final notifier = ServerStateNotifier(server); + final notifier = ServerStateNotifier(apiClient, server); server.updateHandlers({ 'connection': (socket) { final clientId = @@ -395,8 +436,17 @@ final rpcServerStateProvider = StateNotifierProvider< 'message': (socket, dynamic data) async { if (data['cmd'] == 'SET_ACTIVITY') { final activity = data['args']['activity']; + final appId = socket.clientId; + + final currentId = notifier.currentActivityManualId; + if (currentId != null && currentId != appId) { + talker.info( + 'Skipped the new SET_ACTIVITY command due to there is one existing...', + ); + return; + } + 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 @@ -404,29 +454,31 @@ final rpcServerStateProvider = StateNotifierProvider< 3 => 2, // Discord Watching -> Listening _ => 1, // Discord Competing (or null) -> Playing }; + final title = activity['name'] ?? activity['assets']?['small_text']; + final subtitle = + activity['details'] ?? activity['assets']?['large_text']; + var imageSmall = activity['assets']?['small_image']; + var imageLarge = activity['assets']?['large_image']; + if (imageSmall != null && !imageSmall!.contains(':')) imageSmall = 'discord:$imageSmall'; + if (imageLarge != null && !imageLarge!.contains(':')) imageLarge = 'discord:$imageLarge'; 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'], + 'title': title, + 'subtitle': subtitle, 'caption': activity['state'], + 'title_url': activity['assets']?['small_text_url'], + 'subtitle_url': activity['assets']?['large_text_url'], + 'small_image': imageSmall, + 'large_image': imageLarge, '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); - } + + await apiClient.post('/pass/activities', data: activityData); + notifier.setCurrentActivity(appId, activityData); } catch (e) { talker.log('Failed to set remote activity status: $e'); } @@ -440,15 +492,14 @@ final rpcServerStateProvider = StateNotifierProvider< }, 'close': (socket) async { notifier.updateStatus('Client disconnected'); - final appId = socket.clientId; + final currentId = notifier.currentActivityManualId; try { final apiClient = ref.watch(apiClientProvider); await apiClient.delete( '/pass/activities', - queryParameters: {'manualId': appId}, + queryParameters: {'manualId': currentId}, ); - notifier.setCurrentActivityManualId(null); - ref.read(currentAccountStatusProvider.notifier).clearStatus(); + notifier.setCurrentActivity(null, null); } catch (e) { talker.log('Failed to unset remote activity status: $e'); } diff --git a/lib/pods/activity/ipc_server.dart b/lib/pods/activity/ipc_server.dart index 6ca91b97..76a9856d 100644 --- a/lib/pods/activity/ipc_server.dart +++ b/lib/pods/activity/ipc_server.dart @@ -79,6 +79,8 @@ abstract class IpcServer { Map handlers, )? handlePacket; + + void Function(IpcSocketWrapper socket)? onSocketClose; } // Abstract base class for IPC socket wrapper @@ -178,6 +180,8 @@ class MultiPlatformIpcServer extends IpcServer { }, onDone: () { talker.log('IPC connection closed'); + removeSocket(socket); + onSocketClose?.call(socket); socket.close(); }, onError: (e) { diff --git a/lib/screens/account/profile.dart b/lib/screens/account/profile.dart index 23fd5d96..212d84e5 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: 8), + ).padding(horizontal: 8, top: 4, bottom: 8), ), ], ), diff --git a/lib/widgets/account/activity_presence.dart b/lib/widgets/account/activity_presence.dart index 6f0b039c..9aaf924f 100644 --- a/lib/widgets/account/activity_presence.dart +++ b/lib/widgets/account/activity_presence.dart @@ -24,6 +24,7 @@ class ActivityPresenceWidget extends ConsumerWidget { return activitiesAsync.when( data: (activities) => Card( + margin: EdgeInsets.zero, child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 8, @@ -32,10 +33,13 @@ class ActivityPresenceWidget extends ConsumerWidget { 'activities', ).tr().bold().padding(horizontal: 8, vertical: 4), if (activities.isEmpty) - Row(children: [ - const Icon(Symbols.inbox), - Text('dataEmpty').tr() - ],).opacity(0.75), + Row( + spacing: 4, + children: [ + const Icon(Symbols.inbox, size: 16), + Text('dataEmpty').tr().fontSize(13), + ], + ).opacity(0.75).padding(horizontal: 8), ...activities.map( (activity) => Card( elevation: 0, @@ -57,11 +61,22 @@ class ActivityPresenceWidget extends ConsumerWidget { 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); + 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)