├── ios ├── Runner │ ├── zh-Hans.lproj │ │ ├── Main.strings │ │ └── LaunchScreen.strings │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── icon-1024.png │ │ │ ├── icon-57.png │ │ │ ├── icon-20@3x.png │ │ │ ├── icon-29@3x.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-57@2x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-29-ipad.png │ │ │ ├── icon-20@2x-ipad.png │ │ │ ├── icon-29@2x-ipad.png │ │ │ └── Contents.json │ │ └── LaunchImage.imageset │ │ │ ├── launcher_bg.png │ │ │ ├── README.md │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── Podfile └── Podfile.lock ├── android ├── settings_aar.gradle ├── keystore │ ├── key.jks │ └── key.properties ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_bg.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── siyee │ │ │ │ │ └── flutterpicgo │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── .classpath │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── .project │ └── build.gradle ├── .gitignore ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .settings │ └── org.eclipse.buildship.core.prefs ├── .project ├── build.gradle └── settings.gradle ├── docs ├── hostconfig.jpeg ├── design │ ├── launcher.psd │ ├── squareLogo.png │ ├── squareLogo.psd │ ├── launcher_bg.png │ ├── squareLogo144.png │ ├── squareLogo192.png │ ├── squareLogo256.png │ ├── squareLogo384.png │ ├── squareLogo1024.png │ └── db.json ├── dist │ ├── ios_development.cer │ ├── ios_distribution.cer │ ├── pico_dev_profile.mobileprovision │ ├── pico_appstore_profile.mobileprovision │ └── CertificateSigningRequest.certSigningRequest ├── version.json ├── static │ └── location.html └── 设计文档.md ├── assets ├── images │ ├── logo.png │ ├── 2.0x │ │ └── logo.png │ ├── 3.0x │ │ └── logo.png │ ├── 4.0x │ │ └── logo.png │ └── icon_empty_album.png └── fonts │ └── iconfont.ttf ├── lib ├── resources │ ├── table_name_keys.dart │ ├── pb_type_keys.dart │ ├── shared_preferences_keys.dart │ └── theme_colors.dart ├── utils │ ├── strings.dart │ ├── strategy │ │ ├── image_upload_strategy.dart │ │ ├── impl │ │ │ ├── niupic_image_upload.dart │ │ │ ├── lsky_image_upload.dart │ │ │ ├── smms_image_upload.dart │ │ │ ├── upyun_image_upload.dart │ │ │ ├── tcyun_image_upload.dart │ │ │ ├── github_image_upload.dart │ │ │ ├── aliyun_image_upload.dart │ │ │ └── gitee_image_upload.dart │ │ └── upload_strategy_factory.dart │ ├── permission.dart │ ├── encrypt.dart │ ├── extended.dart │ ├── sql.dart │ ├── net.dart │ ├── shared_preferences.dart │ ├── image_upload.dart │ └── local_notification.dart ├── model │ ├── base.dart │ ├── received_notification.dart │ ├── smms_config.dart │ ├── uploaded.dart │ ├── lsky_config.dart │ ├── github_config.dart │ ├── config.dart │ ├── pb_setting.dart │ ├── gitee_config.dart │ ├── upyun_config.dart │ ├── tcyun_config.dart │ ├── qiniu_config.dart │ ├── aliyun_config.dart │ ├── qiniu_content.dart │ ├── gitee_content.dart │ ├── theme_state.dart │ ├── smms_content.dart │ ├── github_content.dart │ └── lsky_content.dart ├── api │ ├── picgo_api.dart │ ├── niupic_api.dart │ ├── lsky_api.dart │ ├── github_api.dart │ ├── smms_api.dart │ ├── gitee_api.dart │ ├── upyun_api.dart │ └── aliyun_api.dart ├── routers │ ├── application.dart │ └── routers.dart ├── views │ ├── pb_setting_page │ │ ├── niupic_page │ │ │ └── niupic_page.dart │ │ ├── smms_page │ │ │ └── smms_page.dart │ │ ├── pb_setting_presenter.dart │ │ ├── upyun_page │ │ │ └── upyun_page.dart │ │ ├── tcyun_page │ │ │ └── tcyun_page.dart │ │ ├── aliyun_page │ │ │ └── aliyun_page.dart │ │ ├── github_page │ │ │ └── github_page.dart │ │ ├── gitee_page │ │ │ └── gitee_page.dart │ │ ├── qiniu_page │ │ │ └── qiniu_page.dart │ │ └── lsky_page │ │ │ └── lsky_page.dart │ ├── 404.dart │ ├── manage_page │ │ ├── base_loading_page_state.dart │ │ ├── smms_page │ │ │ └── smms_repo_page_presenter.dart │ │ ├── lsky_page │ │ │ └── lsky_repo_page_presenter.dart │ │ ├── github_page │ │ │ └── github_repo_page_presenter.dart │ │ ├── gitee_page │ │ │ └── gitee_repo_page_presenter.dart │ │ └── qiniu_page │ │ │ └── qiniu_repo_page_presenter.dart │ ├── home.dart │ ├── album_page │ │ └── album_page_presenter.dart │ ├── picgo_setting_page │ │ └── theme_setting_page.dart │ └── setting_page │ │ └── setting_page.dart ├── components │ ├── upload_item │ │ └── upload_item_presenter.dart │ ├── loading.dart │ └── manage_item.dart └── main.dart ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md └── workflows │ └── build.yml ├── .metadata ├── test └── api │ ├── qiniu_api_test.dart │ ├── tcyun_api_test.dart │ └── aliyun_api_test.dart ├── .gitignore ├── LICENSE └── pubspec.yaml /ios/Runner/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ios/Runner/zh-Hans.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /docs/hostconfig.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/hostconfig.jpeg -------------------------------------------------------------------------------- /android/keystore/key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/keystore/key.jks -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/assets/images/logo.png -------------------------------------------------------------------------------- /docs/design/launcher.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/launcher.psd -------------------------------------------------------------------------------- /assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /docs/design/squareLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo.png -------------------------------------------------------------------------------- /docs/design/squareLogo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo.psd -------------------------------------------------------------------------------- /assets/images/2.0x/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/assets/images/2.0x/logo.png -------------------------------------------------------------------------------- /assets/images/3.0x/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/assets/images/3.0x/logo.png -------------------------------------------------------------------------------- /assets/images/4.0x/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/assets/images/4.0x/logo.png -------------------------------------------------------------------------------- /docs/design/launcher_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/launcher_bg.png -------------------------------------------------------------------------------- /docs/design/squareLogo144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo144.png -------------------------------------------------------------------------------- /docs/design/squareLogo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo192.png -------------------------------------------------------------------------------- /docs/design/squareLogo256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo256.png -------------------------------------------------------------------------------- /docs/design/squareLogo384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo384.png -------------------------------------------------------------------------------- /docs/dist/ios_development.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/dist/ios_development.cer -------------------------------------------------------------------------------- /docs/design/squareLogo1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/design/squareLogo1024.png -------------------------------------------------------------------------------- /docs/dist/ios_distribution.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/dist/ios_distribution.cer -------------------------------------------------------------------------------- /assets/images/icon_empty_album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/assets/images/icon_empty_album.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /docs/dist/pico_dev_profile.mobileprovision: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/dist/pico_dev_profile.mobileprovision -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/drawable/ic_launcher.png -------------------------------------------------------------------------------- /docs/dist/pico_appstore_profile.mobileprovision: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/docs/dist/pico_appstore_profile.mobileprovision -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/mipmap-xxhdpi/launcher_bg.png -------------------------------------------------------------------------------- /lib/resources/table_name_keys.dart: -------------------------------------------------------------------------------- 1 | /// table name keys 2 | 3 | const String TABLE_NAME_PBSETTING = 'pb_setting'; 4 | const String TABLE_NAME_UPLOADED = 'uploaded'; -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/launcher_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/launcher_bg.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PicGo/flutter-picgo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /lib/utils/strings.dart: -------------------------------------------------------------------------------- 1 | /// Returns true if s is either null, empty or is solely made of whitespace characters (as defined by String.trim). 2 | 3 | bool isBlank(String s) => s == null || s.trim().isEmpty; -------------------------------------------------------------------------------- /docs/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "iOS": { 3 | "versionName": "1.9.2", 4 | "versionCode": "24" 5 | }, 6 | "Android": { 7 | "versionName": "1.9.2", 8 | "versionCode": "24" 9 | } 10 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/siyee/flutterpicgo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.siyee.flutterpicgo 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /lib/model/base.dart: -------------------------------------------------------------------------------- 1 | import 'package:sqflite/sqflite.dart'; 2 | 3 | class BaseModel { 4 | Database db; 5 | final String table = ''; 6 | var query; 7 | BaseModel(this.db) { 8 | query = db.query; 9 | } 10 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: I have a suggestion 4 | --- 5 | 6 | ## Feature Request 7 | 8 | **仅限中文与英文**, 其他语言的提交将直接被关闭 9 | 10 | ### 功能需求 11 | 12 | ### 描述您想要的功能. 13 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /android/keystore/key.properties: -------------------------------------------------------------------------------- 1 | # keytool -genkey -v -keystore C:\Users\Administrator\Desktop\flutter-picgo\flutter-picgo\android\key.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias picgo 2 | keyAlias=picgo 3 | keyPassword=ZJyzy520 4 | storePassword=ZJyzy520 5 | storeFile=../keystore/key.jks -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/utils/strategy/image_upload_strategy.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter_picgo/model/uploaded.dart'; 3 | 4 | abstract class ImageUploadStrategy { 5 | 6 | /// 上传照片,根据策略来实现上传的图片 7 | Future upload(File file, String renameImage); 8 | 9 | Future delete(Uploaded uploaded); 10 | 11 | } -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b041144f833e05cf463b8887fa12efdec9493488 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/api/picgo_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_picgo/utils/net.dart'; 3 | 4 | class PicgoApi { 5 | /// 获取App最新版本 6 | static Future getLatestVersion() async { 7 | Response res = await NetUtils.getInstance().get( 8 | 'https://cdn.jsdelivr.net/gh/PicGo/flutter-picgo/docs/version.json'); 9 | return res; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/model/received_notification.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ReceivedNotification { 4 | final int id; 5 | final String title; 6 | final String body; 7 | final String payload; 8 | 9 | ReceivedNotification({ 10 | @required this.id, 11 | @required this.title, 12 | @required this.body, 13 | @required this.payload, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /lib/model/smms_config.dart: -------------------------------------------------------------------------------- 1 | class SMMSConfig { 2 | String token; 3 | 4 | SMMSConfig({this.token}); 5 | 6 | SMMSConfig.fromJson(Map json) { 7 | token = json['token']; 8 | } 9 | 10 | Map toJson() { 11 | final Map data = new Map(); 12 | data['token'] = this.token; 13 | return data; 14 | } 15 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/api/niupic_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_picgo/utils/net.dart'; 3 | 4 | class NiupicApi { 5 | static const BASE_URL = 'https://www.niupic.com/'; 6 | 7 | static Future upload(FormData data) async { 8 | Response res = await NetUtils.getInstance() 9 | .post(BASE_URL + 'index/upload/process', data: data); 10 | return res.data; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Something isn't working as expected 4 | --- 5 | 6 | ## Bug Report 7 | 8 | **仅限中文与英文**, 其他语言的提交将直接被关闭 9 | 10 | ### 您当前的flutter doctor信息或设备信息 11 | 12 | ### 预期的表现 13 | 14 | ### 实际的表现 15 | 16 | ### 预期的分析 (给出您能想到, 任何您能想到的) 17 | 18 | ### 重现的方式, 例如从 A界面 点击 b, 跳转到B页面, 界面出现溢出乱码等. 19 | 20 | ### 用于重现此问题或者可能解决以上问题的示例代码(例如github 链接代码) 21 | -------------------------------------------------------------------------------- /lib/resources/pb_type_keys.dart: -------------------------------------------------------------------------------- 1 | class PBTypeKeys { 2 | static const github = 'github'; 3 | 4 | static const smms = 'smms'; 5 | 6 | static const gitee = 'gitee'; 7 | 8 | static const qiniu = 'qiniu'; 9 | 10 | static const aliyun = 'aliyun'; 11 | 12 | static const tcyun = 'tcyun'; 13 | 14 | static const niupic = 'niupic'; 15 | 16 | static const lsky = 'lsky'; 17 | 18 | static const upyun = 'upyun'; 19 | } 20 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "filename" : "launcher_bg.png", 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir=app 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(6.3)) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /lib/routers/application.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | 3 | enum ENV { 4 | PRODUCTION, 5 | DEV, 6 | } 7 | 8 | class Application { 9 | /// 通过Application设计环境变量 10 | static ENV env = ENV.DEV; 11 | 12 | static FluroRouter router; 13 | 14 | /// 所有获取配置的唯一入口 15 | Map get config { 16 | if (Application.env == ENV.PRODUCTION) { 17 | return {}; 18 | } 19 | if (Application.env == ENV.DEV) { 20 | return {}; 21 | } 22 | return {}; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/resources/shared_preferences_keys.dart: -------------------------------------------------------------------------------- 1 | class SharedPreferencesKeys { 2 | static final String settingIsUploadedRename = 'settingIsUploadedRename'; 3 | static final String settingIsTimestampRename = 'settingIsTimestampRename'; 4 | static final String settingIsUploadedTip = 'settingIsUploadedTip'; 5 | static final String settingIsForceDelete = 'settingIsForceDelete'; 6 | static final String settingDefaultPB = 'settingDefaultPB'; 7 | 8 | static final String localThemeState = 'localThemeState'; 9 | } 10 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/model/uploaded.dart: -------------------------------------------------------------------------------- 1 | class Uploaded { 2 | int id; 3 | String path; 4 | String type; 5 | String info; 6 | 7 | Uploaded(this.id, this.path, this.type, {this.info}); 8 | 9 | Uploaded.fromMap(Map map) { 10 | this.id = map['id']; 11 | this.type = map['type']; 12 | this.path = map['path']; 13 | this.info = map['info']; 14 | } 15 | 16 | Map toMap() { 17 | var map = {'id': id, 'type': type, 'path': path, 'info': info}; 18 | return map; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/niupic_page/niupic_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 3 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 4 | 5 | class NiupicPage extends StatefulWidget { 6 | _NiupicPageState createState() => _NiupicPageState(); 7 | } 8 | 9 | class _NiupicPageState extends BasePBSettingPageState { 10 | @override 11 | onLoadConfig(String config) {} 12 | 13 | @override 14 | String get pbType => PBTypeKeys.niupic; 15 | 16 | @override 17 | String get title => '牛图网图床'; 18 | } 19 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | if #available(iOS 10.0, *) { 12 | UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate 13 | } 14 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/model/lsky_config.dart: -------------------------------------------------------------------------------- 1 | class LskyConfig { 2 | String host; 3 | String email; 4 | String password; 5 | String token; 6 | 7 | LskyConfig({this.host, this.email, this.password, this.token}); 8 | 9 | LskyConfig.fromJson(Map json) { 10 | host = json['host']; 11 | email = json['email']; 12 | password = json['password']; 13 | token = json['token']; 14 | } 15 | 16 | Map toJson() { 17 | final Map data = new Map(); 18 | data['host'] = this.host; 19 | data['email'] = this.email; 20 | data['password'] = this.password; 21 | data['token'] = this.token; 22 | return data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/views/404.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_picgo/routers/application.dart'; 3 | 4 | class PageNotFound extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Scaffold( 8 | appBar: AppBar( 9 | title: Text("页面走丢啦~"), 10 | ), 11 | body: Container( 12 | child: Center( 13 | child: OutlinedButton( 14 | style: ButtonStyle( 15 | side: 16 | MaterialStateProperty.all(BorderSide(color: Colors.blue))), 17 | child: Text('关闭'), 18 | onPressed: () { 19 | Application.router.pop(context); 20 | }, 21 | ), 22 | ), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/model/github_config.dart: -------------------------------------------------------------------------------- 1 | class GithubConfig { 2 | String branch; 3 | String customUrl; 4 | String path; 5 | String repo; 6 | String token; 7 | 8 | GithubConfig({this.branch, this.customUrl, this.path, this.repo, this.token}); 9 | 10 | GithubConfig.fromJson(Map json) { 11 | branch = json['branch']; 12 | customUrl = json['customUrl']; 13 | path = json['path']; 14 | repo = json['repo']; 15 | token = json['token']; 16 | } 17 | 18 | Map toJson() { 19 | final Map data = new Map(); 20 | data['branch'] = this.branch; 21 | data['customUrl'] = this.customUrl; 22 | data['path'] = this.path; 23 | data['repo'] = this.repo; 24 | data['token'] = this.token; 25 | return data; 26 | } 27 | } -------------------------------------------------------------------------------- /test/api/qiniu_api_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/api/qiniu_api.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | main() { 5 | test('测试UpToken生成', () { 6 | String policy = QiniuApi.generatePutPolicy('image', 'test.png'); 7 | QiniuApi.generateUpToken('CRd7Wa4PuSGvs4ArToPTLBMCigGGUY3sk3F8oc8W', 8 | 'f2Jkrlyea5s8h8gLEToa9-k895GNM-BlmQ2RfxwU', policy); 9 | }); 10 | 11 | test('测试AuthToken生成', () { 12 | var token = QiniuApi.generateAuthToken( 13 | 'post', 14 | '/move/bmV3ZG9jczpmaW5kX21hbi50eHQ=/bmV3ZG9jczpmaW5kLm1hbi50eHQ=', 15 | null, 16 | 'rs.qiniu.com', 17 | null, 18 | null, 19 | 'MY_ACCESS_KEY', 20 | 'MY_SECRET_KEY'); 21 | expect(token, 'MY_ACCESS_KEY:1uLvuZM6l6oCzZFqkJ6oI4oFMVQ='); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /docs/static/location.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Flutter-Picgo Release 8 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/model/config.dart: -------------------------------------------------------------------------------- 1 | class Config { 2 | String name; 3 | String value; 4 | String label; 5 | bool needValidate; 6 | String placeholder; 7 | 8 | Config( 9 | {this.name, this.value, this.label, this.needValidate, this.placeholder}); 10 | 11 | Config.fromJson(Map json) { 12 | name = json['name']; 13 | value = json['value']; 14 | label = json['label']; 15 | needValidate = json['needValidate']; 16 | placeholder = json['placeholder']; 17 | } 18 | 19 | Map toJson() { 20 | final Map data = new Map(); 21 | data['name'] = this.name; 22 | data['value'] = this.value; 23 | data['label'] = this.label; 24 | data['needValidate'] = this.needValidate; 25 | data['placeholder'] = this.placeholder; 26 | return data; 27 | } 28 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | maven { url "https://storage.flutter-io.cn/download.flutter.io" } 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | maven { url "https://storage.flutter-io.cn/download.flutter.io" } 20 | } 21 | } 22 | 23 | rootProject.buildDir = '../build' 24 | subprojects { 25 | project.buildDir = "${rootProject.buildDir}/${project.name}" 26 | } 27 | subprojects { 28 | project.evaluationDependsOn(':app') 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/model/pb_setting.dart: -------------------------------------------------------------------------------- 1 | class PBSetting { 2 | int _id; 3 | String _type; 4 | String _name; 5 | String _config; 6 | String _path; 7 | bool _visible; 8 | 9 | PBSetting(); 10 | 11 | int get id => _id; 12 | String get type => _type; 13 | String get name => _name; 14 | String get config => _config; 15 | String get path => _path; 16 | bool get visible => _visible; 17 | 18 | PBSetting.fromMap(Map map) { 19 | this._id = map['id']; 20 | this._type = map['type']; 21 | this._name = map['name']; 22 | this._config = map['config']; 23 | this._path = map['path']; 24 | this._visible = map['visible'] == 1; 25 | } 26 | 27 | Map toMap() { 28 | var map = { 29 | 'id': _id, 30 | 'type': _type, 31 | 'name': _name, 32 | 'config': _config, 33 | 'path': _path, 34 | 'visible': _visible ? 1 : 0, 35 | }; 36 | return map; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .vscode/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | pubspec.lock 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Exceptions to above rules. 45 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 46 | 47 | # ios build 48 | ios/Flutter/.last_build_id 49 | -------------------------------------------------------------------------------- /lib/model/gitee_config.dart: -------------------------------------------------------------------------------- 1 | class GiteeConfig { 2 | String owner; 3 | String path; 4 | String repo; 5 | String token; 6 | String customUrl; 7 | String branch; 8 | 9 | GiteeConfig( 10 | {this.owner, 11 | this.path, 12 | this.repo, 13 | this.token, 14 | this.customUrl, 15 | this.branch}); 16 | 17 | GiteeConfig.fromJson(Map json) { 18 | owner = json['owner']; 19 | path = json['path']; 20 | repo = json['repo']; 21 | token = json['token']; 22 | customUrl = json['customUrl']; 23 | branch = json['branch']; 24 | } 25 | 26 | Map toJson() { 27 | final Map data = new Map(); 28 | data['owner'] = this.owner; 29 | data['path'] = this.path; 30 | data['repo'] = this.repo; 31 | data['token'] = this.token; 32 | data['customUrl'] = this.customUrl; 33 | data['branch'] = this.branch; 34 | return data; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/model/upyun_config.dart: -------------------------------------------------------------------------------- 1 | class UpyunConfig { 2 | String bucket; 3 | String operator; 4 | String options; 5 | String password; 6 | String path; 7 | String url; 8 | 9 | UpyunConfig( 10 | {this.bucket, 11 | this.operator, 12 | this.options, 13 | this.password, 14 | this.path, 15 | this.url}); 16 | 17 | UpyunConfig.fromJson(Map json) { 18 | bucket = json['bucket']; 19 | operator = json['operator']; 20 | options = json['options']; 21 | password = json['password']; 22 | path = json['path']; 23 | url = json['url']; 24 | } 25 | 26 | Map toJson() { 27 | final Map data = new Map(); 28 | data['bucket'] = this.bucket; 29 | data['operator'] = this.operator; 30 | data['options'] = this.options; 31 | data['password'] = this.password; 32 | data['path'] = this.path; 33 | data['url'] = this.url; 34 | return data; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/model/tcyun_config.dart: -------------------------------------------------------------------------------- 1 | class TcyunConfig { 2 | String area; 3 | String bucket; 4 | String customUrl; 5 | String path; 6 | String secretId; 7 | String secretKey; 8 | 9 | TcyunConfig({ 10 | this.area, 11 | this.bucket, 12 | this.customUrl, 13 | this.path, 14 | this.secretId, 15 | this.secretKey}); 16 | 17 | TcyunConfig.fromJson(Map json) { 18 | area = json['area']; 19 | bucket = json['bucket']; 20 | customUrl = json['customUrl']; 21 | path = json['path']; 22 | secretId = json['secretId']; 23 | secretKey = json['secretKey']; 24 | } 25 | 26 | Map toJson() { 27 | final Map data = new Map(); 28 | data['area'] = this.area; 29 | data['bucket'] = this.bucket; 30 | data['customUrl'] = this.customUrl; 31 | data['path'] = this.path; 32 | data['secretId'] = this.secretId; 33 | data['secretKey'] = this.secretKey; 34 | return data; 35 | } 36 | } -------------------------------------------------------------------------------- /lib/utils/permission.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:permission_handler/permission_handler.dart'; 3 | 4 | class PermissionUtils { 5 | static Future requestPhotos() async { 6 | // 检测权限 7 | var status = await Permission.photos.request(); 8 | return status; 9 | } 10 | 11 | static Future requestCemera() async { 12 | var status = await Permission.camera.request(); 13 | return status; 14 | } 15 | 16 | static showPermissionDialog(BuildContext context, {text: '无法正常访问,因为没有权限'}) { 17 | showDialog( 18 | context: context, 19 | builder: (context) { 20 | return AlertDialog( 21 | title: Text('警告'), 22 | content: Text(text), 23 | actions: [ 24 | TextButton( 25 | child: Text('去设置'), 26 | onPressed: () { 27 | openAppSettings(); 28 | }, 29 | ), 30 | ], 31 | ); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /docs/dist/CertificateSigningRequest.certSigningRequest: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIChTCCAW0CAQAwQDEhMB8GCSqGSIb3DQEJARYScWE4OTQxNzg1MjJAcXEuY29t 3 | MQ4wDAYDVQQDDAV6anl6eTELMAkGA1UEBhMCQ04wggEiMA0GCSqGSIb3DQEBAQUA 4 | A4IBDwAwggEKAoIBAQCWX7LYj3eLAJQuUSd2BAKktm6MLrWuGTuXR/csWBFAJpYK 5 | NwFhfoMDHvOUHvv2lIWZm8DyBwworlggxWK7smBjqMYly2uR0ABLG6YeIrPg7AGn 6 | R63gA5z8QR27o/xsbLH9zFwKNgMSiZgKfkRpGWBcyAUsn+FDT+msFVS6vE9w1EeT 7 | F6tTwuc2Bn22RtSvfjXCIkPzyFFP/Kd2ccBVL4791dbL/cwmBWaZaLD0spJrZazW 8 | neJO/qtu8Ay1oWDEvMZG2PL86r7kOnP7wXmBFLYLaCfVx1/4Tk4QUaO/rakl3fOP 9 | ZrdQ+vv/5raPQP017bpFxBY+8SbkcgnGc3EIFCwlAgMBAAGgADANBgkqhkiG9w0B 10 | AQsFAAOCAQEAcV5+95dM+TdMLrDB3QWtwHE04zL3F9tDPkOwC4FyeJuOORQdqN2T 11 | V4LKxtc07jMZubxR+Y8vFkfWKoJnShRWoF8J8tfGUBjxd/fCS7iY2Z4EbgBBst02 12 | /HjLwkhVElUCPTk2vEZjwxJqAR0IBMWf5Pqm3HbZHgbat7Beq6+cm5dvXUSngxeW 13 | dfAt1qRtxv8JDWuBo/Pk5WekdKphOs59l26d5xb36hcrjbNqH+AurXCHYgLlaqck 14 | ZytckuvqTL1ckCaNJ60fHD4vNmb77DvyMvpvwQCzYenE8mjXLM/i7U6SEI/3ndeg 15 | drFfaI1eqCsOubFX0OuN0B5GSJccIM/4MA== 16 | -----END CERTIFICATE REQUEST----- 17 | -------------------------------------------------------------------------------- /lib/model/qiniu_config.dart: -------------------------------------------------------------------------------- 1 | class QiniuConfig { 2 | String accessKey; 3 | String area; 4 | String bucket; 5 | String options; 6 | String path; 7 | String secretKey; 8 | String url; 9 | 10 | QiniuConfig( 11 | {this.accessKey, 12 | this.area, 13 | this.bucket, 14 | this.options, 15 | this.path, 16 | this.secretKey, 17 | this.url}); 18 | 19 | QiniuConfig.fromJson(Map json) { 20 | accessKey = json['accessKey']; 21 | area = json['area']; 22 | bucket = json['bucket']; 23 | options = json['options']; 24 | path = json['path']; 25 | secretKey = json['secretKey']; 26 | url = json['url']; 27 | } 28 | 29 | Map toJson() { 30 | final Map data = new Map(); 31 | data['accessKey'] = this.accessKey; 32 | data['area'] = this.area; 33 | data['bucket'] = this.bucket; 34 | data['options'] = this.options; 35 | data['path'] = this.path; 36 | data['secretKey'] = this.secretKey; 37 | data['url'] = this.url; 38 | return data; 39 | } 40 | } -------------------------------------------------------------------------------- /lib/views/manage_page/base_loading_page_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class BaseLoadingPageState extends State { 4 | /// 当前状态,默认状态为LOADING 5 | LoadState state; 6 | 7 | @override 8 | void initState() { 9 | super.initState(); 10 | state = LoadState.LOADING; 11 | } 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: appBar ?? AppBar(), 17 | body: _buildStateWidget, 18 | ); 19 | } 20 | 21 | Widget get _buildStateWidget { 22 | switch (state) { 23 | case LoadState.EMTPY: 24 | return buildEmtpy(); 25 | case LoadState.ERROR: 26 | return buildError(); 27 | case LoadState.LOADING: 28 | return buildLoading(); 29 | case LoadState.SUCCESS: 30 | return buildSuccess(); 31 | } 32 | return buildError(); 33 | } 34 | 35 | Widget buildEmtpy(); 36 | Widget buildError(); 37 | Widget buildLoading(); 38 | Widget buildSuccess(); 39 | AppBar get appBar; 40 | } 41 | 42 | enum LoadState { LOADING, EMTPY, ERROR, SUCCESS } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mr.Yang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # main.yml 2 | # https://github.com/marketplace/actions/flutter-action 3 | # https://github.com/marketplace/actions/create-release 4 | # 工作流程的名字 5 | name: Build and Release apk 6 | 7 | # 工作流程触发的时机,这里是当一个版本标签推送到仓库时触发 8 | on: 9 | push: 10 | tags: 11 | - v* 12 | 13 | # 这个工作流程需要执行的任务 14 | jobs: 15 | process: 16 | name: all process 17 | runs-on: ubuntu-latest 18 | # 这个任务的步骤 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: actions/setup-java@v1 22 | with: 23 | java-version: '12.x' 24 | - uses: subosito/flutter-action@v1 25 | with: 26 | channel: 'stable' # or: 'dev' or 'beta' 27 | - run: flutter pub get 28 | - run: flutter test 29 | - run: flutter build apk --release --no-tree-shake-icons 30 | - uses: actions/upload-artifact@v2 31 | with: 32 | path: build/app/outputs/apk/release/*.apk 33 | - run: | 34 | curl -F "file=@build/app/outputs/apk/release/app-release.apk" -F "uKey=${{ secrets.PGY_USER_KEY }}" -F "_api_key=${{ secrets.PGY_API_KEY }}" https://upload.pgyer.com/apiv1/app/upload -------------------------------------------------------------------------------- /lib/components/upload_item/upload_item_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:flutter_picgo/utils/image_upload.dart'; 4 | import 'package:flutter_picgo/utils/strategy/upload_strategy_factory.dart'; 5 | 6 | abstract class UploadItemContract { 7 | uploadSuccess(String url); 8 | uploadFaild(String errorMsg); 9 | } 10 | 11 | class UploadItemPresenter { 12 | UploadItemContract _view; 13 | UploadItemPresenter(this._view); 14 | 15 | /// 根据配置上传图片 16 | doUploadImage(File file, String renameImage) async { 17 | // 读取配置 18 | try { 19 | String pbType = await ImageUploadUtils.getDefaultPB(); 20 | var uploader = 21 | ImageUploadUtils(UploadStrategyFactory.getUploadStrategy(pbType)); 22 | var uploadedItem = await uploader.upload(file, renameImage); 23 | if (uploadedItem != null) { 24 | _view.uploadSuccess(uploadedItem.path); 25 | } else { 26 | _view.uploadFaild('上传失败!请重试'); 27 | } 28 | } on DioError catch (e) { 29 | _view.uploadFaild('${e.message}'); 30 | } catch (e) { 31 | _view.uploadFaild('$e'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/utils/encrypt.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert' as convert; 3 | // 文件相关 4 | import 'dart:io'; 5 | 6 | class EncryptUtils { 7 | 8 | /* 9 | * Base64加密 10 | */ 11 | static String base64Encode(String data){ 12 | var content = convert.utf8.encode(data); 13 | var digest = convert.base64Encode(content); 14 | return digest; 15 | } 16 | /* 17 | * Base64解密 18 | */ 19 | static String base64Decode(String data){ 20 | List bytes = convert.base64Decode(data); 21 | // 网上找的很多都是String.fromCharCodes,这个中文会乱码 22 | //String txt1 = String.fromCharCodes(bytes); 23 | String result = convert.utf8.decode(bytes); 24 | return result; 25 | } 26 | 27 | /* 28 | * 通过图片路径将图片转换成Base64字符串 29 | */ 30 | static Future image2Base64(String path) async { 31 | File file = new File(path); 32 | List imageBytes = await file.readAsBytes(); 33 | return convert.base64Encode(imageBytes); 34 | } 35 | 36 | /* 37 | * 将图片文件转换成Base64字符串 38 | */ 39 | static Future imageFile2Base64(File file) async { 40 | List imageBytes = await file.readAsBytes(); 41 | return convert.base64Encode(imageBytes); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /lib/model/aliyun_config.dart: -------------------------------------------------------------------------------- 1 | class AliyunConfig { 2 | String accessKeyId; 3 | String accessKeySecret; 4 | String area; 5 | String bucket; 6 | String customUrl; 7 | String options; 8 | String path; 9 | 10 | AliyunConfig( 11 | {this.accessKeyId, 12 | this.accessKeySecret, 13 | this.area, 14 | this.bucket, 15 | this.customUrl, 16 | this.options, 17 | this.path}); 18 | 19 | AliyunConfig.fromJson(Map json) { 20 | accessKeyId = json['accessKeyId']; 21 | accessKeySecret = json['accessKeySecret']; 22 | area = json['area']; 23 | bucket = json['bucket']; 24 | customUrl = json['customUrl']; 25 | options = json['options']; 26 | path = json['path']; 27 | } 28 | 29 | Map toJson() { 30 | final Map data = new Map(); 31 | data['accessKeyId'] = this.accessKeyId; 32 | data['accessKeySecret'] = this.accessKeySecret; 33 | data['area'] = this.area; 34 | data['bucket'] = this.bucket; 35 | data['customUrl'] = this.customUrl; 36 | data['options'] = this.options; 37 | data['path'] = this.path; 38 | return data; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/extended.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | Widget defaultLoadStateChanged(ExtendedImageState state, 6 | {double iconSize = 16}) { 7 | switch (state.extendedImageLoadState) { 8 | case LoadState.loading: 9 | return Center( 10 | child: Center( 11 | child: SizedBox( 12 | width: iconSize, 13 | height: iconSize, 14 | child: CupertinoActivityIndicator(), 15 | ), 16 | ), 17 | ); 18 | break; 19 | case LoadState.failed: 20 | return GestureDetector( 21 | child: Stack( 22 | fit: StackFit.expand, 23 | alignment: AlignmentDirectional.center, 24 | children: [ 25 | Icon( 26 | Icons.error, 27 | size: iconSize, 28 | color: Colors.grey[600], 29 | ) 30 | ], 31 | ), 32 | onTap: () { 33 | state.reLoadImage(); 34 | }, 35 | ); 36 | break; 37 | default: 38 | return null; 39 | } 40 | } 41 | 42 | class MiniActivityIndicator {} 43 | -------------------------------------------------------------------------------- /lib/model/qiniu_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/components/manage_item.dart'; 2 | 3 | class QiniuContent { 4 | FileContentType type; 5 | /// FILE 时为空, DIR时不能为空 6 | String url; 7 | String key; 8 | String hash; 9 | int fsize; 10 | String mimeType; 11 | int putTime; 12 | String md5; 13 | int status; 14 | 15 | QiniuContent( 16 | {this.type, 17 | this.key, 18 | this.hash, 19 | this.fsize, 20 | this.mimeType, 21 | this.putTime, 22 | this.md5, 23 | this.status}); 24 | 25 | QiniuContent.fromJson(Map json) { 26 | key = json['key']; 27 | hash = json['hash']; 28 | fsize = json['fsize']; 29 | mimeType = json['mimeType']; 30 | putTime = json['putTime']; 31 | md5 = json['md5']; 32 | status = json['status']; 33 | } 34 | 35 | Map toJson() { 36 | final Map data = new Map(); 37 | data['key'] = this.key; 38 | data['hash'] = this.hash; 39 | data['fsize'] = this.fsize; 40 | data['mimeType'] = this.mimeType; 41 | data['putTime'] = this.putTime; 42 | data['md5'] = this.md5; 43 | data['status'] = this.status; 44 | return data; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/model/gitee_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/components/manage_item.dart'; 2 | 3 | class GiteeContent { 4 | FileContentType type; 5 | int size; 6 | String name; 7 | String path; 8 | String sha; 9 | String url; 10 | String htmlUrl; 11 | String downloadUrl; 12 | 13 | GiteeContent( 14 | {this.type, 15 | this.size, 16 | this.name, 17 | this.path, 18 | this.sha, 19 | this.url, 20 | this.htmlUrl, 21 | this.downloadUrl}); 22 | 23 | GiteeContent.fromJson(Map json) { 24 | type = json['type'] == 'file' ? FileContentType.FILE : FileContentType.DIR; 25 | size = json['size']; 26 | name = json['name']; 27 | path = json['path']; 28 | sha = json['sha']; 29 | url = json['url']; 30 | htmlUrl = json['html_url']; 31 | downloadUrl = json['download_url']; 32 | } 33 | 34 | Map toJson() { 35 | final Map data = new Map(); 36 | data['type'] = this.type == FileContentType.FILE ? 'file' : 'dir'; 37 | data['size'] = this.size; 38 | data['name'] = this.name; 39 | data['path'] = this.path; 40 | data['sha'] = this.sha; 41 | data['url'] = this.url; 42 | data['html_url'] = this.htmlUrl; 43 | data['download_url'] = this.downloadUrl; 44 | return data; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | 17 | // https://github.com/flutter/flutter/issues/55077#issuecomment-628366487 18 | // fix : Plugin project :url_launcher_web not found. Please update settings.gradle. 19 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 20 | 21 | def plugins = new Properties() 22 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 23 | if (pluginsFile.exists()) { 24 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 25 | } 26 | 27 | plugins.each { name, path -> 28 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 29 | include ":$name" 30 | project(":$name").projectDir = pluginDirectory 31 | } 32 | -------------------------------------------------------------------------------- /lib/views/manage_page/smms_page/smms_repo_page_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_picgo/api/smms_api.dart'; 4 | import 'package:flutter_picgo/model/smms_content.dart'; 5 | 6 | abstract class SMMSRepoPageContract { 7 | void loadSuccess(List data); 8 | 9 | void loadError(String msg); 10 | } 11 | 12 | class SMMSRepoPagePresenter { 13 | SMMSRepoPageContract _view; 14 | 15 | SMMSRepoPagePresenter(this._view); 16 | 17 | doLoadContents() async { 18 | try { 19 | var result = await SMMSApi.getUploadHistory(); 20 | var resultmap = json.decode(result); 21 | if (resultmap["success"]) { 22 | _view.loadSuccess((resultmap['data'] as List) 23 | .map((e) => SMMSContent.fromJson(e)) 24 | .toList()); 25 | } else { 26 | _view.loadError(resultmap['message'] ?? '未知错误'); 27 | } 28 | } catch (e) { 29 | _view.loadError('$e'); 30 | } 31 | } 32 | 33 | Future doDeleteContents(String path) async { 34 | try { 35 | var result = 36 | await SMMSApi.delete(path.replaceFirst('https://sm.ms/delete/', '')); 37 | var resultmap = json.decode(result); 38 | if (resultmap["success"]) { 39 | return true; 40 | } else { 41 | return false; 42 | } 43 | } catch (e) { 44 | return false; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/utils/sql.dart: -------------------------------------------------------------------------------- 1 | import './db_provider.dart'; 2 | import '../model/base.dart'; 3 | 4 | class Sql extends BaseModel { 5 | final String tableName; 6 | 7 | Sql.setTable(this.tableName) : super(DbProvider.db); 8 | 9 | // sdf 10 | Future get() async { 11 | return await this.query(tableName); 12 | } 13 | 14 | Future getBySql(String where, List whereArgs, 15 | {String orderBy, int limit, int offset}) async { 16 | return await this.query(tableName, 17 | where: where, 18 | whereArgs: whereArgs, 19 | orderBy: orderBy, 20 | limit: limit, 21 | offset: offset); 22 | } 23 | 24 | Future rawUpdate(String sql, [List arguments]) async { 25 | return await this.db.rawUpdate('UPDATE $tableName SET $sql', arguments); 26 | } 27 | 28 | Future rawInsert(String valueNames, [List arguments]) async { 29 | return await this 30 | .db 31 | .rawInsert('INSERT INTO $tableName$valueNames', arguments); 32 | } 33 | 34 | Future rawDelete(String wheres, [List arguments]) async { 35 | return await this 36 | .db 37 | .rawDelete('DELETE FROM $tableName WHERE $wheres', arguments); 38 | } 39 | 40 | String getTableName() { 41 | return tableName; 42 | } 43 | 44 | Future deleteAll() async { 45 | return await this.db.delete(tableName); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/views/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'album_page/album_page.dart'; 4 | import 'setting_page/setting_page.dart'; 5 | 6 | class AppPage extends StatefulWidget { 7 | 8 | final int selectedIndex; 9 | 10 | AppPage({this.selectedIndex = 0}); 11 | 12 | @override 13 | _MainTabsPageState createState() => _MainTabsPageState(this.selectedIndex); 14 | 15 | } 16 | 17 | class _MainTabsPageState extends State { 18 | 19 | int _selectedIndex; 20 | 21 | _MainTabsPageState(this._selectedIndex); 22 | 23 | List _pageList = [AlbumPage(), SettingPage()]; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | body: this._pageList[this._selectedIndex], 29 | bottomNavigationBar: BottomNavigationBar( 30 | currentIndex: this._selectedIndex, 31 | onTap: (value) { 32 | setState(() { 33 | this._selectedIndex = value; 34 | }); 35 | }, 36 | type: BottomNavigationBarType.fixed, 37 | items: [ 38 | BottomNavigationBarItem( 39 | icon: Icon(IconData(0xe621, fontFamily: 'iconfont')), 40 | label:'相册' 41 | ), 42 | BottomNavigationBarItem( 43 | icon: Icon(IconData(0xe634, fontFamily: 'iconfont')), 44 | label: '设置' 45 | ), 46 | ], 47 | ), 48 | ); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /lib/model/theme_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_picgo/utils/shared_preferences.dart'; 3 | 4 | class ThemeState with ChangeNotifier { 5 | ThemeMode _mode; 6 | ThemeMode get currentMode => _mode; 7 | 8 | static const Map modeMap = { 9 | ThemeMode.light: '浅色模式', 10 | ThemeMode.dark: '深色模式', 11 | ThemeMode.system: '跟随系统' 12 | }; 13 | 14 | ThemeState() { 15 | _init(); 16 | } 17 | 18 | _init() async { 19 | var sp = await SpUtil.getInstance(); 20 | int localMode = sp.getInt(SharedPreferencesKeys.localThemeState) ?? 2; 21 | changeThemeState(_parseState(localMode)); 22 | } 23 | 24 | changeThemeState(ThemeMode mode) async { 25 | _mode = mode; 26 | notifyListeners(); 27 | // save state 28 | var sp = await SpUtil.getInstance(); 29 | sp.putInt(SharedPreferencesKeys.localThemeState, _parseInt(mode)); 30 | } 31 | 32 | /// num转enum 33 | /// 0:浅色模式 1:深色模式 2:跟随系统 34 | int _parseInt(ThemeMode s) { 35 | if (s == ThemeMode.light) { 36 | return 0; 37 | } else if (s == ThemeMode.dark) { 38 | return 1; 39 | } else { 40 | return 2; 41 | } 42 | } 43 | 44 | /// enmu转num 45 | ThemeMode _parseState(int mode) { 46 | if (mode == 0) { 47 | return ThemeMode.light; 48 | } else if (mode == 1) { 49 | return ThemeMode.dark; 50 | } else { 51 | return ThemeMode.system; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/model/smms_content.dart: -------------------------------------------------------------------------------- 1 | class SMMSContent { 2 | int width; 3 | int height; 4 | String filename; 5 | String storename; 6 | int size; 7 | String path; 8 | String hash; 9 | int createdAt; 10 | String url; 11 | String delete; 12 | String page; 13 | 14 | SMMSContent( 15 | {this.width, 16 | this.height, 17 | this.filename, 18 | this.storename, 19 | this.size, 20 | this.path, 21 | this.hash, 22 | this.createdAt, 23 | this.url, 24 | this.delete, 25 | this.page}); 26 | 27 | SMMSContent.fromJson(Map json) { 28 | width = json['width']; 29 | height = json['height']; 30 | filename = json['filename']; 31 | storename = json['storename']; 32 | size = json['size']; 33 | path = json['path']; 34 | hash = json['hash']; 35 | createdAt = json['created_at']; 36 | url = json['url']; 37 | delete = json['delete']; 38 | page = json['page']; 39 | } 40 | 41 | Map toJson() { 42 | final Map data = new Map(); 43 | data['width'] = this.width; 44 | data['height'] = this.height; 45 | data['filename'] = this.filename; 46 | data['storename'] = this.storename; 47 | data['size'] = this.size; 48 | data['path'] = this.path; 49 | data['hash'] = this.hash; 50 | data['created_at'] = this.createdAt; 51 | data['url'] = this.url; 52 | data['delete'] = this.delete; 53 | data['page'] = this.page; 54 | return data; 55 | } 56 | } -------------------------------------------------------------------------------- /lib/views/album_page/album_page_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/model/uploaded.dart'; 2 | import 'package:flutter_picgo/resources/table_name_keys.dart'; 3 | import 'package:flutter_picgo/utils/image_upload.dart'; 4 | import 'package:flutter_picgo/utils/sql.dart'; 5 | import 'package:flutter_picgo/utils/strategy/upload_strategy_factory.dart'; 6 | 7 | abstract class AlbumPageContract { 8 | void loadUploadedImages(List uploadeds); 9 | void loadError(); 10 | 11 | void deleteSuccess(Uploaded uploaded); 12 | void deleteError(String msg); 13 | } 14 | 15 | class AlbumPagePresenter { 16 | AlbumPageContract _view; 17 | 18 | AlbumPagePresenter(this._view); 19 | 20 | doLoadUploadedImages(int limit, int offest) async { 21 | try { 22 | var sql = Sql.setTable(TABLE_NAME_UPLOADED); 23 | var result = await sql.getBySql(null, null, 24 | limit: limit, offset: offest * limit, orderBy: 'id DESC'); 25 | List uploadeds = result.map((v) { 26 | return Uploaded.fromMap(v); 27 | }).toList(); 28 | _view.loadUploadedImages(uploadeds); 29 | } catch (e) { 30 | _view.loadError(); 31 | } 32 | } 33 | 34 | doDeleteImage(Uploaded uploaded) async { 35 | try { 36 | ImageUploadUtils uploader = ImageUploadUtils( 37 | UploadStrategyFactory.getUploadStrategy(uploaded.type)); 38 | Uploaded up = await uploader.delete(uploaded); 39 | if (up != null) { 40 | _view.deleteSuccess(uploaded); 41 | } else { 42 | _view.deleteError('删除出错,请重试'); 43 | } 44 | } catch (e) { 45 | _view.deleteError('删除出错,请重试 Error >>> $e'); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/niupic_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_picgo/api/niupic_api.dart'; 3 | import 'package:flutter_picgo/model/uploaded.dart'; 4 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 5 | import 'package:flutter_picgo/utils/image_upload.dart'; 6 | import 'dart:io'; 7 | 8 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 9 | 10 | class NiupicImageUpload implements ImageUploadStrategy { 11 | /// 牛图网不支持删除 12 | @override 13 | Future delete(Uploaded uploaded) async { 14 | return uploaded; 15 | } 16 | 17 | @override 18 | Future upload(File file, String renameImage) async { 19 | FormData formData = FormData.fromMap({ 20 | "image_field": 21 | await MultipartFile.fromFile(file.path, filename: renameImage), 22 | }); 23 | var result = await NiupicApi.upload(formData); 24 | if (result["code"] == 200) { 25 | var uploadedItem = Uploaded( 26 | -1, 'https://${result['data']}', PBTypeKeys.niupic, 27 | info: ''); 28 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 29 | return uploadedItem; 30 | } else { 31 | throw new NiupicError(error: '${result['msg']}'); 32 | } 33 | } 34 | } 35 | 36 | class NiupicError implements Exception { 37 | NiupicError({ 38 | this.error, 39 | }); 40 | 41 | dynamic error; 42 | 43 | String get message => (error?.toString() ?? ''); 44 | 45 | @override 46 | String toString() { 47 | var msg = 'NiupicError $message'; 48 | if (error is Error) { 49 | msg += '\n${error.stackTrace}'; 50 | } 51 | return msg; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/api/lsky_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_picgo/utils/net.dart'; 3 | import 'package:path/path.dart' as path; 4 | 5 | class LskyApi { 6 | static Future token(String email, String pwd, String host) async { 7 | String url = path.joinAll([host, 'api/token']); 8 | Response res = await NetUtils.getInstance().post(url, data: { 9 | 'email': email, 10 | 'password': pwd, 11 | }); 12 | return res.data; 13 | } 14 | 15 | static Future upload(String token, String host, FormData data) async { 16 | String url = path.joinAll([host, 'api/upload']); 17 | Response res = await NetUtils.getInstance() 18 | .post(url, data: data, options: buildCommonOptions(token)); 19 | return res.data; 20 | } 21 | 22 | static Future images(String token, String host, int page, 23 | {int rows = 10}) async { 24 | String url = path.joinAll([host, 'api/images']); 25 | Response res = await NetUtils.getInstance().post( 26 | url, 27 | data: { 28 | 'page': page, 29 | 'rows': rows, 30 | }, 31 | options: buildCommonOptions(token), 32 | ); 33 | return res.data; 34 | } 35 | 36 | static Future delete(String token, String host, String id) async { 37 | String url = path.joinAll([host, 'api/delete']); 38 | Response res = await NetUtils.getInstance().post(url, 39 | data: { 40 | 'id': id, 41 | }, 42 | options: buildCommonOptions(token)); 43 | return res.data; 44 | } 45 | 46 | static Options buildCommonOptions(String token) { 47 | return Options(headers: { 48 | 'token': token, 49 | }, contentType: Headers.formUrlEncodedContentType); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/model/github_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/components/manage_item.dart'; 2 | 3 | class GithubContent { 4 | FileContentType type; 5 | String encoding; 6 | int size; 7 | String name; 8 | String path; 9 | String content; 10 | String sha; 11 | String url; 12 | String gitUrl; 13 | String htmlUrl; 14 | String downloadUrl; 15 | 16 | GithubContent({ 17 | this.type, 18 | this.encoding, 19 | this.size, 20 | this.name, 21 | this.path, 22 | this.content, 23 | this.sha, 24 | this.url, 25 | this.gitUrl, 26 | this.htmlUrl, 27 | this.downloadUrl, 28 | }); 29 | 30 | GithubContent.fromJson(Map json) { 31 | type = json['type'] == 'file' ? FileContentType.FILE : FileContentType.DIR; 32 | encoding = json['encoding']; 33 | size = json['size']; 34 | name = json['name']; 35 | path = json['path']; 36 | content = json['content']; 37 | sha = json['sha']; 38 | url = json['url']; 39 | gitUrl = json['git_url']; 40 | htmlUrl = json['html_url']; 41 | downloadUrl = json['download_url']; 42 | } 43 | 44 | Map toJson() { 45 | final Map data = new Map(); 46 | data['type'] = this.type == FileContentType.FILE ? 'file' : 'dir'; 47 | data['encoding'] = this.encoding; 48 | data['size'] = this.size; 49 | data['name'] = this.name; 50 | data['path'] = this.path; 51 | data['content'] = this.content; 52 | data['sha'] = this.sha; 53 | data['url'] = this.url; 54 | data['git_url'] = this.gitUrl; 55 | data['html_url'] = this.htmlUrl; 56 | data['download_url'] = this.downloadUrl; 57 | return data; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | # target.build_configurations.each do |config| 41 | # if config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'].to_f < 8.0 42 | # config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '8.0' 43 | # end 44 | # end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/api/tcyun_api_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/api/tcyun_api.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | main() { 5 | test('测试生成 KeyTime', () {}); 6 | 7 | test('测试生成 SignKey', () {}); 8 | 9 | test('测试Api 签名', () async { 10 | try { 11 | await TcyunApi.deleteobject( 12 | 'AKID70e28x4gazd17vKiywkITO1NSWHv6s75', 13 | 'pN0MRGo5HCqDxTqpGPTFlCsHPKzUjhZD', 14 | 'test-1253954259', 15 | 'ap-nanjing', 16 | 'wallhaven-2e9j79.jpg'); 17 | } catch (e) {} 18 | }); 19 | 20 | test('PostObjecy 提交', () async { 21 | try { 22 | // String pathname = path.joinAll( 23 | // [Directory.current.path, '..\\assets\\' 'images', 'logo.png']); 24 | // String keyTime = TcyunApi.buildKeyTime(); 25 | // String policy = TcyunApi.buildPolicy('ap-nanjing', 'logo.png', 26 | // 'AKIDvb0B9rqfeOr44kt2ar46rO2cwzl6JwUk', keyTime); 27 | // TcyunApi.postObject( 28 | // 'test-1253954259', 29 | // 'ap-nanjing', 30 | // 'png', 31 | // FormData.fromMap({ 32 | // "key": "logo.png", 33 | // "file": await MultipartFile.fromFile( 34 | // 'C:\\Users\\Administrator\\Desktop\\flutter-picgo\\flutter-picgo\\assets\\images\\logo.png', 35 | // filename: 'logo.png'), 36 | // "policy": base64.encode(utf8.encode(policy)), 37 | // "q-sign-algorithm": "sha1", 38 | // "q-ak": "AKIDvb0B9rqfeOr44kt2ar46rO2cwzl6JwUk", 39 | // "q-key-time": keyTime, 40 | // "q-signature": TcyunApi.buildSignature( 41 | // 'KOEoR1LL5apX1lFRN4VgZB0nJgmdEbie', keyTime, policy) 42 | // })); 43 | } catch (e) {} 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /lib/resources/theme_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // Light Theme Color 4 | 5 | const MaterialColor light = MaterialColor(lightPrimaryValue, { 6 | 50: Color(0xFFE9F2FD), 7 | 100: Color(0xFFC7E0F9), 8 | 200: Color(0xFFA2CBF5), 9 | 300: Color(0xFF7DB6F1), 10 | 400: Color(0xFF61A6EE), 11 | 500: Color(lightPrimaryValue), 12 | 600: Color(0xFF3E8EE9), 13 | 700: Color(0xFF3683E5), 14 | 800: Color(0xFF2E79E2), 15 | 900: Color(0xFF1F68DD), 16 | }); 17 | 18 | const int lightPrimaryValue = 0xFF4596EB; 19 | 20 | // Dark 21 | 22 | const MaterialColor dark = MaterialColor(_darkPrimaryValue, { 23 | 50: Color(0xFFE2E3E3), 24 | 100: Color(0xFFB8B8B8), 25 | 200: Color(0xFF888989), 26 | 300: Color(0xFF58595A), 27 | 400: Color(0xFF353636), 28 | 500: Color(_darkPrimaryValue), 29 | 600: Color(0xFF0F1011), 30 | 700: Color(0xFF0C0D0E), 31 | 800: Color(0xFF0A0A0B), 32 | 900: Color(0xFF050506), 33 | }); 34 | const int _darkPrimaryValue = 0xFF111213; 35 | 36 | // accent 37 | const MaterialColor accent = MaterialColor(_accentValue, { 38 | 50: Color(0xFFFCE9EC), 39 | 100: Color(0xFFF9C9D0), 40 | 200: Color(0xFFF5A5B1), 41 | 300: Color(0xFFF08192), 42 | 400: Color(0xFFED667A), 43 | 500: Color(_accentValue), 44 | 600: Color(0xFFE7445B), 45 | 700: Color(0xFFE43B51), 46 | 800: Color(0xFFE13347), 47 | 900: Color(0xFFDB2335), 48 | }); 49 | 50 | const int _accentValue = 0xFFEA4B63; 51 | 52 | // 主题 53 | 54 | final ThemeData lightThemeData = ThemeData( 55 | brightness: Brightness.light, 56 | primarySwatch: light, 57 | primaryColor: light, 58 | accentColor: light, 59 | ); 60 | 61 | final ThemeData darkThemeData = ThemeData( 62 | brightness: Brightness.dark, 63 | primarySwatch: dark, 64 | primaryColor: dark, 65 | accentColor: accent, 66 | ); 67 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/smms_page/smms_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:fluro/fluro.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:flutter_picgo/model/config.dart'; 7 | import 'package:flutter_picgo/model/smms_config.dart'; 8 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 9 | import 'package:flutter_picgo/routers/application.dart'; 10 | import 'package:flutter_picgo/routers/routers.dart'; 11 | import 'package:flutter_picgo/utils/strings.dart'; 12 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 13 | 14 | class SMMSPage extends StatefulWidget { 15 | _SMMSPageState createState() => _SMMSPageState(); 16 | } 17 | 18 | class _SMMSPageState extends BasePBSettingPageState { 19 | 20 | @override 21 | onLoadConfig(String config) { 22 | List configs = []; 23 | Map map; 24 | if (isBlank(config)) { 25 | map = SMMSConfig().toJson(); 26 | } else { 27 | map = SMMSConfig.fromJson(json.decode(config)).toJson(); 28 | } 29 | map.forEach((key, value) { 30 | Config config; 31 | if (key == 'token') { 32 | config = Config( 33 | label: '设定Token', 34 | placeholder: 'Token', 35 | needValidate: true, 36 | value: value); 37 | } 38 | config.name = key; 39 | configs.add(config); 40 | }); 41 | setConfigs(configs); 42 | } 43 | 44 | @override 45 | String get pbType => PBTypeKeys.smms; 46 | 47 | @override 48 | String get title => 'SM.MS图床'; 49 | 50 | @override 51 | bool get isSupportManage => true; 52 | 53 | @override 54 | handleManage() { 55 | Application.router.navigateTo(context, 56 | '${Routes.settingPbSMMSRepo}', 57 | transition: TransitionType.cupertino); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /docs/design/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "uploaded": [], 3 | "picBed": { 4 | "current": "github", 5 | "list": [ 6 | { 7 | "name": "SM.MS图床", 8 | "type": "smms", 9 | "visible": false 10 | }, 11 | { 12 | "name": "腾讯云COS", 13 | "type": "tcyun", 14 | "visible": false 15 | }, 16 | { 17 | "name": "微博图床", 18 | "type": "weibo", 19 | "visible": false 20 | }, 21 | { 22 | "name": "GitHub图床", 23 | "type": "github", 24 | "visible": true 25 | }, 26 | { 27 | "name": "七牛图床", 28 | "type": "qiniu", 29 | "visible": true 30 | }, 31 | { 32 | "name": "Imgur图床", 33 | "type": "imgur", 34 | "visible": false 35 | }, 36 | { 37 | "name": "阿里云OSS", 38 | "type": "aliyun", 39 | "visible": false 40 | }, 41 | { 42 | "name": "又拍云图床", 43 | "type": "upyun", 44 | "visible": false 45 | } 46 | ], 47 | "github": { 48 | "branch": "master", 49 | "customUrl": "https://github.static.si-yee.com", 50 | "path": "posts/", 51 | "repo": "hackycy/picture-bed", 52 | "token": "7e84c31bfe3845dc0ababd0fdd1ec4d070cd5212" 53 | } 54 | }, 55 | "settings": { 56 | "shortKey": { 57 | "picgo:upload": { 58 | "enable": true, 59 | "key": "CommandOrControl+Shift+P", 60 | "name": "upload", 61 | "label": "快捷上传" 62 | } 63 | }, 64 | "server": { 65 | "enable": true, 66 | "host": "127.0.0.1", 67 | "port": 36677 68 | }, 69 | "showUpdateTip": false, 70 | "autoRename": true, 71 | "rename": true, 72 | "pasteStyle": "markdown", 73 | "uploadNotification": true 74 | }, 75 | "picgoPlugins": {}, 76 | "debug": true, 77 | "PICGO_ENV": "GUI", 78 | "needReload": false 79 | } 80 | -------------------------------------------------------------------------------- /docs/设计文档.md: -------------------------------------------------------------------------------- 1 | # sqlite设计文档 2 | 3 | ## 图床配置表pb_setting 4 | 5 | | 字段 | 类型 | 空 | 默认 | 注释 | 版本 | 6 | | ------- | ------------ | ---- | ---- | --------------------------------- | :-------------------------------: | 7 | | id | int | 否 | | 自增ID | 1+ | 8 | | type | VARCHAR(20) | 否 | | 图床类型 | 1+ | 9 | | name | VARCHAR(50) | 否 | | 图床名称 | 1+ | 10 | | config | VARCHAR(255) | 是 | | 图床配置信息,json格式 | 1+ | 11 | | path | VARCHAR(20) | 否 | | 路由跳转页面路径 | 1+ | 12 | | visible | tinyint(1) | 是 | 1 | 是否在配置中可见,默认为1,即true | 1+ | 13 | 14 | ## 上传图片列表uploaded 15 | 16 | | 字段 | 类型 | 空 | 默认 | 注释 | 版本 | 17 | | ---- | ------------ | ---- | ---- | ---------------------------- | :--------------------------: | 18 | | id | int | 否 | | 自增ID | 1+ | 19 | | path | VARCHAR(255) | 否 | | 图片上传成功后对应的相对路径 | 1+ | 20 | | type | VARCHAR(20) | 否 | | 图床类型 | 1+ | 21 | | info | VARCHAR(255) | 是 | | 图片信息,json格式 | 2+ | 22 | 23 | ## Sqlite升级记录 24 | 25 | | 版本 | 描述 | 26 | | ---- | ------------------------------------------------ | 27 | | 1 | pb_setting,uploaded表初始化,增加Github图床记录 | 28 | | 2 | uploaded新增info列,新增SM.MS图床记录 | 29 | | 3 | 新增Gitee图床记录 | 30 | | 4 | 新增Qiniu图床记录 | 31 | | 5 | 新增阿里云OSS图床记录 | 32 | | 6 | 新增腾讯云COS图床记录 | 33 | | 7 | 新增牛图网图床记录 | 34 | | 8 | 新增兰空图床记录 | 35 | | 9 | 新增又拍云图床记录 | 36 | 37 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-20@2x-ipad.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon-20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon-29-ipad.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon-29@2x-ipad.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon-29@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "icon-40@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon-40@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "icon-57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "icon-57@2x.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "icon-60@2x.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "icon-60@3x.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "icon-1024.png", 71 | "idiom" : "ios-marketing", 72 | "scale" : "1x", 73 | "size" : "1024x1024" 74 | } 75 | ], 76 | "info" : { 77 | "author" : "xcode", 78 | "version" : 1 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/model/lsky_content.dart: -------------------------------------------------------------------------------- 1 | class LskyContent { 2 | int id; 3 | String strategy; 4 | String path; 5 | String name; 6 | Null aliasName; 7 | String pathname; 8 | String size; 9 | String mime; 10 | String sha1; 11 | String md5; 12 | String ip; 13 | int suspicious; 14 | int uploadTime; 15 | String uploadDate; 16 | String url; 17 | 18 | LskyContent( 19 | {this.id, 20 | this.strategy, 21 | this.path, 22 | this.name, 23 | this.aliasName, 24 | this.pathname, 25 | this.size, 26 | this.mime, 27 | this.sha1, 28 | this.md5, 29 | this.ip, 30 | this.suspicious, 31 | this.uploadTime, 32 | this.uploadDate, 33 | this.url}); 34 | 35 | LskyContent.fromJson(Map json) { 36 | id = json['id']; 37 | strategy = json['strategy']; 38 | path = json['path']; 39 | name = json['name']; 40 | aliasName = json['alias_name']; 41 | pathname = json['pathname']; 42 | size = json['size']; 43 | mime = json['mime']; 44 | sha1 = json['sha1']; 45 | md5 = json['md5']; 46 | ip = json['ip']; 47 | suspicious = json['suspicious']; 48 | uploadTime = json['upload_time']; 49 | uploadDate = json['upload_date']; 50 | url = json['url']; 51 | } 52 | 53 | Map toJson() { 54 | final Map data = new Map(); 55 | data['id'] = this.id; 56 | data['strategy'] = this.strategy; 57 | data['path'] = this.path; 58 | data['name'] = this.name; 59 | data['alias_name'] = this.aliasName; 60 | data['pathname'] = this.pathname; 61 | data['size'] = this.size; 62 | data['mime'] = this.mime; 63 | data['sha1'] = this.sha1; 64 | data['md5'] = this.md5; 65 | data['ip'] = this.ip; 66 | data['suspicious'] = this.suspicious; 67 | data['upload_time'] = this.uploadTime; 68 | data['upload_date'] = this.uploadDate; 69 | data['url'] = this.url; 70 | return data; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/api/github_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:flutter_picgo/model/github_config.dart'; 4 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 5 | import 'package:flutter_picgo/utils/image_upload.dart'; 6 | import 'package:flutter_picgo/utils/net.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | 9 | class GithubApi { 10 | static const String BASE_URL = 'https://api.github.com/'; 11 | 12 | static const String CONTENTS = 13 | 'repos/:owner/:repo/contents/:path'; //PUT DELETE GET 14 | 15 | static Future testToken() async { 16 | Response res = await NetUtils.getInstance().get(BASE_URL); 17 | return res.data; 18 | } 19 | 20 | static Future putContent(String url, data) async { 21 | var op = await oAuth(); 22 | Response res = await NetUtils.getInstance() 23 | .put(BASE_URL + url ?? '', data: data, options: op); 24 | return res.data; 25 | } 26 | 27 | static Future deleteContent(String url, data) async { 28 | var op = await oAuth(); 29 | Response res = await NetUtils.getInstance() 30 | .delete(BASE_URL + url, data: data, options: op); 31 | return res.data; 32 | } 33 | 34 | static Future getContents(String url, Map params) async { 35 | var op = await oAuth(); 36 | Response res = await NetUtils.getInstance() 37 | .get(BASE_URL + url, queryParameters: params, options: op); 38 | return res.data; 39 | } 40 | 41 | /// 获取配置中的Token 42 | static Future oAuth() async { 43 | try { 44 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.github); 45 | if (!isBlank(configStr)) { 46 | GithubConfig config = GithubConfig.fromJson(json.decode(configStr)); 47 | if (config != null && !isBlank(config.token)) { 48 | return Options(headers: {"Authorization": 'Token ${config.token}'}); 49 | } 50 | } 51 | return null; 52 | } catch (e) { 53 | return null; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/lsky_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_picgo/api/lsky_api.dart'; 5 | import 'package:flutter_picgo/model/lsky_config.dart'; 6 | import 'package:flutter_picgo/model/uploaded.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/utils/image_upload.dart'; 9 | import 'dart:io'; 10 | 11 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 12 | import 'package:flutter_picgo/utils/strings.dart'; 13 | 14 | class LskyImageUpload implements ImageUploadStrategy { 15 | @override 16 | Future delete(Uploaded uploaded) async { 17 | return uploaded; 18 | } 19 | 20 | @override 21 | Future upload(File file, String renameImage) async { 22 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.lsky); 23 | if (isBlank(configStr)) { 24 | throw LskyError(error: '读取配置文件错误!请重试'); 25 | } 26 | LskyConfig config = LskyConfig.fromJson(json.decode(configStr)); 27 | FormData formData = FormData.fromMap({ 28 | "image": await MultipartFile.fromFile(file.path, filename: renameImage), 29 | }); 30 | var result = await LskyApi.upload(config.token, config.host, formData); 31 | if (result['code'] == 200) { 32 | var uploadedItem = 33 | Uploaded(-1, '${result['data']['url']}', PBTypeKeys.lsky, info: ''); 34 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 35 | return uploadedItem; 36 | } else { 37 | throw new LskyError(error: '${result['msg']}'); 38 | } 39 | } 40 | } 41 | 42 | class LskyError implements Exception { 43 | LskyError({ 44 | this.error, 45 | }); 46 | 47 | dynamic error; 48 | 49 | String get message => (error?.toString() ?? ''); 50 | 51 | @override 52 | String toString() { 53 | var msg = 'LskyError $message'; 54 | if (error is Error) { 55 | msg += '\n${error.stackTrace}'; 56 | } 57 | return msg; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/smms_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:flutter_picgo/api/smms_api.dart'; 4 | import 'package:flutter_picgo/model/uploaded.dart'; 5 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 6 | import 'package:flutter_picgo/utils/image_upload.dart'; 7 | import 'dart:io'; 8 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 9 | import 'package:flutter_picgo/utils/strings.dart'; 10 | 11 | class SMMSImageUpload implements ImageUploadStrategy { 12 | @override 13 | Future upload(File file, String renameImage) async { 14 | FormData formData = FormData.fromMap({ 15 | "smfile": await MultipartFile.fromFile(file.path, filename: renameImage), 16 | }); 17 | var result = await SMMSApi.upload(formData); 18 | var resultmap = json.decode(result); 19 | if (resultmap["success"]) { 20 | var uploaded = Uploaded(-1, resultmap["data"]["url"], PBTypeKeys.smms, 21 | info: resultmap["data"]["hash"]); 22 | await ImageUploadUtils.saveUploadedItem(uploaded); 23 | return uploaded; 24 | } else { 25 | throw new SMMSError(error: resultmap['message']); 26 | } 27 | } 28 | 29 | @override 30 | Future delete(Uploaded uploaded) async { 31 | if (!isBlank(uploaded.info)) { 32 | var result = await SMMSApi.delete(uploaded.info); 33 | var resultmap = json.decode(result); 34 | if (resultmap["success"]) { 35 | return uploaded; 36 | } else { 37 | throw new SMMSError(error: resultmap['message']); 38 | } 39 | } 40 | return uploaded; 41 | } 42 | } 43 | 44 | /// SMMSError describes the error info when request failed. 45 | class SMMSError implements Exception { 46 | SMMSError({ 47 | this.error, 48 | }); 49 | 50 | dynamic error; 51 | 52 | String get message => (error?.toString() ?? ''); 53 | 54 | @override 55 | String toString() { 56 | var msg = 'SMMSError $message'; 57 | if (error is Error) { 58 | msg += '\n${error.stackTrace}'; 59 | } 60 | return msg; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/views/picgo_setting_page/theme_setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_picgo/model/theme_state.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class ThemeSettingPage extends StatefulWidget { 6 | _ThemeSettingPageState createState() => _ThemeSettingPageState(); 7 | } 8 | 9 | class _ThemeSettingPageState extends State { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | appBar: AppBar( 14 | centerTitle: true, 15 | title: const Text('主题设置'), 16 | ), 17 | body: Consumer( 18 | builder: (context, themeState, child) { 19 | return ListView( 20 | children: [ 21 | ListTile( 22 | title: Text(ThemeState.modeMap[ThemeMode.system]), 23 | trailing: themeState.currentMode == ThemeMode.system 24 | ? Icon(Icons.check) 25 | : null, 26 | onTap: () { 27 | _changeThemeMode(themeState, ThemeMode.system); 28 | }, 29 | ), 30 | ListTile( 31 | title: Text(ThemeState.modeMap[ThemeMode.light]), 32 | trailing: themeState.currentMode == ThemeMode.light 33 | ? Icon(Icons.check) 34 | : null, 35 | onTap: () { 36 | _changeThemeMode(themeState, ThemeMode.light); 37 | }, 38 | ), 39 | ListTile( 40 | title: Text(ThemeState.modeMap[ThemeMode.dark]), 41 | trailing: themeState.currentMode == ThemeMode.dark 42 | ? Icon(Icons.check) 43 | : null, 44 | onTap: () { 45 | _changeThemeMode(themeState, ThemeMode.dark); 46 | }, 47 | ) 48 | ], 49 | ); 50 | }, 51 | ), 52 | ); 53 | } 54 | 55 | _changeThemeMode(ThemeState state, ThemeMode mode) { 56 | state.changeThemeState(mode); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/api/smms_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_picgo/model/smms_config.dart'; 5 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 6 | import 'package:flutter_picgo/utils/image_upload.dart'; 7 | import 'package:flutter_picgo/utils/net.dart'; 8 | import 'package:flutter_picgo/utils/strings.dart'; 9 | 10 | class SMMSApi { 11 | static const String BASE_URL = 'https://sm.ms/api/v2/'; 12 | 13 | static Future getProfile() async { 14 | var op = await oAuth(); 15 | Response res = 16 | await NetUtils.getInstance().post(BASE_URL + 'profile', options: op); 17 | return res.data; 18 | } 19 | 20 | static Future upload(FormData formData) async { 21 | var op = await oAuth(); 22 | Response res = await NetUtils.getInstance() 23 | .post(BASE_URL + 'upload', data: formData, options: op); 24 | return res.data; 25 | } 26 | 27 | static Future delete(String hash) async { 28 | var op = await oAuth(); 29 | Response res = await NetUtils.getInstance() 30 | .get(BASE_URL + 'delete/' + hash ?? '', options: op); 31 | return res.data; 32 | } 33 | 34 | static Future deleteByPath(String path) async { 35 | var op = await oAuth(); 36 | Response res = await NetUtils.getInstance().get(path, options: op); 37 | return res.data; 38 | } 39 | 40 | static Future getUploadHistory() async { 41 | var op = await oAuth(); 42 | Response res = await NetUtils.getInstance() 43 | .get(BASE_URL + 'upload_history', options: op); 44 | return res.data; 45 | } 46 | 47 | /// 获取配置中的Token 48 | static Future oAuth() async { 49 | try { 50 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.smms); 51 | if (!isBlank(configStr)) { 52 | SMMSConfig config = SMMSConfig.fromJson(json.decode(configStr)); 53 | if (config != null && !isBlank(config.token)) { 54 | return Options(headers: {"Authorization": '${config.token}'}); 55 | } 56 | } 57 | return null; 58 | } catch (e) { 59 | return null; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/views/manage_page/lsky_page/lsky_repo_page_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_picgo/api/lsky_api.dart'; 4 | import 'package:flutter_picgo/model/lsky_config.dart'; 5 | import 'package:flutter_picgo/model/lsky_content.dart'; 6 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 7 | import 'package:flutter_picgo/utils/image_upload.dart'; 8 | import 'package:flutter_picgo/utils/strings.dart'; 9 | 10 | abstract class LskyRepoPageContract { 11 | void loadSuccess(List data, bool isEnd); 12 | 13 | void loadError(String msg); 14 | } 15 | 16 | class LskyRepoPagePresenter { 17 | LskyRepoPageContract _view; 18 | 19 | LskyRepoPagePresenter(this._view); 20 | 21 | doLoadContents(int page) async { 22 | try { 23 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.lsky); 24 | if (isBlank(configStr)) { 25 | _view.loadError('读取配置错误!'); 26 | return; 27 | } 28 | LskyConfig config = LskyConfig.fromJson(json.decode(configStr)); 29 | var result = await LskyApi.images(config.token, config.host, page); 30 | if (result['code'] == 200) { 31 | bool isEnd = 32 | result['data']['current_page'] == result['data']['last_page']; 33 | _view.loadSuccess( 34 | (result['data']['data'] as List) 35 | .map((e) => LskyContent.fromJson(e)) 36 | .toList(), 37 | isEnd); 38 | } else { 39 | _view.loadError(result['msg']); 40 | } 41 | } catch (e) { 42 | _view.loadError('$e'); 43 | } 44 | } 45 | 46 | Future doDeleteContents(String id) async { 47 | try { 48 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.lsky); 49 | if (isBlank(configStr)) { 50 | return false; 51 | } 52 | LskyConfig config = LskyConfig.fromJson(json.decode(configStr)); 53 | var result = await LskyApi.delete(config.token, config.host, id); 54 | if (result['code'] == 200) { 55 | return true; 56 | } 57 | return false; 58 | } catch (e) { 59 | return false; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/pb_setting_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_picgo/model/pb_setting.dart'; 4 | import 'package:flutter_picgo/resources/table_name_keys.dart'; 5 | import 'package:flutter_picgo/utils/image_upload.dart'; 6 | import 'package:flutter_picgo/utils/sql.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | 9 | abstract class PBSettingPageContract { 10 | void loadPb(List settings); 11 | 12 | void loadError(String errorMsg); 13 | 14 | void transferError(String e); 15 | 16 | void transferSuccess(); 17 | 18 | void exportConfigSuccess(String config); 19 | 20 | void exportConfigError(String message); 21 | } 22 | 23 | class PBSettingPagePresenter { 24 | PBSettingPageContract _view; 25 | 26 | PBSettingPagePresenter(this._view); 27 | 28 | doLoadPb() async { 29 | try { 30 | var sql = Sql.setTable('pb_setting'); 31 | var list = await sql.get(); 32 | var realList = list.map((map) { 33 | return PBSetting.fromMap(map); 34 | }).toList(); 35 | _view.loadPb(realList); 36 | } catch (e) { 37 | _view.loadError('${e.toString()}'); 38 | } 39 | } 40 | 41 | /// transfer picgo json config to flutter-picgo 42 | doTransferJson(String jsonStr) async { 43 | try { 44 | Map map = json.decode(jsonStr); 45 | map.forEach((key, value) async { 46 | var config = json.encode(value); 47 | await ImageUploadUtils.savePBConfig(key, config); 48 | }); 49 | // success 50 | _view.transferSuccess(); 51 | } catch (e) { 52 | _view.transferError('转换失败,请确认配置无误'); 53 | } 54 | } 55 | 56 | /// 导出所有配置 57 | doExportConfig() async { 58 | try { 59 | var sql = Sql.setTable(TABLE_NAME_PBSETTING); 60 | var list = await sql.get(); 61 | Map map = {}; 62 | list.forEach((element) { 63 | var pbItem = PBSetting.fromMap(element); 64 | if (!isBlank(pbItem.config)) { 65 | map[pbItem.type] = json.decode(pbItem.config); 66 | } 67 | }); 68 | _view.exportConfigSuccess(json.encode(map)); 69 | } catch (e) { 70 | _view.exportConfigError('$e'); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/api/gitee_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_picgo/model/gitee_config.dart'; 5 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 6 | import 'package:flutter_picgo/utils/image_upload.dart'; 7 | import 'package:flutter_picgo/utils/net.dart'; 8 | import 'package:flutter_picgo/utils/strings.dart'; 9 | 10 | class GiteeApi { 11 | static const String BASE_URL = 'https://gitee.com/api/v5/'; 12 | 13 | static const String KEY_ACCESS_TOKEN = 'access_token'; 14 | 15 | static Future testToken() async { 16 | Response res = 17 | await NetUtils.getInstance().get(BASE_URL + 'user', queryParameters: { 18 | KEY_ACCESS_TOKEN: await oAuth(), 19 | }); 20 | return res.data; 21 | } 22 | 23 | static Future createFile(String url, Map map) async { 24 | Map realMap = { 25 | KEY_ACCESS_TOKEN: await oAuth(), 26 | }; 27 | realMap.addAll(map); 28 | var data = FormData.fromMap(realMap); 29 | Response res = 30 | await NetUtils.getInstance().post(BASE_URL + url, data: data); 31 | map.clear(); 32 | map = null; 33 | return res.data; 34 | } 35 | 36 | static Future getContents(String url, Map params) async { 37 | Response res = await NetUtils.getInstance() 38 | .get(BASE_URL + url, queryParameters: params); 39 | return res.data; 40 | } 41 | 42 | static Future deleteFile(String url, Map query) async { 43 | Map realQuery = { 44 | KEY_ACCESS_TOKEN: await oAuth(), 45 | }; 46 | realQuery.addAll(query); 47 | Response res = await NetUtils.getInstance() 48 | .delete(BASE_URL + url, queryParameters: realQuery); 49 | return res.data; 50 | } 51 | 52 | /// 获取配置中的Token 53 | static Future oAuth() async { 54 | try { 55 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.gitee); 56 | if (!isBlank(configStr)) { 57 | GiteeConfig config = GiteeConfig.fromJson(json.decode(configStr)); 58 | if (config != null && !isBlank(config.token)) { 59 | return config.token; 60 | } 61 | } 62 | return null; 63 | } catch (e) { 64 | return null; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/upyun_page/upyun_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_picgo/model/config.dart'; 5 | import 'package:flutter_picgo/model/upyun_config.dart'; 6 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 9 | 10 | class UpyunPage extends StatefulWidget { 11 | _UpyunPageState createState() => _UpyunPageState(); 12 | } 13 | 14 | class _UpyunPageState extends BasePBSettingPageState { 15 | @override 16 | onLoadConfig(String config) { 17 | List configs = []; 18 | Map map; 19 | if (isBlank(config)) { 20 | map = UpyunConfig().toJson(); 21 | } else { 22 | map = UpyunConfig.fromJson(json.decode(config)).toJson(); 23 | } 24 | map.forEach((key, value) { 25 | Config config; 26 | if (key == 'bucket') { 27 | config = Config( 28 | label: '设定存储空间名', 29 | placeholder: 'Bucket', 30 | needValidate: true, 31 | value: value); 32 | } else if (key == 'operator') { 33 | config = Config( 34 | label: '设定操作员', 35 | placeholder: '例如:me', 36 | needValidate: true, 37 | value: value); 38 | } else if (key == 'password') { 39 | config = Config( 40 | label: '设定操作员密码', 41 | placeholder: '输入密码', 42 | needValidate: true, 43 | value: value); 44 | } else if (key == 'url') { 45 | config = Config( 46 | label: '设定加速域名', 47 | placeholder: '例如https://xxx.yyy.com', 48 | needValidate: true, 49 | value: value); 50 | } else if (key == 'path') { 51 | config = Config(label: '指定存储路径', placeholder: '例如img/', value: value); 52 | } else if (key == 'options') { 53 | config = 54 | Config(label: '设定网址后缀', placeholder: '例如:!imgslim', value: value); 55 | } 56 | config.name = key; 57 | configs.add(config); 58 | }); 59 | setConfigs(configs); 60 | } 61 | 62 | @override 63 | String get pbType => PBTypeKeys.upyun; 64 | 65 | @override 66 | String get title => '又拍云图床'; 67 | } 68 | -------------------------------------------------------------------------------- /test/api/aliyun_api_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:crypto/crypto.dart'; 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter_picgo/api/aliyun_api.dart'; 6 | import 'package:flutter_picgo/utils/net.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | import 'package:path/path.dart' as path; 9 | 10 | main() { 11 | test('测试Content-MD5', () { 12 | var digest = AliyunApi.generateContentMD5('0123456789'); 13 | expect(digest, 'eB5eJF1ptWaXm4bijSPyxw=='); 14 | }); 15 | 16 | test('测试Auth Header签名', () async { 17 | var sign = AliyunApi.buildSignature('LTAIsXml0iczvY0J', 18 | 'yw8eO9Fa9Py2GAPRGG8N3GPKCeKCXl', 'PUT', 'zjyzy', 'test.txt'); 19 | try { 20 | await NetUtils.getInstance().put( 21 | 'https://zjyzy.oss-cn-shenzhen.aliyuncs.com/test.txt', 22 | options: Options(headers: { 23 | 'Authorization': sign, 24 | 'Date': HttpDate.format(new DateTime.now()), 25 | }, contentType: 'application/x-www-form-urlencoded'), 26 | ); 27 | } on DioError catch (_) {} 28 | }); 29 | 30 | test('测试FormData提交图片', () async { 31 | String pathname = path 32 | .joinAll([Directory.current.path, '..', 'assets/images', 'logo.png']); 33 | var policyText = { 34 | "expiration": 35 | "2030-01-01T12:00:00.000Z", // 设置Policy的失效时间,如果超过失效时间,就无法通过此Policy上传文件 36 | "conditions": [ 37 | {"key": 'logo.png'} // 设置上传文件的大小限制,如果超过限制,文件上传到OSS会报错 38 | ] 39 | }; 40 | var originSign = base64.encode(utf8.encode(json.encode(policyText))); 41 | 42 | var hmacsha1 = Hmac(sha1, utf8.encode('yw8eO9Fa9Py2GAPRGG8N3GPKCeKCXl')); 43 | var sign = hmacsha1.convert(utf8.encode(originSign)); 44 | var encodeSign = base64.encode(sign.bytes); 45 | try { 46 | await NetUtils.getInstance().post( 47 | 'https://zjyzy.oss-cn-shenzhen.aliyuncs.com', 48 | data: FormData.fromMap({ 49 | 'key': 'logo.png', 50 | 'OSSAccessKeyId': 'LTAIsXml0iczvY0J', 51 | 'policy': originSign, 52 | 'Signature': encodeSign, 53 | 'file': await MultipartFile.fromFile(pathname, filename: 'logo.png') 54 | }), 55 | options: Options(contentType: Headers.formUrlEncodedContentType)); 56 | } on DioError catch (_) {} 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/tcyun_page/tcyun_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_picgo/model/config.dart'; 5 | import 'package:flutter_picgo/model/tcyun_config.dart'; 6 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 9 | 10 | class TcyunPage extends StatefulWidget { 11 | _TcyunPageState createState() => _TcyunPageState(); 12 | } 13 | 14 | class _TcyunPageState extends BasePBSettingPageState { 15 | @override 16 | String get pbType => PBTypeKeys.tcyun; 17 | 18 | @override 19 | onLoadConfig(String config) { 20 | List configs = []; 21 | Map map; 22 | if (isBlank(config)) { 23 | map = TcyunConfig().toJson(); 24 | } else { 25 | map = TcyunConfig.fromJson(json.decode(config)).toJson(); 26 | } 27 | map.forEach((key, value) { 28 | Config config; 29 | if (key == 'secretId') { 30 | config = Config( 31 | label: '设定SecretId', 32 | placeholder: 'SecretId', 33 | needValidate: true, 34 | value: value); 35 | } else if (key == 'secretKey') { 36 | config = Config( 37 | label: '设定SecretKey', 38 | placeholder: 'SecretKey', 39 | needValidate: true, 40 | value: value); 41 | } else if (key == 'bucket') { 42 | config = Config( 43 | label: '设定存储空间名', 44 | placeholder: '例如test-1253954259', 45 | needValidate: true, 46 | value: value); 47 | } else if (key == 'area') { 48 | config = Config( 49 | label: '确认存储区域', 50 | placeholder: '例如tj', 51 | needValidate: true, 52 | value: value); 53 | } else if (key == 'path') { 54 | config = Config(label: '指定存储路径', placeholder: '例如img/', value: value); 55 | } else if (key == 'customUrl') { 56 | config = Config( 57 | label: '设定自定义域名', 58 | placeholder: '例如:http://xxx.yyy.cn', 59 | value: value); 60 | } 61 | config.name = key; 62 | configs.add(config); 63 | }); 64 | setConfigs(configs); 65 | } 66 | 67 | @override 68 | String get title => '腾讯云COS图床'; 69 | } 70 | -------------------------------------------------------------------------------- /lib/utils/strategy/upload_strategy_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 2 | import 'package:flutter_picgo/utils/strategy/impl/aliyun_image_upload.dart'; 3 | import 'package:flutter_picgo/utils/strategy/impl/gitee_image_upload.dart'; 4 | import 'package:flutter_picgo/utils/strategy/impl/github_image_upload.dart'; 5 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 6 | import 'package:flutter_picgo/utils/strategy/impl/lsky_image_upload.dart'; 7 | import 'package:flutter_picgo/utils/strategy/impl/niupic_image_upload.dart'; 8 | import 'package:flutter_picgo/utils/strategy/impl/qiniu_image_upload.dart'; 9 | import 'package:flutter_picgo/utils/strategy/impl/smms_image_upload.dart'; 10 | import 'package:flutter_picgo/utils/strategy/impl/tcyun_image_upload.dart'; 11 | import 'package:flutter_picgo/utils/strategy/impl/upyun_image_upload.dart'; 12 | import 'package:flutter_picgo/utils/strings.dart'; 13 | 14 | class UploadStrategyFactory { 15 | static Map cache = {}; 16 | 17 | /// UploadStrategy工厂类,负责创建UploadStrategy 18 | static ImageUploadStrategy getUploadStrategy(String type) { 19 | if (isBlank(type)) { 20 | throw new NullThrownError(); 21 | } 22 | if (cache[type] == null) { 23 | if (type == PBTypeKeys.github) { 24 | /// Github 25 | cache[type] = new GithubImageUpload(); 26 | } else if (type == PBTypeKeys.smms) { 27 | /// SM.MS 28 | cache[type] = new SMMSImageUpload(); 29 | } else if (type == PBTypeKeys.gitee) { 30 | /// Gitee 31 | cache[type] = new GiteeImageUpload(); 32 | } else if (type == PBTypeKeys.qiniu) { 33 | /// 七牛 34 | cache[type] = new QiniuImageUpload(); 35 | } else if (type == PBTypeKeys.aliyun) { 36 | /// 阿里云 37 | cache[type] = new AliyunImageUpload(); 38 | } else if (type == PBTypeKeys.tcyun) { 39 | /// 腾讯云 40 | cache[type] = new TcyunImageUpload(); 41 | } else if (type == PBTypeKeys.niupic) { 42 | /// 牛图网 43 | cache[type] = new NiupicImageUpload(); 44 | } else if (type == PBTypeKeys.lsky) { 45 | /// 兰空 46 | cache[type] = new LskyImageUpload(); 47 | } else if (type == PBTypeKeys.upyun) { 48 | /// 又拍云 49 | cache[type] = new UpyunImageUpload(); 50 | } 51 | } 52 | return cache[type]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluro/fluro.dart'; 3 | import 'package:flutter_picgo/model/theme_state.dart'; 4 | import 'package:flutter_picgo/resources/theme_colors.dart'; 5 | import 'package:flutter_picgo/routers/application.dart'; 6 | import 'package:flutter_picgo/routers/routers.dart'; 7 | import 'package:flutter_picgo/utils/db_provider.dart'; 8 | import 'package:flutter_picgo/utils/local_notification.dart'; 9 | import 'package:provider/provider.dart'; 10 | 11 | ThemeState themeState; 12 | 13 | Future main() async { 14 | /// needed if you intend to initialize in the `main` function 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | final provider = DbProvider(); 17 | await provider.init(); 18 | 19 | /// notification initialization 20 | LocalNotificationUtil.getInstance().initialization(); 21 | 22 | /// theme initialization 23 | themeState = ThemeState(); 24 | 25 | /// run App 26 | runApp(App()); 27 | } 28 | 29 | class App extends StatefulWidget { 30 | App() { 31 | final router = new FluroRouter(); 32 | Routes.configureRoutes(router); 33 | Application.router = router; 34 | } 35 | 36 | _AppState createState() => _AppState(); 37 | } 38 | 39 | class _AppState extends State { 40 | // This widget is the root of your application. 41 | @override 42 | Widget build(BuildContext context) { 43 | return MultiProvider( 44 | providers: [ 45 | ChangeNotifierProvider( 46 | create: (context) => themeState, 47 | ), 48 | ], 49 | child: Consumer( 50 | builder: (context, state, child) { 51 | return state.currentMode == ThemeMode.system 52 | ? MaterialApp( 53 | debugShowCheckedModeBanner: false, 54 | theme: lightThemeData, 55 | darkTheme: darkThemeData, 56 | initialRoute: '/', 57 | onGenerateRoute: Application.router.generator, 58 | ) 59 | : MaterialApp( 60 | debugShowCheckedModeBanner: false, 61 | theme: state.currentMode == ThemeMode.light 62 | ? lightThemeData 63 | : darkThemeData, 64 | initialRoute: '/', 65 | onGenerateRoute: Application.router.generator, 66 | ); 67 | }, 68 | ), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/aliyun_page/aliyun_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_picgo/model/aliyun_config.dart'; 5 | import 'package:flutter_picgo/model/config.dart'; 6 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 9 | 10 | class AliyunPage extends StatefulWidget { 11 | _AliyunPageState createState() => _AliyunPageState(); 12 | } 13 | 14 | class _AliyunPageState extends BasePBSettingPageState { 15 | @override 16 | String get pbType => PBTypeKeys.aliyun; 17 | 18 | @override 19 | onLoadConfig(String config) { 20 | List configs = []; 21 | Map map; 22 | if (isBlank(config)) { 23 | map = AliyunConfig().toJson(); 24 | } else { 25 | map = AliyunConfig.fromJson(json.decode(config)).toJson(); 26 | } 27 | map.forEach((key, value) { 28 | Config config; 29 | if (key == 'accessKeyId') { 30 | config = Config( 31 | label: '设定KeyId', 32 | placeholder: 'AccessKeyId', 33 | needValidate: true, 34 | value: value); 35 | } else if (key == 'accessKeySecret') { 36 | config = Config( 37 | label: '设定KeySecret', 38 | placeholder: 'AccessKeySecret', 39 | needValidate: true, 40 | value: value); 41 | } else if (key == 'bucket') { 42 | config = Config( 43 | label: '设定存储空间名', 44 | placeholder: 'Bucket', 45 | needValidate: true, 46 | value: value); 47 | } else if (key == 'customUrl') { 48 | config = Config( 49 | label: '设定自定义域名', 50 | placeholder: '例如:http://xxx.yyy.cn', 51 | value: value); 52 | } else if (key == 'area') { 53 | config = Config( 54 | label: '确认存储区域', 55 | placeholder: '例如oss-cn-beijing', 56 | needValidate: true, 57 | value: value); 58 | } else if (key == 'options') { 59 | config = 60 | Config(label: '设定网址后缀', placeholder: '例如?imageslim', value: value); 61 | } else if (key == 'path') { 62 | config = Config(label: '指定存储路径', placeholder: '例如img/', value: value); 63 | } 64 | config.name = key; 65 | configs.add(config); 66 | }); 67 | setConfigs(configs); 68 | } 69 | 70 | @override 71 | String get title => '阿里云OSS图床'; 72 | } 73 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Flutter-PicGo 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | NSAppleMusicUsageDescription 26 | Media access needed for a better experience. 27 | NSCalendarsUsageDescription 28 | Calendars access needed for a better experience. 29 | NSCameraUsageDescription 30 | 如果不允许,你将无法访问相机进行扫描二维码~ 31 | NSContactsUsageDescription 32 | Contacts access needed for a better experience. 33 | NSLocationAlwaysUsageDescription 34 | Location access needed for a better experience. 35 | NSLocationWhenInUseUsageDescription 36 | Location access needed for a better experience. 37 | NSMotionUsageDescription 38 | Motion access needed for a better experience. 39 | NSPhotoLibraryUsageDescription 40 | 如果不允许,你将无法访问系统相册进行选择照片上传至图床以及获取上传后的照片链接~ 41 | NSSpeechRecognitionUsageDescription 42 | Speech access needed for a better experience. 43 | UILaunchStoryboardName 44 | LaunchScreen 45 | UIMainStoryboardFile 46 | Main 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | 51 | UISupportedInterfaceOrientations~ipad 52 | 53 | UIInterfaceOrientationPortrait 54 | UIInterfaceOrientationPortraitUpsideDown 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UIViewControllerBasedStatusBarAppearance 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lib/views/manage_page/github_page/github_repo_page_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter_picgo/api/github_api.dart'; 3 | import 'package:flutter_picgo/model/github_config.dart'; 4 | import 'package:flutter_picgo/model/github_content.dart'; 5 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 6 | import 'package:flutter_picgo/utils/image_upload.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | import 'package:path/path.dart' as pathutil; 9 | 10 | abstract class GithubRepoPageContract { 11 | void loadSuccess(List data); 12 | 13 | void loadError(String msg); 14 | } 15 | 16 | class GithubRepoPagePresenter { 17 | GithubRepoPageContract _view; 18 | 19 | GithubRepoPagePresenter(this._view); 20 | 21 | doLoadContents(String path, String prePath) async { 22 | try { 23 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.github); 24 | GithubConfig config = GithubConfig.fromJson(json.decode(configStr)); 25 | if (isBlank(config.branch) || 26 | isBlank(config.repo) || 27 | isBlank(config.token)) { 28 | _view.loadError('读取配置错误!'); 29 | return; 30 | } 31 | String realUrl = pathutil.joinAll([ 32 | 'repos', 33 | config.repo, 34 | 'contents', 35 | prePath ?? '', 36 | path == '/' ? '' : path 37 | ]); 38 | List result = 39 | await GithubApi.getContents(realUrl, {"ref": config.branch}); 40 | var data = result.map((e) { 41 | return GithubContent.fromJson(e); 42 | }).toList(); 43 | _view.loadSuccess(data); 44 | } catch (e) { 45 | _view.loadError('$e'); 46 | } 47 | } 48 | 49 | Future doDeleteContents( 50 | String path, String prePath, String name, String sha) async { 51 | try { 52 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.github); 53 | GithubConfig config = GithubConfig.fromJson(json.decode(configStr)); 54 | if (isBlank(config.repo) || isBlank(config.token)) { 55 | _view.loadError('读取配置错误!'); 56 | return false; 57 | } 58 | String url = pathutil.joinAll([ 59 | 'repos', 60 | config.repo, 61 | 'contents', 62 | prePath ?? '', 63 | path == '/' ? '' : path, 64 | name 65 | ]); 66 | await GithubApi.deleteContent(url, { 67 | "message": 'DELETE BY Flutter-PicGo', 68 | "sha": sha, 69 | "branch": config.branch 70 | }); 71 | return true; 72 | } catch (e) { 73 | return false; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/github_page/github_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:fluro/fluro.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_picgo/model/config.dart'; 6 | import 'package:flutter_picgo/model/github_config.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/routers/application.dart'; 9 | import 'package:flutter_picgo/routers/routers.dart'; 10 | import 'package:flutter_picgo/utils/strings.dart'; 11 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 12 | 13 | class GithubPage extends StatefulWidget { 14 | @override 15 | _GithubPageState createState() => _GithubPageState(); 16 | } 17 | 18 | class _GithubPageState extends BasePBSettingPageState { 19 | @override 20 | onLoadConfig(String config) { 21 | List configs = []; 22 | Map map; 23 | if (isBlank(config)) { 24 | map = GithubConfig().toJson(); 25 | } else { 26 | map = GithubConfig.fromJson(json.decode(config)).toJson(); 27 | } 28 | map.forEach((key, value) { 29 | Config config; 30 | if (key == 'repo') { 31 | config = Config( 32 | label: '设定仓库名', 33 | placeholder: '例如 hackycy/picBed', 34 | needValidate: true, 35 | value: value); 36 | } else if (key == 'token') { 37 | config = Config( 38 | label: '设定Token', 39 | placeholder: 'Token', 40 | needValidate: true, 41 | value: value); 42 | } else if (key == 'customUrl') { 43 | config = Config( 44 | label: '设定自定义域名', 45 | placeholder: '例如:http://xxx.yyy.cloudcdn.cn', 46 | value: value); 47 | } else if (key == 'branch') { 48 | config = Config( 49 | label: '确认分支名', 50 | placeholder: '例如 master', 51 | value: value, 52 | needValidate: true); 53 | } else if (key == 'path') { 54 | config = Config(label: '指定存储路径', placeholder: '例如img/', value: value); 55 | } 56 | config.name = key; 57 | configs.add(config); 58 | }); 59 | setConfigs(configs); 60 | } 61 | 62 | @override 63 | String get pbType => PBTypeKeys.github; 64 | 65 | @override 66 | String get title => 'Github图床'; 67 | 68 | @override 69 | bool get isSupportManage => true; 70 | 71 | @override 72 | handleManage() { 73 | Application.router.navigateTo(context, 74 | '${Routes.settingPbGitubRepo}?path=${Uri.encodeComponent("/")}', 75 | transition: TransitionType.cupertino); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/views/manage_page/gitee_page/gitee_repo_page_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter_picgo/api/gitee_api.dart'; 3 | import 'package:flutter_picgo/model/gitee_config.dart'; 4 | import 'package:flutter_picgo/model/gitee_content.dart'; 5 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 6 | import 'package:flutter_picgo/utils/image_upload.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | import 'package:path/path.dart' as pathutil; 9 | 10 | abstract class GiteeRepoPageContract { 11 | void loadSuccess(List data); 12 | 13 | void loadError(String msg); 14 | } 15 | 16 | class GiteeRepoPagePresenter { 17 | GiteeRepoPageContract _view; 18 | 19 | GiteeRepoPagePresenter(this._view); 20 | 21 | doLoadContents(String path, String prePath) async { 22 | try { 23 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.gitee); 24 | GiteeConfig config = GiteeConfig.fromJson(json.decode(configStr)); 25 | if (isBlank(config.repo) || isBlank(config.token)) { 26 | _view.loadError('读取配置错误!'); 27 | return; 28 | } 29 | String realUrl = pathutil.joinAll([ 30 | 'repos', 31 | config.owner, 32 | config.repo, 33 | 'contents', 34 | prePath ?? '', 35 | path == '/' ? '' : path 36 | ]); 37 | List result = await GiteeApi.getContents(realUrl, 38 | isBlank(config.branch) ? null : {"ref": config.branch ?? ''}); 39 | var data = result.map((e) { 40 | return GiteeContent.fromJson(e); 41 | }).toList(); 42 | _view.loadSuccess(data); 43 | } catch (e) { 44 | _view.loadError('$e'); 45 | } 46 | } 47 | 48 | Future doDeleteContents( 49 | String path, String prePath, String name, String sha) async { 50 | try { 51 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.gitee); 52 | GiteeConfig config = GiteeConfig.fromJson(json.decode(configStr)); 53 | if (isBlank(config.repo) || isBlank(config.token)) { 54 | _view.loadError('读取配置错误!'); 55 | return false; 56 | } 57 | String url = pathutil.joinAll([ 58 | 'repos', 59 | config.owner, 60 | config.repo, 61 | 'contents', 62 | prePath ?? '', 63 | path == '/' ? '' : path, 64 | name 65 | ]); 66 | Map query = { 67 | "message": "DELETE BY Flutter-PicGo", 68 | "sha": sha, 69 | }; 70 | if (!isBlank(config.branch)) { 71 | query["branch"] = config.branch; 72 | } 73 | await GiteeApi.deleteFile(url, query); 74 | return true; 75 | } catch (e) { 76 | return false; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | //Get Key Sign Info 25 | def keystoreProperties = new Properties() 26 | def keystorePropertiesFile = rootProject.file('keystore/key.properties') 27 | if (keystorePropertiesFile.exists()) { 28 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 29 | } 30 | 31 | apply plugin: 'com.android.application' 32 | apply plugin: 'kotlin-android' 33 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 34 | 35 | android { 36 | compileSdkVersion 28 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | } 41 | 42 | lintOptions { 43 | disable 'InvalidPackage' 44 | } 45 | 46 | defaultConfig { 47 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 48 | applicationId "com.siyee.flutterpicgo" 49 | minSdkVersion 19 50 | targetSdkVersion 28 51 | versionCode flutterVersionCode.toInteger() 52 | versionName flutterVersionName 53 | 54 | // support multidex 55 | multiDexEnabled true 56 | } 57 | 58 | signingConfigs { 59 | release { 60 | keyAlias keystoreProperties['keyAlias'] 61 | keyPassword keystoreProperties['keyPassword'] 62 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 63 | storePassword keystoreProperties['storePassword'] 64 | } 65 | } 66 | 67 | buildTypes { 68 | release { 69 | // TODO: Add your own signing config for the release build. 70 | // Signing with the debug keys for now, so `flutter run --release` works. 71 | signingConfig signingConfigs.release 72 | } 73 | } 74 | } 75 | 76 | flutter { 77 | source '../..' 78 | } 79 | 80 | dependencies { 81 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 82 | } 83 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/gitee_page/gitee_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:fluro/fluro.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_picgo/model/config.dart'; 6 | import 'package:flutter_picgo/model/gitee_config.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/routers/application.dart'; 9 | import 'package:flutter_picgo/routers/routers.dart'; 10 | import 'package:flutter_picgo/utils/strings.dart'; 11 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 12 | 13 | class GiteePage extends StatefulWidget { 14 | @override 15 | _GiteePageState createState() => _GiteePageState(); 16 | } 17 | 18 | class _GiteePageState extends BasePBSettingPageState { 19 | @override 20 | onLoadConfig(String config) { 21 | List configs = []; 22 | Map map; 23 | if (isBlank(config)) { 24 | map = GiteeConfig().toJson(); 25 | } else { 26 | map = GiteeConfig.fromJson(json.decode(config)).toJson(); 27 | } 28 | map.forEach((key, value) { 29 | Config config; 30 | if (key == 'owner') { 31 | config = Config( 32 | label: '设定仓库所属空间地址', 33 | placeholder: '例如 hackycy', 34 | needValidate: true, 35 | value: value); 36 | } else if (key == 'repo') { 37 | config = Config( 38 | label: '设定仓库名', 39 | placeholder: '例如 picBed', 40 | needValidate: true, 41 | value: value); 42 | } else if (key == 'token') { 43 | config = Config( 44 | label: '设定Token', 45 | placeholder: 'Token', 46 | needValidate: true, 47 | value: value); 48 | } else if (key == 'customUrl') { 49 | config = Config( 50 | label: '设定自定义域名', 51 | placeholder: '例如:http://xxx.yyy.cloudcdn.cn,不推荐', 52 | value: value); 53 | } else if (key == 'branch') { 54 | config = Config( 55 | label: '确认分支名', placeholder: '不填默认master,建议填写', value: value); 56 | } else if (key == 'path') { 57 | config = Config(label: '指定存储路径', placeholder: '例如img/', value: value); 58 | } 59 | config.name = key; 60 | configs.add(config); 61 | }); 62 | setConfigs(configs); 63 | } 64 | 65 | @override 66 | String get pbType => PBTypeKeys.gitee; 67 | 68 | @override 69 | String get title => 'Gitee图床'; 70 | 71 | @override 72 | bool get isSupportManage => true; 73 | 74 | @override 75 | handleManage() { 76 | Application.router.navigateTo(context, 77 | '${Routes.settingPbGiteeRepo}?path=${Uri.encodeComponent("/")}', 78 | transition: TransitionType.cupertino); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/qiniu_page/qiniu_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:fluro/fluro.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_picgo/model/config.dart'; 7 | import 'package:flutter_picgo/model/qiniu_config.dart'; 8 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 9 | import 'package:flutter_picgo/routers/application.dart'; 10 | import 'package:flutter_picgo/routers/routers.dart'; 11 | import 'package:flutter_picgo/utils/strings.dart'; 12 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 13 | 14 | class QiniuPage extends StatefulWidget { 15 | _QiniuPageState createState() => _QiniuPageState(); 16 | } 17 | 18 | class _QiniuPageState extends BasePBSettingPageState { 19 | @override 20 | String get pbType => PBTypeKeys.qiniu; 21 | 22 | @override 23 | onLoadConfig(String config) { 24 | List configs = []; 25 | Map map; 26 | if (isBlank(config)) { 27 | map = QiniuConfig().toJson(); 28 | } else { 29 | map = QiniuConfig.fromJson(json.decode(config)).toJson(); 30 | } 31 | map.forEach((key, value) { 32 | Config config; 33 | if (key == 'accessKey') { 34 | config = Config( 35 | label: '设定AccessKey', 36 | placeholder: 'AccessKey', 37 | needValidate: true, 38 | value: value); 39 | } else if (key == 'secretKey') { 40 | config = Config( 41 | label: '设定SecretKey', 42 | placeholder: 'SecretKey', 43 | needValidate: true, 44 | value: value); 45 | } else if (key == 'bucket') { 46 | config = Config( 47 | label: '设定存储空间名', 48 | placeholder: 'Bucket', 49 | needValidate: true, 50 | value: value); 51 | } else if (key == 'url') { 52 | config = Config( 53 | label: '设定访问网址', 54 | placeholder: '例如:http://xxx.yyy.cloudcdn.cn', 55 | needValidate: true, 56 | value: value); 57 | } else if (key == 'area') { 58 | config = Config( 59 | label: '确认存储区域', 60 | placeholder: '例如z0', 61 | needValidate: true, 62 | value: value); 63 | } else if (key == 'options') { 64 | config = 65 | Config(label: '设定网址后缀', placeholder: '例如?imageslim', value: value); 66 | } else if (key == 'path') { 67 | config = Config(label: '指定存储路径', placeholder: '例如img/', value: value); 68 | } 69 | config.name = key; 70 | configs.add(config); 71 | }); 72 | setConfigs(configs); 73 | } 74 | 75 | @override 76 | String get title => '七牛图床'; 77 | 78 | @override 79 | bool get isSupportManage => true; 80 | 81 | @override 82 | handleManage() { 83 | Application.router.navigateTo(context, 84 | '${Routes.settingPbQiniuRepo}?path=${Uri.encodeComponent("/")}', 85 | transition: TransitionType.cupertino); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 25 | 29 | 33 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/components/loading.dart: -------------------------------------------------------------------------------- 1 | // 2 | // Created with Android Studio. 3 | // User: 三帆 4 | // Date: 07/08/2019 5 | // Time: 08:40 6 | // email: sanfan.hx@alibaba-inc.com 7 | // tartget: 代码获取自: https://blog.csdn.net/O_time/article/details/86496537 8 | // 9 | import 'dart:async'; 10 | import 'package:flutter/material.dart'; 11 | 12 | // ignore: must_be_immutable 13 | class NetLoadingDialog extends StatefulWidget { 14 | String loadingText; 15 | bool outsideDismiss; 16 | bool loading; 17 | Function dismissCallback; 18 | Future requestCallBack; 19 | 20 | NetLoadingDialog( 21 | {Key key, 22 | this.loadingText = "loading...", 23 | this.outsideDismiss = false, 24 | this.dismissCallback, 25 | this.loading, 26 | this.requestCallBack}) 27 | : super(key: key); 28 | 29 | @override 30 | State createState() => _LoadingDialog(); 31 | } 32 | 33 | class _LoadingDialog extends State { 34 | _dismissDialog() { 35 | if (widget.dismissCallback != null) { 36 | widget.dismissCallback(); 37 | } 38 | Navigator.of(context).pop(); 39 | } 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | if (widget.requestCallBack != null) { 45 | widget.requestCallBack.then((_) { 46 | Navigator.pop(context); 47 | }).catchError((_) { 48 | Navigator.pop(context); 49 | }); 50 | } 51 | } 52 | 53 | @override 54 | Widget build(BuildContext context) { 55 | if (!widget.loading) { 56 | return Container(); 57 | } 58 | return new GestureDetector( 59 | onTap: widget.outsideDismiss ? _dismissDialog : null, 60 | child: Material( 61 | type: MaterialType.transparency, 62 | child: new Center( 63 | child: new SizedBox( 64 | width: 120.0, 65 | height: 120.0, 66 | child: new Container( 67 | decoration: ShapeDecoration( 68 | color: Theme.of(context).brightness == Brightness.dark 69 | ? Colors.black 70 | : Colors.white, 71 | shape: RoundedRectangleBorder( 72 | borderRadius: BorderRadius.all( 73 | Radius.circular(8.0), 74 | ), 75 | ), 76 | ), 77 | child: new Column( 78 | mainAxisAlignment: MainAxisAlignment.center, 79 | crossAxisAlignment: CrossAxisAlignment.center, 80 | children: [ 81 | new CircularProgressIndicator(), 82 | new Padding( 83 | padding: const EdgeInsets.only( 84 | top: 20.0, 85 | ), 86 | child: new Text( 87 | widget.loadingText, 88 | style: new TextStyle(fontSize: 12.0), 89 | ), 90 | ), 91 | ], 92 | ), 93 | ), 94 | ), 95 | ), 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/upyun_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_picgo/api/upyun_api.dart'; 4 | import 'package:flutter_picgo/model/uploaded.dart'; 5 | import 'package:flutter_picgo/model/upyun_config.dart'; 6 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 7 | import 'package:flutter_picgo/utils/image_upload.dart'; 8 | import 'dart:io'; 9 | import 'package:path/path.dart'; 10 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 11 | import 'package:flutter_picgo/utils/strings.dart'; 12 | 13 | class UpyunImageUpload implements ImageUploadStrategy { 14 | @override 15 | Future delete(Uploaded uploaded) async { 16 | UpyunUploadedInfo info; 17 | try { 18 | info = UpyunUploadedInfo.fromJson(json.decode(uploaded.info)); 19 | } catch (e) {} 20 | if (info != null) { 21 | await UpyunApi.deleteObject( 22 | info.bucket, info.operator, info.password, info.key); 23 | } 24 | return uploaded; 25 | } 26 | 27 | @override 28 | Future upload(File file, String renameImage) async { 29 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.upyun); 30 | if (isBlank(configStr)) { 31 | throw UpyunError(error: '读取配置文件错误!请重试'); 32 | } 33 | UpyunConfig config = UpyunConfig.fromJson(json.decode(configStr)); 34 | await UpyunApi.putObject( 35 | file, config.operator, config.password, renameImage, config.bucket, 36 | path: config.path); 37 | String wholeKey = joinAll([config.path ?? '', renameImage]); 38 | String imagePath = 39 | joinAll([config.url, '$wholeKey${config.options ?? ''}']); 40 | var uploadedItem = Uploaded(-1, '$imagePath', PBTypeKeys.upyun, 41 | info: json.encode(UpyunUploadedInfo( 42 | operator: config.operator, 43 | password: config.password, 44 | bucket: config.bucket, 45 | key: wholeKey, 46 | ))); 47 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 48 | return uploadedItem; 49 | } 50 | } 51 | 52 | class UpyunError implements Exception { 53 | UpyunError({ 54 | this.error, 55 | }); 56 | 57 | dynamic error; 58 | 59 | String get message => (error?.toString() ?? ''); 60 | 61 | @override 62 | String toString() { 63 | var msg = 'UpyunError $message'; 64 | if (error is Error) { 65 | msg += '\n${error.stackTrace}'; 66 | } 67 | return msg; 68 | } 69 | } 70 | 71 | class UpyunUploadedInfo { 72 | String operator; 73 | String bucket; 74 | String password; 75 | String key; 76 | 77 | UpyunUploadedInfo({this.operator, this.bucket, this.password, this.key}); 78 | 79 | UpyunUploadedInfo.fromJson(Map json) { 80 | operator = json['operator']; 81 | bucket = json['bucket']; 82 | password = json['password']; 83 | key = json['key']; 84 | } 85 | 86 | Map toJson() { 87 | final Map data = new Map(); 88 | data['operator'] = this.operator; 89 | data['bucket'] = this.bucket; 90 | data['password'] = this.password; 91 | data['key'] = this.key; 92 | return data; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/utils/net.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_picgo/api/qiniu_api.dart'; 3 | import 'package:flutter_picgo/api/tcyun_api.dart'; 4 | import 'package:flutter_picgo/api/upyun_api.dart'; 5 | 6 | const bool inProduction = const bool.fromEnvironment("dart.vm.product"); 7 | 8 | class NetUtils { 9 | Dio _dio; 10 | static NetUtils _instance; 11 | 12 | NetUtils._internal() { 13 | _dio = new Dio(BaseOptions( 14 | connectTimeout: 30000, receiveTimeout: 30000, sendTimeout: 30000)); 15 | 16 | /// Tcyun Interceptor 17 | dio.interceptors.add(TcyunInterceptor()); 18 | 19 | /// Upyun Interceptor 20 | dio.interceptors.add(UpyunInterceptor()); 21 | 22 | /// Qiniu Interceptor 23 | dio.interceptors.add(QiniuInterceptor()); 24 | 25 | /// Log Interceptor 26 | if (!inProduction) { 27 | /// Log 28 | dio.interceptors 29 | .add(LogInterceptor(requestBody: true, responseBody: true)); 30 | } 31 | } 32 | 33 | Dio get dio => _dio; 34 | 35 | static NetUtils getInstance() { 36 | if (_instance == null) { 37 | _instance = NetUtils._internal(); 38 | } 39 | return _instance; 40 | } 41 | 42 | /// get method 43 | Future get(String path, 44 | {Map queryParameters, 45 | Options options, 46 | CancelToken cancelToken, 47 | ProgressCallback onReceiveProgress}) async { 48 | return await dio.get(path, 49 | queryParameters: queryParameters, 50 | options: options, 51 | cancelToken: cancelToken, 52 | onReceiveProgress: onReceiveProgress); 53 | } 54 | 55 | /// post method 56 | Future post( 57 | String path, { 58 | data, 59 | Map queryParameters, 60 | Options options, 61 | CancelToken cancelToken, 62 | ProgressCallback onSendProgress, 63 | ProgressCallback onReceiveProgress, 64 | }) async { 65 | return await dio.post(path, 66 | data: data, 67 | queryParameters: queryParameters, 68 | options: options, 69 | cancelToken: cancelToken, 70 | onSendProgress: onSendProgress, 71 | onReceiveProgress: onReceiveProgress); 72 | } 73 | 74 | /// put method 75 | Future put( 76 | String path, { 77 | data, 78 | Map queryParameters, 79 | Options options, 80 | CancelToken cancelToken, 81 | ProgressCallback onSendProgress, 82 | ProgressCallback onReceiveProgress, 83 | }) async { 84 | return await dio.put(path, 85 | data: data, 86 | queryParameters: queryParameters, 87 | options: options, 88 | cancelToken: cancelToken, 89 | onSendProgress: onSendProgress, 90 | onReceiveProgress: onReceiveProgress); 91 | } 92 | 93 | /// delete method 94 | Future delete(String path, 95 | {data, 96 | Map queryParameters, 97 | Options options, 98 | CancelToken cancelToken}) async { 99 | return await dio.delete(path, 100 | data: data, 101 | queryParameters: queryParameters, 102 | options: options, 103 | cancelToken: cancelToken); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/api/upyun_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:crypto/crypto.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:flutter_picgo/utils/net.dart'; 7 | import 'package:path/path.dart'; 8 | 9 | class UpyunApi { 10 | static const BASE_URL = 'http://v0.api.upyun.com'; 11 | 12 | static const String operatorKey = 'operatorKey'; 13 | static const String passwordKey = 'passwordKey'; 14 | 15 | /// REST API PUT 16 | static Future putObject( 17 | File file, 18 | String operator, 19 | String password, 20 | String name, 21 | String bucket, { 22 | String path = '', 23 | }) async { 24 | String wholePath = joinAll([BASE_URL, bucket, path, name]); 25 | var bytes = file.readAsBytesSync(); 26 | Response res = await NetUtils.getInstance().put(wholePath, 27 | data: Stream.fromIterable(bytes.map((e) => [e])), 28 | options: Options( 29 | headers: { 30 | Headers.contentLengthHeader: bytes.length, 31 | }, 32 | contentType: 'image/${extension(name).replaceFirst('.', '')}', 33 | extra: { 34 | operatorKey: operator, 35 | passwordKey: password, 36 | })); 37 | return res.headers; 38 | } 39 | 40 | /// REST API DELETE 41 | static Future deleteObject( 42 | String bucket, 43 | String operator, 44 | String password, 45 | String key, 46 | ) async { 47 | String wholePath = joinAll([BASE_URL, bucket, key]); 48 | Response res = await NetUtils.getInstance().delete(wholePath, 49 | options: Options( 50 | extra: { 51 | operatorKey: operator, 52 | passwordKey: password, 53 | }, 54 | )); 55 | return res.headers; 56 | } 57 | 58 | /// build policy 59 | static String buildPolicy(String bucket, String saveKey) { 60 | Map map = { 61 | 'bucket': bucket, 62 | 'save-key': saveKey, 63 | 'expiration': DateTime.now().millisecondsSinceEpoch + 30 * 60 * 1000, 64 | }; 65 | return base64.encode(utf8.encode(json.encode(map))); 66 | } 67 | } 68 | 69 | class UpyunInterceptor extends InterceptorsWrapper { 70 | @override 71 | Future onRequest( 72 | RequestOptions options, 73 | RequestInterceptorHandler handler, 74 | ) async { 75 | if (options.path.contains(UpyunApi.BASE_URL.replaceFirst('http://', ''))) { 76 | /// 请求方式,如:GET、POST、PUT、HEAD 等 77 | String method = options.method.toUpperCase(); 78 | String path = options.uri.path; 79 | String date = HttpDate.format(DateTime.now()); 80 | String pwdMd5 = 81 | '${md5.convert(utf8.encode(options.extra[UpyunApi.passwordKey]))}'; 82 | String operator = options.extra[UpyunApi.operatorKey]; 83 | String sign = '$method&$path&$date'; 84 | 85 | /// 签名构造 86 | var hmacsha1 = Hmac(sha1, utf8.encode('$pwdMd5')); 87 | var auth = hmacsha1.convert(utf8.encode(sign)); 88 | String realAuth = base64.encode(auth.bytes); 89 | 90 | /// Add Common Header 91 | options.headers.addAll({ 92 | 'Date': date, 93 | 'Authorization': 'UPYUN $operator:$realAuth', 94 | }); 95 | } 96 | return handler.next(options); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/views/manage_page/qiniu_page/qiniu_repo_page_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter_picgo/api/qiniu_api.dart'; 4 | import 'package:flutter_picgo/components/manage_item.dart'; 5 | import 'package:flutter_picgo/model/qiniu_config.dart'; 6 | import 'package:flutter_picgo/model/qiniu_content.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/utils/image_upload.dart'; 9 | import 'package:flutter_picgo/utils/strings.dart'; 10 | import 'package:path/path.dart' as pathlib; 11 | 12 | abstract class QiniuRepoPageContract { 13 | void loadSuccess(List data); 14 | 15 | void loadError(String msg); 16 | } 17 | 18 | class QiniuRepoPagePresenter { 19 | QiniuRepoPageContract _view; 20 | 21 | QiniuRepoPagePresenter(this._view); 22 | 23 | doLoadContents(String prefix) async { 24 | try { 25 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.qiniu); 26 | QiniuConfig config = QiniuConfig.fromJson(json.decode(configStr)); 27 | if (isBlank(config.accessKey) || isBlank(config.secretKey)) { 28 | _view.loadError('读取配置文件错误'); 29 | return; 30 | } 31 | var result = await QiniuApi.list({ 32 | 'bucket': config.bucket, 33 | 'prefix': prefix == '/' ? '' : prefix, 34 | 'delimiter': Uri.decodeComponent('/'), 35 | }, config.accessKey, config.secretKey); 36 | List data = (result['items'] as List).map((e) { 37 | QiniuContent c = QiniuContent.fromJson(e); 38 | c.type = FileContentType.FILE; 39 | c.url = pathlib.joinAll([ 40 | config.url, 41 | c.key, 42 | ]); 43 | c.key = '${c.key}'.replaceFirst(prefix == '/' ? '' : prefix, ''); 44 | return c; 45 | }).toList(); 46 | if (result['commonPrefixes'] != null) { 47 | (result['commonPrefixes'] as List).forEach((element) { 48 | QiniuContent c = QiniuContent(); 49 | c.type = FileContentType.DIR; 50 | c.url = element; 51 | 52 | /// 例如 xin/ax 去除 xin/ 只显示最后一个 ax 53 | List keys = '$element'.split('/'); 54 | c.key = keys[keys.length - 2]; 55 | data.add(c); 56 | }); 57 | } 58 | _view.loadSuccess(data); 59 | } catch (e) { 60 | _view.loadError('$e'); 61 | } 62 | } 63 | 64 | Future doDeleteContents(String key) async { 65 | try { 66 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.qiniu); 67 | QiniuConfig config = QiniuConfig.fromJson(json.decode(configStr)); 68 | if (isBlank(config.accessKey) || isBlank(config.secretKey)) { 69 | _view.loadError('读取配置文件错误'); 70 | return false; 71 | } 72 | String encodedEntryURI = 73 | QiniuApi.urlSafeBase64Encode(utf8.encode('${config.bucket}:$key')); 74 | String url = '${QiniuApi.BASE_URL}/delete/$encodedEntryURI'; 75 | var result = 76 | await QiniuApi.delete(url, config.accessKey, config.secretKey); 77 | if (isBlank(result.toString())) { 78 | return true; 79 | } else { 80 | _view.loadError('${result['error']}'); 81 | return false; 82 | } 83 | } catch (e) { 84 | return false; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/api/aliyun_api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:crypto/crypto.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:flutter_picgo/utils/net.dart'; 7 | import 'package:flutter_picgo/utils/strings.dart'; 8 | 9 | class AliyunApi { 10 | // 还需要拼接buckername 例如zjyzy.oss-cn-shenzhen.aliyuncs.com 11 | static const BASE_URL = 'aliyuncs.com'; 12 | 13 | static postObject(String bucket, String aera, FormData data) async { 14 | Response res = await NetUtils.getInstance().post( 15 | 'https://$bucket.$aera.aliyuncs.com', 16 | data: data, 17 | ); 18 | return res.headers; 19 | } 20 | 21 | static deleteObject( 22 | String bucket, String aera, String object, String auth) async { 23 | Response res = await NetUtils.getInstance() 24 | .delete('https://$bucket.$aera.aliyuncs.com/$object', 25 | options: Options(headers: { 26 | 'Authorization': auth, 27 | 'Date': HttpDate.format(new DateTime.now()), 28 | })); 29 | return res.headers; 30 | } 31 | 32 | /// Content-MD5的计算 33 | static String generateContentMD5(String content) { 34 | // 先计算MD5加密的二进制数组 35 | var digest = md5.convert(utf8.encode(content)); 36 | return base64.encode(digest.bytes); 37 | } 38 | 39 | /// 构建CanonicalizedResource 40 | static String buildCanonicalizedResource( 41 | String bucketName, String objectName) { 42 | if (isBlank(bucketName)) { 43 | return '/'; 44 | } 45 | if (isBlank(objectName)) { 46 | return '/$bucketName/'; 47 | } else { 48 | return '/$bucketName/$objectName'; 49 | } 50 | } 51 | 52 | /// form policy 53 | static String buildEncodePolicy(String objectName) { 54 | var policyText = { 55 | "expiration": 56 | "2030-01-01T00:00:00.000Z", // 设置Policy的失效时间,如果超过失效时间,就无法通过此Policy上传文件 57 | "conditions": [ 58 | { 59 | "key": objectName, 60 | } // 设置上传文件的大小限制,如果超过限制,文件上传到OSS会报错 61 | ] 62 | }; 63 | var encodePolicy = base64.encode(utf8.encode(json.encode(policyText))); 64 | return encodePolicy; 65 | } 66 | 67 | /// Form Authorization 68 | static String buildPostSignature( 69 | String accessKeyId, String accessKeySecret, String encodePolicy) { 70 | // 使用SecertKey对上一步生成的原始字符串计算HMAC-SHA1签名: 71 | var hmacsha1 = Hmac(sha1, utf8.encode(accessKeySecret)); 72 | var sign = hmacsha1.convert(utf8.encode(encodePolicy)); 73 | var encodeSign = base64.encode(sign.bytes); 74 | return encodeSign; 75 | } 76 | 77 | /// Authorization 78 | static String buildSignature(String accessKeyId, String accessKeySecret, 79 | String verb, String bucketName, String objectName, 80 | {String contentMd5 = '', String contentType = ''}) { 81 | var canonicalizedResource = 82 | buildCanonicalizedResource(bucketName, objectName); 83 | var date = HttpDate.format(new DateTime.now()); 84 | var originSign = 85 | '${verb.toUpperCase()}\n$contentMd5\n$contentType\n$date\n$canonicalizedResource'; 86 | // 使用SecertKey对上一步生成的原始字符串计算HMAC-SHA1签名: 87 | var hmacsha1 = Hmac(sha1, utf8.encode(accessKeySecret)); 88 | var sign = hmacsha1.convert(utf8.encode(originSign)); 89 | var encodeSign = base64.encode(sign.bytes); 90 | return 'OSS $accessKeyId:$encodeSign'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/views/setting_page/setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_picgo/routers/application.dart'; 4 | import 'package:flutter_picgo/routers/routers.dart'; 5 | import 'package:package_info/package_info.dart'; 6 | import 'package:url_launcher/url_launcher.dart'; 7 | 8 | class SettingPage extends StatefulWidget { 9 | @override 10 | _SettingPageState createState() => _SettingPageState(); 11 | } 12 | 13 | class _SettingPageState extends State { 14 | String version; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | _getVersion(); 20 | } 21 | 22 | void _getVersion() async { 23 | final PackageInfo info = await PackageInfo.fromPlatform(); 24 | setState(() { 25 | this.version = info.version; 26 | }); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Scaffold( 32 | appBar: AppBar( 33 | title: Text('设置'), 34 | centerTitle: true, 35 | ), 36 | body: ListView( 37 | children: [ 38 | // 图标面板 39 | Container( 40 | width: double.infinity, 41 | height: 160.0, 42 | child: Column( 43 | children: [ 44 | SizedBox(height: 20), 45 | Center( 46 | child: Container( 47 | width: 80, 48 | height: 80, 49 | child: ClipOval( 50 | child: Image.asset( 51 | 'assets/images/logo.png', 52 | fit: BoxFit.cover, 53 | width: 80, 54 | height: 80, 55 | ), 56 | ), 57 | ), 58 | ), 59 | SizedBox(height: 20), 60 | Center( 61 | child: Text('v${this.version}'), 62 | ), 63 | ], 64 | ), 65 | ), 66 | // 菜单列表 67 | ListTile( 68 | title: Text('图床设置'), 69 | onTap: () { 70 | Application.router.navigateTo(context, Routes.settingPb, 71 | transition: TransitionType.cupertino); 72 | }, 73 | trailing: Icon(Icons.arrow_right), 74 | ), 75 | ListTile( 76 | title: Text('PicGo设置'), 77 | onTap: () { 78 | Application.router.navigateTo(context, Routes.settingPicgo, 79 | transition: TransitionType.cupertino); 80 | }, 81 | trailing: Icon(Icons.arrow_right), 82 | ), 83 | ListTile( 84 | title: Text('关于Flutter-PicGo'), 85 | onTap: () { 86 | launch('https://github.com/hackycy/flutter-picgo'); 87 | }, 88 | trailing: Icon(Icons.arrow_right), 89 | ), 90 | ListTile( 91 | title: Text('建议或报告问题'), 92 | onTap: () { 93 | launch('https://github.com/PicGo/flutter-picgo/issues'); 94 | }, 95 | trailing: Icon(Icons.arrow_right), 96 | ), 97 | ], 98 | ), 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - barcode_scan (0.0.1): 3 | - Flutter 4 | - MTBBarcodeScanner 5 | - SwiftProtobuf 6 | - Flutter (1.0.0) 7 | - flutter_local_notifications (0.0.1): 8 | - Flutter 9 | - FMDB (2.7.5): 10 | - FMDB/standard (= 2.7.5) 11 | - FMDB/standard (2.7.5) 12 | - MTBBarcodeScanner (5.0.11) 13 | - package_info (0.0.1): 14 | - Flutter 15 | - path_provider (0.0.1): 16 | - Flutter 17 | - "permission_handler (5.0.1+1)": 18 | - Flutter 19 | - photo_manager (0.0.1): 20 | - Flutter 21 | - shared_preferences (0.0.1): 22 | - Flutter 23 | - sqflite (0.0.2): 24 | - Flutter 25 | - FMDB (>= 2.7.5) 26 | - SwiftProtobuf (1.9.0) 27 | - url_launcher (0.0.1): 28 | - Flutter 29 | - video_player (0.0.1): 30 | - Flutter 31 | 32 | DEPENDENCIES: 33 | - barcode_scan (from `.symlinks/plugins/barcode_scan/ios`) 34 | - Flutter (from `Flutter`) 35 | - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) 36 | - package_info (from `.symlinks/plugins/package_info/ios`) 37 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 38 | - permission_handler (from `.symlinks/plugins/permission_handler/ios`) 39 | - photo_manager (from `.symlinks/plugins/photo_manager/ios`) 40 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 41 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 42 | - url_launcher (from `.symlinks/plugins/url_launcher/ios`) 43 | - video_player (from `.symlinks/plugins/video_player/ios`) 44 | 45 | SPEC REPOS: 46 | trunk: 47 | - FMDB 48 | - MTBBarcodeScanner 49 | - SwiftProtobuf 50 | 51 | EXTERNAL SOURCES: 52 | barcode_scan: 53 | :path: ".symlinks/plugins/barcode_scan/ios" 54 | Flutter: 55 | :path: Flutter 56 | flutter_local_notifications: 57 | :path: ".symlinks/plugins/flutter_local_notifications/ios" 58 | package_info: 59 | :path: ".symlinks/plugins/package_info/ios" 60 | path_provider: 61 | :path: ".symlinks/plugins/path_provider/ios" 62 | permission_handler: 63 | :path: ".symlinks/plugins/permission_handler/ios" 64 | photo_manager: 65 | :path: ".symlinks/plugins/photo_manager/ios" 66 | shared_preferences: 67 | :path: ".symlinks/plugins/shared_preferences/ios" 68 | sqflite: 69 | :path: ".symlinks/plugins/sqflite/ios" 70 | url_launcher: 71 | :path: ".symlinks/plugins/url_launcher/ios" 72 | video_player: 73 | :path: ".symlinks/plugins/video_player/ios" 74 | 75 | SPEC CHECKSUMS: 76 | barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479 77 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 78 | flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 79 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 80 | MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb 81 | package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 82 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 83 | permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 84 | photo_manager: f7c619c2cc8c2adb8d85c63363babac477de9c67 85 | shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d 86 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 87 | SwiftProtobuf: ecbec1be9036d15655f6b3443a1c4ea693c97932 88 | url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef 89 | video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e 90 | 91 | PODFILE CHECKSUM: 08e41981fcdc78ff11c898bc59c6c49635c114b3 92 | 93 | COCOAPODS: 1.9.1 94 | -------------------------------------------------------------------------------- /lib/views/pb_setting_page/lsky_page/lsky_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:fluro/fluro.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_picgo/api/lsky_api.dart'; 6 | import 'package:flutter_picgo/model/config.dart'; 7 | import 'package:flutter_picgo/model/lsky_config.dart'; 8 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 9 | import 'package:flutter_picgo/routers/application.dart'; 10 | import 'package:flutter_picgo/routers/routers.dart'; 11 | import 'package:flutter_picgo/utils/strings.dart'; 12 | import 'package:flutter_picgo/views/pb_setting_page/base_pb_page_state.dart'; 13 | import 'package:toast/toast.dart'; 14 | 15 | class LskyPage extends StatefulWidget { 16 | _LskyPageState createState() => _LskyPageState(); 17 | } 18 | 19 | class _LskyPageState extends BasePBSettingPageState { 20 | @override 21 | onLoadConfig(String config) { 22 | List configs = []; 23 | Map map; 24 | if (isBlank(config)) { 25 | map = LskyConfig().toJson(); 26 | } else { 27 | map = LskyConfig.fromJson(json.decode(config)).toJson(); 28 | } 29 | map.forEach((key, value) { 30 | Config config; 31 | if (key == 'host') { 32 | config = Config( 33 | label: '设定Host', 34 | placeholder: '例如:https://lsky.si-yee.com', 35 | needValidate: true, 36 | value: value); 37 | } else if (key == 'token') { 38 | config = Config( 39 | label: '设定Token', 40 | placeholder: 'Token', 41 | needValidate: false, 42 | value: value); 43 | } else if (key == 'email') { 44 | config = Config( 45 | label: '设定邮箱', 46 | placeholder: 'Email', 47 | needValidate: true, 48 | value: value); 49 | } else if (key == 'password') { 50 | config = Config( 51 | label: '设定密码', 52 | placeholder: '设定密码', 53 | needValidate: true, 54 | value: value); 55 | } 56 | config.name = key; 57 | configs.add(config); 58 | }); 59 | setConfigs(configs); 60 | } 61 | 62 | @override 63 | String get tip => '点击保存会自动获取Token,如果已填写字段则不会自动生成'; 64 | 65 | @override 66 | String get pbType => PBTypeKeys.lsky; 67 | 68 | @override 69 | save() async { 70 | if (isBlank(controllers['token'].text.trim())) { 71 | // 如果Token为空,则自动获取 72 | String email = controllers['email'].value.text.trim(); 73 | String pwd = controllers['password'].value.text.trim(); 74 | String host = controllers['host'].value.text.trim(); 75 | var result = await LskyApi.token(email, pwd, host); 76 | if (result["code"] == 200) { 77 | // controllers["token"].text = '${result["data"]["token"]}'; 78 | setState(() { 79 | configs[3].value = '${result["data"]["token"]}'; 80 | }); 81 | Future.delayed(Duration(milliseconds: 500), () { 82 | super.save(); 83 | }); 84 | } else { 85 | Toast.show('Token获取失败,请检查配置', context); 86 | } 87 | } else { 88 | super.save(); 89 | } 90 | } 91 | 92 | @override 93 | bool get isSupportManage => true; 94 | 95 | @override 96 | handleManage() { 97 | Application.router.navigateTo(context, Routes.settingPbLskyRepo, 98 | transition: TransitionType.cupertino); 99 | } 100 | 101 | @override 102 | String get title => '兰空图床'; 103 | } 104 | -------------------------------------------------------------------------------- /lib/utils/shared_preferences.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 4 | import 'package:flutter_picgo/resources/shared_preferences_keys.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | export 'package:flutter_picgo/resources/shared_preferences_keys.dart'; 8 | 9 | /// 用来做shared_preferences的存储 10 | class SpUtil { 11 | static SpUtil _instance; 12 | static Future get instance async { 13 | return await getInstance(); 14 | } 15 | 16 | static SharedPreferences _spf; 17 | 18 | SpUtil._(); 19 | 20 | Future _init() async { 21 | _spf = await SharedPreferences.getInstance(); 22 | } 23 | 24 | static Future getInstance() async { 25 | if (_instance == null) { 26 | _instance = new SpUtil._(); 27 | } 28 | if (_spf == null) { 29 | await _instance._init(); 30 | } 31 | return _instance; 32 | } 33 | 34 | static bool _beforeCheck() { 35 | if (_spf == null) { 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | /// 设置默认图床 42 | setDefaultPB(String type) { 43 | putString(SharedPreferencesKeys.settingDefaultPB, type); 44 | } 45 | 46 | /// 获取当前默认图床,默认为github 47 | String getDefaultPB() { 48 | return getString(SharedPreferencesKeys.settingDefaultPB) ?? 49 | PBTypeKeys.github; 50 | } 51 | 52 | // 判断是否存在数据 53 | bool hasKey(String key) { 54 | Set keys = getKeys(); 55 | return keys.contains(key); 56 | } 57 | 58 | Set getKeys() { 59 | if (_beforeCheck()) return null; 60 | return _spf.getKeys(); 61 | } 62 | 63 | get(String key) { 64 | if (_beforeCheck()) return null; 65 | return _spf.get(key); 66 | } 67 | 68 | getString(String key) { 69 | if (_beforeCheck()) return null; 70 | return _spf.getString(key); 71 | } 72 | 73 | Future putString(String key, String value) { 74 | if (_beforeCheck()) return null; 75 | return _spf.setString(key, value); 76 | } 77 | 78 | bool getBool(String key) { 79 | if (_beforeCheck()) return null; 80 | return _spf.getBool(key); 81 | } 82 | 83 | Future putBool(String key, bool value) { 84 | if (_beforeCheck()) return null; 85 | return _spf.setBool(key, value); 86 | } 87 | 88 | int getInt(String key) { 89 | if (_beforeCheck()) return null; 90 | return _spf.getInt(key); 91 | } 92 | 93 | Future putInt(String key, int value) { 94 | if (_beforeCheck()) return null; 95 | return _spf.setInt(key, value); 96 | } 97 | 98 | double getDouble(String key) { 99 | if (_beforeCheck()) return null; 100 | return _spf.getDouble(key); 101 | } 102 | 103 | Future putDouble(String key, double value) { 104 | if (_beforeCheck()) return null; 105 | return _spf.setDouble(key, value); 106 | } 107 | 108 | List getStringList(String key) { 109 | return _spf.getStringList(key); 110 | } 111 | 112 | Future putStringList(String key, List value) { 113 | if (_beforeCheck()) return null; 114 | return _spf.setStringList(key, value); 115 | } 116 | 117 | dynamic getDynamic(String key) { 118 | if (_beforeCheck()) return null; 119 | return _spf.get(key); 120 | } 121 | 122 | Future remove(String key) { 123 | if (_beforeCheck()) return null; 124 | return _spf.remove(key); 125 | } 126 | 127 | Future clear() { 128 | if (_beforeCheck()) return null; 129 | return _spf.clear(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_picgo 2 | description: PicGo For Flutter 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.9.2+24 19 | 20 | environment: 21 | sdk: ">=2.7.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | shared_preferences: ^2.0.5 27 | dio: ^4.0.0 28 | package_info: ^2.0.0 29 | sqflite: ^1.3.2+1 30 | fluro: ^2.0.3 31 | toast: ^0.1.5 32 | json_serializable: ^3.3.0 33 | url_launcher: ^5.7.10 34 | permission_handler: ^5.0.1 35 | barcode_scan: ^3.0.1 36 | provider: ^5.0.0 37 | crypto: ^3.0.1 38 | flutter_local_notifications: ^3.0.1+4 39 | pull_to_refresh: ^1.6.5 40 | wechat_assets_picker: ^5.1.3 41 | extended_image: ^4.0.1 42 | 43 | 44 | # The following adds the Cupertino Icons font to your application. 45 | # Use with the CupertinoIcons class for iOS style icons. 46 | cupertino_icons: ^0.1.3 47 | 48 | dev_dependencies: 49 | flutter_test: 50 | sdk: flutter 51 | 52 | # For information on the generic Dart part of this file, see the 53 | # following page: https://dart.dev/tools/pub/pubspec 54 | 55 | # The following section is specific to Flutter. 56 | flutter: 57 | 58 | # The following line ensures that the Material Icons font is 59 | # included with your application, so that you can use the icons in 60 | # the material Icons class. 61 | uses-material-design: true 62 | 63 | # To add assets to your application, add an assets section, like this: 64 | assets: 65 | - assets/images/logo.png 66 | - assets/images/2.0x/logo.png 67 | - assets/images/3.0x/logo.png 68 | - assets/images/4.0x/logo.png 69 | - assets/images/icon_empty_album.png 70 | # - images/a_dot_ham.jpeg 71 | 72 | # An image asset can refer to one or more resolution-specific "variants", see 73 | # https://flutter.dev/assets-and-images/#resolution-aware. 74 | 75 | # For details regarding adding assets from package dependencies, see 76 | # https://flutter.dev/assets-and-images/#from-packages 77 | 78 | # To add custom fonts to your application, add a fonts section here, 79 | # in this "flutter" section. Each entry in this list should have a 80 | # "family" key with the font family name, and a "fonts" key with a 81 | # list giving the asset and other descriptors for the font. For 82 | # example: 83 | fonts: 84 | - family: Iconfont 85 | fonts: 86 | - asset: assets/fonts/iconfont.ttf 87 | # - asset: fonts/Schyler-Italic.ttf 88 | # style: italic 89 | # - family: Trajan Pro 90 | # fonts: 91 | # - asset: fonts/TrajanPro.ttf 92 | # - asset: fonts/TrajanPro_Bold.ttf 93 | # weight: 700 94 | # 95 | # For details regarding fonts from package dependencies, 96 | # see https://flutter.dev/custom-fonts/#from-packages 97 | -------------------------------------------------------------------------------- /lib/utils/image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:flutter_picgo/model/uploaded.dart'; 3 | import 'package:flutter_picgo/resources/table_name_keys.dart'; 4 | import 'package:flutter_picgo/utils/shared_preferences.dart'; 5 | import 'package:flutter_picgo/utils/sql.dart'; 6 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 7 | 8 | /// 图像上传类 9 | class ImageUploadUtils { 10 | ImageUploadStrategy _strategy; 11 | 12 | ImageUploadUtils(this._strategy); 13 | 14 | Future delete(Uploaded uploaded) async { 15 | var sp = await SpUtil.getInstance(); 16 | var isForceDelete = 17 | sp.getBool(SharedPreferencesKeys.settingIsForceDelete) ?? false; 18 | // 关闭仅关闭本地图片,则需要根据对应策略来删除图片 19 | if (!isForceDelete) { 20 | var upTmp = await _strategy.delete(uploaded); 21 | if (upTmp == null) { 22 | throw Exception('delete remote error'); 23 | } 24 | } 25 | // 删除本地项 26 | var raw = await deleteUploadedItem(uploaded); 27 | if (raw > 0) { 28 | return uploaded; 29 | } else { 30 | throw Exception('delete local error'); 31 | } 32 | } 33 | 34 | Future upload(File file, String renameImage) { 35 | return _strategy.upload(file, renameImage); 36 | } 37 | 38 | /// 获取默认图床类型 39 | static Future getDefaultPB() async { 40 | var sp = await SpUtil.getInstance(); 41 | String pbType = sp.getDefaultPB(); 42 | return pbType; 43 | } 44 | 45 | /// 设置默认图床 46 | static Future setDefaultPB(String type) async { 47 | var sp = await SpUtil.getInstance(); 48 | if (sp.getDefaultPB() != type) { 49 | sp.setDefaultPB(type); 50 | } 51 | } 52 | 53 | /// 保存图床配置 54 | static Future savePBConfig(String type, String config) async { 55 | var sql = Sql.setTable(TABLE_NAME_PBSETTING); 56 | int row = await sql.rawUpdate('config = ? WHERE type = ?', [config, type]); 57 | return row; 58 | } 59 | 60 | /// 获取当前图床配置 61 | static Future getPBConfig(String type) async { 62 | var sql = Sql.setTable(TABLE_NAME_PBSETTING); 63 | var pbsettingRow = await sql.getBySql('type = ?', [type]); 64 | if (pbsettingRow != null && pbsettingRow.length > 0) { 65 | return pbsettingRow.first["config"]; 66 | } 67 | return null; 68 | } 69 | 70 | /// 获取图床名称 71 | static Future getPBName(String type) async { 72 | var sql = Sql.setTable(TABLE_NAME_PBSETTING); 73 | var pbsettingRow = (await sql.getBySql('type = ?', [type])); 74 | if (pbsettingRow != null && pbsettingRow.length > 0) { 75 | return pbsettingRow.first["name"]; 76 | } 77 | return null; 78 | } 79 | 80 | /// 保存已上传项 81 | static Future saveUploadedItem(Uploaded item) async { 82 | var sql = Sql.setTable(TABLE_NAME_UPLOADED); 83 | return await sql.rawInsert('(path, type, info) VALUES(?, ?, ?)', 84 | [item.path, item.type, item.info]); 85 | } 86 | 87 | /// 删除已上传的本地数据库项 88 | static Future deleteUploadedItem(Uploaded item) async { 89 | var sql = Sql.setTable(TABLE_NAME_UPLOADED); 90 | var result = await sql.rawDelete('id = ?', [item.id]); 91 | return result; 92 | } 93 | 94 | /// 获取已上传项的信息 95 | static Future getUploadedItemInfo(int id) async { 96 | var sql = Sql.setTable(TABLE_NAME_UPLOADED); 97 | return (await sql.getBySql('id = ?', [id]))?.first["info"]; 98 | } 99 | 100 | /// 获取已上传列表 101 | static Future> getUploadeds() async { 102 | var sql = Sql.setTable(TABLE_NAME_UPLOADED); 103 | var result = await sql.get(); 104 | List uploadeds = result.map((v) { 105 | return Uploaded.fromMap(v); 106 | }).toList(); 107 | return uploadeds; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/routers/routers.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluro/fluro.dart'; 2 | import 'package:flutter_picgo/routers/router_handler.dart'; 3 | 4 | class Routes { 5 | // 路由Map 6 | static const String root = '/'; 7 | static const String album = '/album'; 8 | static const String notfound = '/404'; 9 | static const String setting = '/setting'; 10 | static const String upload = '/upload'; 11 | static const String handleUpload = '/handle/upload'; 12 | static const String settingPb = '/setting/pb'; 13 | static const String settingPicgo = '/setting/picgo'; 14 | static const String settingPicgoTheme = '/setting/picgo/theme'; 15 | // --------- github ------------------ 16 | static const String settingPbGithub = '/setting/pb/github'; 17 | static const String settingPbGitubRepo = '/setting/pb/github/repo'; 18 | // ----------------------------------- 19 | // --------- smms ------------------ 20 | static const String settingPbSMMS = '/setting/pb/smms'; 21 | static const String settingPbSMMSRepo = '/setting/pb/smms/repo'; 22 | // ----------------------------------- 23 | // --------- gitee ------------------ 24 | static const String settingPbGitee = '/setting/pb/gitee'; 25 | static const String settingPbGiteeRepo = '/setting/pb/gitee/repo'; 26 | // ----------------------------------- 27 | // --------- qiniu ------------------- 28 | static const String settingPbQiniu = '/setting/pb/qiniu'; 29 | static const String settingPbQiniuRepo = 'setting/pb/qiniu/repo'; 30 | // ----------------------------------- 31 | // --------- aliyun ------------------- 32 | static const String settingPbAliyun = '/setting/pb/aliyun'; 33 | // ----------------------------------- 34 | // --------- tcyun ------------------- 35 | static const String settingPbTcyun = '/setting/pb/tcyun'; 36 | // ----------------------------------- 37 | // --------- niupic ------------------- 38 | static const String settingPbNiupic = '/setting/pb/niupic'; 39 | // ----------------------------------- 40 | // --------- lsky ------------------- 41 | static const String settingPbLsky = '/setting/pb/lsky'; 42 | static const String settingPbLskyRepo = '/setting/pb/lsky/repo'; 43 | // ----------------------------------- 44 | // --------- upyun ------------------- 45 | static const String settingPbUpyun = '/setting/pb/upyun'; 46 | // ----------------------------------- 47 | 48 | static void configureRoutes(FluroRouter router) { 49 | router.notFoundHandler = notfoundHandler; 50 | router.define(root, handler: appHandler); 51 | router.define(notfound, handler: notfoundHandler); 52 | router.define(album, handler: albumHandler); 53 | router.define(handleUpload, handler: preUploadHandler); 54 | router.define(setting, handler: settingHandler); 55 | router.define(settingPb, handler: pbsettingHandler); 56 | router.define(settingPbGithub, handler: pbsettingGithubHandler); 57 | router.define(settingPbGitubRepo, handler: pbsettingGithubRepoHandler); 58 | router.define(settingPicgo, handler: picgosettingHandler); 59 | router.define(settingPbSMMS, handler: pbsettingSMMSHandler); 60 | router.define(settingPbGitee, handler: pbsettingGiteeHandler); 61 | router.define(settingPbGiteeRepo, handler: pbsettingGiteeRepoHandler); 62 | router.define(settingPicgoTheme, handler: picggsettingThemeHandler); 63 | router.define(settingPbQiniu, handler: pbsettingQiniuHandler); 64 | router.define(settingPbAliyun, handler: pbsettingAliyunHandler); 65 | router.define(settingPbTcyun, handler: pbsettingTcyunHandler); 66 | router.define(settingPbNiupic, handler: pbsettingNiupicHandler); 67 | router.define(settingPbLsky, handler: pbsettingLskyHandler); 68 | router.define(settingPbUpyun, handler: pbsettingUpyunHandler); 69 | router.define(settingPbSMMSRepo, handler: pbsettingSMMSRepoHandler); 70 | router.define(settingPbLskyRepo, handler: pbsettingLskyRepoHandler); 71 | router.define(settingPbQiniuRepo, handler: pbsettingQiniuRepoHandler); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/components/manage_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_picgo/utils/extended.dart'; 4 | import 'package:path/path.dart'; 5 | 6 | typedef GestureTapCallback = void Function(); 7 | typedef DismissDirectionCallback = void Function(DismissDirection direction); 8 | 9 | class ManageItem extends StatelessWidget { 10 | final Key key; 11 | final String url; 12 | final String name; 13 | final String info; 14 | final FileContentType type; 15 | final GestureTapCallback onTap; 16 | final DismissDirectionCallback onDismiss; 17 | final ConfirmDismissCallback confirmDismiss; 18 | 19 | ManageItem( 20 | this.key, 21 | this.url, 22 | this.name, 23 | this.info, 24 | this.type, { 25 | this.onTap, 26 | this.onDismiss, 27 | this.confirmDismiss, 28 | }); 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return Dismissible( 33 | key: key, 34 | direction: DismissDirection.endToStart, 35 | child: Stack( 36 | children: [ 37 | ListTile( 38 | leading: buildLeading(), 39 | title: Text( 40 | this.name, 41 | maxLines: 1, 42 | overflow: TextOverflow.ellipsis, 43 | textWidthBasis: TextWidthBasis.parent, 44 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400), 45 | ), 46 | subtitle: Text( 47 | this.info, 48 | maxLines: 1, 49 | style: TextStyle(color: Colors.grey), 50 | ), 51 | onTap: onTap, 52 | ), 53 | Align( 54 | alignment: Alignment.bottomCenter, 55 | child: Divider( 56 | height: 1, 57 | color: Theme.of(context).dividerColor, 58 | indent: 80, 59 | ), 60 | ) 61 | ], 62 | ), 63 | onDismissed: onDismiss, 64 | confirmDismiss: confirmDismiss, 65 | background: Container( 66 | color: Colors.red, 67 | ), 68 | ); 69 | } 70 | 71 | Widget buildLeading() { 72 | if (this.type == FileContentType.DIR) { 73 | return buildCenterIcon(Icon(IconData(0xe63f, fontFamily: 'iconfont'))); 74 | } 75 | try { 76 | String path = Uri.parse(this.url).path; 77 | String suffix = extension(path).replaceFirst('.', ''); 78 | var imageSuffixs = ['png', 'bmp', 'jpeg', 'gif', 'jpg']; 79 | if (imageSuffixs.contains(suffix)) { 80 | return SizedBox( 81 | height: 50, 82 | width: 50, 83 | child: Card( 84 | clipBehavior: Clip.antiAlias, 85 | shape: RoundedRectangleBorder( 86 | borderRadius: BorderRadiusDirectional.circular(8)), 87 | child: ExtendedImage.network( 88 | this.url, 89 | width: 50, 90 | height: 50, 91 | fit: BoxFit.cover, 92 | cache: true, 93 | // border: Border.all(color: Colors.grey, width: 1.0), 94 | borderRadius: BorderRadius.all(Radius.circular(8)), 95 | loadStateChanged: (state) => defaultLoadStateChanged(state), 96 | ), 97 | )); 98 | } else { 99 | return buildCenterIcon(Icon(IconData(0xe654, fontFamily: 'iconfont'))); 100 | } 101 | } catch (e) { 102 | return buildCenterIcon(Icon(IconData(0xe654, fontFamily: 'iconfont'))); 103 | } 104 | } 105 | 106 | Widget buildCenterIcon(Icon icon) { 107 | return SizedBox( 108 | height: 50, 109 | width: 50, 110 | child: Center( 111 | child: icon, 112 | ), 113 | ); 114 | } 115 | } 116 | 117 | /// 文件类型 118 | enum FileContentType { 119 | /// 文件 120 | FILE, 121 | 122 | /// 文件夹 123 | DIR, 124 | } 125 | -------------------------------------------------------------------------------- /lib/utils/local_notification.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 4 | 5 | class LocalNotificationUtil { 6 | static LocalNotificationUtil _instance; 7 | 8 | FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin; 9 | NotificationAppLaunchDetails _notificationAppLaunchDetails; 10 | 11 | FlutterLocalNotificationsPlugin get flutterLocalNotificationsPlugin => 12 | _flutterLocalNotificationsPlugin; 13 | 14 | NotificationAppLaunchDetails get notificationAppLaunchDetails => 15 | _notificationAppLaunchDetails; 16 | 17 | LocalNotificationUtil._(); 18 | 19 | /// 必须先调用 20 | initialization() async { 21 | _flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); 22 | // notification 23 | _notificationAppLaunchDetails = await _flutterLocalNotificationsPlugin 24 | .getNotificationAppLaunchDetails(); 25 | 26 | var initializationSettingsAndroid = 27 | AndroidInitializationSettings('ic_launcher'); 28 | // Note: permissions aren't requested here just to demonstrate that can be done later using the `requestPermissions()` method 29 | // of the `IOSFlutterLocalNotificationsPlugin` class 30 | var initializationSettingsIOS = IOSInitializationSettings( 31 | requestAlertPermission: false, //初始化不请求权限 32 | requestBadgePermission: false, 33 | requestSoundPermission: false, 34 | onDidReceiveLocalNotification: null); 35 | var initializationSettings = InitializationSettings( 36 | android: initializationSettingsAndroid, 37 | iOS: initializationSettingsIOS, 38 | macOS: new MacOSInitializationSettings()); 39 | await _flutterLocalNotificationsPlugin.initialize(initializationSettings, 40 | onSelectNotification: (String payload) async { 41 | if (payload != null) {} 42 | }); 43 | } 44 | 45 | /// 需要先请求权限 46 | requestPermissions() { 47 | /// iOS获取权限 48 | if (Platform.isIOS) { 49 | _flutterLocalNotificationsPlugin 50 | .resolvePlatformSpecificImplementation< 51 | IOSFlutterLocalNotificationsPlugin>() 52 | ?.requestPermissions( 53 | alert: true, 54 | badge: true, 55 | sound: true, 56 | ); 57 | } 58 | if (Platform.isAndroid) { 59 | _flutterLocalNotificationsPlugin 60 | .resolvePlatformSpecificImplementation< 61 | AndroidFlutterLocalNotificationsPlugin>() 62 | ?.createNotificationChannel(AndroidNotificationChannel( 63 | AndroidChannelId.upload_channel, '通知提示', '上传通知提示')); 64 | } 65 | } 66 | 67 | /// 显示 68 | show(int id, String title, String body, 69 | NotificationDetails notificationDetails) async { 70 | await _flutterLocalNotificationsPlugin.show( 71 | id, title, body, notificationDetails); 72 | } 73 | 74 | /// 实例获取 75 | static LocalNotificationUtil getInstance() { 76 | if (_instance == null) { 77 | _instance = LocalNotificationUtil._(); 78 | } 79 | return _instance; 80 | } 81 | 82 | /// 上传Channel 83 | static AndroidNotificationDetails uploadAndroidChannel() { 84 | return AndroidNotificationDetails( 85 | AndroidChannelId.upload_channel, '上传通知', '上传通知提示', 86 | importance: Importance.max, priority: Priority.high, ticker: 'ticker'); 87 | } 88 | 89 | /// 默认IOS 90 | static IOSNotificationDetails normalIOSNotificationDetails() { 91 | return IOSNotificationDetails(); 92 | } 93 | 94 | /// 默认MacOS 95 | static MacOSNotificationDetails normalMacOSNotificationDetails() { 96 | return MacOSNotificationDetails(); 97 | } 98 | 99 | static NotificationDetails createNotificationDetails( 100 | AndroidNotificationDetails android, 101 | IOSNotificationDetails iOS, 102 | MacOSNotificationDetails macOS) { 103 | return NotificationDetails(android: android, iOS: iOS, macOS: macOS); 104 | } 105 | } 106 | 107 | class AndroidChannelId { 108 | static const upload_channel = '10000'; 109 | } 110 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/tcyun_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_picgo/api/tcyun_api.dart'; 5 | import 'package:flutter_picgo/model/tcyun_config.dart'; 6 | import 'package:flutter_picgo/model/uploaded.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/utils/image_upload.dart'; 9 | import 'dart:io'; 10 | 11 | import 'package:path/path.dart' as path; 12 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 13 | import 'package:flutter_picgo/utils/strings.dart'; 14 | 15 | class TcyunImageUpload implements ImageUploadStrategy { 16 | @override 17 | Future delete(Uploaded uploaded) async { 18 | TcyunUploaddedInfo info; 19 | try { 20 | info = TcyunUploaddedInfo.fromJson(json.decode(uploaded.info)); 21 | } catch (e) {} 22 | if (info != null) { 23 | await TcyunApi.deleteobject( 24 | info.secretId, info.secretKey, info.bucket, info.area, info.key); 25 | } 26 | return uploaded; 27 | } 28 | 29 | @override 30 | Future upload(File file, String renameImage) async { 31 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.tcyun); 32 | if (isBlank(configStr)) { 33 | throw TcyunError(error: '读取配置文件错误!请重试'); 34 | } 35 | TcyunConfig config = TcyunConfig.fromJson(json.decode(configStr)); 36 | String objectPath = isBlank(config.path) 37 | ? renameImage 38 | : path.joinAll([config.path, renameImage]); 39 | 40 | /// post 41 | String keyTime = TcyunApi.buildKeyTime(); 42 | String policy = TcyunApi.buildPolicy( 43 | config.bucket, objectPath, config.secretId, keyTime); 44 | FormData formData = FormData.fromMap({ 45 | "key": objectPath, 46 | "file": await MultipartFile.fromFile(file.path, filename: renameImage), 47 | "policy": base64.encode(utf8.encode(policy)), 48 | "q-sign-algorithm": "sha1", 49 | "q-ak": config.secretId, 50 | "q-key-time": keyTime, 51 | "q-sign-time": keyTime, 52 | "q-signature": TcyunApi.buildSignature(config.secretKey, keyTime, policy) 53 | }); 54 | await TcyunApi.postObject( 55 | config.secretId, 56 | config.secretKey, 57 | config.bucket, 58 | config.area, 59 | path.extension(renameImage).replaceFirst('.', ''), 60 | formData); 61 | String imgPath = path.joinAll([ 62 | isBlank(config.customUrl) 63 | ? 'https://${config.bucket}.cos.${config.area}.myqcloud.com' 64 | : config.customUrl, 65 | objectPath 66 | ]); 67 | var uploadedItem = Uploaded(-1, '$imgPath', PBTypeKeys.tcyun, 68 | info: json.encode(TcyunUploaddedInfo( 69 | secretId: config.secretId, 70 | secretKey: config.secretKey, 71 | area: config.area, 72 | key: objectPath, 73 | bucket: config.bucket))); 74 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 75 | return uploadedItem; 76 | } 77 | } 78 | 79 | class TcyunError implements Exception { 80 | TcyunError({ 81 | this.error, 82 | }); 83 | 84 | dynamic error; 85 | 86 | String get message => (error?.toString() ?? ''); 87 | 88 | @override 89 | String toString() { 90 | var msg = 'TcyunError $message'; 91 | if (error is Error) { 92 | msg += '\n${error.stackTrace}'; 93 | } 94 | return msg; 95 | } 96 | } 97 | 98 | class TcyunUploaddedInfo { 99 | String area; 100 | String bucket; 101 | String secretId; 102 | String secretKey; 103 | String key; 104 | 105 | TcyunUploaddedInfo( 106 | {this.area, this.bucket, this.secretId, this.secretKey, this.key}); 107 | 108 | TcyunUploaddedInfo.fromJson(Map json) { 109 | area = json['area']; 110 | bucket = json['bucket']; 111 | secretId = json['secretId']; 112 | secretKey = json['secretKey']; 113 | key = json['key']; 114 | } 115 | 116 | Map toJson() { 117 | final Map data = new Map(); 118 | data['area'] = this.area; 119 | data['bucket'] = this.bucket; 120 | data['secretId'] = this.secretId; 121 | data['secretKey'] = this.secretKey; 122 | data['key'] = this.key; 123 | return data; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/github_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_picgo/api/github_api.dart'; 3 | import 'package:flutter_picgo/model/github_config.dart'; 4 | import 'package:flutter_picgo/model/uploaded.dart'; 5 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 6 | import 'package:flutter_picgo/utils/encrypt.dart'; 7 | import 'package:flutter_picgo/utils/image_upload.dart'; 8 | import 'package:flutter_picgo/utils/strings.dart'; 9 | import 'dart:io'; 10 | import 'dart:convert'; 11 | import 'package:path/path.dart' as path; 12 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 13 | 14 | class GithubImageUpload implements ImageUploadStrategy { 15 | static const UPLOAD_COMMIT_MESSAGE = "Upload by Flutter-PicGo"; 16 | static const DELETE_COMMIT_MESSAGE = "Delete by Flutter-PicGo"; 17 | 18 | @override 19 | Future delete(Uploaded uploaded) async { 20 | GithubUploadedInfo info; 21 | try { 22 | info = GithubUploadedInfo.fromJson(json.decode(uploaded.info)); 23 | } catch (e) {} 24 | if (info != null) { 25 | String realUrl = 26 | path.joinAll(['repos', info.ownerrepo, 'contents', info.path]); 27 | await GithubApi.deleteContent(realUrl, { 28 | "message": DELETE_COMMIT_MESSAGE, 29 | "sha": info.sha, 30 | "branch": info.branch 31 | }); 32 | } 33 | return uploaded; 34 | } 35 | 36 | @override 37 | Future upload(File file, String renameImage) async { 38 | try { 39 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.github); 40 | if (!isBlank(configStr)) { 41 | GithubConfig config = GithubConfig.fromJson(json.decode(configStr)); 42 | String realUrl = path.joinAll( 43 | ['repos', config.repo, 'contents', config.path, renameImage]); 44 | var result = await GithubApi.putContent(realUrl, { 45 | "message": UPLOAD_COMMIT_MESSAGE, 46 | "content": await EncryptUtils.image2Base64(file.path), 47 | "branch": config.branch 48 | }); 49 | String imagePath = result["content"]["path"]; 50 | String downloadUrl = result["content"]["download_url"]; 51 | String sha = result["content"]["sha"]; 52 | String imageUrl = isBlank(config.customUrl) 53 | ? downloadUrl 54 | : '${path.joinAll([config.customUrl, imagePath])}'; 55 | var uploadedItem = Uploaded(-1, imageUrl, PBTypeKeys.github, 56 | info: json.encode(GithubUploadedInfo( 57 | path: imagePath, 58 | sha: sha, 59 | branch: config.branch, 60 | ownerrepo: config.repo))); 61 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 62 | return uploadedItem; 63 | } else { 64 | throw GithubError(error: '读取配置文件错误!请重试'); 65 | } 66 | } on DioError catch (e) { 67 | if (e.type == DioErrorType.response && 68 | e.error.toString().indexOf('422') > 0) { 69 | throw GithubError(error: '文件已存在!'); 70 | } else { 71 | throw e; 72 | } 73 | } 74 | } 75 | } 76 | 77 | /// GithubError describes the error info when request failed. 78 | class GithubError implements Exception { 79 | GithubError({ 80 | this.error, 81 | }); 82 | 83 | dynamic error; 84 | 85 | String get message => (error?.toString() ?? ''); 86 | 87 | @override 88 | String toString() { 89 | var msg = 'GithubError $message'; 90 | if (error is Error) { 91 | msg += '\n${error.stackTrace}'; 92 | } 93 | return msg; 94 | } 95 | } 96 | 97 | class GithubUploadedInfo { 98 | String sha; 99 | String branch; 100 | String path; 101 | String ownerrepo; 102 | 103 | GithubUploadedInfo({this.sha, this.branch, this.path, this.ownerrepo}); 104 | 105 | GithubUploadedInfo.fromJson(Map json) { 106 | sha = json['sha']; 107 | branch = json['branch']; 108 | path = json['path']; 109 | ownerrepo = json['ownerrepo']; 110 | } 111 | 112 | Map toJson() { 113 | final Map data = new Map(); 114 | data['sha'] = this.sha; 115 | data['branch'] = this.branch; 116 | data['path'] = this.path; 117 | data['ownerrepo'] = this.ownerrepo; 118 | return data; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/aliyun_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_picgo/api/aliyun_api.dart'; 5 | import 'package:flutter_picgo/model/aliyun_config.dart'; 6 | import 'package:flutter_picgo/model/uploaded.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/utils/image_upload.dart'; 9 | import 'dart:io'; 10 | import 'package:path/path.dart' as path; 11 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 12 | import 'package:flutter_picgo/utils/strings.dart'; 13 | 14 | class AliyunImageUpload implements ImageUploadStrategy { 15 | @override 16 | Future delete(Uploaded uploaded) async { 17 | AliyunUploadedInfo info; 18 | try { 19 | info = AliyunUploadedInfo.fromJson(json.decode(uploaded.info)); 20 | } catch (e) {} 21 | if (info != null) { 22 | String auth = AliyunApi.buildSignature(info.accessKeyId, 23 | info.accessKeySecret, 'delete', info.bucket, info.object); 24 | await AliyunApi.deleteObject(info.bucket, info.area, info.object, auth); 25 | } 26 | return uploaded; 27 | } 28 | 29 | @override 30 | Future upload(File file, String renameImage) async { 31 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.aliyun); 32 | if (isBlank(configStr)) { 33 | throw AliyunError(error: '读取配置文件错误!请重试'); 34 | } 35 | AliyunConfig config = AliyunConfig.fromJson(json.decode(configStr)); 36 | String objectPath = isBlank(config.path) 37 | ? renameImage 38 | : path.joinAll([config.path, renameImage]); 39 | String policy = AliyunApi.buildEncodePolicy(objectPath); 40 | await AliyunApi.postObject( 41 | config.bucket, 42 | config.area, 43 | FormData.fromMap({ 44 | 'key': objectPath, 45 | 'OSSAccessKeyId': config.accessKeyId, 46 | 'policy': policy, 47 | 'Signature': AliyunApi.buildPostSignature( 48 | config.accessKeyId, config.accessKeySecret, policy), 49 | 'file': 50 | await MultipartFile.fromFile(file.path, filename: renameImage), 51 | // OSS支持用户在Post请求体中增加x-oss-content-type,该项允许用户指定Content-Type 52 | 'x-oss-content-type': 53 | 'image/${path.extension(renameImage).replaceFirst('.', '')}' 54 | })); 55 | String imgPath = path.joinAll([ 56 | isBlank(config.customUrl) 57 | ? 'https://${config.bucket}.${config.area}.aliyuncs.com' 58 | : config.customUrl, 59 | objectPath 60 | ]); 61 | var uploadedItem = Uploaded( 62 | -1, '$imgPath${config.options ?? ''}', PBTypeKeys.aliyun, 63 | info: json.encode(AliyunUploadedInfo( 64 | accessKeyId: config.accessKeyId, 65 | accessKeySecret: config.accessKeySecret, 66 | area: config.area, 67 | bucket: config.bucket, 68 | object: objectPath))); 69 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 70 | return uploadedItem; 71 | } 72 | } 73 | 74 | class AliyunError implements Exception { 75 | AliyunError({ 76 | this.error, 77 | }); 78 | 79 | dynamic error; 80 | 81 | String get message => (error?.toString() ?? ''); 82 | 83 | @override 84 | String toString() { 85 | var msg = 'AliyunError $message'; 86 | if (error is Error) { 87 | msg += '\n${error.stackTrace}'; 88 | } 89 | return msg; 90 | } 91 | } 92 | 93 | class AliyunUploadedInfo { 94 | String accessKeyId; 95 | String accessKeySecret; 96 | String area; 97 | String bucket; 98 | String object; 99 | 100 | AliyunUploadedInfo( 101 | {this.accessKeyId, 102 | this.accessKeySecret, 103 | this.area, 104 | this.bucket, 105 | this.object}); 106 | 107 | AliyunUploadedInfo.fromJson(Map json) { 108 | accessKeyId = json['accessKeyId']; 109 | accessKeySecret = json['accessKeySecret']; 110 | area = json['area']; 111 | bucket = json['bucket']; 112 | object = json['object']; 113 | } 114 | 115 | Map toJson() { 116 | final Map data = new Map(); 117 | data['accessKeyId'] = this.accessKeyId; 118 | data['accessKeySecret'] = this.accessKeySecret; 119 | data['area'] = this.area; 120 | data['bucket'] = this.bucket; 121 | data['object'] = this.object; 122 | return data; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/utils/strategy/impl/gitee_image_upload.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_picgo/api/gitee_api.dart'; 5 | import 'package:flutter_picgo/model/gitee_config.dart'; 6 | import 'package:flutter_picgo/model/uploaded.dart'; 7 | import 'package:flutter_picgo/resources/pb_type_keys.dart'; 8 | import 'package:flutter_picgo/utils/encrypt.dart'; 9 | import 'package:flutter_picgo/utils/image_upload.dart'; 10 | import 'package:flutter_picgo/utils/strings.dart'; 11 | import 'dart:io'; 12 | 13 | import 'package:path/path.dart' as path; 14 | import 'package:flutter_picgo/utils/strategy/image_upload_strategy.dart'; 15 | 16 | class GiteeImageUpload implements ImageUploadStrategy { 17 | static const UPLOAD_COMMIT_MESSAGE = "Upload by Flutter-PicGo"; 18 | static const DELETE_COMMIT_MESSAGE = "Delete by Flutter-PicGo"; 19 | 20 | @override 21 | Future delete(Uploaded uploaded) async { 22 | GiteeUploadedInfo info; 23 | try { 24 | info = GiteeUploadedInfo.fromJson(json.decode(uploaded.info)); 25 | } catch (e) {} 26 | if (info != null) { 27 | String realUrl = 28 | path.joinAll(['repos', info.ownerrepo, 'contents', info.path]); 29 | Map mapData = { 30 | "message": DELETE_COMMIT_MESSAGE, 31 | "sha": info.sha, 32 | }; 33 | if (!isBlank(info.branch)) { 34 | mapData["branch"] = info.branch; 35 | } 36 | await GiteeApi.deleteFile(realUrl, mapData); 37 | } 38 | return uploaded; 39 | } 40 | 41 | @override 42 | Future upload(File file, String renameImage) async { 43 | try { 44 | String configStr = await ImageUploadUtils.getPBConfig(PBTypeKeys.gitee); 45 | if (isBlank(configStr)) { 46 | throw GiteeError(error: '读取配置文件错误!请重试'); 47 | } 48 | GiteeConfig config = GiteeConfig.fromJson(json.decode(configStr)); 49 | String realUrl = path.joinAll([ 50 | 'repos', 51 | config.owner, 52 | config.repo, 53 | 'contents', 54 | config.path, 55 | renameImage 56 | ]); 57 | Map mapData = { 58 | "message": UPLOAD_COMMIT_MESSAGE, 59 | "content": await EncryptUtils.image2Base64(file.path) 60 | }; 61 | if (!isBlank(config.branch)) { 62 | mapData["branch"] = config.branch; 63 | } 64 | var result = await GiteeApi.createFile(realUrl, mapData); 65 | String imagePath = result["content"]["path"]; 66 | String downloadUrl = result["content"]["download_url"]; 67 | String sha = result["content"]["sha"]; 68 | String imageUrl = config.customUrl == null || config.customUrl == '' 69 | ? downloadUrl 70 | : '${path.joinAll([config.customUrl, imagePath])}'; 71 | var uploadedItem = Uploaded(-1, imageUrl, PBTypeKeys.gitee, 72 | info: json.encode(GiteeUploadedInfo( 73 | path: imagePath, 74 | sha: sha, 75 | branch: config.branch, 76 | ownerrepo: '${config.owner}/${config.repo}'))); 77 | await ImageUploadUtils.saveUploadedItem(uploadedItem); 78 | return uploadedItem; 79 | } on DioError catch (e) { 80 | if (e.type == DioErrorType.response && 81 | e.error.toString().indexOf('400') > 0) { 82 | throw GiteeError(error: '文件已存在!'); 83 | } else { 84 | throw e; 85 | } 86 | } 87 | } 88 | } 89 | 90 | /// GithubError describes the error info when request failed. 91 | class GiteeError implements Exception { 92 | GiteeError({ 93 | this.error, 94 | }); 95 | 96 | dynamic error; 97 | 98 | String get message => (error?.toString() ?? ''); 99 | 100 | @override 101 | String toString() { 102 | var msg = 'GithubError $message'; 103 | if (error is Error) { 104 | msg += '\n${error.stackTrace}'; 105 | } 106 | return msg; 107 | } 108 | } 109 | 110 | class GiteeUploadedInfo { 111 | String sha; 112 | String branch; 113 | String path; 114 | String ownerrepo; 115 | 116 | GiteeUploadedInfo({this.sha, this.branch, this.path, this.ownerrepo}); 117 | 118 | GiteeUploadedInfo.fromJson(Map json) { 119 | sha = json['sha']; 120 | branch = json['branch']; 121 | path = json['path']; 122 | ownerrepo = json['ownerrepo']; 123 | } 124 | 125 | Map toJson() { 126 | final Map data = new Map(); 127 | data['sha'] = this.sha; 128 | data['branch'] = this.branch; 129 | data['path'] = this.path; 130 | data['ownerrepo'] = this.ownerrepo; 131 | return data; 132 | } 133 | } 134 | --------------------------------------------------------------------------------