mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
opt.: migrate fl_lib
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:countly_flutter/countly_flutter.dart';
|
||||
import 'package:toolbox/core/build_mode.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
class Analysis {
|
||||
static const _url = 'https://countly.lolli.tech';
|
||||
static const _key = '0772e65c696709f879d87db77ae1a811259e3eb9';
|
||||
|
||||
static bool enabled = false;
|
||||
|
||||
static Future<void> init() async {
|
||||
if (enabled) return;
|
||||
if (!BuildMode.isRelease) return;
|
||||
if (!Stores.setting.collectUsage.fetch()) return;
|
||||
if (isAndroid || isIOS) {
|
||||
enabled = true;
|
||||
final config = CountlyConfig(_url, _key)
|
||||
.setLoggingEnabled(false)
|
||||
.enableCrashReporting();
|
||||
await Countly.initWithConfig(config);
|
||||
await Countly.giveAllConsent();
|
||||
}
|
||||
}
|
||||
|
||||
static void recordView(String view) {
|
||||
if (enabled) {
|
||||
Countly.instance.views.startView(view);
|
||||
}
|
||||
}
|
||||
|
||||
static void recordException(Object exception, [bool fatal = false]) {
|
||||
if (enabled) {
|
||||
Countly.logException(exception.toString(), !fatal, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/// See: https://github.com/flutter/flutter/issues/11392
|
||||
///
|
||||
enum _BuildMode {
|
||||
release,
|
||||
debug,
|
||||
profile,
|
||||
}
|
||||
|
||||
final _buildMode = () {
|
||||
if (const bool.fromEnvironment('dart.vm.product')) {
|
||||
return _BuildMode.release;
|
||||
}
|
||||
var result = _BuildMode.profile;
|
||||
assert(() {
|
||||
result = _BuildMode.debug;
|
||||
return true;
|
||||
}());
|
||||
return result;
|
||||
}();
|
||||
|
||||
class BuildMode {
|
||||
static bool isDebug = (_buildMode == _BuildMode.debug);
|
||||
static bool isProfile = (_buildMode == _BuildMode.profile);
|
||||
static bool isRelease = (_buildMode == _BuildMode.release);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _interactiveStates = <MaterialState>{
|
||||
MaterialState.pressed,
|
||||
MaterialState.hovered,
|
||||
MaterialState.focused,
|
||||
MaterialState.selected
|
||||
};
|
||||
|
||||
extension ColorX on Color {
|
||||
String get toHex {
|
||||
final redStr = red.toRadixString(16).padLeft(2, '0');
|
||||
final greenStr = green.toRadixString(16).padLeft(2, '0');
|
||||
final blueStr = blue.toRadixString(16).padLeft(2, '0');
|
||||
return '#$redStr$greenStr$blueStr';
|
||||
}
|
||||
|
||||
bool get isBrightColor {
|
||||
return getBrightnessFromColor == Brightness.light;
|
||||
}
|
||||
|
||||
Brightness get getBrightnessFromColor {
|
||||
return ThemeData.estimateBrightnessForColor(this);
|
||||
}
|
||||
|
||||
MaterialStateProperty<Color?> get materialStateColor {
|
||||
return MaterialStateProperty.resolveWith((states) {
|
||||
if (states.any(_interactiveStates.contains)) {
|
||||
return this;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
MaterialColor get materialColor => MaterialColor(
|
||||
value,
|
||||
{
|
||||
50: withOpacity(0.05),
|
||||
100: withOpacity(0.1),
|
||||
200: withOpacity(0.2),
|
||||
300: withOpacity(0.3),
|
||||
400: withOpacity(0.4),
|
||||
500: withOpacity(0.5),
|
||||
600: withOpacity(0.6),
|
||||
700: withOpacity(0.7),
|
||||
800: withOpacity(0.8),
|
||||
900: withOpacity(0.9),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension ContextX on BuildContext {
|
||||
void pop<T extends Object?>([T? result]) {
|
||||
Navigator.of(this).pop<T>(result);
|
||||
}
|
||||
|
||||
bool get canPop => Navigator.of(this).canPop();
|
||||
|
||||
bool get isDark => Theme.of(this).brightness == Brightness.dark;
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:choice/choice.dart';
|
||||
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/store.dart';
|
||||
import 'package:toolbox/view/widget/choice_chip.dart';
|
||||
import 'package:toolbox/view/widget/tag.dart';
|
||||
import 'package:toolbox/view/widget/val_builder.dart';
|
||||
|
||||
import '../../../data/res/ui.dart';
|
||||
import '../../../view/widget/input_field.dart';
|
||||
|
||||
extension DialogX on BuildContext {
|
||||
Future<T?> showRoundDialog<T>({
|
||||
Widget? child,
|
||||
List<Widget>? actions,
|
||||
Widget? title,
|
||||
bool barrierDismiss = true,
|
||||
void Function(BuildContext)? onContext,
|
||||
}) async {
|
||||
return await showDialog<T>(
|
||||
context: this,
|
||||
barrierDismissible: barrierDismiss,
|
||||
builder: (ctx) {
|
||||
onContext?.call(ctx);
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
content: child,
|
||||
actions: actions,
|
||||
actionsPadding: const EdgeInsets.all(17),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<T> showLoadingDialog<T>({
|
||||
required Future<T> Function() fn,
|
||||
bool barrierDismiss = false,
|
||||
}) async {
|
||||
BuildContext? ctx;
|
||||
showRoundDialog(
|
||||
child: UIs.centerSizedLoading,
|
||||
barrierDismiss: barrierDismiss,
|
||||
onContext: (c) => ctx = c,
|
||||
);
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
} finally {
|
||||
/// Wait for context to be unmounted
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (ctx?.mounted == true) {
|
||||
ctx?.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final _recoredPwd = <String, String>{};
|
||||
|
||||
/// Show a dialog to input password
|
||||
///
|
||||
/// [hostId] set it to null to skip remembering the password
|
||||
Future<String?> showPwdDialog({
|
||||
String? hostId,
|
||||
String? title,
|
||||
String? label,
|
||||
}) async {
|
||||
if (!mounted) return null;
|
||||
return await showRoundDialog<String>(
|
||||
title: Text(title ?? hostId ?? l10n.pwd),
|
||||
child: Input(
|
||||
controller: TextEditingController(text: _recoredPwd[hostId]),
|
||||
autoFocus: true,
|
||||
type: TextInputType.visiblePassword,
|
||||
obscureText: true,
|
||||
onSubmitted: (val) {
|
||||
pop(val);
|
||||
if (hostId != null && Stores.setting.rememberPwdInMem.fetch()) {
|
||||
_recoredPwd[hostId] = val;
|
||||
}
|
||||
},
|
||||
label: label ?? l10n.pwd,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<T>?> showPickDialog<T>({
|
||||
required List<T?> items,
|
||||
String Function(T)? name,
|
||||
bool multi = true,
|
||||
List<T>? initial,
|
||||
bool clearable = false,
|
||||
List<Widget>? actions,
|
||||
}) async {
|
||||
var vals = initial ?? <T>[];
|
||||
final sure = await showRoundDialog<bool>(
|
||||
title: Text(l10n.choose),
|
||||
child: SingleChildScrollView(
|
||||
child: Choice<T>(
|
||||
onChanged: (value) => vals = value,
|
||||
multiple: multi,
|
||||
clearable: clearable,
|
||||
value: vals,
|
||||
builder: (state, _) {
|
||||
return Wrap(
|
||||
children: List<Widget>.generate(
|
||||
items.length,
|
||||
(index) {
|
||||
final item = items[index];
|
||||
if (item == null) return UIs.placeholder;
|
||||
return ChoiceChipX<T>(
|
||||
label: name?.call(item) ?? item.toString(),
|
||||
state: state,
|
||||
value: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
if (actions != null) ...actions,
|
||||
TextButton(
|
||||
onPressed: () => pop(true),
|
||||
child: Text(l10n.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (sure == true && vals.isNotEmpty) {
|
||||
return vals;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<T?> showPickSingleDialog<T>({
|
||||
required List<T?> items,
|
||||
String Function(T)? name,
|
||||
T? initial,
|
||||
bool clearable = false,
|
||||
List<Widget>? actions,
|
||||
}) async {
|
||||
final vals = await showPickDialog<T>(
|
||||
items: items,
|
||||
name: name,
|
||||
multi: false,
|
||||
initial: initial == null ? null : [initial],
|
||||
actions: actions,
|
||||
);
|
||||
if (vals != null && vals.isNotEmpty) {
|
||||
return vals.first;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<T>?> showPickWithTagDialog<T>({
|
||||
required List<T?> Function(String? tag) itemsBuilder,
|
||||
required ValueNotifier<List<String>> tags,
|
||||
String Function(T)? name,
|
||||
List<T>? initial,
|
||||
bool clearable = false,
|
||||
bool multi = false,
|
||||
List<Widget>? actions,
|
||||
}) async {
|
||||
var vals = initial ?? <T>[];
|
||||
final tag = ValueNotifier<String?>(null);
|
||||
final sure = await showRoundDialog<bool>(
|
||||
title: Text(l10n.choose),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListenableBuilder(
|
||||
listenable: tag,
|
||||
builder: (_, __) => TagSwitcher(
|
||||
tags: tags,
|
||||
width: 300,
|
||||
initTag: tag.value,
|
||||
onTagChanged: (e) => tag.value = e,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
SingleChildScrollView(
|
||||
child: ValBuilder(
|
||||
listenable: tag,
|
||||
builder: (val) {
|
||||
final items = itemsBuilder(val);
|
||||
return Choice<T>(
|
||||
onChanged: (value) => vals = value,
|
||||
multiple: multi,
|
||||
clearable: clearable,
|
||||
value: vals,
|
||||
builder: (state, _) {
|
||||
return Wrap(
|
||||
children: List<Widget>.generate(
|
||||
items.length,
|
||||
(index) {
|
||||
final item = items[index];
|
||||
if (item == null) return UIs.placeholder;
|
||||
return ChoiceChipX<T>(
|
||||
label: name?.call(item) ?? item.toString(),
|
||||
state: state,
|
||||
value: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (actions != null) ...actions,
|
||||
TextButton(
|
||||
onPressed: () => pop(true),
|
||||
child: Text(l10n.ok),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (sure == true && vals.isNotEmpty) {
|
||||
return vals;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension SnackBarX on BuildContext {
|
||||
void showSnackBar(String text) =>
|
||||
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||
content: Text(text),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
|
||||
void showSnackBarWithAction(
|
||||
String content,
|
||||
String action,
|
||||
GestureTapCallback onTap,
|
||||
) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
action: SnackBarAction(
|
||||
label: action,
|
||||
onPressed: onTap,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
extension DateTimeX on DateTime {
|
||||
String get hourMinute {
|
||||
return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// Format: 2021-01-01-0000
|
||||
String get numStr {
|
||||
final year = this.year.toString();
|
||||
final month = this.month.toString().padLeft(2, '0');
|
||||
final day = this.day.toString().padLeft(2, '0');
|
||||
final hour = this.hour.toString().padLeft(2, '0');
|
||||
final minute = this.minute.toString().padLeft(2, '0');
|
||||
return '$year-$month-$day-$hour$minute';
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
|
||||
extension DurationX on Duration {
|
||||
String get toStr {
|
||||
final days = inDays;
|
||||
if (days > 0) {
|
||||
return '$days ${l10n.day}';
|
||||
}
|
||||
final hours = inHours % 24;
|
||||
if (hours > 0) {
|
||||
return '$hours ${l10n.hour}';
|
||||
}
|
||||
final minutes = inMinutes % 60;
|
||||
if (minutes > 0) {
|
||||
return '$minutes ${l10n.minute}';
|
||||
}
|
||||
final seconds = inSeconds % 60;
|
||||
return '$seconds ${l10n.second}';
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
extension EnumListX<T> on List<T> {
|
||||
T fromIndex(int index, [T? defaultValue]) {
|
||||
try {
|
||||
return this[index];
|
||||
} catch (e) {
|
||||
if (defaultValue != null) {
|
||||
return defaultValue;
|
||||
}
|
||||
throw Exception('Invalid index: $index');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
extension ListX<T> on List<T> {
|
||||
List<T> joinWith(T item, [bool self = true]) {
|
||||
final list = self ? this : List<T>.from(this);
|
||||
for (var i = length - 1; i > 0; i--) {
|
||||
list.insert(i, item);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
List<T> combine(List<T> other, [bool self = true]) {
|
||||
final list = self ? this : List<T>.from(this);
|
||||
for (var i = 0; i < length; i++) {
|
||||
list[i] = other[i];
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
T? get firstOrNull => isEmpty ? null : first;
|
||||
|
||||
T? get lastOrNull => isEmpty ? null : last;
|
||||
|
||||
T? firstWhereOrNull(bool Function(T element) test) {
|
||||
try {
|
||||
return firstWhere(test);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
T? lastWhereOrNull(bool Function(T element) test) {
|
||||
try {
|
||||
return lastWhere(test);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension LocaleX on Locale {
|
||||
String get code {
|
||||
if (countryCode == null) {
|
||||
return languageCode;
|
||||
}
|
||||
return '${languageCode}_$countryCode';
|
||||
}
|
||||
}
|
||||
|
||||
extension String2Locale on String {
|
||||
Locale? get toLocale {
|
||||
// Issue #151
|
||||
if (isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final parts = split('_');
|
||||
if (parts.length == 1) {
|
||||
return Locale(parts[0]);
|
||||
}
|
||||
return Locale(parts[0], parts[1]);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension MideaQueryX on MediaQueryData {
|
||||
bool get useDoubleColumn => size.width > 639;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
extension NumX on num {
|
||||
String get bytes2Str {
|
||||
const suffix = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
double value = toDouble();
|
||||
int squareTimes = 0;
|
||||
for (; value / 1024 > 1 && squareTimes < suffix.length - 1; squareTimes++) {
|
||||
value /= 1024;
|
||||
}
|
||||
var finalValue = value.toStringAsFixed(1);
|
||||
if (finalValue.endsWith('.0')) {
|
||||
finalValue = finalValue.replaceFirst('.0', '');
|
||||
}
|
||||
return '$finalValue ${suffix[squareTimes]}';
|
||||
}
|
||||
|
||||
String get kb2Str => (this * 1024).bytes2Str;
|
||||
}
|
||||
|
||||
extension BigIntX on BigInt {
|
||||
String get bytes2Str {
|
||||
const suffix = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
double value = toDouble();
|
||||
int squareTimes = 0;
|
||||
for (; value / 1024 > 1 && squareTimes < suffix.length - 1; squareTimes++) {
|
||||
value /= 1024;
|
||||
}
|
||||
var finalValue = value.toStringAsFixed(1);
|
||||
if (finalValue.endsWith('.0')) {
|
||||
finalValue = finalValue.replaceFirst('.0', '');
|
||||
}
|
||||
return '$finalValue ${suffix[squareTimes]}';
|
||||
}
|
||||
|
||||
String get kb2Str => (this * BigInt.from(1024)).bytes2Str;
|
||||
}
|
||||
|
||||
extension IntX on int {
|
||||
Duration secondsToDuration() => Duration(seconds: this);
|
||||
DateTime get tsToDateTime => DateTime.fromMillisecondsSinceEpoch(this * 1000);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import 'package:toolbox/core/extension/listx.dart';
|
||||
import 'package:toolbox/core/persistant_store.dart';
|
||||
|
||||
typedef _OnMove<T> = void Function(List<T>);
|
||||
|
||||
extension OrderX<T> on List<T> {
|
||||
void move(
|
||||
int oldIndex,
|
||||
int newIndex, {
|
||||
StorePropertyBase<List<T>>? property,
|
||||
_OnMove<T>? onMove,
|
||||
}) {
|
||||
if (oldIndex == newIndex) return;
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final item = this[oldIndex];
|
||||
removeAt(oldIndex);
|
||||
insert(newIndex, item);
|
||||
property?.put(this);
|
||||
onMove?.call(this);
|
||||
}
|
||||
|
||||
void update(T id, T newId) {
|
||||
final index = indexOf(id);
|
||||
if (index == -1) return;
|
||||
this[index] = newId;
|
||||
}
|
||||
|
||||
int index(T id) {
|
||||
return indexOf(id);
|
||||
}
|
||||
|
||||
void moveByItem(
|
||||
int o,
|
||||
int n, {
|
||||
/// The list after filtering.
|
||||
///
|
||||
/// It's used to find the index of the item.
|
||||
List<T>? filtered,
|
||||
StorePropertyBase<List<T>>? property,
|
||||
_OnMove<T>? onMove,
|
||||
}) {
|
||||
if (o == n) return;
|
||||
if (o < n) {
|
||||
n -= 1;
|
||||
}
|
||||
final index = indexOf((filtered ?? this)[o]);
|
||||
if (index == -1) return;
|
||||
var newIndex = indexOf((filtered ?? this)[n]);
|
||||
if (newIndex == -1) return;
|
||||
if (o < n) {
|
||||
newIndex += 1;
|
||||
}
|
||||
move(index, newIndex, property: property, onMove: onMove);
|
||||
}
|
||||
|
||||
/// order: ['d', 'b', 'e']
|
||||
/// this: ['a', 'b', 'c', 'd']\
|
||||
/// result: ['d', 'b', 'a', 'c']\
|
||||
/// return: ['e']
|
||||
List<String> reorder({
|
||||
required List<String> order,
|
||||
required bool Function(T, String) finder,
|
||||
}) {
|
||||
final newOrder = <T>[];
|
||||
final missed = <T>[];
|
||||
final surplus = <String>[];
|
||||
for (final id in order.toSet()) {
|
||||
final item = firstWhereOrNull((element) => finder(element, id));
|
||||
if (item == null) {
|
||||
surplus.add(id);
|
||||
} else {
|
||||
newOrder.add(item);
|
||||
}
|
||||
}
|
||||
for (final item in this) {
|
||||
if (!newOrder.contains(item)) {
|
||||
missed.add(item);
|
||||
}
|
||||
}
|
||||
clear();
|
||||
addAll(newOrder);
|
||||
addAll(missed);
|
||||
return surplus;
|
||||
}
|
||||
|
||||
/// Dart uses memory address to compare objects by default.
|
||||
/// This method compares the values of the objects.
|
||||
bool equals(List<T> other) {
|
||||
if (length != other.length) return false;
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] != other[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:dartssh2/dartssh2.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
import 'package:toolbox/core/extension/uint8list.dart';
|
||||
|
||||
import '../../data/res/misc.dart';
|
||||
|
||||
@@ -81,7 +79,7 @@ extension SSHClientX on SSHClient {
|
||||
isRequestingPwd = true;
|
||||
final user = Miscs.pwdRequestWithUserReg.firstMatch(data)?.group(1);
|
||||
if (context == null) return;
|
||||
final pwd = await context.showPwdDialog(title: user, hostId: id);
|
||||
final pwd = await context.showPwdDialog(title: user, id: id);
|
||||
if (pwd == null || pwd.isEmpty) {
|
||||
session.kill(SSHSignal.TERM);
|
||||
} else {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension StringX on String {
|
||||
/// Format: `#8b2252` or `8b2252`
|
||||
Color? get hexToColor {
|
||||
final hexCode = replaceAll('#', '');
|
||||
final val = int.tryParse('FF$hexCode', radix: 16);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
return Color(val);
|
||||
}
|
||||
|
||||
Uint8List get uint8List => Uint8List.fromList(utf8.encode(this));
|
||||
|
||||
/// Upper the first letter.
|
||||
String get upperFirst {
|
||||
if (isEmpty) {
|
||||
return this;
|
||||
}
|
||||
final runes = codeUnits;
|
||||
if (runes[0] >= 97 && runes[0] <= 122) {
|
||||
final origin = String.fromCharCode(runes[0]);
|
||||
final upper = origin.toUpperCase();
|
||||
return replaceFirst(origin, upper);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
extension StringXX on String? {
|
||||
String? get selfIfNotNullEmpty => this?.isEmpty == true ? null : this;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
extension FutureUint8ListX on Future<Uint8List> {
|
||||
Future<String> get string async => utf8.decode(await this);
|
||||
Future<ByteData> get byteData async => (await this).buffer.asByteData();
|
||||
}
|
||||
|
||||
extension Uint8ListX on Uint8List {
|
||||
String get string => utf8.decode(this);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/view/widget/cardx.dart';
|
||||
|
||||
extension WidgetX on Widget {
|
||||
Widget get card {
|
||||
return CardX(child: this);
|
||||
}
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
|
||||
// abstract final class SecureStore {
|
||||
// static const _secureStorage = FlutterSecureStorage();
|
||||
|
||||
// static HiveAesCipher? _cipher;
|
||||
|
||||
// static const _hiveKey = 'hive_key';
|
||||
|
||||
// static Future<void> init() async {
|
||||
// final encryptionKeyString = await _secureStorage.read(key: _hiveKey);
|
||||
// if (encryptionKeyString == null) {
|
||||
// final key = Hive.generateSecureKey();
|
||||
// await _secureStorage.write(
|
||||
// key: _hiveKey,
|
||||
// value: base64UrlEncode(key),
|
||||
// );
|
||||
// }
|
||||
// final key = await _secureStorage.read(key: _hiveKey);
|
||||
// if (key == null) {
|
||||
// throw Exception('Failed to init SecureStore');
|
||||
// }
|
||||
// final encryptionKeyUint8List = base64Url.decode(key);
|
||||
// _cipher = HiveAesCipher(encryptionKeyUint8List);
|
||||
// }
|
||||
// }
|
||||
|
||||
final _logger = Logger('Store');
|
||||
|
||||
class PersistentStore {
|
||||
late final Box box;
|
||||
|
||||
final String boxName;
|
||||
|
||||
PersistentStore(this.boxName);
|
||||
|
||||
Future<void> init() async => box = await Hive.openBox(
|
||||
boxName,
|
||||
//encryptionCipher: SecureStore._cipher,
|
||||
);
|
||||
|
||||
_StoreProperty<T> property<T>(
|
||||
String key,
|
||||
T defaultValue, {
|
||||
bool updateLastModified = true,
|
||||
}) {
|
||||
return _StoreProperty<T>(
|
||||
box,
|
||||
key,
|
||||
defaultValue,
|
||||
updateLastModified: updateLastModified,
|
||||
);
|
||||
}
|
||||
|
||||
_StoreListProperty<T> listProperty<T>(
|
||||
String key,
|
||||
List<T> defaultValue, {
|
||||
bool updateLastModified = true,
|
||||
}) {
|
||||
return _StoreListProperty<T>(
|
||||
box,
|
||||
key,
|
||||
defaultValue,
|
||||
updateLastModified: updateLastModified,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension BoxX on Box {
|
||||
static const _internalPreffix = '_sbi_';
|
||||
|
||||
/// Last modified timestamp
|
||||
static const String lastModifiedKey = '${_internalPreffix}lastModified';
|
||||
int? get lastModified {
|
||||
final val = get(lastModifiedKey);
|
||||
if (val == null || val is! int) {
|
||||
final time = timeStamp;
|
||||
put(lastModifiedKey, time);
|
||||
return time;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
Future<void> updateLastModified([int? time]) => put(
|
||||
lastModifiedKey,
|
||||
time ?? timeStamp,
|
||||
);
|
||||
|
||||
/// Convert db to json
|
||||
Map<String, dynamic> toJson({bool includeInternal = true}) {
|
||||
final json = <String, dynamic>{};
|
||||
for (final key in keys) {
|
||||
if (key is String &&
|
||||
key.startsWith(_internalPreffix) &&
|
||||
!includeInternal) {
|
||||
continue;
|
||||
}
|
||||
json[key] = get(key);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class StorePropertyBase<T> {
|
||||
ValueListenable<T> listenable();
|
||||
T fetch();
|
||||
Future<void> put(T value);
|
||||
Future<void> delete();
|
||||
}
|
||||
|
||||
class _StoreProperty<T> implements StorePropertyBase<T> {
|
||||
_StoreProperty(
|
||||
this._box,
|
||||
this._key,
|
||||
this.defaultValue, {
|
||||
this.updateLastModified = true,
|
||||
});
|
||||
|
||||
final Box _box;
|
||||
final String _key;
|
||||
T defaultValue;
|
||||
bool updateLastModified;
|
||||
|
||||
@override
|
||||
ValueListenable<T> listenable() {
|
||||
return PropertyListenable<T>(_box, _key, defaultValue);
|
||||
}
|
||||
|
||||
@override
|
||||
T fetch() {
|
||||
final stored = _box.get(_key, defaultValue: defaultValue);
|
||||
if (stored is! T) {
|
||||
_logger.warning('StoreProperty("$_key") is: ${stored.runtimeType}');
|
||||
return defaultValue;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> put(T value) {
|
||||
if (updateLastModified) _box.updateLastModified();
|
||||
return _box.put(_key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete() {
|
||||
return _box.delete(_key);
|
||||
}
|
||||
}
|
||||
|
||||
class _StoreListProperty<T> implements StorePropertyBase<List<T>> {
|
||||
_StoreListProperty(
|
||||
this._box,
|
||||
this._key,
|
||||
this.defaultValue, {
|
||||
this.updateLastModified = true,
|
||||
});
|
||||
|
||||
final Box _box;
|
||||
final String _key;
|
||||
List<T> defaultValue;
|
||||
bool updateLastModified;
|
||||
|
||||
@override
|
||||
ValueListenable<List<T>> listenable() {
|
||||
return PropertyListenable<List<T>>(_box, _key, defaultValue);
|
||||
}
|
||||
|
||||
@override
|
||||
List<T> fetch() {
|
||||
final val = _box.get(_key, defaultValue: defaultValue)!;
|
||||
try {
|
||||
if (val is! List) {
|
||||
final exception = 'StoreListProperty("$_key") is: ${val.runtimeType}';
|
||||
_logger.warning(exception);
|
||||
throw Exception(exception);
|
||||
}
|
||||
return List<T>.from(val);
|
||||
} catch (_) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> put(List<T> value) {
|
||||
if (updateLastModified) _box.updateLastModified();
|
||||
return _box.put(_key, value);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete() {
|
||||
return _box.delete(_key);
|
||||
}
|
||||
}
|
||||
|
||||
class PropertyListenable<T> extends ValueListenable<T> {
|
||||
PropertyListenable(this.box, this.key, this.defaultValue);
|
||||
|
||||
final Box box;
|
||||
final String key;
|
||||
T? defaultValue;
|
||||
|
||||
final List<VoidCallback> _listeners = [];
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
_subscription ??= box.watch().listen((event) {
|
||||
if (key == event.key) {
|
||||
for (var listener in _listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
_listeners.remove(listener);
|
||||
|
||||
if (_listeners.isEmpty) {
|
||||
_subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
T get value {
|
||||
final val = box.get(key, defaultValue: defaultValue);
|
||||
if (val == null || val is! T) {
|
||||
return defaultValue!;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/analysis.dart';
|
||||
import 'package:toolbox/data/model/server/private_key_info.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
@@ -35,14 +34,13 @@ import '../view/page/snippet/list.dart';
|
||||
import '../view/page/storage/sftp.dart';
|
||||
import '../view/page/storage/sftp_mission.dart';
|
||||
|
||||
class AppRoute {
|
||||
class AppRoutes {
|
||||
final Widget page;
|
||||
final String title;
|
||||
|
||||
AppRoute(this.page, this.title);
|
||||
AppRoutes(this.page, this.title);
|
||||
|
||||
Future<T?> go<T>(BuildContext context) {
|
||||
Analysis.recordView(title);
|
||||
return Navigator.push<T>(
|
||||
context,
|
||||
Stores.setting.cupertinoRoute.fetch()
|
||||
@@ -61,49 +59,49 @@ class AppRoute {
|
||||
return Future.value(null);
|
||||
}
|
||||
|
||||
static AppRoute serverDetail({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(ServerDetailPage(key: key, spi: spi), 'server_detail');
|
||||
static AppRoutes serverDetail({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoutes(ServerDetailPage(key: key, spi: spi), 'server_detail');
|
||||
}
|
||||
|
||||
static AppRoute serverTab({Key? key}) {
|
||||
return AppRoute(ServerPage(key: key), 'server_tab');
|
||||
static AppRoutes serverTab({Key? key}) {
|
||||
return AppRoutes(ServerPage(key: key), 'server_tab');
|
||||
}
|
||||
|
||||
static AppRoute serverEdit({Key? key, ServerPrivateInfo? spi}) {
|
||||
return AppRoute(
|
||||
static AppRoutes serverEdit({Key? key, ServerPrivateInfo? spi}) {
|
||||
return AppRoutes(
|
||||
ServerEditPage(spi: spi),
|
||||
'server_${spi == null ? 'add' : 'edit'}',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute keyEdit({Key? key, PrivateKeyInfo? pki}) {
|
||||
return AppRoute(
|
||||
static AppRoutes keyEdit({Key? key, PrivateKeyInfo? pki}) {
|
||||
return AppRoutes(
|
||||
PrivateKeyEditPage(pki: pki),
|
||||
'key_${pki == null ? 'add' : 'edit'}',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute keyList({Key? key}) {
|
||||
return AppRoute(PrivateKeysListPage(key: key), 'key_detail');
|
||||
static AppRoutes keyList({Key? key}) {
|
||||
return AppRoutes(PrivateKeysListPage(key: key), 'key_detail');
|
||||
}
|
||||
|
||||
static AppRoute snippetEdit({Key? key, Snippet? snippet}) {
|
||||
return AppRoute(
|
||||
static AppRoutes snippetEdit({Key? key, Snippet? snippet}) {
|
||||
return AppRoutes(
|
||||
SnippetEditPage(snippet: snippet),
|
||||
'snippet_${snippet == null ? 'add' : 'edit'}',
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute snippetList({Key? key}) {
|
||||
return AppRoute(SnippetListPage(key: key), 'snippet_detail');
|
||||
static AppRoutes snippetList({Key? key}) {
|
||||
return AppRoutes(SnippetListPage(key: key), 'snippet_detail');
|
||||
}
|
||||
|
||||
static AppRoute ssh({
|
||||
static AppRoutes ssh({
|
||||
Key? key,
|
||||
required ServerPrivateInfo spi,
|
||||
String? initCmd,
|
||||
}) {
|
||||
return AppRoute(
|
||||
return AppRoutes(
|
||||
SSHPage(
|
||||
key: key,
|
||||
spi: spi,
|
||||
@@ -113,13 +111,13 @@ class AppRoute {
|
||||
);
|
||||
}
|
||||
|
||||
static AppRoute sshVirtKeySetting({Key? key}) {
|
||||
return AppRoute(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
||||
static AppRoutes sshVirtKeySetting({Key? key}) {
|
||||
return AppRoutes(SSHVirtKeySettingPage(key: key), 'ssh_virt_key_setting');
|
||||
}
|
||||
|
||||
static AppRoute localStorage(
|
||||
static AppRoutes localStorage(
|
||||
{Key? key, bool isPickFile = false, String? initDir}) {
|
||||
return AppRoute(
|
||||
return AppRoutes(
|
||||
LocalStoragePage(
|
||||
key: key,
|
||||
isPickFile: isPickFile,
|
||||
@@ -128,16 +126,16 @@ class AppRoute {
|
||||
'local_storage');
|
||||
}
|
||||
|
||||
static AppRoute sftpMission({Key? key}) {
|
||||
return AppRoute(SftpMissionPage(key: key), 'sftp_mission');
|
||||
static AppRoutes sftpMission({Key? key}) {
|
||||
return AppRoutes(SftpMissionPage(key: key), 'sftp_mission');
|
||||
}
|
||||
|
||||
static AppRoute sftp(
|
||||
static AppRoutes sftp(
|
||||
{Key? key,
|
||||
required ServerPrivateInfo spi,
|
||||
String? initPath,
|
||||
bool isSelect = false}) {
|
||||
return AppRoute(
|
||||
return AppRoutes(
|
||||
SftpPage(
|
||||
key: key,
|
||||
spi: spi,
|
||||
@@ -147,28 +145,28 @@ class AppRoute {
|
||||
'sftp');
|
||||
}
|
||||
|
||||
static AppRoute backup({Key? key}) {
|
||||
return AppRoute(BackupPage(key: key), 'backup');
|
||||
static AppRoutes backup({Key? key}) {
|
||||
return AppRoutes(BackupPage(key: key), 'backup');
|
||||
}
|
||||
|
||||
static AppRoute debug({Key? key}) {
|
||||
return AppRoute(DebugPage(key: key), 'debug');
|
||||
static AppRoutes debug({Key? key}) {
|
||||
return AppRoutes(DebugPage(key: key), 'debug');
|
||||
}
|
||||
|
||||
static AppRoute docker({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(ContainerPage(key: key, spi: spi), 'docker');
|
||||
static AppRoutes docker({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoutes(ContainerPage(key: key, spi: spi), 'docker');
|
||||
}
|
||||
|
||||
/// - Pop true if the text is changed & [path] is not null
|
||||
/// - Pop text if [path] is null
|
||||
static AppRoute editor({
|
||||
static AppRoutes editor({
|
||||
Key? key,
|
||||
String? path,
|
||||
String? text,
|
||||
String? langCode,
|
||||
String? title,
|
||||
}) {
|
||||
return AppRoute(
|
||||
return AppRoutes(
|
||||
EditorPage(
|
||||
key: key,
|
||||
path: path,
|
||||
@@ -179,45 +177,45 @@ class AppRoute {
|
||||
'editor');
|
||||
}
|
||||
|
||||
// static AppRoute fullscreen({Key? key}) {
|
||||
// return AppRoute(FullScreenPage(key: key), 'fullscreen');
|
||||
// static AppRoutes fullscreen({Key? key}) {
|
||||
// return AppRoutes(FullScreenPage(key: key), 'fullscreen');
|
||||
// }
|
||||
|
||||
static AppRoute home({Key? key}) {
|
||||
return AppRoute(HomePage(key: key), 'home');
|
||||
static AppRoutes home({Key? key}) {
|
||||
return AppRoutes(HomePage(key: key), 'home');
|
||||
}
|
||||
|
||||
static AppRoute ping({Key? key}) {
|
||||
return AppRoute(PingPage(key: key), 'ping');
|
||||
static AppRoutes ping({Key? key}) {
|
||||
return AppRoutes(PingPage(key: key), 'ping');
|
||||
}
|
||||
|
||||
static AppRoute process({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(ProcessPage(key: key, spi: spi), 'process');
|
||||
static AppRoutes process({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoutes(ProcessPage(key: key, spi: spi), 'process');
|
||||
}
|
||||
|
||||
static AppRoute settings({Key? key}) {
|
||||
return AppRoute(SettingPage(key: key), 'setting');
|
||||
static AppRoutes settings({Key? key}) {
|
||||
return AppRoutes(SettingPage(key: key), 'setting');
|
||||
}
|
||||
|
||||
static AppRoute serverOrder({Key? key}) {
|
||||
return AppRoute(ServerOrderPage(key: key), 'server_order');
|
||||
static AppRoutes serverOrder({Key? key}) {
|
||||
return AppRoutes(ServerOrderPage(key: key), 'server_order');
|
||||
}
|
||||
|
||||
static AppRoute serverDetailOrder({Key? key}) {
|
||||
return AppRoute(ServerDetailOrderPage(key: key), 'server_detail_order');
|
||||
static AppRoutes serverDetailOrder({Key? key}) {
|
||||
return AppRoutes(ServerDetailOrderPage(key: key), 'server_detail_order');
|
||||
}
|
||||
|
||||
static AppRoute iosSettings({Key? key}) {
|
||||
return AppRoute(IOSSettingsPage(key: key), 'ios_setting');
|
||||
static AppRoutes iosSettings({Key? key}) {
|
||||
return AppRoutes(IOSSettingsPage(key: key), 'ios_setting');
|
||||
}
|
||||
|
||||
static AppRoute androidSettings({Key? key}) {
|
||||
return AppRoute(AndroidSettingsPage(key: key), 'android_setting');
|
||||
static AppRoutes androidSettings({Key? key}) {
|
||||
return AppRoutes(AndroidSettingsPage(key: key), 'android_setting');
|
||||
}
|
||||
|
||||
static AppRoute snippetResult(
|
||||
static AppRoutes snippetResult(
|
||||
{Key? key, required List<SnippetResult?> results}) {
|
||||
return AppRoute(
|
||||
return AppRoutes(
|
||||
SnippetResultPage(
|
||||
key: key,
|
||||
results: results,
|
||||
@@ -225,15 +223,15 @@ class AppRoute {
|
||||
'snippet_result');
|
||||
}
|
||||
|
||||
static AppRoute iperf({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(IPerfPage(key: key, spi: spi), 'iperf');
|
||||
static AppRoutes iperf({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoutes(IPerfPage(key: key, spi: spi), 'iperf');
|
||||
}
|
||||
|
||||
static AppRoute serverFuncBtnsOrder({Key? key}) {
|
||||
return AppRoute(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
|
||||
static AppRoutes serverFuncBtnsOrder({Key? key}) {
|
||||
return AppRoutes(ServerFuncBtnsOrderPage(key: key), 'server_func_btns_seq');
|
||||
}
|
||||
|
||||
static AppRoute pve({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoute(PvePage(key: key, spi: spi), 'pve');
|
||||
static AppRoutes pve({Key? key, required ServerPrivateInfo spi}) {
|
||||
return AppRoutes(PvePage(key: key, spi: spi), 'pve');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.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/utils/misc.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/ui.dart';
|
||||
import 'package:toolbox/data/model/app/update.dart';
|
||||
import 'package:toolbox/data/res/build_data.dart';
|
||||
import 'package:toolbox/data/res/logger.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
import 'package:toolbox/data/service/app.dart';
|
||||
import 'package:toolbox/locator.dart';
|
||||
|
||||
Future<bool> isFileAvailable(String url) async {
|
||||
try {
|
||||
final resp = await Dio().head(url);
|
||||
return resp.statusCode == 200;
|
||||
} catch (e) {
|
||||
Loggers.app.warning('HEAD update file failed', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> doUpdate(BuildContext context, {bool force = false}) async {
|
||||
final update = await locator<AppService>().getUpdate();
|
||||
|
||||
final newest = update.build.last.current;
|
||||
if (newest == null) {
|
||||
Loggers.app.warning('Update not available on ${OS.type}');
|
||||
return;
|
||||
}
|
||||
|
||||
Pros.app.newestBuild = newest;
|
||||
|
||||
if (!force && newest <= BuildData.build) {
|
||||
Loggers.app.info('Update ignored: ${BuildData.build} >= $newest');
|
||||
return;
|
||||
}
|
||||
Loggers.app.info('Update available: $newest');
|
||||
|
||||
final url = update.url.current!;
|
||||
|
||||
if (isFileUrl(url) && !await isFileAvailable(url)) {
|
||||
Loggers.app.warning('Update file not available');
|
||||
return;
|
||||
}
|
||||
|
||||
final min = update.build.min.current;
|
||||
|
||||
if (min != null && min > BuildData.build) {
|
||||
context.showRoundDialog(
|
||||
child: Text(l10n.updateTipTooLow(newest)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => _doUpdate(update, context),
|
||||
child: Text(l10n.ok),
|
||||
)
|
||||
],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.showSnackBarWithAction(
|
||||
'${l10n.updateTip(newest)} \n${update.changelog.current}',
|
||||
l10n.update,
|
||||
() => _doUpdate(update, context),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _doUpdate(AppUpdate update, BuildContext context) async {
|
||||
final url = update.url.current;
|
||||
if (url == null) {
|
||||
Loggers.app.warning('Update url is null');
|
||||
context.showSnackBar(l10n.failed);
|
||||
return;
|
||||
}
|
||||
|
||||
await openUrl(url);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract final class Funcs {
|
||||
static const int _defaultDurationTime = 377;
|
||||
static const String _defaultThrottleId = 'default';
|
||||
static final Map<String, int> startTimeMap = <String, int>{
|
||||
_defaultThrottleId: 0
|
||||
};
|
||||
|
||||
static void throttle(
|
||||
VoidCallback? func, {
|
||||
String id = _defaultThrottleId,
|
||||
int duration = _defaultDurationTime,
|
||||
Function? continueClick,
|
||||
}) {
|
||||
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||
if (currentTime - (startTimeMap[id] ?? 0) > duration) {
|
||||
func?.call();
|
||||
startTimeMap[id] = DateTime.now().millisecondsSinceEpoch;
|
||||
} else {
|
||||
continueClick?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,6 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:plain_notification_token/plain_notification_token.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
Future<String?> pickOneFile() async {
|
||||
Pros.app.moveBg = false;
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.any);
|
||||
Pros.app.moveBg = true;
|
||||
return result?.files.single.path;
|
||||
}
|
||||
|
||||
Future<String?> getToken() async {
|
||||
if (isIOS) {
|
||||
@@ -29,22 +21,3 @@ String? getFileName(String? path) {
|
||||
}
|
||||
return path.split('/').last;
|
||||
}
|
||||
|
||||
/// Join two path with `/`
|
||||
String pathJoin(String path1, String path2) {
|
||||
return path1 + (path1.endsWith('/') ? '' : '/') + path2;
|
||||
}
|
||||
|
||||
/// Check if a url is a file url (ends with a file extension)
|
||||
bool isFileUrl(String url) => url.split('/').last.contains('.');
|
||||
|
||||
int get timeStamp => DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
bool isBaseType(Object? obj) {
|
||||
return obj is String ||
|
||||
obj is int ||
|
||||
obj is double ||
|
||||
obj is bool ||
|
||||
obj is List ||
|
||||
obj is Map;
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:local_auth_android/local_auth_android.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:local_auth_ios/types/auth_messages_ios.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:local_auth/error_codes.dart' as errs;
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
|
||||
abstract final class BioAuth {
|
||||
static final _auth = LocalAuthentication();
|
||||
|
||||
static final isPlatformSupported = isAndroid || isIOS || isWindows;
|
||||
|
||||
static bool _isAuthing = false;
|
||||
|
||||
static Future<bool> get isAvail async {
|
||||
if (!isPlatformSupported) return false;
|
||||
if (!await _auth.canCheckBiometrics) {
|
||||
return false;
|
||||
}
|
||||
final biometrics = await _auth.getAvailableBiometrics();
|
||||
|
||||
/// [biometrics] on Android and Windows is returned with error
|
||||
/// Handle it specially
|
||||
if (isAndroid || isWindows) return biometrics.isNotEmpty;
|
||||
return biometrics.contains(BiometricType.face) ||
|
||||
biometrics.contains(BiometricType.fingerprint);
|
||||
}
|
||||
|
||||
static Future<void> go([int count = 0]) async {
|
||||
if (Stores.setting.useBioAuth.fetch()) {
|
||||
if (!_isAuthing) {
|
||||
_isAuthing = true;
|
||||
final val = await goWithResult();
|
||||
_isAuthing = false;
|
||||
switch (val) {
|
||||
case AuthResult.success:
|
||||
break;
|
||||
case AuthResult.fail:
|
||||
case AuthResult.cancel:
|
||||
go(count + 1);
|
||||
break;
|
||||
case AuthResult.notAvail:
|
||||
Stores.setting.useBioAuth.put(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<AuthResult> goWithResult() async {
|
||||
if (!await isAvail) return AuthResult.notAvail;
|
||||
try {
|
||||
await _auth.stopAuthentication();
|
||||
final reuslt = await _auth.authenticate(
|
||||
localizedReason: l10n.authRequired,
|
||||
options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
stickyAuth: true,
|
||||
),
|
||||
authMessages: [
|
||||
AndroidAuthMessages(
|
||||
biometricHint: l10n.bioAuth,
|
||||
biometricNotRecognized: l10n.failed,
|
||||
biometricRequiredTitle: l10n.authRequired,
|
||||
biometricSuccess: l10n.success,
|
||||
cancelButton: l10n.cancel,
|
||||
),
|
||||
IOSAuthMessages(
|
||||
lockOut: l10n.authRequired,
|
||||
cancelButton: l10n.ok,
|
||||
),
|
||||
]);
|
||||
if (reuslt) {
|
||||
return AuthResult.success;
|
||||
}
|
||||
return AuthResult.fail;
|
||||
} on PlatformException catch (e) {
|
||||
switch (e.code) {
|
||||
case errs.notEnrolled:
|
||||
return AuthResult.notAvail;
|
||||
case errs.lockedOut:
|
||||
case errs.permanentlyLockedOut:
|
||||
exit(0);
|
||||
}
|
||||
return AuthResult.cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AuthResult {
|
||||
success,
|
||||
// Not match
|
||||
fail,
|
||||
// User cancel
|
||||
cancel,
|
||||
// Device doesn't support biometrics
|
||||
notAvail,
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:toolbox/core/extension/stringx.dart';
|
||||
|
||||
enum OS {
|
||||
android,
|
||||
ios,
|
||||
linux,
|
||||
macos,
|
||||
windows,
|
||||
web,
|
||||
fuchsia,
|
||||
unknown;
|
||||
|
||||
static final type = () {
|
||||
if (kIsWeb) {
|
||||
return OS.web;
|
||||
}
|
||||
if (Platform.isAndroid) {
|
||||
return OS.android;
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
return OS.ios;
|
||||
}
|
||||
if (Platform.isLinux) {
|
||||
return OS.linux;
|
||||
}
|
||||
if (Platform.isMacOS) {
|
||||
return OS.macos;
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
return OS.windows;
|
||||
}
|
||||
if (Platform.isFuchsia) {
|
||||
return OS.fuchsia;
|
||||
}
|
||||
return OS.unknown;
|
||||
}();
|
||||
|
||||
@override
|
||||
String toString() => switch (this) {
|
||||
OS.macos => 'macOS',
|
||||
OS.ios => 'iOS',
|
||||
final val => val.name.upperFirst,
|
||||
};
|
||||
}
|
||||
|
||||
final isAndroid = OS.type == OS.android;
|
||||
final isIOS = OS.type == OS.ios;
|
||||
final isLinux = OS.type == OS.linux;
|
||||
final isMacOS = OS.type == OS.macos;
|
||||
final isWindows = OS.type == OS.windows;
|
||||
final isWeb = OS.type == OS.web;
|
||||
final isMobile = OS.type == OS.ios || OS.type == OS.android;
|
||||
final isDesktop =
|
||||
OS.type == OS.linux || OS.type == OS.macos || OS.type == OS.windows;
|
||||
@@ -1,26 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
|
||||
final _pathSep = Platform.pathSeparator;
|
||||
String get pathSeparator => _pathSep;
|
||||
|
||||
/// Available only on desktop,
|
||||
/// return null on mobile
|
||||
String? getHomeDir() {
|
||||
final envVars = Platform.environment;
|
||||
if (isMacOS || isLinux) {
|
||||
return envVars['HOME'];
|
||||
} else if (isWindows) {
|
||||
return envVars['UserProfile'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Join two paths with platform specific separator
|
||||
String joinPath(String path1, String path2) {
|
||||
if (isWindows) {
|
||||
return path1 + (path1.endsWith('\\') ? '' : '\\') + path2;
|
||||
}
|
||||
return path1 + (path1.endsWith('/') ? '' : '/') + path2;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
abstract final class PermUtils {
|
||||
static Future<bool> request(Permission permission) async {
|
||||
final status = await permission.status;
|
||||
if (status.isGranted) {
|
||||
return true;
|
||||
} else {
|
||||
final result = await permission.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
|
||||
abstract final class Shares {
|
||||
static Future<bool> files(List<String> filePaths) async {
|
||||
for (final filePath in filePaths) {
|
||||
if (!await File(filePath).exists()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
var text = '';
|
||||
if (filePaths.length == 1) {
|
||||
text = filePaths.first.split('/').last;
|
||||
} else {
|
||||
text = '${filePaths.length} ${l10n.files}';
|
||||
}
|
||||
Pros.app.moveBg = false;
|
||||
// ignore: deprecated_member_use
|
||||
await Share.shareFiles(filePaths, subject: text);
|
||||
Pros.app.moveBg = true;
|
||||
return filePaths.isNotEmpty;
|
||||
}
|
||||
|
||||
static Future<bool> text(String text) async {
|
||||
Pros.app.moveBg = false;
|
||||
await Share.share(text);
|
||||
Pros.app.moveBg = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void copy(String text) {
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
}
|
||||
|
||||
static Future<String?> paste([String type = 'text/plain']) async {
|
||||
final data = await Clipboard.getData(type);
|
||||
return data?.text;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toolbox/core/extension/context/dialog.dart';
|
||||
import 'package:toolbox/core/extension/context/locale.dart';
|
||||
import 'package:toolbox/data/model/server/server_private_info.dart';
|
||||
import 'package:toolbox/data/res/provider.dart';
|
||||
@@ -14,6 +14,7 @@ abstract final class KeybordInteractive {
|
||||
try {
|
||||
final res = await (ctx ?? Pros.app.ctx)?.showPwdDialog(
|
||||
title: '2FA ${l10n.pwd}',
|
||||
id: spi.id,
|
||||
label: spi.id,
|
||||
);
|
||||
return res == null ? null : [res];
|
||||
|
||||
@@ -2,13 +2,13 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:icloud_storage/icloud_storage.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/model/app/sync.dart';
|
||||
|
||||
import '../../../data/model/app/error.dart';
|
||||
import '../../../data/res/path.dart';
|
||||
|
||||
abstract final class ICloud {
|
||||
static const _containerId = 'iCloud.tech.lolli.serverbox';
|
||||
@@ -31,7 +31,7 @@ abstract final class ICloud {
|
||||
try {
|
||||
await ICloudStorage.upload(
|
||||
containerId: _containerId,
|
||||
filePath: localPath ?? '${await Paths.doc}/$relativePath',
|
||||
filePath: localPath ?? '${Paths.doc}/$relativePath',
|
||||
destinationRelativePath: relativePath,
|
||||
onProgress: (stream) {
|
||||
stream.listen(
|
||||
@@ -85,7 +85,7 @@ abstract final class ICloud {
|
||||
await ICloudStorage.download(
|
||||
containerId: _containerId,
|
||||
relativePath: relativePath,
|
||||
destinationFilePath: localPath ?? '${await Paths.doc}/$relativePath',
|
||||
destinationFilePath: localPath ?? '${Paths.doc}/$relativePath',
|
||||
onProgress: (stream) {
|
||||
stream.listen(
|
||||
null,
|
||||
@@ -139,7 +139,7 @@ abstract final class ICloud {
|
||||
}
|
||||
}));
|
||||
|
||||
final docPath = await Paths.doc;
|
||||
final docPath = Paths.doc;
|
||||
|
||||
/// compare files in iCloud and local
|
||||
missions.addAll(allFiles.map((file) async {
|
||||
@@ -205,7 +205,7 @@ abstract final class ICloud {
|
||||
return;
|
||||
}
|
||||
|
||||
final dlFile = await File(await Paths.bak).readAsString();
|
||||
final dlFile = await File(Paths.bakPath).readAsString();
|
||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
||||
await dlBak.restore();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:computer/computer.dart';
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:toolbox/data/model/app/backup.dart';
|
||||
import 'package:toolbox/data/model/app/error.dart';
|
||||
import 'package:toolbox/data/res/path.dart';
|
||||
import 'package:toolbox/data/res/store.dart';
|
||||
import 'package:webdav_client/webdav_client.dart';
|
||||
|
||||
@@ -37,7 +37,7 @@ abstract final class Webdav {
|
||||
}) async {
|
||||
try {
|
||||
await _client.writeFile(
|
||||
localPath ?? '${await Paths.doc}/$relativePath',
|
||||
localPath ?? '${Paths.doc}/$relativePath',
|
||||
_prefix + relativePath,
|
||||
);
|
||||
} catch (e, s) {
|
||||
@@ -64,7 +64,7 @@ abstract final class Webdav {
|
||||
try {
|
||||
await _client.readFile(
|
||||
_prefix + relativePath,
|
||||
localPath ?? '${await Paths.doc}/$relativePath',
|
||||
localPath ?? '${Paths.doc}/$relativePath',
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.warning('Download $relativePath failed');
|
||||
@@ -104,7 +104,7 @@ abstract final class Webdav {
|
||||
}
|
||||
|
||||
try {
|
||||
final dlFile = await File(await Paths.bak).readAsString();
|
||||
final dlFile = await File(Paths.bakPath).readAsString();
|
||||
final dlBak = await Computer.shared.start(Backup.fromJsonString, dlFile);
|
||||
await dlBak.restore();
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_lib/fl_lib.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:toolbox/core/utils/platform/base.dart';
|
||||
import 'package:toolbox/core/utils/misc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'misc.dart';
|
||||
import '../extension/uint8list.dart';
|
||||
|
||||
Future<bool> openUrl(String url) async {
|
||||
return await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user