🐛 Activity refined
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:dio/dio.dart' hide Response;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/activity.dart';
|
import 'package:island/models/activity.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
import 'package:island/talker.dart';
|
import 'package:island/talker.dart';
|
||||||
import 'package:island/widgets/account/status.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||||
@@ -119,6 +119,11 @@ class ActivityRpcServer {
|
|||||||
_handleIpcPacket(socket, packet);
|
_handleIpcPacket(socket, packet);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set up IPC close handler
|
||||||
|
_ipcServer!.onSocketClose = (socket) {
|
||||||
|
handlers['close']?.call(socket);
|
||||||
|
};
|
||||||
|
|
||||||
await _ipcServer!.start();
|
await _ipcServer!.start();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.log('[$kRpcLogPrefix] IPC server error: $e');
|
talker.log('[$kRpcLogPrefix] IPC server error: $e');
|
||||||
@@ -298,31 +303,37 @@ class ServerState {
|
|||||||
final String status;
|
final String status;
|
||||||
final List<String> activities;
|
final List<String> activities;
|
||||||
final String? currentActivityManualId;
|
final String? currentActivityManualId;
|
||||||
|
final Map<String, dynamic>? currentActivityData;
|
||||||
|
|
||||||
ServerState({
|
ServerState({
|
||||||
required this.status,
|
required this.status,
|
||||||
this.activities = const [],
|
this.activities = const [],
|
||||||
this.currentActivityManualId,
|
this.currentActivityManualId,
|
||||||
|
this.currentActivityData,
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerState copyWith({
|
ServerState copyWith({
|
||||||
String? status,
|
String? status,
|
||||||
List<String>? activities,
|
List<String>? activities,
|
||||||
String? currentActivityManualId,
|
String? currentActivityManualId,
|
||||||
|
Map<String, dynamic>? currentActivityData,
|
||||||
}) {
|
}) {
|
||||||
return ServerState(
|
return ServerState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
activities: activities ?? this.activities,
|
activities: activities ?? this.activities,
|
||||||
currentActivityManualId:
|
currentActivityManualId:
|
||||||
currentActivityManualId ?? this.currentActivityManualId,
|
currentActivityManualId ?? this.currentActivityManualId,
|
||||||
|
currentActivityData: currentActivityData ?? this.currentActivityData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServerStateNotifier extends StateNotifier<ServerState> {
|
class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||||
final ActivityRpcServer server;
|
final ActivityRpcServer server;
|
||||||
|
final Dio apiClient;
|
||||||
|
Timer? _renewalTimer;
|
||||||
|
|
||||||
ServerStateNotifier(this.server)
|
ServerStateNotifier(this.apiClient, this.server)
|
||||||
: super(ServerState(status: 'Server not started'));
|
: super(ServerState(status: 'Server not started'));
|
||||||
|
|
||||||
String? get currentActivityManualId => state.currentActivityManualId;
|
String? get currentActivityManualId => state.currentActivityManualId;
|
||||||
@@ -350,8 +361,37 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
|||||||
state = state.copyWith(activities: [...state.activities, activity]);
|
state = state.copyWith(activities: [...state.activities, activity]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setCurrentActivityManualId(String? id) {
|
void setCurrentActivity(String? id, Map<String, dynamic>? data) {
|
||||||
state = state.copyWith(currentActivityManualId: id);
|
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,
|
ServerStateNotifier,
|
||||||
ServerState
|
ServerState
|
||||||
>((ref) {
|
>((ref) {
|
||||||
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
final server = ActivityRpcServer({});
|
final server = ActivityRpcServer({});
|
||||||
final notifier = ServerStateNotifier(server);
|
final notifier = ServerStateNotifier(apiClient, server);
|
||||||
server.updateHandlers({
|
server.updateHandlers({
|
||||||
'connection': (socket) {
|
'connection': (socket) {
|
||||||
final clientId =
|
final clientId =
|
||||||
@@ -395,8 +436,17 @@ final rpcServerStateProvider = StateNotifierProvider<
|
|||||||
'message': (socket, dynamic data) async {
|
'message': (socket, dynamic data) async {
|
||||||
if (data['cmd'] == 'SET_ACTIVITY') {
|
if (data['cmd'] == 'SET_ACTIVITY') {
|
||||||
final activity = data['args']['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'}');
|
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
|
// https://discord.com/developers/docs/topics/rpc#setactivity-set-activity-argument-structure
|
||||||
final type = switch (activity['type']) {
|
final type = switch (activity['type']) {
|
||||||
0 => 1, // Discord Playing -> Playing
|
0 => 1, // Discord Playing -> Playing
|
||||||
@@ -404,29 +454,31 @@ final rpcServerStateProvider = StateNotifierProvider<
|
|||||||
3 => 2, // Discord Watching -> Listening
|
3 => 2, // Discord Watching -> Listening
|
||||||
_ => 1, // Discord Competing (or null) -> Playing
|
_ => 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 {
|
try {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
final currentId = notifier.currentActivityManualId;
|
|
||||||
final isUpdate = currentId == appId;
|
|
||||||
final activityData = {
|
final activityData = {
|
||||||
'type': type,
|
'type': type,
|
||||||
'manual_id': appId,
|
'manual_id': appId,
|
||||||
'title': activity['name'],
|
'title': title,
|
||||||
'subtitle': activity['details'],
|
'subtitle': subtitle,
|
||||||
'caption': activity['state'],
|
'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,
|
'meta': activity,
|
||||||
'lease_minutes': kPresenseActivityLease,
|
'lease_minutes': kPresenseActivityLease,
|
||||||
};
|
};
|
||||||
if (isUpdate) {
|
|
||||||
await apiClient.put(
|
await apiClient.post('/pass/activities', data: activityData);
|
||||||
'/pass/activities',
|
notifier.setCurrentActivity(appId, activityData);
|
||||||
queryParameters: {'manualId': appId},
|
|
||||||
data: {'lease_minutes': kPresenseActivityLease},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await apiClient.post('/pass/activities', data: activityData);
|
|
||||||
notifier.setCurrentActivityManualId(appId);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.log('Failed to set remote activity status: $e');
|
talker.log('Failed to set remote activity status: $e');
|
||||||
}
|
}
|
||||||
@@ -440,15 +492,14 @@ final rpcServerStateProvider = StateNotifierProvider<
|
|||||||
},
|
},
|
||||||
'close': (socket) async {
|
'close': (socket) async {
|
||||||
notifier.updateStatus('Client disconnected');
|
notifier.updateStatus('Client disconnected');
|
||||||
final appId = socket.clientId;
|
final currentId = notifier.currentActivityManualId;
|
||||||
try {
|
try {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
await apiClient.delete(
|
await apiClient.delete(
|
||||||
'/pass/activities',
|
'/pass/activities',
|
||||||
queryParameters: {'manualId': appId},
|
queryParameters: {'manualId': currentId},
|
||||||
);
|
);
|
||||||
notifier.setCurrentActivityManualId(null);
|
notifier.setCurrentActivity(null, null);
|
||||||
ref.read(currentAccountStatusProvider.notifier).clearStatus();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
talker.log('Failed to unset remote activity status: $e');
|
talker.log('Failed to unset remote activity status: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ abstract class IpcServer {
|
|||||||
Map<String, Function> handlers,
|
Map<String, Function> handlers,
|
||||||
)?
|
)?
|
||||||
handlePacket;
|
handlePacket;
|
||||||
|
|
||||||
|
void Function(IpcSocketWrapper socket)? onSocketClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Abstract base class for IPC socket wrapper
|
// Abstract base class for IPC socket wrapper
|
||||||
@@ -178,6 +180,8 @@ class MultiPlatformIpcServer extends IpcServer {
|
|||||||
},
|
},
|
||||||
onDone: () {
|
onDone: () {
|
||||||
talker.log('IPC connection closed');
|
talker.log('IPC connection closed');
|
||||||
|
removeSocket(socket);
|
||||||
|
onSocketClose?.call(socket);
|
||||||
socket.close();
|
socket.close();
|
||||||
},
|
},
|
||||||
onError: (e) {
|
onError: (e) {
|
||||||
|
|||||||
@@ -1013,7 +1013,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: ActivityPresenceWidget(
|
child: ActivityPresenceWidget(
|
||||||
uname: name,
|
uname: name,
|
||||||
).padding(horizontal: 8),
|
).padding(horizontal: 8, top: 4, bottom: 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ActivityPresenceWidget extends ConsumerWidget {
|
|||||||
return activitiesAsync.when(
|
return activitiesAsync.when(
|
||||||
data:
|
data:
|
||||||
(activities) => Card(
|
(activities) => Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -32,10 +33,13 @@ class ActivityPresenceWidget extends ConsumerWidget {
|
|||||||
'activities',
|
'activities',
|
||||||
).tr().bold().padding(horizontal: 8, vertical: 4),
|
).tr().bold().padding(horizontal: 8, vertical: 4),
|
||||||
if (activities.isEmpty)
|
if (activities.isEmpty)
|
||||||
Row(children: [
|
Row(
|
||||||
const Icon(Symbols.inbox),
|
spacing: 4,
|
||||||
Text('dataEmpty').tr()
|
children: [
|
||||||
],).opacity(0.75),
|
const Icon(Symbols.inbox, size: 16),
|
||||||
|
Text('dataEmpty').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
).opacity(0.75).padding(horizontal: 8),
|
||||||
...activities.map(
|
...activities.map(
|
||||||
(activity) => Card(
|
(activity) => Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -57,11 +61,22 @@ class ActivityPresenceWidget extends ConsumerWidget {
|
|||||||
StreamBuilder(
|
StreamBuilder(
|
||||||
stream: Stream.periodic(const Duration(seconds: 1)),
|
stream: Stream.periodic(const Duration(seconds: 1)),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
final duration = DateTime.now().difference(activity.createdAt);
|
final duration = DateTime.now().difference(
|
||||||
final hours = duration.inHours.toString().padLeft(2, '0');
|
activity.createdAt,
|
||||||
final minutes = (duration.inMinutes % 60).toString().padLeft(2, '0');
|
);
|
||||||
final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
|
final hours = duration.inHours.toString().padLeft(
|
||||||
return Text('$hours:$minutes:$seconds').textColor(Colors.green);
|
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)
|
if (activity.subtitle?.isNotEmpty ?? false)
|
||||||
|
|||||||
Reference in New Issue
Block a user