Files
MWClash/lib/widgets/scaffold.dart
chen08209 676f2d058a Add windows server mode start process verify
Add linux deb dependencies

Add backup recovery strategy select

Support custom text scaling

Optimize the display of different text scale

Optimize windows setup experience

Optimize startTun performance

Optimize android tv experience

Optimize default option

Optimize computed text size

Optimize hyperOS freeform window

Add developer mode

Update core

Optimize more details
2025-05-01 00:02:29 +08:00

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 AppBar? 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: Stack(
alignment: Alignment.bottomCenter,
children: [
widget.appBar ??
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,
)
];
}