opt.: migrate fl_lib

This commit is contained in:
lollipopkit
2024-05-14 22:29:37 +08:00
parent 248430e5b0
commit 04dfede535
136 changed files with 686 additions and 3896 deletions

View File

@@ -1,101 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:window_manager/window_manager.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
/// System status bar height
static double? barHeight;
static bool drawTitlebar = false;
const CustomAppBar({
super.key,
this.title,
this.actions,
this.centerTitle = true,
this.leading,
this.backgroundColor,
});
final Widget? title;
final List<Widget>? actions;
final bool? centerTitle;
final Widget? leading;
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final bar = AppBar(
key: key,
title: title,
actions: actions,
centerTitle: centerTitle,
leading: leading,
backgroundColor: backgroundColor,
toolbarHeight: (barHeight ?? 0) + kToolbarHeight,
);
if (!drawTitlebar) return bar;
return Stack(
children: [
bar,
Positioned(
right: 0,
top: 0,
child: GestureDetector(
onVerticalDragStart: (_) {
windowManager.startDragging();
},
onHorizontalDragStart: (_) {
windowManager.startDragging();
},
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
icon: Transform.translate(
offset: const Offset(0, -3.5),
child: const Icon(Icons.minimize, size: 13),
),
onPressed: () => windowManager.minimize(),
),
IconButton(
icon: const Icon(Icons.crop_square, size: 13),
onPressed: () async {
if (await windowManager.isMaximized()) {
windowManager.unmaximize();
} else {
windowManager.maximize();
}
},
),
IconButton(
icon: const Icon(Icons.close, size: 14),
onPressed: () => windowManager.close(),
),
],
),
),
),
],
);
}
static Future<void> updateTitlebarHeight() async {
switch (Platform.operatingSystem) {
case 'macos':
barHeight = 27;
break;
case 'linux' || 'windows':
if (!Stores.setting.hideTitleBar.fetch()) break;
barHeight = 37;
drawTitlebar = true;
break;
default:
break;
}
}
@override
Size get preferredSize => Size.fromHeight((barHeight ?? 0) + kToolbarHeight);
}

View File

@@ -1,113 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
final class AutoHide extends StatefulWidget {
final Widget child;
final ScrollController controller;
final AxisDirection direction;
final double offset;
const AutoHide({
super.key,
required this.child,
required this.controller,
required this.direction,
this.offset = 55,
});
@override
State<AutoHide> createState() => AutoHideState();
}
final class AutoHideState extends State<AutoHide> {
bool _visible = true;
bool _isScrolling = false;
Timer? _timer;
@override
void initState() {
super.initState();
widget.controller.addListener(_scrollListener);
_setupTimer();
}
@override
void dispose() {
widget.controller.removeListener(_scrollListener);
_timer?.cancel();
_timer = null;
super.dispose();
}
void show() {
debugPrint('show');
if (_visible) return;
setState(() {
_visible = true;
});
_setupTimer();
}
void _setupTimer() {
_timer?.cancel();
_timer = Timer.periodic(const Duration(seconds: 3), (_) {
if (_isScrolling) return;
if (!_visible) return;
final canScroll =
widget.controller.positions.any((e) => e.maxScrollExtent >= 0);
if (!canScroll) return;
setState(() {
_visible = false;
});
_timer?.cancel();
_timer = null;
});
}
void _scrollListener() {
if (_isScrolling) return;
_isScrolling = true;
if (!_visible) {
setState(() {
_visible = true;
});
_setupTimer();
}
_isScrolling = false;
}
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: Durations.medium1,
curve: Curves.easeInOutCubic,
transform: _transform,
child: widget.child,
);
}
Matrix4? get _transform {
switch (widget.direction) {
case AxisDirection.down:
return _visible
? Matrix4.identity()
: Matrix4.translationValues(0, widget.offset, 0);
case AxisDirection.up:
return _visible
? Matrix4.identity()
: Matrix4.translationValues(0, -widget.offset, 0);
case AxisDirection.left:
return _visible
? Matrix4.identity()
: Matrix4.translationValues(-widget.offset, 0, 0);
case AxisDirection.right:
return _visible
? Matrix4.identity()
: Matrix4.translationValues(widget.offset, 0, 0);
}
}
}

View File

@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
class CardX extends StatelessWidget {
const CardX({super.key, required this.child, this.color});
final Widget child;
final Color? color;
@override
Widget build(BuildContext context) {
return Card(
key: key,
clipBehavior: Clip.antiAlias,
color: color,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(17)),
),
child: child,
);
}
}

View File

@@ -1,31 +0,0 @@
import 'package:choice/selection.dart';
import 'package:flutter/material.dart';
class ChoiceChipX<T> extends StatelessWidget {
const ChoiceChipX({
super.key,
required this.label,
required this.state,
required this.value,
});
final String label;
final ChoiceController<T> state;
final T value;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: ChoiceChip(
label: Text(label),
side: BorderSide.none,
showCheckmark: true,
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 8),
labelPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
selected: state.selected(value),
onSelected: state.onSelected(value),
),
);
}
}

View File

@@ -1,77 +0,0 @@
import 'package:flutter/material.dart';
enum _ColorPropType {
r,
g,
b,
}
class ColorPicker extends StatefulWidget {
final Color color;
final ValueChanged<Color> onColorChanged;
const ColorPicker({
super.key,
required this.color,
required this.onColorChanged,
});
@override
_ColorPickerState createState() => _ColorPickerState();
}
class _ColorPickerState extends State<ColorPicker> {
late int _r = widget.color.red;
late int _g = widget.color.green;
late int _b = widget.color.blue;
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildProgress(_ColorPropType.r, 'R', _r.toDouble()),
_buildProgress(_ColorPropType.g, 'G', _g.toDouble()),
_buildProgress(_ColorPropType.b, 'B', _b.toDouble()),
],
);
}
Widget _buildProgress(_ColorPropType type, String title, double value) {
return Row(
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Expanded(
child: Slider(
value: value,
onChanged: (v) {
setState(() {
switch (type) {
case _ColorPropType.r:
_r = v.toInt();
break;
case _ColorPropType.g:
_g = v.toInt();
break;
case _ColorPropType.b:
_b = v.toInt();
break;
}
});
widget.onColorChanged(Color.fromARGB(255, _r, _g, _b));
},
min: 0,
max: 255,
divisions: 255,
label: value.toInt().toString(),
),
),
],
);
}
}

View File

@@ -1,68 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/locale.dart';
final class CountDownBtn extends StatefulWidget {
final int seconds;
final String text;
final Color? afterColor;
final VoidCallback onTap;
const CountDownBtn({
super.key,
required this.onTap,
this.seconds = 3,
this.text = 'Go',
this.afterColor,
});
@override
State<CountDownBtn> createState() => _CountDownBtnState();
}
final class _CountDownBtnState extends State<CountDownBtn> {
late int _seconds = widget.seconds;
Timer? _timer;
@override
void initState() {
super.initState();
_startCountDown();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
bool get isCounting => _seconds > 0;
void _startCountDown() {
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!isCounting) {
_timer?.cancel();
}
setState(() {
_seconds--;
});
});
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
if (isCounting) return;
widget.onTap();
},
child: Text(
isCounting ? '$_seconds${l10n.second}' : widget.text,
style: TextStyle(
color: _seconds > 0 ? Colors.grey : widget.afterColor,
),
),
);
}
}

View File

@@ -1,18 +0,0 @@
import 'package:flutter/material.dart';
const _shape = Border();
class ExpandTile extends ExpansionTile {
const ExpandTile({
super.key,
super.leading,
required super.title,
super.children,
super.subtitle,
super.initiallyExpanded,
super.tilePadding,
super.childrenPadding,
super.trailing,
super.controller,
}) : super(shape: _shape, collapsedShape: _shape);
}

View File

@@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
/// 渐隐渐显实现
class FadeIn extends StatefulWidget {
final Widget child;
final Duration duration;
const FadeIn({
super.key,
required this.child,
this.duration = const Duration(milliseconds: 477),
});
@override
_MyFadeInState createState() => _MyFadeInState();
}
class _MyFadeInState extends State<FadeIn> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_animation = Tween(
begin: 0.0,
end: 1.0,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.forward();
return FadeTransition(
opacity: _animation,
child: widget.child,
);
}
}

View File

@@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/ui.dart';
class FutureWidget<T> extends StatelessWidget {
final Future<T> future;
final Widget loading;
final Widget Function(Object? error, StackTrace? trace) error;
final Widget Function(T? data) success;
final Widget Function(AsyncSnapshot<Object?> snapshot)? active;
const FutureWidget({
super.key,
required this.future,
this.loading = UIs.placeholder,
required this.error,
required this.success,
this.active,
});
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: future,
builder: (context, snapshot) {
if (snapshot.hasError) {
return error(snapshot.error, snapshot.stackTrace);
}
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return loading;
case ConnectionState.active:
if (active != null) {
return active!(snapshot);
}
return loading;
case ConnectionState.done:
return success(snapshot.data);
}
},
);
}
}

View File

@@ -1,28 +0,0 @@
import 'package:flutter/material.dart';
final class IconBtn extends StatelessWidget {
final IconData icon;
final double size;
final Color? color;
final void Function() onTap;
const IconBtn({
super.key,
required this.icon,
required this.onTap,
this.size = 17,
this.color,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(17),
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(icon, size: size, color: color),
),
);
}
}

View File

@@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/ui.dart';
final class IconTextBtn extends StatelessWidget {
final String text;
final IconData icon;
final VoidCallback? onPressed;
final Orientation orientation;
const IconTextBtn({
super.key,
required this.text,
required this.icon,
this.onPressed,
this.orientation = Orientation.portrait,
});
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onPressed,
tooltip: text,
icon: orientation == Orientation.landscape
? Row(
children: [
Icon(icon),
UIs.width7,
Text(text, style: UIs.text13Grey),
],
)
: Column(
children: [
Icon(icon),
UIs.height7,
Text(text, style: UIs.text13Grey),
],
));
}
}

View File

@@ -1,102 +0,0 @@
import 'package:flutter/material.dart';
import 'cardx.dart';
class Input extends StatefulWidget {
final TextEditingController? controller;
final int maxLines;
final int? minLines;
final String? hint;
final String? label;
final void Function(String)? onSubmitted;
final void Function(String)? onChanged;
final bool obscureText;
final IconData? icon;
final TextInputType? type;
final FocusNode? node;
final bool autoCorrect;
final bool suggestiion;
final String? errorText;
final Widget? prefix;
final bool autoFocus;
final void Function(bool)? onViewPwdTap;
const Input({
super.key,
this.controller,
this.maxLines = 1,
this.minLines,
this.hint,
this.label,
this.onSubmitted,
this.onChanged,
this.obscureText = false,
this.icon,
this.type,
this.node,
this.autoCorrect = false,
this.suggestiion = false,
this.errorText,
this.prefix,
this.autoFocus = false,
this.onViewPwdTap,
});
@override
State<StatefulWidget> createState() => _InputState();
}
class _InputState extends State<Input> {
bool _obscureText = false;
@override
void initState() {
super.initState();
_obscureText = widget.obscureText;
}
@override
Widget build(BuildContext context) {
return CardX(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: TextField(
controller: widget.controller,
maxLines: widget.maxLines,
minLines: widget.minLines,
obscureText: _obscureText,
decoration: InputDecoration(
hintText: widget.hint,
labelText: widget.label,
errorText: widget.errorText,
border: InputBorder.none,
prefixIcon: widget.icon == null ? null : Icon(widget.icon),
prefix: widget.prefix,
suffixIcon: widget.obscureText
? IconButton(
icon: Icon(
_obscureText ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
if (widget.onViewPwdTap != null) {
widget.onViewPwdTap?.call(_obscureText);
}
},
)
: null,
),
keyboardType: widget.type,
focusNode: widget.node,
autocorrect: widget.autoCorrect,
enableSuggestions: widget.suggestiion,
autofocus: widget.autoFocus,
onSubmitted: widget.onSubmitted,
onChanged: widget.onChanged,
),
),
);
}
}

View File

@@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/ui.dart';
final class KvRow extends StatelessWidget {
final String k;
final String v;
final void Function()? onTap;
final Widget? Function()? kBuilder;
final Widget? Function()? vBuilder;
const KvRow({
super.key,
required this.k,
required this.v,
this.onTap,
this.kBuilder,
this.vBuilder,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
kBuilder?.call() ?? Text(k, style: UIs.text12),
UIs.width7,
vBuilder?.call() ??
Text(
v,
style: UIs.text11Grey,
overflow: TextOverflow.ellipsis,
),
if (onTap != null) UIs.width7,
if (onTap != null) const Icon(Icons.keyboard_arrow_right, size: 16),
],
),
),
);
}
}

View File

@@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/extension/context/snackbar.dart';
import 'package:toolbox/core/utils/ui.dart';
import 'package:toolbox/data/res/color.dart';
final class SimpleMarkdown extends StatelessWidget {
const SimpleMarkdown({
super.key,
required this.data,
this.styleSheet,
});
final String data;
final MarkdownStyleSheet? styleSheet;
@override
Widget build(BuildContext context) {
return MarkdownBody(
data: data,
onTapLink: (text, href, title) {
if (href != null && href.isNotEmpty) {
openUrl(href);
return;
}
context.showSnackBar(l10n.failed);
},
styleSheet: styleSheet?.copyWith(
a: TextStyle(color: primaryColor),
) ??
MarkdownStyleSheet(
a: TextStyle(color: primaryColor),
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:circle_chart/circle_chart.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
final class PercentCircle extends StatelessWidget {
final double percent;
@@ -23,7 +23,7 @@ final class PercentCircle extends StatelessWidget {
alignment: Alignment.center,
children: [
CircleChart(
progressColor: primaryColor,
progressColor: UIs.primaryColor,
progressNumber: percent,
maxNumber: 100,
width: 57,

View File

@@ -1,8 +1,9 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/ui.dart';
class PopupMenu<T> extends StatelessWidget {
final List<PopupMenuEntry<T>> items;
final List<T> items;
final Widget Function(T) builder;
final void Function(T) onSelected;
final Widget child;
final EdgeInsetsGeometry padding;
@@ -11,6 +12,7 @@ class PopupMenu<T> extends StatelessWidget {
const PopupMenu({
super.key,
required this.items,
required this.builder,
required this.onSelected,
this.child = UIs.popMenuChild,
this.padding = const EdgeInsets.all(7),
@@ -20,7 +22,9 @@ class PopupMenu<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenuButton<T>(
itemBuilder: (_) => items,
itemBuilder: (_) => items
.map((e) => PopupMenuItem(value: e, child: builder(e)))
.toList(),
onSelected: onSelected,
initialValue: initialValue,
padding: padding,

View File

@@ -1,25 +0,0 @@
import 'package:flutter/material.dart';
final class AvgWidthRow extends StatelessWidget {
final List<Widget> children;
final double? width;
final double padding;
const AvgWidthRow({
super.key,
required this.children,
this.width,
this.padding = 0,
});
@override
Widget build(BuildContext context) {
final width =
((this.width ?? MediaQuery.of(context).size.width) - padding) /
children.length;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: children.map((e) => SizedBox(width: width, child: e)).toList(),
);
}
}

View File

@@ -1,87 +0,0 @@
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/common.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/view/widget/future_widget.dart';
final class SearchPage<T> extends SearchDelegate<T> {
final Future<List<T>> Function(String) future;
final Widget Function(BuildContext, T) builder;
final EdgeInsetsGeometry? padding;
final Duration throttleInterval;
List<T> _cache = [];
DateTime? _lastSearch;
SearchPage({
required this.future,
required this.builder,
this.padding,
this.throttleInterval = const Duration(milliseconds: 200),
});
@override
List<Widget>? buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
},
),
];
}
@override
Widget? buildLeading(BuildContext context) {
return IconButton(
onPressed: context.pop,
icon: const Icon(Icons.arrow_back),
);
}
@override
Widget buildResults(BuildContext context) {
return _buildList(context);
}
@override
Widget buildSuggestions(BuildContext context) {
return _buildList(context);
}
Widget _buildList(BuildContext context) {
return FutureWidget(
future: _search(query),
loading: const Center(child: UIs.centerSizedLoading),
error: (error, trace) {
return Center(
child: Text('$error\n$trace'),
);
},
success: (list) {
if (list == null || list.isEmpty) {
return Center(child: Text(l10n.noResult));
}
return ListView.builder(
padding: padding,
itemCount: list.length,
itemBuilder: (_, index) => builder(context, list[index]),
);
},
);
}
Future<List<T>> _search(String query) async {
final lastSearch = _lastSearch;
if (lastSearch != null) {
final now = DateTime.now();
if (now.difference(lastSearch) < throttleInterval) {
return _cache;
}
}
_cache = await future(query);
return _cache;
}
}

View File

@@ -1,24 +1,17 @@
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/common.dart';
import 'package:toolbox/core/extension/context/dialog.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/core/extension/context/snackbar.dart';
import 'package:toolbox/core/extension/ssh_client.dart';
import 'package:toolbox/core/extension/uint8list.dart';
import 'package:toolbox/core/utils/platform/base.dart';
import 'package:toolbox/core/utils/platform/path.dart';
import 'package:toolbox/data/model/app/menu/base.dart';
import 'package:toolbox/data/model/app/menu/server_func.dart';
import 'package:toolbox/data/model/app/shell_func.dart';
import 'package:toolbox/data/model/pkg/manager.dart';
import 'package:toolbox/data/model/server/dist.dart';
import 'package:toolbox/data/model/server/snippet.dart';
import 'package:toolbox/data/res/path.dart';
import 'package:toolbox/data/res/provider.dart';
import 'package:toolbox/data/res/store.dart';
import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/view/widget/count_down_btn.dart';
import '../../core/route.dart';
import '../../core/utils/server.dart';
@@ -37,18 +30,8 @@ class ServerFuncBtnsTopRight extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopupMenu<ServerFuncBtn>(
items: ServerFuncBtn.values
.map((e) => PopupMenuItem<ServerFuncBtn>(
value: e,
child: Row(
children: [
e.icon(),
const SizedBox(width: 10),
Text(e.toStr),
],
),
))
.toList(),
items: ServerFuncBtn.values,
builder: (e) => PopMenu.build(e, e.icon, e.toStr),
padding: const EdgeInsets.symmetric(horizontal: 10),
onSelected: (val) => _onTapMoreBtns(val, spi, context),
);
@@ -87,7 +70,7 @@ class ServerFuncBtns extends StatelessWidget {
onPressed: () => _onTapMoreBtns(e, spi, context),
padding: EdgeInsets.zero,
tooltip: e.toStr,
icon: e.icon(),
icon: Icon(e.icon, size: 15),
)
: Padding(
padding: const EdgeInsets.only(bottom: 13),
@@ -97,7 +80,7 @@ class ServerFuncBtns extends StatelessWidget {
IconButton(
onPressed: () => _onTapMoreBtns(e, spi, context),
padding: EdgeInsets.zero,
icon: e.icon(),
icon: Icon(e.icon, size: 17),
),
Text(e.toStr, style: UIs.text11Grey)
],
@@ -119,7 +102,7 @@ void _onTapMoreBtns(
_onPkg(context, spi);
break;
case ServerFuncBtn.sftp:
AppRoute.sftp(spi: spi).checkGo(
AppRoutes.sftp(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
@@ -130,6 +113,7 @@ void _onTapMoreBtns(
return;
}
final snippets = await context.showPickWithTagDialog<Snippet>(
title: l10n.snippet,
tags: Pros.snippet.tags,
itemsBuilder: (e) {
if (e == null) return Pros.snippet.snippets;
@@ -138,23 +122,24 @@ void _onTapMoreBtns(
.toList();
},
name: (e) => e.name,
all: l10n.all,
);
if (snippets == null || snippets.isEmpty) return;
final snippet = snippets.firstOrNull;
if (snippet == null) return;
AppRoute.ssh(spi: spi, initCmd: snippet.fmtWith(spi)).checkGo(
AppRoutes.ssh(spi: spi, initCmd: snippet.fmtWith(spi)).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
break;
case ServerFuncBtn.container:
AppRoute.docker(spi: spi).checkGo(
AppRoutes.docker(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
break;
case ServerFuncBtn.process:
AppRoute.process(spi: spi).checkGo(
AppRoutes.process(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
@@ -163,7 +148,7 @@ void _onTapMoreBtns(
_gotoSSH(spi, context);
break;
case ServerFuncBtn.iperf:
AppRoute.iperf(spi: spi).checkGo(
AppRoutes.iperf(spi: spi).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);
@@ -174,7 +159,7 @@ void _onTapMoreBtns(
void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
// run built-in ssh on macOS due to incompatibility
if (isMobile || isMacOS) {
AppRoute.ssh(spi: spi).go(context);
AppRoutes.ssh(spi: spi).go(context);
return;
}
final extraArgs = <String>[];
@@ -186,7 +171,7 @@ void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
final tempKeyFileName = 'srvbox_pk_${spi.keyId}';
/// For security reason, save the private key file to app doc path
return joinPath(await Paths.doc, tempKeyFileName);
return Paths.doc.joinPath(tempKeyFileName);
}();
final file = File(path);
final shouldGenKey = spi.keyId != null;
@@ -199,12 +184,12 @@ void _gotoSSH(ServerPrivateInfo spi, BuildContext context) async {
}
final sshCommand = ["ssh", "${spi.user}@${spi.ip}"] + extraArgs;
final system = OS.type;
final system = Pfs.type;
switch (system) {
case OS.windows:
case Pfs.windows:
await Process.start("cmd", ["/c", "start"] + sshCommand);
break;
case OS.linux:
case Pfs.linux:
await Process.start("x-terminal-emulator", ["-e"] + sshCommand);
break;
default:
@@ -282,7 +267,7 @@ Future<void> _onPkg(BuildContext context, ServerPrivateInfo spi) async {
// Confirm upgrade
final gotoUpgrade = await context.showRoundDialog<bool>(
title: Text(l10n.attention),
title: l10n.attention,
child: SingleChildScrollView(
child: Text(
'${l10n.pkgUpgradeTip}\n${l10n.foundNUpdate(upgradeable.length)}\n\n$upgradeCmd'),
@@ -298,7 +283,7 @@ Future<void> _onPkg(BuildContext context, ServerPrivateInfo spi) async {
if (gotoUpgrade != true) return;
AppRoute.ssh(spi: spi, initCmd: upgradeCmd).checkGo(
AppRoutes.ssh(spi: spi, initCmd: upgradeCmd).checkGo(
context: context,
check: () => _checkClient(context, spi.id),
);

View File

@@ -1,40 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:toolbox/view/widget/val_builder.dart';
import '../../core/persistant_store.dart';
class StoreSwitch extends StatelessWidget {
final StorePropertyBase<bool> prop;
/// Exec before make change, after validator.
final FutureOr<void> Function(bool)? callback;
/// If return false, the switch will not change.
final bool Function(bool)? validator;
const StoreSwitch({
super.key,
required this.prop,
this.callback,
this.validator,
});
@override
Widget build(BuildContext context) {
return ValBuilder(
listenable: prop.listenable(),
builder: (value) {
return Switch(
value: value,
onChanged: (value) async {
if (validator?.call(value) == false) return;
await callback?.call(value);
prop.put(value);
},
);
},
);
}
}

View File

@@ -1,254 +0,0 @@
import 'package:flutter/material.dart';
import 'package:toolbox/core/extension/context/common.dart';
import 'package:toolbox/core/extension/context/dialog.dart';
import 'package:toolbox/core/extension/context/locale.dart';
import 'package:toolbox/data/res/ui.dart';
import 'package:toolbox/view/widget/input_field.dart';
import 'package:toolbox/view/widget/cardx.dart';
import 'package:toolbox/view/widget/val_builder.dart';
import '../../data/res/color.dart';
const _kTagBtnHeight = 31.0;
class TagBtn extends StatelessWidget {
final String content;
final void Function() onTap;
final bool isEnable;
const TagBtn({
super.key,
required this.onTap,
required this.isEnable,
required this.content,
});
@override
Widget build(BuildContext context) {
return _wrap(
Text(
content,
textAlign: TextAlign.center,
style: isEnable ? UIs.text13 : UIs.text13Grey,
),
onTap: onTap,
);
}
}
class TagEditor extends StatefulWidget {
final List<String> tags;
final void Function(List<String>)? onChanged;
final void Function(String old, String new_)? onRenameTag;
final List<String> allTags;
const TagEditor({
super.key,
required this.tags,
this.onChanged,
this.onRenameTag,
this.allTags = const <String>[],
});
@override
State<StatefulWidget> createState() => _TagEditorState();
}
class _TagEditorState extends State<TagEditor> {
@override
Widget build(BuildContext context) {
return CardX(
child: ListTile(
// Align the place of TextField.prefixIcon
leading: const Padding(
padding: EdgeInsets.only(left: 10),
child: Icon(Icons.tag),
),
title: _buildTags(widget.tags),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () => _showAddTagDialog(),
),
),
);
}
Widget _buildTags(List<String> tags) {
final suggestions = widget.allTags.where((e) => !tags.contains(e)).toList();
final suggestionLen = suggestions.length;
/// Add vertical divider if suggestions.length > 0
final counts = tags.length + suggestionLen + (suggestionLen == 0 ? 0 : 1);
if (counts == 0) return Text(l10n.tag);
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: _kTagBtnHeight),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
if (index < tags.length) {
return _buildTagItem(tags[index]);
} else if (index > tags.length) {
return _buildTagItem(
suggestions[index - tags.length - 1],
isAdd: true,
);
}
return const VerticalDivider();
},
itemCount: counts,
),
);
}
Widget _buildTagItem(String tag, {bool isAdd = false}) {
return _wrap(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'#$tag',
textAlign: TextAlign.center,
style: isAdd ? UIs.text13Grey : UIs.text13,
),
const SizedBox(width: 4.0),
Icon(
isAdd ? Icons.add_circle : Icons.cancel,
size: 13.7,
),
],
),
onTap: () {
if (isAdd) {
widget.tags.add(tag);
} else {
widget.tags.remove(tag);
}
widget.onChanged?.call(widget.tags);
setState(() {});
},
onLongPress: () => _showRenameDialog(tag),
);
}
void _showAddTagDialog() {
final textEditingController = TextEditingController();
context.showRoundDialog(
title: Text(l10n.add),
child: Input(
autoFocus: true,
icon: Icons.tag,
controller: textEditingController,
hint: l10n.tag,
),
actions: [
TextButton(
onPressed: () {
final tag = textEditingController.text;
widget.tags.add(tag.trim());
widget.onChanged?.call(widget.tags);
context.pop();
},
child: Text(l10n.add),
),
],
);
}
void _showRenameDialog(String tag) {
final textEditingController = TextEditingController(text: tag);
context.showRoundDialog(
title: Text(l10n.rename),
child: Input(
autoFocus: true,
icon: Icons.abc,
controller: textEditingController,
hint: l10n.tag,
),
actions: [
TextButton(
onPressed: () {
final newTag = textEditingController.text.trim();
if (newTag.isEmpty) return;
widget.onRenameTag?.call(tag, newTag);
context.pop();
setState(() {});
},
child: Text(l10n.rename),
),
],
);
}
}
class TagSwitcher extends StatelessWidget implements PreferredSizeWidget {
final ValueNotifier<List<String>> tags;
final double width;
final void Function(String?) onTagChanged;
final String? initTag;
const TagSwitcher({
super.key,
required this.tags,
required this.width,
required this.onTagChanged,
this.initTag,
});
@override
Widget build(BuildContext context) {
return ValBuilder(
listenable: tags,
builder: (vals) {
if (vals.isEmpty) return UIs.placeholder;
final items = <String?>[null, ...vals];
return Container(
height: _kTagBtnHeight,
width: width,
padding: const EdgeInsets.symmetric(horizontal: 7),
alignment: Alignment.center,
color: Colors.transparent,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
final item = items[index];
return TagBtn(
content: item == null ? l10n.all : '#$item',
isEnable: initTag == item,
onTap: () => onTagChanged(item),
);
},
itemCount: items.length,
),
);
},
);
}
@override
Size get preferredSize => const Size.fromHeight(_kTagBtnHeight);
}
Widget _wrap(
Widget child, {
void Function()? onTap,
void Function()? onLongPress,
}) {
return Padding(
padding: const EdgeInsets.all(3),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
child: Material(
color: primaryColor.withAlpha(20),
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.fromLTRB(11.7, 2.7, 11.7, 0),
child: child,
),
),
),
),
);
}

View File

@@ -1,5 +1,5 @@
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/ui.dart';
class TwoLineText extends StatelessWidget {
const TwoLineText({super.key, required this.up, required this.down});

View File

@@ -1,80 +0,0 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/data/res/color.dart';
import '../../core/utils/ui.dart';
final _reg = RegExp(
r"(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]*");
class UrlText extends StatelessWidget {
final String text;
final String? replace;
final TextAlign? textAlign;
final TextStyle style;
const UrlText({
super.key,
required this.text,
this.replace,
this.textAlign,
this.style = const TextStyle(),
});
List<InlineSpan> _buildTextSpans(Color c) {
final widgets = <InlineSpan>[];
int start = 0;
for (final match in _reg.allMatches(text)) {
final group0 = match.group(0);
if (group0 != null && group0.isNotEmpty) {
if (start != match.start) {
widgets.add(
TextSpan(
text: text.substring(start, match.start),
style: style.copyWith(color: c),
),
);
}
widgets.add(_LinkTextSpan(
replace: replace,
text: group0,
style: style.copyWith(color: primaryColor),
));
start = match.end;
}
}
if (start < text.length) {
widgets.add(
TextSpan(
text: text.substring(start),
style: style.copyWith(color: c),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return RichText(
textAlign: textAlign ?? TextAlign.start,
text: TextSpan(
children: _buildTextSpans(DynamicColors.content.resolve(context)),
),
);
}
}
class _LinkTextSpan extends TextSpan {
_LinkTextSpan({super.style, required String text, String? replace})
: super(
text: replace ?? text,
recognizer: TapGestureRecognizer()
..onTap = () {
openUrl(text);
},
);
}

View File

@@ -1,35 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
final class ValBuilder<T> extends ValueListenableBuilder<T> {
ValBuilder({
super.key,
required ValueListenable<T> listenable,
required Widget Function(T) builder,
}) : super(
valueListenable: listenable,
builder: (_, val, __) => builder(val),
);
}
final class ValChildBuilder<T> extends ValueListenableBuilder<T> {
ValChildBuilder({
super.key,
required ValueListenable<T> listenable,
required Widget Function(T, Widget?) builder,
super.child,
}) : super(
valueListenable: listenable,
builder: (_, val, child) => builder(val, child),
);
}
final class ListenBuilder extends ListenableBuilder {
ListenBuilder({
super.key,
required super.listenable,
required Widget Function() builder,
}) : super(
builder: (_, __) => builder(),
);
}