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
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 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,
|
|
)
|
|
];
|
|
}
|