import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/pop_scope.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'chip.dart'; class CommonScaffold extends StatefulWidget { final AppBar? appBar; final Widget body; final Widget? bottomNavigationBar; final Widget? sideNavigationBar; final Color? backgroundColor; final String? title; final Widget? leading; final List? actions; final bool automaticallyImplyLeading; final bool? centerTitle; final AppBarEditState? appBarEditState; const CommonScaffold({ super.key, this.appBar, required this.body, this.sideNavigationBar, this.backgroundColor, this.bottomNavigationBar, this.leading, this.title, this.actions, this.automaticallyImplyLeading = true, this.centerTitle, this.appBarEditState, }); CommonScaffold.open({ Key? key, required Widget body, required String title, required Function onBack, required List actions, }) : this( key: key, body: body, title: title, automaticallyImplyLeading: false, actions: actions, leading: IconButton( icon: const BackButtonIcon(), onPressed: () { onBack(); }, ), ); @override State createState() => CommonScaffoldState(); } class CommonScaffoldState extends State { late final ValueNotifier _appBarState; final ValueNotifier _floatingActionButton = ValueNotifier(null); final ValueNotifier> _keywordsNotifier = ValueNotifier([]); final ValueNotifier _loading = ValueNotifier(false); final _textController = TextEditingController(); Function(List)? _onKeywordsUpdate; Widget? get _sideNavigationBar => widget.sideNavigationBar; set actions(List actions) { _appBarState.value = _appBarState.value.copyWith(actions: actions); } bool get _isSearch { return _appBarState.value.searchState?.isSearch == true; } bool get _isEdit { return _appBarState.value.editState?.isEdit == true; } set onKeywordsUpdate(Function(List)? onKeywordsUpdate) { _onKeywordsUpdate = onKeywordsUpdate; } @override void initState() { super.initState(); _appBarState = ValueNotifier( AppBarState( editState: widget.appBarEditState, ), ); } updateSearchState( AppBarSearchState? Function(AppBarSearchState? state) builder, ) { _appBarState.value = _appBarState.value.copyWith( searchState: builder( _appBarState.value.searchState, ), ); } updateEditState( AppBarEditState? Function(AppBarEditState? state) builder, ) { _appBarState.value = _appBarState.value.copyWith( editState: builder( _appBarState.value.editState, ), ); } set floatingActionButton(Widget? floatingActionButton) { if (_floatingActionButton.value != floatingActionButton) { _floatingActionButton.value = floatingActionButton; } } Widget _buildSearchingAppBarTheme(Widget child) { final ThemeData theme = Theme.of(context); final ColorScheme colorScheme = theme.colorScheme; return Theme( data: theme.copyWith( appBarTheme: theme.appBarTheme.copyWith( backgroundColor: colorScheme.brightness == Brightness.dark ? Colors.grey[900] : Colors.white, iconTheme: theme.primaryIconTheme.copyWith(color: Colors.grey), titleTextStyle: theme.textTheme.titleLarge, toolbarTextStyle: theme.textTheme.bodyMedium, ), inputDecorationTheme: InputDecorationTheme( hintStyle: theme.inputDecorationTheme.hintStyle, border: InputBorder.none, ), ), child: child, ); } Future loadingRun( Future Function() futureFunction, { String? title, }) async { _loading.value = true; try { final res = await futureFunction(); _loading.value = false; return res; } catch (e) { globalState.showMessage( title: title ?? appLocalizations.tip, message: TextSpan( text: e.toString(), ), ); _loading.value = false; return null; } } _handleClearInput() { _textController.text = ""; if (_appBarState.value.searchState != null) { _appBarState.value.searchState!.onSearch(""); } } _handleClear() { if (_textController.text.isNotEmpty) { _handleClearInput(); return; } updateSearchState( (state) => state?.copyWith( isSearch: false, ), ); } _handleExitSearching() { _handleClearInput(); updateSearchState( (state) => state?.copyWith( isSearch: false, ), ); } @override void dispose() { _appBarState.dispose(); _textController.dispose(); _floatingActionButton.dispose(); super.dispose(); } @override void didUpdateWidget(CommonScaffold oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.title != widget.title) { _appBarState.value = AppBarState(); _floatingActionButton.value = null; _textController.text = ""; _keywordsNotifier.value = []; _onKeywordsUpdate = null; } else if (oldWidget.appBarEditState != widget.appBarEditState) { _appBarState.value = _appBarState.value.copyWith( editState: widget.appBarEditState, ); } } addKeyword(String keyword) { final isContains = _keywordsNotifier.value.contains(keyword); if (isContains) return; final keywords = List.from(_keywordsNotifier.value)..add(keyword); _keywordsNotifier.value = keywords; } _deleteKeyword(String keyword) { final isContains = _keywordsNotifier.value.contains(keyword); if (!isContains) return; final keywords = List.from(_keywordsNotifier.value) ..remove(keyword); _keywordsNotifier.value = keywords; } Widget? _buildLeading() { if (_isEdit) { return IconButton( onPressed: _appBarState.value.editState?.onExit, icon: Icon(Icons.close), ); } return _isSearch ? IconButton( onPressed: _handleExitSearching, icon: Icon(Icons.arrow_back), ) : widget.leading; } Widget _buildTitle(AppBarSearchState? startState) { return _isSearch ? TextField( autofocus: true, controller: _textController, style: context.textTheme.titleLarge, onChanged: (value) { if (startState != null) { startState.onSearch(value); } }, decoration: InputDecoration( hintText: appLocalizations.search, ), ) : Text( !_isEdit ? widget.title! : appLocalizations.selectedCountTitle( "${_appBarState.value.editState?.editCount ?? 0}", ), ); } List _buildActions( bool hasSearch, List actions, ) { if (_isSearch) { return genActions([ IconButton( onPressed: _handleClear, icon: Icon(Icons.close), ), ]); } return genActions( [ if (hasSearch) IconButton( onPressed: () { updateSearchState( (state) => state?.copyWith( isSearch: true, ), ); }, icon: Icon(Icons.search), ), ...actions ], ); } Widget _buildAppBarWrap(Widget appBar) { if (_isEdit) { return CommonPopScope( onPop: () { if (_isEdit) { _appBarState.value.editState?.onExit(); return false; } return true; }, child: appBar, ); } return _isSearch ? _buildSearchingAppBarTheme(appBar) : appBar; } PreferredSizeWidget _buildAppBar() { return PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: Theme( data: Theme.of(context).copyWith( appBarTheme: AppBarTheme( systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark ? Brightness.light : Brightness.dark, systemNavigationBarIconBrightness: Theme.of(context).brightness == Brightness.dark ? Brightness.light : Brightness.dark, systemNavigationBarColor: widget.bottomNavigationBar != null ? context.colorScheme.surfaceContainer : context.colorScheme.surface, systemNavigationBarDividerColor: Colors.transparent, ), ), ), child: Stack( alignment: Alignment.bottomCenter, children: [ widget.appBar ?? ValueListenableBuilder( valueListenable: _appBarState, builder: (_, state, __) { return _buildAppBarWrap( AppBar( centerTitle: widget.centerTitle ?? false, automaticallyImplyLeading: widget.automaticallyImplyLeading, leading: _buildLeading(), title: _buildTitle(state.searchState), actions: _buildActions( state.searchState != null, state.actions.isNotEmpty ? state.actions : widget.actions ?? [], ), ), ); }, ), ValueListenableBuilder( valueListenable: _loading, builder: (_, value, __) { return value == true ? const LinearProgressIndicator() : Container(); }, ), ], ), ), ); } @override Widget build(BuildContext context) { assert(widget.appBar != null || widget.title != null); final body = SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ValueListenableBuilder( valueListenable: _keywordsNotifier, builder: (_, keywords, __) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_onKeywordsUpdate != null) { _onKeywordsUpdate!(keywords); } }); if (keywords.isEmpty) { return SizedBox(); } return Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), child: Wrap( runSpacing: 8, spacing: 8, children: [ for (final keyword in keywords) CommonChip( label: keyword, type: ChipType.delete, onPressed: () { _deleteKeyword(keyword); }, ), ], ), ); }, ), Expanded( child: widget.body, ), ], ), ); final scaffold = Scaffold( appBar: _buildAppBar(), body: body, backgroundColor: widget.backgroundColor, floatingActionButton: ValueListenableBuilder( valueListenable: _floatingActionButton, builder: (_, value, __) { return IntrinsicWidth( child: IntrinsicHeight( child: FadeScaleBox( child: value ?? SizedBox(), ), ), ); }, ), bottomNavigationBar: widget.bottomNavigationBar, ); return _sideNavigationBar != null ? Row( mainAxisSize: MainAxisSize.min, children: [ _sideNavigationBar!, Expanded( flex: 1, child: scaffold, ), ], ) : scaffold; } } List genActions(List actions, {double? space}) { return [ ...actions.separated( SizedBox( width: space ?? 4, ), ), SizedBox( width: 8, ) ]; }