♻️ Permisson rewrite

This commit is contained in:
niuhuan 2023-04-12 15:53:37 +08:00
parent 2c30885bc3
commit 4a2489dcf4
19 changed files with 393 additions and 74 deletions

View File

@ -89,21 +89,11 @@ VPN->代理->分流, 这三个功能如果同时设置, 您会在您手机的VPN
## 请您遵守使用规则
本文中提到的本软件拓展包括但是不限于以下内容
软件副本分发以及代码使用规则
- 使用本软件进行继续开发形成的软件。
- 引入本软件部分内容为依赖/使用本软件内代码的同时包含本软件内一致内容或功能。
- 直接对本软件进行打包发布
软件副本分发以及代码使用规则规则
- 本软件仅供学习交流使用, 本软件或本软件的拓展, 个人或企业不可用于商业用途, 不可上架任何商店。
- 本软件的拓展在未经允许的情况下可以自用但不允许释放任何releases。
- 本软件的代码在未经允许的情况下可以自用但不允许释放任何releases, 个人或企业不可用于商业用途, 不可上架任何商店。。
- 不要在任何其他 **二次元软件****聊天社区****开发社区** 内, 发布有关本软件的链接或信息, 对于观点不同产生的分歧作者不站队任何立场。
- 不要发送本软件安装包到 **任何社区内** , 不要将APK/IPA/ZIP/DMG发送包括任何聊天软件内的群聊功能。 分享本软件时, 在社区中使用Github中提供的Releases页面的链接, 或使用私聊窗口发送。
源代码使用规则
- 不要发送本软件安装包到 **任何社区内** , 不要将APK/IPA/ZIP/DMG发送包括任何聊天软件内的群聊功能。 请使用Github中提供的Releases页面的链接。
- 对本仓库的fork需要保留本仓库的链接, 以引导用户在主要仓库进行讨论。
责任声明

View File

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<queries>
<intent>

View File

@ -1,3 +1,6 @@
v1.7.2
- [x] 🐛 修复安卓13导入导出的问题
- [x] 🐛 修复测速不好用的问题
- [x] ♻️ 梳理一些权限
- [x] ✨ 增加批量下载功能
- [x] ✨ 增加PAT入会发电

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_styled_toast/flutter_styled_toast.dart';
import 'package:pikapika/screens/AccessKeyReplaceScreen.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uri_to_file/uri_to_file.dart';
@ -300,7 +301,17 @@ StreamSubscription<String?> linkSubscript(BuildContext context) {
return linkStream.listen((uri) async {
if (uri == null) return;
var parsed = Uri.parse(uri);
if (RegExp(r"^pika://comic/([0-9A-z]+)/$").allMatches(uri).isNotEmpty) {
if (RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$").allMatches(uri).isNotEmpty) {
String accessKey = RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$")
.allMatches(uri)
.first
.group(1)!;
Navigator.of(context).push(
mixRoute(
builder: (BuildContext context) => AccessKeyReplaceScreen(accessKey: accessKey),
),
);
} else if (RegExp(r"^pika://comic/([0-9A-z]+)/$").allMatches(uri).isNotEmpty) {
String comicId = RegExp(r"^pika://comic/([0-9A-z]+)/$")
.allMatches(uri)
.first

View File

@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pikapika/basic/Common.dart';
import 'package:pikapika/basic/config/Platform.dart';
import 'package:url_launcher/url_launcher.dart';
import 'Method.dart';
@ -68,8 +69,13 @@ Future<dynamic> saveImageQuiet(String path, BuildContext context) async {
}
Future<dynamic> _saveImageAndroid(String path, BuildContext context) async {
var p = await Permission.storage.request();
if (!p.isGranted) {
late bool g;
if (androidVersion < 30) {
g = await Permission.storage.request().isGranted;
}else{
g = await Permission.manageExternalStorage.request().isGranted;
}
if (!g) {
return;
}
return method.androidSaveFileToImage(path);

View File

@ -1065,6 +1065,7 @@ class ProInfoPat {
required this.reBind,
required this.errorType,
required this.errorMsg,
required this.accessKey,
});
late final bool isPro;
late final String patId;
@ -1073,6 +1074,7 @@ class ProInfoPat {
late final int reBind;
late final int errorType;
late final String errorMsg;
late final String accessKey;
ProInfoPat.fromJson(Map<String, dynamic> json){
isPro = json['is_pro'];
@ -1082,6 +1084,7 @@ class ProInfoPat {
reBind = json['re_bind'];
errorType = json['error_type'];
errorMsg = json['error_msg'];
accessKey = json['access_key'];
}
Map<String, dynamic> toJson() {
@ -1093,6 +1096,7 @@ class ProInfoPat {
_data['re_bind'] = reBind;
_data['error_type'] = errorType;
_data['error_msg'] = errorMsg;
_data['access_key'] = accessKey;
return _data;
}
}

View File

@ -1013,4 +1013,20 @@ class Method {
Future downloadAll(List<String> comicIds) {
return _flatInvoke("downloadAll", comicIds);
}
Future setPatAccessKey(String accessKey) {
return _flatInvoke("setPatAccessKey", accessKey);
}
Future reloadPatAccount() {
return _flatInvoke("reloadPatAccount", "");
}
Future bindThisAccount() {
return _flatInvoke("bindThisAccount", "");
}
Future clearPat() {
return _flatInvoke("clearPat", "");
}
}

View File

@ -7,6 +7,7 @@ import 'package:permission_handler/permission_handler.dart';
import '../Common.dart';
import '../Method.dart';
import 'Platform.dart';
const _propertyName = "chooserRoot";
late String _chooserRoot;
@ -30,7 +31,13 @@ Future<dynamic> initChooserRoot() async {
Future<String> currentChooserRoot() async {
if (Platform.isAndroid) {
if (!(await Permission.storage.request()).isGranted) {
late bool g;
if (androidVersion < 30) {
g = await Permission.storage.request().isGranted;
}else{
g = await Permission.manageExternalStorage.request().isGranted;
}
if (!g) {
throw Exception("申请权限被拒绝");
}
}

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import '../Cross.dart';
import '../Method.dart';
import 'Platform.dart';
const _propertyName = "exportPath";
late String _exportPath;
@ -35,7 +36,13 @@ Future<String> attachExportPath() async {
path = await method.iosGetDocumentDir();
} else {
if (Platform.isAndroid) {
if (!(await Permission.storage.request()).isGranted) {
late bool g;
if (androidVersion < 30) {
g = await Permission.storage.request().isGranted;
}else{
g = await Permission.manageExternalStorage.request().isGranted;
}
if (!g) {
throw Exception("申请权限被拒绝");
}
}

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:pikapika/basic/Method.dart';
import 'package:pikapika/screens/components/ContentLoading.dart';
import '../basic/config/IsPro.dart';
class AccessKeyReplaceScreen extends StatefulWidget {
final String accessKey;
const AccessKeyReplaceScreen({Key? key, required this.accessKey})
: super(key: key);
@override
State<StatefulWidget> createState() => _AccessKeyReplaceScreenState();
}
class _AccessKeyReplaceScreenState extends State<AccessKeyReplaceScreen> {
var _loading = false;
var _message = "";
var _success = false;
_set() async {
setState(() {
_loading = true;
});
try {
await method.setPatAccessKey(widget.accessKey);
await reloadIsPro();
_success = true;
} catch (e) {
_message = "错误 : $e";
} finally {
setState(() {
_loading = false;
});
}
}
Widget _content() {
if (_loading) {
return const ContentLoading(label: "加载中");
}
if (_success) {
return const Text("您的赞助登录成功, 请返回");
}
return Column(
children: [
Expanded(child: Container()),
Text(widget.accessKey),
Text(_message),
Container(
height: 10,
),
MaterialButton(
color: Colors.grey,
onPressed: _set,
child: const Text("确认"),
),
Expanded(child: Container()),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("更换PAT账户"),
),
body: Center(
child: _content(),
),
);
}
}

View File

@ -160,7 +160,11 @@ class _ComicsScreenState extends State<ComicsScreen> {
appBar = AppBar(
title: Text(title),
actions: [
commonPopMenu(context),
commonPopMenu(
context,
setState: setState,
comicListController: _comicListController,
),
addressPopMenu(context),
_chooseCategoryAction(),
],

View File

@ -37,6 +37,7 @@ import 'package:pikapika/basic/config/Version.dart';
import 'package:pikapika/basic/config/VolumeController.dart';
import 'package:pikapika/basic/config/ShadowCategoriesMode.dart';
import 'package:pikapika/basic/config/WillPopNotice.dart';
import 'package:pikapika/screens/AccessKeyReplaceScreen.dart';
import 'package:pikapika/screens/ComicInfoScreen.dart';
import 'package:pikapika/screens/PkzArchiveScreen.dart';
import 'package:uni_links/uni_links.dart';
@ -126,15 +127,37 @@ class _InitScreenState extends State<InitScreen> {
}
if (initUrl != null) {
var parsed = Uri.parse(initUrl!);
if (RegExp(r"^pika://comic/([0-9A-z]+)/$").allMatches(initUrl!).isNotEmpty) {
String comicId = RegExp(r"^pika://comic/([0-9A-z]+)/$").allMatches(initUrl!).first.group(1)!;
if (RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$")
.allMatches(initUrl!)
.isNotEmpty) {
String accessKey = RegExp(r"^pika://access_key/([0-9A-z:\-]+)/$")
.allMatches(initUrl!)
.first
.group(1)!;
Navigator.of(context).pushReplacement(mixRoute(
builder: (BuildContext context) =>
AccessKeyReplaceScreen(accessKey: accessKey),
));
return;
} else if (RegExp(r"^pika://comic/([0-9A-z]+)/$")
.allMatches(initUrl!)
.isNotEmpty) {
String comicId = RegExp(r"^pika://comic/([0-9A-z]+)/$")
.allMatches(initUrl!)
.first
.group(1)!;
Navigator.of(context).pushReplacement(mixRoute(
builder: (BuildContext context) =>
ComicInfoScreen(comicId: comicId, holdPkz: true),
));
return;
} if (RegExp(r"^https?://pika/comic/([0-9A-z]+)/$").allMatches(initUrl!).isNotEmpty) {
String comicId = RegExp(r"^https?://pika/comic/([0-9A-z]+)/$").allMatches(initUrl!).first.group(1)!;
} else if (RegExp(r"^https?://pika/comic/([0-9A-z]+)/$")
.allMatches(initUrl!)
.isNotEmpty) {
String comicId = RegExp(r"^https?://pika/comic/([0-9A-z]+)/$")
.allMatches(initUrl!)
.first
.group(1)!;
Navigator.of(context).pushReplacement(mixRoute(
builder: (BuildContext context) =>
ComicInfoScreen(comicId: comicId, holdPkz: true),
@ -147,7 +170,9 @@ class _InitScreenState extends State<InitScreen> {
PkzArchiveScreen(pkzPath: file.path, holdPkz: true),
));
return;
} else if (RegExp(r"^.*\.((pki)|(zip))$").allMatches(parsed.path).isNotEmpty) {
} else if (RegExp(r"^.*\.((pki)|(zip))$")
.allMatches(parsed.path)
.isNotEmpty) {
File file = await toFile(initUrl!);
Navigator.of(context).pushReplacement(
mixRoute(

View File

@ -14,6 +14,7 @@ import 'package:uri_to_file/uri_to_file.dart';
import '../basic/Common.dart';
import '../basic/Navigator.dart';
import '../basic/config/IconLoading.dart';
import '../basic/config/Platform.dart';
import 'PkzComicInfoScreen.dart';
class PkzArchiveScreen extends StatefulWidget {
@ -75,9 +76,16 @@ class _PkzArchiveScreenState extends State<PkzArchiveScreen> with RouteAware {
Future _load() async {
await method.viewPkz(_fileName, widget.pkzPath);
var p = await Permission.storage.request();
if (!p.isGranted) {
throw 'error permission';
if (Platform.isAndroid) {
late bool g;
if (androidVersion < 30) {
g = await Permission.storage.request().isGranted;
}else{
g = await Permission.manageExternalStorage.request().isGranted;
}
if (!g) {
throw 'error permission';
}
}
_info = await method.pkzInfo(widget.pkzPath);
if (_info.comics.length == 1) {

View File

@ -1,7 +1,12 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:pikapika/basic/Common.dart';
import 'package:pikapika/basic/Method.dart';
import 'package:pikapika/screens/AccessKeyReplaceScreen.dart';
import '../basic/config/IconLoading.dart';
import '../basic/config/IsPro.dart';
class ProScreen extends StatefulWidget {
@ -21,9 +26,20 @@ class _ProScreenState extends State<ProScreen> {
_username = value;
});
});
proEvent.subscribe(_setState);
super.initState();
}
@override
void dispose() {
proEvent.unsubscribe(_setState);
super.dispose();
}
_setState(_) {
setState(() {});
}
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
@ -51,43 +67,25 @@ class _ProScreenState extends State<ProScreen> {
const Padding(
padding: EdgeInsets.all(20),
child: Text(
"点击\"我曾经发过电\"进同步发电状态\n"
"点击\"我刚才发了电\"兑换作者给您的礼物卡\n"
"\"关于\"界面找到维护地址用爱发电",
),
),
const Divider(),
const Padding(
padding: EdgeInsets.all(20),
child: Text(
"发电小功能 \n"
" 多线程下载\n"
" 批量导入导出\n"
" 跳页",
"\"关于\"界面找到维护地址可获得发电指引\n\n"
"1. \"签到/游戏/兑换\" \n"
" (1). \"我曾经发过电\"可同步相应发电状态\n"
" (2). \"我刚才发了电\"兑换作者给您的礼物卡\n"
"\n"
"2. \"PAT入会\"\n"
" 🔗将社区账号链接到软件, 同步成员状态, 订阅式发电"
"",
),
),
const Divider(),
ListTile(
title: const Text("签到或礼物卡"),
title: const Text("签到/游戏/兑换"),
subtitle: Text(
proInfoAf.isPro
? "发电中 (${DateTime.fromMillisecondsSinceEpoch(1000 * proInfoAf.expire).toString()})"
: "未发电",
),
),
...(proInfoPat.patId.isNotEmpty ? [
ListTile(
onTap: () {
managementPat();
},
title: const Text("P站支持"),
subtitle: Text((
proInfoPat.isPro
? "发电中"
: "未发电") + ("(点击管理)"),
),
),
] : []),
const Divider(),
ListTile(
title: const Text("我曾经发过电"),
@ -120,12 +118,156 @@ class _ProScreenState extends State<ProScreen> {
},
),
const Divider(),
...patPro(),
const Divider(),
const Padding(
padding: EdgeInsets.all(20),
child: Text(
"发电小功能 \n"
" 多线程下载\n"
" 批量导入导出\n"
" 跳页",
),
),
const Divider(),
const Divider(),
],
),
);
}
void managementPat() {
// todo
List<Widget> patPro() {
List<Widget> widgets = [];
if (proInfoPat.accessKey.isNotEmpty) {
var text = "密钥 : 已录入";
if (proInfoPat.patId.isNotEmpty) {
text += "\nPAT账号 : ${proInfoPat.patId}";
}
if (proInfoPat.bindUid.isNotEmpty) {
text += "\n绑定PIKA账号 : ${proInfoPat.bindUid}";
}
if (proInfoPat.requestDelete > 0) {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(
proInfoPat.requestDelete * 1000,
isUtc: true,
);
String formattedDate =
DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime.toLocal());
text += "\n绑定账号时间 : $formattedDate";
}
if (proInfoPat.reBind > 0) {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(
proInfoPat.reBind * 1000,
isUtc: true,
);
String formattedDate =
DateFormat('yyyy-MM-dd HH:mm:ss').format(dateTime.toLocal());
text += "\n可以换绑时间 : $formattedDate";
}
List<TextSpan> append = [];
if (proInfoPat.bindUid == "") {
append.add(const TextSpan(
text: "\n(请点击这里绑定到当前账号发电)",
style: TextStyle(color: Colors.blue),
));
} else if (proInfoPat.bindUid != _username) {
append.add(const TextSpan(
text: "\n(请点换绑到当前账号发电)",
style: TextStyle(color: Colors.red),
));
} else if (proInfoPat.isPro == false) {
append.add(const TextSpan(
text: "\n(未检测到入会, 请到下载页入会)",
style: TextStyle(color: Colors.orange),
));
} else {
append.add(const TextSpan(
text: "\n(PAT正常)",
style: TextStyle(color: Colors.green),
));
}
widgets.add(ListTile(
onTap: () async {
print(jsonEncode(proInfoPat));
var choose = await chooseMapDialog<int>(
context,
{
"更新PAT发电状态": 2,
"绑定到此账号": 3,
"更换PAT密钥": 1,
"清除PAT信息": 4,
},
"请选择",
);
switch (choose) {
case 1:
addPatAccount();
break;
case 2:
reloadPatAccount();
break;
case 3:
bindThisAccount();
break;
case 4:
clearPat();
break;
}
},
title: const Text("PAT入会"),
subtitle: Text.rich(TextSpan(children: [
TextSpan(text: text),
...append,
])),
));
} else {
widgets.add(ListTile(
onTap: () {
addPatAccount();
},
title: const Text("PAT入会"),
subtitle: const Text("点击绑定"),
));
}
return widgets;
}
void addPatAccount() async {
print(jsonEncode(proInfoPat));
String? key = await inputString(context, "请输入授权代码");
if (key != null) {
await Navigator.of(context)
.push(mixRoute(builder: (BuildContext context) {
return AccessKeyReplaceScreen(accessKey: key);
}));
}
}
reloadPatAccount() async {
defaultToast(context, "请稍后");
try {
await method.reloadPatAccount();
await reloadIsPro();
defaultToast(context, "SUCCESS");
} catch (e) {
defaultToast(context, "FAIL : $e");
} finally {}
}
bindThisAccount() async {
defaultToast(context, "请稍后");
try {
await method.bindThisAccount();
await reloadIsPro();
defaultToast(context, "SUCCESS");
} catch (e) {
defaultToast(context, "FAIL : $e");
} finally {}
}
clearPat() async {
await method.clearPat();
await reloadIsPro();
defaultToast(context, "Success");
}
}

View File

@ -8,8 +8,10 @@ import 'package:pikapika/screens/components/RightClickPop.dart';
import '../basic/Entities.dart';
import '../basic/config/Address.dart';
import '../basic/config/IconLoading.dart';
import 'components/ComicList.dart';
import 'components/ComicPager.dart';
import 'components/Common.dart';
import 'components/GoDownloadSelect.dart';
//
class SearchScreen extends StatefulWidget {
@ -27,6 +29,7 @@ class SearchScreen extends StatefulWidget {
}
class _SearchScreenState extends State<SearchScreen> {
late final _comicListController = ComicListController();
late final TextEditingController _textEditController =
TextEditingController(text: widget.keyword);
late final SearchBar _searchBar = SearchBar(
@ -51,7 +54,11 @@ class _SearchScreenState extends State<SearchScreen> {
return AppBar(
title: Text("${categoryTitle(widget.category)} ${widget.keyword}"),
actions: [
commonPopMenu(context),
commonPopMenu(
context,
setState: setState,
comicListController: _comicListController,
),
addressPopMenu(context),
_chooseCategoryAction(),
_searchBar.getSearchAction(context),
@ -66,7 +73,7 @@ class _SearchScreenState extends State<SearchScreen> {
categoryTitle(null),
...filteredList(
storedCategories,
(c) => !shadowCategories.contains(c),
(c) => !shadowCategories.contains(c),
),
]);
if (category != null) {
@ -100,7 +107,7 @@ class _SearchScreenState extends State<SearchScreen> {
}
@override
Widget build(BuildContext context){
Widget build(BuildContext context) {
return rightClickPop(
child: buildScreen(context),
context: context,
@ -110,7 +117,9 @@ class _SearchScreenState extends State<SearchScreen> {
Widget buildScreen(BuildContext context) {
return Scaffold(
appBar: _searchBar.build(context),
appBar: _comicListController.selecting
? downAppBar(context, _comicListController, setState)
: _searchBar.build(context),
body: ComicPager(
fetchPage: _fetch,
),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:pikapika/basic/Common.dart';
import 'package:pikapika/screens/components/ComicList.dart';
import '../../basic/config/IsPro.dart';
@ -39,9 +40,12 @@ Widget commonPopMenu(
PopupMenuItem<int>(
value: 3,
child: ListTile(
leading: const Icon(Icons.download),
leading: Icon(
Icons.download,
color: isPro ? null : Colors.grey,
),
title: Text(
"下载" + (isPro ? "" : "Pro"),
"批量下载" + (isPro ? "" : "(发电)"),
style: TextStyle(
color: isPro ? null : Colors.grey,
),
@ -63,6 +67,10 @@ Widget commonPopMenu(
chooseShadowCategories(context);
break;
case 3:
if (!isPro) {
defaultToast(context, "请先发电呀");
return;
}
if (setState != null) {
if (comicListController != null) {
setState(() {

View File

@ -58,13 +58,15 @@ AppBar downAppBar(
MaterialButton(
minWidth: 0,
onPressed: () async {
// todo
final list = _comicListController.selected;
var list = _comicListController.selected;
if (list.isEmpty) {
defaultToast(context, "请选择漫画");
return;
}
_comicListController.selecting = false;
list = list.toList();
setState((){
_comicListController.selecting = false;
});
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) {
return DownloadComicsScreen(list);

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
url: "https://pub.dev"
source: hosted
version: "3.3.6"
version: "3.3.7"
async:
dependency: transitive
description:
@ -273,10 +273,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
sha256: d4cb8ab04f770dab9d04c7959e5f6d22e8c5280343d425f9344f93832cf58445
sha256: a1546ff5861fc15812953d4733b520c3d371cec3d2859a001ff04c46c4d81883
url: "https://pub.dev"
source: hosted
version: "0.8.7+2"
version: "0.8.7+3"
image_picker_platform_interface:
dependency: transitive
description:
@ -286,7 +286,7 @@ packages:
source: hosted
version: "2.6.3"
intl:
dependency: transitive
dependency: "direct main"
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
@ -574,10 +574,10 @@ packages:
dependency: transitive
description:
name: url_launcher_ios
sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92"
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
url: "https://pub.dev"
source: hosted
version: "6.1.3"
version: "6.1.4"
url_launcher_linux:
dependency: transitive
description:
@ -630,10 +630,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.1.4"
xml:
dependency: transitive
description:

View File

@ -51,6 +51,7 @@ dependencies:
uri_to_file: ^0.2.0
uni_links: ^0.5.1
filesystem_picker: ^3.0.0-beta.1
intl: ^0.17.0
dev_dependencies: