390 lines
12 KiB
Dart
390 lines
12 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:defer_pointer/defer_pointer.dart';
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/controller.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/state.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'widgets/start_button.dart';
|
|
|
|
typedef _IsEditWidgetBuilder = Widget Function(bool isEdit);
|
|
|
|
class DashboardView extends ConsumerStatefulWidget {
|
|
const DashboardView({super.key});
|
|
|
|
@override
|
|
ConsumerState<DashboardView> createState() => _DashboardViewState();
|
|
}
|
|
|
|
class _DashboardViewState extends ConsumerState<DashboardView> {
|
|
final key = GlobalKey<SuperGridState>();
|
|
final _isEditNotifier = ValueNotifier<bool>(false);
|
|
final _addedWidgetsNotifier = ValueNotifier<List<GridItem>>([]);
|
|
|
|
@override
|
|
dispose() {
|
|
_isEditNotifier.dispose();
|
|
_addedWidgetsNotifier.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Widget _buildIsEdit(_IsEditWidgetBuilder builder) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: _isEditNotifier,
|
|
builder: (_, isEdit, _) {
|
|
return builder(isEdit);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _handleConnection() async {
|
|
final coreStatus = ref.read(coreStatusProvider);
|
|
if (coreStatus == CoreStatus.connecting) {
|
|
return;
|
|
}
|
|
final tip = coreStatus == CoreStatus.connected
|
|
? appLocalizations.forceRestartCoreTip
|
|
: appLocalizations.restartCoreTip;
|
|
final res = await globalState.showMessage(message: TextSpan(text: tip));
|
|
if (res != true) {
|
|
return;
|
|
}
|
|
appController.restartCore();
|
|
}
|
|
|
|
List<Widget> _buildActions(bool isEdit) {
|
|
return [
|
|
if (!isEdit)
|
|
Consumer(
|
|
builder: (_, ref, _) {
|
|
final coreStatus = ref.watch(coreStatusProvider);
|
|
return Tooltip(
|
|
message: appLocalizations.coreStatus,
|
|
child: FadeScaleBox(
|
|
alignment: Alignment.centerRight,
|
|
child: coreStatus == CoreStatus.connected
|
|
? IconButton.filled(
|
|
visualDensity: VisualDensity.compact,
|
|
iconSize: 20,
|
|
padding: EdgeInsets.zero,
|
|
style: IconButton.styleFrom(
|
|
backgroundColor: Colors.greenAccent,
|
|
foregroundColor: switch (Theme.brightnessOf(
|
|
context,
|
|
)) {
|
|
Brightness.light =>
|
|
context.colorScheme.onSurfaceVariant,
|
|
Brightness.dark =>
|
|
context.colorScheme.onPrimaryFixedVariant,
|
|
},
|
|
),
|
|
onPressed: _handleConnection,
|
|
icon: Icon(Icons.check, fontWeight: FontWeight.w900),
|
|
)
|
|
: FilledButton.icon(
|
|
key: ValueKey(coreStatus),
|
|
onPressed: _handleConnection,
|
|
style: FilledButton.styleFrom(
|
|
visualDensity: VisualDensity.compact,
|
|
padding: EdgeInsets.symmetric(horizontal: 12),
|
|
backgroundColor: switch (coreStatus) {
|
|
CoreStatus.connecting => null,
|
|
CoreStatus.connected => Colors.greenAccent,
|
|
CoreStatus.disconnected =>
|
|
context.colorScheme.error,
|
|
},
|
|
foregroundColor: switch (coreStatus) {
|
|
CoreStatus.connecting => null,
|
|
CoreStatus.connected => switch (Theme.brightnessOf(
|
|
context,
|
|
)) {
|
|
Brightness.light =>
|
|
context.colorScheme.onSurfaceVariant,
|
|
Brightness.dark => null,
|
|
},
|
|
CoreStatus.disconnected =>
|
|
context.colorScheme.onError,
|
|
},
|
|
),
|
|
icon: SizedBox(
|
|
height: globalState.measure.bodyMediumHeight,
|
|
width: globalState.measure.bodyMediumHeight,
|
|
child: switch (coreStatus) {
|
|
CoreStatus.connecting => Padding(
|
|
padding: EdgeInsets.all(2),
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 3,
|
|
color: context.colorScheme.onPrimary,
|
|
backgroundColor: Colors.transparent,
|
|
),
|
|
),
|
|
CoreStatus.connected => Icon(
|
|
Icons.check_sharp,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
CoreStatus.disconnected => Icon(
|
|
Icons.restart_alt_sharp,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
},
|
|
),
|
|
label: Text(switch (coreStatus) {
|
|
CoreStatus.connecting => appLocalizations.connecting,
|
|
CoreStatus.connected => appLocalizations.connected,
|
|
CoreStatus.disconnected =>
|
|
appLocalizations.disconnected,
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
if (isEdit)
|
|
ValueListenableBuilder(
|
|
valueListenable: _addedWidgetsNotifier,
|
|
builder: (_, addedChildren, child) {
|
|
if (addedChildren.isEmpty) {
|
|
return Container();
|
|
}
|
|
return child!;
|
|
},
|
|
child: IconButton(
|
|
onPressed: () {
|
|
_showAddWidgetsModal();
|
|
},
|
|
icon: Icon(Icons.add_circle),
|
|
),
|
|
),
|
|
FadeRotationScaleBox(
|
|
child: isEdit
|
|
? IconButton(
|
|
key: ValueKey(true),
|
|
icon: Icon(Icons.save, key: ValueKey('save-icon')),
|
|
onPressed: _handleUpdateIsEdit,
|
|
)
|
|
: IconButton(
|
|
key: ValueKey(false),
|
|
icon: Icon(Icons.edit, key: ValueKey('edit-icon')),
|
|
onPressed: _handleUpdateIsEdit,
|
|
),
|
|
),
|
|
];
|
|
}
|
|
|
|
void _showAddWidgetsModal() {
|
|
showSheet(
|
|
builder: (_, type) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: _addedWidgetsNotifier,
|
|
builder: (_, value, _) {
|
|
return AdaptiveSheetScaffold(
|
|
type: type,
|
|
body: _AddDashboardWidgetModal(
|
|
items: value,
|
|
onAdd: (gridItem) {
|
|
key.currentState?.handleAdd(gridItem);
|
|
},
|
|
),
|
|
title: appLocalizations.add,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
context: context,
|
|
);
|
|
}
|
|
|
|
Future<void> _handleUpdateIsEdit() async {
|
|
if (_isEditNotifier.value == true) {
|
|
await _handleSave();
|
|
}
|
|
_isEditNotifier.value = !_isEditNotifier.value;
|
|
}
|
|
|
|
Future<void> _handleSave() async {
|
|
final currentState = key.currentState;
|
|
if (currentState == null) {
|
|
return;
|
|
}
|
|
if (mounted) {
|
|
await currentState.isTransformCompleter;
|
|
final dashboardWidgets = currentState.children
|
|
.map((item) => DashboardWidget.getDashboardWidget(item))
|
|
.toList();
|
|
ref
|
|
.read(appSettingProvider.notifier)
|
|
.update(
|
|
(state) => state.copyWith(dashboardWidgets: dashboardWidgets),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final dashboardState = ref.watch(dashboardStateProvider);
|
|
final columns = max(4 * ((dashboardState.contentWidth / 280).ceil()), 8);
|
|
final spacing = 14.ap;
|
|
final children = [
|
|
...dashboardState.dashboardWidgets
|
|
.where(
|
|
(item) => item.platforms.contains(SupportPlatform.currentPlatform),
|
|
)
|
|
.map((item) => item.widget),
|
|
];
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_addedWidgetsNotifier.value = DashboardWidget.values
|
|
.where(
|
|
(item) =>
|
|
!children.contains(item.widget) &&
|
|
item.platforms.contains(SupportPlatform.currentPlatform),
|
|
)
|
|
.map((item) => item.widget)
|
|
.toList();
|
|
});
|
|
return _buildIsEdit(
|
|
(isEdit) => CommonScaffold(
|
|
title: appLocalizations.dashboard,
|
|
actions: _buildActions(isEdit),
|
|
floatingActionButton: const StartButton(),
|
|
body: Align(
|
|
alignment: Alignment.topCenter,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16).copyWith(bottom: 88),
|
|
child: isEdit
|
|
? SystemBackBlock(
|
|
child: CommonPopScope(
|
|
child: SuperGrid(
|
|
key: key,
|
|
crossAxisCount: columns,
|
|
crossAxisSpacing: spacing,
|
|
mainAxisSpacing: spacing,
|
|
children: [
|
|
...dashboardState.dashboardWidgets
|
|
.where(
|
|
(item) => item.platforms.contains(
|
|
SupportPlatform.currentPlatform,
|
|
),
|
|
)
|
|
.map((item) => item.widget),
|
|
],
|
|
onUpdate: () {
|
|
_handleSave();
|
|
},
|
|
),
|
|
onPop: (context) {
|
|
_handleUpdateIsEdit();
|
|
return false;
|
|
},
|
|
),
|
|
)
|
|
: Grid(
|
|
crossAxisCount: columns,
|
|
crossAxisSpacing: spacing,
|
|
mainAxisSpacing: spacing,
|
|
children: children,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddDashboardWidgetModal extends StatelessWidget {
|
|
final List<GridItem> items;
|
|
final Function(GridItem item) onAdd;
|
|
|
|
const _AddDashboardWidgetModal({required this.items, required this.onAdd});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DeferredPointerHandler(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.all(16),
|
|
child: Grid(
|
|
crossAxisCount: 8,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
children: items
|
|
.map(
|
|
(item) => item.wrap(
|
|
builder: (child) {
|
|
return _AddedContainer(
|
|
onAdd: () {
|
|
onAdd(item);
|
|
},
|
|
child: child,
|
|
);
|
|
},
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddedContainer extends StatefulWidget {
|
|
final Widget child;
|
|
final VoidCallback onAdd;
|
|
|
|
const _AddedContainer({required this.child, required this.onAdd});
|
|
|
|
@override
|
|
State<_AddedContainer> createState() => _AddedContainerState();
|
|
}
|
|
|
|
class _AddedContainerState extends State<_AddedContainer> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_AddedContainer oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.child != widget.child) {}
|
|
}
|
|
|
|
Future<void> _handleAdd() async {
|
|
widget.onAdd();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
ActivateBox(child: widget.child),
|
|
Positioned(
|
|
top: -8,
|
|
right: -8,
|
|
child: DeferPointer(
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: IconButton.filled(
|
|
iconSize: 20,
|
|
padding: EdgeInsets.all(2),
|
|
onPressed: _handleAdd,
|
|
icon: Icon(Icons.add),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|