Skip to content

Commit 449aa48

Browse files
committed
feat(WebDAV): add webdav upload/download
基本完成了WebDAV的主要功能实现 Signed-off-by: OctagonalStar <[email protected]>
1 parent b54291e commit 449aa48

File tree

8 files changed

+282
-54
lines changed

8 files changed

+282
-54
lines changed

lib/funcs/sync.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'dart:convert';
2+
import 'dart:typed_data' show Uint8List;
3+
4+
import 'package:flutter/material.dart';
5+
import 'package:webdav_client/webdav_client.dart';
6+
7+
import 'package:arabic_learning/package_replacement/storage.dart' show SharedPreferences;
8+
9+
class WebDAV {
10+
String uri;
11+
String user;
12+
String password;
13+
WebDAV({required this.uri, required this.user, required this.password});
14+
15+
bool isReachable = false;
16+
bool isReadable = false;
17+
bool isWriteable = false;
18+
19+
late Client client;
20+
21+
static Future<List<dynamic>> test(String uri, String user, {String password = ''}) async {
22+
Client tempClient = newClient(
23+
uri,
24+
user: user,
25+
password: password
26+
);
27+
try{
28+
tempClient.setHeaders(
29+
{
30+
'accept-charset': 'utf-8',
31+
'Content-Type': 'text/xml',
32+
},
33+
);
34+
tempClient.setConnectTimeout(8000);
35+
tempClient.setSendTimeout(60000);
36+
tempClient.setReceiveTimeout(60000);
37+
} catch (e) {
38+
return [false, false, "base setting error: $e"];
39+
}
40+
try{
41+
await tempClient.ping(); // test for connection
42+
} catch (e) {
43+
return [false, false, "remote server didn't response: $e"];
44+
}
45+
try {
46+
await tempClient.readDir('/'); // test for read
47+
} catch (e) {
48+
return [true, false, 'no read access: $e'];
49+
}
50+
try{
51+
await tempClient.write("TestFile", Uint8List(64)); // test for write
52+
await tempClient.remove("TestFile");
53+
} catch (e) {
54+
return [true, false, 'no write access: $e'];
55+
}
56+
return [true, true, 'ok'];
57+
}
58+
59+
Future<void> connect() async {
60+
try{
61+
client = newClient(
62+
uri,
63+
user: user,
64+
password: password
65+
);
66+
client.setHeaders({'accept-charset': 'utf-8'});
67+
client.setConnectTimeout(8000);
68+
client.setSendTimeout(8000);
69+
client.setReceiveTimeout(8000);
70+
await client.ping(); // test for connection
71+
isReachable = true;
72+
await client.readDir(''); // test for read
73+
isReadable = true;
74+
await client.write("TestFile", Uint8List(64)); // test for write
75+
isWriteable = true;
76+
await client.remove("TestFile");
77+
} catch (e) {
78+
debugPrint(e.toString());
79+
}
80+
}
81+
82+
Future<bool> upload(SharedPreferences pref,{bool force = false}) async {
83+
if(isWriteable || force) {
84+
await client.write("arabic_learning.bak", utf8.encode(jsonEncode(pref.export())));
85+
return true;
86+
}
87+
return false;
88+
}
89+
90+
Future<bool> download(SharedPreferences pref,{bool force = false}) async {
91+
if(isReadable || force) {
92+
Map<String, dynamic> file = jsonDecode(utf8.decode(await client.read("arabic_learning.bak")));
93+
pref.recovery(file);
94+
return true;
95+
}
96+
return false;
97+
}
98+
}

lib/package_replacement/storage.dart

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ class SharedPreferences {
77
late bool type; // true: shpr ; false: indexDB
88
late idb.IdbFactory idbFactory;
99
late idb.Database db;
10-
Map<String, dynamic> dbCache = {}; // 使用缓存避免异步加载
1110
late shpr.SharedPreferences prefs;
11+
static const List<String> usedKeys = ["settingData", "wordData", "fsrsData"]; // ! change this whenever add new setting key !
12+
Map<String, dynamic> dbCache = {}; // 使用缓存避免异步加载
1213
static Future<SharedPreferences> getInstance() async {
1314
SharedPreferences rt = SharedPreferences();
1415
if(kIsWeb) {
@@ -24,9 +25,9 @@ class SharedPreferences {
2425
);
2526
var txn = rt.db.transaction("data", "readonly");
2627
var store = txn.objectStore("data");
27-
rt.dbCache["settingData"] = await store.getObject("settingData");
28-
rt.dbCache["wordData"] = await store.getObject("wordData");
29-
rt.dbCache["fsrsData"] = await store.getObject("fsrsData");
28+
for(String keyName in usedKeys) {
29+
rt.dbCache[keyName] = await store.getObject(keyName);
30+
}
3031
rt.type = false;
3132
} catch (e) {
3233
// print("FallBack to shpr $e");
@@ -82,4 +83,23 @@ class SharedPreferences {
8283
return false;
8384
}
8485
}
86+
87+
Map<String, dynamic> export() {
88+
if(type) {
89+
Map<String, dynamic> overall = {};
90+
for(String keyName in usedKeys){
91+
overall[keyName] = prefs.getString(keyName);
92+
}
93+
return overall;
94+
} else {
95+
return dbCache;
96+
}
97+
}
98+
99+
void recovery(Map<String, dynamic> backup) {
100+
if(!type) dbCache = {}; // create a new instance
101+
for(String keyName in usedKeys) {
102+
setString(keyName, backup[keyName]);
103+
}
104+
}
85105
}

lib/sub_pages_builder/setting_pages/data_download_page.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class DownloadPage extends StatelessWidget {
2828
children: [
2929
SettingItem(
3030
title: "来自 Github @${StaticsVar.onlineDictOwner} 学长的词库 (在此表示感谢)",
31+
padding: EdgeInsets.all(8.0),
3132
children: snapshot.data!,
3233
)
3334
],

lib/sub_pages_builder/setting_pages/item_widget.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class SettingItem extends StatelessWidget {
55
final String title;
66
final EdgeInsetsGeometry? padding;
77
final List<Widget> children;
8-
const SettingItem({super.key, required this.children, required this.title, this.padding});
8+
const SettingItem({super.key, required this.title, required this.children, this.padding});
99

1010
@override
1111
Widget build(BuildContext context) {

lib/sub_pages_builder/setting_pages/sync_page.dart

Lines changed: 148 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:arabic_learning/funcs/ui.dart';
22
import 'package:arabic_learning/sub_pages_builder/setting_pages/item_widget.dart';
3+
import 'package:arabic_learning/funcs/sync.dart';
34
import 'package:arabic_learning/vars/global.dart';
45
import 'package:arabic_learning/vars/statics_var.dart';
56
import 'package:flutter/material.dart';
@@ -14,10 +15,17 @@ class DataSyncPage extends StatefulWidget {
1415

1516
class _DataSyncPage extends State<DataSyncPage> {
1617
bool? enabled;
18+
bool isUploading = false;
19+
bool isDownloading = false;
1720

1821
@override
1922
Widget build(BuildContext context) {
2023
enabled ??= context.read<Global>().settingData["sync"]["enabled"];
24+
final WebDAV webdav = WebDAV(
25+
uri: context.read<Global>().settingData["sync"]["account"]["uri"],
26+
user: context.read<Global>().settingData["sync"]["account"]["userName"],
27+
password: context.read<Global>().settingData["sync"]["account"]["passWord"]
28+
);
2129

2230
return Scaffold(
2331
appBar: AppBar(
@@ -26,64 +34,156 @@ class _DataSyncPage extends State<DataSyncPage> {
2634
body: ListView(
2735
children: [
2836
TextContainer(text: "该功能还处在预览阶段", style: TextStyle(color: Colors.redAccent)),
29-
SyncRemoteSettingWidget(setPageState: setState)
30-
],
31-
),
32-
);
33-
}
34-
}
35-
36-
class SyncRemoteSettingWidget extends StatelessWidget {
37-
final Function setPageState;
38-
const SyncRemoteSettingWidget({super.key, required this.setPageState});
39-
40-
@override
41-
Widget build(BuildContext context) {
42-
return Column(
43-
crossAxisAlignment: CrossAxisAlignment.start,
44-
children: [
45-
SettingItem(
46-
title: "远程",
47-
padding: EdgeInsets.all(8.0),
48-
children: [
49-
Row(
50-
children: [
51-
Icon(Icons.account_box, size: 36),
52-
Expanded(
53-
child: Column(
54-
crossAxisAlignment: CrossAxisAlignment.start,
37+
Column(
38+
crossAxisAlignment: CrossAxisAlignment.start,
39+
children: [
40+
SettingItem(
41+
title: "远程",
42+
padding: EdgeInsets.all(8.0),
43+
children: [
44+
Row(
45+
children: [
46+
Icon(Icons.account_box, size: 36),
47+
Expanded(
48+
child: Text("WebDAV账户"),
49+
),
50+
ElevatedButton(
51+
style: ElevatedButton.styleFrom(
52+
shape: RoundedRectangleBorder(borderRadius: StaticsVar.br)
53+
),
54+
onPressed: () async {
55+
await popAccountSetting(context);
56+
setState(() {});
57+
},
58+
child: Text("绑定")
59+
),
60+
],
61+
),
62+
Row(
5563
children: [
56-
Text("WebDAV账户"),
64+
Text("联通性检查: "),
65+
if((context.read<Global>().settingData["sync"]["account"]["uri"] as String).isEmpty) Text("未绑定", style: Theme.of(context).textTheme.labelSmall),
66+
FutureBuilder(
67+
future: WebDAV.test(
68+
context.read<Global>().settingData["sync"]["account"]["uri"],
69+
context.read<Global>().settingData["sync"]["account"]["userName"],
70+
password: context.read<Global>().settingData["sync"]["account"]["passWord"]
71+
),
72+
builder: (context, snapshot) {
73+
if(snapshot.hasError) {
74+
return Row(
75+
children: [
76+
Icon(Icons.circle, color: Colors.redAccent, size: 18),
77+
Text("在测试中遇到了未知的异常", style: TextStyle(fontSize: 8))
78+
],
79+
);
80+
}
81+
if(snapshot.connectionState == ConnectionState.waiting) {
82+
return CircularProgressIndicator();
83+
}
84+
if(snapshot.hasData) {
85+
return Row(
86+
children: [
87+
Icon(Icons.circle, color: snapshot.data![1] ? Colors.greenAccent : snapshot.data![0] ? Colors.amber : Colors.redAccent, size: 18)
88+
],
89+
);
90+
}
91+
return CircularProgressIndicator();
92+
},
93+
)
94+
],
95+
)
96+
],
97+
),
98+
StatefulBuilder(
99+
builder: (context, setLocalState) {
100+
return SettingItem(
101+
title: "同步",
102+
padding: EdgeInsets.all(8.0),
103+
children: [
104+
Row(
105+
children: [
106+
Expanded(
107+
child: Column(
108+
crossAxisAlignment: CrossAxisAlignment.start,
109+
children: [
110+
Text("上传数据"),
111+
Text("将本地配置上传到WebDAV服务器", style: TextStyle(color: Colors.grey, fontSize: 8.0))
112+
],
113+
)
114+
),
115+
isUploading
116+
? CircularProgressIndicator()
117+
:ElevatedButton(
118+
onPressed: () async {
119+
setLocalState(() {
120+
isUploading = true;
121+
});
122+
try{
123+
if(!webdav.isReachable) await webdav.connect();
124+
if(context.mounted) await webdav.upload(context.read<Global>().prefs);
125+
if(!context.mounted) return;
126+
} catch (e) {
127+
alart(context, e.toString());
128+
return;
129+
}
130+
setLocalState(() {
131+
isUploading = false;
132+
});
133+
alart(context, "已上传");
134+
},
135+
child: Text("上传")
136+
)
137+
],
138+
),
57139
Row(
58140
children: [
59-
Text("联通性检查: "),
60-
if((context.read<Global>().settingData["sync"]["account"]["uri"] as String).isEmpty) Text("未绑定", style: Theme.of(context).textTheme.labelSmall),
61-
Icon(Icons.circle, color: Colors.greenAccent, size: 12)
141+
Expanded(
142+
child: Column(
143+
crossAxisAlignment: CrossAxisAlignment.start,
144+
children: [
145+
Text("恢复数据"),
146+
Text("从WebDAV服务器恢复配置", style: TextStyle(color: Colors.grey, fontSize: 8.0))
147+
],
148+
)
149+
),
150+
isDownloading
151+
? CircularProgressIndicator()
152+
: ElevatedButton(
153+
onPressed: () async {
154+
setLocalState(() {
155+
isDownloading = true;
156+
});
157+
try{
158+
if(!webdav.isReachable) await webdav.connect();
159+
if(context.mounted) if(await webdav.download(context.read<Global>().prefs) && context.mounted) context.read<Global>().conveySetting();
160+
if(!context.mounted) return;
161+
} catch (e) {
162+
alart(context, e.toString());
163+
return;
164+
}
165+
setLocalState(() {
166+
isDownloading = false;
167+
});
168+
alart(context, "已恢复\n部分设置可能需要软件重启后才能生效");
169+
},
170+
child: Text("恢复")
171+
)
62172
],
63173
)
64174
],
65-
),
66-
),
67-
ElevatedButton(
68-
style: ElevatedButton.styleFrom(
69-
shape: RoundedRectangleBorder(borderRadius: StaticsVar.br)
70-
),
71-
onPressed: () async {
72-
await popAccountSetting(context);
73-
setPageState(() {});
74-
},
75-
child: Text("绑定")
76-
),
77-
],
78-
),
79-
],
80-
)
81-
82-
],
175+
);
176+
}
177+
),
178+
],
179+
),
180+
],
181+
),
83182
);
84183
}
85184
}
86185

186+
87187
Future<void> popAccountSetting(BuildContext context) async {
88188
TextEditingController uriController = TextEditingController();
89189
TextEditingController accountController = TextEditingController();

0 commit comments

Comments
 (0)