├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── se │ │ │ │ └── bocker │ │ │ │ └── codestatsflutter │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── 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 │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── icon │ └── ic_launcher.png └── web_hi_res_512.png ├── fonts └── OCRAEXT.TTF ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── flutter_export_environment.sh ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── main.m ├── lib ├── bloc │ ├── bloc_provider.dart │ ├── codestats_bloc.dart │ ├── state.dart │ └── state.g.dart ├── hydrated.dart ├── main.dart ├── models │ ├── pulse │ │ ├── pulse.dart │ │ ├── pulse.g.dart │ │ ├── xp.dart │ │ └── xp.g.dart │ └── user │ │ ├── day_language_xps.dart │ │ ├── day_language_xps.g.dart │ │ ├── user.dart │ │ ├── user.g.dart │ │ ├── xp.dart │ │ └── xp.g.dart ├── passthrough_simulation.dart ├── queries.dart ├── schema.graphql ├── sequence_animation.dart ├── utils.dart └── widgets │ ├── Snappable.dart │ ├── add_user_page.dart │ ├── backdrop.dart │ ├── bouncable.dart │ ├── breathing_widget.dart │ ├── choose_user_menu.dart │ ├── dash_board_body.dart │ ├── day_language_xps.dart │ ├── day_of_year_xps.dart │ ├── dots_indicator.dart │ ├── expandable_user.dart │ ├── expandable_user_list.dart │ ├── explosion.dart │ ├── fluid_slider.dart │ ├── glass_crack │ └── glass_crack.dart │ ├── language_levels.dart │ ├── level_percent_indicator.dart │ ├── level_progress_circle.dart │ ├── linear_percent_indicator.dart │ ├── no_user.dart │ ├── profile_page.dart │ ├── pulse_notification.dart │ ├── random_loading_animation.dart │ ├── recent_period_selector.dart │ ├── reload_data.dart │ ├── settings.dart │ ├── shimmer.dart │ ├── spotlight.dart │ ├── subheader.dart │ ├── tab_navigator.dart │ ├── tiltable_stack.dart │ ├── total_xp_header.dart │ └── wave_progress.dart ├── midi └── zelda.sf2 ├── pubspec.yaml └── screenshots ├── adduser.png ├── demo.webp ├── languages.png ├── profile.png ├── recent.png ├── settings.png └── year.png /.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/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /.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: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jonathan Böcker 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 | # Code::Stats Viewer 2 | 3 | Get it on Google Play 4 | 5 | 6 | ## Stargazers over time 7 | 8 | [![Stargazers over time](https://starchart.cc/Schwusch/codestats_flutter.svg)](https://starchart.cc/Schwusch/codestats_flutter) 9 | 10 | ![](screenshots/year.png) | ![](screenshots/profile.png) | ![](screenshots/recent.png) 11 | | ------------------------- | ------------------------- | ----------- 12 | ![](screenshots/languages.png) | ![](screenshots/settings.png) | ![](screenshots/adduser.png) 13 | 14 | 15 | Old demo: 16 | 17 | ![](screenshots/demo.webp) 18 | 19 | ## Running the app 20 | 21 | To run this project: 22 | - Follow the [Flutter installation instructions](https://flutter.io/setup/) 23 | - Clone this project and run `flutter doctor` in the project root directory 24 | - Run `flutter run` 25 | 26 | ## The code 27 | 28 | Application-specific code is in [/lib](/lib). -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: true 4 | errors: 5 | todo: ignore 6 | missing_return: error 7 | 8 | linter: 9 | rules: 10 | - avoid_empty_else 11 | - cancel_subscriptions 12 | - close_sinks 13 | - unnecessary_const 14 | - unnecessary_new -------------------------------------------------------------------------------- /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 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "se.bocker.codestatsflutter" 42 | minSdkVersion 17 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 11 | 15 | 22 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/se/bocker/codestatsflutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package se.bocker.codestatsflutter 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | 6 | import io.flutter.app.FlutterActivity 7 | import io.flutter.plugin.common.MethodChannel 8 | import io.flutter.plugins.GeneratedPluginRegistrant 9 | 10 | class MainActivity : FlutterActivity() { 11 | private var intentData: String? = null 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | GeneratedPluginRegistrant.registerWith(this) 16 | 17 | if (Intent.ACTION_VIEW == intent.action) { 18 | intentData = intent.data?.lastPathSegment 19 | } 20 | 21 | MethodChannel(flutterView, "app.channel.shared.data").setMethodCallHandler { methodCall, result -> 22 | when (methodCall.method) { 23 | "getIntentLastPathSegment" -> { 24 | result.success(intentData) 25 | intentData = null 26 | } 27 | } 28 | } 29 | } 30 | 31 | override fun onNewIntent(intent: Intent?) { 32 | super.onNewIntent(intent) 33 | if (Intent.ACTION_VIEW == intent?.action) { 34 | intentData = intent.data?.lastPathSegment 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.41' 3 | repositories { 4 | google() 5 | jcenter() 6 | maven { url "https://jitpack.io" } 7 | } 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.5.0' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 02 18:53:44 CEST 2019 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.4.1-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/icon/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/assets/icon/ic_launcher.png -------------------------------------------------------------------------------- /assets/web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/assets/web_hi_res_512.png -------------------------------------------------------------------------------- /fonts/OCRAEXT.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/fonts/OCRAEXT.TTF -------------------------------------------------------------------------------- /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/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/home/schwusch/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/home/schwusch/codestats_flutter" 5 | export "FLUTTER_TARGET=lib/main.dart" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "SYMROOT=${SOURCE_ROOT}/../build/ios" 8 | export "FLUTTER_FRAMEWORK_DIR=/home/schwusch/flutter/bin/cache/artifacts/engine/ios" 9 | export "FLUTTER_BUILD_NAME=1.0.21" 10 | export "FLUTTER_BUILD_NUMBER=22" 11 | -------------------------------------------------------------------------------- /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 parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | 38 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 39 | # referring to absolute paths on developers' machines. 40 | system('rm -rf .symlinks') 41 | system('mkdir -p .symlinks/plugins') 42 | 43 | # Flutter Pods 44 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 45 | if generated_xcode_build_settings.empty? 46 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 47 | end 48 | generated_xcode_build_settings.map { |p| 49 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 50 | symlink = File.join('.symlinks', 'flutter') 51 | File.symlink(File.dirname(p[:path]), symlink) 52 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 53 | end 54 | } 55 | 56 | # Plugin Pods 57 | plugin_pods = parse_KV_file('../.flutter-plugins') 58 | plugin_pods.map { |p| 59 | symlink = File.join('.symlinks', 'plugins', p[:name]) 60 | File.symlink(p[:path], symlink) 61 | pod p[:name], :path => File.join(symlink, 'ios') 62 | } 63 | end 64 | 65 | post_install do |installer| 66 | installer.pods_project.targets.each do |target| 67 | target.build_configurations.each do |config| 68 | config.build_settings['ENABLE_BITCODE'] = 'NO' 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /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.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 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/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.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/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/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 | codestats_flutter 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/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /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/bloc/bloc_provider.dart: -------------------------------------------------------------------------------- 1 | 2 | // Generic Interface for all BLoCs 3 | import 'package:flutter/material.dart'; 4 | 5 | abstract class BlocBase { 6 | void dispose(); 7 | } 8 | 9 | // Generic BLoC provider 10 | class BlocProvider extends StatefulWidget { 11 | BlocProvider({ 12 | Key key, 13 | @required this.child, 14 | @required this.bloc, 15 | }): super(key: key); 16 | 17 | final T bloc; 18 | final Widget child; 19 | 20 | @override 21 | _BlocProviderState createState() => _BlocProviderState(); 22 | 23 | static T of(BuildContext context){ 24 | final type = _typeOf>(); 25 | BlocProvider provider = context.ancestorWidgetOfExactType(type); 26 | return provider.bloc; 27 | } 28 | 29 | static Type _typeOf() => T; 30 | } 31 | 32 | class _BlocProviderState extends State> { 33 | @override 34 | void dispose() { 35 | widget.bloc.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return widget.child; 42 | } 43 | } -------------------------------------------------------------------------------- /lib/bloc/state.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/models/user/user.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'state.g.dart'; 5 | 6 | @JsonSerializable(nullable: true, useWrappers: true) 7 | class UserState { 8 | Map allUsers; 9 | 10 | UserState({ 11 | this.allUsers 12 | }); 13 | 14 | factory UserState.empty() => UserState(allUsers: {}); 15 | 16 | factory UserState.fromJson(Map json) => _$UserStateFromJson(json); 17 | Map toJson() => _$UserStateToJson(this); 18 | } -------------------------------------------------------------------------------- /lib/bloc/state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | UserState _$UserStateFromJson(Map json) { 10 | return UserState( 11 | allUsers: (json['allUsers'] as Map)?.map((k, e) => 12 | MapEntry( 13 | k, e == null ? null : User.fromJson(e as Map))) ?? {}); 14 | } 15 | 16 | Map _$UserStateToJson(UserState instance) => 17 | _$UserStateJsonMapWrapper(instance); 18 | 19 | class _$UserStateJsonMapWrapper extends $JsonMapWrapper { 20 | final UserState _v; 21 | _$UserStateJsonMapWrapper(this._v); 22 | 23 | @override 24 | Iterable get keys => const ['allUsers']; 25 | 26 | @override 27 | dynamic operator [](Object key) { 28 | if (key is String) { 29 | switch (key) { 30 | case 'allUsers': 31 | return _v.allUsers; 32 | } 33 | } 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/hydrated.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io' show File; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:rxdart/rxdart.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | /// A [BehaviorSubject] that automatically persists its values and hydrates on creation. 8 | /// 9 | /// HydratedSubject supports serialized classes and [shared_preferences] types such as: `int`, `double`, `bool`, `String`, and `List` 10 | /// 11 | /// Serialized classes are supported by using the `hydrate: (String)=>Class` and `persist: (Class)=>String` constructor arguments. 12 | /// 13 | /// Example: 14 | /// 15 | /// ``` 16 | /// final count$ = HydratedSubject("count", seedValue: 0); 17 | /// ``` 18 | /// 19 | /// Serialized class example: 20 | /// 21 | /// ``` 22 | /// final user$ = HydratedSubject( 23 | /// "user", 24 | /// hydrate: (String s) => User.fromJSON(s), 25 | /// persist: (User user) => user.toJSON(), 26 | /// seedValue: User.empty(), 27 | /// ); 28 | /// ``` 29 | /// 30 | /// Hydration is performed automatically and is asynchronous. The `onHydrate` callback is called when hydration is complete. 31 | /// 32 | /// ``` 33 | /// final user$ = HydratedSubject( 34 | /// "count", 35 | /// onHydrate: () => loading$.add(false), 36 | /// ); 37 | /// ``` 38 | 39 | class HydratedSubject extends Subject implements ValueObservable { 40 | String _key; 41 | T _seedValue; 42 | _Wrapper _wrapper; 43 | 44 | T Function(String value) _hydrate; 45 | String Function(T value) _persist; 46 | void Function() onHydrate; 47 | bool isHydrated = false; 48 | 49 | Future get _localPath async { 50 | final directory = await getApplicationDocumentsDirectory(); 51 | 52 | return directory.path; 53 | } 54 | 55 | Future get _localFile async { 56 | final path = await _localPath; 57 | return File('$path/$_key.txt'); 58 | } 59 | 60 | 61 | 62 | HydratedSubject._( 63 | this._key, 64 | this._seedValue, 65 | this._hydrate, 66 | this._persist, 67 | this.onHydrate, 68 | StreamController controller, 69 | Observable observable, 70 | this._wrapper, 71 | ) : super(controller, observable) { 72 | hydrateSubject(); 73 | } 74 | 75 | factory HydratedSubject( 76 | String key, { 77 | T seedValue, 78 | T Function(String value) hydrate, 79 | String Function(T value) persist, 80 | void onHydrate(), 81 | void onListen(), 82 | void onCancel(), 83 | bool sync: false, 84 | }) { 85 | // assert that T is a type compatible with shared_preferences, 86 | // or that we have hydrate and persist mapping functions 87 | assert(T == int || 88 | T == double || 89 | T == bool || 90 | T == String || 91 | [""] is T || 92 | (hydrate != null && persist != null)); 93 | 94 | // ignore: close_sinks 95 | final controller = StreamController.broadcast( 96 | onListen: onListen, 97 | onCancel: onCancel, 98 | sync: sync, 99 | ); 100 | 101 | final wrapper = _Wrapper(seedValue); 102 | 103 | return HydratedSubject._( 104 | key, 105 | seedValue, 106 | hydrate, 107 | persist, 108 | onHydrate, 109 | controller, 110 | Observable.defer( 111 | () => wrapper.latestValue == null 112 | ? controller.stream 113 | : Observable(controller.stream) 114 | .startWith(wrapper.latestValue), 115 | reusable: true), 116 | wrapper); 117 | } 118 | 119 | @override 120 | void onAdd(T event) { 121 | _wrapper.latestValue = event; 122 | _persistValue(event); 123 | } 124 | 125 | @override 126 | ValueObservable get stream => this; 127 | 128 | /// Get the latest value emitted by the Subject 129 | @override 130 | T get value => _wrapper.latestValue; 131 | 132 | /// Set and emit the new value 133 | set value(T newValue) => add(newValue); 134 | 135 | /// Hydrates the HydratedSubject with a value stored on the user's device. 136 | /// 137 | /// Must be called to retreive values stored on the device. 138 | Future hydrateSubject() async { 139 | final file = await _localFile; 140 | 141 | var val; 142 | 143 | if (T == int) 144 | val = int.parse( await file.readAsString()); 145 | else if (T == double) 146 | val = double.parse(await file.readAsString()); 147 | else if (T == bool) 148 | val = (await file.readAsString()) == 'true'; 149 | else if (T == String) 150 | val = await file.readAsString(); 151 | else if (this._hydrate != null) 152 | val = await compute(this._hydrate, await file.readAsString()); 153 | else 154 | Exception( 155 | "HydratedSubject – shared_preferences returned an invalid type", 156 | ); 157 | 158 | // do not hydrate if the store is empty or matches the seed value 159 | // TODO: allow writing of seedValue if it is intentional 160 | if (val != null && val != _seedValue) { 161 | add(val); 162 | } 163 | isHydrated = true; 164 | if (onHydrate != null) { 165 | this.onHydrate(); 166 | } 167 | } 168 | 169 | _persistValue(T val) async { 170 | final file = await _localFile; 171 | 172 | if (val is int || val is double || val is bool || val is String) 173 | await file.writeAsString('$val'); 174 | else if (this._persist != null) 175 | await file.writeAsString(await compute(this._persist, val)); 176 | else 177 | Exception( 178 | "HydratedSubject – value must be int, double, bool, String, or List", 179 | ); 180 | } 181 | 182 | /// A unique key that references a storage container for a value persisted on the device. 183 | String get key => this._key; 184 | } 185 | 186 | class _Wrapper { 187 | T latestValue; 188 | 189 | _Wrapper(this.latestValue); 190 | } 191 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/widgets/add_user_page.dart'; 4 | import 'package:codestats_flutter/widgets/tab_navigator.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:flutter_midi/flutter_midi.dart'; 8 | 9 | void main() { 10 | runApp(CodeStatsApp()); 11 | SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); 12 | } 13 | 14 | class CodeStatsApp extends StatefulWidget { 15 | static const platform = MethodChannel('app.channel.shared.data'); 16 | 17 | @override 18 | CodeStatsAppState createState() => CodeStatsAppState(); 19 | } 20 | 21 | class CodeStatsAppState extends State 22 | with WidgetsBindingObserver { 23 | final UserBloc _bloc = UserBloc(); 24 | 25 | getIntentLastPathSegment({bool fetchAll = false}) async { 26 | String user; 27 | try { 28 | user = 29 | await CodeStatsApp.platform.invokeMethod("getIntentLastPathSegment"); 30 | 31 | } catch (e) {} 32 | print("getIntentLastPathSegment: $user"); 33 | 34 | var addUser = () { 35 | _bloc.addUser(user); 36 | }; 37 | 38 | if (user != null && user != "users") { 39 | if (_bloc.currentUserController.isHydrated) { 40 | addUser(); 41 | } else { 42 | _bloc.currentUserController.onHydrate = addUser; 43 | } 44 | } else if(fetchAll) { 45 | if (_bloc.currentUserController.isHydrated) { 46 | _bloc.fetchAllUsers(); 47 | } else { 48 | _bloc.currentUserController.onHydrate = () { 49 | _bloc.fetchAllUsers(); 50 | }; 51 | } 52 | } 53 | } 54 | 55 | void loadMidi(String asset) async { 56 | print("Loading File..."); 57 | FlutterMidi.unmute(); 58 | ByteData _byte = await rootBundle.load(asset); 59 | FlutterMidi.prepare(sf2: _byte, name: asset.replaceAll("midi/", "")); 60 | } 61 | 62 | @override 63 | void initState() { 64 | super.initState(); 65 | loadMidi("midi/zelda.sf2"); 66 | getIntentLastPathSegment(fetchAll: true); 67 | WidgetsBinding.instance.addObserver(this); 68 | } 69 | 70 | @override 71 | void dispose() { 72 | WidgetsBinding.instance.removeObserver(this); 73 | super.dispose(); 74 | } 75 | 76 | @override 77 | void didChangeAppLifecycleState(AppLifecycleState state) { 78 | super.didChangeAppLifecycleState(state); 79 | if (state == AppLifecycleState.resumed) { 80 | getIntentLastPathSegment(fetchAll: true); 81 | } 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return BlocProvider( 87 | bloc: _bloc, 88 | child: MaterialApp( 89 | debugShowCheckedModeBanner: false, 90 | title: 'Code::Stats', 91 | theme: ThemeData( 92 | textTheme: Typography(platform: TargetPlatform.android).white.apply( 93 | bodyColor: Colors.blueGrey[600], 94 | displayColor: Colors.blueGrey[600]), 95 | primarySwatch: Colors.blueGrey, 96 | ), 97 | initialRoute: "home", 98 | routes: { 99 | "home": (_) => TabNavigator(bloc: _bloc,), 100 | "addUser": (_) => AddUserPage(), 101 | }, 102 | ), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/models/pulse/pulse.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/models/pulse/xp.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'pulse.g.dart'; 5 | 6 | @JsonSerializable(nullable: true, useWrappers: true) 7 | class Pulse { 8 | final String machine; 9 | final String sent_at; 10 | final String sent_at_local; 11 | final List xps; 12 | 13 | Pulse(this.machine, this.sent_at, this.sent_at_local, this.xps); 14 | 15 | factory Pulse.fromJson(Map json) => _$PulseFromJson(json); 16 | 17 | Map toJson() => _$PulseToJson(this); 18 | } 19 | -------------------------------------------------------------------------------- /lib/models/pulse/pulse.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'pulse.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Pulse _$PulseFromJson(Map json) { 10 | return Pulse( 11 | json['machine'] as String, 12 | json['sent_at'] as String, 13 | json['sent_at_local'] as String, 14 | (json['xps'] as List) 15 | ?.map((e) => 16 | e == null ? null : PulseXp.fromJson(e as Map)) 17 | ?.toList()); 18 | } 19 | 20 | Map _$PulseToJson(Pulse instance) => 21 | _$PulseJsonMapWrapper(instance); 22 | 23 | class _$PulseJsonMapWrapper extends $JsonMapWrapper { 24 | final Pulse _v; 25 | _$PulseJsonMapWrapper(this._v); 26 | 27 | @override 28 | Iterable get keys => 29 | const ['machine', 'sent_at', 'sent_at_local', 'xps']; 30 | 31 | @override 32 | dynamic operator [](Object key) { 33 | if (key is String) { 34 | switch (key) { 35 | case 'machine': 36 | return _v.machine; 37 | case 'sent_at': 38 | return _v.sent_at; 39 | case 'sent_at_local': 40 | return _v.sent_at_local; 41 | case 'xps': 42 | return _v.xps; 43 | } 44 | } 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/models/pulse/xp.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'xp.g.dart'; 4 | 5 | @JsonSerializable(nullable: true, useWrappers: true) 6 | class PulseXp { 7 | final int amount; 8 | final String language; 9 | 10 | PulseXp(this.amount, this.language); 11 | 12 | factory PulseXp.fromJson(Map json) => _$PulseXpFromJson(json); 13 | 14 | Map toJson() => _$PulseXpToJson(this); 15 | 16 | @override 17 | String toString() => "${language}: ${amount} XP"; 18 | } 19 | -------------------------------------------------------------------------------- /lib/models/pulse/xp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'xp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | PulseXp _$PulseXpFromJson(Map json) { 10 | return PulseXp(json['amount'] as int, json['language'] as String); 11 | } 12 | 13 | Map _$PulseXpToJson(PulseXp instance) => 14 | _$PulseXpJsonMapWrapper(instance); 15 | 16 | class _$PulseXpJsonMapWrapper extends $JsonMapWrapper { 17 | final PulseXp _v; 18 | _$PulseXpJsonMapWrapper(this._v); 19 | 20 | @override 21 | Iterable get keys => const ['amount', 'language']; 22 | 23 | @override 24 | dynamic operator [](Object key) { 25 | if (key is String) { 26 | switch (key) { 27 | case 'amount': 28 | return _v.amount; 29 | case 'language': 30 | return _v.language; 31 | } 32 | } 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/models/user/day_language_xps.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'day_language_xps.g.dart'; 4 | 5 | @JsonSerializable(nullable: true, useWrappers: true) 6 | class DayLanguageXps { 7 | final int xp; 8 | final String language; 9 | final String date; 10 | 11 | DayLanguageXps(this.xp, this.language, this.date); 12 | 13 | factory DayLanguageXps.fromJson(Map json) => _$DayLanguageXpsFromJson(json); 14 | 15 | Map toJson() => _$DayLanguageXpsToJson(this); 16 | } -------------------------------------------------------------------------------- /lib/models/user/day_language_xps.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'day_language_xps.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | DayLanguageXps _$DayLanguageXpsFromJson(Map json) { 10 | return DayLanguageXps( 11 | json['xp'] as int, json['language'] as String, json['date'] as String); 12 | } 13 | 14 | Map _$DayLanguageXpsToJson(DayLanguageXps instance) => 15 | _$DayLanguageXpsJsonMapWrapper(instance); 16 | 17 | class _$DayLanguageXpsJsonMapWrapper extends $JsonMapWrapper { 18 | final DayLanguageXps _v; 19 | _$DayLanguageXpsJsonMapWrapper(this._v); 20 | 21 | @override 22 | Iterable get keys => const ['xp', 'language', 'date']; 23 | 24 | @override 25 | dynamic operator [](Object key) { 26 | if (key is String) { 27 | switch (key) { 28 | case 'xp': 29 | return _v.xp; 30 | case 'language': 31 | return _v.language; 32 | case 'date': 33 | return _v.date; 34 | } 35 | } 36 | return null; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/models/user/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/models/user/day_language_xps.dart'; 2 | import 'package:codestats_flutter/models/user/xp.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | 5 | part 'user.g.dart'; 6 | 7 | @JsonSerializable(nullable: true, useWrappers: true) 8 | class User { 9 | final List totalMachines; 10 | final List totalLangs; 11 | final List recentMachines; 12 | final List recentLangs; 13 | final Map hourOfDayXps; 14 | final Map dayOfYearXps; 15 | final List dayLanguageXps; 16 | final String registered; 17 | int totalXp; 18 | 19 | User( 20 | this.totalMachines, 21 | this.totalLangs, 22 | this.recentMachines, 23 | this.recentLangs, 24 | this.hourOfDayXps, 25 | this.dayOfYearXps, 26 | this.dayLanguageXps, 27 | this.totalXp, 28 | this.registered, 29 | ); 30 | 31 | factory User.fromJson(Map json) => _$UserFromJson(json); 32 | 33 | Map toJson() => _$UserToJson(this); 34 | } 35 | -------------------------------------------------------------------------------- /lib/models/user/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) { 10 | return User( 11 | (json['totalMachines'] as List) 12 | ?.map( 13 | (e) => e == null ? null : Xp.fromJson(e as Map)) 14 | ?.toList(), 15 | (json['totalLangs'] as List) 16 | ?.map( 17 | (e) => e == null ? null : Xp.fromJson(e as Map)) 18 | ?.toList(), 19 | (json['recentMachines'] as List) 20 | ?.map( 21 | (e) => e == null ? null : Xp.fromJson(e as Map)) 22 | ?.toList(), 23 | (json['recentLangs'] as List) 24 | ?.map( 25 | (e) => e == null ? null : Xp.fromJson(e as Map)) 26 | ?.toList(), 27 | (json['hourOfDayXps'] as Map) 28 | ?.map((k, e) => MapEntry(k, e as int)), 29 | (json['dayOfYearXps'] as Map) 30 | ?.map((k, e) => MapEntry(k, e as int)), 31 | (json['dayLanguageXps'] as List) 32 | ?.map((e) => e == null 33 | ? null 34 | : DayLanguageXps.fromJson(e as Map)) 35 | ?.toList(), 36 | json['totalXp'] as int, 37 | json['registered'] as String); 38 | } 39 | 40 | Map _$UserToJson(User instance) => 41 | _$UserJsonMapWrapper(instance); 42 | 43 | class _$UserJsonMapWrapper extends $JsonMapWrapper { 44 | final User _v; 45 | _$UserJsonMapWrapper(this._v); 46 | 47 | @override 48 | Iterable get keys => const [ 49 | 'totalMachines', 50 | 'totalLangs', 51 | 'recentMachines', 52 | 'recentLangs', 53 | 'hourOfDayXps', 54 | 'dayOfYearXps', 55 | 'dayLanguageXps', 56 | 'registered', 57 | 'totalXp' 58 | ]; 59 | 60 | @override 61 | dynamic operator [](Object key) { 62 | if (key is String) { 63 | switch (key) { 64 | case 'totalMachines': 65 | return _v.totalMachines; 66 | case 'totalLangs': 67 | return _v.totalLangs; 68 | case 'recentMachines': 69 | return _v.recentMachines; 70 | case 'recentLangs': 71 | return _v.recentLangs; 72 | case 'hourOfDayXps': 73 | return _v.hourOfDayXps; 74 | case 'dayOfYearXps': 75 | return _v.dayOfYearXps; 76 | case 'dayLanguageXps': 77 | return _v.dayLanguageXps; 78 | case 'registered': 79 | return _v.registered; 80 | case 'totalXp': 81 | return _v.totalXp; 82 | } 83 | } 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/models/user/xp.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'xp.g.dart'; 4 | 5 | @JsonSerializable(nullable: true, useWrappers: true) 6 | class Xp { 7 | int xp; 8 | final String name; 9 | 10 | Xp(this.xp, this.name); 11 | 12 | factory Xp.fromJson(Map json) => _$XpFromJson(json); 13 | 14 | Map toJson() => _$XpToJson(this); 15 | } 16 | -------------------------------------------------------------------------------- /lib/models/user/xp.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'xp.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Xp _$XpFromJson(Map json) { 10 | return Xp(json['xp'] as int, json['name'] as String); 11 | } 12 | 13 | Map _$XpToJson(Xp instance) => _$XpJsonMapWrapper(instance); 14 | 15 | class _$XpJsonMapWrapper extends $JsonMapWrapper { 16 | final Xp _v; 17 | _$XpJsonMapWrapper(this._v); 18 | 19 | @override 20 | Iterable get keys => const ['xp', 'name']; 21 | 22 | @override 23 | dynamic operator [](Object key) { 24 | if (key is String) { 25 | switch (key) { 26 | case 'xp': 27 | return _v.xp; 28 | case 'name': 29 | return _v.name; 30 | } 31 | } 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/passthrough_simulation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/physics.dart'; 2 | 3 | class PassThroughSimulation extends Simulation { 4 | 5 | final double reverse; 6 | 7 | PassThroughSimulation({this.reverse = 0}); 8 | 9 | @override 10 | double dx(double time) => (reverse - time).abs(); 11 | 12 | @override 13 | bool isDone(double time) { 14 | if (reverse == 0) { 15 | return false; 16 | } 17 | return reverse - time < 0; 18 | } 19 | 20 | @override 21 | double x(double time) => (reverse - time).abs(); 22 | } -------------------------------------------------------------------------------- /lib/queries.dart: -------------------------------------------------------------------------------- 1 | import 'package:date_format/date_format.dart'; 2 | 3 | String formatDateUtc(DateTime date) { 4 | return formatDate( 5 | date, [yyyy, "-", mm, "-", dd, "T", HH, ":", mm, ":", ss, ".", SSS, z]); 6 | } 7 | 8 | String profiles(List users, DateTime since, int recentDays) { 9 | var buffer = StringBuffer(); 10 | buffer.write("{\n"); 11 | 12 | users.forEach((user) => buffer.write(""" 13 | $user: profile(username: "$user") { 14 | ...ProfileInfo 15 | } 16 | """)); 17 | 18 | buffer.write("""} 19 | fragment ProfileInfo on Profile { 20 | totalXp: totalXp 21 | totalLangs: languages { 22 | name 23 | xp 24 | } 25 | recentLangs: languages(since: "${formatDateUtc(since.subtract(Duration(hours: 12)).toUtc())}") { 26 | name 27 | xp 28 | } 29 | totalMachines: machines { 30 | name 31 | xp 32 | } 33 | recentMachines: machines(since: "${formatDateUtc(since.subtract(Duration(hours: 12)).toUtc())}") { 34 | name 35 | xp 36 | } 37 | dayLanguageXps: dayLanguageXps(since: "${formatDate(since.subtract(Duration(days: recentDays)), [ 38 | yyyy, 39 | '-', 40 | mm, 41 | '-', 42 | dd 43 | ])}") { 44 | date 45 | language 46 | xp 47 | } 48 | dayOfYearXps: dayOfYearXps 49 | hourOfDayXps: hourOfDayXps 50 | registered: registered 51 | } 52 | """); 53 | 54 | return buffer.toString(); 55 | } 56 | -------------------------------------------------------------------------------- /lib/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: RootQueryType 3 | } 4 | 5 | """ISO 8601 date""" 6 | scalar Date 7 | 8 | """RFC3339 time with timezone""" 9 | scalar Datetime 10 | 11 | """User profile public data""" 12 | type Profile { 13 | """User's dates when they have been active and their XP""" 14 | dates(since: Date): [ProfileDate] 15 | 16 | """ 17 | User's dates since given date with their summed XP per language per day 18 | """ 19 | dayLanguageXps(since: Date!): [ProfileDaylanguage] 20 | 21 | """User's XP by day of week""" 22 | dayOfWeekXps(since: Date): [ProfileDowXp] 23 | 24 | """User's XP by day of year""" 25 | dayOfYearXps: ProfileDayOfYearXp 26 | 27 | """User's XP by hour of day (in 24 hour format)""" 28 | hourOfDayXps: ProfileHourOfDayXp 29 | 30 | """User's languages and their XP""" 31 | languages(since: Datetime): [ProfileLanguage] 32 | 33 | """User's machines and their XP""" 34 | machines(since: Datetime): [ProfileMachine] 35 | 36 | """Timestamp when user registered into service""" 37 | registered: Datetime 38 | 39 | """Total amount of XP of user""" 40 | totalXp: Int 41 | } 42 | 43 | """Date when user was active and its total XP for a profile""" 44 | type ProfileDate { 45 | date: String 46 | xp: Int 47 | } 48 | 49 | """Date when profile has an amount of XP of the language""" 50 | type ProfileDaylanguage { 51 | date: Date 52 | language: String 53 | xp: Int 54 | } 55 | 56 | """ 57 | Map where key is number of day of year (including leap day, like in the year 2000) and value is amount of XP 58 | """ 59 | scalar ProfileDayOfYearXp 60 | 61 | """Day of week and its combined XP""" 62 | type ProfileDowXp { 63 | day: Int 64 | xp: Int 65 | } 66 | 67 | """ 68 | Map where key is hour of day (in 24 hour format) and value is amount of XP 69 | """ 70 | scalar ProfileHourOfDayXp 71 | 72 | """Language and its total XP for a profile""" 73 | type ProfileLanguage { 74 | name: String 75 | xp: Int 76 | } 77 | 78 | """Machine and its total XP for a profile""" 79 | type ProfileMachine { 80 | name: String 81 | xp: Int 82 | } 83 | 84 | type RootQueryType { 85 | """Get profile by username""" 86 | profile( 87 | """Username of profile""" 88 | username: String 89 | ): Profile 90 | } 91 | 92 | -------------------------------------------------------------------------------- /lib/sequence_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | class _AnimationInformation { 5 | _AnimationInformation({ 6 | this.animatable, 7 | this.from, 8 | this.to, 9 | this.curve, 10 | this.tag, 11 | }); 12 | 13 | final Animatable animatable; 14 | final Duration from; 15 | final Duration to; 16 | final Curve curve; 17 | final Object tag; 18 | } 19 | 20 | class SequenceAnimationBuilder { 21 | List<_AnimationInformation> _animations = []; 22 | 23 | /// Adds an [Animatable] to the sequence, in the most cases this would be a [Tween]. 24 | /// The from and to [Duration] specify points in time where the animation takes place. 25 | /// You can also specify a [Curve] for the [Animatable]. 26 | /// 27 | /// [Animatable]s which animate on the same tag are not allowed to overlap and they also need to be add in the same order they are played. 28 | /// These restrictions only apply to [Animatable]s operating on the same tag. 29 | /// 30 | /// 31 | /// ## Sample code 32 | /// 33 | /// ```dart 34 | /// SequenceAnimation sequenceAnimation = new SequenceAnimationBuilder() 35 | /// .addAnimatable( 36 | /// animatable: new ColorTween(begin: Colors.red, end: Colors.yellow), 37 | /// from: const Duration(seconds: 0), 38 | /// to: const Duration(seconds: 2), 39 | /// tag: "color", 40 | /// ) 41 | /// .animate(controller); 42 | /// ``` 43 | /// 44 | SequenceAnimationBuilder addAnimatable({ 45 | @required Animatable animatable, 46 | @required Duration from, 47 | @required Duration to, 48 | Curve curve: Curves.linear, 49 | @required Object tag, 50 | }) { 51 | assert(to >= from); 52 | _animations.add(_AnimationInformation( 53 | animatable: animatable, from: from, to: to, curve: curve, tag: tag)); 54 | return this; 55 | } 56 | 57 | /// The controllers duration is going to be overwritten by this class, you should not specify it on your own 58 | SequenceAnimation animate(AnimationController controller) { 59 | int longestTimeMicro = 0; 60 | _animations.forEach((info) { 61 | int micro = info.to.inMicroseconds; 62 | if (micro > longestTimeMicro) { 63 | longestTimeMicro = micro; 64 | } 65 | }); 66 | // Sets the duration of the controller 67 | controller.duration = Duration(microseconds: longestTimeMicro); 68 | 69 | Map animatables = {}; 70 | Map begins = {}; 71 | Map ends = {}; 72 | 73 | _animations.forEach((info) { 74 | assert(info.to.inMicroseconds <= longestTimeMicro); 75 | 76 | double begin = info.from.inMicroseconds / longestTimeMicro; 77 | double end = info.to.inMicroseconds / longestTimeMicro; 78 | Interval intervalCurve = Interval(begin, end, curve: info.curve); 79 | if (animatables[info.tag] == null) { 80 | animatables[info.tag] = 81 | IntervalAnimatable.chainCurve(info.animatable, intervalCurve); 82 | begins[info.tag] = begin; 83 | ends[info.tag] = end; 84 | } else { 85 | assert( 86 | ends[info.tag] <= begin, 87 | "When animating the same property you need to: \n" 88 | "a) Have them not overlap \n" 89 | "b) Add them in an ordered fashion"); 90 | animatables[info.tag] = IntervalAnimatable( 91 | animatable: animatables[info.tag], 92 | defaultAnimatable: 93 | IntervalAnimatable.chainCurve(info.animatable, intervalCurve), 94 | begin: begins[info.tag], 95 | end: ends[info.tag], 96 | ); 97 | ends[info.tag] = end; 98 | } 99 | }); 100 | 101 | Map result = {}; 102 | 103 | animatables.forEach((tag, animInfo) { 104 | result[tag] = animInfo.animate(controller); 105 | }); 106 | 107 | return SequenceAnimation._internal(result); 108 | } 109 | } 110 | 111 | class SequenceAnimation { 112 | final Map _animations; 113 | 114 | /// Use the [SequenceAnimationBuilder] to construct this class. 115 | SequenceAnimation._internal(this._animations); 116 | 117 | /// Returns the animation with a given tag, this animation is tied to the controller. 118 | Animation operator [](Object key) { 119 | assert(_animations.containsKey(key), 120 | "There was no animatable with the key: $key"); 121 | return _animations[key]; 122 | } 123 | } 124 | 125 | /// Evaluates [animatable] if the animation is in the time-frame of [begin] (inclusive) and [end] (inclusive), 126 | /// if not it evaluates the [defaultAnimatable] 127 | class IntervalAnimatable extends Animatable { 128 | IntervalAnimatable({ 129 | @required this.animatable, 130 | @required this.defaultAnimatable, 131 | @required this.begin, 132 | @required this.end, 133 | }); 134 | 135 | final Animatable animatable; 136 | final Animatable defaultAnimatable; 137 | 138 | /// The relative begin to of [animatable] 139 | /// If your [AnimationController] is running from 0->1, this needs to be a value between those two 140 | final double begin; 141 | 142 | /// The relative end to of [animatable] 143 | /// If your [AnimationController] is running from 0->1, this needs to be a value between those two 144 | final double end; 145 | 146 | /// Chains an [Animatable] with a [CurveTween] and the given [Interval]. 147 | /// Basically, the animation is being constrained to the given interval 148 | static Animatable chainCurve(Animatable parent, Interval interval) { 149 | return parent.chain(CurveTween(curve: interval)); 150 | } 151 | 152 | @override 153 | T transform(double t) { 154 | if (t >= begin && t <= end) { 155 | return animatable.transform(t); 156 | } else { 157 | return defaultAnimatable.transform(t); 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:codestats_flutter/models/user/user.dart'; 4 | import 'package:intl/intl.dart' show NumberFormat; 5 | import 'package:superpower/superpower.dart'; 6 | 7 | int getLevel(int xp) => (0.025 * sqrt(xp)).floor(); 8 | 9 | int getXp(int level) => pow(level / 0.025, 2).round(); 10 | 11 | int getRecentXp(User userModel) => 12 | $(userModel.recentMachines).sumBy((elem) => elem.xp).floor(); 13 | 14 | final _formatter = NumberFormat("#,###"); 15 | String formatNumber(num num) => _formatter.format(num); -------------------------------------------------------------------------------- /lib/widgets/Snappable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | import 'dart:typed_data'; 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/rendering.dart'; 8 | import 'package:image/image.dart' as image; 9 | 10 | class Snappable extends StatefulWidget { 11 | /// Widget to be snapped 12 | final Widget child; 13 | 14 | /// Direction and range of snap effect 15 | /// (Where and how far will particles go) 16 | final Offset offset; 17 | 18 | /// Duration of whole snap animation 19 | final Duration duration; 20 | 21 | /// How much can particle be randomized, 22 | /// For example if [offset] is (100, 100) and [randomDislocationOffset] is (10,10), 23 | /// Each layer can be moved to maximum between 90 and 110. 24 | final Offset randomDislocationOffset; 25 | 26 | /// Number of layers of images, 27 | /// The more of them the better effect but the more heavy it is for CPU 28 | final int numberOfBuckets; 29 | 30 | /// Quick helper to snap widgets when touched 31 | /// If true wraps the widget in [GestureDetector] and starts [snap] when tapped 32 | /// Defaults to false 33 | final bool snapOnTap; 34 | 35 | /// Function that gets called when snap ends 36 | final VoidCallback onSnapped; 37 | 38 | const Snappable({ 39 | Key key, 40 | @required this.child, 41 | this.offset = const Offset(64, -32), 42 | this.duration = const Duration(milliseconds: 5000), 43 | this.randomDislocationOffset = const Offset(64, 32), 44 | this.numberOfBuckets = 16, 45 | this.snapOnTap = false, 46 | this.onSnapped, 47 | }) : super(key: key); 48 | 49 | @override 50 | SnappableState createState() => SnappableState(); 51 | } 52 | 53 | class SnappableState extends State 54 | with SingleTickerProviderStateMixin { 55 | static const double _singleLayerAnimationLength = 0.6; 56 | static const double _lastLayerAnimationStart = 57 | 1 - _singleLayerAnimationLength; 58 | 59 | bool get isGone => _animationController.isCompleted; 60 | 61 | /// Main snap effect controller 62 | AnimationController _animationController; 63 | 64 | /// Key to get image of a [widget.child] 65 | GlobalKey _globalKey = GlobalKey(); 66 | 67 | /// Layers of image 68 | List _layers; 69 | 70 | /// Values from -1 to 1 to dislocate the layers a bit 71 | List _randoms; 72 | 73 | /// Size of child widget 74 | Size size; 75 | 76 | @override 77 | void initState() { 78 | super.initState(); 79 | _animationController = AnimationController( 80 | vsync: this, 81 | duration: widget.duration, 82 | ); 83 | 84 | if (widget.onSnapped != null) { 85 | _animationController.addStatusListener((status) { 86 | if (status == AnimationStatus.completed) widget.onSnapped(); 87 | }); 88 | } 89 | } 90 | 91 | @override 92 | void dispose() { 93 | _animationController.dispose(); 94 | super.dispose(); 95 | } 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return GestureDetector( 100 | onTap: widget.snapOnTap ? () => isGone ? reset() : snap() : null, 101 | child: Stack( 102 | children: [ 103 | if (_layers != null) ..._layers.map(_imageToWidget), 104 | AnimatedBuilder( 105 | animation: _animationController, 106 | builder: (context, child) { 107 | return _animationController.isDismissed ? child : Container(); 108 | }, 109 | child: RepaintBoundary( 110 | key: _globalKey, 111 | child: widget.child, 112 | ), 113 | ) 114 | ], 115 | ), 116 | ); 117 | } 118 | 119 | /// I am... INEVITABLE ~Thanos 120 | Future snap() async { 121 | //get image from child 122 | if (!_animationController.isAnimating) { 123 | final fullImage = await _getImageFromWidget(); 124 | 125 | //create an image for every bucket 126 | List _images = List.generate( 127 | widget.numberOfBuckets, 128 | (i) => image.Image(fullImage.width, fullImage.height), 129 | ); 130 | 131 | //for every line of pixels 132 | for (int y = 0; y < fullImage.height; y++) { 133 | //generate weight list of probabilities determining 134 | //to which bucket should given pixels go 135 | List weights = List.generate( 136 | widget.numberOfBuckets, 137 | (bucket) => _gauss( 138 | y / fullImage.height, 139 | bucket / widget.numberOfBuckets, 140 | ), 141 | ); 142 | int sumOfWeights = weights.fold(0, (sum, el) => sum + el); 143 | 144 | //for every pixel in a line 145 | for (int x = 0; x < fullImage.width; x++) { 146 | //get the pixel from fullImage 147 | int pixel = fullImage.getPixel(x, y); 148 | //choose a bucket for a pixel 149 | int imageIndex = _pickABucket(weights, sumOfWeights); 150 | //set the pixel from chosen bucket 151 | _images[imageIndex].setPixel(x, y, pixel); 152 | } 153 | } 154 | 155 | _layers = await compute, List>( 156 | _encodeImages, _images); 157 | 158 | //prepare random dislocations and set state 159 | setState(() { 160 | _randoms = List.generate( 161 | widget.numberOfBuckets, 162 | (i) => (math.Random().nextDouble() - 0.5) * 2, 163 | ); 164 | }); 165 | 166 | //give a short delay to draw images 167 | await Future.delayed(Duration(milliseconds: 100)); 168 | 169 | //start the snap! 170 | _animationController.forward(); 171 | } 172 | } 173 | 174 | /// I am... IRON MAN ~Tony Stark 175 | void reset() { 176 | setState(() { 177 | _layers = null; 178 | _animationController.reset(); 179 | }); 180 | } 181 | 182 | Widget _imageToWidget(Uint8List layer) { 183 | //get layer's index in the list 184 | int index = _layers.indexOf(layer); 185 | 186 | //based on index, calculate when this layer should start and end 187 | double animationStart = (index / _layers.length) * _lastLayerAnimationStart; 188 | double animationEnd = animationStart + _singleLayerAnimationLength; 189 | 190 | //create interval animation using only part of whole animation 191 | CurvedAnimation animation = CurvedAnimation( 192 | parent: _animationController, 193 | curve: Interval( 194 | animationStart, 195 | animationEnd, 196 | curve: Curves.easeOut, 197 | ), 198 | ); 199 | 200 | Offset randomOffset = widget.randomDislocationOffset.scale( 201 | _randoms[index], 202 | _randoms[index], 203 | ); 204 | 205 | Animation offsetAnimation = Tween( 206 | begin: Offset.zero, 207 | end: widget.offset + randomOffset, 208 | ).animate(animation); 209 | 210 | return AnimatedBuilder( 211 | animation: _animationController, 212 | child: Image.memory(layer), 213 | builder: (context, child) { 214 | return Transform.translate( 215 | offset: offsetAnimation.value, 216 | child: Opacity( 217 | opacity: math.cos(animation.value * math.pi / 2), 218 | child: child, 219 | ), 220 | ); 221 | }, 222 | ); 223 | } 224 | 225 | /// Returns index of a randomly chosen bucket 226 | int _pickABucket(List weights, int sumOfWeights) { 227 | int rnd = math.Random().nextInt(sumOfWeights); 228 | int chosenImage = 0; 229 | for (int i = 0; i < widget.numberOfBuckets; i++) { 230 | if (rnd < weights[i]) { 231 | chosenImage = i; 232 | break; 233 | } 234 | rnd -= weights[i]; 235 | } 236 | return chosenImage; 237 | } 238 | 239 | /// Gets an Image from a [child] and caches [size] for later us 240 | Future _getImageFromWidget() async { 241 | RenderRepaintBoundary boundary = 242 | _globalKey.currentContext.findRenderObject(); 243 | //cache image for later 244 | size = boundary.size; 245 | var img = await boundary.toImage(); 246 | var byteData = await img.toByteData(format: ImageByteFormat.png); 247 | var pngBytes = byteData.buffer.asUint8List(); 248 | 249 | return image.decodeImage(pngBytes); 250 | } 251 | 252 | int _gauss(double center, double value) => 253 | (1000 * math.exp(-(math.pow((value - center), 2) / 0.14))).round(); 254 | } 255 | 256 | /// This is slow! Run it in separate isolate 257 | List _encodeImages(List images) { 258 | return images.map((img) => Uint8List.fromList(image.encodePng(img))).toList(); 259 | } 260 | -------------------------------------------------------------------------------- /lib/widgets/add_user_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/utils.dart'; 4 | import 'package:codestats_flutter/widgets/random_loading_animation.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class AddUserPage extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | final UserBloc _userBloc = BlocProvider.of(context); 11 | final TextEditingController _textEditingController = 12 | TextEditingController(); 13 | 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text("Add user"), 17 | ), 18 | body: Padding( 19 | padding: const EdgeInsets.all(8.0), 20 | child: Column( 21 | children: [ 22 | TextField( 23 | textInputAction: TextInputAction.go, 24 | controller: _textEditingController, 25 | style: TextStyle(fontSize: 24, color: Colors.blueGrey), 26 | decoration: InputDecoration( 27 | icon: Icon(Icons.account_circle), 28 | labelText: 'Username', 29 | ), 30 | onChanged: _userBloc.searchUser.add, 31 | ), 32 | Padding( 33 | padding: const EdgeInsets.all(8.0), 34 | child: StreamBuilder( 35 | stream: _userBloc.userValidation, 36 | builder: (context, AsyncSnapshot snapshot) { 37 | if (snapshot.data == ValidUser.Unknown || 38 | snapshot.data == ValidUser.Valid) { 39 | return Container(); 40 | } else if (snapshot.data == ValidUser.Error) { 41 | return Text( 42 | "Network error", 43 | textAlign: TextAlign.center, 44 | style: TextStyle( 45 | color: Colors.blueGrey, 46 | ), 47 | ); 48 | } else if (snapshot.data == ValidUser.Invalid) { 49 | return Column( 50 | children: [ 51 | Text( 52 | "No user named:", 53 | textAlign: TextAlign.center, 54 | style: TextStyle( 55 | fontSize: 24, 56 | color: Colors.blueGrey, 57 | ), 58 | ), 59 | Text( 60 | "'${_textEditingController.text.trim()}'", 61 | textAlign: TextAlign.center, 62 | style: TextStyle( 63 | fontSize: 24, 64 | color: Colors.blueGrey, 65 | ), 66 | ), 67 | ], 68 | ); 69 | } else { 70 | return RandomLoadingAnimation( 71 | size: 24, 72 | ); 73 | } 74 | }, 75 | ), 76 | ), 77 | StreamBuilder( 78 | stream: _userBloc.searchResult, 79 | builder: 80 | (context, AsyncSnapshot> snapshot) { 81 | if (snapshot.hasData) { 82 | var totalXp = snapshot.data["total_xp"] ?? 0; 83 | var userName = snapshot.data["user"]; 84 | 85 | return Card( 86 | child: ListTile( 87 | title: Text( 88 | snapshot.data["user"] ?? "Empty search result"), 89 | subtitle: Text( 90 | "Level ${getLevel(totalXp)}, ${formatNumber(totalXp)} XP", 91 | style: TextStyle(color: Colors.blueGrey), 92 | ), 93 | trailing: userName != null 94 | ? IconButton( 95 | icon: Icon(Icons.add_circle_outline), 96 | onPressed: () { 97 | 98 | _userBloc.setUserValidation 99 | .add(ValidUser.Unknown); 100 | _userBloc.addUser(userName); 101 | _textEditingController.clear(); 102 | Navigator.of(context).pop(); 103 | }, 104 | ) 105 | : null, 106 | ), 107 | ); 108 | } else 109 | return Container(); 110 | }, 111 | ), 112 | ], 113 | ), 114 | )); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/widgets/backdrop.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class Backdrop extends InheritedWidget { 6 | final BackdropScaffoldState data; 7 | 8 | Backdrop({Key key, @required this.data, @required Widget child}) 9 | : super(key: key, child: child); 10 | 11 | static BackdropScaffoldState of(BuildContext context) => 12 | (context.inheritFromWidgetOfExactType(Backdrop) as Backdrop).data; 13 | 14 | @override 15 | bool updateShouldNotify(Backdrop old) => true; 16 | } 17 | 18 | class BackdropScaffold extends StatefulWidget { 19 | final AnimationController controller; 20 | final Widget title; 21 | final Widget backLayer; 22 | final Widget frontLayer; 23 | final Widget bottomNavigationBar; 24 | final List actions; 25 | final double headerHeight; 26 | final BorderRadius frontLayerBorderRadius; 27 | final BackdropIconPosition iconPosition; 28 | final TabBar appbarBottom; 29 | 30 | BackdropScaffold({ 31 | Key key, 32 | this.controller, 33 | this.title, 34 | this.backLayer, 35 | this.frontLayer, 36 | this.actions = const [], 37 | this.headerHeight = 32.0, 38 | this.frontLayerBorderRadius = const BorderRadius.only( 39 | topLeft: Radius.circular(16.0), 40 | topRight: Radius.circular(16.0), 41 | ), 42 | this.iconPosition = BackdropIconPosition.leading, 43 | this.bottomNavigationBar, this.appbarBottom, 44 | }) : super(key: key); 45 | 46 | @override 47 | BackdropScaffoldState createState() => BackdropScaffoldState(); 48 | } 49 | 50 | class BackdropScaffoldState extends State 51 | with SingleTickerProviderStateMixin { 52 | bool shouldDisposeController = false; 53 | AnimationController _controller; 54 | final scaffoldKey = GlobalKey(); 55 | 56 | AnimationController get controller => _controller; 57 | 58 | @override 59 | void initState() { 60 | super.initState(); 61 | if (widget.controller == null) { 62 | shouldDisposeController = true; 63 | _controller = AnimationController( 64 | vsync: this, duration: Duration(milliseconds: 100), value: 1.0); 65 | } else { 66 | _controller = widget.controller; 67 | } 68 | } 69 | 70 | @override 71 | void dispose() { 72 | super.dispose(); 73 | if (shouldDisposeController) { 74 | _controller.dispose(); 75 | } 76 | } 77 | 78 | bool get isTopPanelVisible { 79 | final AnimationStatus status = controller.status; 80 | return status == AnimationStatus.completed || 81 | status == AnimationStatus.forward; 82 | } 83 | 84 | bool get isBackPanelVisible { 85 | final AnimationStatus status = controller.status; 86 | return status == AnimationStatus.dismissed || 87 | status == AnimationStatus.reverse; 88 | } 89 | 90 | void fling() { 91 | controller.fling(velocity: isTopPanelVisible ? -1.0 : 1.0); 92 | } 93 | 94 | void showBackLayer() { 95 | if (isTopPanelVisible) { 96 | controller.fling(velocity: -1.0); 97 | } 98 | } 99 | 100 | void showFrontLayer() { 101 | if (isBackPanelVisible) { 102 | controller.fling(velocity: 1.0); 103 | } 104 | } 105 | 106 | Animation getPanelAnimation( 107 | BuildContext context, BoxConstraints constraints) { 108 | final height = constraints.biggest.height; 109 | final backPanelHeight = height - widget.headerHeight; 110 | final frontPanelHeight = -backPanelHeight; 111 | 112 | return RelativeRectTween( 113 | begin: RelativeRect.fromLTRB(0.0, backPanelHeight, 0.0, frontPanelHeight), 114 | end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), 115 | ).animate(CurvedAnimation( 116 | parent: controller, 117 | curve: Curves.linear, 118 | )); 119 | } 120 | 121 | Widget _buildInactiveLayer(BuildContext context) { 122 | return Visibility( 123 | visible: _controller.status == AnimationStatus.dismissed, 124 | child: GestureDetector( 125 | onTap: () => fling(), 126 | behavior: HitTestBehavior.opaque, 127 | child: SizedBox.expand( 128 | child: Container( 129 | decoration: BoxDecoration( 130 | color: Colors.grey.shade200.withOpacity(0.7), 131 | ), 132 | ), 133 | ), 134 | ), 135 | ); 136 | } 137 | 138 | Widget _buildBackPanel() { 139 | var status = _controller.status; 140 | return Visibility( 141 | replacement: SizedBox.expand(), 142 | visible: status == AnimationStatus.dismissed || 143 | status == AnimationStatus.reverse || 144 | status == AnimationStatus.forward, 145 | child: Material( 146 | color: Theme.of(context).primaryColor, 147 | child: widget.backLayer, 148 | ), 149 | ); 150 | } 151 | 152 | Widget _buildFrontPanel(BuildContext context) { 153 | return Material( 154 | elevation: 12.0, 155 | borderRadius: widget.frontLayerBorderRadius, 156 | child: Stack( 157 | children: [ 158 | widget.frontLayer, 159 | _buildInactiveLayer(context), 160 | ], 161 | ), 162 | ); 163 | } 164 | 165 | Future _willPopCallback(BuildContext context) async { 166 | if (isBackPanelVisible) { 167 | showFrontLayer(); 168 | return null; 169 | } 170 | return true; 171 | } 172 | 173 | Widget _buildBody(BuildContext context) { 174 | return WillPopScope( 175 | onWillPop: () => _willPopCallback(context), 176 | child: Scaffold( 177 | key: scaffoldKey, 178 | appBar: AppBar( 179 | title: widget.title, 180 | actions: widget.iconPosition == BackdropIconPosition.action 181 | ? [BackdropToggleButton()] + widget.actions 182 | : widget.actions, 183 | elevation: 0.0, 184 | leading: widget.iconPosition == BackdropIconPosition.leading 185 | ? BackdropToggleButton() 186 | : null, 187 | bottom: widget.appbarBottom, 188 | ), 189 | body: LayoutBuilder( 190 | builder: (context, constraints) { 191 | return Container( 192 | child: Stack( 193 | children: [ 194 | _buildBackPanel(), 195 | PositionedTransition( 196 | rect: getPanelAnimation(context, constraints), 197 | child: _buildFrontPanel(context), 198 | ), 199 | ], 200 | ), 201 | ); 202 | }, 203 | ), 204 | bottomNavigationBar: widget.bottomNavigationBar, 205 | ), 206 | ); 207 | } 208 | 209 | @override 210 | Widget build(BuildContext context) { 211 | return Backdrop( 212 | data: this, 213 | child: Builder( 214 | builder: (context) => _buildBody(context), 215 | ), 216 | ); 217 | } 218 | } 219 | 220 | class BackdropToggleButton extends StatelessWidget { 221 | final AnimatedIconData icon; 222 | 223 | const BackdropToggleButton({ 224 | this.icon = AnimatedIcons.close_menu, 225 | }); 226 | 227 | @override 228 | Widget build(BuildContext context) { 229 | return IconButton( 230 | icon: AnimatedIcon( 231 | icon: icon, 232 | progress: Backdrop.of(context).controller.view, 233 | ), 234 | onPressed: () { 235 | //FocusScope.of(context).requestFocus(FocusNode()); 236 | Backdrop.of(context).fling(); 237 | }, 238 | ); 239 | } 240 | } 241 | 242 | enum BackdropIconPosition { none, leading, action } 243 | -------------------------------------------------------------------------------- /lib/widgets/bouncable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/physics.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | typedef OnTap = void Function(); 6 | 7 | class Bouncable extends StatefulWidget { 8 | final Widget child; 9 | 10 | const Bouncable({Key key, this.child, this.onTap}) : super(key: key); 11 | final OnTap onTap; 12 | 13 | @override 14 | _BouncableState createState() => _BouncableState(); 15 | } 16 | 17 | class _BouncableState extends State 18 | with SingleTickerProviderStateMixin { 19 | AnimationController depth; 20 | SpringSimulation springSimulation; 21 | static SpringDescription spring = 22 | SpringDescription(mass: 1, stiffness: 400, damping: 6); 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | depth = AnimationController( 28 | value: 0, 29 | duration: const Duration(milliseconds: 500), 30 | vsync: this, 31 | lowerBound: -2, 32 | upperBound: 2, 33 | ); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | depth.dispose(); 39 | super.dispose(); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | return GestureDetector( 45 | onTapDown: (_) { 46 | if(widget.onTap != null) 47 | widget.onTap(); 48 | depth.animateWith(SpringSimulation(spring, depth.value, 0, -30)); 49 | }, 50 | child: AnimatedBuilder( 51 | animation: depth, 52 | builder: (_, __) => 53 | Transform.scale( 54 | scale: depth.value * 0.1 + 1, 55 | child: widget.child, 56 | ), 57 | ), 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/widgets/breathing_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BreathingWidget extends StatefulWidget { 4 | final Widget child; 5 | 6 | const BreathingWidget({Key key, @required this.child}) : super(key: key); 7 | 8 | @override 9 | _BreathingWidgetState createState() => _BreathingWidgetState(); 10 | } 11 | 12 | class _BreathingWidgetState extends State 13 | with TickerProviderStateMixin { 14 | AnimationController _breathingController; 15 | AnimationController _pulseController; 16 | Animation _animation; 17 | var _breathe = 0.0; 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | _breathingController = 23 | AnimationController(vsync: this, duration: Duration(seconds: 1)); 24 | _pulseController = AnimationController( 25 | vsync: this, duration: Duration(milliseconds: 1500)); 26 | 27 | _breathingController.addStatusListener((status) { 28 | if (status == AnimationStatus.completed) { 29 | _breathingController.reverse(); 30 | } else if (status == AnimationStatus.dismissed) { 31 | _breathingController.forward(); 32 | } 33 | }); 34 | 35 | _animation = CurvedAnimation( 36 | parent: _breathingController, 37 | curve: Curves.fastOutSlowIn, 38 | reverseCurve: Curves.fastOutSlowIn.flipped); 39 | 40 | _breathingController.addListener(() => setState(() { 41 | if (_breathingController.status == AnimationStatus.forward && 42 | _animation.value > 0.3) { 43 | if (_pulseController.status != AnimationStatus.forward) { 44 | _pulseController.value = 0.0; 45 | } 46 | _pulseController.forward(); 47 | } 48 | _breathe = _animation.value; 49 | })); 50 | 51 | _breathingController.forward(); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | _breathingController.dispose(); 57 | _pulseController.dispose(); 58 | super.dispose(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return CustomPaint( 64 | painter: PulsePainter(Curves.easeOut.transform(_pulseController.value)), 65 | child: Transform.scale( 66 | scale: 0.95 + (0.1 * _breathe), 67 | child: widget.child, 68 | ), 69 | ); 70 | } 71 | } 72 | 73 | class PulsePainter extends CustomPainter { 74 | final double scale; 75 | 76 | PulsePainter(this.scale); 77 | 78 | @override 79 | void paint(Canvas canvas, Size size) { 80 | var rect = 81 | Offset.zero.translate(-(size.width * 2.5), -5 - (size.height * 2.5)) & 82 | (size * 6); 83 | 84 | var dynamicColor = Colors.grey.withOpacity( 85 | 1.0 - (scale * 0.2 + 0.85).clamp(0.0, 1.0), 86 | ); 87 | 88 | var gradient = RadialGradient( 89 | colors: [ 90 | Colors.transparent.withOpacity(0.0), 91 | dynamicColor, 92 | Colors.transparent.withOpacity(0.0), 93 | dynamicColor, 94 | Colors.transparent.withOpacity(0.0), 95 | ], 96 | stops: [ 97 | scale, 98 | (scale + 0.05).clamp(0.0, 1.0), 99 | (scale + 0.07).clamp(0.0, 1.0), 100 | (scale + 0.10).clamp(0.0, 1.0), 101 | (scale + 0.2).clamp(0.0, 1.0), 102 | ], 103 | ); 104 | 105 | canvas.drawRect( 106 | rect, 107 | Paint() 108 | ..shader = gradient.createShader(rect) 109 | ..blendMode = BlendMode.srcATop); 110 | } 111 | 112 | @override 113 | bool shouldRepaint(CustomPainter oldDelegate) => true; 114 | } 115 | -------------------------------------------------------------------------------- /lib/widgets/choose_user_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/bloc/state.dart'; 4 | import 'package:codestats_flutter/widgets/backdrop.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class ChooseUserMenu extends StatelessWidget { 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | UserBloc bloc = BlocProvider.of(context); 12 | 13 | return StreamBuilder( 14 | stream: bloc.userStateController, 15 | builder: (context, AsyncSnapshot snapshot) => 16 | snapshot.hasData && snapshot.data.allUsers.isNotEmpty 17 | ? PopupMenuButton( 18 | icon: Icon(Icons.people), 19 | onSelected: (String user) { 20 | Backdrop.of(context).showFrontLayer(); 21 | bloc.selectUser.add(user); 22 | }, 23 | itemBuilder: (BuildContext context) => 24 | snapshot.data.allUsers.keys 25 | .map((user) => PopupMenuItem( 26 | value: user, 27 | child: Text(user), 28 | )) 29 | .toList(), 30 | ) 31 | : Container(), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/widgets/dash_board_body.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 2 | import 'package:codestats_flutter/widgets/day_language_xps.dart'; 3 | import 'package:codestats_flutter/widgets/day_of_year_xps.dart'; 4 | import 'package:codestats_flutter/widgets/language_levels.dart'; 5 | import 'package:codestats_flutter/widgets/no_user.dart'; 6 | import 'package:codestats_flutter/widgets/profile_page.dart'; 7 | import 'package:codestats_flutter/widgets/random_loading_animation.dart'; 8 | import 'package:flutter/material.dart'; 9 | 10 | class DashBoardBody extends StatefulWidget { 11 | final UserBloc bloc; 12 | 13 | const DashBoardBody({Key key, @required this.bloc}) : super(key: key); 14 | 15 | @override 16 | _DashBoardBodyState createState() => _DashBoardBodyState(); 17 | } 18 | 19 | class _DashBoardBodyState extends State { 20 | @override 21 | Widget build(BuildContext context) { 22 | return StreamBuilder( 23 | stream: widget.bloc.currentUser, 24 | initialData: UserWrap(), 25 | builder: (BuildContext context, AsyncSnapshot snapshot) { 26 | var user = snapshot.data; 27 | if (user.name != null && user.name.isNotEmpty && user.data == null) { 28 | return Center( 29 | child: RandomLoadingAnimation(), 30 | ); 31 | } else if (user.name == null || user.name.isEmpty) { 32 | widget.bloc.selectNextUser(); 33 | return NoUser(); 34 | } else { 35 | return TabBarView( 36 | children: [ 37 | ProfilePage( 38 | user: user, 39 | ), 40 | DayLanguageXpsWidget( 41 | userModel: user.data, 42 | ), 43 | LanguageLevelPage( 44 | userModel: user.data, 45 | ), 46 | DayOfYearXps( 47 | userModel: user.data, 48 | scrollController: ScrollController(), 49 | ) 50 | ], 51 | ); 52 | } 53 | }, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/widgets/day_language_xps.dart: -------------------------------------------------------------------------------- 1 | import 'package:charts_common/common.dart' 2 | show 3 | LinePointHighlighterFollowLineType, 4 | SelectionTrigger, 5 | SmallTickRendererSpec, 6 | TickLabelAnchor, 7 | TextStyleSpec, 8 | LegendDefaultMeasure, 9 | DayTickProviderSpec; 10 | import 'package:charts_flutter/flutter.dart' as charts 11 | show 12 | TimeSeriesChart, 13 | Series, 14 | BarRendererConfig, 15 | BarGroupingType, 16 | NumericAxisSpec, 17 | BasicNumericTickFormatterSpec, 18 | LinePointHighlighter, 19 | DomainHighlighter, 20 | SeriesLegend, 21 | DateTimeAxisSpec, 22 | ColorUtil, 23 | BehaviorPosition, 24 | OutsideJustification, 25 | SelectNearest; 26 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 27 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 28 | import 'package:codestats_flutter/models/user/day_language_xps.dart'; 29 | import 'package:codestats_flutter/models/user/user.dart'; 30 | import 'package:collection/collection.dart' show groupBy; 31 | import 'package:flutter/foundation.dart'; 32 | import 'package:flutter/material.dart'; 33 | 34 | class DayLanguageXpsWidget extends StatefulWidget { 35 | const DayLanguageXpsWidget({ 36 | Key key, 37 | @required this.userModel, 38 | }) : super(key: key); 39 | 40 | final User userModel; 41 | 42 | @override 43 | _DayLanguageXpsWidgetState createState() => _DayLanguageXpsWidgetState(); 44 | } 45 | 46 | class _DayLanguageXpsWidgetState extends State { 47 | bool animate = false; 48 | 49 | @override 50 | void didUpdateWidget(DayLanguageXpsWidget oldWidget) { 51 | animate = true; 52 | super.didUpdateWidget(oldWidget); 53 | } 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | final UserBloc bloc = BlocProvider.of(context); 58 | if (widget.userModel.dayLanguageXps.isEmpty) { 59 | return Center( 60 | child: Text("No recent activity :("), 61 | ); 62 | } 63 | 64 | var series = 65 | groupBy(widget.userModel.dayLanguageXps, (elem) => elem.language) 66 | .values 67 | .map((dlx) => charts.Series( 68 | id: dlx.first.language, 69 | domainFn: domainFn, 70 | measureFn: measureFn, 71 | data: dlx, 72 | colorFn: (elem, _) => bloc.languageColor(elem.language), 73 | )) 74 | .toList(); 75 | 76 | return Padding( 77 | padding: EdgeInsets.all(8.0), 78 | child: charts.TimeSeriesChart( 79 | series, 80 | animate: animate, 81 | animationDuration: Duration(milliseconds: 800), 82 | defaultInteractions: false, 83 | defaultRenderer: charts.BarRendererConfig( 84 | groupingType: charts.BarGroupingType.stacked, 85 | ), 86 | primaryMeasureAxis: charts.NumericAxisSpec( 87 | tickFormatterSpec: charts.BasicNumericTickFormatterSpec( 88 | (value) => "${value.round()} XP"), 89 | ), 90 | domainAxis: charts.DateTimeAxisSpec( 91 | usingBarRenderer: true, 92 | renderSpec: SmallTickRendererSpec( 93 | labelAnchor: TickLabelAnchor.centered, 94 | labelOffsetFromTickPx: 0, 95 | ), 96 | tickProviderSpec: DayTickProviderSpec( 97 | increments: [1], 98 | ), 99 | ), 100 | behaviors: [ 101 | charts.LinePointHighlighter( 102 | showHorizontalFollowLine: 103 | LinePointHighlighterFollowLineType.nearest), 104 | charts.SelectNearest( 105 | eventTrigger: SelectionTrigger.tapAndDrag, 106 | ), 107 | charts.DomainHighlighter(), 108 | charts.SeriesLegend( 109 | entryTextStyle: TextStyleSpec( 110 | color: charts.ColorUtil.fromDartColor(Colors.black), 111 | ), 112 | measureFormatter: (xp) => xp != null ? "${xp?.round()} XP" : "", 113 | legendDefaultMeasure: LegendDefaultMeasure.sum, 114 | showMeasures: true, 115 | position: charts.BehaviorPosition.top, 116 | outsideJustification: charts.OutsideJustification.start, 117 | horizontalFirst: false, 118 | desiredMaxRows: (series.length / 2).ceil(), 119 | ), 120 | ], 121 | )); 122 | } 123 | } 124 | 125 | DateTime domainFn(DayLanguageXps elem, int _) => DateTime.parse(elem.date); 126 | 127 | num measureFn(DayLanguageXps elem, int _) => elem.xp; 128 | -------------------------------------------------------------------------------- /lib/widgets/day_of_year_xps.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/models/user/user.dart'; 2 | import 'package:codestats_flutter/widgets/Snappable.dart'; 3 | import 'package:codestats_flutter/widgets/subheader.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:intl/intl.dart' show DateFormat; 6 | import 'package:superpower/superpower.dart'; 7 | import 'package:draggable_scrollbar/draggable_scrollbar.dart'; 8 | 9 | class Month { 10 | final int number; 11 | final Map daysXp = {}; 12 | 13 | Month(this.number); 14 | } 15 | 16 | class DayOfYearXps extends StatelessWidget { 17 | final User userModel; 18 | final ScrollController scrollController; 19 | 20 | const DayOfYearXps({ 21 | Key key, 22 | @required this.userModel, 23 | this.scrollController, 24 | }) : super(key: key); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | List months = List.generate(12, (i) => Month(i + 1)); 29 | final formatter = DateFormat('MMM d'); 30 | List days = []; 31 | 32 | var doyx = userModel.dayOfYearXps 33 | .map((key, value) => MapEntry(int.parse(key), value)); 34 | 35 | var maxXp = $(doyx.values).max().clamp(1, double.maxFinite); 36 | 37 | doyx.keys.forEach((day) { 38 | var date = DateTime(2000).add(Duration(days: day - 1)); 39 | months[date.month - 1].daysXp[date.day] = doyx[day]; 40 | }); 41 | 42 | months.forEach( 43 | (month) { 44 | var keys = month.daysXp.keys.toList()..sort(); 45 | days.addAll( 46 | keys.map( 47 | (day) { 48 | var xpPercent = month.daysXp[day] / maxXp; 49 | 50 | var thenDate = DateTime(2020, month.number, day); 51 | var todayDate = DateTime.now(); 52 | 53 | bool today = 54 | todayDate.month == month.number && todayDate.day == day; 55 | 56 | var style = TextStyle( 57 | color: xpPercent < 0.4 58 | ? Colors.blueGrey[600] 59 | : Colors.grey.shade300, 60 | fontWeight: today ? FontWeight.bold : FontWeight.normal, 61 | decoration: 62 | today ? TextDecoration.underline : TextDecoration.none, 63 | ); 64 | 65 | var xpStr = month.daysXp[day].toString(); 66 | 67 | return Snappable( 68 | snapOnTap: true, 69 | child: Container( 70 | color: Colors.black.withOpacity(xpPercent), 71 | child: AspectRatio( 72 | aspectRatio: 1, 73 | child: LayoutBuilder( 74 | builder: (context, constraints) => Column( 75 | mainAxisAlignment: MainAxisAlignment.center, 76 | children: [ 77 | Text( 78 | formatter.format( 79 | thenDate, 80 | ), 81 | style: style.copyWith( 82 | fontSize: constraints.maxWidth * .2), 83 | ), 84 | Text( 85 | xpStr, 86 | style: style.copyWith( 87 | fontSize: 18 - 88 | xpStr.length * constraints.maxWidth * .015), 89 | ), 90 | ], 91 | ), 92 | ), 93 | ), 94 | ), 95 | ); 96 | }, 97 | ), 98 | ); 99 | }, 100 | ); 101 | 102 | return Column( 103 | mainAxisSize: MainAxisSize.max, 104 | children: [ 105 | SubHeader( 106 | text: "Total XP by day of year", 107 | ), 108 | Expanded( 109 | child: DraggableScrollbar.semicircle( 110 | controller: scrollController, 111 | child: GridView.count( 112 | controller: scrollController, 113 | crossAxisCount: 7, 114 | children: days, 115 | padding: EdgeInsets.only( 116 | bottom: 30, 117 | left: 8, 118 | right: 8, 119 | ), 120 | ), 121 | ), 122 | ), 123 | ], 124 | ); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/widgets/dots_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class DotsIndicator extends AnimatedWidget { 6 | const DotsIndicator({ 7 | @required this.controller, 8 | @required this.itemCount, 9 | @required this.onPageSelected, 10 | this.color = Colors.white, 11 | }) : assert(controller != null), 12 | assert(itemCount != null), 13 | assert(onPageSelected != null), 14 | super(listenable: controller); 15 | 16 | final PageController controller; 17 | final int itemCount; 18 | final ValueChanged onPageSelected; 19 | final Color color; 20 | 21 | static const double _kDotSize = 6.0; 22 | static const double _kMaxZoom = 1.5; 23 | static const double _kDotSpacing = 25.0; 24 | 25 | Widget _buildDot(int index) { 26 | final double page = controller.hasClients 27 | ? controller?.page ?? controller.initialPage.toDouble() 28 | : controller.initialPage.toDouble(); 29 | final double correctedPage = page % itemCount; 30 | double selectedness; 31 | if (correctedPage > itemCount - 1 && index == 0) { 32 | selectedness = 33 | Curves.easeOut.transform(correctedPage - correctedPage.floor()); 34 | } else { 35 | selectedness = Curves.easeOut 36 | .transform(max(0.0, 1.0 - (correctedPage - index).abs())); 37 | } 38 | final double zoom = 1.0 + (_kMaxZoom - 1.0) * selectedness; 39 | 40 | final int pageForClicking = (page / itemCount).floor() * itemCount + index; 41 | 42 | return Container( 43 | width: _kDotSpacing, 44 | child: Center( 45 | child: Material( 46 | borderRadius: BorderRadius.circular(_kDotSize * zoom / 2), 47 | color: color, 48 | child: Container( 49 | width: _kDotSize * zoom, 50 | height: _kDotSize * zoom, 51 | child: InkWell(onTap: () => onPageSelected(pageForClicking))), 52 | ))); 53 | } 54 | 55 | Widget build(BuildContext context) { 56 | return Container( 57 | height: 58 | _kDotSize * 2, // put in fixed container to avoid "bouncing" on resize 59 | child: Row( 60 | mainAxisAlignment: MainAxisAlignment.center, 61 | children: List.generate(itemCount, _buildDot), 62 | ), 63 | ); 64 | } 65 | } -------------------------------------------------------------------------------- /lib/widgets/expandable_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/utils.dart' show formatNumber, getLevel; 4 | import 'package:codestats_flutter/widgets/backdrop.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:share/share.dart'; 7 | 8 | class ExpandableUser { 9 | final String user; 10 | bool isExpanded = false; 11 | 12 | ExpandableUser({@required this.user}); 13 | 14 | Widget build(BuildContext context) { 15 | UserBloc bloc = BlocProvider.of(context); 16 | return Row( 17 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 18 | children: [ 19 | IconButton( 20 | color: Colors.white, 21 | icon: Icon( 22 | Icons.share, 23 | ), 24 | onPressed: () => Share.share("https://codestats.net/users/$user"), 25 | ), 26 | IconButton( 27 | color: Colors.white, 28 | icon: Icon(Icons.delete), 29 | onPressed: () { 30 | _showDialog(context, user, bloc); 31 | }, 32 | ) 33 | ], 34 | ); 35 | } 36 | 37 | ExpansionPanelHeaderBuilder get headerBuilder => 38 | (BuildContext context, bool isExpanded) { 39 | UserBloc bloc = BlocProvider.of(context); 40 | return ListTile( 41 | onTap: () { 42 | bloc.selectUser.add(user); 43 | Backdrop.of(context).fling(); 44 | }, 45 | title: Text( 46 | user, 47 | style: TextStyle( 48 | color: Colors.white, 49 | ), 50 | ), 51 | subtitle: Text( 52 | "${formatNumber(bloc.userStateController.value.allUsers[user]?.totalXp ?? 0)} XP", 53 | style: TextStyle( 54 | color: Colors.white, 55 | ), 56 | ), 57 | leading: CircleAvatar( 58 | child: Text( 59 | "${getLevel( 60 | bloc.userStateController.value.allUsers[user]?.totalXp ?? 61 | 0)}"), 62 | ), 63 | ); 64 | }; 65 | 66 | _showDialog(BuildContext context, String user, UserBloc bloc) { 67 | showDialog(context: context, 68 | builder: (context) => 69 | AlertDialog( 70 | title: Text("Remove $user?"), 71 | actions: [ 72 | FlatButton(onPressed: () => Navigator.pop(context), 73 | child: Text("Cancel"),), 74 | FlatButton(onPressed: () { 75 | bloc.removeUser(user); 76 | Navigator.pop(context); 77 | }, child: Text("Ok")) 78 | ], 79 | ) 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/widgets/expandable_user_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/widgets/expandable_user.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ExpandableUserList extends StatefulWidget { 5 | final List users; 6 | 7 | const ExpandableUserList({Key key, this.users}) : super(key: key); 8 | 9 | @override 10 | _ExpandableUserListState createState() => _ExpandableUserListState(); 11 | } 12 | 13 | class _ExpandableUserListState extends State { 14 | List expandableUsers; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | expandableUsers = widget.users 20 | .map((user) => ExpandableUser( 21 | user: user, 22 | )) 23 | .toList(); 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Theme( 29 | data: Theme.of(context).copyWith( 30 | cardColor: Colors.blueGrey[500], 31 | ), 32 | child: Padding( 33 | padding: EdgeInsets.only(top: 12, bottom: 12), 34 | child: ExpansionPanelList( 35 | children: expandableUsers 36 | .map( 37 | (expUser) => ExpansionPanel( 38 | isExpanded: expUser.isExpanded, 39 | headerBuilder: expUser.headerBuilder, 40 | body: expUser.build(context), 41 | ), 42 | ) 43 | .toList(), 44 | expansionCallback: (int index, bool isExpanded) { 45 | setState(() { 46 | expandableUsers[index].isExpanded = !isExpanded; 47 | }); 48 | }, 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/widgets/explosion.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as Math; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vector_math/vector_math_64.dart'; 4 | 5 | List particles; 6 | 7 | class Explode extends StatefulWidget { 8 | final Widget child; 9 | final int particleCount; 10 | final ExplodeType type; 11 | final List colors; 12 | final bool explode; 13 | 14 | static GlobalKey<_ExplodeState> getKey() { 15 | return GlobalKey<_ExplodeState>(); 16 | } 17 | 18 | final Duration duration; 19 | 20 | const Explode( 21 | {Key key, 22 | this.colors, 23 | this.type = ExplodeType.Spread, 24 | this.duration = const Duration(seconds: 1, milliseconds: 300), 25 | this.particleCount = 100, 26 | this.child, 27 | this.explode = false}) 28 | : super(key: key); 29 | 30 | @override 31 | State createState() => _ExplodeState(); 32 | } 33 | 34 | class _ExplodeState extends State with SingleTickerProviderStateMixin { 35 | AnimationController controller; 36 | Animation animation; 37 | Animation animationTwo; 38 | Animation shakeAnimation; 39 | Math.Random random = Math.Random(); 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | controller = AnimationController(vsync: this, duration: widget.duration) 45 | ..addListener(() { 46 | setState(() {}); 47 | }); 48 | shakeAnimation = Tween(begin: 0.0, end: 1.0).animate( 49 | CurvedAnimation(parent: controller, curve: Interval(0.0, 0.2))); 50 | animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( 51 | parent: controller, 52 | curve: Interval(0.2, 1.0, 53 | curve: widget.type == ExplodeType.Spread 54 | ? Curves.linear 55 | : Curves.bounceOut))); 56 | } 57 | 58 | @override 59 | void didUpdateWidget(Explode oldWidget) { 60 | if(widget.explode) 61 | explode(); 62 | super.didUpdateWidget(oldWidget); 63 | } 64 | 65 | void explode({Function onFinish}) { 66 | if (controller.isDismissed) { 67 | controller.reset(); 68 | controller.forward(); 69 | if (onFinish != null) { 70 | onFinish(); 71 | } 72 | } 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return LayoutBuilder( 78 | builder: (context, constraints) { 79 | particles = List.generate(widget.particleCount, (i) { 80 | return Particle( 81 | left: 82 | random.nextInt(constraints.maxWidth.toInt() - 10).toDouble(), 83 | top: 84 | random.nextInt(constraints.maxHeight.toInt() - 10).toDouble(), 85 | color: widget.colors[i % widget.colors.length], 86 | sizeFactor: random.nextInt(1000).toDouble() / 1000, 87 | spread: widget.type == ExplodeType.Spread); 88 | }); 89 | return InkWell( 90 | child: controller.value < 0.4 91 | ? Transform( 92 | transform: Matrix4.translation(getTranslation()), 93 | child: widget.child) 94 | : CustomPaint( 95 | foregroundPainter: ParticlesPainter(animation.value), 96 | child: Container( 97 | width: constraints.maxWidth, 98 | height: constraints.maxHeight, 99 | ), 100 | ), 101 | ); 102 | }, 103 | ); 104 | } 105 | 106 | Vector3 getTranslation() { 107 | double progress = shakeAnimation.value; 108 | double offset = 3 * Math.sin(progress * Math.pi * 2); 109 | return Vector3(offset, offset, offset); 110 | } 111 | } 112 | 113 | class ParticlesPainter extends CustomPainter { 114 | final double span; 115 | 116 | ParticlesPainter(this.span); 117 | 118 | @override 119 | void paint(Canvas canvas, Size size) { 120 | particles.forEach((particle) { 121 | particle.advance(span, span > 0.4, size.height); 122 | Paint paint = Paint() 123 | ..color = particle.color.withOpacity( 124 | Math.min(Math.max((0.4 * (1 - span) + 1 - span), 0), 1)) 125 | ..style = PaintingStyle.fill; 126 | canvas.drawCircle(Offset(particle.left, particle.top), 127 | particle.sizeFactor * 10 * span, paint); 128 | }); 129 | } 130 | 131 | @override 132 | bool shouldRepaint(CustomPainter oldDelegate) { 133 | // TODO: implement shouldRepaint 134 | return true; 135 | } 136 | } 137 | 138 | class Particle { 139 | double left; 140 | double top; 141 | double initialLeft; 142 | double initialTop; 143 | double sizeFactor; 144 | Color color; 145 | ExplodeType type; 146 | final bool spread; 147 | int direction; 148 | double topMax; 149 | double leftMax; 150 | double bottomMax; 151 | double x; 152 | 153 | Particle({this.left, this.top, this.color, this.sizeFactor, this.spread}) { 154 | direction = Math.Random().nextBool() ? 1 : -1; 155 | initialLeft = left; 156 | initialTop = top; 157 | x = Math.Random().nextInt(1000) / 1000.0; 158 | leftMax = direction == 1 ? (left + 150 * x) : left - 200 * x; 159 | topMax = top - 150; 160 | bottomMax = initialTop + 150; 161 | } 162 | 163 | advance(double span, bool stage, double height) { 164 | if (spread) { 165 | left = initialLeft * (1 - span) + leftMax * span; 166 | top = initialTop + 167 | 50 * span + 168 | 100 * Math.sin(Math.pi / 2 + 2 * span * Math.pi); 169 | } else { 170 | top = initialTop * (1 - span) + height * span; 171 | } 172 | } 173 | } 174 | 175 | enum ExplodeType { Spread, Drop } 176 | -------------------------------------------------------------------------------- /lib/widgets/language_levels.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/models/user/user.dart'; 2 | import 'package:codestats_flutter/models/user/xp.dart'; 3 | import 'package:codestats_flutter/widgets/level_percent_indicator.dart'; 4 | import 'package:collection/collection.dart'; 5 | import 'package:draggable_scrollbar/draggable_scrollbar.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class LanguageLevelPage extends StatefulWidget { 9 | final User userModel; 10 | 11 | const LanguageLevelPage({Key key, @required this.userModel}) 12 | : super(key: key); 13 | 14 | @override 15 | _LanguageLevelPageState createState() => _LanguageLevelPageState(); 16 | } 17 | 18 | class _LanguageLevelPageState extends State with SingleTickerProviderStateMixin{ 19 | ScrollController scrollController = ScrollController(); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | widget.userModel.totalLangs.sort((a, b) => b.xp - a.xp); 24 | Map> recentLanguages = 25 | groupBy(widget.userModel.recentLangs, (Xp element) => element.name); 26 | 27 | return LayoutBuilder( 28 | builder: (context, BoxConstraints constraints) => 29 | DraggableScrollbar.arrows( 30 | backgroundColor: Colors.grey.shade500, 31 | padding: EdgeInsets.only(right: 4.0), 32 | child: ListView.builder( 33 | controller: scrollController, 34 | padding: EdgeInsets.only(top: 24, bottom: 24), 35 | itemCount: widget.userModel.totalLangs.length, 36 | itemBuilder: (context, index) => LevelPercentIndicator( 37 | width: constraints.maxWidth * 0.7, 38 | name: widget.userModel.totalLangs[index].name, 39 | xp: widget.userModel.totalLangs[index].xp, 40 | recent: recentLanguages[widget.userModel.totalLangs[index].name] 41 | ?.first 42 | ?.xp, 43 | ), 44 | ), 45 | controller: scrollController, 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/widgets/level_percent_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/passthrough_simulation.dart'; 2 | import 'package:codestats_flutter/utils.dart'; 3 | import 'package:codestats_flutter/widgets/explosion.dart'; 4 | import 'package:codestats_flutter/widgets/linear_percent_indicator.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'dart:math'; 7 | 8 | class LevelPercentIndicator extends StatefulWidget { 9 | const LevelPercentIndicator( 10 | {Key key, 11 | @required this.width, 12 | @required this.xp, 13 | @required this.name, 14 | this.recent}) 15 | : super(key: key); 16 | 17 | final double width; 18 | final int xp; 19 | final String name; 20 | final int recent; 21 | 22 | @override 23 | _LevelPercentIndicatorState createState() => _LevelPercentIndicatorState(); 24 | } 25 | 26 | class _LevelPercentIndicatorState extends State 27 | with SingleTickerProviderStateMixin { 28 | AnimationController ctrl; 29 | 30 | @override 31 | void initState() { 32 | ctrl = AnimationController( 33 | value: 0, 34 | vsync: this, 35 | duration: Duration(hours: 1), 36 | lowerBound: 0, 37 | upperBound: double.maxFinite); 38 | super.initState(); 39 | } 40 | 41 | double get xFreq => 60; 42 | 43 | double get yfreq => 53; 44 | 45 | double get rotFreq => 46; 46 | 47 | @override 48 | Widget build(BuildContext context) { 49 | var level = getLevel(widget.xp); 50 | var previousLevelXp = getXp(level).toDouble(); 51 | var nextLevelXp = getXp(level + 1); 52 | var thisLevelXpSoFar = widget.xp - previousLevelXp; 53 | var thisLevelXpTotal = nextLevelXp - previousLevelXp; 54 | double percent = thisLevelXpSoFar / thisLevelXpTotal; 55 | String percentText = "${(percent * 100).round()} %"; 56 | double recentPercent; 57 | 58 | if (widget.recent != null) { 59 | recentPercent = percent; 60 | percent = ((thisLevelXpSoFar - widget.recent) / thisLevelXpTotal) 61 | .clamp(0.0, 1.0) 62 | .toDouble(); 63 | } 64 | 65 | return ConstrainedBox( 66 | constraints: BoxConstraints(maxHeight: 50), 67 | child: Listener( 68 | onPointerDown: (details) { 69 | ctrl.animateWith(PassThroughSimulation()); 70 | }, 71 | onPointerUp: (details) { 72 | ctrl.animateWith(PassThroughSimulation(reverse: ctrl.value / 2)); 73 | }, 74 | child: AnimatedBuilder( 75 | animation: ctrl, 76 | builder: (context, _) { 77 | return Explode( 78 | explode: ctrl.value > 5, 79 | colors: [Colors.black, Colors.red, Colors.lightGreen.shade400], 80 | child: Transform( 81 | alignment: FractionalOffset.center, 82 | transform: Matrix4.identity() 83 | ..translate( 84 | sin(ctrl.value * xFreq) * ctrl.value, 85 | sin(ctrl.value * yfreq) * ctrl.value, 86 | ) 87 | ..rotateZ(sin(ctrl.value * rotFreq) * ctrl.value * 0.005), 88 | child: Column( 89 | children: [ 90 | RichText( 91 | text: TextSpan( 92 | style: DefaultTextStyle.of(context).style, 93 | children: [ 94 | TextSpan( 95 | text: widget.name, 96 | style: TextStyle(fontWeight: FontWeight.bold)), 97 | TextSpan( 98 | text: 99 | " level $level (${formatNumber(widget.xp)} XP) ${widget.recent != null ? '(+${widget.recent})' : ''}", 100 | ), 101 | ]), 102 | ), 103 | LinearPercentIndicator( 104 | animation: true, 105 | width: widget.width, 106 | lineHeight: 14.0, 107 | percent: percent, 108 | recent: recentPercent, 109 | center: Text( 110 | percentText, 111 | style: TextStyle( 112 | fontSize: 12.0, 113 | color: Colors.black, 114 | ), 115 | ), 116 | leading: Text("$level"), 117 | trailing: Text("${level + 1}"), 118 | alignment: MainAxisAlignment.center, 119 | progressColor: Color.lerp(Colors.lightGreen.shade400, 120 | Colors.red, (ctrl.value / 5).clamp(0.0, 1.0)), 121 | recentColor: Color.lerp(Colors.amber.shade600, Colors.red, 122 | (ctrl.value / 5).clamp(0.0, 1.0)), 123 | ), 124 | ], 125 | ), 126 | ), 127 | ); 128 | }, 129 | ), 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/widgets/level_progress_circle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 4 | import 'package:codestats_flutter/models/user/user.dart'; 5 | import 'package:codestats_flutter/utils.dart'; 6 | import 'package:codestats_flutter/widgets/tiltable_stack.dart'; 7 | import 'package:codestats_flutter/widgets/wave_progress.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:flutter_circular_chart/flutter_circular_chart.dart'; 11 | 12 | class LevelProgressCircle extends StatefulWidget { 13 | const LevelProgressCircle({ 14 | Key key, 15 | @required this.bloc, 16 | @required this.user, 17 | }) : super(key: key); 18 | 19 | final UserBloc bloc; 20 | final UserWrap user; 21 | 22 | @override 23 | LevelProgressCircleState createState() => LevelProgressCircleState(); 24 | } 25 | 26 | class LevelProgressCircleState extends State 27 | with SingleTickerProviderStateMixin { 28 | GlobalKey chartKey = GlobalKey(); 29 | GlobalKey waveKey = GlobalKey(); 30 | StreamSubscription circularChartSubscription; 31 | final channel = EventChannel('fourierStream'); 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | circularChartSubscription = 37 | widget.bloc.currentUser.listen((UserWrap state) { 38 | if (state.data != null) { 39 | chartKey.currentState.updateData( 40 | [createCircularStack(state.data)], 41 | ); 42 | } 43 | }); 44 | } 45 | 46 | CircularStackEntry createCircularStack(User userModel) { 47 | List segments = []; 48 | var level = getLevel(userModel.totalXp); 49 | var previousLevelXp = getXp(level).toDouble(); 50 | var nextLevelXp = getXp(level + 1); 51 | var thisLevelXpSoFar = userModel.totalXp - previousLevelXp; 52 | var thisLevelXpTotal = nextLevelXp - previousLevelXp; 53 | var recentXp = getRecentXp(userModel).toDouble(); 54 | 55 | bool recentXpLessThanSoFarOnLevel = recentXp < thisLevelXpSoFar; 56 | 57 | if (recentXpLessThanSoFarOnLevel) { 58 | segments.add( 59 | CircularSegmentEntry( 60 | thisLevelXpSoFar - recentXp, Colors.lightGreen[400], 61 | rankKey: 'completed'), 62 | ); 63 | segments.add( 64 | CircularSegmentEntry(recentXp, Colors.amber[600], rankKey: 'recent'), 65 | ); 66 | } else { 67 | segments.add( 68 | CircularSegmentEntry(thisLevelXpSoFar, Colors.amber[600], 69 | rankKey: 'recent'), 70 | ); 71 | } 72 | 73 | segments.add( 74 | CircularSegmentEntry( 75 | (thisLevelXpTotal - thisLevelXpSoFar).toDouble(), Colors.grey[300], 76 | rankKey: 'Remaining'), 77 | ); 78 | 79 | return CircularStackEntry(segments); 80 | } 81 | 82 | @override 83 | dispose() { 84 | super.dispose(); 85 | circularChartSubscription.cancel(); 86 | } 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | final level = getLevel(widget.user.data.totalXp); 91 | final previousLevelXp = getXp(level).toDouble(); 92 | final nextLevelXp = getXp(level + 1); 93 | final thisLevelXpSoFar = widget.user.data.totalXp - previousLevelXp; 94 | final thisLevelXpTotal = nextLevelXp - previousLevelXp; 95 | 96 | chartKey.currentState?.updateData([createCircularStack(widget.user.data)]); 97 | waveKey.currentState?.update(thisLevelXpSoFar / thisLevelXpTotal); 98 | 99 | return LayoutBuilder( 100 | builder: (context, constraints) => TiltableStack( 101 | alignment: Alignment.center, 102 | children: [ 103 | AnimatedCircularChart( 104 | duration: Duration(seconds: 1), 105 | key: chartKey, 106 | size: Size.square(constraints.maxWidth * 3 / 4), 107 | edgeStyle: SegmentEdgeStyle.round, 108 | initialChartData: [], 109 | holeLabel: Container(), 110 | ), 111 | SizedBox.fromSize( 112 | key: ValueKey("foo"), 113 | size: Size.square(constraints.maxWidth * 3 / 4 - 80), 114 | child: Material( 115 | elevation: 4, 116 | color: Colors.grey.shade100, 117 | shape: CircleBorder(), 118 | child: WaveProgress( 119 | constraints.maxWidth * 2 / 3, 120 | Colors.blueGrey.shade200.withAlpha(100), 121 | thisLevelXpSoFar / thisLevelXpTotal, 122 | key: waveKey, 123 | ), 124 | ), 125 | ), 126 | Column( 127 | mainAxisAlignment: MainAxisAlignment.center, 128 | children: [ 129 | Text( 130 | 'LEVEL', 131 | style: TextStyle( 132 | color: Colors.black, 133 | ), 134 | ), 135 | Text( 136 | '$level', 137 | style: TextStyle( 138 | fontSize: 32, 139 | color: Colors.black, 140 | fontWeight: FontWeight.bold, 141 | ), 142 | ), 143 | Padding( 144 | padding: EdgeInsets.only(top: 8), 145 | child: Text.rich( 146 | TextSpan( 147 | text: '${formatNumber(thisLevelXpSoFar)}', 148 | style: TextStyle( 149 | fontSize: 12, 150 | color: Colors.black, 151 | fontWeight: FontWeight.bold, 152 | ), 153 | children: [ 154 | TextSpan( 155 | text: ' / ${formatNumber(thisLevelXpTotal)} XP', 156 | style: TextStyle( 157 | fontWeight: FontWeight.normal, 158 | ), 159 | ) 160 | ]), 161 | ), 162 | ), 163 | Padding( 164 | padding: EdgeInsets.only(top: 8), 165 | child: Row( 166 | mainAxisAlignment: MainAxisAlignment.center, 167 | children: [ 168 | Text( 169 | '12h ', 170 | style: TextStyle( 171 | color: Colors.black, 172 | ), 173 | ), 174 | Icon(Icons.timer), 175 | Text( 176 | ' +${formatNumber(getRecentXp(widget.user.data))} XP', 177 | style: TextStyle( 178 | color: Colors.black, 179 | ), 180 | ), 181 | ], 182 | ), 183 | ) 184 | ], 185 | ), 186 | ], 187 | ) 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/widgets/linear_percent_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum LinearStrokeCap { butt, round, roundAll } 4 | 5 | class LinearPercentIndicator extends StatefulWidget { 6 | ///Percent value between 0.0 and 1.0 7 | final double percent; 8 | final double recent; 9 | final double width; 10 | 11 | ///Height of the line 12 | final double lineHeight; 13 | 14 | ///Color of the background of the Line , default = transparent 15 | final Color fillColor; 16 | 17 | ///First color applied to the complete line 18 | final Color backgroundColor; 19 | final Color recentColor; 20 | final Color progressColor; 21 | 22 | ///true if you want the Line to have animation 23 | final bool animation; 24 | 25 | ///duration of the animation in milliseconds, It only applies if animation attribute is true 26 | final int animationDuration; 27 | 28 | ///widget at the left of the Line 29 | final Widget leading; 30 | 31 | ///widget at the right of the Line 32 | final Widget trailing; 33 | 34 | ///widget inside the Line 35 | final Widget center; 36 | 37 | ///The kind of finish to place on the end of lines drawn, values supported: butt, round, roundAll 38 | final LinearStrokeCap linearStrokeCap; 39 | 40 | ///alignment of the Row (leading-widget-center-trailing) 41 | final MainAxisAlignment alignment; 42 | 43 | ///padding to the LinearPercentIndicator 44 | final EdgeInsets padding; 45 | 46 | LinearPercentIndicator( 47 | {Key key, 48 | this.fillColor = Colors.transparent, 49 | this.percent = 0.0, 50 | this.lineHeight = 5.0, 51 | @required this.width, 52 | this.backgroundColor = const Color(0xFFB8C7CB), 53 | this.progressColor = Colors.red, 54 | this.animation = false, 55 | this.animationDuration = 1000, 56 | this.leading, 57 | this.trailing, 58 | this.center, 59 | this.linearStrokeCap, 60 | this.padding = const EdgeInsets.symmetric(horizontal: 10.0), 61 | this.alignment = MainAxisAlignment.start, 62 | this.recentColor = Colors.green, 63 | this.recent = 0.0}) 64 | : super(key: key) { 65 | if (percent < 0.0 || percent > 1.0) { 66 | throw Exception("Percent value must be a double between 0.0 and 1.0"); 67 | } 68 | } 69 | 70 | @override 71 | _LinearPercentIndicatorState createState() => _LinearPercentIndicatorState(); 72 | } 73 | 74 | class _LinearPercentIndicatorState extends State 75 | with SingleTickerProviderStateMixin { 76 | AnimationController _animationController; 77 | Animation _animation; 78 | double _percent = 0.0; 79 | double _recent = 0.0; 80 | 81 | @override 82 | void dispose() { 83 | _animationController?.dispose(); 84 | super.dispose(); 85 | } 86 | 87 | @override 88 | void initState() { 89 | if (widget.animation) { 90 | _animationController = AnimationController( 91 | vsync: this, 92 | duration: Duration(milliseconds: widget.animationDuration)); 93 | _animation = CurvedAnimation( 94 | parent: Tween(begin: 0.0, end: 1.0).animate( 95 | _animationController, 96 | ), 97 | curve: Curves.bounceOut, 98 | ); 99 | _animationController.forward(); 100 | } 101 | 102 | _percent = widget.percent; 103 | _recent = widget.recent; 104 | super.initState(); 105 | } 106 | 107 | @override 108 | void didUpdateWidget(LinearPercentIndicator oldWidget) { 109 | super.didUpdateWidget(oldWidget); 110 | if (oldWidget.percent != widget.percent || oldWidget.recent != widget.recent) { 111 | setState(() { 112 | _recent = widget.recent; 113 | _percent = widget.percent; 114 | _animationController?.forward(from: 0); 115 | }); 116 | } 117 | } 118 | 119 | @override 120 | Widget build(BuildContext context) { 121 | var items = List(); 122 | if (widget.leading != null) { 123 | items.add( 124 | Padding(padding: EdgeInsets.only(right: 5.0), child: widget.leading)); 125 | } 126 | items.add(Container( 127 | width: widget.width, 128 | height: widget.lineHeight * 2, 129 | padding: widget.padding, 130 | child: CustomPaint( 131 | painter: LinearPainter( 132 | animation: _animation, 133 | progress: _percent, 134 | recent: _recent, 135 | center: widget.center, 136 | progressColor: widget.progressColor, 137 | recentColor: widget.recentColor, 138 | backgroundColor: widget.backgroundColor, 139 | linearStrokeCap: widget.linearStrokeCap, 140 | lineWidth: widget.lineHeight), 141 | child: (widget.center != null) 142 | ? Center(child: widget.center) 143 | : Container(), 144 | ))); 145 | 146 | if (widget.trailing != null) { 147 | items.add( 148 | Padding(padding: EdgeInsets.only(left: 5.0), child: widget.trailing)); 149 | } 150 | 151 | return Material( 152 | color: Colors.transparent, 153 | child: Container( 154 | color: widget.fillColor, 155 | child: Row( 156 | mainAxisAlignment: widget.alignment, 157 | crossAxisAlignment: CrossAxisAlignment.center, 158 | children: items, 159 | )), 160 | ); 161 | } 162 | } 163 | 164 | class LinearPainter extends CustomPainter { 165 | final Paint _paintBackground = Paint(); 166 | final Paint _paintRecentLine = Paint(); 167 | final Paint _paintLine = Paint(); 168 | final lineWidth; 169 | final recent; 170 | final progress; 171 | final center; 172 | final Color recentColor; 173 | final Color progressColor; 174 | final Color backgroundColor; 175 | final LinearStrokeCap linearStrokeCap; 176 | final Animation animation; 177 | 178 | LinearPainter({ 179 | this.recent, 180 | this.recentColor, 181 | this.lineWidth, 182 | this.progress, 183 | this.center, 184 | this.progressColor, 185 | this.backgroundColor, 186 | this.linearStrokeCap = LinearStrokeCap.butt, 187 | this.animation, 188 | }) : super(repaint: animation) { 189 | _paintBackground.color = backgroundColor; 190 | _paintBackground.style = PaintingStyle.stroke; 191 | _paintBackground.strokeWidth = lineWidth; 192 | 193 | _paintLine.color = progress.toString() == "0.0" 194 | ? progressColor.withOpacity(0.0) 195 | : progressColor; 196 | _paintLine.style = PaintingStyle.stroke; 197 | _paintLine.strokeWidth = lineWidth; 198 | 199 | if (recent != null && recentColor != null) { 200 | _paintRecentLine.color = recent.toString() == "0.0" 201 | ? recentColor.withOpacity(0.0) 202 | : recentColor; 203 | _paintRecentLine.style = PaintingStyle.stroke; 204 | _paintRecentLine.strokeWidth = lineWidth; 205 | } 206 | 207 | if (linearStrokeCap == LinearStrokeCap.round) { 208 | _paintLine.strokeCap = StrokeCap.round; 209 | _paintRecentLine.strokeCap = StrokeCap.round; 210 | } else if (linearStrokeCap == LinearStrokeCap.butt) { 211 | _paintLine.strokeCap = StrokeCap.butt; 212 | _paintRecentLine.strokeCap = StrokeCap.butt; 213 | } else { 214 | _paintLine.strokeCap = StrokeCap.round; 215 | _paintRecentLine.strokeCap = StrokeCap.round; 216 | _paintBackground.strokeCap = StrokeCap.round; 217 | } 218 | } 219 | 220 | @override 221 | void paint(Canvas canvas, Size size) { 222 | final start = Offset(0.0, size.height / 2); 223 | final end = Offset(size.width, size.height / 2); 224 | canvas.drawLine(start, end, _paintBackground); 225 | 226 | var _percent = progress * (animation?.value ?? 1); 227 | var _recent = (recent ?? 0) * (animation?.value ?? 1); 228 | 229 | canvas.drawLine( 230 | start, 231 | Offset( 232 | size.width * (_recent ?? 0), 233 | size.height / 2, 234 | ), 235 | _paintRecentLine); 236 | canvas.drawLine( 237 | start, 238 | Offset( 239 | size.width * _percent, 240 | size.height / 2, 241 | ), 242 | _paintLine, 243 | ); 244 | } 245 | 246 | @override 247 | bool shouldRepaint(CustomPainter oldDelegate) => false; 248 | } 249 | -------------------------------------------------------------------------------- /lib/widgets/no_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/widgets/breathing_widget.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_inappbrowser/flutter_inappbrowser.dart'; 4 | 5 | ChromeSafariBrowser browser = ChromeSafariBrowser(InAppBrowser()); 6 | 7 | class NoUser extends StatelessWidget { 8 | const NoUser({ 9 | Key key, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Center( 15 | child: Column( 16 | mainAxisAlignment: MainAxisAlignment.center, 17 | crossAxisAlignment: CrossAxisAlignment.center, 18 | children: [ 19 | Padding( 20 | padding: EdgeInsets.only(bottom: 16), 21 | child: Text("No user chosen"), 22 | ), 23 | BreathingWidget( 24 | child: Padding( 25 | padding: EdgeInsets.only(bottom: 16.0), 26 | child: FloatingActionButton.extended( 27 | onPressed: () { 28 | Navigator.of(context).pushNamed("addUser"); 29 | }, 30 | icon: Icon(Icons.add), 31 | label: Text("Add user"), 32 | ), 33 | ), 34 | ), 35 | FloatingActionButton.extended( 36 | heroTag: null, 37 | icon: Icon(Icons.web), 38 | label: Text("Create profile"), 39 | onPressed: () async { 40 | browser.open('https://codestats.net/signup', options: { 41 | "addShareButton": true, 42 | }); 43 | }, 44 | ) 45 | ], 46 | ), 47 | ); 48 | } 49 | } -------------------------------------------------------------------------------- /lib/widgets/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/models/user/xp.dart'; 4 | import 'package:codestats_flutter/sequence_animation.dart'; 5 | import 'package:codestats_flutter/utils.dart' show formatNumber; 6 | import 'package:codestats_flutter/widgets/level_percent_indicator.dart'; 7 | import 'package:codestats_flutter/widgets/level_progress_circle.dart'; 8 | import 'package:codestats_flutter/widgets/subheader.dart'; 9 | import 'package:codestats_flutter/widgets/total_xp_header.dart'; 10 | import 'package:collection/collection.dart'; 11 | import 'package:fl_chart/fl_chart.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:intl/intl.dart'; 14 | import 'package:superpower/superpower.dart'; 15 | 16 | class ProfilePage extends StatefulWidget { 17 | final UserWrap user; 18 | 19 | const ProfilePage({ 20 | Key key, 21 | @required this.user, 22 | }) : super(key: key); 23 | 24 | @override 25 | _ProfilePageState createState() => _ProfilePageState(); 26 | } 27 | 28 | class _ProfilePageState extends State 29 | with TickerProviderStateMixin { 30 | SequenceAnimation sequence; 31 | 32 | AnimationController _controller; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | 38 | _controller = AnimationController( 39 | vsync: this, 40 | duration: const Duration(milliseconds: 500), 41 | ); 42 | 43 | sequence = SequenceAnimationBuilder() 44 | .addAnimatable( 45 | animatable: Tween( 46 | begin: Offset(-2.0, 0.0), 47 | end: Offset.zero, 48 | ).chain(CurveTween(curve: Curves.fastOutSlowIn)), 49 | from: Duration(milliseconds: 200), 50 | to: Duration(milliseconds: 500), 51 | tag: "totalXPtext", 52 | ) 53 | .addAnimatable( 54 | animatable: Tween( 55 | begin: Offset(2.0, 0.0), 56 | end: Offset.zero, 57 | ).chain(CurveTween(curve: Curves.fastOutSlowIn)), 58 | from: Duration(milliseconds: 400), 59 | to: Duration(milliseconds: 700), 60 | tag: "average", 61 | ) 62 | .addAnimatable( 63 | animatable: Tween( 64 | begin: Offset(-2.0, 0.0), 65 | end: Offset.zero, 66 | ).chain(CurveTween(curve: Curves.fastOutSlowIn)), 67 | from: Duration(milliseconds: 500), 68 | to: Duration(milliseconds: 800), 69 | tag: "machines", 70 | ) 71 | .addAnimatable( 72 | animatable: Tween( 73 | begin: Offset(2.0, 0.0), 74 | end: Offset.zero, 75 | ).chain(CurveTween(curve: Curves.fastOutSlowIn)), 76 | from: Duration(milliseconds: 700), 77 | to: Duration(milliseconds: 1000), 78 | tag: "hourofday", 79 | ) 80 | .animate(_controller); 81 | 82 | _controller.forward(); 83 | } 84 | 85 | @override 86 | void dispose() { 87 | _controller.dispose(); 88 | super.dispose(); 89 | } 90 | 91 | @override 92 | Widget build(BuildContext context) { 93 | final UserBloc bloc = BlocProvider.of(context); 94 | final formatter = DateFormat('MMMM d yyyy'); 95 | 96 | DateTime registered; 97 | try { 98 | registered = DateTime.parse(widget.user.data.registered); 99 | } catch(e) { 100 | return Container(); 101 | } 102 | 103 | Duration userTime = DateTime.now().difference(registered); 104 | 105 | var hoursOfDayData = $(widget.user.data.hourOfDayXps.entries 106 | .map((entry) => MapEntry(int.parse(entry.key), entry.value))) 107 | ..sort((a, b) => a.key - b.key); 108 | 109 | var minY = hoursOfDayData.minBy((elem) => elem?.value ?? 0)?.value ?? 0; 110 | var maxY = hoursOfDayData.maxBy((elem) => elem?.value ?? 0)?.value ?? 0; 111 | 112 | Map> recentMachines = 113 | groupBy(widget.user.data.recentMachines, (Xp element) => element.name); 114 | 115 | // sort the machines by level 116 | widget.user.data.totalMachines.sort((a, b) => b.xp - a.xp); 117 | 118 | List gradientColors = [ 119 | Colors.blueGrey.shade300, 120 | Colors.blueGrey.shade100, 121 | Colors.blueGrey.shade100, 122 | Colors.blueGrey.shade300, 123 | Colors.blueGrey.shade300, 124 | ]; 125 | 126 | var spots = hoursOfDayData 127 | .map((value) => FlSpot(value.key.toDouble(), value.value.toDouble())) 128 | .toList(); 129 | 130 | return SingleChildScrollView( 131 | child: Column( 132 | mainAxisAlignment: MainAxisAlignment.center, 133 | children: [ 134 | TotalXp(totalXp: widget.user.data.totalXp), 135 | SlideTransition( 136 | position: sequence["totalXPtext"], 137 | child: Text("XP since ${formatter.format(registered)}"), 138 | ), 139 | SlideTransition( 140 | position: sequence["average"], 141 | child: Text( 142 | "Average ${((widget.user.data?.totalXp ?? 0) / (userTime.inDays == 0 ? 1 : userTime.inDays))?.round() ?? 0} XP per day"), 143 | ), 144 | LevelProgressCircle( 145 | user: widget.user, 146 | bloc: bloc, 147 | ), 148 | if (widget.user.data.totalMachines.isNotEmpty) 149 | SlideTransition( 150 | position: sequence["machines"], 151 | child: SubHeader( 152 | text: "Machines", 153 | ), 154 | ), 155 | LayoutBuilder( 156 | builder: (context, BoxConstraints constraints) => Column( 157 | children: widget.user.data.totalMachines 158 | .map( 159 | (machine) => LevelPercentIndicator( 160 | width: constraints.maxWidth * 0.7, 161 | name: machine.name, 162 | xp: machine.xp, 163 | recent: recentMachines[machine.name]?.first?.xp, 164 | ), 165 | ) 166 | .toList(), 167 | ), 168 | ), 169 | if (spots != null && spots.isNotEmpty) 170 | SlideTransition( 171 | position: sequence["hourofday"], 172 | child: SubHeader( 173 | text: "Total XP per hour of day", 174 | ), 175 | ), 176 | if (spots != null && spots.isNotEmpty) 177 | AspectRatio( 178 | aspectRatio: 1.70, 179 | child: Container( 180 | child: Padding( 181 | padding: EdgeInsets.only( 182 | right: 18.0, left: 12.0, top: 24, bottom: 12), 183 | child: FlChart( 184 | chart: LineChart( 185 | LineChartData( 186 | maxX: 23, 187 | minX: 0, 188 | maxY: maxY.toDouble(), 189 | minY: (minY.toDouble() - (maxY - minY) * 0.05), 190 | lineTouchData: LineTouchData( 191 | touchTooltipData: TouchTooltipData( 192 | tooltipBgColor: Colors.white.withOpacity(0.5), 193 | ), 194 | touchSpotThreshold: 10), 195 | gridData: FlGridData( 196 | show: true, 197 | drawHorizontalGrid: true, 198 | getDrawingVerticalGridLine: (value) => FlLine( 199 | color: Colors.grey.shade200, 200 | strokeWidth: 1, 201 | ), 202 | getDrawingHorizontalGridLine: (value) => FlLine( 203 | color: Colors.grey.shade200, 204 | strokeWidth: 1, 205 | ), 206 | verticalInterval: (maxY - minY) / 3), 207 | titlesData: FlTitlesData( 208 | show: true, 209 | bottomTitles: SideTitles( 210 | showTitles: true, 211 | reservedSize: 22, 212 | textStyle: TextStyle( 213 | color: const Color(0xff68737d), 214 | fontWeight: FontWeight.bold, 215 | fontSize: 16), 216 | margin: 8, 217 | getTitles: (value) { 218 | switch (value.toInt()) { 219 | case 0: 220 | case 6: 221 | case 12: 222 | case 18: 223 | case 23: 224 | return value.toInt().toString(); 225 | break; 226 | default: 227 | return null; 228 | } 229 | }), 230 | leftTitles: SideTitles( 231 | showTitles: true, 232 | textStyle: TextStyle( 233 | color: const Color(0xff67727d), 234 | fontWeight: FontWeight.bold, 235 | ), 236 | getTitles: (value) { 237 | if (value > 0) { 238 | return '${formatNumber(value)}'; 239 | } 240 | return null; 241 | }, 242 | reservedSize: maxY.toString().length * 8.0, 243 | margin: 12, 244 | ), 245 | ), 246 | borderData: FlBorderData( 247 | show: false, 248 | border: 249 | Border.all(color: Color(0xff37434d), width: 1)), 250 | lineBarsData: [ 251 | LineChartBarData( 252 | spots: spots, 253 | isCurved: true, 254 | colors: gradientColors, 255 | barWidth: 5, 256 | isStrokeCapRound: true, 257 | dotData: FlDotData( 258 | show: true, 259 | dotColor: Colors.blueGrey, 260 | ), 261 | belowBarData: BelowBarData( 262 | show: true, 263 | colors: gradientColors 264 | .map((color) => color.withOpacity(0.3)) 265 | .toList(), 266 | ), 267 | ), 268 | ], 269 | ), 270 | ), 271 | ), 272 | ), 273 | ), 274 | ) 275 | ], 276 | ), 277 | ); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/widgets/pulse_notification.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 4 | import 'package:confetti/confetti.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class PulseNotification extends StatefulWidget { 8 | const PulseNotification({ 9 | Key key, 10 | @required this.bloc, 11 | this.child, 12 | }) : super(key: key); 13 | 14 | final UserBloc bloc; 15 | final Widget child; 16 | 17 | @override 18 | _PulseNotificationState createState() => _PulseNotificationState(); 19 | } 20 | 21 | class _PulseNotificationState extends State 22 | with SingleTickerProviderStateMixin { 23 | AnimationController _controller; 24 | CurvedAnimation animation; 25 | 26 | @override 27 | void initState() { 28 | _controller = 29 | AnimationController(vsync: this, duration: Duration(seconds: 2)); 30 | animation = CurvedAnimation( 31 | parent: _controller, 32 | curve: Curves.elasticIn, 33 | ); 34 | super.initState(); 35 | } 36 | 37 | ConfettiController confettiController = ConfettiController( 38 | duration: Duration(seconds: 1), 39 | ); 40 | 41 | @override 42 | void dispose() { 43 | confettiController.dispose(); 44 | _controller.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | String message; 51 | return StreamBuilder( 52 | stream: widget.bloc.pulses, 53 | builder: 54 | (BuildContext context, AsyncSnapshot> snap) { 55 | if (snap.hasData && !snap.data.used) { 56 | confettiController.play(); 57 | _controller.forward(from: 0.0); 58 | message = snap.data.value; 59 | } 60 | 61 | return Stack( 62 | alignment: Alignment.center, 63 | children: [ 64 | widget.child, 65 | AnimatedBuilder( 66 | animation: animation, 67 | builder: (BuildContext context, Widget child) => 68 | Transform.rotate( 69 | angle: -pi / 12, 70 | child: Transform.translate( 71 | child: Material( 72 | type: MaterialType.transparency, 73 | child: Text( 74 | message ?? "", 75 | style: TextStyle( 76 | fontSize: 50, 77 | fontFamily: 'OCRAEXT', 78 | fontWeight: FontWeight.bold, 79 | color: Colors.green, 80 | shadows: [ 81 | Shadow( 82 | color: Colors.grey, 83 | blurRadius: 5.0, 84 | offset: Offset(3.0, .0)), 85 | ], 86 | ), 87 | ), 88 | ), 89 | offset: Offset(animation.value * 500, 0.0), 90 | ), 91 | ), 92 | ), 93 | ConfettiWidget( 94 | confettiController: confettiController, 95 | blastDirection: -pi / 2, 96 | emissionFrequency: 0.05, 97 | numberOfParticles: 5, 98 | shouldLoop: false, 99 | ) 100 | ], 101 | ); 102 | }, 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/widgets/random_loading_animation.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 5 | 6 | class RandomLoadingAnimation extends StatelessWidget { 7 | final double size; 8 | final color; 9 | 10 | const RandomLoadingAnimation({ 11 | Key key, 12 | this.size = 75, 13 | this.color = Colors.blueGrey, 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final rand = Random(); 19 | 20 | switch (rand.nextInt(17)) { 21 | case 0: 22 | return SpinKitHourGlass( 23 | color: color, 24 | size: size, 25 | ); 26 | case 1: 27 | return SpinKitCircle( 28 | color: color, 29 | size: size, 30 | ); 31 | case 2: 32 | return SpinKitCubeGrid( 33 | color: color, 34 | size: size, 35 | ); 36 | case 3: 37 | return SpinKitDoubleBounce( 38 | color: color, 39 | size: size, 40 | ); 41 | case 4: 42 | return SpinKitDualRing( 43 | color: color, 44 | size: size, 45 | ); 46 | case 5: 47 | return SpinKitFadingCube( 48 | color: color, 49 | size: size, 50 | ); 51 | case 6: 52 | return SpinKitRipple( 53 | color: color, 54 | size: size, 55 | ); 56 | case 7: 57 | return SpinKitFadingGrid( 58 | color: color, 59 | size: size, 60 | ); 61 | case 8: 62 | return SpinKitPulse( 63 | color: color, 64 | size: size, 65 | ); 66 | case 9: 67 | return SpinKitPouringHourglass( 68 | color: color, 69 | size: size, 70 | ); 71 | case 10: 72 | return SpinKitPumpingHeart( 73 | color: color, 74 | size: size, 75 | ); 76 | case 11: 77 | return SpinKitFadingFour( 78 | color: color, 79 | size: size, 80 | ); 81 | case 12: 82 | return SpinKitThreeBounce( 83 | color: color, 84 | size: size, 85 | ); 86 | case 13: 87 | return SpinKitWave( 88 | color: color, 89 | size: size, 90 | ); 91 | case 14: 92 | return SpinKitWanderingCubes( 93 | color: color, 94 | size: size, 95 | ); 96 | case 15: 97 | return SpinKitRotatingPlain( 98 | color: color, 99 | size: size, 100 | ); 101 | default: 102 | return SpinKitChasingDots( 103 | color: color, 104 | size: size, 105 | ); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/widgets/recent_period_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/widgets/fluid_slider.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class RecentPeriodSettings extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | UserBloc bloc = BlocProvider.of(context); 10 | return Padding( 11 | padding: EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 12), 12 | child: StreamBuilder( 13 | stream: bloc.recentLength, 14 | builder:(_, __) => FluidSlider( 15 | sliderColor: Colors.blueGrey.shade700, 16 | min: 2, 17 | max: 14, 18 | value: bloc.recentLength.value?.toDouble() ?? 7, 19 | onChanged: (value) => bloc.recentLength.add(value.round()), 20 | onChangeEnd: (value) { 21 | if(value.round() != 1) bloc.fetchAllUsers(); 22 | }, 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/widgets/reload_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 4 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 5 | import 'package:codestats_flutter/widgets/random_loading_animation.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | class ReloadData extends StatefulWidget { 9 | @override 10 | ReloadDataState createState() => ReloadDataState(); 11 | } 12 | 13 | class ReloadDataState extends State { 14 | StreamSubscription errorSubscription; 15 | 16 | @override 17 | void didChangeDependencies() { 18 | super.didChangeDependencies(); 19 | var bloc = BlocProvider.of(context); 20 | errorSubscription?.cancel(); 21 | errorSubscription = bloc.errors.listen( 22 | (error) => Scaffold.of(context).showSnackBar( 23 | SnackBar( 24 | content: Text(error), 25 | ), 26 | ), 27 | ); 28 | } 29 | 30 | @override 31 | void dispose() { 32 | super.dispose(); 33 | errorSubscription.cancel(); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | UserBloc bloc = BlocProvider.of(context); 39 | 40 | return StreamBuilder( 41 | stream: bloc.dataFetching, 42 | initialData: DataFetching.Done, 43 | builder: (context, snapshot) { 44 | if (snapshot.data == DataFetching.Loading) { 45 | return Padding( 46 | padding: const EdgeInsets.only(right: 16), 47 | child: RandomLoadingAnimation( 48 | color: Colors.white, 49 | size: 16, 50 | ), 51 | ); 52 | } else { 53 | return IconButton( 54 | icon: Icon(Icons.refresh), 55 | onPressed: () => bloc.fetchAllUsers(), 56 | ); 57 | } 58 | }, 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/widgets/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/bloc/state.dart'; 4 | import 'package:codestats_flutter/widgets/expandable_user_list.dart'; 5 | import 'package:codestats_flutter/widgets/recent_period_selector.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:superpower/superpower.dart'; 8 | import 'package:package_info/package_info.dart'; 9 | 10 | class Settings extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | UserBloc bloc = BlocProvider.of(context); 14 | 15 | return Scaffold( 16 | backgroundColor: Colors.blueGrey, 17 | body: ListView( 18 | padding: const EdgeInsets.only(top: 30, bottom: 30), 19 | children: [ 20 | ListTile( 21 | title: Text( 22 | "Nr of days in 'Recent' tab", 23 | style: TextStyle( 24 | color: Colors.blueGrey.shade100, 25 | fontSize: 24, 26 | fontWeight: FontWeight.bold, 27 | fontStyle: FontStyle.italic), 28 | ), 29 | ), 30 | RecentPeriodSettings(), 31 | ListTile( 32 | title: Text( 33 | "Users", 34 | style: TextStyle( 35 | color: Colors.blueGrey.shade100, 36 | fontSize: 24, 37 | fontWeight: FontWeight.bold, 38 | fontStyle: FontStyle.italic), 39 | ), 40 | trailing: IconButton( 41 | color: Colors.white, 42 | icon: Icon(Icons.add_circle_outline), 43 | onPressed: () { 44 | Navigator.of(context).pushNamed("addUser"); 45 | }, 46 | ), 47 | ), 48 | StreamBuilder( 49 | stream: bloc.userStateController, 50 | builder: (context, AsyncSnapshot snapshot) { 51 | var users = snapshot.data?.allUsers; 52 | if (users != null && users.isNotEmpty) { 53 | return ExpandableUserList( 54 | key: UniqueKey(), 55 | users: $(users.keys).sorted(), 56 | ); 57 | } else 58 | return Container(); 59 | }, 60 | ), 61 | ListTile( 62 | onTap: () async { 63 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 64 | showAboutDialog( 65 | context: context, 66 | applicationIcon: CircleAvatar( 67 | backgroundColor: Colors.transparent, 68 | child: Image.asset("assets/icon/ic_launcher.png"), 69 | ), 70 | children: [ 71 | Text("The Code::Stats logo is licensed with CC BY-NC 4.0"), 72 | Text("Copyright © 2016, Mikko Ahlroth"), 73 | ], 74 | applicationVersion: packageInfo.version, 75 | applicationName: packageInfo.appName, 76 | ); 77 | }, 78 | leading: Icon( 79 | Icons.info, 80 | color: Colors.white, 81 | ), 82 | title: Text( 83 | "About Code::Stats", 84 | style: TextStyle(color: Colors.white), 85 | ), 86 | ) 87 | ], 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/widgets/shimmer.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// * author: hunghd 3 | /// * email: hunghd.yb@gmail.com 4 | /// 5 | /// A package provides an easy way to add shimmer effect to Flutter application 6 | /// 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/rendering.dart'; 10 | 11 | /// 12 | /// An enum defines all supported directions of shimmer effect 13 | /// 14 | /// * [ShimmerDirection.ltr] left to right direction 15 | /// * [ShimmerDirection.rtl right to left direction 16 | /// * [ShimmerDirection.ttb] top to bottom direction 17 | /// * [ShimmerDirection.btt] bottom to top direction 18 | /// 19 | enum ShimmerDirection { ltr, rtl, ttb, btt } 20 | 21 | /// 22 | /// A widget renders shimmer effect over [child] widget tree. 23 | /// 24 | /// [child] defines an area that shimmer effect blends on. You can build [child] 25 | /// from whatever [Widget] you like but there're some notices in order to get 26 | /// exact expected effect and get better rendering performance: 27 | /// 28 | /// * Use static [Widget] (which is an instance of [StatelessWidget]). 29 | /// * [Widget] should be a solid color element. Every colors you set on these 30 | /// [Widget]s will be overridden by colors of [gradient]. 31 | /// * Shimmer effect only affects to opaque areas of [child], transparent areas 32 | /// still stays transparent. 33 | /// 34 | /// [period] controls the speed of shimmer effect. The default value is 1500 35 | /// milliseconds. 36 | /// 37 | /// [direction] controls the direction of shimmer effect. The default value 38 | /// is [ShimmerDirection.ltr]. 39 | /// 40 | /// [gradient] controls colors of shimmer effect. 41 | /// 42 | class Shimmer extends StatefulWidget { 43 | final Widget child; 44 | final Duration period; 45 | final ShimmerDirection direction; 46 | final Gradient gradient; 47 | 48 | Shimmer({ 49 | Key key, 50 | @required this.child, 51 | @required this.gradient, 52 | this.direction = ShimmerDirection.ltr, 53 | this.period = const Duration(milliseconds: 1500), 54 | }) : super(key: key); 55 | 56 | /// 57 | /// A convenient constructor provides an easy and convenient way to create a 58 | /// [Shimmer] which [gradient] is [LinearGradient] made up of `baseColor` and 59 | /// `highlightColor`. 60 | /// 61 | Shimmer.fromColors( 62 | {Key key, 63 | @required this.child, 64 | @required Color baseColor, 65 | @required Color highlightColor, 66 | this.period = const Duration(milliseconds: 1500), 67 | this.direction = ShimmerDirection.ltr}) 68 | : gradient = LinearGradient( 69 | begin: Alignment.topLeft, 70 | end: Alignment.centerRight, 71 | colors: [ 72 | baseColor, 73 | baseColor, 74 | highlightColor, 75 | baseColor, 76 | baseColor 77 | ], 78 | stops: [ 79 | 0.0, 80 | 0.35, 81 | 0.5, 82 | 0.65, 83 | 1.0 84 | ]), 85 | super(key: key); 86 | 87 | @override 88 | _ShimmerState createState() => _ShimmerState(); 89 | 90 | @override 91 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { 92 | super.debugFillProperties(properties); 93 | properties.add(DiagnosticsProperty('gradient', gradient, defaultValue: null)); 94 | properties.add(EnumProperty('direction', direction)); 95 | properties.add(DiagnosticsProperty('period', period, defaultValue: null)); 96 | } 97 | 98 | } 99 | 100 | class _ShimmerState extends State with SingleTickerProviderStateMixin { 101 | AnimationController controller; 102 | 103 | @override 104 | void initState() { 105 | super.initState(); 106 | controller = AnimationController(vsync: this, duration: widget.period) 107 | ..addListener(() { 108 | setState(() {}); 109 | }); 110 | /* ..addStatusListener((status) { 111 | if (status == AnimationStatus.completed) { 112 | controller.repeat(); 113 | } 114 | });*/ 115 | controller.forward(); 116 | } 117 | 118 | @override 119 | void didUpdateWidget(Shimmer oldWidget) { 120 | 121 | super.didUpdateWidget(oldWidget); 122 | controller.forward(from: 0.0); 123 | } 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | return _Shimmer( 128 | child: widget.child, 129 | direction: widget.direction, 130 | gradient: widget.gradient, 131 | percent: controller.value, 132 | ); 133 | } 134 | 135 | @override 136 | void dispose() { 137 | controller.dispose(); 138 | super.dispose(); 139 | } 140 | } 141 | 142 | class _Shimmer extends SingleChildRenderObjectWidget { 143 | final double percent; 144 | final ShimmerDirection direction; 145 | final Gradient gradient; 146 | 147 | _Shimmer({Widget child, this.percent, this.direction, this.gradient}) 148 | : super(child: child); 149 | 150 | @override 151 | _ShimmerFilter createRenderObject(BuildContext context) { 152 | return _ShimmerFilter(percent, direction, gradient); 153 | } 154 | 155 | @override 156 | void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) { 157 | shimmer.percent = percent; 158 | } 159 | } 160 | 161 | class _ShimmerFilter extends RenderProxyBox { 162 | final _clearPaint = Paint(); 163 | final Paint _gradientPaint; 164 | final Gradient _gradient; 165 | final ShimmerDirection _direction; 166 | double _percent; 167 | Rect _rect; 168 | 169 | _ShimmerFilter(this._percent, this._direction, this._gradient) 170 | : _gradientPaint = Paint()..blendMode = BlendMode.srcIn; 171 | 172 | @override 173 | bool get alwaysNeedsCompositing => child != null; 174 | 175 | set percent(double newValue) { 176 | if (newValue != _percent) { 177 | _percent = newValue; 178 | markNeedsPaint(); 179 | } 180 | } 181 | 182 | @override 183 | void paint(PaintingContext context, Offset offset) { 184 | if (child != null) { 185 | assert(needsCompositing); 186 | 187 | final width = child.size.width; 188 | final height = child.size.height; 189 | Rect rect; 190 | double dx, dy; 191 | if (_direction == ShimmerDirection.rtl) { 192 | dx = _offset(width, -width, _percent); 193 | dy = 0.0; 194 | rect = Rect.fromLTWH(offset.dx - width, offset.dy, 3 * width, height); 195 | } else if (_direction == ShimmerDirection.ttb) { 196 | dx = 0.0; 197 | dy = _offset(-height, height, _percent); 198 | rect = Rect.fromLTWH(offset.dx, offset.dy - height, width, 3 * height); 199 | } else if (_direction == ShimmerDirection.btt) { 200 | dx = 0.0; 201 | dy = _offset(height, -height, _percent); 202 | rect = Rect.fromLTWH(offset.dx, offset.dy - height, width, 3 * height); 203 | } else { 204 | dx = _offset(-width, width, _percent); 205 | dy = 0.0; 206 | rect = Rect.fromLTWH(offset.dx - width, offset.dy, 3 * width, height); 207 | } 208 | if (_rect != rect) { 209 | _gradientPaint.shader = _gradient.createShader(rect); 210 | _rect = rect; 211 | } 212 | 213 | context.canvas.saveLayer(offset & child.size, _clearPaint); 214 | context.paintChild(child, offset); 215 | context.canvas.translate(dx, dy); 216 | context.canvas.drawRect(rect, _gradientPaint); 217 | context.canvas.restore(); 218 | } 219 | } 220 | 221 | double _offset(double start, double end, double percent) { 222 | return start + (end - start) * percent; 223 | } 224 | } -------------------------------------------------------------------------------- /lib/widgets/spotlight.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | import 'package:flutter/painting.dart'; 8 | 9 | import 'dots_indicator.dart'; 10 | 11 | class _SpotlightLayoutDelegate extends MultiChildLayoutDelegate { 12 | _SpotlightLayoutDelegate({ 13 | @required this.page, 14 | @required this.itemCount, 15 | }); 16 | 17 | final double page; 18 | final int itemCount; 19 | 20 | final double _z = 1.30; 21 | 22 | @override 23 | void performLayout(Size size) { 24 | final double offset = (page % itemCount) / itemCount; 25 | final Offset center = Offset(size.width / 2, size.height / 4); 26 | 27 | for (int i = 0; i < itemCount; i++) { 28 | final String childId = 'item$i'; 29 | final double alpha = (i + offset * itemCount) * (2 * pi / itemCount); 30 | 31 | final double x = (1 - sin(alpha)) / 2; 32 | final double z = _z - (1 - cos(alpha)) / 2; 33 | 34 | if (hasChild(childId)) { 35 | final Size imageSize = 36 | layoutChild(childId, BoxConstraints.tight((size / 4) * z)); 37 | 38 | positionChild(childId, 39 | Offset(size.width * x - (size.width / 8 * z), size.height / 6)); 40 | } 41 | } 42 | } 43 | 44 | @override 45 | bool shouldRelayout(_SpotlightLayoutDelegate oldDelegate) => 46 | page != oldDelegate.page || itemCount != oldDelegate.itemCount; 47 | } 48 | 49 | class Spotlight extends StatefulWidget { 50 | const Spotlight({ 51 | Key key, 52 | @required this.children, 53 | @required this.titles, 54 | @required this.descriptions, 55 | }) : assert(children.length == descriptions.length), 56 | super(key: key); 57 | 58 | final List children; 59 | final List titles; 60 | final List descriptions; 61 | 62 | @override 63 | _SpotlightState createState() => _SpotlightState(); 64 | } 65 | 66 | class _SpotlightState extends State { 67 | final PageController _pageController = PageController(keepPage: false); 68 | static const Duration _kDuration = Duration(milliseconds: 300); 69 | static const Cubic _kCurve = Curves.ease; 70 | 71 | double _page = 0.0; 72 | int _pageIndex = 0; 73 | 74 | int get itemCount => widget.children.length; 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | final List imagesWithId = []; 79 | final List renderLast = []; 80 | // Paint order is determined by order of layout ids 81 | for (int i = 0; i < itemCount; i++) { 82 | final double offset = (_page % itemCount) / itemCount; 83 | final double alpha = (i + offset * itemCount) * (2 * pi / itemCount); 84 | // If in foreground, render last 85 | if (alpha % (2 * pi) < pi / 2 || alpha % (2 * pi) > 3 * pi / 2) { 86 | renderLast.add(LayoutId( 87 | id: 'item$i', 88 | child: widget.children[i], 89 | )); 90 | continue; 91 | } 92 | imagesWithId.add(LayoutId( 93 | id: 'item$i', 94 | child: widget.children[i], 95 | )); 96 | } 97 | imagesWithId.addAll(renderLast); 98 | return Stack( 99 | children: [ 100 | NotificationListener( 101 | onNotification: (ScrollNotification notification) { 102 | if (notification.depth == 0 && 103 | notification is ScrollUpdateNotification) { 104 | final PageMetrics metrics = notification.metrics; 105 | if (metrics.page >= 0) { 106 | setState(() { 107 | _page = metrics.page; 108 | _pageIndex = 109 | (itemCount - (_page % itemCount).round()) % itemCount; 110 | }); 111 | } 112 | } 113 | return false; 114 | }, 115 | child: Scrollable( 116 | dragStartBehavior: DragStartBehavior.start, 117 | axisDirection: AxisDirection.right, 118 | controller: _pageController, 119 | physics: const PageScrollPhysics(), 120 | viewportBuilder: (BuildContext context, ViewportOffset position) { 121 | return Viewport( 122 | offset: position, 123 | axisDirection: AxisDirection.right, 124 | slivers: [ 125 | SliverFixedExtentList( 126 | itemExtent: MediaQuery.of(context).size.width, 127 | delegate: SliverChildBuilderDelegate( 128 | (BuildContext context, int index) { 129 | return Container( 130 | color: Colors.transparent, 131 | ); 132 | }), 133 | ), 134 | ], 135 | ); 136 | }, 137 | ), 138 | ), 139 | CustomMultiChildLayout( 140 | children: imagesWithId, 141 | delegate: _SpotlightLayoutDelegate( 142 | itemCount: itemCount, 143 | page: _page, 144 | ), 145 | ), 146 | Positioned( 147 | bottom: 0, 148 | left: 0, 149 | right: 0, 150 | height: MediaQuery.of(context).size.height / 2, 151 | child: Padding( 152 | padding: EdgeInsets.all(25.0), 153 | child: Column( 154 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 155 | crossAxisAlignment: CrossAxisAlignment.center, 156 | children: [ 157 | Text(widget.titles[_pageIndex], 158 | style: Theme.of(context).textTheme.headline), 159 | Text( 160 | widget.descriptions[_pageIndex], 161 | style: Theme.of(context) 162 | .textTheme 163 | .body2 164 | .copyWith(fontSize: 18.0), 165 | textAlign: TextAlign.center, 166 | ), 167 | ], 168 | ), 169 | ), 170 | ), 171 | Positioned( 172 | bottom: 0, 173 | left: 0, 174 | right: 0, 175 | child: Padding( 176 | padding: EdgeInsets.all(20.0), 177 | child: DotsIndicator( 178 | controller: _pageController, 179 | itemCount: itemCount, 180 | color: CupertinoColors.inactiveGray, 181 | onPageSelected: (int page) { 182 | _pageController.animateToPage( 183 | page, 184 | duration: _kDuration, 185 | curve: _kCurve, 186 | ); 187 | }, 188 | ), 189 | ), 190 | ), 191 | ], 192 | ); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /lib/widgets/subheader.dart: -------------------------------------------------------------------------------- 1 | import 'package:auto_size_text/auto_size_text.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SubHeader extends StatelessWidget { 5 | const SubHeader({ 6 | Key key, 7 | @required this.text, 8 | }) : super(key: key); 9 | 10 | final String text; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Padding( 15 | padding: EdgeInsets.only(top: 20, left: 16, right: 16, bottom: 20), 16 | child: Center( 17 | child: AutoSizeText( 18 | text, 19 | maxLines: 1, 20 | style: TextStyle( 21 | fontWeight: FontWeight.bold, 22 | fontSize: 30, 23 | fontFamily: 'OCRAEXT', 24 | shadows: [ 25 | Shadow( 26 | offset: Offset(2.5, 2.5), 27 | blurRadius: 3.0, 28 | color: Colors.black.withOpacity(0.3), 29 | ), 30 | ], 31 | ), 32 | ), 33 | ), 34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /lib/widgets/tab_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:bubble_tab_indicator/bubble_tab_indicator.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/widgets/backdrop.dart'; 4 | import 'package:codestats_flutter/widgets/choose_user_menu.dart'; 5 | import 'package:codestats_flutter/widgets/dash_board_body.dart'; 6 | import 'package:codestats_flutter/widgets/glass_crack/glass_crack.dart'; 7 | import 'package:codestats_flutter/widgets/pulse_notification.dart'; 8 | import 'package:codestats_flutter/widgets/reload_data.dart'; 9 | import 'package:codestats_flutter/widgets/settings.dart'; 10 | import 'package:flutter/material.dart'; 11 | 12 | class TabNavigator extends StatefulWidget { 13 | final UserBloc bloc; 14 | 15 | TabNavigator({Key key, this.bloc}) : super(key: key); 16 | 17 | @override 18 | _TabNavigatorState createState() => _TabNavigatorState(); 19 | } 20 | 21 | class _TabNavigatorState extends State { 22 | final tabs = [ 23 | Tab(text: "Profile"), 24 | Tab(text: "Recent"), 25 | Tab(text: "Languages"), 26 | Tab(text: "Year") 27 | ]; 28 | 29 | GlobalKey backdropKey = GlobalKey(); 30 | bool breakGlass = false; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return DefaultTabController( 35 | length: tabs.length, 36 | child: WillPopScope( 37 | child: Stack( 38 | children: [ 39 | PulseNotification( 40 | bloc: widget.bloc, 41 | child: BackdropScaffold( 42 | key: backdropKey, 43 | title: StreamBuilder( 44 | stream: widget.bloc.selectedUser, 45 | builder: (context, snapshot) => Text(snapshot.data ?? ""), 46 | ), 47 | appbarBottom: TabBar( 48 | indicatorSize: TabBarIndicatorSize.tab, 49 | indicator: BubbleTabIndicator( 50 | indicatorHeight: 25.0, 51 | indicatorColor: Colors.blueGrey.shade600, 52 | tabBarIndicatorSize: TabBarIndicatorSize.tab, 53 | ), 54 | tabs: tabs, 55 | ), 56 | frontLayer: Stack( 57 | alignment: Alignment.center, 58 | children: [ 59 | DashBoardBody( 60 | bloc: widget.bloc, 61 | ), 62 | ], 63 | ), 64 | backLayer: Settings(), 65 | iconPosition: BackdropIconPosition.leading, 66 | actions: [ 67 | ReloadData(), 68 | ChooseUserMenu(), 69 | ], 70 | ), 71 | ), 72 | if (breakGlass) 73 | Positioned.fill( 74 | child: GestureDetector( 75 | onTap: () { 76 | setState(() { 77 | breakGlass = false; 78 | }); 79 | }, 80 | child: GlassCrack(), 81 | ), 82 | ), 83 | ], 84 | ), 85 | onWillPop: () async { 86 | if (breakGlass || 87 | (backdropKey.currentState?.isBackPanelVisible ?? false)) { 88 | return true; 89 | } 90 | setState(() { 91 | breakGlass = true; 92 | }); 93 | return false; 94 | }, 95 | ), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/widgets/tiltable_stack.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:codestats_flutter/main.dart'; 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/physics.dart'; 7 | import 'package:flutter/rendering.dart'; 8 | import 'package:rxdart/rxdart.dart'; 9 | 10 | class TiltableStack extends StatefulWidget { 11 | final List children; 12 | final Alignment alignment; 13 | 14 | const TiltableStack({ 15 | Key key, 16 | @required this.children, 17 | this.alignment = Alignment.center, 18 | }) : super(key: key); 19 | 20 | @override 21 | _TiltableStackState createState() => _TiltableStackState(); 22 | } 23 | 24 | class _TiltableStackState extends State 25 | with TickerProviderStateMixin { 26 | AnimationController tilt; 27 | AnimationController depth; 28 | double pitch = 0; 29 | double yaw = 0; 30 | Offset _offset; 31 | SpringSimulation springSimulation; 32 | PublishSubject stream = PublishSubject(); 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | tilt = AnimationController( 38 | value: 1, 39 | duration: const Duration(milliseconds: 500), 40 | vsync: this, 41 | lowerBound: -2, 42 | upperBound: 2, 43 | )..addListener(() { 44 | if (_offset == null) { 45 | pitch *= tilt.value; 46 | yaw *= tilt.value; 47 | updateTransformation(); 48 | } 49 | }); 50 | depth = AnimationController( 51 | value: 0, 52 | duration: const Duration(milliseconds: 500), 53 | vsync: this, 54 | lowerBound: -2, 55 | upperBound: 2, 56 | )..addListener(updateTransformation); 57 | } 58 | 59 | @override 60 | dispose() { 61 | tilt.dispose(); 62 | depth.dispose(); 63 | stream.close(); 64 | super.dispose(); 65 | } 66 | 67 | updateTransformation() { 68 | stream.add(true); 69 | } 70 | 71 | SpringDescription spring = SpringDescription(mass: 1, stiffness: 400, damping: 6); 72 | 73 | cancelPan() { 74 | tilt.animateWith(SpringSimulation(spring, 1, 0, tilt.velocity)); 75 | depth.animateWith(SpringSimulation(spring, depth.value, 0, depth.velocity)); 76 | _offset = null; 77 | } 78 | 79 | startPan() { 80 | CodeStatsApp.platform.invokeMethod("startFourier"); 81 | depth.animateWith(SpringSimulation(spring, depth.value, 1, depth.velocity)); 82 | } 83 | 84 | updatePan(LongPressMoveUpdateDetails drag) { 85 | var size = MediaQuery.of(context).size; 86 | var offset = _globalToLocal(context, drag.globalPosition); 87 | if (_offset == null) { 88 | _offset = offset; 89 | } 90 | 91 | pitch += (offset.dy - _offset.dy) * (1 / size.height); 92 | yaw -= (offset.dx - _offset.dx) * (1 / size.width); 93 | _offset = offset; 94 | 95 | updateTransformation(); 96 | } 97 | 98 | @override 99 | Widget build(BuildContext context) { 100 | return GestureDetector( 101 | onLongPressMoveUpdate: updatePan, 102 | onLongPressStart: (_) => startPan(), 103 | onLongPressEnd: (_) => cancelPan(), 104 | onTapDown: (_) => depth.animateWith(SpringSimulation(spring, depth.value, 0, -5)), 105 | child: StreamBuilder( 106 | stream: stream.stream, 107 | builder: (context, snap) => TiltedStack( 108 | data: TransformationData(pitch, yaw, depth.value), 109 | alignment: widget.alignment, 110 | children: widget.children, 111 | ), 112 | ), 113 | ); 114 | } 115 | 116 | Offset _globalToLocal(BuildContext context, Offset globalPosition) { 117 | final RenderBox box = context.findRenderObject(); 118 | return box.globalToLocal(globalPosition); 119 | } 120 | } 121 | 122 | class TransformationData { 123 | final double pitch; 124 | final double yaw; 125 | final double depth; 126 | 127 | TransformationData(this.pitch, this.yaw, this.depth); 128 | } 129 | 130 | class TiltedStack extends StatelessWidget { 131 | const TiltedStack({ 132 | Key key, 133 | @required this.children, 134 | @required this.alignment, 135 | @required this.data, 136 | }) : super(key: key); 137 | 138 | final TransformationData data; 139 | final List children; 140 | final AlignmentGeometry alignment; 141 | 142 | @override 143 | Widget build(BuildContext context) => Stack( 144 | alignment: alignment, 145 | children: children 146 | .asMap() 147 | .map( 148 | (i, element) { 149 | return MapEntry( 150 | i, 151 | Stack( 152 | alignment: Alignment.center, 153 | children: [ 154 | Transform( 155 | transform: Matrix4.identity() 156 | ..setEntry(3, 2, 0.001) 157 | ..rotateX(data.pitch) 158 | ..rotateY(data.yaw) 159 | ..translate( 160 | -data.yaw * i * 70, data.pitch * i * 70, 0) 161 | ..scale((data.depth ?? 0) * (i + 1) * 0.1 + 1), 162 | child: element, 163 | alignment: FractionalOffset.center, 164 | ), 165 | if (element.key is ValueKey && (data.depth ?? 0) > 0) 166 | Opacity( 167 | child: Transform( 168 | transform: Matrix4.identity() 169 | ..setEntry(3, 2, 0.001) 170 | ..rotateX(data.pitch) 171 | ..rotateY(data.yaw) 172 | ..translate(-data.yaw * i * 1.5 * 70, 173 | data.pitch * i * 1.5 * 70, 0) 174 | ..scale((data.depth ?? 0) * (i + 1) * 0.1 + 1), 175 | child: children[i + 1], 176 | alignment: FractionalOffset.center, 177 | ), 178 | opacity: 0.08, 179 | ), 180 | ], 181 | ), 182 | ); 183 | }, 184 | ) 185 | .values 186 | .toList(growable: false), 187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /lib/widgets/total_xp_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:codestats_flutter/bloc/bloc_provider.dart'; 2 | import 'package:codestats_flutter/bloc/codestats_bloc.dart'; 3 | import 'package:codestats_flutter/widgets/bouncable.dart'; 4 | import 'package:flip_panel/flip_panel.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_midi/flutter_midi.dart'; 7 | import 'package:rxdart/rxdart.dart'; 8 | import 'package:superpower/superpower.dart'; 9 | import 'package:tonic/tonic.dart'; 10 | 11 | class TotalXp extends StatelessWidget { 12 | const TotalXp({ 13 | Key key, 14 | @required this.totalXp, 15 | }) : super(key: key); 16 | 17 | final int totalXp; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final UserBloc bloc = BlocProvider.of(context); 22 | final scale = 23 | ScalePattern.findByName('Harmonic Minor').at(PitchClass(integer: 4)); 24 | List midis = []; 25 | 26 | final xpStr = "$totalXp"; 27 | var digitWidgits = List(); 28 | 29 | $(xpStr.split('')).forEachIndexed( 30 | (char, index) { 31 | midis.add(name2midi(scale.pitchClasses[index] 32 | .toPitch(octave: index > 4 ? 4 : 3) 33 | .toString())); 34 | digitWidgits.add( 35 | Expanded( 36 | child: Bouncable( 37 | onTap: () => FlutterMidi.playMidiNote(midi: midis[index]), 38 | child: LayoutBuilder( 39 | builder: (context, constraints) => Container( 40 | margin: EdgeInsets.all(4.0), 41 | decoration: ShapeDecoration( 42 | shadows: [ 43 | BoxShadow( 44 | color: Colors.grey, 45 | blurRadius: 2, 46 | //spreadRadius: 1, 47 | offset: Offset(3, 3)) 48 | ], 49 | shape: RoundedRectangleBorder( 50 | borderRadius: BorderRadius.circular(10), 51 | ), 52 | ), 53 | child: FlipPanel.stream( 54 | key: ValueKey(bloc.currentUserController.value), 55 | spacing: 2, 56 | initValue: "0", 57 | duration: Duration(milliseconds: 100), 58 | itemStream: Observable.concat( 59 | [ 60 | Observable.range(0, int.parse(char)).transform( 61 | IntervalStreamTransformer( 62 | Duration(milliseconds: 250), 63 | ), 64 | ), 65 | bloc.currentUser.map((user) { 66 | if(user.data?.totalXp != null && "${user.data.totalXp}".length > index) 67 | return "${user.data.totalXp}"[index]; 68 | else 69 | return "0"; 70 | }), 71 | ], 72 | ), 73 | itemBuilder: (context, value) => Container( 74 | decoration: BoxDecoration( 75 | borderRadius: BorderRadius.circular(10), 76 | color: Colors.blueGrey.shade200, 77 | ), 78 | padding: EdgeInsets.all(6.0), 79 | child: SizedBox( 80 | width: constraints.maxWidth - 12, 81 | height: constraints.maxWidth, 82 | child: Center( 83 | child: Text( 84 | '$value', 85 | style: TextStyle( 86 | fontWeight: FontWeight.bold, 87 | fontSize: constraints.maxWidth * 0.9, 88 | fontFamily: "OCRAEXT", 89 | color: Colors.white, 90 | ), 91 | maxLines: 1, 92 | ), 93 | ), 94 | ), 95 | ), 96 | ), 97 | ), 98 | ), 99 | ), 100 | ), 101 | ); 102 | }, 103 | ); 104 | 105 | return Padding( 106 | padding: EdgeInsets.symmetric(horizontal: 4, vertical: 8), 107 | child: Row( 108 | children: digitWidgits, 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/widgets/wave_progress.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class WaveProgress extends StatefulWidget { 6 | final double size; 7 | final Color fillColor; 8 | final double progress; 9 | final int frequency; 10 | 11 | WaveProgress(this.size, this.fillColor, this.progress, 12 | {Key key, this.frequency = 0}) 13 | : super(key: key); 14 | 15 | @override 16 | WaveProgressState createState() => WaveProgressState(); 17 | } 18 | 19 | class WaveProgressState extends State 20 | with TickerProviderStateMixin { 21 | AnimationController waveController; 22 | AnimationController heightController; 23 | Animation heightAnimation; 24 | Tween heightTween; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | waveController = AnimationController( 30 | vsync: this, 31 | duration: Duration(milliseconds: 2500), 32 | ); 33 | 34 | heightController = AnimationController( 35 | vsync: this, 36 | duration: Duration(seconds: 1), 37 | ); 38 | 39 | heightAnimation = CurvedAnimation( 40 | parent: Tween(begin: 0.0, end: 1.0).animate(heightController), 41 | curve: Curves.easeOutCubic, 42 | ); 43 | 44 | heightTween = Tween(begin: 0.0, end: widget.progress); 45 | 46 | heightController.forward(from: 0.0); 47 | waveController.repeat(); 48 | } 49 | 50 | @override 51 | void dispose() { 52 | waveController.dispose(); 53 | heightController.dispose(); 54 | super.dispose(); 55 | } 56 | 57 | void update(double progress) { 58 | setState(() { 59 | heightTween = Tween( 60 | begin: heightTween.evaluate(heightController), 61 | end: progress, 62 | ); 63 | heightController.forward(from: 0.0); 64 | }); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Container( 70 | width: widget.size, 71 | height: widget.size, 72 | //decoration: new BoxDecoration(color: Colors.green), 73 | child: ClipPath( 74 | clipper: CircleClipper(), 75 | child: AnimatedBuilder( 76 | animation: waveController, 77 | builder: (BuildContext context, Widget child) { 78 | return CustomPaint( 79 | painter: WaveProgressPainter(heightTween.animate(heightAnimation), 80 | waveController, widget.fillColor, widget.frequency), 81 | ); 82 | }, 83 | ), 84 | ), 85 | ); 86 | } 87 | } 88 | 89 | class WaveProgressPainter extends CustomPainter { 90 | Animation _waveAnimation; 91 | Animation _heightAnimation; 92 | Color fillColor; 93 | final int frequency; 94 | 95 | WaveProgressPainter( 96 | this._heightAnimation, 97 | this._waveAnimation, 98 | this.fillColor, 99 | this.frequency, 100 | ) : super(repaint: _waveAnimation); 101 | 102 | @override 103 | void paint(Canvas canvas, Size size) { 104 | // draw small wave 105 | double p = _heightAnimation.value; 106 | double baseHeight = (1 - p) * size.height; 107 | 108 | Paint wave2Paint = Paint()..color = fillColor.withOpacity(0.5); 109 | double n = 4.2; 110 | double amp = 4.0; 111 | 112 | Path path = Path(); 113 | path.moveTo(0.0, baseHeight); 114 | for (double i = 0.0; i < size.width; i++) { 115 | path.lineTo( 116 | i, 117 | baseHeight + 118 | sin((i / size.width * 2 * pi * n) + 119 | (_waveAnimation.value * 2 * pi) + 120 | pi * 1) * 121 | amp); 122 | } 123 | 124 | path.lineTo(size.width, size.height); 125 | path.lineTo(0.0, size.height); 126 | path.close(); 127 | canvas.drawPath(path, wave2Paint); 128 | 129 | // draw big wave 130 | Paint wave1Paint = Paint()..color = fillColor; 131 | n = 2.2; 132 | amp = 10.0; 133 | 134 | path = Path(); 135 | path.moveTo(0.0, baseHeight); 136 | for (double i = 0.0; i < size.width; i++) { 137 | path.lineTo( 138 | i, 139 | baseHeight + 140 | sin((i / size.width * 2 * pi * n) + 141 | (_waveAnimation.value * 2 * pi)) * 142 | amp); 143 | } 144 | 145 | path.lineTo(size.width, size.height); 146 | path.lineTo(0.0, size.height); 147 | path.close(); 148 | canvas.drawPath(path, wave1Paint); 149 | } 150 | 151 | @override 152 | bool shouldRepaint(CustomPainter oldDelegate) => true; 153 | } 154 | 155 | class CircleClipper extends CustomClipper { 156 | @override 157 | Path getClip(Size size) { 158 | return Path() 159 | ..addOval(Rect.fromCircle( 160 | center: Offset(size.width / 2, size.height / 2), 161 | radius: size.width / 2)); 162 | } 163 | 164 | @override 165 | bool shouldReclip(CustomClipper oldClipper) { 166 | return true; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /midi/zelda.sf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/midi/zelda.sf2 -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: codestats_flutter 2 | description: View codestats in your phone 3 | 4 | version: 1.0.22+23 5 | 6 | environment: 7 | sdk: '>=2.2.2 <3.0.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | version: <=1.2.1 13 | package_info: ^0.3.2 14 | date_format: ^1.0.5 15 | charts_flutter: 16 | path: ../charts/charts_flutter 17 | superpower: ^0.4.0 18 | flutter_circular_chart: 19 | git: 20 | url: https://github.com/Schwusch/flutter_circular_chart.git 21 | fl_chart: ^0.1.5 22 | intl: ^0.15.7 23 | json_annotation: ^2.0.0 24 | phoenix_wings: ^0.2.1 25 | dio: 2.1.13 26 | flutter_spinkit: ^3.1.0 27 | rxdart: ^0.20.0 28 | share: ^0.5.3 29 | auto_size_text: ^0.3.0 30 | random_color: ^1.0.3 31 | path_provider: ^1.0.0 32 | flutter_inappbrowser: ^1.2.1 33 | flip_panel: ^1.0.0 34 | draggable_scrollbar: ^0.0.4 35 | bubble_tab_indicator: ^0.1.4 36 | image: ^2.1.4 37 | # Music related 38 | tonic: 0.2.3 39 | flutter_midi: ^1.0.0 40 | # ??? 41 | confetti: ^0.1.0 42 | 43 | dev_dependencies: 44 | json_serializable: ^2.0.1 45 | build_runner: ^1.1.2 46 | 47 | flutter: 48 | uses-material-design: true 49 | assets: 50 | - assets/icon/ic_launcher.png 51 | - midi/zelda.sf2 52 | 53 | fonts: 54 | - family: OCRAEXT 55 | fonts: 56 | - asset: fonts/OCRAEXT.TTF 57 | -------------------------------------------------------------------------------- /screenshots/adduser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/adduser.png -------------------------------------------------------------------------------- /screenshots/demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/demo.webp -------------------------------------------------------------------------------- /screenshots/languages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/languages.png -------------------------------------------------------------------------------- /screenshots/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/profile.png -------------------------------------------------------------------------------- /screenshots/recent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/recent.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/settings.png -------------------------------------------------------------------------------- /screenshots/year.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schwusch/codestats_flutter/11b1fdd0ca5ac7a7660ef6b4ea059946ef7b8b13/screenshots/year.png --------------------------------------------------------------------------------