diff --git a/lib/bloc/gebura/gebura_bloc.dart b/lib/bloc/gebura/gebura_bloc.dart index 1bd66da..5357703 100644 --- a/lib/bloc/gebura/gebura_bloc.dart +++ b/lib/bloc/gebura/gebura_bloc.dart @@ -17,6 +17,7 @@ import '../../ffi/ffi_model.dart'; import '../../l10n/l10n.dart'; import '../../model/gebura_model.dart'; import '../../repo/grpc/api_helper.dart'; +import '../../repo/grpc/type_helper.dart'; import '../../repo/local/gebura.dart'; part 'gebura_event.dart'; @@ -88,11 +89,35 @@ class GeburaBloc extends Bloc { } ownedAppInsts.addAll(appInstResp.getData().appInsts); } + final appInstRunTimes = {}; + for (final appInst in ownedAppInsts) { + final runTimeResp = await _api.doRequest( + (client) => client.sumAppInstRunTime, + SumAppInstRunTimeRequest( + appInstId: appInst.id, + timeAggregation: TimeAggregation( + aggregationType: + TimeAggregation_AggregationType.AGGREGATION_TYPE_OVERALL, + timeRange: toPBTimeRange( + DateTime.now().subtract(const Duration(days: 365 * 10)), + DateTime.now()), + ), + ), + ); + if (runTimeResp.status != ApiStatus.success) { + continue; + } + if (runTimeResp.getData().runTimeGroups.isNotEmpty) { + final group = runTimeResp.getData().runTimeGroups.first; + appInstRunTimes[appInst.id] = fromPBDuration(group.duration); + } + } emit(GeburaRefreshLibraryState( state.copyWith( purchasedAppInfos: resp.getData().appInfos, ownedApps: ownedApps, ownedAppInsts: ownedAppInsts, + appInstRunTimes: appInstRunTimes, ), EventStatus.success, msg: resp.error, @@ -372,11 +397,17 @@ class GeburaBloc extends Bloc { msg: S.current.applicationExitAbnormally)); return; } - state.runState![appID] = AppRunState( + final runState = AppRunState( running: false, startTime: DateTime.fromMillisecondsSinceEpoch(start * 1000), endTime: DateTime.fromMillisecondsSinceEpoch(end * 1000), ); + state.runState![appID] = runState; + add(GeburaReportAppRunTimeEvent( + event.appInstID, + runState.startTime!, + runState.endTime!, + )); emit(GeburaRunAppState( state, appID, @@ -738,6 +769,31 @@ class GeburaBloc extends Bloc { msg: resp.error)); add(GeburaRefreshLibraryEvent()); }, transformer: droppable()); + + on((event, emit) async { + emit(GeburaReportAppRunTimeState(state, EventStatus.processing)); + final resp = await _api.doRequest( + (client) => client.addAppInstRunTime, + AddAppInstRunTimeRequest( + appInstId: event.appInstID, + timeRange: toPBTimeRange( + event.startTime, + event.endTime, + ), + ), + ); + if (resp.status != ApiStatus.success) { + emit(GeburaReportAppRunTimeState(state, EventStatus.failed, + msg: resp.error)); + return; + } + emit(GeburaReportAppRunTimeState( + state, + EventStatus.success, + msg: resp.error, + )); + add(GeburaRefreshLibraryEvent()); + }); } LocalAppInstLauncherSetting? getAppLauncherSetting(InternalID id) { diff --git a/lib/bloc/gebura/gebura_event.dart b/lib/bloc/gebura/gebura_event.dart index aade6b5..ced6370 100644 --- a/lib/bloc/gebura/gebura_event.dart +++ b/lib/bloc/gebura/gebura_event.dart @@ -145,3 +145,15 @@ final class GeburaSearchNewAppInfoEvent extends GeburaEvent { GeburaSearchNewAppInfoEvent(this.query); } + +final class GeburaReportAppRunTimeEvent extends GeburaEvent { + final InternalID appInstID; + final DateTime startTime; + final DateTime endTime; + + GeburaReportAppRunTimeEvent( + this.appInstID, + this.startTime, + this.endTime, + ); +} diff --git a/lib/bloc/gebura/gebura_state.dart b/lib/bloc/gebura/gebura_state.dart index 43d0dae..45507a2 100644 --- a/lib/bloc/gebura/gebura_state.dart +++ b/lib/bloc/gebura/gebura_state.dart @@ -11,6 +11,7 @@ class GeburaState { late Map? appLauncherSettings; late Map? runState; + late Map? appInstRunTimes; late String? localLibraryState; late SteamScanResult? localSteamScanResult; @@ -45,6 +46,29 @@ class GeburaState { return result; } + Duration? getRunTime(Int64 id) { + if (appInstRunTimes == null) { + return null; + } + final insts = getAppInsts(id); + if (insts.isEmpty) { + return null; + } + return insts.values.fold( + Duration.zero, + (previousValue, element) { + return previousValue + + element.fold( + Duration.zero, + (previousValue, element) { + return previousValue + + (appInstRunTimes![element.id] ?? Duration.zero); + }, + ); + }, + ); + } + GeburaState({ this.appInfoMap, this.purchasedAppInfos, @@ -60,6 +84,7 @@ class GeburaState { this.localSteamAppInsts, this.importedSteamAppInsts, this.localSteamLibraryFolders, + this.appInstRunTimes, }); GeburaState copyWith({ @@ -77,6 +102,7 @@ class GeburaState { List? localSteamAppInsts, List? importedSteamAppInsts, List? localSteamLibraryFolders, + Map? appInstRunTimes, }) { return GeburaState( appInfoMap: appInfoMap ?? this.appInfoMap, @@ -95,6 +121,7 @@ class GeburaState { importedSteamAppInsts ?? this.importedSteamAppInsts, localSteamLibraryFolders: localSteamLibraryFolders ?? this.localSteamLibraryFolders, + appInstRunTimes: appInstRunTimes ?? this.appInstRunTimes, ); } @@ -113,6 +140,7 @@ class GeburaState { localSteamAppInsts = other.localSteamAppInsts; importedSteamAppInsts = other.importedSteamAppInsts; localSteamLibraryFolders = other.localSteamLibraryFolders; + appInstRunTimes = other.appInstRunTimes; } } @@ -315,3 +343,15 @@ class GeburaSearchNewAppInfoState extends GeburaState with EventStatusMixin { @override final String? msg; } + +class GeburaReportAppRunTimeState extends GeburaState with EventStatusMixin { + GeburaReportAppRunTimeState(GeburaState state, this.statusCode, {this.msg}) + : super() { + _from(state); + } + + @override + final EventStatus? statusCode; + @override + final String? msg; +} diff --git a/lib/repo/grpc/type_helper.dart b/lib/repo/grpc/type_helper.dart new file mode 100644 index 0000000..d81d8db --- /dev/null +++ b/lib/repo/grpc/type_helper.dart @@ -0,0 +1,18 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:tuihub_protos/google/protobuf/duration.pb.dart' as duration_pb; +import 'package:tuihub_protos/google/protobuf/timestamp.pb.dart'; +import 'package:tuihub_protos/librarian/v1/common.pb.dart'; + +duration_pb.Duration toPBDuration(Duration duration) { + return duration_pb.Duration(seconds: Int64(duration.inSeconds)); +} + +TimeRange toPBTimeRange(DateTime start, DateTime end) { + return TimeRange() + ..startTime = Timestamp.fromDateTime(start) + ..duration = toPBDuration(end.difference(start)); +} + +Duration fromPBDuration(duration_pb.Duration duration) { + return Duration(seconds: duration.seconds.toInt()); +} diff --git a/lib/view/pages/gebura/gebura_library_detail.dart b/lib/view/pages/gebura/gebura_library_detail.dart index cfc7ba0..c92c7de 100644 --- a/lib/view/pages/gebura/gebura_library_detail.dart +++ b/lib/view/pages/gebura/gebura_library_detail.dart @@ -66,7 +66,22 @@ class GeburaLibraryDetailPage extends StatelessWidget { .add(GeburaFetchAppLauncherSettingEvent(item.id)); } } - + final runTime = state.getRunTime(item.id.id); + late String runTimeStr; + if (runTime != null) { + if (runTime.inSeconds == 0) { + runTimeStr = ''; + } else if (runTime.inSeconds < 100) { + runTimeStr = '${runTime.inSeconds} 秒'; + } else if (runTime.inMinutes < 100) { + runTimeStr = '${runTime.inMinutes} 分钟'; + } else { + runTimeStr = + '${((runTime.inSeconds.toDouble()) / 3600).toStringAsFixed(1)} 小时'; + } + } else { + runTimeStr = '错误'; + } final runState = state.runState != null ? state.runState![item.id] : null; return Scaffold( @@ -175,15 +190,18 @@ class GeburaLibraryDetailPage extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(16), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ if (PlatformHelper.isWindowsApp()) if (setting != null) ElevatedButton.icon( - onPressed: () async { - context - .read() - .add(GeburaRunAppEvent(item.id)); - }, + onPressed: (runState?.running ?? false) + ? null + : () async { + context + .read() + .add(GeburaRunAppEvent(item.id)); + }, icon: Icon( setting.type == AppLauncherType.steam ? FontAwesomeIcons.steam @@ -200,26 +218,17 @@ class GeburaLibraryDetailPage extends StatelessWidget { const SizedBox( width: 24, ), - // Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Text('开发商:${item.details.developer}'), - // Text('发行商:${item.details.publisher}'), - // Text('发行日期:${item.details.releaseDate}'), - // ], - // ), - // const SizedBox( - // width: 24, - // ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '运行状态:${runState?.running ?? false ? '运行中' : '未运行'}'), - Text('启动时间:${runState?.startTime ?? ''}'), - Text('停止时间:${runState?.endTime ?? ''}'), - ], - ), + if (runTimeStr.isNotEmpty) + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('总运行时间', + style: + Theme.of(context).textTheme.bodySmall), + Text(runTimeStr), + ], + ), const Expanded(child: SizedBox()), _GeburaLibraryDetailAppSettings(item: item), ],