diff --git a/lib/bloc/gebura/gebura_bloc.dart b/lib/bloc/gebura/gebura_bloc.dart index 4345360..23ccd04 100644 --- a/lib/bloc/gebura/gebura_bloc.dart +++ b/lib/bloc/gebura/gebura_bloc.dart @@ -641,6 +641,88 @@ class GeburaBloc extends Bloc { )); add(GeburaRefreshLibraryEvent()); }); + + on((event, emit) async { + emit(GeburaSearchNewAppInfoState(state, EventStatus.processing)); + final resp = await _api.doRequest( + (client) => client.searchNewAppInfos, + SearchNewAppInfosRequest( + paging: PagingRequest( + pageSize: Int64(10), + pageNum: Int64(1), + ), + name: event.query, + ), + ); + if (resp.status != ApiStatus.success) { + emit(GeburaSearchNewAppInfoState(state, EventStatus.failed, + msg: resp.error)); + return; + } + emit(GeburaSearchNewAppInfoState( + state, + EventStatus.success, + msg: resp.error, + infos: resp.getData().appInfos, + )); + }, transformer: droppable()); + + on((event, emit) async { + emit(GeburaAssignAppInfoState(state, EventStatus.processing)); + final syncResp = await _api.doRequest( + (client) => client.syncAppInfos, + SyncAppInfosRequest( + appInfoIds: [ + AppInfoID( + internal: false, + source: event.appInfoSource, + sourceAppId: event.appInfoSourceID) + ], + waitData: true, + )); + if (syncResp.status != ApiStatus.success) { + emit(GeburaAssignAppInfoState(state, EventStatus.failed, + msg: syncResp.error)); + return; + } + final boundInfoResp = await _api.doRequest( + (client) => client.getBoundAppInfos, + GetBoundAppInfosRequest( + appInfoId: syncResp.getData().appInfos.first.id, + )); + if (boundInfoResp.status != ApiStatus.success) { + emit(GeburaAssignAppInfoState(state, EventStatus.failed, + msg: boundInfoResp.error)); + return; + } + if (!boundInfoResp + .getData() + .appInfos + .any((element) => element.internal)) { + emit(GeburaAssignAppInfoState(state, EventStatus.failed, + msg: 'No internal app info found')); + } + final appInfoId = boundInfoResp + .getData() + .appInfos + .firstWhere((element) => element.internal) + .id; + final resp = await _api.doRequest( + (client) => client.assignApp, + AssignAppRequest( + appInfoId: appInfoId, + appId: event.appID, + ), + ); + if (resp.status != ApiStatus.success) { + emit(GeburaAssignAppInfoState(state, EventStatus.failed, + msg: resp.error)); + return; + } + emit(GeburaAssignAppInfoState(state, EventStatus.success, + msg: resp.error)); + add(GeburaRefreshLibraryEvent()); + }, transformer: droppable()); } LocalAppInstLauncherSetting? getAppLauncherSetting(InternalID id) { diff --git a/lib/bloc/gebura/gebura_event.dart b/lib/bloc/gebura/gebura_event.dart index 25cfa5d..740c958 100644 --- a/lib/bloc/gebura/gebura_event.dart +++ b/lib/bloc/gebura/gebura_event.dart @@ -124,3 +124,18 @@ final class GeburaRefreshAppInfoEvent extends GeburaEvent { GeburaRefreshAppInfoEvent(this.appInfoID); } + +final class GeburaAssignAppWithNewInfoEvent extends GeburaEvent { + final InternalID appID; + final String appInfoSource; + final String appInfoSourceID; + + GeburaAssignAppWithNewInfoEvent( + this.appID, this.appInfoSource, this.appInfoSourceID); +} + +final class GeburaSearchNewAppInfoEvent extends GeburaEvent { + final String query; + + GeburaSearchNewAppInfoEvent(this.query); +} diff --git a/lib/bloc/gebura/gebura_state.dart b/lib/bloc/gebura/gebura_state.dart index 1da36ed..43d0dae 100644 --- a/lib/bloc/gebura/gebura_state.dart +++ b/lib/bloc/gebura/gebura_state.dart @@ -288,3 +288,30 @@ class GeburaRefreshAppInfoState extends GeburaState with EventStatusMixin { @override final String? msg; } + +class GeburaAssignAppInfoState extends GeburaState with EventStatusMixin { + GeburaAssignAppInfoState(GeburaState state, this.statusCode, {this.msg}) + : super() { + _from(state); + } + + @override + final EventStatus? statusCode; + @override + final String? msg; +} + +class GeburaSearchNewAppInfoState extends GeburaState with EventStatusMixin { + GeburaSearchNewAppInfoState(GeburaState state, this.statusCode, + {this.infos, this.msg}) + : super() { + _from(state); + } + + final List? infos; + + @override + final EventStatus? statusCode; + @override + final String? msg; +} diff --git a/lib/route.dart b/lib/route.dart index bca7989..a51537c 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -17,6 +17,7 @@ import 'repo/grpc/api_helper.dart'; import 'view/layout/overlapping_panels.dart'; import 'view/pages/chesed/chesed_home_page.dart'; import 'view/pages/frame_page.dart'; +import 'view/pages/gebura/gebura_assign_app_panel.dart'; import 'view/pages/gebura/gebura_library_detail.dart'; import 'view/pages/gebura/gebura_library_overview.dart'; import 'view/pages/gebura/gebura_library_settings.dart'; @@ -135,6 +136,9 @@ class AppRoutes { AppRoutes._('$_gebura/${GeburaFunctions.librarySettings}'); static AppRoutes geburaLibraryDetail(int id) => AppRoutes._('$geburaLibrary?id=$id'); + static AppRoutes geburaLibraryAssignApp(int id) => + AppRoutes._('$geburaLibrary?id=$id&action=${_GeburaActions.assignApp}', + isAction: true); static const String _chesed = '$_module/${ModuleName._chesed}'; static const AppRoutes chesed = AppRoutes._(_chesed); @@ -238,6 +242,10 @@ class GeburaFunctions { static const String librarySettings = 'librarySettings'; } +class _GeburaActions { + static const String assignApp = 'assignApp'; +} + class _SettingsFunctions { static const String client = 'client'; static const String notifyTarget = 'notifyTarget'; @@ -438,12 +446,17 @@ GoRouter getRouter(MainBloc mainBloc, ApiHelper apiHelper) { pageBuilder: (context, state) { final function = state.pathParameters['function'] ?? AppRoutes.geburaStore.toString(); + final action = state.uri.queryParameters['action'] ?? + _GeburaActions.assignApp; final geburaPages = { GeburaFunctions.store: const GeburaStorePage(), GeburaFunctions.library: const GeburaLibraryOverview(), GeburaFunctions.librarySettings: const GeburaLibrarySettings(), }; + final geburaActions = { + _GeburaActions.assignApp: const GeburaAssignAppPanel(), + }; Widget? page = geburaPages[function]; if (state.uri.queryParameters['id']?.isNotEmpty ?? false) { final idStr = state.uri.queryParameters['id'] ?? ''; @@ -461,6 +474,7 @@ GoRouter getRouter(MainBloc mainBloc, ApiHelper apiHelper) { function: function, ), middlePart: page, + rightPart: geburaActions[action], ), ), ); diff --git a/lib/view/pages/gebura/gebura_assign_app_panel.dart b/lib/view/pages/gebura/gebura_assign_app_panel.dart new file mode 100644 index 0000000..200e210 --- /dev/null +++ b/lib/view/pages/gebura/gebura_assign_app_panel.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tuihub_protos/librarian/v1/common.pb.dart'; + +import '../../../../route.dart'; +import '../../../bloc/gebura/gebura_bloc.dart'; +import '../../components/toast.dart'; +import '../../specialized/right_panel_form.dart'; + +class GeburaAssignAppPanel extends StatefulWidget { + const GeburaAssignAppPanel({super.key}); + + @override + State createState() => _GeburaAssignAppPanelState(); +} + +class _GeburaAssignAppPanelState extends State { + void close(BuildContext context) { + AppRoutes.geburaLibraryAssignApp(0).pop(context); + } + + int _index = 0; + List? searchResults; + String? searchMsg; + AppInfo? selectedAppInfo; + final TextEditingController _searchController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is GeburaSearchNewAppInfoState) { + setState(() { + if (state.success) { + searchResults = state.infos; + if (searchResults?.isNotEmpty ?? false) { + searchMsg = null; + } else { + searchMsg = '无结果'; + } + } else if (state.processing) { + searchMsg = '加载中'; + } else if (state.failed) { + searchMsg = '加载失败,${state.msg}'; + } else { + searchMsg = '无结果'; + } + }); + } + if (state is GeburaAssignAppInfoState && state.success) { + const Toast(title: '', message: '设置成功').show(context); + close(context); + } + }, + builder: (context, state) { + final app = + state.selectedLibraryItem != null && state.libraryItems != null + ? state.libraryItems!.firstWhere((element) => + element.id.id.toInt() == state.selectedLibraryItem!) + : null; + return RightPanelForm( + title: const Text('设置应用信息'), + formFields: [ + Stepper( + currentStep: _index, + onStepCancel: () { + if (_index > 0) { + setState(() { + _index -= 1; + }); + } + }, + onStepContinue: () { + if (_index <= 1) { + setState(() { + _index += 1; + }); + } + }, + onStepTapped: (int index) { + setState(() { + _index = index; + }); + }, + steps: [ + Step( + title: const Text('应用'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + TextFormField( + initialValue: app?.name, + decoration: const InputDecoration( + border: OutlineInputBorder(), + label: Text('名称'), + ), + ), + ], + ), + ), + Step( + title: const Text('查找应用信息'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + TextFormField( + controller: _searchController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + label: const Text('搜索'), + suffixIcon: IconButton( + icon: const Icon(Icons.search), + onPressed: () { + context + .read() + .add(GeburaSearchNewAppInfoEvent( + _searchController.text, + )); + }, + ), + ), + onChanged: (value) { + context + .read() + .add(GeburaSearchNewAppInfoEvent( + value, + )); + }, + ), + const SizedBox( + height: 8, + ), + ListView(shrinkWrap: true, children: [ + if (searchMsg != null) + Padding( + padding: const EdgeInsets.all(8), + child: Text(searchMsg!), + ) + else if (searchResults != null) + for (final info in searchResults!) + ListTile( + title: Text(info.name), + subtitle: Text(info.source), + onTap: () { + setState(() { + selectedAppInfo = info; + _index += 1; + }); + }, + ), + ]), + ], + ), + ), + Step( + title: const Text('确认'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 8, + ), + Text('受影响的应用: ${app?.name}'), + Text('新应用信息: ${selectedAppInfo?.name}'), + ], + ), + ), + ], + ), + ], + errorMsg: state is GeburaAssignAppInfoState && state.failed + ? state.msg + : null, + onSubmit: () { + context.read().add(GeburaAssignAppWithNewInfoEvent( + app!.id, + selectedAppInfo!.source, + selectedAppInfo!.sourceAppId, + )); + }, + submitting: state is GeburaAssignAppInfoState && state.processing, + close: () => close(context), + ); + }, + ); + } +} diff --git a/lib/view/pages/gebura/gebura_library_detail.dart b/lib/view/pages/gebura/gebura_library_detail.dart index 166193e..86412fa 100644 --- a/lib/view/pages/gebura/gebura_library_detail.dart +++ b/lib/view/pages/gebura/gebura_library_detail.dart @@ -16,6 +16,7 @@ import '../../../bloc/main_bloc.dart'; import '../../../common/platform.dart'; import '../../../model/gebura_model.dart'; import '../../../repo/grpc/l10n.dart'; +import '../../../route.dart'; import '../../components/toast.dart'; import '../../helper/spacing.dart'; import '../../helper/url.dart'; @@ -39,6 +40,12 @@ class GeburaLibraryDetailPage extends StatelessWidget { } }, builder: (context, state) { + if (state.libraryItems == null || + !state.libraryItems!.any( + (element) => element.id.id == Int64(id), + )) { + AppRoutes.geburaLibrary.go(context); + } final item = state.libraryItems!.firstWhere( (element) => element.id.id == Int64(id), ); @@ -534,29 +541,18 @@ class _GeburaLibraryDetailAppSettingsState itemBuilder: (BuildContext context) => >[ PopupMenuItem( value: 1, - enabled: state.ownedApps! + enabled: (state.ownedApps ?? []) .any((element) => element.id.id == widget.item.id.id), child: const Text('设置应用信息'), onTap: () { - showDialog( - context: context, - builder: (_) { - return BlocProvider.value( - value: context.read(), - child: _GeburaLibraryDetailChangeAppInfoDialog( - item: App( - id: widget.item.id, - name: widget.item.name, - ), - ), - ); - }, - ); + AppRoutes.geburaLibraryAssignApp(widget.item.id.id.toInt()) + .go(context); }, ), PopupMenuItem( value: 2, - enabled: state.appInfoMap!.values + enabled: (state.appInfoMap ?? >{}) + .values .expand((element) => element) .any((element) => element.id.id == widget.item.id.id), child: const Text('刷新应用信息'),