opt.: migrate fl_lib
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user