🐛 Activity refined
This commit is contained in:
@@ -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<String> activities;
|
||||
final String? currentActivityManualId;
|
||||
final Map<String, dynamic>? currentActivityData;
|
||||
|
||||
ServerState({
|
||||
required this.status,
|
||||
this.activities = const [],
|
||||
this.currentActivityManualId,
|
||||
this.currentActivityData,
|
||||
});
|
||||
|
||||
ServerState copyWith({
|
||||
String? status,
|
||||
List<String>? activities,
|
||||
String? currentActivityManualId,
|
||||
Map<String, dynamic>? currentActivityData,
|
||||
}) {
|
||||
return ServerState(
|
||||
status: status ?? this.status,
|
||||
activities: activities ?? this.activities,
|
||||
currentActivityManualId:
|
||||
currentActivityManualId ?? this.currentActivityManualId,
|
||||
currentActivityData: currentActivityData ?? this.currentActivityData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||
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<ServerState> {
|
||||
state = state.copyWith(activities: [...state.activities, activity]);
|
||||
}
|
||||
|
||||
void setCurrentActivityManualId(String? id) {
|
||||
state = state.copyWith(currentActivityManualId: id);
|
||||
void setCurrentActivity(String? id, Map<String, dynamic>? 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<void> _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');
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ abstract class IpcServer {
|
||||
Map<String, Function> 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) {
|
||||
|
||||
@@ -1013,7 +1013,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
SliverToBoxAdapter(
|
||||
child: ActivityPresenceWidget(
|
||||
uname: name,
|
||||
).padding(horizontal: 8),
|
||||
).padding(horizontal: 8, top: 4, bottom: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user