结构初始化

This commit is contained in:
LollipopKit
2021-09-13 14:25:54 +08:00
commit 4dd509a1d9
90 changed files with 2715 additions and 0 deletions

27
lib/app.dart Normal file
View File

@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:toolbox/page/home.dart';
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ToolBox',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'ToolBox'),
);
}
}

34
lib/core/analysis.dart Normal file
View File

@@ -0,0 +1,34 @@
import 'dart:async';
import 'package:countly_flutter/countly_flutter.dart';
class Analysis {
static const _url = 'https://countly.xuty.cc';
static const _key = '80372a2a66424b32d0ac8991bfa1ef058bd36b1f';
static bool _enabled = false;
static Future<void> init(bool debug) async {
if (_url.isEmpty || _key.isEmpty) {
return;
}
_enabled = true;
await Countly.setLoggingEnabled(debug);
await Countly.init(_url, _key);
await Countly.start();
await Countly.enableCrashReporting();
await Countly.giveAllConsent();
print('Countly init successfully.');
}
static void recordView(String view) {
if (!_enabled) return;
Countly.recordView(view);
}
static void recordException(Object exception, [bool fatal = false]) {
if (!_enabled) return;
Countly.logException(exception.toString(), !fatal, null);
}
}

25
lib/core/build_mode.dart Normal file
View File

@@ -0,0 +1,25 @@
/// See: https://github.com/flutter/flutter/issues/11392
///
enum _BuildMode {
release,
debug,
profile,
}
_BuildMode _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);
}

View File

@@ -0,0 +1,78 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
class PersistentStore<E> {
late Box<E> box;
Future<PersistentStore<E>> init({String boxName = 'defaultBox'}) async {
box = await Hive.openBox(boxName);
return this;
}
StoreProperty<T> property<T>(String key, {T? defaultValue}) {
return StoreProperty<T>(box, key, defaultValue);
}
}
class StoreProperty<T> {
StoreProperty(this._box, this._key, this.defaultValue);
final Box _box;
final String _key;
T? defaultValue;
ValueListenable<T> listenable() {
return PropertyListenable<T>(_box, _key, defaultValue);
}
T? fetch() {
return _box.get(_key, defaultValue: defaultValue);
}
Future<void> put(T value) {
return _box.put(_key, value);
}
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 => box.get(key, defaultValue: defaultValue);
}

View File

@@ -0,0 +1,36 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
class ProviderBase with ChangeNotifier {
void setState(void Function() callback) {
callback();
notifyListeners();
}
}
enum ProviderState {
idle,
busy,
}
class BusyProvider extends ProviderBase {
bool _isBusy = false;
bool get isBusy => _isBusy;
setBusyState([bool isBusy = true]) {
_isBusy = isBusy;
notifyListeners();
}
FutureOr<T> busyRun<T>(FutureOr<T> Function() func) async {
setBusyState(true);
try {
return await Future.sync(func);
} catch (e) {
rethrow;
} finally {
setBusyState(false);
}
}
}

14
lib/core/route.dart Normal file
View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:toolbox/core/analysis.dart';
class AppRoute {
final Widget page;
final String title;
AppRoute(this.page, this.title);
void go(BuildContext context) {
Analysis.recordView(title);
Navigator.push(context, MaterialPageRoute(builder: (context) => page));
}
}

42
lib/core/update.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/res/build_data.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) {
print('update file not available: $e');
return false;
}
}
Future<void> doUpdate(BuildContext context, {bool force = false}) async {
final update = await locator<AppService>().getUpdate();
locator<AppProvider>().setNewestBuild(update.newest);
if (!force && update.newest <= BuildData.build) {
print('Update ignored due to current: ${BuildData.build}, '
'update: ${update.newest}');
return;
}
print('Update available: ${update.newest}');
if (Platform.isAndroid && !await isFileAvailable(update.android)) {
return;
}
showSnackBarWithAction(
context,
'${BuildData.name}有更新啦Ver${update.newest}\n${update.changelog}',
'更新',
() => openUrl(Platform.isAndroid ? update.android : update.ios));
}

42
lib/core/utils.dart Normal file
View File

@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void unawaited(Future<void> future) {}
bool isDarkMode(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark;
void showSnackBar(BuildContext context, Widget child) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: child));
void showSnackBarWithAction(
BuildContext context, String content, String action, Function onTap) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(content),
action: SnackBarAction(
label: action,
onPressed: () => onTap,
),
));
}
Future<bool> openUrl(String url) async {
print('openUrl $url');
if (!await canLaunch(url)) {
print('canLaunch false');
return false;
}
final ok = await launch(url, forceSafariVC: false);
if (ok == true) {
return true;
}
print('launch $url failed');
return false;
}

View File

@@ -0,0 +1,44 @@
///
/// Code generated by jsonToDartModel https://ashamp.github.io/jsonToDartModel/
///
class AppUpdate {
/*
{
"newest": 33,
"android": "https://v2.custed.lolli.tech/res/tiku/apk/33.apk",
"ios": "https://",
"min": 30,
"changelog": ""
}
*/
late int newest;
late String android;
late String ios;
late int min;
late String changelog;
AppUpdate({
required this.newest,
required this.android,
required this.ios,
required this.min,
required this.changelog,
});
AppUpdate.fromJson(Map<String, dynamic> json) {
newest = json["newest"]?.toInt();
android = json["android"].toString();
ios = json["ios"].toString();
min = json["min"].toInt();
changelog = json["changelog"].toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["newest"] = newest;
data["android"] = android;
data["ios"] = ios;
data["min"] = min;
data["changelog"] = changelog;
return data;
}
}

View File

@@ -0,0 +1,22 @@
import 'package:toolbox/core/provider_base.dart';
import 'package:toolbox/data/service/app.dart';
class AppProvider extends BusyProvider {
Map? _notify;
Map? get notify => _notify;
int? _newestBuild;
int? get newestBuild => _newestBuild;
Future<void> loadData() async {
setBusyState(true);
final service = AppService();
_notify = await service.getNotify();
setBusyState(false);
notifyListeners();
}
void setNewestBuild(int build) {
_newestBuild = build;
notifyListeners();
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
class DebugProvider extends ChangeNotifier {
final widgets = <Widget>[];
void addText(String text) {
_addText(text);
notifyListeners();
}
void _addText(String text) {
_addWidget(Text(text));
}
void addError(Object error) {
_addError(error);
notifyListeners();
}
void _addError(Object error) {
_addMultiline(error, Colors.red);
}
void addMultiline(Object data, [Color color = Colors.blue]) {
_addMultiline(data, color);
notifyListeners();
}
void _addMultiline(Object data, [Color color = Colors.blue]) {
final widget = Text(
'$data',
style: TextStyle(
color: color,
),
);
_addWidget(SingleChildScrollView(
child: widget,
scrollDirection: Axis.horizontal,
));
}
void addWidget(Widget widget) {
_addWidget(widget);
notifyListeners();
}
void _addWidget(Widget widget) {
final outlined = Container(
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
border: Border.all(
color: Colors.green,
),
),
child: widget,
);
widgets.add(outlined);
}
void clear() {
widgets.clear();
notifyListeners();
}
}

View File

@@ -0,0 +1,10 @@
// This file is generated by ./make.dart
class BuildData {
static const String name = "ToolBox";
static const int build = 40;
static const String engine =
"Flutter 2.2.3 • channel stable • https://github.com/flutter/flutter.git\nFramework • revision f4abaa0735 (9 weeks ago) • 2021-07-01 12:46:11 -0700\nEngine • revision 241c87ad80\nTools • Dart 2.13.4\n";
static const String buildAt = "2021-09-04 09:34:17.507782";
static const int modifications = 0;
}

16
lib/data/res/color.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:toolbox/core/utils.dart';
class DynamicColor {
/// 白天模式显示的颜色
Color light;
/// 暗黑模式显示的颜色
Color dark;
DynamicColor(this.light, this.dark);
resolve(BuildContext context) => isDarkMode(context) ? dark : light;
}
final mainColor = DynamicColor(Colors.black87, Colors.white70);

View File

@@ -0,0 +1,79 @@
const hitokotos = [
'与众不同的生活方式很累人呢,因为找不到借口',
'面对就好,去经历就好。',
'将愿望倾入不愿忘却的回忆中',
'努力是不会背叛自己的,虽然梦想有时会背叛自己',
'用你的笑容去改变这个世界,别让这个世界改变了你的笑容',
'我有在反省,但我不后悔',
'世界上没有一个人能代替另一个人。',
'我的腿让我停下,可是心却不允许我那么做',
'可是就算我们通一千次短信,我们各自的心,大概也只会相互靠近一厘米吧',
'勇气,就是即便害怕也会去做',
'只要有想见面的人,自己就不再是孤单一人',
'我很好奇!',
'在走廊上跌倒会流鼻血,在人生中跌倒会流眼泪。',
'已经无法回到过去了。也不知道将来会是什么模样。',
'相信十年后的八月,我们还会相遇。',
'看到的感受到的永远都不会消失永远都不会忘记,与各种各样的相遇一同永存。',
'我们走过风走过雨,就是没能走进彼此的内心。',
'即使从梦中醒来,还会有回忆留下。',
'悲伤教会了我喜悦。',
'梦总是有会醒来的时候,不会醒的梦总有一天会变成悲伤。',
'真正让我难受的,大概是因为让你看到如此狼狈的自己。',
'真正重要的东西,总是没有的人比拥有的人清楚。',
'我们所过的每个平凡的日常,也许就是连续发生的奇迹',
'要是因为烦恼很痛苦,就选择了轻松的选项,将来一定会后悔',
'心,可是很重的',
'人生在世何其痛苦,所以咖啡至少该甜一点',
'每个人的生命都由秒来计算,因为你永远都不知道下一秒会发生什么。',
'为什么要担心?如果努力了,担心不会让结果变得更好。',
'爱欲于人,犹如执炬,逆风而行,必有烧手之患',
'我们是独立的个体,却不是孤独的存在。',
'幸运的人一生都在被童年治愈,不幸的人一生都在治愈童年。',
'只要结局有可能变好,我们就不能胆怯,就像只要能看到一点蓝天,就不能对天气绝望。',
'无论我们能活多久,我们能够享受的只有无法分割的此刻,此外别无其他。',
'认真的思索,真诚的明辨是非,有这种态度,大概可算是善良吧。',
'多少事,从来急,天地转,光阴迫,一万年太久,只争朝夕。',
'物质决定意识,意识反作用于物质',
'因为痛苦太有价值,因为回忆太珍贵,所以我们更要继续往前走。',
'愿生活不太拥挤,愿笑容不必刻意。愿孤独不再长久,愿碎镜终有重圆。',
'世界之大为何我们相遇,难道是缘分,难道是天意。',
'我们生活在阴沟里,但有人依然仰望星空。',
'宇宙是蚂蚁的梦。',
'时间不在于你拥有多少,而在于你怎样使用。',
'要保持希望在每天清晨太阳升起。',
'我在最没有能力的年纪,遇见了最想照顾一生的人。',
'搞怪的不是红绿灯,而是我数不清的犹豫。',
'醉后不知天在水,满船清梦压星河。',
'人生得意须尽欢,莫让金樽空对月。',
'云想衣裳花想容,春风拂槛露华浓。',
'疏影横斜水清浅,暗香浮动月黄昏。',
'仰天大笑出门去,我辈岂是蓬蒿人。',
'溪云初起日沉阁,山雨欲来风满楼。',
'劝君莫惜金缕衣,劝君惜取少年时。',
'最是人间留不住,朱颜辞镜花辞树。',
'当人们做不到一些事情的时候,他们会对你说你也同样不能。',
'爱你所爱,行你所行,听从你心,无问西东。',
'你现在的气质里,藏着你走过的路,读过的书和爱过的人。',
'我希望在20出头的生命里做一件到八十岁想起来都还会微笑的事。',
'时光会把你雕刻成你应有的样子。',
'冬天之所以那么冷是为了告诉大家身边人的温暖有多重要。',
'你需要找出面对明天的力量。',
'面对无知的嘲笑,我只能为他们默哀。',
'既然忘不掉,就把它留在心中,让时间去冲淡它。',
'我曾经失落失望失掉所有方向,直到看见平凡才是唯一的答案。',
'合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。',
'世间所有的相遇,都是久别重逢。',
'斜晖脉脉水悠悠,肠断白频洲.',
'不要跟过去的自己比,要期待未来的自己,珍爱现在的自己。',
'我知道这个世上有人在等我,尽管我不知道我在等谁。当因为这样,我每天都很快乐。'
'我从远方赶来,恰巧你们也在。',
'Write the code, Change the world.',
'生活不止眼前的苟且,还应该有诗和远方的田野。',
'善恶终有报,天道好轮回。不信抬头看,苍天饶过谁。',
'年轻时最大的财富,不是你的青春,不是你的美貌,也不是你有充沛的精力,而是你有犯错误的机会。',
'闾阎扑地,钟鸣鼎食之家;舸舰迷津,青雀黄龙之舳。',
'鹤汀凫渚,穷岛屿之萦回;桂殿兰宫,即冈峦之体势。',
'螃蟹在剥我的壳,笔记本在写我。漫天的我落在枫叶上雪花上。而你在想我。',
'一个人至少拥有一个梦想,有一个理由去坚强。心若没有栖息的地方,到哪里都是在流浪。',
];

3
lib/data/res/url.dart Normal file
View File

@@ -0,0 +1,3 @@
const backendUrl = 'https://v2.custed.lolli.tech';
const baseUrl = backendUrl + '/res/toolbox';
const joinQQGroupUrl = 'https://jq.qq.com/?_wv=1027&k=G0hUmPAq';

15
lib/data/service/app.dart Normal file
View File

@@ -0,0 +1,15 @@
import 'package:dio/dio.dart';
import 'package:toolbox/data/model/update.dart';
import 'package:toolbox/data/res/url.dart';
class AppService {
Future<Map> getNotify() async {
final resp = await Dio().get('$baseUrl/notify.json');
return resp.data;
}
Future<AppUpdate> getUpdate() async {
final resp = await Dio().get('$baseUrl/update.json');
return AppUpdate.fromJson(resp.data);
}
}

View File

@@ -0,0 +1,6 @@
import 'package:toolbox/core/persistant_store.dart';
class SettingStore extends PersistentStore {
StoreProperty<bool> get receiveNotification =>
property('notify', defaultValue: true);
}

28
lib/locator.dart Normal file
View File

@@ -0,0 +1,28 @@
import 'package:get_it/get_it.dart';
import 'package:toolbox/data/provider/app.dart';
import 'package:toolbox/data/provider/debug.dart';
import 'package:toolbox/data/service/app.dart';
import 'package:toolbox/data/store/setting.dart';
GetIt locator = GetIt.instance;
void setupLocatorForServices() {
locator.registerLazySingleton(() => AppService());
}
void setupLocatorForProviders() {
locator.registerSingleton(AppProvider());
locator.registerSingleton(DebugProvider());
}
Future<void> setupLocatorForStores() async {
final setting = SettingStore();
await setting.init(boxName: 'setting');
locator.registerSingleton(setting);
}
Future<void> setupLocator() async {
await setupLocatorForStores();
setupLocatorForProviders();
setupLocatorForServices();
}

6
lib/main.dart Normal file
View File

@@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
import 'package:toolbox/app.dart';
void main() {
runApp(const MyApp());
}

141
lib/page/home.dart Normal file
View File

@@ -0,0 +1,141 @@
import 'dart:convert';
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late TextEditingController _textEditingController;
late TextEditingController _textEditingControllerResult;
late MediaQueryData _media;
late ThemeData _theme;
static const List<String> _typeOption = [
'base64 decode',
'base64 encode',
'URL encode',
'URL decode'
];
int _typeOptionIndex = 0;
@override
void initState() {
super.initState();
_textEditingController = TextEditingController();
_textEditingControllerResult = TextEditingController();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
_theme = Theme.of(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Column(
children: [
_buildInputTop(),
_buildTypeOption(),
_buildResult(),
],
)),
floatingActionButton: FloatingActionButton(
onPressed: () {
_textEditingControllerResult.text = doConvert();
},
tooltip: 'convert',
child: const Icon(Icons.send),
),
);
}
String doConvert() {
final text = _textEditingController.text;
switch (_typeOptionIndex) {
case 0:
return utf8.decode(base64.decode(text));
case 1:
return base64.encode(utf8.encode(text));
case 2:
return Uri.encodeFull(text);
case 3:
return Uri.decodeFull(text);
default:
return '未知编码';
}
}
Widget _buildInputTop() {
return SizedBox(
height: _media.size.height * 0.33,
child: _buildInput(_textEditingController),
);
}
Widget _buildTypeOption() {
return Card(
child: ExpansionTile(
title: Text(
_typeOption[_typeOptionIndex],
style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500),
),
children: _typeOption
.map((e) => ListTile(
title: Text(
e,
style: TextStyle(
color:
_theme.textTheme.bodyText2!.color!.withAlpha(177)),
),
trailing: _buildRadio(_typeOption.indexOf(e)),
))
.toList(),
),
);
}
Widget _buildResult() {
return SizedBox(
height: _media.size.height * 0.33,
child: _buildInput(_textEditingControllerResult),
);
}
Widget _buildInput(TextEditingController controller) {
return Card(
child: TextField(
maxLines: 20,
decoration: InputDecoration(
fillColor: Theme.of(context).cardColor,
filled: true,
border: InputBorder.none),
controller: controller,
),
);
}
Radio _buildRadio(int index) {
return Radio<int>(
value: index,
groupValue: _typeOptionIndex,
onChanged: (int? value) {
setState(() {
_typeOptionIndex = value!;
});
},
);
}
}