50 Commits

Author SHA1 Message Date
Mr.Yang
807e94159c Merge pull request #7 from PicGo/dev
Merge Dev
2020-07-20 15:34:13 +08:00
hackycy
9fbdb0cf01 build:release v1.5.2+12 2020-07-20 15:33:08 +08:00
hackycy
4b76d588b4 docs:update version.json 2020-07-20 15:32:32 +08:00
hackycy
6b9d8d8c3e fix:fix release env ios tcyun appear delete error 2020-07-20 15:32:22 +08:00
Mr.Yang
93d3c3ab15 Merge pull request #6 from PicGo/dev
merge dev
2020-07-20 14:53:06 +08:00
hackycy
d4d8b36f8e build:release v1.5.1+11 2020-07-20 14:51:57 +08:00
hackycy
81481a99f9 docs:update version.json 2020-07-20 14:51:45 +08:00
hackycy
8c0b2f772c styles:remove visible all count 2020-07-20 14:50:36 +08:00
hackycy
434152c063 feature:complete paging Load 2020-07-20 14:47:46 +08:00
hackycy
b91692e237 chore:add pull_to_refresh lib 2020-07-20 14:46:28 +08:00
hackycy
b0cbcbb1d8 feature:add in app updates 2020-07-20 11:55:43 +08:00
hackycy
02c65a6875 docs:update readme 2020-07-20 11:12:53 +08:00
hackycy
c85ad49515 docs:update version.json 2020-07-20 11:10:01 +08:00
hackycy
3b016f170a docs:update version.json 2020-07-20 10:46:21 +08:00
hackycy
a7723d8ea7 docs:update readme 2020-07-18 21:29:47 +08:00
hackycy
87eba041d8 docs:update readme 2020-07-18 19:00:14 +08:00
hackycy
1080ae4534 docs:update readme 2020-07-18 18:56:39 +08:00
hackycy
1ef205ca71 docs:update readme 2020-07-17 11:53:20 +08:00
hackycy
d951e1a83b build:release v1.5.0+10 2020-07-17 11:49:57 +08:00
hackycy
3647607e7c fix:remove pbsetting visible item 2020-07-17 11:49:12 +08:00
hackycy
82ab0a5a14 styles:update notification upload tip 2020-07-16 23:27:58 +08:00
hackycy
bfe58ef8a1 feature:complete tcyun image upload 2020-07-16 23:25:25 +08:00
hackycy
a639cb8d09 styles:remove debounce 2020-07-16 21:31:59 +08:00
hackycy
3837f184c5 fix:tcyun unuse v4 api, remove appid 2020-07-16 21:17:34 +08:00
hackycy
79557cd14a test:test post object 2020-07-16 20:59:59 +08:00
hackycy
eda3dea7a5 feature:add tcyun postObject func 2020-07-16 00:14:48 +08:00
hackycy
5e7907a125 test:unit test tcyun 2020-07-15 23:20:46 +08:00
hackycy
bc18d869c3 feature:complete tcyun sign interceptor 2020-07-15 23:18:28 +08:00
hackycy
8348d84c44 test:test tcyun api 2020-07-15 18:15:22 +08:00
hackycy
1bef17638c docs:add version.json 2020-07-15 12:17:45 +08:00
hackycy
ee2d3b7e3e styles:remove unuse import 2020-07-15 11:44:57 +08:00
hackycy
32f121d93b fix:fix debugPrint cause error 2020-07-15 11:44:33 +08:00
hackycy
46f1470aec styles:Judge empty clipUrl 2020-07-15 11:28:09 +08:00
hackycy
44aeddbb7b feature:complete do not copy automatically when uploading 2020-07-15 11:25:43 +08:00
hackycy
b38e94c53c fix:upload tip text fix 2020-07-15 11:23:16 +08:00
hackycy
8bdd43b757 fix:fix upload fail tip 2020-07-15 11:22:06 +08:00
hackycy
67dbaa1cc0 fix:fix the configuration list disappears during QR code conversion 2020-07-15 10:48:37 +08:00
hackycy
72e84620e2 docs:update readme 2020-07-14 23:48:00 +08:00
hackycy
caba6995c3 chore:update issue template 2020-07-14 23:31:58 +08:00
hackycy
0c314b918c docs:update readme 2020-07-14 23:25:19 +08:00
hackycy
3f96199d44 feature:complete tcyun setting page 2020-07-14 22:49:44 +08:00
hackycy
1a7869251c refactor:loadConfig in BasePBSettingPageState 2020-07-14 21:50:48 +08:00
hackycy
a2ee90dd70 feature:add tcyun page router 2020-07-14 17:15:50 +08:00
hackycy
a21ab4174e feature:create tcyun upload strategy 2020-07-14 16:40:39 +08:00
hackycy
f91c5c80ac fix:fix qrcode alert tip 2020-07-14 12:21:09 +08:00
hackycy
780cd7c3c2 feature:upgrade sqlite version 2020-07-14 12:14:19 +08:00
hackycy
78c8df5e77 docs:update design doc 2020-07-14 12:13:53 +08:00
hackycy
b45b0e452d feature:add tcyun type 2020-07-14 12:13:12 +08:00
hackycy
afdb4313f9 styles:UploadStrategyFactory add cache 2020-07-14 12:07:35 +08:00
hackycy
61c07dda5f styles:print to debugPrint 2020-07-10 12:02:32 +08:00
31 changed files with 761 additions and 161 deletions

View File

@@ -7,7 +7,7 @@ about: Something isn't working as expected
**仅限中文与英文**, 其他语言的提交将直接被关闭
### 您当前的flutter doctor信息
### 您当前的flutter doctor信息或设备信息
### 预期的表现

View File

@@ -13,15 +13,16 @@
Flutter-PicGo: 一个用于快速上传图片并获取图片URL链接的**手机版**工具
Flutter-PicGo 本体支持如下图床:
**Flutter-PicGo 本体支持如下图床:**
- GitHub [v1.0+]
- SM.MS [v1.1+]
- Gitee [v1.2+]
- 七牛云 [v1.3+]
- 阿里云OSS [v1.4+]
- 腾讯云COS [v1.5+]
> 开发进度可以查看 Projects,会同步更新开发进度
> 开发进度可以查看 [Projects](https://github.com/PicGo/flutter-picgo/projects),会同步更新开发进度
# 特色功能
@@ -30,13 +31,16 @@ Flutter-PicGo 本体支持如下图床:
- 适配深色模式,可跟随系统或手动设置
- 支持将flutter-picgo的配置导出至剪切板
> 基本使用配置与PicGo无异,可参考[配置文档](https://picgo.github.io/PicGo-Doc/zh/guide/config.html#%E5%9B%BE%E5%BA%8A%E5%8C%BA)。
> 腾讯云COS仅支持v5版配置
# 应用截图
![](https://github.static.si-yee.com/image_picker_82452E23-BE11-4712-BFBA-8E93038DB410-3851-00000340B21CCF62.png)
# 下载安装
点击此处下载[应用](https://github.com/hackycy/flutter-picgo/releases)。
点击此处下载应用[releases](https://github.com/hackycy/flutter-picgo/releases)或[蒲公英](https://www.pgyer.com/flutter-picgo)。
Android版直接下载apk安装包或扫码

10
docs/version.json Normal file
View File

@@ -0,0 +1,10 @@
{
"iOS": {
"versionName": "1.5.2",
"versionCode": "12"
},
"Android": {
"versionName": "1.5.1",
"versionCode": "12"
}
}

View File

@@ -29,4 +29,5 @@
| 3 | 新增Gitee图床记录 |
| 4 | 新增Qiniu图床记录 |
| 5 | 新增阿里云OSS图床记录 |
| 6 | 新增腾讯云COS图床记录 |

11
lib/api/picgo_api.dart Normal file
View File

@@ -0,0 +1,11 @@
import 'package:dio/dio.dart';
import 'package:flutter_picgo/utils/net.dart';
class PicgoApi {
/// 获取App最新版本
static Future getLatestVersion() async {
Response res = await NetUtils.getInstance().get(
'https://cdn.jsdelivr.net/gh/PicGo/flutter-picgo@dev/docs/version.json');
return res;
}
}

173
lib/api/tcyun_api.dart Normal file
View File

@@ -0,0 +1,173 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:flutter_picgo/utils/net.dart';
import 'package:flutter_picgo/utils/strings.dart';
class TcyunApi {
static const String BASE_URL = 'myqcloud.com';
static const String secretKey = 'secretKey';
static const String secretId = 'secretId';
static Future postObject(String secretId, String secretKey, String bucket, String area, String ext, FormData formData) async {
Response res = await NetUtils.getInstance().post(
'https://$bucket.cos.$area.$BASE_URL/',
data: formData,
options: Options(
extra: {TcyunApi.secretId: secretId, TcyunApi.secretKey: secretKey},
contentType: 'image/$ext'
)
);
return res.headers;
}
static Future deleteobject(String secretId, String secretKey, String bucket,
String area, String key) async {
Response res = await NetUtils.getInstance().delete(
'https://$bucket.cos.$area.$BASE_URL/$key',
options: Options(
extra: {TcyunApi.secretId: secretId, TcyunApi.secretKey: secretKey},
));
return res.headers;
}
/// 构造“策略”(Policy)
/// 经过 Base64 编码的“策略”(Policy)内容
static String buildPolicy(
String bucket, String key, String secretId, String keyTime) {
Map<String, dynamic> map = {
"expiration": "2030-01-01T00:00:00.000Z",
"conditions": [
{"bucket": bucket},
{"key": key},
{"q-sign-algorithm": "sha1"},
{"q-ak": secretId},
{"q-sign-time": keyTime}
]
};
return json.encode(map);
}
/// post Signature
static String buildSignature(
String secretKey, String keyTime, String policy) {
/// signkey
var hmacsha1Signkey = Hmac(sha1, utf8.encode(secretKey));
var signKey = hmacsha1Signkey.convert(utf8.encode(keyTime));
// string to sign
var stringToSign = sha1.convert(utf8.encode(policy));
// signature
var hmacsha1Signature = Hmac(sha1, utf8.encode('$signKey'));
var signature = hmacsha1Signature.convert(utf8.encode('$stringToSign'));
return '$signature';
}
/// 生成 KeyTime
/// 获取当前时间对应的 Unix 时间戳 StartTimestamp,Unix 时间戳是从 UTC(协调世界时,
/// 或 GMT 格林威治时间)1970年1月1日0时0分0秒(北京时间 1970年1月1日8时0分0秒)起至现在的总秒数。
/// 根据上述时间戳和期望的签名有效时长算出签名过期时间对应的 Unix 时间戳 EndTimestamp。
/// 拼接签名有效时间,格式为StartTimestamp;EndTimestamp,即为 KeyTime。
/// 示例:1557902800;1557910000
static String buildKeyTime() {
int startTime = (new DateTime.now().millisecondsSinceEpoch) ~/ 1000;
int endTime = startTime + 99999;
return '$startTime;$endTime';
}
/// 生成 SignKey
/// 使用 HMAC-SHA1 以 SecretKey 为密钥,以 KeyTime 为消息,计算消息摘要(哈希值),即为 SignKey。
static String buildSignKey(String secretKey, String keyTime) {
// 使用SecertKey对上一步生成的原始字符串计算HMAC-SHA1签名:
var hmacsha1 = Hmac(sha1, utf8.encode(secretKey));
var sign = hmacsha1.convert(utf8.encode(keyTime));
return '$sign';
}
}
/// TcYun验签拦截器
class TcyunInterceptor extends InterceptorsWrapper {
@override
Future onRequest(RequestOptions options) async {
if (options.path.contains(TcyunApi.BASE_URL)) {
/// 生成 KeyTime
var keytime = TcyunApi.buildKeyTime();
/// 生成 SignKey
var signKey = TcyunApi.buildSignKey(
'${options.extra[TcyunApi.secretKey]}', keytime);
/// 生成 UrlParamList 和 HttpParameters
String urlParamList = '';
String httpParameters = '';
if (options.queryParameters != null) {
options.queryParameters.forEach((key, value) {
urlParamList += '${key.toLowerCase()};';
httpParameters +=
'${key.toLowerCase()}=${isBlank(value) ? '' : value}&';
});
}
if (urlParamList.length > 0) {
urlParamList = urlParamList.substring(0, urlParamList.length - 1);
}
if (httpParameters.length > 0) {
httpParameters = httpParameters.substring(0, httpParameters.length - 1);
}
/// 生成 HeaderList 和 HttpHeaders
String headerList = '';
String httpHeaders = '';
Map<String, dynamic> headers = {
// 'Date': HttpDate.format(new DateTime.now()),
'Host': options.uri.host
};
headers.addAll(options.headers);
if (options.method.toUpperCase() == 'GET' ||
options.method.toUpperCase() == 'HEAD' ||
options.method.toUpperCase() == 'DELETE' ||
options.method.toUpperCase() == 'OPTIONS') {
headers.remove('content-type');
}
if (headers != null) {
headers.forEach((key, value) {
print('key = $key, value = $value');
headerList += '${Uri.encodeComponent(key).toLowerCase()};';
httpHeaders +=
'${Uri.encodeComponent(key).toLowerCase()}=${Uri.encodeComponent(value)}&';
});
}
if (headerList.length > 0) {
headerList = headerList.substring(0, headerList.length - 1);
}
if (httpHeaders.length > 0) {
httpHeaders = httpHeaders.substring(0, httpHeaders.length - 1);
}
/// 生成 HttpString
String method = options.method.toLowerCase();
String httpString =
'$method\n${options.uri.path}\n$httpParameters\n$httpHeaders\n';
/// 生成 StringToSign
String stringtoSign =
'sha1\n$keytime\n${sha1.convert(utf8.encode(httpString))}\n';
/// 生成 Signature
var hmacsha1 = Hmac(sha1, utf8.encode(signKey));
var sign = hmacsha1.convert(utf8.encode(stringtoSign));
/// 生成签名
var realSign = 'q-sign-algorithm=sha1' +
'&q-ak=${options.extra[TcyunApi.secretId]}' +
'&q-sign-time=$keytime' +
'&q-key-time=$keytime' +
'&q-header-list=$headerList' +
'&q-url-param-list=$urlParamList' +
'&q-signature=$sign';
headers.addAll({'Authorization': realSign});
options.headers = headers;
}
return options;
}
}

View File

@@ -0,0 +1,36 @@
class TcyunConfig {
String area;
String bucket;
String customUrl;
String path;
String secretId;
String secretKey;
TcyunConfig({
this.area,
this.bucket,
this.customUrl,
this.path,
this.secretId,
this.secretKey});
TcyunConfig.fromJson(Map<String, dynamic> json) {
area = json['area'];
bucket = json['bucket'];
customUrl = json['customUrl'];
path = json['path'];
secretId = json['secretId'];
secretKey = json['secretKey'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['area'] = this.area;
data['bucket'] = this.bucket;
data['customUrl'] = this.customUrl;
data['path'] = this.path;
data['secretId'] = this.secretId;
data['secretKey'] = this.secretKey;
return data;
}
}

View File

@@ -8,4 +8,6 @@ class PBTypeKeys {
static const qiniu = 'qiniu';
static const aliyun = 'aliyun';
static const tcyun = 'tcyun';
}

View File

@@ -10,6 +10,7 @@ import 'package:flutter_picgo/views/pb_setting_page/github_page/github_repo_page
import 'package:flutter_picgo/views/pb_setting_page/pb_setting_page.dart';
import 'package:flutter_picgo/views/pb_setting_page/qiniu_page/qiniu_page.dart';
import 'package:flutter_picgo/views/pb_setting_page/smms_page/smms_page.dart';
import 'package:flutter_picgo/views/pb_setting_page/tcyun_page/tcyun_page.dart';
import 'package:flutter_picgo/views/picgo_setting_page/theme_setting_page.dart';
import 'package:flutter_picgo/views/upload_page/upload_page.dart';
import 'package:flutter_picgo/views/pb_setting_page/github_page/github_page.dart';
@@ -114,6 +115,11 @@ var pbsettingAliyunHandler = new Handler(
handlerFunc: (context, parameters) => AliyunPage(),
);
// 腾讯云COS图床设置页面
var pbsettingTcyunHandler = new Handler(
handlerFunc: (context, parameters) => TcyunPage(),
);
// picgo设置页面
var picgosettingHandler = new Handler(
handlerFunc: (BuildContext context, Map<String, List<String>> params) =>

View File

@@ -25,9 +25,12 @@ class Routes {
// --------- qiniu -------------------
static const String settingPbQiniu = '/setting/pb/qiniu';
// -----------------------------------
// --------- qiniu -------------------
// --------- aliyun -------------------
static const String settingPbAliyun = '/setting/pb/aliyun';
// -----------------------------------
// --------- aliyun -------------------
static const String settingPbTcyun = '/setting/pb/tcyun';
// -----------------------------------
static void configureRoutes(Router router) {
router.notFoundHandler = notfoundHandler;
@@ -46,5 +49,6 @@ class Routes {
router.define(settingPicgoTheme, handler: picggsettingThemeHandler);
router.define(settingPbQiniu, handler: pbsettingQiniuHandler);
router.define(settingPbAliyun, handler: pbsettingAliyunHandler);
router.define(settingPbTcyun, handler: pbsettingTcyunHandler);
}
}

View File

@@ -34,7 +34,7 @@ class DbProvider {
try {
db = await openDatabase(
path,
version: 5,
version: 6,
onCreate: (db, version) async {
// 创建pb_setting表
_initPb(db);
@@ -102,6 +102,9 @@ class DbProvider {
// 阿里云OSS
await db.rawInsert(
'INSERT INTO $TABLE_NAME_PBSETTING(type, path, name, config, visible) VALUES("${PBTypeKeys.aliyun}", "/setting/pb/aliyun", "阿里云OSS图床", NULL, 1)');
// 腾讯云COS
await db.rawInsert(
'INSERT INTO $TABLE_NAME_PBSETTING(type, path, name, config, visible) VALUES("${PBTypeKeys.tcyun}", "/setting/pb/tcyun", "腾讯云COS图床", NULL, 1)');
// copy data
// update authors set dynasty_index=(select id from dynasties where dynasties .name=authors.dynasty) where dynasty in (select name from dynasties )
if (isExists) {

View File

@@ -1,18 +0,0 @@
import 'package:flutter/foundation.dart';
import 'dart:async';
class Debounce {
final int milliseconds;
VoidCallback action;
Timer _timer;
Debounce({this.milliseconds});
run(VoidCallback action) {
if (_timer != null) {
_timer.cancel();
}
_timer = Timer(Duration(milliseconds: milliseconds), action);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter_picgo/api/tcyun_api.dart';
const bool inProduction = const bool.fromEnvironment("dart.vm.product");
@@ -10,9 +11,13 @@ class NetUtils {
_dio = new Dio(BaseOptions(
connectTimeout: 30000, receiveTimeout: 30000, sendTimeout: 30000));
if (!inProduction) {
/// Log
dio.interceptors
.add(LogInterceptor(requestBody: true, responseBody: true));
}
/// Tcyun Interceptor
dio.interceptors.add(TcyunInterceptor());
}
Dio get dio => _dio;

View File

@@ -11,8 +11,14 @@ class Sql extends BaseModel {
return await this.query(tableName);
}
Future<List> getBySql(String where, List<dynamic> whereArgs) async {
return await this.query(tableName, where: where, whereArgs: whereArgs);
Future<List> getBySql(String where, List<dynamic> whereArgs,
{String orderBy, int limit, int offset}) async {
return await this.query(tableName,
where: where,
whereArgs: whereArgs,
orderBy: orderBy,
limit: limit,
offset: offset);
}
Future<int> rawUpdate(String sql, [List<dynamic> arguments]) async {

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_picgo/api/aliyun_api.dart';
import 'package:flutter_picgo/model/aliyun_config.dart';
import 'package:flutter_picgo/model/uploaded.dart';
@@ -29,7 +30,7 @@ class AliyunImageUpload implements ImageUploadStrategy {
@override
Future<Uploaded> upload(File file, String renameImage) async {
String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.aliyun);
print(configStr);
debugPrint(configStr);
if (isBlank(configStr)) {
throw AliyunError(error: '读取配置文件错误!请重试');
}

View File

@@ -1,6 +1,5 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_picgo/api/qiniu_api.dart';
import 'package:flutter_picgo/model/qiniu_config.dart';
@@ -37,12 +36,11 @@ class QiniuImageUpload extends ImageUploadStrategy {
if (isBlank(result.toString())) {
return uploaded;
} else {
throw new QiniuError(error: result['error'].toString());
throw new QiniuError(error: '${result['error']}');
}
}
return uploaded;
} on DioError catch (e) {
print(e.response.data);
if (e.type == DioErrorType.RESPONSE &&
e.error.toString().indexOf('400') > 0) {
throw QiniuError(error: '400 请求报文格式错误');
@@ -100,7 +98,6 @@ class QiniuImageUpload extends ImageUploadStrategy {
await ImageUploadUtils.saveUploadedItem(uploadedItem);
return uploadedItem;
} on DioError catch (e) {
debugPrint(e.response.data);
if (e.type == DioErrorType.RESPONSE &&
e.error.toString().indexOf('400') > 0) {
throw QiniuError(error: '400 请求报文格式错误');

View File

@@ -0,0 +1,126 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_picgo/api/tcyun_api.dart';
import 'package:flutter_picgo/model/tcyun_config.dart';
import 'package:flutter_picgo/model/uploaded.dart';
import 'package:flutter_picgo/resources/pb_type_keys.dart';
import 'package:flutter_picgo/utils/image_upload.dart';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart';
import 'package:flutter_picgo/utils/strings.dart';
class TcyunImageUpload implements ImageUploadStrategy {
@override
Future<Uploaded> delete(Uploaded uploaded) async {
TcyunUploaddedInfo info;
try {
info = TcyunUploaddedInfo.fromJson(json.decode(uploaded.info));
} catch (e) {}
if (info != null) {
await TcyunApi.deleteobject(
info.secretId, info.secretKey, info.bucket, info.area, info.key);
}
return uploaded;
}
@override
Future<Uploaded> upload(File file, String renameImage) async {
String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.tcyun);
if (isBlank(configStr)) {
throw TcyunError(error: '读取配置文件错误!请重试');
}
TcyunConfig config = TcyunConfig.fromJson(json.decode(configStr));
String objectPath = isBlank(config.path)
? renameImage
: path.joinAll([config.path, renameImage]);
/// post
String keyTime = TcyunApi.buildKeyTime();
String policy = TcyunApi.buildPolicy(
config.bucket, objectPath, config.secretId, keyTime);
FormData formData = FormData.fromMap({
"key": objectPath,
"file": await MultipartFile.fromFile(file.path, filename: renameImage),
"policy": base64.encode(utf8.encode(policy)),
"q-sign-algorithm": "sha1",
"q-ak": config.secretId,
"q-key-time": keyTime,
"q-sign-time": keyTime,
"q-signature": TcyunApi.buildSignature(config.secretKey, keyTime, policy)
});
print(TcyunApi.buildSignature(config.secretKey, keyTime, policy));
await TcyunApi.postObject(
config.secretId,
config.secretKey,
config.bucket,
config.area,
path.extension(renameImage).replaceFirst('.', ''),
formData);
String imgPath = path.joinAll([
isBlank(config.customUrl)
? 'https://${config.bucket}.cos.${config.area}.myqcloud.com'
: config.customUrl,
objectPath
]);
var uploadedItem = Uploaded(-1, '$imgPath', PBTypeKeys.tcyun,
info: json.encode(TcyunUploaddedInfo(
secretId: config.secretId,
secretKey: config.secretKey,
area: config.area,
key: objectPath,
bucket: config.bucket)));
await ImageUploadUtils.saveUploadedItem(uploadedItem);
return uploadedItem;
}
}
class TcyunError implements Exception {
TcyunError({
this.error,
});
dynamic error;
String get message => (error?.toString() ?? '');
@override
String toString() {
var msg = 'TcyunError $message';
if (error is Error) {
msg += '\n${error.stackTrace}';
}
return msg;
}
}
class TcyunUploaddedInfo {
String area;
String bucket;
String secretId;
String secretKey;
String key;
TcyunUploaddedInfo(
{this.area, this.bucket, this.secretId, this.secretKey, this.key});
TcyunUploaddedInfo.fromJson(Map<String, dynamic> json) {
area = json['area'];
bucket = json['bucket'];
secretId = json['secretId'];
secretKey = json['secretKey'];
key = json['key'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['area'] = this.area;
data['bucket'] = this.bucket;
data['secretId'] = this.secretId;
data['secretKey'] = this.secretKey;
data['key'] = this.key;
return data;
}
}

View File

@@ -5,25 +5,38 @@ import 'package:flutter_picgo/utils/strategy/impl/github_image_upload.dart';
import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart';
import 'package:flutter_picgo/utils/strategy/impl/qiniu_image_upload.dart';
import 'package:flutter_picgo/utils/strategy/impl/smms_image_upload.dart';
import 'package:flutter_picgo/utils/strategy/impl/tcyun_image_upload.dart';
import 'package:flutter_picgo/utils/strings.dart';
class UploadStrategyFactory {
static Map<String, ImageUploadStrategy> cache = {};
/// UploadStrategy工厂类,负责创建UploadStrategy
static ImageUploadStrategy getUploadStrategy(String type) {
if (isBlank(type)) {
throw new NullThrownError();
}
if (type == PBTypeKeys.github) {
return new GithubImageUpload();
} else if (type == PBTypeKeys.smms) {
return new SMMSImageUpload();
} else if (type == PBTypeKeys.gitee) {
return new GiteeImageUpload();
} else if (type == PBTypeKeys.qiniu) {
return new QiniuImageUpload();
} else if (type == PBTypeKeys.aliyun) {
return new AliyunImageUpload();
if (cache[type] == null) {
if (type == PBTypeKeys.github) {
/// Github
cache[type] = new GithubImageUpload();
} else if (type == PBTypeKeys.smms) {
/// SM.MS
cache[type] = new SMMSImageUpload();
} else if (type == PBTypeKeys.gitee) {
/// Gitee
cache[type] = new GiteeImageUpload();
} else if (type == PBTypeKeys.qiniu) {
/// 七牛
cache[type] = new QiniuImageUpload();
} else if (type == PBTypeKeys.aliyun) {
/// 阿里云
cache[type] = new AliyunImageUpload();
} else if (type == PBTypeKeys.tcyun) {
/// 腾讯云
cache[type] = new TcyunImageUpload();
}
}
return null;
return cache[type];
}
}

View File

@@ -10,6 +10,7 @@ import 'package:flutter_picgo/views/album_page/album_page_presenter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:toast/toast.dart';
import 'package:flutter/services.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
class AlbumPage extends StatefulWidget {
@override
@@ -19,6 +20,10 @@ class AlbumPage extends StatefulWidget {
class _AlbumPageState extends State<AlbumPage> implements AlbumPageContract {
AlbumPagePresenter _presenter;
List<Uploaded> _uploadeds = [];
RefreshController _refreshController =
RefreshController(initialRefresh: false);
int _perPageItemSize = 8;
int _currentPage = 0;
_AlbumPageState() {
_presenter = AlbumPagePresenter(this);
@@ -27,7 +32,7 @@ class _AlbumPageState extends State<AlbumPage> implements AlbumPageContract {
@override
void initState() {
super.initState();
_presenter.doLoadUploadedImages();
_onRefresh();
}
@override
@@ -49,8 +54,44 @@ class _AlbumPageState extends State<AlbumPage> implements AlbumPageContract {
transition: TransitionType.cupertino);
},
),
body: RefreshIndicator(
body: SmartRefresher(
enablePullDown: true,
enablePullUp: this._uploadeds.length >= _perPageItemSize,
header: ClassicHeader(
refreshStyle: RefreshStyle.Follow,
idleText: '下拉刷新',
releaseText: '释放刷新',
completeText: '加载完成',
refreshingText: '刷新中',
failedText: '加载失败,请重试',
),
footer: CustomFooter(
builder: (BuildContext context, LoadStatus mode) {
Widget body;
if (mode == LoadStatus.idle) {
body = Text("上拉加载");
} else if (mode == LoadStatus.loading) {
body = SizedBox(
width: 15,
height: 15,
child: CircularProgressIndicator(),
);
} else if (mode == LoadStatus.failed) {
body = Text("加载失败!点击重试!");
} else if (mode == LoadStatus.canLoading) {
body = Text("松手,加载更多!");
} else {
body = Text("没有更多数据了!");
}
return Container(
height: 55.0,
child: Center(child: body),
);
},
),
onRefresh: _onRefresh,
onLoading: _onLoading,
controller: _refreshController,
child: _uploadeds.length > 0 ? albumView() : emptyView()),
);
}
@@ -142,35 +183,44 @@ class _AlbumPageState extends State<AlbumPage> implements AlbumPageContract {
}
Widget emptyView() {
return ListView(
children: <Widget>[
SizedBox(height: 100),
Center(
child: Container(
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
child: Image.asset('assets/images/icon_empty_album.png',
fit: BoxFit.fill),
),
),
SizedBox(height: 10),
Center(
child: Text(
'相册暂无任何照片,快点击右下角按钮去上传吧',
style: TextStyle(color: Colors.grey),
),
)
],
Center(
child: Text(
'相册暂无任何照片,快点击右下角按钮去上传吧',
style: TextStyle(color: Colors.grey),
),
)
],
),
);
}
Future<dynamic> _onRefresh() async {
/// 刷新
_onRefresh() async {
// _uploadeds.clear();
setState(() {
_uploadeds.clear();
this._currentPage = 0;
this._uploadeds.clear();
_refreshController.resetNoData();
});
return _presenter.doLoadUploadedImages();
_presenter.doLoadUploadedImages(_perPageItemSize, this._currentPage);
}
/// 上拉加载
_onLoading() async {
_presenter.doLoadUploadedImages(_perPageItemSize, _currentPage += 1);
}
/// 处理图片点击
handleTap(int index) {
Clipboard.setData(ClipboardData(text: _uploadeds[index].path));
Toast.show('已复制到剪切板', context);
@@ -179,13 +229,29 @@ class _AlbumPageState extends State<AlbumPage> implements AlbumPageContract {
@override
void loadUploadedImages(List<Uploaded> uploadeds) {
setState(() {
this._uploadeds.addAll(uploadeds);
if (this._currentPage == 0) {
_refreshController.refreshCompleted();
} else if (this._currentPage > 0 &&
(uploadeds == null || uploadeds.length == 0)) {
_refreshController.loadNoData();
} else {
this._currentPage += 1;
_refreshController.loadComplete();
}
if (uploadeds != null && uploadeds.length > 0) {
this._uploadeds.addAll(uploadeds);
}
});
}
@override
void loadError() {
Toast.show('加载失败', context);
if (this._currentPage == 1) {
_refreshController.refreshFailed();
} else {
_refreshController.loadFailed();
}
}
@override

View File

@@ -17,10 +17,11 @@ class AlbumPagePresenter {
AlbumPagePresenter(this._view);
doLoadUploadedImages() async {
doLoadUploadedImages(int limit, int offest) async {
try {
var sql = Sql.setTable(TABLE_NAME_UPLOADED);
var result = await sql.get();
var result = await sql.getBySql(null, null,
limit: limit, offset: offest * limit, orderBy: 'id DESC');
List<Uploaded> uploadeds = result.map((v) {
return Uploaded.fromMap(v);
}).toList();

View File

@@ -1,26 +1,20 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_picgo/model/aliyun_config.dart';
import 'package:flutter_picgo/model/config.dart';
import 'package:flutter_picgo/resources/pb_type_keys.dart';
import 'package:flutter_picgo/views/pb_setting_page/aliyun_page/aliyun_page_presenter.dart';
import 'package:flutter_picgo/utils/strings.dart';
import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart';
class AliyunPage extends StatefulWidget {
_AliyunPageState createState() => _AliyunPageState();
}
class _AliyunPageState extends BasePBSettingPageState<AliyunPage>
implements AliyunPageContract {
AliyunPagePresenter _presenter;
_AliyunPageState() {
_presenter = AliyunPagePresenter(this);
}
class _AliyunPageState extends BasePBSettingPageState<AliyunPage> {
@override
void initState() {
super.initState();
_presenter.doLoadConfig();
}
@override
@@ -33,9 +27,15 @@ class _AliyunPageState extends BasePBSettingPageState<AliyunPage>
String get pbType => PBTypeKeys.aliyun;
@override
loadConfig(AliyunConfig config) {
onLoadConfig(String config) {
List<Config> configs = [];
config.toJson().forEach((key, value) {
Map<String, dynamic> map;
if (isBlank(config)) {
map = AliyunConfig().toJson();
} else {
map = AliyunConfig.fromJson(json.decode(config)).toJson();
}
map.forEach((key, value) {
Config config;
if (key == 'accessKeyId') {
config = Config(

View File

@@ -1,28 +0,0 @@
import 'dart:convert';
import 'package:flutter_picgo/model/aliyun_config.dart';
import 'package:flutter_picgo/resources/pb_type_keys.dart';
import 'package:flutter_picgo/utils/image_upload.dart';
import 'package:flutter_picgo/utils/strings.dart';
abstract class AliyunPageContract {
loadConfig(AliyunConfig config);
}
class AliyunPagePresenter {
AliyunPageContract _view;
AliyunPagePresenter(this._view);
doLoadConfig() async {
try {
var configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.aliyun);
if (!isBlank(configStr)) {
AliyunConfig config = AliyunConfig.fromJson(json.decode(configStr));
_view.loadConfig(config);
} else {
_view.loadConfig(AliyunConfig());
}
} catch (e) {}
}
}

View File

@@ -13,6 +13,12 @@ abstract class BasePBSettingPageState<T extends StatefulWidget>
Map<String, TextEditingController> controllers = {};
GlobalKey<FormState> _formKey;
@override
void initState() {
super.initState();
loadConfig();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -140,6 +146,9 @@ abstract class BasePBSettingPageState<T extends StatefulWidget>
/// 当前图床类型
String get pbType;
/// 子类根据config String自定义需求
onLoadConfig(String config);
/// 表单验证
bool get validate => _formKey?.currentState?.validate() ?? true;
@@ -165,6 +174,14 @@ abstract class BasePBSettingPageState<T extends StatefulWidget>
}
}
/// 加载图床配置
loadConfig() async {
try {
var configStr = await ImageUploadUtils.getPBConfig(pbType);
onLoadConfig(configStr);
} catch (e) {}
}
/// 配置默认图床
_setDefaultPB() async {
if (!isBlank(pbType)) {

View File

@@ -47,7 +47,7 @@ class _PBSettingPageState extends State<PBSettingPage>
return AlertDialog(
title: Text('关于使用二维码扫描说明'),
content: Text('该功能是为了方便使用了PicGo PC端的用户。\n\n' +
'在PC端已经配置好的用户可以将PicGo的配置文件转换成二维码。\n\n' +
'在PC端已经配置好的用户可以使用PicGo新版的生成图床配置二维码功能。\n\n' +
'然后使用Flutter-PicGo直接扫描即可进行配置转换。'),
actions: <Widget>[
FlatButton(

View File

@@ -1,27 +1,21 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_picgo/model/config.dart';
import 'package:flutter_picgo/model/qiniu_config.dart';
import 'package:flutter_picgo/resources/pb_type_keys.dart';
import 'package:flutter_picgo/utils/strings.dart';
import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart';
import 'package:flutter_picgo/views/pb_setting_page/qiniu_page/qiniu_page_presenter.dart';
class QiniuPage extends StatefulWidget {
_QiniuPageState createState() => _QiniuPageState();
}
class _QiniuPageState extends BasePBSettingPageState<QiniuPage>
implements QiniuPageContract {
QiniuPagePresenter _presenter;
_QiniuPageState() {
_presenter = QiniuPagePresenter(this);
}
class _QiniuPageState extends BasePBSettingPageState<QiniuPage> {
@override
void initState() {
super.initState();
_presenter.doLoadConfig();
}
@override
@@ -34,9 +28,15 @@ class _QiniuPageState extends BasePBSettingPageState<QiniuPage>
);
@override
loadConfig(QiniuConfig config) {
onLoadConfig(String config) {
List<Config> configs = [];
config.toJson().forEach((key, value) {
Map<String, dynamic> map;
if (isBlank(config)) {
map = QiniuConfig().toJson();
} else {
map = QiniuConfig.fromJson(json.decode(config)).toJson();
}
map.forEach((key, value) {
Config config;
if (key == 'accessKey') {
config = Config(

View File

@@ -1,28 +0,0 @@
import 'dart:convert';
import 'package:flutter_picgo/model/qiniu_config.dart';
import 'package:flutter_picgo/resources/pb_type_keys.dart';
import 'package:flutter_picgo/utils/image_upload.dart';
import 'package:flutter_picgo/utils/strings.dart';
abstract class QiniuPageContract {
loadConfig(QiniuConfig config);
}
class QiniuPagePresenter {
QiniuPageContract _view;
QiniuPagePresenter(this._view);
doLoadConfig() async {
try {
var configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.qiniu);
if (!isBlank(configStr)) {
QiniuConfig config = QiniuConfig.fromJson(json.decode(configStr));
_view.loadConfig(config);
} else {
_view.loadConfig(QiniuConfig());
}
} catch (e) {}
}
}

View File

@@ -0,0 +1,72 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_picgo/model/config.dart';
import 'package:flutter_picgo/model/tcyun_config.dart';
import 'package:flutter_picgo/resources/pb_type_keys.dart';
import 'package:flutter_picgo/utils/strings.dart';
import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart';
class TcyunPage extends StatefulWidget {
_TcyunPageState createState() => _TcyunPageState();
}
class _TcyunPageState extends BasePBSettingPageState<TcyunPage> {
@override
AppBar get appbar => AppBar(
title: Text('腾讯云COS图床'),
centerTitle: true,
);
@override
String get pbType => PBTypeKeys.tcyun;
@override
onLoadConfig(String config) {
List<Config> configs = [];
Map<String, dynamic> map;
if (isBlank(config)) {
map = TcyunConfig().toJson();
} else {
map = TcyunConfig.fromJson(json.decode(config)).toJson();
}
map.forEach((key, value) {
Config config;
if (key == 'secretId') {
config = Config(
label: '设定SecretId',
placeholder: 'SecretId',
needValidate: true,
value: value);
} else if (key == 'secretKey') {
config = Config(
label: '设定SecretKey',
placeholder: 'SecretKey',
needValidate: true,
value: value);
} else if (key == 'bucket') {
config = Config(
label: '设定存储空间名',
placeholder: '例如test-1253954259',
needValidate: true,
value: value);
} else if (key == 'area') {
config = Config(
label: '确认存储区域',
placeholder: '例如tj',
needValidate: true,
value: value);
} else if (key == 'path') {
config = Config(label: '指定存储路径', placeholder: '例如img/', value: value);
} else if (key == 'customUrl') {
config = Config(
label: '设定自定义域名',
placeholder: '例如:http://xxx.yyy.cn',
value: value);
}
config.name = key;
configs.add(config);
});
setConfigs(configs);
}
}

View File

@@ -1,11 +1,17 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:fluro/fluro.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_picgo/api/picgo_api.dart';
import 'package:flutter_picgo/routers/application.dart';
import 'package:flutter_picgo/routers/routers.dart';
import 'package:flutter_picgo/utils/local_notification.dart';
import 'package:flutter_picgo/utils/shared_preferences.dart';
import 'package:package_info/package_info.dart';
import 'package:toast/toast.dart';
import 'package:url_launcher/url_launcher.dart';
class PicGoSettingPage extends StatefulWidget {
@override
@@ -17,6 +23,7 @@ class _PicGoSettingPageState extends State<PicGoSettingPage> {
bool isTimestampRename = false;
bool isUploadedTip = false;
bool isForceDelete = false;
bool isNeedUpdate = false;
@override
void initState() {
@@ -33,6 +40,8 @@ class _PicGoSettingPageState extends State<PicGoSettingPage> {
u?.getBool(SharedPreferencesKeys.settingIsForceDelete) ?? false;
});
});
// update
_getLatestVersion();
}
@override
@@ -111,13 +120,26 @@ class _PicGoSettingPageState extends State<PicGoSettingPage> {
transition: TransitionType.cupertino);
},
),
ListTile(
title: Text('设置显示图床'),
onTap: () {},
),
// ListTile(
// title: Text('设置显示图床'),
// onTap: () {},
// ),
ListTile(
title: Text('检查更新'),
onTap: () {},
onTap: () {
_handleUpdateTap();
},
trailing: CircleAvatar(
backgroundColor: Colors.transparent,
child: Container(
width: 8,
height: 8,
// color: Colors.red,
decoration: BoxDecoration(
color: isNeedUpdate ? Colors.red : Colors.transparent,
borderRadius: BorderRadius.circular(4)),
),
),
),
],
);
@@ -131,4 +153,38 @@ class _PicGoSettingPageState extends State<PicGoSettingPage> {
instance.putBool(key, value);
Toast.show('保存成功', context);
}
_getLatestVersion() async {
try {
Response res = await PicgoApi.getLatestVersion();
PackageInfo info = await PackageInfo.fromPlatform();
int version = int.parse(info.buildNumber);
debugPrint('$version');
int remoteVersion = 0;
if (Platform.isAndroid) {
remoteVersion = int.parse('${res.data["Android"]["versionCode"]}');
} else if (Platform.isIOS) {
remoteVersion = int.parse('${res.data["iOS"]["versionCode"]}');
}
if (version < remoteVersion) {
setState(() {
this.isNeedUpdate = true;
});
}
} catch (e) {
print(e);
}
}
_handleUpdateTap() async {
if (isNeedUpdate) {
if (Platform.isAndroid) {
launch('https://www.pgyer.com/flutter-picgo');
} else if (Platform.isIOS) {
launch('https://apps.apple.com/cn/app/flutter-picgo/id1519714305');
}
} else {
Toast.show('当前已经是最新版本', context);
}
}
}

View File

@@ -21,10 +21,11 @@ class _UploadPageState extends State<UploadPage>
implements UploadPageContract {
String _title = '';
String _previewPath = '';
String _renameImage;
String _clipUrl;
String _renameImage = '';
String _clipUrl = '';
TextEditingController _controller;
int _selectButton = 1;
// 默认不选中则不复制
int _selectButton = 0;
// 通知提示
bool needNotify = false;
@@ -280,16 +281,16 @@ class _UploadPageState extends State<UploadPage>
}
@override
uploadFaild(String errorMsg) {
uploadFaild(String errorMsg) async {
if (needNotify) {
_showNotification(0, '上传失败:$errorMsg');
await _showNotification(0, '上传失败:$errorMsg');
}
Toast.show(errorMsg ?? '', context);
}
/// 设置剪切板
setClipData([bool needShowTip = true]) {
if (_clipUrl == null || _clipUrl == '') {
if (isBlank(_clipUrl)) {
Toast.show('暂无可获取图片', context);
return;
}
@@ -313,11 +314,13 @@ class _UploadPageState extends State<UploadPage>
default:
{
//statements;
cliptext = _clipUrl;
cliptext = '';
}
break;
}
Clipboard.setData(ClipboardData(text: cliptext));
if (!isBlank(cliptext)) {
Clipboard.setData(ClipboardData(text: cliptext));
}
if (needShowTip) {
Toast.show('已复制到剪切板', context);
}
@@ -326,11 +329,11 @@ class _UploadPageState extends State<UploadPage>
@override
uploadSuccess(String imageUrl) async {
if (needNotify) {
await _showNotification(1, '上传成功:图片链接已复制到剪切板,原链接:$imageUrl');
await _showNotification(1, '上传成功, 图片链接:$imageUrl');
}
this._clipUrl = imageUrl;
setClipData(false);
Toast.show('上传成功:已复制到剪切板,图片链接为:$imageUrl', context);
Toast.show('上传成功,图片链接为:$imageUrl', context);
}
Future<void> _showNotification(int id, String body) async {

View File

@@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.4.1+9
version: 1.5.2+12
environment:
sdk: ">=2.7.0 <3.0.0"
@@ -39,6 +39,7 @@ dependencies:
provider: ^4.1.3
crypto: ^2.1.4
flutter_local_notifications: ^1.4.4+1
pull_to_refresh: ^1.6.0
# The following adds the Cupertino Icons font to your application.

View File

@@ -0,0 +1,60 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_picgo/api/tcyun_api.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as path;
main() {
test('测试生成 KeyTime', () {
print(TcyunApi.buildKeyTime());
});
test('测试生成 SignKey', () {
print(TcyunApi.buildSignKey('adsadaads', TcyunApi.buildKeyTime()));
});
test('测试Api 签名', () async {
try {
await TcyunApi.deleteobject(
'AKID70e28x4gazd17vKiywkITO1NSWHv6s75',
'pN0MRGo5HCqDxTqpGPTFlCsHPKzUjhZD',
'test-1253954259',
'ap-nanjing',
'wallhaven-2e9j79.jpg');
} catch (e) {
print(e);
}
});
test('PostObjecy 提交', () async {
try {
String pathname = path.joinAll(
[Directory.current.path, '..\\assets\\' 'images', 'logo.png']);
print(pathname);
String keyTime = TcyunApi.buildKeyTime();
String policy = TcyunApi.buildPolicy('ap-nanjing', 'logo.png',
'AKIDvb0B9rqfeOr44kt2ar46rO2cwzl6JwUk', keyTime);
print(policy);
// TcyunApi.postObject(
// 'test-1253954259',
// 'ap-nanjing',
// 'png',
// FormData.fromMap({
// "key": "logo.png",
// "file": await MultipartFile.fromFile(
// 'C:\\Users\\Administrator\\Desktop\\flutter-picgo\\flutter-picgo\\assets\\images\\logo.png',
// filename: 'logo.png'),
// "policy": base64.encode(utf8.encode(policy)),
// "q-sign-algorithm": "sha1",
// "q-ak": "AKIDvb0B9rqfeOr44kt2ar46rO2cwzl6JwUk",
// "q-key-time": keyTime,
// "q-signature": TcyunApi.buildSignature(
// 'KOEoR1LL5apX1lFRN4VgZB0nJgmdEbie', keyTime, policy)
// }));
} catch (e) {
print(e);
}
});
}