├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── check.yml │ └── release.yml ├── .gitignore ├── .metadata ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── android ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── app │ ├── .classpath │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── stefanji │ │ │ └── fluttergitlab │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ ├── launch_background.xml │ │ └── screen.png │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── string.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── key.jks ├── key.properties └── settings.gradle ├── art ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── F4Lab_arch.png ├── f4lab_home.png └── logo.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ ├── Flutter.podspec │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-20x20@1x.png │ │ ├── Icon-20x20@2x.png │ │ ├── Icon-20x20@3x.png │ │ ├── Icon-29x29@1x.png │ │ ├── Icon-29x29@2x.png │ │ ├── Icon-29x29@3x.png │ │ ├── Icon-40x40@2x.png │ │ ├── Icon-40x40@3x.png │ │ ├── Icon-60x60@2x.png │ │ ├── Icon-60x60@3x.png │ │ ├── Icon-76x76@1x.png │ │ ├── Icon-76x76@2x.png │ │ ├── Icon-83.5@2x.png │ │ └── Icon-marketing-1024x1024.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── api.dart ├── const.dart ├── gitlab_client.dart ├── main.dart ├── main_dev.dart ├── model │ ├── approvals.dart │ ├── commit.dart │ ├── diff.dart │ ├── discussion.dart │ ├── group.dart │ ├── jobs.dart │ ├── merge_request.dart │ ├── pipeline.dart │ ├── project.dart │ ├── runner.dart │ ├── todo.dart │ └── user.dart ├── providers │ ├── package_info.dart │ ├── theme.dart │ └── user.dart ├── ui │ ├── activity │ │ └── activity_tab.dart │ ├── config │ │ └── config_page.dart │ ├── group │ │ └── groups_tab.dart │ ├── home_nav.dart │ ├── home_page.dart │ ├── project │ │ ├── jobs │ │ │ └── jobs_tab.dart │ │ ├── mr │ │ │ ├── approve.dart │ │ │ ├── commit_diff.dart │ │ │ ├── diff.dart │ │ │ ├── merge_request_action.dart │ │ │ ├── mr_detail_tabs.dart │ │ │ ├── mr_home.dart │ │ │ ├── mr_list.dart │ │ │ ├── mr_list_item.dart │ │ │ └── mr_tab_jobs.dart │ │ ├── project_detail.dart │ │ └── project_tabs.dart │ └── todo │ │ └── todo_tab.dart ├── user_helper.dart ├── util │ ├── date_util.dart │ ├── exception_capture.dart │ └── widget_util.dart └── widget │ └── comm_ListView.dart ├── pubspec.yaml ├── script ├── run_dev.sh └── upload-apk.sh └── test └── util └── date_util_test.dart /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: stefanJi 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. Pixel3] 28 | - OS: [e.g. Android 7.0] 29 | - Version [e.g. 1.6.1] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: stefanJi 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: F4LabCI 2 | on: pull_request 3 | 4 | jobs: 5 | test: 6 | name: Test on ${{ matrix.os }} 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, windows-latest, macos-latest] 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-java@v1 14 | with: 15 | java-version: "12.x" 16 | - uses: subosito/flutter-action@v1 17 | with: 18 | # same with pubspec.yaml 19 | flutter-version: "1.12.13+hotfix.9" 20 | channel: "stable" 21 | - run: flutter pub get 22 | - run: flutter analyze --no-pub --no-current-package lib/ test/ 23 | - run: flutter test --no-pub test/ 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: F4LabCIRelease 2 | on: 3 | push: 4 | tags: 5 | - "release-v*" 6 | 7 | jobs: 8 | release-to-gitHub: 9 | name: release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-java@v1 14 | with: 15 | java-version: "12.x" 16 | - uses: subosito/flutter-action@v1 17 | with: 18 | # same with pubspec.yaml 19 | flutter-version: "1.12.13+hotfix.9" 20 | channel: "stable" 21 | - run: flutter pub get 22 | - run: flutter pub deps 23 | - run: flutter analyze --no-pub --no-current-package lib/ test/ 24 | - run: flutter test --no-pub test/ 25 | - run: flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi 26 | - uses: softprops/action-gh-release@v1 27 | with: 28 | files: build/app/outputs/apk/release/*.apk 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/settings.json 21 | 22 | # Flutter repo-specific 23 | /bin/cache/ 24 | /bin/mingit/ 25 | /dev/benchmarks/mega_gallery/ 26 | /dev/bots/.recipe_deps 27 | /dev/bots/android_tools/ 28 | /dev/docs/doc/ 29 | /dev/docs/flutter.docs.zip 30 | /dev/docs/lib/ 31 | /dev/docs/pubspec.yaml 32 | /dev/integration_tests/**/xcuserdata 33 | /dev/integration_tests/**/Pods 34 | /packages/flutter/coverage/ 35 | version 36 | 37 | # packages file containing multi-root paths 38 | .packages.generated 39 | 40 | # Flutter/Dart/Pub related 41 | **/doc/api/ 42 | .dart_tool/ 43 | .flutter-plugins 44 | .packages 45 | .pub-cache/ 46 | .pub/ 47 | build/ 48 | flutter_*.png 49 | linked_*.ds 50 | unlinked.ds 51 | unlinked_spec.ds 52 | 53 | # Android related 54 | **/android/**/gradle-wrapper.jar 55 | **/android/.gradle 56 | **/android/captures/ 57 | **/android/gradlew 58 | **/android/gradlew.bat 59 | **/android/local.properties 60 | **/android/**/GeneratedPluginRegistrant.java 61 | **/android/key.properties 62 | *.jks 63 | 64 | # iOS/XCode related 65 | **/ios/**/*.mode1v3 66 | **/ios/**/*.mode2v3 67 | **/ios/**/*.moved-aside 68 | **/ios/**/*.pbxuser 69 | **/ios/**/*.perspectivev3 70 | **/ios/**/*sync/ 71 | **/ios/**/.sconsign.dblite 72 | **/ios/**/.tags* 73 | **/ios/**/.vagrant/ 74 | **/ios/**/DerivedData/ 75 | **/ios/**/Icon? 76 | **/ios/**/Pods/ 77 | **/ios/**/.symlinks/ 78 | **/ios/**/profile 79 | **/ios/**/xcuserdata 80 | **/ios/.generated/ 81 | **/ios/Flutter/App.framework 82 | **/ios/Flutter/Flutter.framework 83 | **/ios/Flutter/Generated.xcconfig 84 | **/ios/Flutter/app.flx 85 | **/ios/Flutter/app.zip 86 | **/ios/Flutter/flutter_assets/ 87 | **/ios/Flutter/flutter_export_environment.sh 88 | **/ios/ServiceDefinitions.json 89 | **/ios/Runner/GeneratedPluginRegistrant.* 90 | 91 | # Coverage 92 | coverage/ 93 | 94 | # Exceptions to above rules. 95 | !**/ios/**/default.mode1v3 96 | !**/ios/**/default.mode2v3 97 | !**/ios/**/default.pbxuser 98 | !**/ios/**/default.perspectivev3 99 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 100 | 101 | .flutter-plugins 102 | .flutter-plugins-dependencies -------------------------------------------------------------------------------- /.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: 6a3ff018b199a7febbe2b5adbb564081d8f49e2f 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Flutter4GitlabDev", 9 | "type": "dart", 10 | "request": "launch", 11 | "program": "lib/main_dev.dart" 12 | }, 13 | { 14 | "name": "Flutter4Gitlab", 15 | "request": "launch", 16 | "type": "dart" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 StefanJi 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # F4Lab 4 | 5 | *A glitlab client made by flutter. Support Android & IOS.* 6 | 7 | 8 | [![](https://github.com/stefanJi/Flutter4GitLab/workflows/F4LabCI/badge.svg)](https://github.com/stefanJi/Flutter4GitLab/actions) 9 | 10 | 11 | 12 | |home|config|nav|project|merge requests| merge request|commit|diff| 13 | |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| 14 | |![](./art/f4lab_home.png)| ![](./art/8.png)| ![](./art/1.png) | ![](./art/2.png) | ![](./art/3.png) | ![](./art/4.png) | ![](./art/5.png) | ![](./art/6.png) | 15 | 16 | ## Usage 17 | 18 | ### Running in Android device 19 | 20 | 1. [Download release apk](https://github.com/stefanJi/Flutter4GitLab/releases) 21 | 2. Install apk and then run 22 | 23 | ### Running in IOS device 24 | 25 | 1. Follow [Dev](#Dev) Section 26 | 27 | ## Dev 28 | 29 | > First, you shuold setup your Flutter development env. [Set up an editor](https://flutter.io/docs/get-started/editor). 30 | 31 | ### Run Project 32 | 33 | 1. `fork` or `clone` this project 34 | 2. In project root dir, run: 35 | - `flutter packages pub get` 36 | - `flutter run` 37 | 38 | --- 39 | 40 |
41 | Features: welcome to contribute for the following features 42 | 43 | - **App** 44 | - [x] Login by Personal Access Token 45 | - [x] Projects 46 | - [x] Themes mode 47 | - [ ] Markdown and code highlighting support 48 | - [ ] Search Users/Orgs, Repos, Issues/MRs & Code. 49 | - **Repositories** 50 | - [ ] Search Repos 51 | - [ ] Browse and search Repos 52 | - [x] See your public, private and forked Repos 53 | - [ ] Filter Branches and Commits 54 | - **Issues and Merge Requests** 55 | - [x] Commit code diff 56 | - [x] Run pipeline jobs 57 | - [x] Rebase when merge request 58 | - [x] Merge MRs 59 | - [x] MRs statuses 60 | - [x] Approve or UnApprove MR 61 | - [x] CI Status 62 | - [x] Play|Cancel|Retry CI Job 63 | - [x] Filter Merge Requests State. (opened, closed, locked, merged) 64 | - [x] Filter Merge Requests Assign. (all, assigned_to_me) 65 | - [x] Discussion of merge request 66 | - **Organisations** 67 | - [x] Feeds 68 | - [x] Repos 69 | - **PipeLines** 70 | - [x] List project's pipepine 71 | - [x] Play, Retry, Cancel Pipeline Job 72 | 73 |
74 | 75 |
76 | Api: GitLab Server || Local Server 77 | 78 | - [**GitLab Api Doc**](https://gitlab.com/help/api/README.md) 79 | - Or Your personal GitLab.(Eg: https://gitlab.exsample.com/help/api/README.md) 80 | 81 |
82 | 83 |
84 | Dependencies: Lib && Plugin 85 | 86 | - Android Minimum **SDK 16**, IOS Minimun **9.0** 87 | - [**Flutter**](https://github.com/flutter/flutter) 88 | - [**shared_preferences**](https://pub.dartlang.org/packages/shared_preferences) 89 | - [**pull_to_refresh**](https://pub.dartlang.org/packages/pull_to_refresh) 90 | - [**xml**](https://pub.dartlang.org/packages/xml) 91 | - [**url_launcher**](https://pub.dartlang.org/packages/url_launcher) 92 | - [**sentry**](https://pub.dartlang.org/packages/sentry) 93 | - [**flutter_stetho**](https://pub.dartlang.org/packages/flutter_stetho) 94 | - [**Dio**](https://github.com/flutterchina/dio) 95 | - [**Provider**](https://github.com/rrousselGit/provider) 96 | 97 |
98 | 99 | ## Contribution 100 | 101 | Please **contribute** to the project either by **_creating a PR_** or **_submitting an issue_** on GitHub. 102 | 103 | ## License 104 | 105 | > Copyright (C) 2018 StefanJi. 106 | > (See the [LICENSE](./LICENSE) file for the whole license text.) 107 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /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 | def versionSplit = flutterVersionName.split("\\.") 25 | flutterVersionCode = 0 26 | versionSplit.each({ s -> 27 | flutterVersionCode += Integer.valueOf(s) 28 | }) 29 | println "version name: $flutterVersionName" 30 | println "base version code: $flutterVersionCode" 31 | println "armeabi-v7a version code: ${1000 + flutterVersionCode}" 32 | println "arm64_v8a version code: ${2000 + flutterVersionCode}" 33 | println "x86_64 version code: ${4000 + flutterVersionCode}" 34 | 35 | 36 | apply plugin: 'com.android.application' 37 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 38 | 39 | def keystorePropertiesFile = rootProject.file("key.properties") 40 | def keystoreProperties = new Properties() 41 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 42 | 43 | android { 44 | compileSdkVersion 29 45 | buildToolsVersion "28.0.3" 46 | 47 | lintOptions { 48 | disable 'InvalidPackage' 49 | } 50 | 51 | defaultConfig { 52 | applicationId "io.github.stefanji.fluttergitlab" 53 | minSdkVersion 16 54 | targetSdkVersion 29 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 58 | } 59 | 60 | signingConfigs { 61 | release { 62 | keyAlias keystoreProperties['keyAlias'] 63 | keyPassword keystoreProperties['keyPassword'] 64 | storeFile file(keystoreProperties['storeFile']) 65 | storePassword keystoreProperties['storePassword'] 66 | } 67 | } 68 | buildTypes { 69 | release { 70 | shrinkResources true 71 | signingConfig signingConfigs.release 72 | minifyEnabled true 73 | 74 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 75 | } 76 | } 77 | } 78 | 79 | flutter { 80 | source '../..' 81 | } 82 | 83 | dependencies { 84 | testImplementation 'junit:junit:4.12' 85 | androidTestImplementation 'androidx.test:runner:1.2.0' 86 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 87 | } 88 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | #Flutter Wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/github/stefanji/fluttergitlab/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.stefanji.fluttergitlab; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/app/src/main/res/drawable/screen.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/string.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | F4Lab 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.6.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 18 18:24:56 CST 2020 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.4-all.zip 7 | -------------------------------------------------------------------------------- /android/key.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/android/key.jks -------------------------------------------------------------------------------- /android/key.properties: -------------------------------------------------------------------------------- 1 | storePassword=123456 2 | keyPassword=123456 3 | keyAlias=key 4 | storeFile=../key.jks -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /art/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/1.png -------------------------------------------------------------------------------- /art/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/2.png -------------------------------------------------------------------------------- /art/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/3.png -------------------------------------------------------------------------------- /art/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/4.png -------------------------------------------------------------------------------- /art/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/5.png -------------------------------------------------------------------------------- /art/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/6.png -------------------------------------------------------------------------------- /art/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/7.png -------------------------------------------------------------------------------- /art/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/8.png -------------------------------------------------------------------------------- /art/F4Lab_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/F4Lab_arch.png -------------------------------------------------------------------------------- /art/f4lab_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/f4lab_home.png -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/art/logo.png -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 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 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'High-performance, high-fidelity mobile apps.' 10 | s.homepage = 'https://flutter.io' 11 | s.license = { :type => 'MIT' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '8.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /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 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 2E41D7365257AE2408546E22 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01511DF71B2CE60B6E96581F /* libPods-Runner.a */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 14 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 15 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 16 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 17 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 18 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXCopyFilesBuildPhase section */ 22 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 23 | isa = PBXCopyFilesBuildPhase; 24 | buildActionMask = 2147483647; 25 | dstPath = ""; 26 | dstSubfolderSpec = 10; 27 | files = ( 28 | ); 29 | name = "Embed Frameworks"; 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXCopyFilesBuildPhase section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 01511DF71B2CE60B6E96581F /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 37 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 39 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 40 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 41 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 42 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 43 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 44 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 46 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | 9FE6D3E0C5CC1B70AD424743 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 51 | D80B2CEF7C78B73B94072AC5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | 2E41D7365257AE2408546E22 /* libPods-Runner.a in Frameworks */, 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 2FFC179183DE97F5770EDA82 /* Pods */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | 9FE6D3E0C5CC1B70AD424743 /* Pods-Runner.debug.xcconfig */, 70 | D80B2CEF7C78B73B94072AC5 /* Pods-Runner.release.xcconfig */, 71 | ); 72 | name = Pods; 73 | sourceTree = ""; 74 | }; 75 | 9740EEB11CF90186004384FC /* Flutter */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 79 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 80 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 81 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 82 | ); 83 | name = Flutter; 84 | sourceTree = ""; 85 | }; 86 | 97C146E51CF9000F007C117D = { 87 | isa = PBXGroup; 88 | children = ( 89 | 9740EEB11CF90186004384FC /* Flutter */, 90 | 97C146F01CF9000F007C117D /* Runner */, 91 | 97C146EF1CF9000F007C117D /* Products */, 92 | 2FFC179183DE97F5770EDA82 /* Pods */, 93 | ECD79A50260CE789C9BAC617 /* Frameworks */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 97C146EF1CF9000F007C117D /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 97C146EE1CF9000F007C117D /* Runner.app */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 97C146F01CF9000F007C117D /* Runner */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 109 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 110 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 111 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 112 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 113 | 97C147021CF9000F007C117D /* Info.plist */, 114 | 97C146F11CF9000F007C117D /* Supporting Files */, 115 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 116 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 117 | ); 118 | path = Runner; 119 | sourceTree = ""; 120 | }; 121 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 97C146F21CF9000F007C117D /* main.m */, 125 | ); 126 | name = "Supporting Files"; 127 | sourceTree = ""; 128 | }; 129 | ECD79A50260CE789C9BAC617 /* Frameworks */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 01511DF71B2CE60B6E96581F /* libPods-Runner.a */, 133 | ); 134 | name = Frameworks; 135 | sourceTree = ""; 136 | }; 137 | /* End PBXGroup section */ 138 | 139 | /* Begin PBXNativeTarget section */ 140 | 97C146ED1CF9000F007C117D /* Runner */ = { 141 | isa = PBXNativeTarget; 142 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 143 | buildPhases = ( 144 | 646E3F5D8122DEB262BD8D57 /* [CP] Check Pods Manifest.lock */, 145 | 9740EEB61CF901F6004384FC /* Run Script */, 146 | 97C146EA1CF9000F007C117D /* Sources */, 147 | 97C146EB1CF9000F007C117D /* Frameworks */, 148 | 97C146EC1CF9000F007C117D /* Resources */, 149 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 150 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 151 | ); 152 | buildRules = ( 153 | ); 154 | dependencies = ( 155 | ); 156 | name = Runner; 157 | productName = Runner; 158 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 159 | productType = "com.apple.product-type.application"; 160 | }; 161 | /* End PBXNativeTarget section */ 162 | 163 | /* Begin PBXProject section */ 164 | 97C146E61CF9000F007C117D /* Project object */ = { 165 | isa = PBXProject; 166 | attributes = { 167 | LastUpgradeCheck = 0910; 168 | ORGANIZATIONNAME = "The Chromium Authors"; 169 | TargetAttributes = { 170 | 97C146ED1CF9000F007C117D = { 171 | CreatedOnToolsVersion = 7.3.1; 172 | }; 173 | }; 174 | }; 175 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 176 | compatibilityVersion = "Xcode 3.2"; 177 | developmentRegion = English; 178 | hasScannedForEncodings = 0; 179 | knownRegions = ( 180 | en, 181 | Base, 182 | ); 183 | mainGroup = 97C146E51CF9000F007C117D; 184 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 185 | projectDirPath = ""; 186 | projectRoot = ""; 187 | targets = ( 188 | 97C146ED1CF9000F007C117D /* Runner */, 189 | ); 190 | }; 191 | /* End PBXProject section */ 192 | 193 | /* Begin PBXResourcesBuildPhase section */ 194 | 97C146EC1CF9000F007C117D /* Resources */ = { 195 | isa = PBXResourcesBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 199 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 200 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 201 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 202 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXResourcesBuildPhase section */ 207 | 208 | /* Begin PBXShellScriptBuildPhase section */ 209 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 210 | isa = PBXShellScriptBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | ); 214 | inputPaths = ( 215 | ); 216 | name = "Thin Binary"; 217 | outputPaths = ( 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | shellPath = /bin/sh; 221 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 222 | }; 223 | 646E3F5D8122DEB262BD8D57 /* [CP] Check Pods Manifest.lock */ = { 224 | isa = PBXShellScriptBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | inputPaths = ( 229 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 230 | "${PODS_ROOT}/Manifest.lock", 231 | ); 232 | name = "[CP] Check Pods Manifest.lock"; 233 | outputPaths = ( 234 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | shellPath = /bin/sh; 238 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 239 | showEnvVarsInLog = 0; 240 | }; 241 | 9740EEB61CF901F6004384FC /* Run Script */ = { 242 | isa = PBXShellScriptBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | ); 246 | inputPaths = ( 247 | ); 248 | name = "Run Script"; 249 | outputPaths = ( 250 | ); 251 | runOnlyForDeploymentPostprocessing = 0; 252 | shellPath = /bin/sh; 253 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 254 | }; 255 | /* End PBXShellScriptBuildPhase section */ 256 | 257 | /* Begin PBXSourcesBuildPhase section */ 258 | 97C146EA1CF9000F007C117D /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 263 | 97C146F31CF9000F007C117D /* main.m in Sources */, 264 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXSourcesBuildPhase section */ 269 | 270 | /* Begin PBXVariantGroup section */ 271 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 272 | isa = PBXVariantGroup; 273 | children = ( 274 | 97C146FB1CF9000F007C117D /* Base */, 275 | ); 276 | name = Main.storyboard; 277 | sourceTree = ""; 278 | }; 279 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 280 | isa = PBXVariantGroup; 281 | children = ( 282 | 97C147001CF9000F007C117D /* Base */, 283 | ); 284 | name = LaunchScreen.storyboard; 285 | sourceTree = ""; 286 | }; 287 | /* End PBXVariantGroup section */ 288 | 289 | /* Begin XCBuildConfiguration section */ 290 | 97C147031CF9000F007C117D /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ALWAYS_SEARCH_USER_PATHS = NO; 294 | CLANG_ANALYZER_NONNULL = YES; 295 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 296 | CLANG_CXX_LIBRARY = "libc++"; 297 | CLANG_ENABLE_MODULES = YES; 298 | CLANG_ENABLE_OBJC_ARC = YES; 299 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 300 | CLANG_WARN_BOOL_CONVERSION = YES; 301 | CLANG_WARN_COMMA = YES; 302 | CLANG_WARN_CONSTANT_CONVERSION = YES; 303 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 304 | CLANG_WARN_EMPTY_BODY = YES; 305 | CLANG_WARN_ENUM_CONVERSION = YES; 306 | CLANG_WARN_INFINITE_RECURSION = YES; 307 | CLANG_WARN_INT_CONVERSION = YES; 308 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 309 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 310 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 311 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 312 | CLANG_WARN_STRICT_PROTOTYPES = YES; 313 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 314 | CLANG_WARN_UNREACHABLE_CODE = YES; 315 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 316 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 317 | COPY_PHASE_STRIP = NO; 318 | DEBUG_INFORMATION_FORMAT = dwarf; 319 | ENABLE_STRICT_OBJC_MSGSEND = YES; 320 | ENABLE_TESTABILITY = YES; 321 | GCC_C_LANGUAGE_STANDARD = gnu99; 322 | GCC_DYNAMIC_NO_PIC = NO; 323 | GCC_NO_COMMON_BLOCKS = YES; 324 | GCC_OPTIMIZATION_LEVEL = 0; 325 | GCC_PREPROCESSOR_DEFINITIONS = ( 326 | "DEBUG=1", 327 | "$(inherited)", 328 | ); 329 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 330 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 331 | GCC_WARN_UNDECLARED_SELECTOR = YES; 332 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 333 | GCC_WARN_UNUSED_FUNCTION = YES; 334 | GCC_WARN_UNUSED_VARIABLE = YES; 335 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 336 | MTL_ENABLE_DEBUG_INFO = YES; 337 | ONLY_ACTIVE_ARCH = YES; 338 | SDKROOT = iphoneos; 339 | TARGETED_DEVICE_FAMILY = "1,2"; 340 | }; 341 | name = Debug; 342 | }; 343 | 97C147041CF9000F007C117D /* Release */ = { 344 | isa = XCBuildConfiguration; 345 | buildSettings = { 346 | ALWAYS_SEARCH_USER_PATHS = NO; 347 | CLANG_ANALYZER_NONNULL = YES; 348 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 349 | CLANG_CXX_LIBRARY = "libc++"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 353 | CLANG_WARN_BOOL_CONVERSION = YES; 354 | CLANG_WARN_COMMA = YES; 355 | CLANG_WARN_CONSTANT_CONVERSION = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_EMPTY_BODY = YES; 358 | CLANG_WARN_ENUM_CONVERSION = YES; 359 | CLANG_WARN_INFINITE_RECURSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 363 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 364 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 365 | CLANG_WARN_STRICT_PROTOTYPES = YES; 366 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 367 | CLANG_WARN_UNREACHABLE_CODE = YES; 368 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 369 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 370 | COPY_PHASE_STRIP = NO; 371 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 372 | ENABLE_NS_ASSERTIONS = NO; 373 | ENABLE_STRICT_OBJC_MSGSEND = YES; 374 | GCC_C_LANGUAGE_STANDARD = gnu99; 375 | GCC_NO_COMMON_BLOCKS = YES; 376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 377 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 378 | GCC_WARN_UNDECLARED_SELECTOR = YES; 379 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 380 | GCC_WARN_UNUSED_FUNCTION = YES; 381 | GCC_WARN_UNUSED_VARIABLE = YES; 382 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 383 | MTL_ENABLE_DEBUG_INFO = NO; 384 | SDKROOT = iphoneos; 385 | TARGETED_DEVICE_FAMILY = "1,2"; 386 | VALIDATE_PRODUCT = YES; 387 | }; 388 | name = Release; 389 | }; 390 | 97C147061CF9000F007C117D /* Debug */ = { 391 | isa = XCBuildConfiguration; 392 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 396 | ENABLE_BITCODE = NO; 397 | FRAMEWORK_SEARCH_PATHS = ( 398 | "$(inherited)", 399 | "$(PROJECT_DIR)/Flutter", 400 | ); 401 | INFOPLIST_FILE = Runner/Info.plist; 402 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 403 | LIBRARY_SEARCH_PATHS = ( 404 | "$(inherited)", 405 | "$(PROJECT_DIR)/Flutter", 406 | ); 407 | PRODUCT_BUNDLE_IDENTIFIER = io.github.stefanji.flutterGitlab; 408 | PRODUCT_NAME = "$(TARGET_NAME)"; 409 | VERSIONING_SYSTEM = "apple-generic"; 410 | }; 411 | name = Debug; 412 | }; 413 | 97C147071CF9000F007C117D /* Release */ = { 414 | isa = XCBuildConfiguration; 415 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 416 | buildSettings = { 417 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 418 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 419 | ENABLE_BITCODE = NO; 420 | FRAMEWORK_SEARCH_PATHS = ( 421 | "$(inherited)", 422 | "$(PROJECT_DIR)/Flutter", 423 | ); 424 | INFOPLIST_FILE = Runner/Info.plist; 425 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 426 | LIBRARY_SEARCH_PATHS = ( 427 | "$(inherited)", 428 | "$(PROJECT_DIR)/Flutter", 429 | ); 430 | PRODUCT_BUNDLE_IDENTIFIER = io.github.stefanji.flutterGitlab; 431 | PRODUCT_NAME = "$(TARGET_NAME)"; 432 | VERSIONING_SYSTEM = "apple-generic"; 433 | }; 434 | name = Release; 435 | }; 436 | /* End XCBuildConfiguration section */ 437 | 438 | /* Begin XCConfigurationList section */ 439 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 440 | isa = XCConfigurationList; 441 | buildConfigurations = ( 442 | 97C147031CF9000F007C117D /* Debug */, 443 | 97C147041CF9000F007C117D /* Release */, 444 | ); 445 | defaultConfigurationIsVisible = 0; 446 | defaultConfigurationName = Release; 447 | }; 448 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 449 | isa = XCConfigurationList; 450 | buildConfigurations = ( 451 | 97C147061CF9000F007C117D /* Debug */, 452 | 97C147071CF9000F007C117D /* Release */, 453 | ); 454 | defaultConfigurationIsVisible = 0; 455 | defaultConfigurationName = Release; 456 | }; 457 | /* End XCConfigurationList section */ 458 | }; 459 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 460 | } 461 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "ipad", 5 | "size": "20x20", 6 | "scale": "1x", 7 | "filename": "Icon-20x20@1x.png" 8 | }, 9 | { 10 | "idiom": "iphone", 11 | "size": "20x20", 12 | "scale": "2x", 13 | "filename": "Icon-20x20@2x.png" 14 | }, 15 | { 16 | "idiom": "iphone", 17 | "size": "20x20", 18 | "scale": "3x", 19 | "filename": "Icon-20x20@3x.png" 20 | }, 21 | { 22 | "idiom": "iphone", 23 | "size": "29x29", 24 | "scale": "1x", 25 | "filename": "Icon-29x29@1x.png" 26 | }, 27 | { 28 | "idiom": "iphone", 29 | "size": "29x29", 30 | "scale": "2x", 31 | "filename": "Icon-29x29@2x.png" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "size": "29x29", 36 | "scale": "3x", 37 | "filename": "Icon-29x29@3x.png" 38 | }, 39 | { 40 | "idiom": "iphone", 41 | "size": "40x40", 42 | "scale": "2x", 43 | "filename": "Icon-40x40@2x.png" 44 | }, 45 | { 46 | "idiom": "iphone", 47 | "size": "40x40", 48 | "scale": "3x", 49 | "filename": "Icon-40x40@3x.png" 50 | }, 51 | { 52 | "idiom": "iphone", 53 | "size": "60x60", 54 | "scale": "2x", 55 | "filename": "Icon-60x60@2x.png" 56 | }, 57 | { 58 | "idiom": "iphone", 59 | "size": "60x60", 60 | "scale": "3x", 61 | "filename": "Icon-60x60@3x.png" 62 | }, 63 | { 64 | "idiom": "ipad", 65 | "size": "76x76", 66 | "scale": "1x", 67 | "filename": "Icon-76x76@1x.png" 68 | }, 69 | { 70 | "idiom": "ipad", 71 | "size": "76x76", 72 | "scale": "2x", 73 | "filename": "Icon-76x76@2x.png" 74 | }, 75 | { 76 | "idiom": "ipad", 77 | "size": "167x167", 78 | "scale": "2x", 79 | "filename": "Icon-83.5@2x.png" 80 | }, 81 | { 82 | "idiom": "ios-marketing", 83 | "size": "1024x1024", 84 | "scale": "1x", 85 | "filename": "Icon-marketing-1024x1024.png" 86 | } 87 | ], 88 | "info": { 89 | "version": 1, 90 | "author": "apetools.webprofusion.com" 91 | } 92 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-marketing-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-marketing-1024x1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanJi/Flutter4GitLab/d0a245c933742b1d4ce413d46a69e44685027911/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | F4Lab 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:F4Lab/gitlab_client.dart'; 5 | import 'package:F4Lab/model/approvals.dart' hide User; 6 | import 'package:F4Lab/model/diff.dart'; 7 | import 'package:F4Lab/model/jobs.dart'; 8 | import 'package:F4Lab/model/merge_request.dart'; 9 | import 'package:F4Lab/model/user.dart'; 10 | import 'package:flutter/foundation.dart'; 11 | import 'package:http/http.dart'; 12 | 13 | class ApiResp { 14 | bool success; 15 | T? data; 16 | String? err; 17 | 18 | ApiResp(this.success, [this.data, this.err]); 19 | } 20 | 21 | class ApiEndPoint { 22 | static const merge_request_states = ["opened", "closed", "locked", "merged"]; 23 | static const merge_request_scopes = ["all", "assigned_to_me"]; 24 | 25 | /// Get a project's all merge requests with state and scope filter 26 | /// [state] one of [merge_request_states] 27 | /// [scope] one of [merge_request_scopes] 28 | /// 29 | static String mergeRequests(int projectId, 30 | {required String state, required String scope}) => 31 | "projects/$projectId/merge_requests?state=${state ?? "opened"}&scope=${scope ?? "all"}"; 32 | 33 | static String singleMergeRequest(int projectId, int mrIId) => 34 | "projects/$projectId/merge_requests/$mrIId?include_rebase_in_progress=true&include_diverged_commits_count=true"; 35 | 36 | static String mergeRequestCommit(int projectId, int mrIID) => 37 | "projects/$projectId/merge_requests/$mrIID/commits"; 38 | 39 | static String mrApproveData(int projectId, int mrIId) => 40 | 'projects/$projectId/merge_requests/$mrIId/approvals'; 41 | 42 | static String approveMergeRequest(int projectId, int mrIId, bool approve) => 43 | "projects/$projectId/merge_requests/$mrIId/${approve ? "approve" : "unapprove"}"; 44 | 45 | static String rebaseMR(int projectId, int mrIId) => 46 | "projects/$projectId/merge_requests/$mrIId/rebase"; 47 | 48 | static String mergeRequestPipelines(int projectId, int mrIId) => 49 | "projects/$projectId/merge_requests/$mrIId/pipelines"; 50 | 51 | static String pipelineJobs(int projectId, int pipelineId) => 52 | "projects/$projectId/pipelines/$pipelineId/jobs"; 53 | 54 | static String projectJobs(int projectId) => "projects/$projectId/jobs"; 55 | 56 | static String mergeMR( 57 | int projectId, 58 | int mrIId, { 59 | bool shouldRemoveSourceBranch = false, 60 | bool mergeMrWhenPipelineSuccess = false, 61 | bool squash = false, 62 | String mergeCommitMessage = "", 63 | }) => 64 | 'projects/$projectId/merge_requests/$mrIId/merge?' + 65 | 'should_remove_source_branch=$shouldRemoveSourceBranch' + 66 | '&merge_when_pipeline_succeeds=$mergeMrWhenPipelineSuccess' + 67 | '&squash=$squash' + 68 | '$mergeCommitMessage'; 69 | 70 | static String cancelMergeMrWhenPipelineSuccess(int projectId, int mrIId) => 71 | "projects/$projectId/merge_requests/$mrIId/cancel_merge_when_pipeline_succeeds"; 72 | 73 | static String triggerPipelineJob(int projectId, int jobId, String action) => 74 | "projects/$projectId/jobs/$jobId/$action"; 75 | 76 | static String commitDiff(int projectId, String sha) => 77 | "projects/$projectId/repository/commits/$sha/diff"; 78 | 79 | static String mergeRequestDiscussion(int projectId, int mrIId) => 80 | "projects/$projectId/merge_requests/$mrIId/discussions"; 81 | } 82 | 83 | class ApiService { 84 | static bool respStatusIsOk(int statusCode) => (statusCode ~/ 100) == 2; 85 | 86 | static dynamic respConvertToUtf8(Response resp) => 87 | utf8.decode(resp.bodyBytes); 88 | 89 | static Map respConvertToMap(Response resp) { 90 | if (!respStatusIsOk(resp.statusCode)) { 91 | throw Exception("Response error: ${resp.statusCode} ${resp.body}"); 92 | } 93 | return jsonDecode(respConvertToUtf8(resp)); 94 | } 95 | 96 | static List respConvertToList(Response resp) { 97 | if (!respStatusIsOk(resp.statusCode)) { 98 | throw Exception("Response error: ${resp.statusCode} ${resp.body}"); 99 | } 100 | return jsonDecode(respConvertToUtf8(resp)); 101 | } 102 | 103 | static Future> getAuthUser() async { 104 | return GitlabClient.buildDio().get('user').then((resp) { 105 | debugPrint(resp.toString()); 106 | final ok = respStatusIsOk(resp.statusCode!); 107 | return ApiResp(ok, User.fromJson(resp.data)); 108 | }).catchError((err) => ApiResp(false, null, err.toString())); 109 | } 110 | 111 | static Future> getSingleMR(int projectId, int mrIId) { 112 | final endPoint = ApiEndPoint.singleMergeRequest(projectId, mrIId); 113 | final client = GitlabClient.newInstance(); 114 | return client.get(Uri.parse(endPoint)).then((resp) { 115 | return ApiResp(respStatusIsOk(resp.statusCode), 116 | MergeRequest.fromJson(respConvertToMap(resp))); 117 | }).catchError((err) { 118 | return ApiResp(false, null, err?.toString()); 119 | }).whenComplete(client.close); 120 | } 121 | 122 | static Future approve( 123 | int projectId, int mrIId, bool isApprove) async { 124 | final endPoint = 125 | ApiEndPoint.approveMergeRequest(projectId, mrIId, isApprove); 126 | final client = GitlabClient.newInstance(); 127 | return client.post(Uri.parse(endPoint)).then((resp) { 128 | return ApiResp(respStatusIsOk(resp.statusCode), resp.body, 129 | respStatusIsOk(resp.statusCode) ? "" : resp.body); 130 | }).catchError((err) { 131 | return ApiResp(false, null, err?.toString()); 132 | }).whenComplete(client.close); 133 | } 134 | 135 | static Future> rebaseMr(int projectId, int mrIId) { 136 | final endPoint = ApiEndPoint.rebaseMR(projectId, mrIId); 137 | final client = GitlabClient.newInstance(); 138 | return client 139 | .put(Uri.parse(endPoint)) 140 | .then((resp) => ApiResp(respStatusIsOk(resp.statusCode))) 141 | .whenComplete(client.close); 142 | } 143 | 144 | static Future> mrApproveData(int projectId, int mrIId) { 145 | final endPoint = ApiEndPoint.mrApproveData(projectId, mrIId); 146 | final client = GitlabClient.newInstance(); 147 | return client 148 | .get(Uri.parse(endPoint)) 149 | .then((resp) { 150 | return ApiResp(respStatusIsOk(resp.statusCode), 151 | Approvals.fromJson(respConvertToMap(resp))); 152 | }) 153 | .catchError((err) => ApiResp(false, null, err?.toString())) 154 | .whenComplete(client.close); 155 | } 156 | 157 | static Future acceptMR(int projectId, int mrIId, 158 | {bool shouldRemoveSourceBranch = false, 159 | bool mergeMrWhenPipelineSuccess = false, 160 | bool squash = false, 161 | String? mergeCommitMessage}) { 162 | final endPoint = ApiEndPoint.mergeMR(projectId, mrIId, 163 | shouldRemoveSourceBranch: shouldRemoveSourceBranch, 164 | mergeMrWhenPipelineSuccess: mergeMrWhenPipelineSuccess, 165 | mergeCommitMessage: mergeCommitMessage ?? "", 166 | squash: squash); 167 | final client = GitlabClient.newInstance(); 168 | return client.put(Uri.parse(endPoint)).then((resp) { 169 | return ApiResp(respStatusIsOk(resp.statusCode)); 170 | }).catchError((err) { 171 | return ApiResp(false, null, err?.toString()); 172 | }).whenComplete(client.close); 173 | } 174 | 175 | static Future>> pipelineJobs( 176 | int projectId, int pipelineId) { 177 | final endPoint = ApiEndPoint.pipelineJobs(projectId, pipelineId); 178 | return GitlabClient.buildDio() 179 | .get(endPoint) 180 | .then((resp) => 181 | ApiResp(true, resp.data!.map((it) => Jobs.fromJson(it)).toList())) 182 | .catchError((err) => ApiResp>(false, null, err)); 183 | } 184 | 185 | static Future triggerPipelineJob( 186 | int projectId, int jobId, String action) { 187 | final endPoint = ApiEndPoint.triggerPipelineJob(projectId, jobId, action); 188 | final client = GitlabClient.newInstance(); 189 | return client 190 | .post(Uri.parse(endPoint)) 191 | .then((resp) => ApiResp(respStatusIsOk(resp.statusCode))) 192 | .catchError((err) => ApiResp(false, null, err?.toString())) 193 | .whenComplete(client.close); 194 | } 195 | 196 | static Future>> commitDiff(int projectId, String sha) { 197 | final endPoint = ApiEndPoint.commitDiff(projectId, sha); 198 | return GitlabClient.buildDio() 199 | .get(endPoint) 200 | .then((resp) => ApiResp( 201 | true, resp.data!.map((item) => Diff.fromJson(item)).toList())) 202 | .catchError((err) => ApiResp>(false, null, err)); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /lib/const.dart: -------------------------------------------------------------------------------- 1 | const APP_LEGEND = 'A GitLab Client by Flutter.'; 2 | const KEY_ACCESS_TOKEN = 'key.access.token'; 3 | const KEY_HOST = 'key.host'; 4 | const KEY_API_VERSION = 'key.api.version'; 5 | const KEY_TAB_INDEX = 'key.tab.index'; 6 | const APP_ICON_URL = "http://image.youcute.cn/18-11-2/26473177.jpg"; 7 | const APP_REPO_URL = "https://github.com/stefanJi/Flutter4GitLab"; 8 | const APP_FEED_BACK_URL = "https://github.com/stefanJi/Flutter4GitLab/issues"; 9 | const KEY_THEME_IS_DARK = "is_dark"; 10 | -------------------------------------------------------------------------------- /lib/gitlab_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:http/http.dart' as http; 5 | 6 | const DEFAULT_API_VERSION = 'v4'; 7 | const USER_AGENT = 8 | "F4Lab Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"; 9 | const KEY_TOKEN = "private-token"; 10 | const KEY_USER_AGENT = "user-agent"; 11 | 12 | class GitlabClient extends http.BaseClient { 13 | static String globalHOST = ""; 14 | static String globalTOKEN = ""; 15 | static String apiVersion = ""; 16 | 17 | final http.Client _inner = http.Client(); 18 | 19 | static GitlabClient newInstance() => GitlabClient(); 20 | 21 | static setUpTokenAndHost(String token, String host, String version) { 22 | globalTOKEN = token; 23 | globalHOST = host; 24 | apiVersion = version; 25 | authHeaders[KEY_TOKEN] = globalTOKEN; 26 | } 27 | 28 | Future send(http.BaseRequest request) { 29 | authHeaders.forEach((k, v) => request.headers[k] = v); 30 | return _inner.send(request); 31 | } 32 | 33 | @override 34 | Future get(endPoint, {Map? headers}) { 35 | return super.get(getRequestUrl(endPoint), headers: headers); 36 | } 37 | 38 | Future getRss(endPoint, {Map? headers}) { 39 | return super.get(Uri.parse("$globalHOST/$endPoint")); 40 | } 41 | 42 | @override 43 | Future post(endPoint, 44 | {Map? headers, body, Encoding? encoding}) { 45 | return super.post(getRequestUrl(endPoint), 46 | headers: headers, body: body, encoding: encoding); 47 | } 48 | 49 | @override 50 | Future put(url, 51 | {Map? headers, body, Encoding? encoding}) { 52 | return super.put(getRequestUrl(url), 53 | headers: headers, body: body, encoding: encoding); 54 | } 55 | 56 | static Uri getRequestUrl(Uri endPoint) => 57 | Uri.parse("$globalHOST/api/$apiVersion/${endPoint.path}"); 58 | 59 | static String baseUrl() => "$globalHOST/api/$apiVersion/"; 60 | 61 | static Dio buildDio() { 62 | Dio dio = Dio(); 63 | dio.options.baseUrl = baseUrl(); 64 | dio.options.connectTimeout = 5000; //5s 65 | dio.options.receiveTimeout = 5000; 66 | authHeaders.forEach((k, v) => dio.options.headers[k] = v); 67 | dio.options.responseType = ResponseType.json; 68 | return dio; 69 | } 70 | 71 | static Map authHeaders = { 72 | KEY_TOKEN: globalTOKEN, 73 | KEY_USER_AGENT: USER_AGENT 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/providers/package_info.dart'; 2 | import 'package:F4Lab/providers/theme.dart'; 3 | import 'package:F4Lab/providers/user.dart'; 4 | import 'package:F4Lab/ui/config/config_page.dart'; 5 | import 'package:F4Lab/ui/home_page.dart'; 6 | import 'package:F4Lab/util/exception_capture.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:provider/single_child_widget.dart'; 10 | 11 | void main() { 12 | runApp(MyApp()); 13 | } 14 | 15 | class MyApp extends StatelessWidget { 16 | MyApp() { 17 | FlutterError.onError = MyApp.errorHandler; 18 | } 19 | 20 | static void errorHandler(FlutterErrorDetails details, 21 | {bool forceReport = false}) { 22 | sentry.captureException( 23 | details.exception, 24 | stackTrace: details.stack, 25 | ); 26 | } 27 | 28 | List _buildProviders(BuildContext context) { 29 | return [ 30 | ChangeNotifierProvider(create: (_) => ThemeProvider()), 31 | ChangeNotifierProvider(create: (_) => UserProvider()), 32 | ChangeNotifierProvider(create: (_) => PackageInfoProvider()), 33 | ]; 34 | } 35 | 36 | Map _buildRoutes() => { 37 | '/': (_) => HomePage(), 38 | '/config': (_) => ConfigPage(), 39 | }; 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return MultiProvider( 44 | providers: _buildProviders(context), 45 | child: Consumer(builder: (context, theme, _) { 46 | return MaterialApp( 47 | title: 'GitLab', 48 | initialRoute: '/', 49 | theme: theme.currentTheme, 50 | routes: _buildRoutes()); 51 | })); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/main_dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/main.dart'; 2 | import 'package:flutter/material.dart'; 3 | // import 'package:flutter_stetho/flutter_stetho.dart'; 4 | 5 | void main() { 6 | // Stetho.initialize(); 7 | runApp(MyApp()); 8 | } 9 | -------------------------------------------------------------------------------- /lib/model/approvals.dart: -------------------------------------------------------------------------------- 1 | class Approvals { 2 | late int id; 3 | late int iid; 4 | late int projectId; 5 | late String title; 6 | late String description; 7 | late String state; 8 | late String createdAt; 9 | late String updatedAt; 10 | late String mergeStatus; 11 | late int approvalsRequired; 12 | late int approvalsLeft; 13 | late List approvedBy = []; 14 | 15 | Approvals( 16 | {required this.id, 17 | required this.iid, 18 | required this.projectId, 19 | required this.title, 20 | required this.description, 21 | required this.state, 22 | required this.createdAt, 23 | required this.updatedAt, 24 | required this.mergeStatus, 25 | required this.approvalsRequired, 26 | required this.approvalsLeft, 27 | required this.approvedBy}); 28 | 29 | Approvals.fromJson(Map json) { 30 | id = json['id']; 31 | iid = json['iid']; 32 | projectId = json['project_id']; 33 | title = json['title']; 34 | description = json['description']; 35 | state = json['state']; 36 | createdAt = json['created_at']; 37 | updatedAt = json['updated_at']; 38 | mergeStatus = json['merge_status']; 39 | approvalsRequired = json['approvals_required']; 40 | approvalsLeft = json['approvals_left']; 41 | if (json['approved_by'] != null) { 42 | approvedBy = []; 43 | json['approved_by'].forEach((v) { 44 | approvedBy.add(new ApprovedBy.fromJson(v)); 45 | }); 46 | } 47 | } 48 | 49 | Map toJson() { 50 | final Map data = new Map(); 51 | data['id'] = this.id; 52 | data['iid'] = this.iid; 53 | data['project_id'] = this.projectId; 54 | data['title'] = this.title; 55 | data['description'] = this.description; 56 | data['state'] = this.state; 57 | data['created_at'] = this.createdAt; 58 | data['updated_at'] = this.updatedAt; 59 | data['merge_status'] = this.mergeStatus; 60 | data['approvals_required'] = this.approvalsRequired; 61 | data['approvals_left'] = this.approvalsLeft; 62 | if (this.approvedBy != null) { 63 | data['approved_by'] = this.approvedBy.map((v) => v.toJson()).toList(); 64 | } 65 | return data; 66 | } 67 | } 68 | 69 | class ApprovedBy { 70 | User? user; 71 | 72 | ApprovedBy({this.user}); 73 | 74 | ApprovedBy.fromJson(Map json) { 75 | user = json['user'] != null ? new User.fromJson(json['user']) : null; 76 | } 77 | 78 | Map toJson() { 79 | final Map data = new Map(); 80 | if (this.user != null) { 81 | data['user'] = this.user!.toJson(); 82 | } 83 | return data; 84 | } 85 | } 86 | 87 | class User { 88 | late String name; 89 | late String username; 90 | late int id; 91 | late String state; 92 | late String avatarUrl; 93 | late String webUrl; 94 | 95 | User( 96 | {required this.name, 97 | required this.username, 98 | required this.id, 99 | required this.state, 100 | required this.avatarUrl, 101 | required this.webUrl}); 102 | 103 | User.fromJson(Map json) { 104 | name = json['name']; 105 | username = json['username']; 106 | id = json['id']; 107 | state = json['state']; 108 | avatarUrl = json['avatar_url']; 109 | webUrl = json['web_url']; 110 | } 111 | 112 | Map toJson() { 113 | final Map data = new Map(); 114 | data['name'] = this.name; 115 | data['username'] = this.username; 116 | data['id'] = this.id; 117 | data['state'] = this.state; 118 | data['avatar_url'] = this.avatarUrl; 119 | data['web_url'] = this.webUrl; 120 | return data; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/model/commit.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/util/date_util.dart'; 2 | 3 | class Commit { 4 | late String id; 5 | late String shortId; 6 | late String title; 7 | late String authorName; 8 | late String authorEmail; 9 | late DateTime createdAt; 10 | late String message; 11 | 12 | Commit( 13 | {required this.id, 14 | required this.shortId, 15 | required this.title, 16 | required this.authorName, 17 | required this.authorEmail, 18 | required this.createdAt, 19 | required this.message}); 20 | 21 | Commit.fromJson(Map json) { 22 | id = json['id']; 23 | shortId = json['short_id']; 24 | title = json['title']; 25 | authorName = json['author_name']; 26 | authorEmail = json['author_email']; 27 | createdAt = string2Datetime(json['created_at']); 28 | message = json['message']; 29 | } 30 | 31 | Map toJson() { 32 | final Map data = new Map(); 33 | data['id'] = this.id; 34 | data['short_id'] = this.shortId; 35 | data['title'] = this.title; 36 | data['author_name'] = this.authorName; 37 | data['author_email'] = this.authorEmail; 38 | data['created_at'] = this.createdAt; 39 | data['message'] = this.message; 40 | return data; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/model/diff.dart: -------------------------------------------------------------------------------- 1 | class Diff { 2 | late String oldPath; 3 | late String newPath; 4 | late String aMode; 5 | late String bMode; 6 | late bool newFile; 7 | late bool renamedFile; 8 | late bool deletedFile; 9 | late String diff; 10 | 11 | Diff( 12 | {required this.oldPath, 13 | required this.newPath, 14 | required this.aMode, 15 | required this.bMode, 16 | required this.newFile, 17 | required this.renamedFile, 18 | required this.deletedFile, 19 | required this.diff}); 20 | 21 | Diff.fromJson(Map json) { 22 | oldPath = json['old_path']; 23 | newPath = json['new_path']; 24 | aMode = json['a_mode']; 25 | bMode = json['b_mode']; 26 | newFile = json['new_file']; 27 | renamedFile = json['renamed_file']; 28 | deletedFile = json['deleted_file']; 29 | diff = json['diff']; 30 | } 31 | 32 | Map toJson() { 33 | final Map data = new Map(); 34 | data['old_path'] = this.oldPath; 35 | data['new_path'] = this.newPath; 36 | data['a_mode'] = this.aMode; 37 | data['b_mode'] = this.bMode; 38 | data['new_file'] = this.newFile; 39 | data['renamed_file'] = this.renamedFile; 40 | data['deleted_file'] = this.deletedFile; 41 | data['diff'] = this.diff; 42 | return data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/model/discussion.dart: -------------------------------------------------------------------------------- 1 | class Discussion { 2 | late String id; 3 | late bool individualNote; 4 | late List notes = []; 5 | 6 | Discussion( 7 | {required this.id, required this.individualNote, required this.notes}); 8 | 9 | Discussion.fromJson(Map json) { 10 | id = json['id']; 11 | individualNote = json['individual_note']; 12 | if (json['notes'] != null) { 13 | notes = []; 14 | json['notes'].forEach((v) { 15 | notes.add(new Notes.fromJson(v)); 16 | }); 17 | } 18 | } 19 | 20 | Map toJson() { 21 | final Map data = new Map(); 22 | data['id'] = this.id; 23 | data['individual_note'] = this.individualNote; 24 | data['notes'] = this.notes.map((v) => v.toJson()).toList(); 25 | return data; 26 | } 27 | } 28 | 29 | class Notes { 30 | late int id; 31 | late String type; 32 | late String body; 33 | late String attachment; 34 | Author? author; 35 | late String createdAt; 36 | late String updatedAt; 37 | late bool system; 38 | late int noteableId; 39 | late String noteableType; 40 | late int noteableIid; 41 | late bool resolved; 42 | late bool resolvable; 43 | Author? resolvedBy; 44 | 45 | Notes( 46 | {required this.id, 47 | required this.type, 48 | required this.body, 49 | required this.attachment, 50 | this.author, 51 | required this.createdAt, 52 | required this.updatedAt, 53 | required this.system, 54 | required this.noteableId, 55 | required this.noteableType, 56 | required this.noteableIid, 57 | required this.resolved, 58 | required this.resolvable, 59 | this.resolvedBy}); 60 | 61 | Notes.fromJson(Map json) { 62 | id = json['id']; 63 | type = json['type']; 64 | body = json['body']; 65 | attachment = json['attachment']; 66 | author = 67 | json['author'] != null ? new Author.fromJson(json['author']) : null; 68 | createdAt = json['created_at']; 69 | updatedAt = json['updated_at']; 70 | system = json['system']; 71 | noteableId = json['noteable_id']; 72 | noteableType = json['noteable_type']; 73 | noteableIid = json['noteable_iid']; 74 | resolved = json['resolved'] != null ? json['resolved'] : false; 75 | resolvable = json['resolvable']; 76 | resolvedBy = json['resolved_by'] != null 77 | ? new Author.fromJson(json['author']) 78 | : null; 79 | } 80 | 81 | Map toJson() { 82 | final Map data = new Map(); 83 | data['id'] = this.id; 84 | data['type'] = this.type; 85 | data['body'] = this.body; 86 | data['attachment'] = this.attachment; 87 | if (this.author != null) { 88 | data['author'] = this.author?.toJson(); 89 | } 90 | data['created_at'] = this.createdAt; 91 | data['updated_at'] = this.updatedAt; 92 | data['system'] = this.system; 93 | data['noteable_id'] = this.noteableId; 94 | data['noteable_type'] = this.noteableType; 95 | data['noteable_iid'] = this.noteableIid; 96 | data['resolved'] = this.resolved; 97 | data['resolvable'] = this.resolvable; 98 | data['resolved_by'] = this.resolvedBy; 99 | return data; 100 | } 101 | } 102 | 103 | class Author { 104 | late int id; 105 | late String name; 106 | late String username; 107 | late String state; 108 | late String avatarUrl; 109 | late String webUrl; 110 | 111 | Author( 112 | {required this.id, 113 | required this.name, 114 | required this.username, 115 | required this.state, 116 | required this.avatarUrl, 117 | required this.webUrl}); 118 | 119 | Author.fromJson(Map json) { 120 | id = json['id']; 121 | name = json['name']; 122 | username = json['username']; 123 | state = json['state']; 124 | avatarUrl = json['avatar_url']; 125 | webUrl = json['web_url']; 126 | } 127 | 128 | Map toJson() { 129 | final Map data = new Map(); 130 | data['id'] = this.id; 131 | data['name'] = this.name; 132 | data['username'] = this.username; 133 | data['state'] = this.state; 134 | data['avatar_url'] = this.avatarUrl; 135 | data['web_url'] = this.webUrl; 136 | return data; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/model/group.dart: -------------------------------------------------------------------------------- 1 | class Group { 2 | late int id; 3 | late String name; 4 | late String path; 5 | late String description; 6 | late String visibility; 7 | late bool lfsEnabled; 8 | String? avatarUrl; 9 | late String webUrl; 10 | late bool requestAccessEnabled; 11 | late String fullName; 12 | late String fullPath; 13 | int? parentId; 14 | 15 | Group( 16 | {required this.id, 17 | required this.name, 18 | required this.path, 19 | required this.description, 20 | required this.visibility, 21 | required this.lfsEnabled, 22 | this.avatarUrl, 23 | required this.webUrl, 24 | required this.requestAccessEnabled, 25 | required this.fullName, 26 | required this.fullPath, 27 | this.parentId}); 28 | 29 | Group.fromJson(Map json) { 30 | id = json['id']; 31 | name = json['name']; 32 | path = json['path']; 33 | description = json['description']; 34 | visibility = json['visibility']; 35 | lfsEnabled = json['lfs_enabled']; 36 | avatarUrl = json['avatar_url']; 37 | webUrl = json['web_url']; 38 | requestAccessEnabled = json['request_access_enabled']; 39 | fullName = json['full_name']; 40 | fullPath = json['full_path']; 41 | parentId = json['parent_id']; 42 | } 43 | 44 | Map toJson() { 45 | final Map data = new Map(); 46 | data['id'] = this.id; 47 | data['name'] = this.name; 48 | data['path'] = this.path; 49 | data['description'] = this.description; 50 | data['visibility'] = this.visibility; 51 | data['lfs_enabled'] = this.lfsEnabled; 52 | data['avatar_url'] = this.avatarUrl; 53 | data['web_url'] = this.webUrl; 54 | data['request_access_enabled'] = this.requestAccessEnabled; 55 | data['full_name'] = this.fullName; 56 | data['full_path'] = this.fullPath; 57 | data['parent_id'] = this.parentId; 58 | return data; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/model/jobs.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/model/pipeline.dart'; 2 | import 'package:F4Lab/model/runner.dart'; 3 | import 'package:F4Lab/model/user.dart'; 4 | import 'package:F4Lab/util/date_util.dart'; 5 | 6 | class Jobs { 7 | Commit? commit; 8 | late String coverage; 9 | late DateTime createdAt; 10 | late String startedAt; 11 | late String finishedAt; 12 | late double duration; 13 | late String artifactsExpireAt; 14 | late int id; 15 | late String name; 16 | Pipeline? pipeline; 17 | late String ref; 18 | late List artifacts; 19 | late Runner runner; 20 | late String stage; 21 | late String status; 22 | late bool tag; 23 | late String webUrl; 24 | User? user; 25 | 26 | Jobs( 27 | {required this.commit, 28 | required this.coverage, 29 | required this.createdAt, 30 | required this.startedAt, 31 | required this.finishedAt, 32 | required this.duration, 33 | required this.artifactsExpireAt, 34 | required this.id, 35 | required this.name, 36 | this.pipeline, 37 | required this.ref, 38 | required this.artifacts, 39 | required this.runner, 40 | required this.stage, 41 | required this.status, 42 | required this.tag, 43 | required this.webUrl, 44 | this.user}); 45 | 46 | Jobs.fromJson(Map json) { 47 | commit = 48 | json['commit'] != null ? new Commit.fromJson(json['commit']) : null; 49 | coverage = json['coverage']; 50 | createdAt = string2Datetime(json['created_at']); 51 | startedAt = json['started_at']; 52 | finishedAt = json['finished_at']; 53 | duration = json['duration']; 54 | artifactsExpireAt = json['artifacts_expire_at']; 55 | id = json['id']; 56 | name = json['name']; 57 | pipeline = json['pipeline'] != null 58 | ? new Pipeline.fromJson(json['pipeline']) 59 | : null; 60 | ref = json['ref']; 61 | artifacts = json['artifacts'].cast(); 62 | runner = Runner.fromJson(json['runner']); 63 | stage = json['stage']; 64 | status = json['status']; 65 | tag = json['tag']; 66 | webUrl = json['web_url']; 67 | user = json['user'] != null ? new User.fromJsonInJobs(json['user']) : null; 68 | } 69 | 70 | Map toJson() { 71 | final Map data = new Map(); 72 | if (this.commit != null) { 73 | data['commit'] = this.commit?.toJson(); 74 | } 75 | data['coverage'] = this.coverage; 76 | data['created_at'] = this.createdAt; 77 | data['started_at'] = this.startedAt; 78 | data['finished_at'] = this.finishedAt; 79 | data['duration'] = this.duration; 80 | data['artifacts_expire_at'] = this.artifactsExpireAt; 81 | data['id'] = this.id; 82 | data['name'] = this.name; 83 | if (this.pipeline != null) { 84 | data['pipeline'] = this.pipeline?.toJson(); 85 | } 86 | data['ref'] = this.ref; 87 | data['artifacts'] = this.artifacts; 88 | data['runner'] = this.runner.toJson(); 89 | data['stage'] = this.stage; 90 | data['status'] = this.status; 91 | data['tag'] = this.tag; 92 | data['web_url'] = this.webUrl; 93 | if (this.user != null) { 94 | data['user'] = this.user?.toJsonInJobs(); 95 | } 96 | return data; 97 | } 98 | } 99 | 100 | class Commit { 101 | late String authorEmail; 102 | late String authorName; 103 | late String createdAt; 104 | late String id; 105 | late String message; 106 | late String shortId; 107 | late String title; 108 | 109 | Commit( 110 | {required this.authorEmail, 111 | required this.authorName, 112 | required this.createdAt, 113 | required this.id, 114 | required this.message, 115 | required this.shortId, 116 | required this.title}); 117 | 118 | Commit.fromJson(Map json) { 119 | authorEmail = json['author_email']; 120 | authorName = json['author_name']; 121 | createdAt = json['created_at']; 122 | id = json['id']; 123 | message = json['message']; 124 | shortId = json['short_id']; 125 | title = json['title']; 126 | } 127 | 128 | Map toJson() { 129 | final Map data = new Map(); 130 | data['author_email'] = this.authorEmail; 131 | data['author_name'] = this.authorName; 132 | data['created_at'] = this.createdAt; 133 | data['id'] = this.id; 134 | data['message'] = this.message; 135 | data['short_id'] = this.shortId; 136 | data['title'] = this.title; 137 | return data; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/model/merge_request.dart: -------------------------------------------------------------------------------- 1 | class MergeRequest { 2 | late int id; 3 | late int iid; 4 | late int projectId; 5 | late String title; 6 | late String description; 7 | late String state; 8 | MergedBy? mergedBy; 9 | late String? mergedAt; 10 | Author? closedBy; 11 | late String? closedAt; 12 | late String createdAt; 13 | late String updatedAt; 14 | late String targetBranch; 15 | late String sourceBranch; 16 | late int upvotes; 17 | late int downvotes; 18 | Author? author; 19 | Assignee? assignee; 20 | late int sourceProjectId; 21 | late int targetProjectId; 22 | late List labels; 23 | late bool workInProgress; 24 | Milestone? milestone; 25 | late bool mergeWhenPipelineSucceeds; 26 | late String mergeStatus; 27 | late String sha; 28 | String? mergeCommitSha; 29 | late int userNotesCount; 30 | String? discussionLocked; 31 | bool? shouldRemoveSourceBranch; 32 | late bool forceRemoveSourceBranch; 33 | late bool allowCollaboration; 34 | late bool allowMaintainerToPush; 35 | late String webUrl; 36 | TimeStats? timeStats; 37 | late bool squash; 38 | late int divergedCommitsCount; 39 | late bool rebaseInProgress; 40 | 41 | MergeRequest( 42 | {required this.id, 43 | required this.iid, 44 | required this.projectId, 45 | required this.title, 46 | required this.description, 47 | required this.state, 48 | this.mergedBy, 49 | required this.mergedAt, 50 | this.closedBy, 51 | this.closedAt, 52 | required this.createdAt, 53 | required this.updatedAt, 54 | required this.targetBranch, 55 | required this.sourceBranch, 56 | required this.upvotes, 57 | required this.downvotes, 58 | this.author, 59 | this.assignee, 60 | required this.sourceProjectId, 61 | required this.targetProjectId, 62 | required this.labels, 63 | required this.workInProgress, 64 | this.milestone, 65 | required this.mergeWhenPipelineSucceeds, 66 | required this.mergeStatus, 67 | required this.sha, 68 | this.mergeCommitSha, 69 | required this.userNotesCount, 70 | this.discussionLocked, 71 | this.shouldRemoveSourceBranch, 72 | required this.forceRemoveSourceBranch, 73 | required this.allowCollaboration, 74 | required this.allowMaintainerToPush, 75 | required this.webUrl, 76 | this.timeStats, 77 | required this.squash, 78 | required this.divergedCommitsCount, 79 | required this.rebaseInProgress}); 80 | 81 | MergeRequest.fromJson(Map json) { 82 | id = json['id']; 83 | iid = json['iid']; 84 | projectId = json['project_id']; 85 | title = json['title']; 86 | description = json['description']; 87 | state = json['state']; 88 | mergedBy = json['merged_by'] != null 89 | ? new MergedBy.fromJson(json['merged_by']) 90 | : null; 91 | mergedAt = json['merged_at']; 92 | closedBy = 93 | json['closed_by'] != null ? Author.fromJson(json['closed_by']) : null; 94 | closedAt = json['closed_at']; 95 | createdAt = json['created_at']; 96 | updatedAt = json['updated_at']; 97 | targetBranch = json['target_branch']; 98 | sourceBranch = json['source_branch']; 99 | upvotes = json['upvotes']; 100 | downvotes = json['downvotes']; 101 | author = 102 | json['author'] != null ? new Author.fromJson(json['author']) : null; 103 | assignee = json['assignee'] != null 104 | ? new Assignee.fromJson(json['assignee']) 105 | : null; 106 | sourceProjectId = json['source_project_id']; 107 | targetProjectId = json['target_project_id']; 108 | labels = (json['labels'] ?? []).cast(); 109 | workInProgress = json['work_in_progress']; 110 | milestone = json['milestone'] != null 111 | ? new Milestone.fromJson(json['milestone']) 112 | : null; 113 | mergeWhenPipelineSucceeds = json['merge_when_pipeline_succeeds']; 114 | mergeStatus = json['merge_status']; 115 | sha = json['sha']; 116 | mergeCommitSha = json['merge_commit_sha']; 117 | userNotesCount = json['user_notes_count']; 118 | discussionLocked = json['discussion_locked']; 119 | shouldRemoveSourceBranch = json['should_remove_source_branch']; 120 | forceRemoveSourceBranch = json['force_remove_source_branch']; 121 | webUrl = json['web_url']; 122 | timeStats = json['time_stats'] != null 123 | ? new TimeStats.fromJson(json['time_stats']) 124 | : null; 125 | squash = json['squash']; 126 | divergedCommitsCount = json['diverged_commits_count'] != null 127 | ? json['diverged_commits_count'] 128 | : 0; 129 | rebaseInProgress = json['rebase_in_progress'] ?? false; 130 | } 131 | } 132 | 133 | class MergedBy { 134 | late int id; 135 | late String name; 136 | late String username; 137 | late String state; 138 | late String avatarUrl; 139 | late String webUrl; 140 | 141 | MergedBy( 142 | {required this.id, 143 | required this.name, 144 | required this.username, 145 | required this.state, 146 | required this.avatarUrl, 147 | required this.webUrl}); 148 | 149 | MergedBy.fromJson(Map json) { 150 | id = json['id']; 151 | name = json['name']; 152 | username = json['username']; 153 | state = json['state']; 154 | avatarUrl = json['avatar_url']; 155 | webUrl = json['web_url']; 156 | } 157 | 158 | Map toJson() { 159 | final Map data = new Map(); 160 | data['id'] = this.id; 161 | data['name'] = this.name; 162 | data['username'] = this.username; 163 | data['state'] = this.state; 164 | data['avatar_url'] = this.avatarUrl; 165 | data['web_url'] = this.webUrl; 166 | return data; 167 | } 168 | } 169 | 170 | class Author { 171 | late int id; 172 | late String name; 173 | late String username; 174 | late String state; 175 | late String avatarUrl; 176 | late String webUrl; 177 | 178 | Author( 179 | {required this.id, 180 | required this.name, 181 | required this.username, 182 | required this.state, 183 | required this.avatarUrl, 184 | required this.webUrl}); 185 | 186 | Author.fromJson(Map json) { 187 | id = json['id']; 188 | name = json['name']; 189 | username = json['username']; 190 | state = json['state']; 191 | avatarUrl = json['avatar_url']; 192 | webUrl = json['web_url']; 193 | } 194 | 195 | Map toJson() { 196 | final Map data = new Map(); 197 | data['id'] = this.id; 198 | data['name'] = this.name; 199 | data['username'] = this.username; 200 | data['state'] = this.state; 201 | data['avatar_url'] = this.avatarUrl; 202 | data['web_url'] = this.webUrl; 203 | return data; 204 | } 205 | } 206 | 207 | class Assignee { 208 | late int id; 209 | late String name; 210 | late String username; 211 | late String state; 212 | late String avatarUrl; 213 | late String webUrl; 214 | 215 | Assignee( 216 | {required this.id, 217 | required this.name, 218 | required this.username, 219 | required this.state, 220 | required this.avatarUrl, 221 | required this.webUrl}); 222 | 223 | Assignee.fromJson(Map json) { 224 | id = json['id']; 225 | name = json['name']; 226 | username = json['username']; 227 | state = json['state']; 228 | avatarUrl = json['avatar_url']; 229 | webUrl = json['web_url']; 230 | } 231 | 232 | Map toJson() { 233 | final Map data = new Map(); 234 | data['id'] = this.id; 235 | data['name'] = this.name; 236 | data['username'] = this.username; 237 | data['state'] = this.state; 238 | data['avatar_url'] = this.avatarUrl; 239 | data['web_url'] = this.webUrl; 240 | return data; 241 | } 242 | } 243 | 244 | class Milestone { 245 | late int id; 246 | late int iid; 247 | late int projectId; 248 | late String title; 249 | late String description; 250 | late String state; 251 | late String createdAt; 252 | late String updatedAt; 253 | late String dueDate; 254 | late String startDate; 255 | late String webUrl; 256 | 257 | Milestone( 258 | {required this.id, 259 | required this.iid, 260 | required this.projectId, 261 | required this.title, 262 | required this.description, 263 | required this.state, 264 | required this.createdAt, 265 | required this.updatedAt, 266 | required this.dueDate, 267 | required this.startDate, 268 | required this.webUrl}); 269 | 270 | Milestone.fromJson(Map json) { 271 | id = json['id']; 272 | iid = json['iid']; 273 | projectId = json['project_id']; 274 | title = json['title']; 275 | description = json['description']; 276 | state = json['state']; 277 | createdAt = json['created_at']; 278 | updatedAt = json['updated_at']; 279 | dueDate = json['due_date']; 280 | startDate = json['start_date']; 281 | webUrl = json['web_url']; 282 | } 283 | 284 | Map toJson() { 285 | final Map data = new Map(); 286 | data['id'] = this.id; 287 | data['iid'] = this.iid; 288 | data['project_id'] = this.projectId; 289 | data['title'] = this.title; 290 | data['description'] = this.description; 291 | data['state'] = this.state; 292 | data['created_at'] = this.createdAt; 293 | data['updated_at'] = this.updatedAt; 294 | data['due_date'] = this.dueDate; 295 | data['start_date'] = this.startDate; 296 | data['web_url'] = this.webUrl; 297 | return data; 298 | } 299 | } 300 | 301 | class TimeStats { 302 | late int timeEstimate; 303 | late int totalTimeSpent; 304 | int? humanTimeEstimate; 305 | int? humanTotalTimeSpent; 306 | 307 | TimeStats( 308 | {required this.timeEstimate, 309 | required this.totalTimeSpent, 310 | this.humanTimeEstimate, 311 | this.humanTotalTimeSpent}); 312 | 313 | TimeStats.fromJson(Map json) { 314 | timeEstimate = json['time_estimate']; 315 | totalTimeSpent = json['total_time_spent']; 316 | humanTimeEstimate = json['human_time_estimate']; 317 | humanTotalTimeSpent = json['human_total_time_spent']; 318 | } 319 | 320 | Map toJson() { 321 | final Map data = new Map(); 322 | data['time_estimate'] = this.timeEstimate; 323 | data['total_time_spent'] = this.totalTimeSpent; 324 | data['human_time_estimate'] = this.humanTimeEstimate; 325 | data['human_total_time_spent'] = this.humanTotalTimeSpent; 326 | return data; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /lib/model/pipeline.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/model/user.dart'; 2 | 3 | class Pipeline { 4 | late int id; 5 | late String sha; 6 | late String ref; 7 | late String status; 8 | late String webUrl; 9 | late String beforeSha; 10 | late bool tag; 11 | late String yamlErrors; 12 | User? user; 13 | late String createdAt; 14 | late String updatedAt; 15 | late String startedAt; 16 | late String finishedAt; 17 | late String committedAt; 18 | late int duration; 19 | late String coverage; 20 | 21 | Pipeline( 22 | {required this.id, 23 | required this.sha, 24 | required this.ref, 25 | required this.status, 26 | required this.webUrl, 27 | required this.beforeSha, 28 | required this.tag, 29 | required this.yamlErrors, 30 | this.user, 31 | required this.createdAt, 32 | required this.updatedAt, 33 | required this.startedAt, 34 | required this.finishedAt, 35 | required this.committedAt, 36 | required this.duration, 37 | required this.coverage}); 38 | 39 | Pipeline.fromJson(Map json) { 40 | id = json['id']; 41 | sha = json['sha']; 42 | ref = json['ref']; 43 | status = json['status']; 44 | webUrl = json['web_url']; 45 | beforeSha = json['before_sha']; 46 | tag = json['tag']; 47 | yamlErrors = json['yaml_errors']; 48 | user = json['user'] != null ? new User.fromJson(json['user']) : null; 49 | createdAt = json['created_at']; 50 | updatedAt = json['updated_at']; 51 | startedAt = json['started_at']; 52 | finishedAt = json['finished_at']; 53 | committedAt = json['committed_at']; 54 | duration = json['duration']; 55 | coverage = json['coverage']; 56 | } 57 | 58 | Map toJson() { 59 | final Map data = new Map(); 60 | data['id'] = this.id; 61 | data['sha'] = this.sha; 62 | data['ref'] = this.ref; 63 | data['status'] = this.status; 64 | data['web_url'] = this.webUrl; 65 | data['before_sha'] = this.beforeSha; 66 | data['tag'] = this.tag; 67 | data['yaml_errors'] = this.yamlErrors; 68 | if (this.user != null) { 69 | data['user'] = this.user?.toJson(); 70 | } 71 | data['created_at'] = this.createdAt; 72 | data['updated_at'] = this.updatedAt; 73 | data['started_at'] = this.startedAt; 74 | data['finished_at'] = this.finishedAt; 75 | data['committed_at'] = this.committedAt; 76 | data['duration'] = this.duration; 77 | data['coverage'] = this.coverage; 78 | return data; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/model/project.dart: -------------------------------------------------------------------------------- 1 | class Project { 2 | late int id; 3 | late String description; 4 | late String defaultBranch; 5 | late String sshUrlToRepo; 6 | late String httpUrlToRepo; 7 | late String webUrl; 8 | String? readmeUrl; 9 | late List tagList; 10 | late String name; 11 | late String nameWithNamespace; 12 | late String path; 13 | late String pathWithNamespace; 14 | late String createdAt; 15 | late String lastActivityAt; 16 | late int forksCount; 17 | String? avatarUrl; 18 | late int starCount; 19 | 20 | Project( 21 | {required this.id, 22 | required this.description, 23 | required this.defaultBranch, 24 | required this.sshUrlToRepo, 25 | required this.httpUrlToRepo, 26 | required this.webUrl, 27 | this.readmeUrl, 28 | required this.tagList, 29 | required this.name, 30 | required this.nameWithNamespace, 31 | required this.path, 32 | required this.pathWithNamespace, 33 | required this.createdAt, 34 | required this.lastActivityAt, 35 | required this.forksCount, 36 | this.avatarUrl, 37 | required this.starCount}); 38 | 39 | Project.fromJson(Map json) { 40 | id = json['id']; 41 | description = json['description']; 42 | defaultBranch = json['default_branch']; 43 | sshUrlToRepo = json['ssh_url_to_repo']; 44 | httpUrlToRepo = json['http_url_to_repo']; 45 | webUrl = json['web_url']; 46 | readmeUrl = json['readme_url']; 47 | tagList = json['tag_list'].cast(); 48 | name = json['name']; 49 | nameWithNamespace = json['name_with_namespace']; 50 | path = json['path']; 51 | pathWithNamespace = json['path_with_namespace']; 52 | createdAt = json['created_at']; 53 | lastActivityAt = json['last_activity_at']; 54 | forksCount = json['forks_count']; 55 | avatarUrl = json['avatar_url']; 56 | starCount = json['star_count']; 57 | } 58 | 59 | Map toJson() { 60 | final Map data = new Map(); 61 | data['id'] = this.id; 62 | data['description'] = this.description; 63 | data['default_branch'] = this.defaultBranch; 64 | data['ssh_url_to_repo'] = this.sshUrlToRepo; 65 | data['http_url_to_repo'] = this.httpUrlToRepo; 66 | data['web_url'] = this.webUrl; 67 | data['readme_url'] = this.readmeUrl; 68 | data['tag_list'] = this.tagList; 69 | data['name'] = this.name; 70 | data['name_with_namespace'] = this.nameWithNamespace; 71 | data['path'] = this.path; 72 | data['path_with_namespace'] = this.pathWithNamespace; 73 | data['created_at'] = this.createdAt; 74 | data['last_activity_at'] = this.lastActivityAt; 75 | data['forks_count'] = this.forksCount; 76 | data['avatar_url'] = this.avatarUrl; 77 | data['star_count'] = this.starCount; 78 | return data; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/model/runner.dart: -------------------------------------------------------------------------------- 1 | class Runner { 2 | late bool active; 3 | late String description; 4 | late int id; 5 | late bool isShared; 6 | late String ipAddress; 7 | late String name; 8 | late bool online; 9 | late String status; 10 | 11 | Runner( 12 | {required this.active, 13 | required this.description, 14 | required this.id, 15 | required this.isShared, 16 | required this.ipAddress, 17 | required this.name, 18 | required this.online, 19 | required this.status}); 20 | 21 | Runner.fromJson(Map json) { 22 | active = json['active']; 23 | description = json['description']; 24 | id = json['id']; 25 | isShared = json['is_shared']; 26 | ipAddress = json['ip_address']; 27 | name = json['name']; 28 | online = json['online']; 29 | status = json['status']; 30 | } 31 | 32 | Map toJson() { 33 | final Map data = new Map(); 34 | data['active'] = this.active; 35 | data['description'] = this.description; 36 | data['id'] = this.id; 37 | data['is_shared'] = this.isShared; 38 | data['ip_address'] = this.ipAddress; 39 | data['name'] = this.name; 40 | data['online'] = this.online; 41 | data['status'] = this.status; 42 | return data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/model/todo.dart: -------------------------------------------------------------------------------- 1 | class Todo { 2 | late int id; 3 | Project? project; 4 | Author? author; 5 | late String actionName; 6 | late String targetType; 7 | Target? target; 8 | late String targetUrl; 9 | late String body; 10 | late String state; 11 | late String createdAt; 12 | 13 | Todo( 14 | {required this.id, 15 | this.project, 16 | this.author, 17 | required this.actionName, 18 | required this.targetType, 19 | this.target, 20 | required this.targetUrl, 21 | required this.body, 22 | required this.state, 23 | required this.createdAt}); 24 | 25 | Todo.fromJson(Map json) { 26 | id = json['id']; 27 | project = 28 | json['project'] != null ? new Project.fromJson(json['project']) : null; 29 | author = 30 | json['author'] != null ? new Author.fromJson(json['author']) : null; 31 | actionName = json['action_name']; 32 | targetType = json['target_type']; 33 | target = 34 | json['target'] != null ? new Target.fromJson(json['target']) : null; 35 | targetUrl = json['target_url']; 36 | body = json['body']; 37 | state = json['state']; 38 | createdAt = json['created_at']; 39 | } 40 | 41 | Map toJson() { 42 | final Map data = new Map(); 43 | data['id'] = this.id; 44 | if (this.project != null) { 45 | data['project'] = this.project?.toJson(); 46 | } 47 | if (this.author != null) { 48 | data['author'] = this.author?.toJson(); 49 | } 50 | data['action_name'] = this.actionName; 51 | data['target_type'] = this.targetType; 52 | if (this.target != null) { 53 | data['target'] = this.target?.toJson(); 54 | } 55 | data['target_url'] = this.targetUrl; 56 | data['body'] = this.body; 57 | data['state'] = this.state; 58 | data['created_at'] = this.createdAt; 59 | return data; 60 | } 61 | } 62 | 63 | class Project { 64 | late int id; 65 | late String name; 66 | late String nameWithNamespace; 67 | late String path; 68 | late String pathWithNamespace; 69 | 70 | Project( 71 | {required this.id, 72 | required this.name, 73 | required this.nameWithNamespace, 74 | required this.path, 75 | required this.pathWithNamespace}); 76 | 77 | Project.fromJson(Map json) { 78 | id = json['id']; 79 | name = json['name']; 80 | nameWithNamespace = json['name_with_namespace']; 81 | path = json['path']; 82 | pathWithNamespace = json['path_with_namespace']; 83 | } 84 | 85 | Map toJson() { 86 | final Map data = new Map(); 87 | data['id'] = this.id; 88 | data['name'] = this.name; 89 | data['name_with_namespace'] = this.nameWithNamespace; 90 | data['path'] = this.path; 91 | data['path_with_namespace'] = this.pathWithNamespace; 92 | return data; 93 | } 94 | } 95 | 96 | class Author { 97 | late String name; 98 | late String username; 99 | late int id; 100 | late String state; 101 | late String avatarUrl; 102 | late String webUrl; 103 | 104 | Author( 105 | {required this.name, 106 | required this.username, 107 | required this.id, 108 | required this.state, 109 | required this.avatarUrl, 110 | required this.webUrl}); 111 | 112 | Author.fromJson(Map json) { 113 | name = json['name']; 114 | username = json['username']; 115 | id = json['id']; 116 | state = json['state']; 117 | avatarUrl = json['avatar_url']; 118 | webUrl = json['web_url']; 119 | } 120 | 121 | Map toJson() { 122 | final Map data = new Map(); 123 | data['name'] = this.name; 124 | data['username'] = this.username; 125 | data['id'] = this.id; 126 | data['state'] = this.state; 127 | data['avatar_url'] = this.avatarUrl; 128 | data['web_url'] = this.webUrl; 129 | return data; 130 | } 131 | } 132 | 133 | class Target { 134 | late int id; 135 | late int iid; 136 | late int projectId; 137 | late String title; 138 | late String description; 139 | late String state; 140 | late String createdAt; 141 | late String updatedAt; 142 | late String targetBranch; 143 | late String sourceBranch; 144 | late int upvotes; 145 | late int downvotes; 146 | Author? author; 147 | Assignee? assignee; 148 | late int sourceProjectId; 149 | late int targetProjectId; 150 | late List labels; 151 | late bool workInProgress; 152 | Milestone? milestone; 153 | late bool mergeWhenPipelineSucceeds; 154 | late String mergeStatus; 155 | late bool subscribed; 156 | late int userNotesCount; 157 | 158 | Target( 159 | {required this.id, 160 | required this.iid, 161 | required this.projectId, 162 | required this.title, 163 | required this.description, 164 | required this.state, 165 | required this.createdAt, 166 | required this.updatedAt, 167 | required this.targetBranch, 168 | required this.sourceBranch, 169 | required this.upvotes, 170 | required this.downvotes, 171 | this.author, 172 | this.assignee, 173 | required this.sourceProjectId, 174 | required this.targetProjectId, 175 | required this.labels, 176 | required this.workInProgress, 177 | this.milestone, 178 | required this.mergeWhenPipelineSucceeds, 179 | required this.mergeStatus, 180 | required this.subscribed, 181 | required this.userNotesCount}); 182 | 183 | Target.fromJson(Map json) { 184 | id = json['id']; 185 | iid = json['iid']; 186 | projectId = json['project_id']; 187 | title = json['title']; 188 | description = json['description']; 189 | state = json['state']; 190 | createdAt = json['created_at']; 191 | updatedAt = json['updated_at']; 192 | targetBranch = json['target_branch']; 193 | sourceBranch = json['source_branch']; 194 | upvotes = json['upvotes']; 195 | downvotes = json['downvotes']; 196 | author = 197 | json['author'] != null ? new Author.fromJson(json['author']) : null; 198 | assignee = json['assignee'] != null 199 | ? new Assignee.fromJson(json['assignee']) 200 | : null; 201 | sourceProjectId = json['source_project_id']; 202 | targetProjectId = json['target_project_id']; 203 | labels = json['labels'].cast(); 204 | workInProgress = json['work_in_progress']; 205 | milestone = json['milestone'] != null 206 | ? new Milestone.fromJson(json['milestone']) 207 | : null; 208 | mergeWhenPipelineSucceeds = json['merge_when_pipeline_succeeds']; 209 | mergeStatus = json['merge_status']; 210 | subscribed = json['subscribed']; 211 | userNotesCount = json['user_notes_count']; 212 | } 213 | 214 | Map toJson() { 215 | final Map data = new Map(); 216 | data['id'] = this.id; 217 | data['iid'] = this.iid; 218 | data['project_id'] = this.projectId; 219 | data['title'] = this.title; 220 | data['description'] = this.description; 221 | data['state'] = this.state; 222 | data['created_at'] = this.createdAt; 223 | data['updated_at'] = this.updatedAt; 224 | data['target_branch'] = this.targetBranch; 225 | data['source_branch'] = this.sourceBranch; 226 | data['upvotes'] = this.upvotes; 227 | data['downvotes'] = this.downvotes; 228 | if (this.author != null) { 229 | data['author'] = this.author?.toJson(); 230 | } 231 | if (this.assignee != null) { 232 | data['assignee'] = this.assignee?.toJson(); 233 | } 234 | data['source_project_id'] = this.sourceProjectId; 235 | data['target_project_id'] = this.targetProjectId; 236 | data['labels'] = this.labels; 237 | data['work_in_progress'] = this.workInProgress; 238 | if (this.milestone != null) { 239 | data['milestone'] = this.milestone?.toJson(); 240 | } 241 | data['merge_when_pipeline_succeeds'] = this.mergeWhenPipelineSucceeds; 242 | data['merge_status'] = this.mergeStatus; 243 | data['subscribed'] = this.subscribed; 244 | data['user_notes_count'] = this.userNotesCount; 245 | return data; 246 | } 247 | } 248 | 249 | class Assignee { 250 | late String name; 251 | late String username; 252 | late int id; 253 | late String state; 254 | late String avatarUrl; 255 | late String webUrl; 256 | 257 | Assignee( 258 | {required this.name, 259 | required this.username, 260 | required this.id, 261 | required this.state, 262 | required this.avatarUrl, 263 | required this.webUrl}); 264 | 265 | Assignee.fromJson(Map json) { 266 | name = json['name']; 267 | username = json['username']; 268 | id = json['id']; 269 | state = json['state']; 270 | avatarUrl = json['avatar_url']; 271 | webUrl = json['web_url']; 272 | } 273 | 274 | Map toJson() { 275 | final Map data = new Map(); 276 | data['name'] = this.name; 277 | data['username'] = this.username; 278 | data['id'] = this.id; 279 | data['state'] = this.state; 280 | data['avatar_url'] = this.avatarUrl; 281 | data['web_url'] = this.webUrl; 282 | return data; 283 | } 284 | } 285 | 286 | class Milestone { 287 | late int id; 288 | late int iid; 289 | late int projectId; 290 | late String title; 291 | late String description; 292 | late String state; 293 | late String createdAt; 294 | late String updatedAt; 295 | late String dueDate; 296 | 297 | Milestone( 298 | {required this.id, 299 | required this.iid, 300 | required this.projectId, 301 | required this.title, 302 | required this.description, 303 | required this.state, 304 | required this.createdAt, 305 | required this.updatedAt, 306 | required this.dueDate}); 307 | 308 | Milestone.fromJson(Map json) { 309 | id = json['id']; 310 | iid = json['iid']; 311 | projectId = json['project_id']; 312 | title = json['title']; 313 | description = json['description']; 314 | state = json['state']; 315 | createdAt = json['created_at']; 316 | updatedAt = json['updated_at']; 317 | dueDate = json['due_date']; 318 | } 319 | 320 | Map toJson() { 321 | final Map data = new Map(); 322 | data['id'] = this.id; 323 | data['iid'] = this.iid; 324 | data['project_id'] = this.projectId; 325 | data['title'] = this.title; 326 | data['description'] = this.description; 327 | data['state'] = this.state; 328 | data['created_at'] = this.createdAt; 329 | data['updated_at'] = this.updatedAt; 330 | data['due_date'] = this.dueDate; 331 | return data; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /lib/model/user.dart: -------------------------------------------------------------------------------- 1 | class User { 2 | late int id; 3 | late String username; 4 | late String email; 5 | late String name; 6 | late String state; 7 | late String avatarUrl; 8 | late String webUrl; 9 | late String createdAt; 10 | late String bio; 11 | late String location; 12 | late String publicEmail; 13 | late String skype; 14 | late String linkedin; 15 | late String twitter; 16 | late String websiteUrl; 17 | late String organization; 18 | late String lastSignInAt; 19 | late String confirmedAt; 20 | late int themeId; 21 | late String lastActivityOn; 22 | late int colorSchemeId; 23 | late int projectsLimit; 24 | late String currentSignInAt; 25 | List identities = []; 26 | late bool canCreateGroup; 27 | late bool canCreateProject; 28 | late bool twoFactorEnabled; 29 | late bool external; 30 | late bool privateProfile; 31 | 32 | User( 33 | {required this.id, 34 | required this.username, 35 | required this.email, 36 | required this.name, 37 | required this.state, 38 | required this.avatarUrl, 39 | required this.webUrl, 40 | required this.createdAt, 41 | required this.bio, 42 | required this.location, 43 | required this.publicEmail, 44 | required this.skype, 45 | required this.linkedin, 46 | required this.twitter, 47 | required this.websiteUrl, 48 | required this.organization, 49 | required this.lastSignInAt, 50 | required this.confirmedAt, 51 | required this.themeId, 52 | required this.lastActivityOn, 53 | required this.colorSchemeId, 54 | required this.projectsLimit, 55 | required this.currentSignInAt, 56 | required this.identities, 57 | required this.canCreateGroup, 58 | required this.canCreateProject, 59 | required this.twoFactorEnabled, 60 | required this.external, 61 | required this.privateProfile}); 62 | 63 | User.fromJson(Map json) { 64 | id = json['id']; 65 | username = json['username']; 66 | email = json['email']; 67 | name = json['name']; 68 | state = json['state']; 69 | avatarUrl = json['avatar_url']; 70 | webUrl = json['web_url']; 71 | createdAt = json['created_at']; 72 | bio = json['bio']; 73 | location = json['location']; 74 | publicEmail = json['public_email']; 75 | skype = json['skype']; 76 | linkedin = json['linkedin']; 77 | twitter = json['twitter']; 78 | websiteUrl = json['website_url']; 79 | organization = json['organization']; 80 | lastSignInAt = json['last_sign_in_at']; 81 | confirmedAt = json['confirmed_at']; 82 | themeId = json['theme_id']; 83 | lastActivityOn = json['last_activity_on']; 84 | colorSchemeId = json['color_scheme_id']; 85 | projectsLimit = json['projects_limit']; 86 | currentSignInAt = json['current_sign_in_at']; 87 | if (json['identities'] != null) { 88 | identities = []; 89 | json['identities'].forEach((v) { 90 | identities.add(new Identities.fromJson(v)); 91 | }); 92 | } 93 | canCreateGroup = json['can_create_group']; 94 | canCreateProject = json['can_create_project']; 95 | twoFactorEnabled = json['two_factor_enabled']; 96 | external = json['external']; 97 | privateProfile = json['private_profile']; 98 | } 99 | 100 | User.fromJsonInJobs(Map json) { 101 | id = json['id']; 102 | name = json['name']; 103 | username = json['username']; 104 | state = json['state']; 105 | avatarUrl = json['avatar_url']; 106 | webUrl = json['web_url']; 107 | createdAt = json['created_at']; 108 | bio = json['bio']; 109 | location = json['location']; 110 | publicEmail = json['public_email']; 111 | skype = json['skype']; 112 | linkedin = json['linkedin']; 113 | twitter = json['twitter']; 114 | websiteUrl = json['website_url']; 115 | organization = json['organization']; 116 | } 117 | 118 | Map toJson() { 119 | final Map data = new Map(); 120 | data['id'] = this.id; 121 | data['username'] = this.username; 122 | data['email'] = this.email; 123 | data['name'] = this.name; 124 | data['state'] = this.state; 125 | data['avatar_url'] = this.avatarUrl; 126 | data['web_url'] = this.webUrl; 127 | data['created_at'] = this.createdAt; 128 | data['bio'] = this.bio; 129 | data['location'] = this.location; 130 | data['public_email'] = this.publicEmail; 131 | data['skype'] = this.skype; 132 | data['linkedin'] = this.linkedin; 133 | data['twitter'] = this.twitter; 134 | data['website_url'] = this.websiteUrl; 135 | data['organization'] = this.organization; 136 | data['last_sign_in_at'] = this.lastSignInAt; 137 | data['confirmed_at'] = this.confirmedAt; 138 | data['theme_id'] = this.themeId; 139 | data['last_activity_on'] = this.lastActivityOn; 140 | data['color_scheme_id'] = this.colorSchemeId; 141 | data['projects_limit'] = this.projectsLimit; 142 | data['current_sign_in_at'] = this.currentSignInAt; 143 | if (this.identities != null) { 144 | data['identities'] = this.identities.map((v) => v.toJson()).toList(); 145 | } 146 | data['can_create_group'] = this.canCreateGroup; 147 | data['can_create_project'] = this.canCreateProject; 148 | data['two_factor_enabled'] = this.twoFactorEnabled; 149 | data['external'] = this.external; 150 | data['private_profile'] = this.privateProfile; 151 | return data; 152 | } 153 | 154 | Map toJsonInJobs() { 155 | final Map data = new Map(); 156 | data['id'] = this.id; 157 | data['name'] = this.name; 158 | data['username'] = this.username; 159 | data['state'] = this.state; 160 | data['avatar_url'] = this.avatarUrl; 161 | data['web_url'] = this.webUrl; 162 | data['created_at'] = this.createdAt; 163 | data['bio'] = this.bio; 164 | data['location'] = this.location; 165 | data['public_email'] = this.publicEmail; 166 | data['skype'] = this.skype; 167 | data['linkedin'] = this.linkedin; 168 | data['twitter'] = this.twitter; 169 | data['website_url'] = this.websiteUrl; 170 | data['organization'] = this.organization; 171 | return data; 172 | } 173 | 174 | @override 175 | String toString() { 176 | return 'User{id: $id, username: $username, name: $name}'; 177 | } 178 | } 179 | 180 | class Identities { 181 | late String provider; 182 | late String externUid; 183 | 184 | Identities({required this.provider, required this.externUid}); 185 | 186 | Identities.fromJson(Map json) { 187 | provider = json['provider']; 188 | externUid = json['extern_uid']; 189 | } 190 | 191 | Map toJson() { 192 | final Map data = new Map(); 193 | data['provider'] = this.provider; 194 | data['extern_uid'] = this.externUid; 195 | return data; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/providers/package_info.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/util/exception_capture.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:package_info/package_info.dart'; 4 | 5 | class PackageInfoProvider extends ChangeNotifier { 6 | PackageInfo _packageInfo = 7 | PackageInfo(appName: "", packageName: "", version: "", buildNumber: ""); 8 | 9 | PackageInfo get packageInfo { 10 | if (_packageInfo.appName.isEmpty) { 11 | sentry.captureException(Exception("PackageInfo get null appName")); 12 | _packageInfo = PackageInfo( 13 | appName: "", 14 | packageName: _packageInfo.packageName, 15 | version: _packageInfo.version, 16 | buildNumber: _packageInfo.buildNumber); 17 | } 18 | return _packageInfo; 19 | } 20 | 21 | PackageInfoProvider() { 22 | _loadPackageInfo(); 23 | } 24 | 25 | _loadPackageInfo() async { 26 | _packageInfo = await PackageInfo.fromPlatform(); 27 | notifyListeners(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/providers/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | import '../const.dart'; 6 | 7 | class ThemeProvider with ChangeNotifier { 8 | static ThemeData _dark = ThemeData( 9 | primaryColor: Colors.black, 10 | accentColor: Colors.deepOrange, 11 | brightness: Brightness.dark); 12 | 13 | static ThemeData _light = ThemeData( 14 | primaryColor: Colors.deepOrange, 15 | accentColor: Colors.black, 16 | brightness: Brightness.light); 17 | 18 | ThemeData _currentTheme = _dark; 19 | 20 | ThemeData get currentTheme => _currentTheme; 21 | 22 | ThemeProvider() { 23 | _loadFromLocal(); 24 | } 25 | 26 | void switchToDark() { 27 | _currentTheme = _dark; 28 | _updateLocal(true); 29 | notifyListeners(); 30 | } 31 | 32 | void switchToLight() { 33 | _currentTheme = _light; 34 | _updateLocal(false); 35 | notifyListeners(); 36 | } 37 | 38 | void _updateLocal(bool isDark) async { 39 | SharedPreferences prefs = await SharedPreferences.getInstance(); 40 | prefs.setBool(KEY_THEME_IS_DARK, isDark); 41 | } 42 | 43 | void _loadFromLocal() async { 44 | SharedPreferences prefs = await SharedPreferences.getInstance(); 45 | final isDark = prefs.getBool(KEY_THEME_IS_DARK) ?? true; 46 | _currentTheme = isDark ? _dark : _light; 47 | notifyListeners(); 48 | } 49 | 50 | bool get isDark => currentTheme == _dark; 51 | } 52 | -------------------------------------------------------------------------------- /lib/providers/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/const.dart'; 3 | import 'package:F4Lab/gitlab_client.dart'; 4 | import 'package:F4Lab/model/user.dart'; 5 | import 'package:F4Lab/user_helper.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | import 'package:shared_preferences/shared_preferences.dart'; 8 | 9 | class UserProvider with ChangeNotifier { 10 | User? _user = UserHelper.getUser(); 11 | bool _loading = false; 12 | String? _error; 13 | 14 | User? get user => _user; 15 | 16 | bool get loading => _loading; 17 | 18 | String? get error => _error; 19 | 20 | UserProvider() { 21 | _initUser(); 22 | } 23 | 24 | void setUser(User u) { 25 | _user = u; 26 | _error = null; 27 | UserHelper.setUser(u); 28 | } 29 | 30 | void _initUser() async { 31 | _loading = true; 32 | notifyListeners(); 33 | 34 | String? err = await UserHelper.initUser(); 35 | _loading = false; 36 | _error = err; 37 | _user = UserHelper.getUser(); 38 | notifyListeners(); 39 | } 40 | 41 | //region config token 42 | String? _testErr; 43 | bool _testSuccess = false; 44 | bool _testing = false; 45 | 46 | String? get testErr => _testErr; 47 | 48 | bool get testSuccess => _testSuccess; 49 | 50 | bool get testing => _testing; 51 | 52 | void testConfig(String host, String token, String? version) async { 53 | _testing = true; 54 | _testSuccess = false; 55 | _testErr = null; 56 | notifyListeners(); 57 | 58 | GitlabClient.setUpTokenAndHost(token, host, version ?? DEFAULT_API_VERSION); 59 | final resp = await ApiService.getAuthUser(); 60 | if (resp.success && resp.data != null) { 61 | _testSuccess = true; 62 | _testErr = null; 63 | final SharedPreferences sp = await SharedPreferences.getInstance(); 64 | sp.setString(KEY_ACCESS_TOKEN, token); 65 | sp.setString(KEY_HOST, host); 66 | sp.setString(KEY_API_VERSION, version ?? DEFAULT_API_VERSION); 67 | setUser(resp.data!); 68 | } else { 69 | _testSuccess = false; 70 | _testErr = resp.err ?? "Error"; 71 | } 72 | _testing = false; 73 | notifyListeners(); 74 | } 75 | 76 | void resetTestState() { 77 | _testSuccess = false; 78 | _testing = false; 79 | _testErr = null; 80 | } 81 | 82 | // endregion 83 | 84 | void logOut() async { 85 | final sp = await SharedPreferences.getInstance(); 86 | sp.remove(KEY_HOST); 87 | sp.remove(KEY_ACCESS_TOKEN); 88 | sp.remove(KEY_API_VERSION); 89 | _user = null; 90 | notifyListeners(); 91 | } 92 | 93 | @override 94 | String toString() { 95 | return 'UserProvider{user: $_user, loading: $_loading, error: $_error, configError: $_testErr, testSuccess: $_testSuccess, testing: $_testing}'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/ui/activity/activity_tab.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:F4Lab/gitlab_client.dart'; 4 | import 'package:F4Lab/util/widget_util.dart'; 5 | import 'package:F4Lab/widget/comm_ListView.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | import 'package:xml/xml.dart'; 9 | 10 | class TabActivity extends CommListWidget { 11 | TabActivity() : super(canPullUp: false); 12 | 13 | @override 14 | State createState() => FeedState(); 15 | } 16 | 17 | class FeedState extends CommListState { 18 | @override 19 | loadData({nextPage: 1}) async { 20 | final url = "dashboard/projects.atom"; 21 | final client = GitlabClient.newInstance(); 22 | final data = await client.getRss(url).then((resp) { 23 | final data = utf8.decode(resp.bodyBytes); 24 | final XmlDocument doc = parse(data); 25 | var entries = doc.findAllElements("entry"); 26 | final feeds = entries.map((ele) { 27 | return { 28 | 'title': ele.findElements("title").single.text, 29 | 'updated': ele.findElements('updated').single.text, 30 | 'link': ele.findElements('link').single.getAttribute("href"), 31 | 'avatar': 32 | ele.findElements('media:thumbnail').single.getAttribute('url') 33 | }; 34 | }); 35 | return feeds.toList(); 36 | }).whenComplete(client.close); 37 | return data; 38 | } 39 | 40 | @override 41 | Widget childBuild(BuildContext context, int index) { 42 | final item = data[index]; 43 | return Card( 44 | child: ListTile( 45 | leading: loadAvatar(item['avatar'], item['title']), 46 | title: Text(item['title']), 47 | onTap: () {}, 48 | ), 49 | ); 50 | } 51 | 52 | @override 53 | String? endPoint() => null; 54 | } 55 | -------------------------------------------------------------------------------- /lib/ui/config/config_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/const.dart'; 2 | import 'package:F4Lab/gitlab_client.dart'; 3 | import 'package:F4Lab/providers/user.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | class ConfigPage extends StatefulWidget { 9 | @override 10 | State createState() => _ConfigState(); 11 | } 12 | 13 | class _ConfigState extends State { 14 | String? _token, _host, _version; 15 | late UserProvider userProvider; 16 | 17 | @override 18 | void initState() { 19 | super.initState(); 20 | _loadConfig(); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | userProvider = Provider.of(context); 26 | if (userProvider.testSuccess) { 27 | userProvider.resetTestState(); 28 | //TODO: delay pop to wait build finish, maybe have a better way 29 | Future.delayed(Duration(milliseconds: 300), () => Navigator.pop(context)); 30 | } 31 | return Scaffold( 32 | appBar: AppBar( 33 | title: Text("Config"), 34 | elevation: 0, 35 | backgroundColor: Theme.of(context).primaryColor, 36 | ), 37 | body: Builder( 38 | builder: (context) { 39 | return Padding( 40 | padding: EdgeInsets.all(10), 41 | child: Column( 42 | mainAxisSize: MainAxisSize.min, 43 | children: [ 44 | TextField( 45 | decoration: InputDecoration( 46 | hintText: _token ?? "Access Token:", 47 | helperText: 48 | "You can create personal access token from your GitLab profile."), 49 | textInputAction: TextInputAction.next, 50 | keyboardType: TextInputType.url, 51 | onChanged: (token) => _token = token, 52 | ), 53 | TextField( 54 | decoration: InputDecoration( 55 | hintText: _host ?? "GitLab Host:", 56 | helperText: "Like https://gitlab.example.com"), 57 | textInputAction: TextInputAction.done, 58 | keyboardType: TextInputType.url, 59 | onChanged: (host) => _host = host, 60 | ), 61 | TextField( 62 | decoration: InputDecoration( 63 | hintText: _version ?? "Your gitlab api version", 64 | helperText: "Api version, default v4"), 65 | textInputAction: TextInputAction.done, 66 | keyboardType: TextInputType.text, 67 | maxLines: 1, 68 | onChanged: (v) => _version = v, 69 | ), 70 | userProvider.testErr != null 71 | ? Text(userProvider.testErr ?? "", 72 | style: TextStyle(color: Colors.red)) 73 | : const IgnorePointer(ignoring: true), 74 | userProvider.testing 75 | ? Column( 76 | children: [ 77 | CircularProgressIndicator(), 78 | Text("Test connectiong") 79 | ], 80 | ) 81 | : const IgnorePointer(ignoring: true), 82 | ], 83 | )); 84 | }, 85 | ), 86 | bottomSheet: BottomSheet( 87 | onClosing: () {}, 88 | builder: (context) { 89 | return Padding( 90 | padding: EdgeInsets.only(bottom: 50), 91 | child: Row( 92 | children: [ 93 | Expanded( 94 | child: OutlinedButton( 95 | child: Text("Test&Save"), 96 | onPressed: () { 97 | if (_token == null || _host == null) { 98 | return; 99 | } 100 | _testConfig(context); 101 | }, 102 | ), 103 | flex: 2, 104 | ), 105 | Expanded( 106 | child: OutlinedButton( 107 | child: Text("Reset"), 108 | onPressed: () => _reset(), 109 | ), 110 | flex: 1, 111 | ), 112 | ], 113 | )); 114 | }), 115 | ); 116 | } 117 | 118 | void _testConfig(BuildContext context) { 119 | userProvider.testConfig(_host!, _token!, _version); 120 | } 121 | 122 | void _loadConfig() async { 123 | final SharedPreferences sp = await SharedPreferences.getInstance(); 124 | setState(() { 125 | _token = sp.getString(KEY_ACCESS_TOKEN); 126 | _host = sp.getString(KEY_HOST); 127 | _version = sp.getString(KEY_API_VERSION) ?? DEFAULT_API_VERSION; 128 | }); 129 | } 130 | 131 | void _reset() { 132 | Navigator.pop(context); 133 | userProvider.logOut(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/ui/group/groups_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/model/group.dart'; 2 | import 'package:F4Lab/widget/comm_ListView.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class TabGroups extends CommListWidget { 6 | TabGroups() : super(canPullUp: false, withPage: false); 7 | 8 | @override 9 | State createState() => _State(); 10 | } 11 | 12 | class _State extends CommListState { 13 | @override 14 | Widget build(BuildContext context) { 15 | return data != null 16 | ? GridView.builder( 17 | itemCount: data.length, 18 | gridDelegate: 19 | SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), 20 | itemBuilder: (context, index) { 21 | return childBuild(context, index); 22 | }).build(context) 23 | : super.build(context); 24 | } 25 | 26 | @override 27 | Widget childBuild(BuildContext context, int index) { 28 | final item = data[index]; 29 | return _buildItem(item); 30 | } 31 | 32 | Widget _buildItem(item) { 33 | final group = Group.fromJson(item); 34 | return Card( 35 | child: InkWell( 36 | onTap: () { 37 | Scaffold.of(context).hideCurrentSnackBar(); 38 | Scaffold.of(context).showSnackBar( 39 | SnackBar( 40 | content: Text("${group.name}: ${group.description}"), 41 | backgroundColor: Theme.of(context).primaryColor, 42 | ), 43 | ); 44 | }, 45 | child: Center( 46 | child: Padding( 47 | padding: EdgeInsets.all(10), 48 | child: CircleAvatar( 49 | radius: 40, 50 | child: Text( 51 | group.name, 52 | textAlign: TextAlign.center, 53 | overflow: TextOverflow.fade, 54 | ), 55 | ), 56 | ), 57 | ), 58 | ), 59 | ); 60 | } 61 | 62 | @override 63 | String endPoint() => "groups"; 64 | } 65 | -------------------------------------------------------------------------------- /lib/ui/home_nav.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/const.dart'; 2 | import 'package:F4Lab/model/user.dart'; 3 | import 'package:F4Lab/providers/package_info.dart'; 4 | import 'package:F4Lab/providers/theme.dart'; 5 | import 'package:F4Lab/providers/user.dart'; 6 | import 'package:F4Lab/ui/activity/activity_tab.dart'; 7 | import 'package:F4Lab/ui/group/groups_tab.dart'; 8 | import 'package:F4Lab/ui/project/project_tabs.dart'; 9 | import 'package:F4Lab/util/widget_util.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/widgets.dart'; 12 | import 'package:provider/provider.dart'; 13 | import 'package:shared_preferences/shared_preferences.dart'; 14 | import 'package:url_launcher/url_launcher.dart'; 15 | 16 | class HomeNav extends StatefulWidget { 17 | @override 18 | State createState() => _State(); 19 | } 20 | 21 | class TabItem { 22 | final IconData icon; 23 | final String name; 24 | final WidgetBuilder builder; 25 | 26 | TabItem(this.icon, this.name, this.builder); 27 | } 28 | 29 | class _State extends State { 30 | int _currentTab = 0; 31 | List _items = []; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | _items = [ 37 | TabItem(Icons.category, "Project", (_) => TabProject()), 38 | TabItem(Icons.today, "Activity", (_) => TabActivity()), 39 | TabItem(Icons.group, "Groups", (_) => TabGroups()) 40 | ]; 41 | _loadNavIndexFromLocal(); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | final themeProvider = Provider.of(context, listen: false); 47 | final userProvider = Provider.of(context, listen: false); 48 | final user = userProvider.user; 49 | final tabs = _items.map((item) => item.builder(context)).toList(); 50 | return Scaffold( 51 | appBar: AppBar(title: Text(_items[_currentTab].name)), 52 | drawer: _buildNav(context, user, themeProvider, _items), 53 | body: Builder(builder: (context) { 54 | return IndexedStack(index: _currentTab, children: tabs); 55 | }), 56 | ); 57 | } 58 | 59 | Drawer _buildNav(BuildContext context, User? user, 60 | ThemeProvider themeProvider, List items) { 61 | List widgets = []; 62 | 63 | final header = UserAccountsDrawerHeader( 64 | decoration: 65 | BoxDecoration(color: Theme.of(context).scaffoldBackgroundColor), 66 | accountName: Text( 67 | user?.name ?? "", 68 | style: TextStyle( 69 | color: Theme.of(context).accentColor, 70 | ), 71 | ), 72 | accountEmail: Text( 73 | user?.email ?? "", 74 | style: TextStyle( 75 | color: Theme.of(context).highlightColor, 76 | ), 77 | ), 78 | currentAccountPicture: loadAvatar(user?.avatarUrl, user?.name), 79 | ); 80 | final tabs = _buildTabNav(items, context); 81 | final config = ListTile( 82 | leading: Icon(Icons.settings), 83 | title: Text("Config"), 84 | onTap: () => _navigateToConfig(context), 85 | ); 86 | 87 | final packageInfoProvider = Provider.of(context); 88 | final about = AboutListTile( 89 | icon: Icon(Icons.apps), 90 | applicationName: packageInfoProvider.packageInfo.appName, 91 | applicationVersion: packageInfoProvider.packageInfo.version, 92 | applicationLegalese: APP_LEGEND, 93 | applicationIcon: Image.network( 94 | APP_ICON_URL, 95 | width: 60, 96 | height: 60, 97 | ), 98 | aboutBoxChildren: [ 99 | OutlinedButton( 100 | child: Text("FeedBack"), 101 | onPressed: () => launch(APP_FEED_BACK_URL), 102 | ), 103 | OutlinedButton( 104 | child: Text("See in GitHub"), 105 | onPressed: () => launch(APP_REPO_URL), 106 | ) 107 | ], 108 | ); 109 | final footer = Padding( 110 | padding: EdgeInsets.only(left: 20), 111 | child: Row( 112 | children: [ 113 | Text("Dark Theme"), 114 | Switch( 115 | onChanged: (isDark) => _changeTheme(isDark, themeProvider), 116 | value: themeProvider.isDark, 117 | ) 118 | ], 119 | ), 120 | ); 121 | 122 | widgets.add(header); 123 | widgets.addAll(tabs); 124 | widgets.add(config); 125 | widgets.add(about); 126 | widgets.add(footer); 127 | return Drawer(child: ListView(children: widgets)); 128 | } 129 | 130 | List _buildTabNav(List items, BuildContext context) { 131 | return items.map((item) { 132 | final index = items.indexOf(item); 133 | return ListTile( 134 | selected: _currentTab == index, 135 | leading: Icon(item.icon), 136 | title: Text(item.name), 137 | onTap: () => _switchTab(item, index, context), 138 | ); 139 | }).toList(); 140 | } 141 | 142 | void _changeTheme(bool isDark, ThemeProvider themeProvider) { 143 | if (isDark) { 144 | themeProvider.switchToDark(); 145 | } else { 146 | themeProvider.switchToLight(); 147 | } 148 | } 149 | 150 | void _switchTab(TabItem item, int index, BuildContext context) { 151 | Navigator.of(context).pop(); 152 | setState(() => _currentTab = index); 153 | _saveNavIndexToLocal(index); 154 | } 155 | 156 | void _navigateToConfig(BuildContext context) { 157 | Navigator.pop(context); 158 | Navigator.pushNamed(context, '/config'); 159 | } 160 | 161 | void _saveNavIndexToLocal(int tabIndex) async { 162 | final sp = await SharedPreferences.getInstance(); 163 | sp.setInt(KEY_TAB_INDEX, tabIndex); 164 | } 165 | 166 | void _loadNavIndexFromLocal() { 167 | SharedPreferences.getInstance() 168 | .then((sp) => sp.getInt(KEY_TAB_INDEX) ?? 0) 169 | .then((index) { 170 | if (mounted) { 171 | setState(() => _currentTab = index); 172 | } 173 | }); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/ui/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/providers/package_info.dart'; 2 | import 'package:F4Lab/providers/user.dart'; 3 | import 'package:F4Lab/ui/home_nav.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class HomePage extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | final userProvider = Provider.of(context); 11 | if (userProvider.loading) { 12 | return _buildLoading(); 13 | } 14 | if (userProvider.user == null) { 15 | return _buildWelcome(context); 16 | } 17 | return HomeNav(); 18 | } 19 | 20 | Widget _buildLoading() => 21 | Scaffold(body: Center(child: CircularProgressIndicator())); 22 | 23 | Widget _buildWelcome(BuildContext context) { 24 | final provider = Provider.of(context); 25 | return Scaffold( 26 | body: Container( 27 | color: Theme.of(context).primaryColor, 28 | child: Center( 29 | child: Column( 30 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 31 | crossAxisAlignment: CrossAxisAlignment.center, 32 | children: [ 33 | Padding( 34 | padding: EdgeInsets.only(top: 100), 35 | child: Text( 36 | provider.packageInfo.appName, 37 | style: TextStyle( 38 | color: Theme.of(context).accentColor, 39 | fontStyle: FontStyle.italic, 40 | fontWeight: FontWeight.bold, 41 | fontSize: 36, 42 | shadows: [ 43 | Shadow( 44 | offset: Offset(0, 5), 45 | color: Theme.of(context).accentColor, 46 | blurRadius: 20) 47 | ]), 48 | ), 49 | ), 50 | Padding( 51 | padding: EdgeInsets.only(bottom: 100), 52 | child: Column(children: [ 53 | Text("👇", style: TextStyle(fontSize: 50)), 54 | OutlineButton( 55 | onPressed: () => _navigateToConfig(context), 56 | child: Text("Config Access_Token & Host"), 57 | ), 58 | ]), 59 | ) 60 | ], 61 | ), 62 | ), 63 | )); 64 | } 65 | 66 | void _navigateToConfig(BuildContext context) { 67 | Navigator.pushNamed(context, '/config'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/ui/project/jobs/jobs_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/jobs.dart'; 3 | import 'package:F4Lab/widget/comm_ListView.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | class JobsTab extends CommListWidget { 8 | final int projectId; 9 | 10 | JobsTab(this.projectId); 11 | 12 | @override 13 | State createState() => _State(); 14 | } 15 | 16 | class _State extends CommListState { 17 | @override 18 | Widget childBuild(BuildContext context, int index) { 19 | return JobWidget(job: Jobs.fromJson(data[index])); 20 | } 21 | 22 | @override 23 | String endPoint() => ApiEndPoint.projectJobs(widget.projectId); 24 | } 25 | 26 | class JobWidget extends StatelessWidget { 27 | final Jobs job; 28 | 29 | const JobWidget({Key? key, required this.job}) : super(key: key); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Card(child: Padding(padding: EdgeInsets.all(8), child: _item(job))); 34 | } 35 | 36 | Widget _item(Jobs job) { 37 | return Column( 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | children: [ 40 | Text(job.commit?.title ?? "", 41 | style: TextStyle(fontWeight: FontWeight.bold)), 42 | Row( 43 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 44 | children: [ 45 | Text("#${job.id} ${job.ref}"), 46 | Row( 47 | children: [ 48 | Padding( 49 | child: Text(job.status), 50 | padding: EdgeInsets.all(3), 51 | ), 52 | statusIcons.containsKey(job.status) 53 | ? statusIcons[job.status] 54 | : Icon(Icons.error, size: 18) 55 | ], 56 | ) 57 | ], 58 | ), 59 | Row( 60 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 61 | children: [ 62 | Text(job.stage), 63 | Text(job.name), 64 | ], 65 | ), 66 | ], 67 | ); 68 | } 69 | 70 | static get statusIcons => { 71 | "success": Icon(Icons.check_circle, color: Colors.green, size: 18), 72 | "manual": Icon(Icons.build, size: 18), 73 | "failed": Icon(Icons.error_outline, color: Colors.redAccent, size: 18), 74 | "skipped": Icon(Icons.skip_next, size: 18), 75 | "canceled": Icon(Icons.cancel, size: 18) 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /lib/ui/project/mr/approve.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/approvals.dart'; 3 | import 'package:F4Lab/user_helper.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | class MrApprove extends StatefulWidget { 8 | final int projectId; 9 | final int mrIID; 10 | final bool showActions; 11 | 12 | MrApprove(this.projectId, this.mrIID, {this.showActions = false}); 13 | 14 | @override 15 | State createState() => _MrApproveState(); 16 | } 17 | 18 | class _MrApproveState extends State { 19 | Approvals? approval; 20 | bool isApproving = false; 21 | 22 | void _loadApprove() async { 23 | final apiResp = 24 | await ApiService.mrApproveData(widget.projectId, widget.mrIID); 25 | if (mounted) { 26 | setState(() { 27 | approval = apiResp.data; 28 | }); 29 | } 30 | } 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | _loadApprove(); 36 | } 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | if (approval == null || isApproving) { 41 | return LinearProgressIndicator(); 42 | } 43 | return _buildItem(approval); 44 | } 45 | 46 | Widget _buildItem(Approvals? ap) { 47 | int requireApproves = ap?.approvalsRequired ?? 0; 48 | int? hadApproval = ap?.approvedBy != null ? ap?.approvedBy.length : 0; 49 | bool allApproval = requireApproves == hadApproval; 50 | bool iHadApproval = ap?.approvedBy != null 51 | ? ap!.approvedBy.any((item) => item.user?.id == UserHelper.getUser()?.id) 52 | : false; 53 | 54 | return Card( 55 | child: Padding( 56 | padding: EdgeInsets.all(5), 57 | child: Row( 58 | children: [ 59 | Icon(Icons.sentiment_neutral), 60 | Expanded( 61 | child: Text( 62 | "Approvals: $hadApproval of $requireApproves", 63 | style: 64 | TextStyle(color: allApproval ? Colors.green : Colors.grey), 65 | ), 66 | ), 67 | widget.showActions ? _buildActions(iHadApproval) : IgnorePointer(), 68 | ], 69 | ), 70 | ), 71 | ); 72 | } 73 | 74 | Widget _buildActions(bool iHadApproval) { 75 | return OutlineButton( 76 | onPressed: () => _approveOrUnApprove(!iHadApproval), 77 | child: Text(iHadApproval ? "UnApprove" : "Approve"), 78 | ); 79 | } 80 | 81 | void _approveOrUnApprove(bool isApprove) async { 82 | setState(() { 83 | isApproving = true; 84 | }); 85 | final ApiResp apiData = 86 | await ApiService.approve(widget.projectId, widget.mrIID, isApprove); 87 | setState(() { 88 | isApproving = false; 89 | }); 90 | if (apiData.success) { 91 | _loadApprove(); 92 | } else { 93 | ScaffoldMessenger.of(context).showSnackBar( 94 | SnackBar( 95 | content: Text("${apiData.err}"), 96 | backgroundColor: Colors.red, 97 | ), 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/ui/project/mr/commit_diff.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/commit.dart'; 3 | import 'package:F4Lab/model/diff.dart'; 4 | import 'package:F4Lab/ui/project/mr/diff.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class PageCommitDiff extends StatefulWidget { 8 | final int projectId; 9 | final Commit commit; 10 | 11 | const PageCommitDiff(this.projectId, this.commit); 12 | 13 | @override 14 | State createState() => _DiffState(); 15 | } 16 | 17 | class _DiffState extends State { 18 | List diffs = []; 19 | double _codeFontSize = 14.0; 20 | 21 | _loadDiffs() async { 22 | final resp = 23 | await ApiService.commitDiff(widget.projectId, widget.commit.id); 24 | if (resp.success) { 25 | if (mounted) { 26 | setState(() => diffs = resp.data ?? []); 27 | } 28 | } 29 | } 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | _loadDiffs(); 35 | } 36 | 37 | _buildFontControl() { 38 | return Row( 39 | children: [ 40 | Text( 41 | "Code Size: $_codeFontSize", 42 | ), 43 | OutlinedButton( 44 | child: Text("-"), 45 | onPressed: () => setState(() => _codeFontSize -= 1), 46 | ), 47 | OutlinedButton( 48 | child: Text("+"), 49 | onPressed: () => setState(() => _codeFontSize += 1), 50 | ) 51 | ], 52 | ); 53 | } 54 | 55 | _buildDiff(Diff item) { 56 | return Card( 57 | child: ExpansionTile( 58 | title: Column( 59 | crossAxisAlignment: CrossAxisAlignment.start, 60 | children: [ 61 | item.newFile 62 | ? IgnorePointer() 63 | : Text( 64 | item.oldPath, 65 | style: TextStyle(color: Colors.red), 66 | ), 67 | item.deletedFile 68 | ? IgnorePointer() 69 | : Text( 70 | item.newPath, 71 | style: TextStyle(color: Colors.green), 72 | ), 73 | ], 74 | ), 75 | children: [ 76 | _buildFontControl(), 77 | Divider(color: Colors.grey), 78 | SingleChildScrollView( 79 | scrollDirection: Axis.horizontal, 80 | child: Column( 81 | crossAxisAlignment: CrossAxisAlignment.start, 82 | children: diffToText(item.diff, Colors.red, Colors.green, Colors.black, 83 | fontSize: _codeFontSize), 84 | ), 85 | ), 86 | ])); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return Scaffold( 92 | appBar: AppBar( 93 | title: Text(widget.commit.title), 94 | ), 95 | body: diffs == null 96 | ? Center(child: CircularProgressIndicator()) 97 | : ListView.builder( 98 | itemCount: diffs.length, 99 | itemBuilder: (context, index) { 100 | final item = diffs[index]; 101 | return _buildDiff(item); 102 | }), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/ui/project/mr/diff.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class CodeDiff extends StatelessWidget { 4 | final String diff; 5 | final Color deleteColor; 6 | final Color addColor; 7 | final Color normalColor; 8 | 9 | const CodeDiff(this.diff, this.deleteColor, this.addColor, this.normalColor, 10 | {Key? key}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Column( 16 | crossAxisAlignment: CrossAxisAlignment.start, 17 | children: diffToText(diff, deleteColor, addColor, normalColor), 18 | ); 19 | } 20 | } 21 | 22 | List diffToText( 23 | String diff, Color deleteColor, Color addColor, Color normalColor, 24 | {double? fontSize}) { 25 | final lines = diff.split("\n"); 26 | return lines.map((line) { 27 | final remove = line.indexOf("-") == 0; 28 | final add = line.indexOf("+") == 0; 29 | if (remove || add) { 30 | line = " " + line.substring(1, line.length); 31 | } 32 | final style = TextStyle( 33 | color: remove ? deleteColor : (add ? addColor : normalColor), 34 | fontSize: fontSize); 35 | return Text( 36 | line, 37 | style: style, 38 | ); 39 | }).toList(); 40 | } 41 | -------------------------------------------------------------------------------- /lib/ui/project/mr/merge_request_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/merge_request.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | class MergeRequestAction extends StatefulWidget { 7 | final MergeRequest mr; 8 | final ValueChanged mergeRequestChange; 9 | 10 | const MergeRequestAction(this.mr, this.mergeRequestChange); 11 | 12 | @override 13 | State createState() => _MergeReqestState(); 14 | } 15 | 16 | class _MergeReqestState extends State { 17 | bool _removeBranch = false; 18 | bool _mergeWhenPiplineSuccess = false; 19 | bool _squashCommit = false; 20 | 21 | bool _canMerge = false; 22 | bool _loading = false; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _mergeWhenPiplineSuccess = widget.mr.mergeWhenPipelineSucceeds; 28 | } 29 | 30 | void _rebase() async { 31 | _showLoading(); 32 | final resp = await ApiService.rebaseMr(widget.mr.projectId, widget.mr.iid); 33 | if (resp.success) { 34 | widget.mergeRequestChange(null); 35 | } 36 | _hideLoading(); 37 | } 38 | 39 | void _merge() async { 40 | _showLoading(); 41 | final resp = await ApiService.acceptMR(widget.mr.projectId, widget.mr.iid, 42 | shouldRemoveSourceBranch: _removeBranch, 43 | squash: _squashCommit, 44 | mergeMrWhenPipelineSuccess: _mergeWhenPiplineSuccess); 45 | if (resp.success) { 46 | widget.mergeRequestChange(null); 47 | } 48 | _hideLoading(); 49 | } 50 | 51 | Widget _buildMergeButton() { 52 | final mr = widget.mr; 53 | String title; 54 | VoidCallback onPress; 55 | if (mr.divergedCommitsCount > 0) { 56 | title = mr.rebaseInProgress ? "Rebaseing" : "Rebase"; 57 | onPress = mr.rebaseInProgress ? ()=>{} : _rebase; 58 | } else if (mr.workInProgress) { 59 | title = "Remove WIP(Not Suuport)"; 60 | onPress = ()=>{}; 61 | } else if (mr.state == "merged") { 62 | _canMerge = false; 63 | title = "Merged"; 64 | onPress = ()=>{}; 65 | } else { 66 | _canMerge = true; 67 | title = "Merge"; 68 | onPress = _merge; 69 | } 70 | return ElevatedButton(child: Text(title), onPressed: onPress); 71 | } 72 | 73 | Widget _buildRefreshButton() { 74 | return IconButton( 75 | icon: Icon(Icons.refresh), 76 | onPressed: () { 77 | widget.mergeRequestChange(null); 78 | }, 79 | ); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | final checkBoxs = Column( 85 | children: [ 86 | CheckboxListTile( 87 | value: _removeBranch, 88 | onChanged: (bool? newValue) { 89 | setState(() => _removeBranch = newValue!); 90 | }, 91 | title: Text("Remove Source Branch ?"), 92 | ), 93 | CheckboxListTile( 94 | value: _mergeWhenPiplineSuccess, 95 | onChanged: (bool? newValue) { 96 | setState(() => _mergeWhenPiplineSuccess = newValue!); 97 | }, 98 | title: Text("Merge When Pipeline Success ?"), 99 | ), 100 | CheckboxListTile( 101 | value: _squashCommit, 102 | onChanged: (bool? newValue) { 103 | setState(() => _squashCommit = newValue!); 104 | }, 105 | title: Text("Squashed into a single commit ?"), 106 | ), 107 | ], 108 | ); 109 | 110 | final actions = Row( 111 | children: [ 112 | Expanded( 113 | flex: 1, 114 | child: _buildRefreshButton(), 115 | ), 116 | Expanded( 117 | flex: 1, 118 | child: _buildMergeButton(), 119 | ), 120 | ], 121 | ); 122 | 123 | final widgets = [ 124 | _canMerge ? checkBoxs : IgnorePointer(), 125 | actions, 126 | _loading ? LinearProgressIndicator() : IgnorePointer() 127 | ]; 128 | 129 | final content = ListTile( 130 | title: const Text("Merge request actions"), 131 | subtitle: Column(children: widgets), 132 | ); 133 | 134 | return Card(child: content); 135 | } 136 | 137 | void _showLoading() { 138 | setState(() => _loading = true); 139 | } 140 | 141 | void _hideLoading() { 142 | setState(() => _loading = false); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/ui/project/mr/mr_detail_tabs.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart' show ApiEndPoint; 2 | import 'package:F4Lab/model/commit.dart'; 3 | import 'package:F4Lab/model/discussion.dart'; 4 | import 'package:F4Lab/ui/project/mr/commit_diff.dart'; 5 | import 'package:F4Lab/util/date_util.dart'; 6 | import 'package:F4Lab/util/widget_util.dart'; 7 | import 'package:F4Lab/widget/comm_ListView.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/widgets.dart'; 10 | 11 | class CommitTab extends CommListWidget { 12 | final int projectId; 13 | final int mrIId; 14 | 15 | CommitTab(this.projectId, this.mrIId); 16 | 17 | @override 18 | State createState() => _CommitState(); 19 | } 20 | 21 | class _CommitState extends CommListState { 22 | @override 23 | Widget childBuild(BuildContext context, int index) { 24 | final Commit commit = Commit.fromJson(data[index]); 25 | return Card( 26 | child: ListTile( 27 | title: Text(commit.title), 28 | subtitle: Text(datetime2String(commit.createdAt)), 29 | onTap: () { 30 | Navigator.of(context).push(MaterialPageRoute( 31 | builder: (context) => PageCommitDiff(widget.projectId, commit))); 32 | }, 33 | ), 34 | ); 35 | } 36 | 37 | @override 38 | String endPoint() => 39 | ApiEndPoint.mergeRequestCommit(widget.projectId, widget.mrIId); 40 | } 41 | 42 | class DiscussionTab extends CommListWidget { 43 | final int projectId; 44 | final int mrIId; 45 | 46 | DiscussionTab(this.projectId, this.mrIId); 47 | 48 | @override 49 | State createState() => _DiscussionState(); 50 | } 51 | 52 | class _DiscussionState extends CommListState { 53 | @override 54 | Widget childBuild(BuildContext context, int index) { 55 | final Discussion discussion = Discussion.fromJson(data[index]); 56 | final List notes = []; 57 | discussion.notes.forEach((note) { 58 | if (!note.system) { 59 | notes.add(note); 60 | } 61 | }); 62 | 63 | final items = notes.map((item) { 64 | return ListTile( 65 | isThreeLine: true, 66 | title: Text(item.body), 67 | subtitle: Text(item.author?.name ?? ""), 68 | leading: loadAvatar(item.author?.avatarUrl, item.author?.name), 69 | trailing: Icon( 70 | item.resolved ? Icons.check_circle : Icons.error_outline, 71 | color: item.resolved ? Colors.green : Colors.redAccent, 72 | semanticLabel: "Resolved?", 73 | ), 74 | ); 75 | }).toList(); 76 | 77 | return Card( 78 | child: Column( 79 | crossAxisAlignment: CrossAxisAlignment.start, 80 | children: items, 81 | )); 82 | } 83 | 84 | @override 85 | String endPoint() => 86 | ApiEndPoint.mergeRequestDiscussion(widget.projectId, widget.mrIId); 87 | 88 | @override 89 | bool itemShouldRemove(dynamic item) => 90 | Discussion.fromJson(item).individualNote; 91 | } 92 | -------------------------------------------------------------------------------- /lib/ui/project/mr/mr_home.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/merge_request.dart'; 3 | import 'package:F4Lab/ui/project/mr/approve.dart'; 4 | import 'package:F4Lab/ui/project/mr/merge_request_action.dart'; 5 | import 'package:F4Lab/ui/project/mr/mr_tab_jobs.dart'; 6 | import 'package:F4Lab/ui/project/mr/mr_detail_tabs.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | class PageMrDetail extends StatefulWidget { 10 | final int _projectId; 11 | final int _mergeRequestIId; 12 | 13 | PageMrDetail(this._projectId, this._mergeRequestIId); 14 | 15 | @override 16 | State createState() => PageMrState(); 17 | } 18 | 19 | class PageMrState extends State { 20 | MergeRequest? _mr; 21 | 22 | void _onMergeRequestChange(void v) { 23 | _loadMergeRequest(); 24 | } 25 | 26 | void _loadMergeRequest() async { 27 | setState(() => _mr = null); 28 | final resp = await ApiService.getSingleMR( 29 | widget._projectId, widget._mergeRequestIId); 30 | if (resp.success) { 31 | setState(() => _mr = resp.data); 32 | } 33 | } 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | _loadMergeRequest(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return DefaultTabController( 44 | length: 4, 45 | child: Scaffold( 46 | appBar: AppBar( 47 | centerTitle: false, 48 | title: Text("MR #${widget._mergeRequestIId}"), 49 | bottom: TabBar(isScrollable: true, tabs: [ 50 | Tab(text: "Overview"), 51 | Tab(text: "Commits"), 52 | Tab(text: "Discussions"), 53 | Tab(text: "Jobs"), 54 | ]), 55 | ), 56 | body: _mr == null 57 | ? Center(child: CircularProgressIndicator()) 58 | : TabBarView( 59 | children: [ 60 | _buildInfo(), 61 | CommitTab(_mr!.projectId, _mr!.iid), 62 | DiscussionTab(_mr!.projectId, _mr!.iid), 63 | MergeRequestJobsTab(_mr!.projectId, _mr!.iid) 64 | ], 65 | ), 66 | )); 67 | } 68 | 69 | Icon _getStatusColor(String status) { 70 | switch (status) { 71 | case "can_be_merged": 72 | return Icon(Icons.check, color: Colors.green); 73 | case "cannot_be_merged": 74 | return Icon(Icons.highlight_off, color: Colors.red); 75 | default: 76 | return Icon(Icons.check, color: Colors.green); 77 | } 78 | } 79 | 80 | Widget _buildInfo() { 81 | final title = Text( 82 | _mr!.title, 83 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 26.0), 84 | ); 85 | 86 | final desc = 87 | _mr?.description != null ? Text(_mr!.description) : IgnorePointer(); 88 | 89 | final mrPlan = Card( 90 | child: ListTile( 91 | title: Text("Merge: ${_mr!.sourceBranch} -> ${_mr!.targetBranch}"), 92 | trailing: _getStatusColor(_mr!.mergeStatus), 93 | ), 94 | ); 95 | 96 | // final approvalAction = _buildApproval(_mr!.projectId, _mr!.iid); 97 | 98 | final mrAction = MergeRequestAction(_mr!, _onMergeRequestChange); 99 | 100 | return SingleChildScrollView( 101 | padding: EdgeInsets.all(10.0), 102 | child: Column( 103 | crossAxisAlignment: CrossAxisAlignment.start, 104 | children: [ 105 | title, 106 | desc, 107 | mrPlan, 108 | // approvalAction, 109 | mrAction, 110 | ], 111 | )); 112 | } 113 | 114 | Widget _buildApproval(int projectId, int mrIid) { 115 | return MrApprove(projectId, mrIid, showActions: true); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/ui/project/mr/mr_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/merge_request.dart'; 3 | import 'package:F4Lab/ui/project/mr/mr_list_item.dart'; 4 | import 'package:F4Lab/widget/comm_ListView.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class MRTab extends StatefulWidget { 8 | final int projectId; 9 | 10 | const MRTab(this.projectId); 11 | 12 | @override 13 | State createState() { 14 | return _State(); 15 | } 16 | } 17 | 18 | class _State extends State { 19 | late String curState; 20 | late String curScope; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | curState = ApiEndPoint.merge_request_states[0]; 26 | curScope = ApiEndPoint.merge_request_scopes[0]; 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Column( 32 | mainAxisSize: MainAxisSize.max, 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | _buildFilter(), 36 | Expanded( 37 | child: MrTab(widget.projectId, 38 | mrState: curState, 39 | scope: curScope, 40 | key: ValueKey("$curState-$curScope"))), 41 | ]); 42 | } 43 | 44 | Widget _buildFilter() { 45 | final stateSelector = DropdownButton( 46 | value: curState, 47 | items: ApiEndPoint.merge_request_states 48 | .map>((String value) { 49 | return DropdownMenuItem( 50 | value: value, 51 | child: Text(value), 52 | ); 53 | }).toList(), 54 | onChanged: (value) { 55 | if (value != curState) { 56 | setState(() { 57 | curState = value.toString(); 58 | }); 59 | } 60 | }); 61 | final scopeSelector = DropdownButton( 62 | value: curScope, 63 | items: ApiEndPoint.merge_request_scopes 64 | .map>((String value) { 65 | return DropdownMenuItem( 66 | value: value, 67 | child: Text(value), 68 | ); 69 | }).toList(), 70 | onChanged: (value) { 71 | if (value != curScope) { 72 | setState(() { 73 | curScope = value.toString(); 74 | }); 75 | } 76 | }); 77 | 78 | return Padding( 79 | padding: EdgeInsets.all(4), 80 | child: Row( 81 | crossAxisAlignment: CrossAxisAlignment.center, 82 | children: [ 83 | const Text( 84 | "Filter: ", 85 | style: TextStyle(fontWeight: FontWeight.bold), 86 | ), 87 | stateSelector, 88 | const SizedBox( 89 | width: 10, 90 | ), 91 | scopeSelector, 92 | ], 93 | )); 94 | } 95 | } 96 | 97 | class MrTab extends CommListWidget { 98 | final int projectId; 99 | final String mrState; 100 | final String scope; 101 | final Key key; 102 | 103 | MrTab(this.projectId, {required this.mrState, required this.key, required this.scope}); 104 | 105 | @override 106 | State createState() => _MrState(); 107 | } 108 | 109 | class _MrState extends CommListState { 110 | @override 111 | Widget childBuild(BuildContext context, int index) { 112 | return _buildItem(data[index]); 113 | } 114 | 115 | Widget _buildItem(item) { 116 | final mr = MergeRequest.fromJson(item); 117 | return MrListItem(mr: mr); 118 | } 119 | 120 | @override 121 | String endPoint() => ApiEndPoint.mergeRequests(widget.projectId, 122 | state: widget.mrState, scope: widget.scope); 123 | } 124 | -------------------------------------------------------------------------------- /lib/ui/project/mr/mr_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/model/merge_request.dart'; 2 | import 'package:F4Lab/ui/project/mr/approve.dart'; 3 | import 'package:F4Lab/ui/project/mr/mr_home.dart'; 4 | import 'package:F4Lab/util/widget_util.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class MrListItem extends StatelessWidget { 9 | final MergeRequest mr; 10 | 11 | const MrListItem({Key? key, required this.mr}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | bool assigned = mr.assignee != null; 16 | bool hadDescription = mr.description != null && (mr.description.isNotEmpty); 17 | String branch = "${mr.sourceBranch} → ${mr.targetBranch}"; 18 | String uName = ""; 19 | String? avatarUrl; 20 | if (assigned) { 21 | uName = mr.assignee?.username ?? ""; 22 | avatarUrl = mr.assignee?.avatarUrl; 23 | } 24 | var card = Card( 25 | elevation: 1.0, 26 | margin: EdgeInsets.only(bottom: 5.0, left: 4.0, right: 4.0, top: 5.0), 27 | child: InkWell( 28 | onTap: () => _toMrDetail(context, mr), 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | ListTile( 33 | title: RichText( 34 | text: TextSpan( 35 | text: uName + " ", 36 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24), 37 | children: [ 38 | TextSpan( 39 | text: mr.title, 40 | style: TextStyle( 41 | fontSize: 18, 42 | fontWeight: FontWeight.normal, 43 | fontStyle: FontStyle.italic)) 44 | ]), 45 | ), 46 | subtitle: Column( 47 | crossAxisAlignment: CrossAxisAlignment.start, 48 | children: [Chip(label: Text(branch)), Text(mr.webUrl)], 49 | ), 50 | leading: mr.mergeStatus == 'can_be_merged' 51 | ? Icon( 52 | Icons.done, 53 | color: Colors.green, 54 | ) 55 | : Icon( 56 | Icons.highlight_off, 57 | color: Colors.red, 58 | ), 59 | trailing: assigned 60 | ? loadAvatar( 61 | avatarUrl, 62 | uName, 63 | ) 64 | : IgnorePointer(), 65 | ), 66 | hadDescription 67 | ? Padding( 68 | padding: EdgeInsets.all(10), 69 | child: Text( 70 | mr.description, 71 | ), 72 | ) 73 | : IgnorePointer(), 74 | // MrApprove(mr.projectId, mr.iid) 75 | ], 76 | ), 77 | ), 78 | ); 79 | return card; 80 | } 81 | 82 | _toMrDetail(BuildContext context, MergeRequest mr) { 83 | Navigator.of(context).push(MaterialPageRoute( 84 | builder: (context) => PageMrDetail(mr.projectId, mr.iid))); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/ui/project/mr/mr_tab_jobs.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/model/jobs.dart'; 3 | import 'package:F4Lab/model/pipeline.dart'; 4 | import 'package:F4Lab/util/date_util.dart'; 5 | import 'package:F4Lab/widget/comm_ListView.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class MergeRequestJobsTab extends CommListWidget { 9 | final int projectId; 10 | final int mrIId; 11 | 12 | MergeRequestJobsTab(this.projectId, this.mrIId); 13 | 14 | @override 15 | State createState() => _JobsState(); 16 | } 17 | 18 | class _JobsState extends CommListState { 19 | @override 20 | Widget childBuild(BuildContext context, int index) { 21 | final pipeline = Pipeline.fromJson(data[index]); 22 | return _PipelineJobs(widget.projectId, pipeline.id, index); 23 | } 24 | 25 | @override 26 | String endPoint() => 27 | ApiEndPoint.mergeRequestPipelines(widget.projectId, widget.mrIId); 28 | } 29 | 30 | class _PipelineJobs extends StatefulWidget { 31 | final int pipelineId; 32 | final int projectId; 33 | final int index; 34 | 35 | _PipelineJobs(this.projectId, this.pipelineId, this.index); 36 | 37 | @override 38 | State createState() => _PipelineJobsState(); 39 | } 40 | 41 | class _PipelineJobsState extends State<_PipelineJobs> { 42 | bool _loading = true; 43 | List _jobs = []; 44 | 45 | final colors = { 46 | 'created': Colors.teal, 47 | 'pending': Colors.grey, 48 | 'running': Colors.teal, 49 | 'failed': Colors.red, 50 | 'success': Colors.green, 51 | 'canceled': Colors.grey, 52 | 'skipped': Colors.grey, 53 | 'manual': Colors.blue 54 | }; 55 | 56 | _loadJobs() async { 57 | setState(() { 58 | _loading = true; 59 | _jobs = []; 60 | }); 61 | final apiResp = 62 | await ApiService.pipelineJobs(widget.projectId, widget.pipelineId); 63 | if (mounted) { 64 | setState(() { 65 | _loading = false; 66 | final data = apiResp.data 67 | ?..sort((j1, j2) => j2.createdAt.compareTo(j1.createdAt)); 68 | data?.forEach((element) { 69 | _jobs.add(element); 70 | }); 71 | }); 72 | } 73 | } 74 | 75 | @override 76 | void initState() { 77 | super.initState(); 78 | _loadJobs(); 79 | } 80 | 81 | Widget _buildStatus(Jobs job) { 82 | return Chip( 83 | label: Text( 84 | job.status, 85 | ), 86 | labelStyle: TextStyle(color: colors[job.status]), 87 | ); 88 | } 89 | 90 | Widget _buildAction(Jobs item) { 91 | Widget buildBtn() { 92 | switch (item.status) { 93 | case 'skipped': 94 | case 'pending': 95 | return IgnorePointer(); 96 | case 'running': 97 | return OutlinedButton( 98 | child: const Text("cancel"), 99 | onPressed: () => _doAction(widget.projectId, item.id, 'cancel')); 100 | case 'failed': 101 | case 'success': 102 | return OutlinedButton( 103 | child: const Text("retry"), 104 | onPressed: () => _doAction(widget.projectId, item.id, 'retry')); 105 | case 'created': 106 | case 'canceled': 107 | case 'manual': 108 | return OutlinedButton( 109 | child: const Text("play"), 110 | onPressed: () => _doAction(widget.projectId, item.id, 'play')); 111 | } 112 | return IgnorePointer(); 113 | } 114 | 115 | return Row( 116 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 117 | children: [ 118 | Text( 119 | item.name, 120 | style: const TextStyle(fontWeight: FontWeight.bold), 121 | ), 122 | buildBtn() 123 | ], 124 | ); 125 | } 126 | 127 | @override 128 | Widget build(BuildContext context) { 129 | final color = Theme.of(context).cardColor; 130 | return _loading 131 | ? LinearProgressIndicator() 132 | : Card( 133 | color: widget.index.isOdd 134 | ? Color.fromRGBO( 135 | color.red, color.green, color.blue, color.opacity - 0.2) 136 | : color, 137 | margin: EdgeInsets.only(bottom: 20, left: 4, right: 4, top: 10), 138 | child: Padding( 139 | padding: EdgeInsets.only(left: 5, right: 5), 140 | child: Column( 141 | children: _jobs.map((job) { 142 | return Column( 143 | crossAxisAlignment: CrossAxisAlignment.end, 144 | children: [ 145 | Row( 146 | crossAxisAlignment: CrossAxisAlignment.start, 147 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 148 | children: [ 149 | Expanded( 150 | flex: 3, 151 | child: Column( 152 | mainAxisSize: MainAxisSize.max, 153 | crossAxisAlignment: 154 | CrossAxisAlignment.start, 155 | children: [ 156 | Text( 157 | "${job.commit?.title.substring(0, 1).toUpperCase()}${job.commit?.title.substring(1)}", 158 | style: const TextStyle( 159 | fontWeight: FontWeight.bold, 160 | fontSize: 18), 161 | ), 162 | Padding( 163 | child: Text( 164 | datetime2String(job.createdAt)), 165 | padding: const EdgeInsets.all(5), 166 | ), 167 | Padding( 168 | child: Text(job.user?.name ?? ""), 169 | padding: const EdgeInsets.all(5), 170 | ), 171 | ])), 172 | Expanded(flex: 1, child: _buildStatus(job)), 173 | ], 174 | ), 175 | _buildAction(job), 176 | Divider() 177 | ]); 178 | }).toList(), 179 | ))); 180 | } 181 | 182 | _doAction(projectId, int jobId, String action) async { 183 | _loading = true; 184 | final resp = await ApiService.triggerPipelineJob(projectId, jobId, action); 185 | if (mounted) { 186 | setState(() => _loading = false); 187 | } 188 | if (!resp.success) { 189 | } else { 190 | _loadJobs(); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/ui/project/project_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/ui/project/jobs/jobs_tab.dart'; 2 | import 'package:F4Lab/ui/project/mr/mr_list.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class PageProjectDetail extends StatelessWidget { 6 | final String projectName; 7 | final int projectId; 8 | 9 | PageProjectDetail(this.projectName, this.projectId); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return DefaultTabController( 14 | length: 2, 15 | child: Scaffold( 16 | appBar: AppBar( 17 | title: Text("$projectName"), 18 | centerTitle: false, 19 | bottom: TabBar( 20 | tabs: [ 21 | Tab(text: "MR"), 22 | Tab(text: "Jobs"), 23 | ], 24 | )), 25 | body: TabBarView( 26 | children: [MRTab(projectId), JobsTab(projectId)], 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/project/project_tabs.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/model/project.dart' as model; 2 | import 'package:F4Lab/ui/project/project_detail.dart'; 3 | import 'package:F4Lab/util/widget_util.dart'; 4 | import 'package:F4Lab/widget/comm_ListView.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | const projectTypes = { 8 | "All": "membership=true", 9 | "Your": "owned=true", 10 | "Starred": "starred=true" 11 | }; 12 | 13 | class TabProject extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return DefaultTabController( 17 | length: 3, 18 | child: Scaffold( 19 | appBar: TabBar( 20 | labelColor: Theme.of(context).accentColor, 21 | indicatorColor: Theme.of(context).primaryColor, 22 | tabs: 23 | projectTypes.keys.map((title) => Tab(text: title)).toList()), 24 | body: TabBarView( 25 | children: projectTypes.values 26 | .map((option) => ProjectTab(option)) 27 | .toList()), 28 | )); 29 | } 30 | } 31 | 32 | class ProjectTab extends CommListWidget { 33 | final String type; 34 | 35 | ProjectTab(this.type); 36 | 37 | @override 38 | State createState() => ProjectState(); 39 | } 40 | 41 | class ProjectState extends CommListState { 42 | @override 43 | Widget childBuild(BuildContext context, int index) { 44 | final item = model.Project.fromJson(data[index]); 45 | final name = item.name; 46 | final color = Theme.of(context).primaryColor; 47 | return Card( 48 | child: ListTile( 49 | leading: loadAvatar(item.avatarUrl, name, color: color), 50 | title: Text(item.nameWithNamespace), 51 | subtitle: Text(item.description), 52 | trailing: true 53 | ? Chip( 54 | label: Text(item.defaultBranch, 55 | style: TextStyle(color: Theme.of(context).primaryColor)), 56 | backgroundColor: Theme.of(context).backgroundColor, 57 | ) 58 | : IgnorePointer( 59 | ignoring: true, 60 | ), 61 | onTap: () { 62 | _navToProjectDetail(item.name, item.id); 63 | }, 64 | ), 65 | ); 66 | } 67 | 68 | _navToProjectDetail(String name, int projectId) { 69 | Navigator.of(context).push(MaterialPageRoute( 70 | builder: (context) => PageProjectDetail(name, projectId))); 71 | } 72 | 73 | @override 74 | String endPoint() => 75 | "projects?order_by=updated_at&per_page=10&simple=true&${widget.type}"; 76 | } 77 | -------------------------------------------------------------------------------- /lib/ui/todo/todo_tab.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/model/todo.dart' as TodoModel; 2 | import 'package:F4Lab/util/widget_util.dart'; 3 | import 'package:F4Lab/widget/comm_ListView.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class TabTodo extends CommListWidget { 7 | @override 8 | State createState() => TodoState(); 9 | } 10 | 11 | class TodoState extends CommListState { 12 | @override 13 | Widget childBuild(BuildContext context, int index) { 14 | final todoItem = TodoModel.Todo.fromJson(data[index]); 15 | return Card( 16 | child: ExpansionTile( 17 | leading: loadAvatar(todoItem.author?.avatarUrl, todoItem.author?.name), 18 | title: Text.rich(TextSpan( 19 | text: "${todoItem.author?.name} ", 20 | style: TextStyle(fontWeight: FontWeight.w100), 21 | children: [ 22 | TextSpan( 23 | text: todoItem.actionName.toUpperCase(), 24 | style: TextStyle(fontWeight: FontWeight.bold)), 25 | TextSpan( 26 | text: " ${todoItem.targetType} ", 27 | style: TextStyle(fontWeight: FontWeight.w400)), 28 | TextSpan(text: todoItem.target?.title) 29 | ])), 30 | trailing: OutlinedButton( 31 | child: Text("Done"), 32 | onPressed: () {}, 33 | ), 34 | children: [Text(todoItem.createdAt)], 35 | )); 36 | } 37 | 38 | @override 39 | String endPoint() => "todos?state=pending"; 40 | } 41 | -------------------------------------------------------------------------------- /lib/user_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/api.dart'; 2 | import 'package:F4Lab/const.dart'; 3 | import 'package:F4Lab/gitlab_client.dart'; 4 | import 'package:F4Lab/model/user.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | class UserHelper { 8 | static User? _user; 9 | 10 | static void setUser(User? u) { 11 | _user = u; 12 | } 13 | 14 | static User? getUser() { 15 | return _user; 16 | } 17 | 18 | static Future initUser() async { 19 | final SharedPreferences sp = await SharedPreferences.getInstance(); 20 | final token = sp.getString(KEY_ACCESS_TOKEN) ?? null; 21 | final host = sp.getString(KEY_HOST) ?? null; 22 | final v = sp.getString(KEY_API_VERSION) ?? null; 23 | if (token == null || host == null || v == null) { 24 | setUser(null); 25 | return "Not found host or toekn or api_version"; 26 | } 27 | GitlabClient.setUpTokenAndHost(token, host, v); 28 | final resp = await ApiService.getAuthUser(); 29 | final err = resp.err; 30 | if (resp.success) { 31 | setUser(resp.data); 32 | } 33 | return err; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/util/date_util.dart: -------------------------------------------------------------------------------- 1 | DateTime string2Datetime(String date) { 2 | return DateTime.parse(date).toLocal(); 3 | } 4 | 5 | String datetime2String(DateTime datetime) { 6 | return "${datetime.year}/${datetime.month}/${datetime.day} ${datetime.hour}:${datetime.minute}:${datetime.second}"; 7 | } 8 | -------------------------------------------------------------------------------- /lib/util/exception_capture.dart: -------------------------------------------------------------------------------- 1 | import 'package:sentry/sentry.dart'; 2 | 3 | SentryClient sentry = new SentryClient(SentryOptions(dsn: "https://a49f4f9002e04a81959c51f769a4e013@sentry.io/1406491")); 4 | -------------------------------------------------------------------------------- /lib/util/widget_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Widget loadAvatar(String? url, String? name, {Color color = Colors.teal}) { 4 | assert(name != null); 5 | if (url != null) { 6 | debugPrint("[loadAvatar] Start load: $url"); 7 | NetworkImage image; 8 | image = new NetworkImage(url); 9 | return CircleAvatar( 10 | backgroundImage: image, 11 | backgroundColor: color, 12 | ); 13 | } 14 | return new CircleAvatar( 15 | child: Text( 16 | name ?? "", 17 | textAlign: TextAlign.center, 18 | overflow: TextOverflow.ellipsis, 19 | softWrap: true, 20 | ), 21 | backgroundColor: color, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/widget/comm_ListView.dart: -------------------------------------------------------------------------------- 1 | import 'package:F4Lab/gitlab_client.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 6 | 7 | ///[canPullUp] This bool will affect whether or not to have the function of drop-up load 8 | ///[canPullDown] This bool will affect whether or not to have the function of drop-down refresh 9 | ///[withPage] This bool will affect whether or not to add page arg to request url 10 | abstract class CommListWidget extends StatefulWidget { 11 | final bool canPullUp; 12 | 13 | final bool canPullDown; 14 | 15 | final bool withPage; 16 | 17 | CommListWidget( 18 | {this.canPullDown = true, this.canPullUp = true, this.withPage = true}); 19 | } 20 | 21 | abstract class CommListState extends State 22 | with AutomaticKeepAliveClientMixin { 23 | List data = []; 24 | late int page; 25 | late int total; 26 | late int next; 27 | 28 | RefreshController _refreshController = RefreshController(); 29 | 30 | /// eg: merge_request?status=open 31 | String? endPoint(); 32 | 33 | loadData({nextPage: 1}) async { 34 | Dio dio = GitlabClient.buildDio(); 35 | 36 | var url; 37 | final _endPoint = endPoint() ?? ""; 38 | if (widget.withPage) { 39 | if (!_endPoint.contains("?")) { 40 | url = "$_endPoint?page=$nextPage&per_page=10"; 41 | } else { 42 | url = "$_endPoint&page=$nextPage&per_page=10"; 43 | } 44 | } else { 45 | url = _endPoint; 46 | } 47 | 48 | final remoteData = await dio 49 | .get(url) 50 | .then((resp) { 51 | page = int.tryParse(resp.headers['x-page']![0]) ?? 0; 52 | total = int.tryParse(resp.headers['x-total-pages']![0]) ?? 0; 53 | next = int.tryParse(resp.headers['x-next-page']![0]) ?? 0; 54 | return resp; 55 | }) 56 | .then((resp) => resp.data) 57 | .catchError((err) { 58 | print("Error: $err"); 59 | return []; 60 | }); 61 | 62 | return Future(() { 63 | final List _remote = []; 64 | remoteData.forEach((item) { 65 | if (!itemShouldRemove(item)) { 66 | _remote.add(item); 67 | } 68 | }); 69 | return _remote; 70 | }); 71 | } 72 | 73 | _loadMore() async { 74 | if (page == total) { 75 | _refreshController.loadNoData(); 76 | } else { 77 | final remoteData = await loadData(nextPage: next); 78 | if (mounted) { 79 | setState(() { 80 | data.addAll(remoteData); 81 | }); 82 | if (page == total) { 83 | _refreshController.loadNoData(); 84 | } else { 85 | _refreshController.loadComplete(); 86 | } 87 | } 88 | } 89 | } 90 | 91 | _loadNew() async { 92 | final remoteDate = await loadData() as List; 93 | if (mounted) { 94 | setState(() { 95 | data = remoteDate; 96 | }); 97 | _refreshController.refreshCompleted(); 98 | } 99 | } 100 | 101 | @override 102 | void initState() { 103 | super.initState(); 104 | _loadNew(); 105 | } 106 | 107 | @override 108 | void dispose() { 109 | _refreshController.dispose(); 110 | super.dispose(); 111 | } 112 | 113 | Widget childBuild(BuildContext context, int index); 114 | 115 | bool itemShouldRemove(dynamic item) => false; 116 | 117 | Widget buildEmptyView() { 118 | return Center( 119 | child: Container( 120 | margin: EdgeInsets.only(top: 100), 121 | child: Text.rich( 122 | TextSpan( 123 | text: "🎉 No More 🎉\n\nPull Down To Refresh", 124 | style: TextStyle(fontSize: 16), 125 | ), 126 | textAlign: TextAlign.center, 127 | ), 128 | ), 129 | ); 130 | } 131 | 132 | Widget buildDataListView() { 133 | return SmartRefresher( 134 | controller: _refreshController, 135 | enablePullDown: widget.canPullDown, 136 | enablePullUp: widget.canPullUp, 137 | onRefresh: () => _loadNew(), 138 | onLoading: () => _loadMore(), 139 | child: data.length == 0 140 | ? ListView.builder( 141 | itemCount: 1, 142 | itemBuilder: (ctx, index) { 143 | return buildEmptyView(); 144 | }) 145 | : ListView.builder( 146 | itemCount: data.length, 147 | itemBuilder: (context, index) { 148 | return childBuild(context, index); 149 | })); 150 | } 151 | 152 | @override 153 | Widget build(BuildContext context) { 154 | super.build(context); 155 | return data == null 156 | ? Center(child: CircularProgressIndicator()) 157 | : buildDataListView(); 158 | } 159 | 160 | @override 161 | bool get wantKeepAlive => true; 162 | } 163 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: F4Lab 2 | description: A new Flutter applicatio fot GitLab. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.1.4 11 | 12 | environment: 13 | sdk: "2.13.4" 14 | flutter: "2.2.3" 15 | 16 | dependencies: 17 | flutter: 18 | sdk: flutter 19 | # plugin 20 | shared_preferences: ^2.0.6 21 | url_launcher: ^6.0.9 22 | # lib 23 | xml: ^5.1.2 24 | cupertino_icons: ^1.0.3 25 | pull_to_refresh: ^2.0.0 26 | dio: ^4.0.0 27 | http: ^0.13.3 28 | sentry: ^5.1.0 29 | provider: ^5.0.0 30 | package_info: ^2.0.2 31 | 32 | dev_dependencies: 33 | flutter_test: 34 | sdk: flutter 35 | 36 | # For information on the generic Dart part of this file, see the 37 | # following page: https://www.dartlang.org/tools/pub/pubspec 38 | 39 | # The following section is specific to Flutter. 40 | flutter: 41 | # The following line ensures that the Material Icons font is 42 | # included with your application, so that you can use the icons in 43 | # the material Icons class. 44 | uses-material-design: true 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | # An image asset can refer to one or more resolution-specific "variants", see 50 | # https://flutter.io/assets-and-images/#resolution-aware. 51 | # For details regarding adding assets from package dependencies, see 52 | # https://flutter.io/assets-and-images/#from-packages 53 | # To add custom fonts to your application, add a fonts section here, 54 | # in this "flutter" section. Each entry in this list should have a 55 | # "family" key with the font family name, and a "fonts" key with a 56 | # list giving the asset and other descriptors for the font. For 57 | # example: 58 | # fonts: 59 | # - family: Schyler 60 | # fonts: 61 | # - asset: fonts/Schyler-Regular.ttf 62 | # - asset: fonts/Schyler-Italic.ttf 63 | # style: italic 64 | # - family: Trajan Pro 65 | # fonts: 66 | # - asset: fonts/TrajanPro.ttf 67 | # - asset: fonts/TrajanPro_Bold.ttf 68 | # weight: 700 69 | # 70 | # For details regarding fonts from package dependencies, 71 | # see https://flutter.io/custom-fonts/#from-packages 72 | -------------------------------------------------------------------------------- /script/run_dev.sh: -------------------------------------------------------------------------------- 1 | flutter run -t ./lib/main_dev.dart -------------------------------------------------------------------------------- /script/upload-apk.sh: -------------------------------------------------------------------------------- 1 | apk_path="build/app/outputs/apk/release/app-release.apk" 2 | 3 | echo "Start upload apk" 4 | 5 | curl "http://devtools.qiniu.com/linux/amd64/qrsctl?ref=developer.qiniu.com" -o qrsctl 6 | chmod +x qrsctl 7 | 8 | ./qrsctl login $QINIU_User $QINIU_Passwd 9 | DATE=`date '+%Y%m%d_%H%M%S'` 10 | name="apk/f4lab_${DATE}.apk" 11 | echo "Start put file ${name}" 12 | ./qrsctl put -c myapk $name $apk_path 13 | -------------------------------------------------------------------------------- /test/util/date_util_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:F4Lab/util/date_util.dart'; 3 | 4 | void main() { 5 | group("Date util", () { 6 | test("string2Datetime", () { 7 | final strDate = "2019-03-28T13:46:44.208"; 8 | final date = string2Datetime(strDate); 9 | expect(date, DateTime(2019, 03, 28, 13, 46, 44, 208)); 10 | }); 11 | 12 | test("datetime2String", () { 13 | final dateTime = DateTime(2019, 03, 28, 13, 46, 44); 14 | final strDate = "2019/3/28 13:46:44"; 15 | expect(datetime2String(dateTime), strDate); 16 | }); 17 | }); 18 | } 19 | --------------------------------------------------------------------------------