476 lines
13 KiB
Dart
476 lines
13 KiB
Dart
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 PreferredSizeWidget? appBar;
|
|
final Widget body;
|
|
final Widget? bottomNavigationBar;
|
|
final Widget? sideNavigationBar;
|
|
final Color? backgroundColor;
|
|
final String? title;
|
|
final Widget? leading;
|
|
final List<Widget>? 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<Widget> actions,
|
|
}) : this(
|
|
key: key,
|
|
body: body,
|
|
title: title,
|
|
automaticallyImplyLeading: false,
|
|
actions: actions,
|
|
leading: IconButton(
|
|
icon: const BackButtonIcon(),
|
|
onPressed: () {
|
|
onBack();
|
|
},
|
|
),
|
|
);
|
|
|
|
@override
|
|
State<CommonScaffold> createState() => CommonScaffoldState();
|
|
}
|
|
|
|
class CommonScaffoldState extends State<CommonScaffold> {
|
|
late final ValueNotifier<AppBarState> _appBarState;
|
|
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
|
|
final ValueNotifier<List<String>> _keywordsNotifier = ValueNotifier([]);
|
|
final ValueNotifier<bool> _loading = ValueNotifier(false);
|
|
|
|
final _textController = TextEditingController();
|
|
|
|
Function(List<String>)? _onKeywordsUpdate;
|
|
|
|
Widget? get _sideNavigationBar => widget.sideNavigationBar;
|
|
|
|
set actions(List<Widget> 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<String>)? 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<T?> loadingRun<T>(
|
|
Future<T> 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<String>.from(_keywordsNotifier.value)..add(keyword);
|
|
_keywordsNotifier.value = keywords;
|
|
}
|
|
|
|
_deleteKeyword(String keyword) {
|
|
final isContains = _keywordsNotifier.value.contains(keyword);
|
|
if (!isContains) return;
|
|
final keywords = List<String>.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<Widget> _buildActions(
|
|
bool hasSearch,
|
|
List<Widget> 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: widget.appBar ??
|
|
Stack(
|
|
alignment: Alignment.bottomCenter,
|
|
children: [
|
|
ValueListenableBuilder<AppBarState>(
|
|
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<Widget?>(
|
|
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<Widget> genActions(List<Widget> actions, {double? space}) {
|
|
return <Widget>[
|
|
...actions.separated(
|
|
SizedBox(
|
|
width: space ?? 4,
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 8,
|
|
)
|
|
];
|
|
}
|