Compare commits

..

98 Commits

Author SHA1 Message Date
kolaente 1bfec41239
feat(ci): build app for PRs and pushes to main 2021-10-25 20:24:03 +02:00
kolaente 032bf1c532
feat(ci): build & upload release apk 2021-10-11 22:49:40 +02:00
kolaente 0db897f4f0
feat(ci): build & upload release apk 2021-10-11 22:43:30 +02:00
kolaente f0c3b92220
feat(ci): upload release bundle artifact 2021-10-11 22:25:42 +02:00
kolaente 21256ca9ec
fix: don't try to set headline 2021-10-11 22:09:25 +02:00
kolaente 76fedd0c49
feat(ci): use stable flutter version 2021-10-11 22:08:51 +02:00
kolaente 8a7c40653c
fix(ci): don't run build runner 2021-10-11 22:04:49 +02:00
kolaente b7e3536c9f
fix(deps): remove broken test package constraint 2021-10-11 22:02:08 +02:00
kolaente 39057668af
feat(ci): release on pushes to main 2021-10-11 21:58:28 +02:00
kolaente d0b72f49e9
fix(ci): invalid yaml 2021-10-11 21:57:12 +02:00
kolaente de808b65b3
fix(ci): invalid yaml 2021-10-11 21:56:27 +02:00
kolaente 97b7793805
feat(ci): add release apk build 2021-10-11 21:55:14 +02:00
Aleksandr Borisenko af6c8e895b Updated .gitingore 2021-08-23 10:24:30 +03:00
Aleksandr Borisenko 7b50bce37b Updated Flutter & SDK 2021-08-23 10:08:11 +03:00
Aleksandr Borisenko 9231772185 ListProvider from PR39 with fixes 2021-06-10 10:58:25 +03:00
Aleksandr Borisenko 21ef483c32 Ported working the Edit Task dialog 2021-06-08 08:50:05 +03:00
Aleksandr Borisenko 74f7756626 Merged and fixed PRs 37 & 39 2021-06-04 12:34:25 +03:00
Aleksandr Borisenko 7bb04473a3 Null aware handler for the owner field 2021-03-19 21:13:44 +03:00
Aleksandr Borisenko 50e17b045a Minor fixes; Added signing config for Android 2021-03-19 18:13:38 +03:00
Aleksandr Borisenko 59d5907b29 Fixes for API calls 2021-03-15 10:31:38 +03:00
Aleksandr Borisenko 493d965562 Fixes for Flutter 2.0 2021-03-08 09:06:07 +03:00
JonasFranz da80f1853c Upgrade android embedding to v2 (#60)
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #60
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: JonasFranz <email@jfdev.de>
Co-committed-by: JonasFranz <email@jfdev.de>
2021-01-18 13:20:32 +00:00
JonasFranz 9892d4d1ad Fix badges (#59)
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #59
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: JonasFranz <email@jfdev.de>
Co-committed-by: JonasFranz <email@jfdev.de>
2021-01-17 15:03:10 +00:00
Jonas Franz 08f7b6a60f Re-enable dependencies for iOS build 2021-01-17 15:52:04 +01:00
Jonas Franz d38bad5220 Remove depenedncy 2021-01-16 19:07:30 +01:00
Jonas Franz db99873619 rbenv fix 2021-01-16 19:07:10 +01:00
Jonas Franz 5d3d9fe0db Add rbenv 2021-01-16 19:01:36 +01:00
Jonas Franz 405bfb4e81 Use local bundle path 2021-01-16 18:15:34 +01:00
Jonas Franz 00fdc3eeda Use bundle install 2021-01-16 18:10:34 +01:00
JonasFranz 0a141f6e11 Fix iOS build (#58)
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #58
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: JonasFranz <email@jfdev.de>
Co-committed-by: JonasFranz <email@jfdev.de>
2021-01-10 20:41:44 +00:00
renovate cacdb9996c Update dependency gradle to v4.10.3 (#54)
Update dependency gradle to v4.10.3

Reviewed-on: #54
Co-Authored-By: renovate <renovatebot@kolaente.de>
Co-Committed-By: renovate <renovatebot@kolaente.de>
2020-12-26 17:54:01 +00:00
renovate 6f9c8f7149 Update dependency flutter_secure_storage to v3.3.5 (#53)
Update dependency flutter_secure_storage to v3.3.5

Reviewed-on: #53
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-Authored-By: renovate <renovatebot@kolaente.de>
Co-Committed-By: renovate <renovatebot@kolaente.de>
2020-12-26 17:53:48 +00:00
kolaente a539e13aca Fix s3 release bucket credentials 2020-12-26 18:42:53 +01:00
renovate b56c40a881 Update dependency flutter_launcher_icons to ^0.8.0 (#52)
Update dependency flutter_launcher_icons to ^0.8.0

Reviewed-on: #52
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-Authored-By: renovate <renovatebot@kolaente.de>
Co-Committed-By: renovate <renovatebot@kolaente.de>
2020-12-25 11:42:11 +00:00
renovate 5779daed83 Configure Renovate (#51)
Add renovate.json

Reviewed-on: #51
Co-Authored-By: renovate <renovatebot@kolaente.de>
Co-Committed-By: renovate <renovatebot@kolaente.de>
2020-12-24 14:17:01 +00:00
JonasFranz fffb759fc9 Remove chown to fix ci (#49)
Remove chown to fix ci

Reviewed-on: #49
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-Authored-By: JonasFranz <email@jfdev.de>
Co-Committed-By: JonasFranz <email@jfdev.de>
2020-10-24 10:47:00 +00:00
kolaente 05c9a96a50 Use the updated logo 2020-09-03 21:29:23 +02:00
kolaente bbcd24447a Update dependencies 2020-07-10 10:34:58 +02:00
kolaente d18b049fb6 Revert "Add sentry (#43)"
This reverts commit 9c2622e7
2020-07-10 10:32:16 +02:00
JonasFranz 239ff62fc5 Publish iOS Version into Testers group automatically (#47)
Update 'lib/constants.dart'

Remove test branch from pipeline

Add push

Add beta dist

Co-authored-by: Jonas Franz <email@jfdev.de>
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #47
Reviewed-by: konrad <konrad@kola-entertainments.de>
2020-06-24 08:37:29 +00:00
JonasFranz 826acc26f8 Add dark mode (#46)
Merge branch 'master' into feature/dark-mode

Add white logo in dark mode

Make button shadow dark

Format

Add dark mode

Co-authored-by: kolaente <k@knt.li>
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #46
Reviewed-by: konrad <konrad@kola-entertainments.de>
2020-06-17 17:41:40 +00:00
JonasFranz 84ab307e5f Improve error handling (#45)
Improve error handling

Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #45
Reviewed-by: konrad <konrad@kola-entertainments.de>
2020-06-17 16:51:23 +00:00
JonasFranz e371c55a52 Provide build for iOS (#42)
Remove pipeline for feature/ios

Merge branch 'master' of ssh://git.kolaente.de:9022/vikunja/app into feature/ios

Fix CI

Add keychain pw

ensure keychain

Disable code signing while building

Fix CI

Merge branch 'feature/ios' of ssh://git.kolaente.de:9022/vikunja/app into feature/ios

Add keychain password

Merge branch 'master' into feature/ios

Add compliance

Add secrets

Remove build app step

Use other version

Set Utf8

Fix CI

Fix CI

Fix beta deployment

Add deploy step

Fix CI

Fix CI

Fix ci

fix ci

fix ci

test ci

fix ci

Fix keychain

Fix keychain

Use custom keychain

Add security unlock

Add MATCH_PASSWORD

Add match

Add fastlane

Add ios pipeline to build

Add ios pipeline to build

Co-authored-by: Jonas Franz <info@jonasfranz.software>
Co-authored-by: Buildslave <buildslave@macmini.fritz.box>
Reviewed-on: #42
Reviewed-by: konrad <konrad@kola-entertainments.de>
2020-06-17 15:54:19 +00:00
JonasFranz 6b3cca0fd9 Add sentry (#43)
Refactor DSN to constants

Use correct import

Add sentry

Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: #43
Reviewed-by: konrad <konrad@kola-entertainments.de>
2020-06-17 15:37:42 +00:00
kolaente 45d324884d Don't try to run tests in pipeline 2020-06-16 00:18:09 +02:00
kolaente 857c02375f Format 2020-06-15 23:48:15 +02:00
kolaente a0f5540b7b Fix all json fields being snake_case 2020-06-15 23:46:10 +02:00
kolaente 67945366eb Rename namespace name and task text to title 2020-06-15 23:42:12 +02:00
kolaente df003106c8 Format 2020-04-27 17:09:33 +02:00
kolaente f973d3940f Fix date format 2020-04-27 17:02:55 +02:00
kolaente 236cf4e186 Change release bucket 2020-03-01 22:56:05 +01:00
kolaente 6974befb6e Use the same image for building and testing in ci 2020-01-15 23:38:02 +01:00
konrad edbadd9913 Make it build again (#38)
Fix parsing of user model if email is not present

Use user avatar hash instead of calculating it from the email

Format

Replace GravatarImageProvider

Set min sdk version to 19

Change target api version to 28

Limit drone pipeline execution to master or pr

Remove drone debug

Use username instead of id

Format

"Fix" clone permissions

Drone debug

Fix drone permissions with different flutter build docker image

Switch CI build image

Bump Gradle sdk version

Fix formatting

Update packages for support for androidX

Update gitignore

AndroidX

Make GravatarImageProvider work again

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: #38
2020-01-12 12:59:28 +00:00
konrad d1eb3fbc37 Updated dockerimage to use the vikunja one for building apps 2019-04-25 19:35:52 +02:00
konrad 666f90345e Show a message if a list or namespace is empty (#29) 2019-03-18 17:05:32 +00:00
konrad 614b5565e1 Namespace edit (#36) 2019-03-18 17:00:34 +00:00
konrad 5fff812501 Logout (#35)
:C
2019-03-18 16:56:15 +00:00
konrad 496d97b2be Fixed namespaces loading every time a widget was loaded (#34) 2019-03-18 15:30:54 +00:00
konrad 7bd35fda22 Refactor (#31) 2019-03-16 13:29:00 +00:00
konrad 56de1c2a56 Fix the build and update drone (#33) 2019-03-15 22:14:37 +00:00
konrad 863d2995c2 Snackbars for all actions (#30) 2019-03-15 06:52:50 +00:00
konrad f38179322b Reload items after adding it (#28) 2019-03-14 21:27:13 +00:00
konrad 2c5e78b520 List edit design improvements (#27) 2019-03-14 21:25:57 +00:00
konrad 3c0a1812ce Login page design improvements (#26) 2019-03-14 21:12:02 +00:00
konrad cf90a090e1 Theme update (#23) 2019-03-11 20:38:05 +00:00
konrad ee89f8b6cd Added better error messages to login and register (#25) 2019-03-11 20:37:25 +00:00
konrad 0b870f8f62 List edit (#21) 2019-03-11 20:29:15 +00:00
konrad ae6d9f0222 Cleanup pubspec (#24) 2019-03-10 21:41:55 +00:00
konrad d757b2df69 Fixed app not working with the newest api change which has multiple reminders (#19) 2018-12-03 21:26:00 +00:00
konrad 3e7b6d4fd0 Fix build (#18) 2018-11-05 15:43:30 +00:00
konrad 0b2b9a1d22 Added formatting check to makefile and ci (#17) 2018-10-15 17:16:47 +00:00
konrad cc38133aca Added register (#13) 2018-10-08 14:26:01 +00:00
konrad 302dba2df6 Fix Typo (#14) 2018-10-02 15:53:57 +00:00
konrad 51265630dc Improved regex (#12) 2018-09-27 16:04:37 +00:00
JonasFranz 08fa9167a2 Add improved loading indicators (#9) 2018-09-27 15:55:56 +00:00
konrad b87a9a4633 Fixed url regex (#11) 2018-09-25 12:06:41 +00:00
konrad e68944116c Fixed version (#8) 2018-09-23 16:39:20 +00:00
konrad 02bcadfa24 Removed .idea folder (#7) 2018-09-23 16:23:53 +00:00
konrad 6bb3942302 Fixed build (#4) 2018-09-23 16:04:00 +00:00
konrad 7705151f17 added fdroid flavour (#3) 2018-09-23 15:38:02 +00:00
konrad 7842855d77 Added drone (#2) 2018-09-23 10:41:28 +00:00
konrad bbdd63782f Cleanup (#1) 2018-09-22 20:56:16 +00:00
Jonas Franz a62abc0b35 Add API implementations of List, Namespace, Task, User
Use Services in order to retrieve data
2018-09-17 18:16:50 +02:00
Jonas Franz bedacfdcad Add working login implementation 2018-09-17 15:35:57 +02:00
Jonas Franz e59001784d Fix a bug causing to not-start at new devices 2018-09-16 22:19:43 +02:00
Jonas Franz 5af3e4cac7 Add mocked services 2018-09-16 22:13:50 +02:00
Jonas Franz 03fda04afa Reformatted code 2018-09-16 21:47:53 +02:00
Jonas Franz 80d55a3518 Add login view
Add services and models
Add mocks
2018-09-16 21:47:33 +02:00
Jonas Franz 0155bcdf15 Add list support 2018-09-15 19:40:59 +02:00
Jonas Franz 733b0e6ae2 Fix alert for cupertino OS 2018-09-15 18:33:41 +02:00
Jonas Franz e865fb904b Add ListPage 2018-09-15 18:21:48 +02:00
Jonas Franz fb3c6e78ba Reformat code
Add "Add namespace" functionality
2018-09-15 17:22:28 +02:00
Jonas Franz 744dd0db94 Add namespace 2018-09-15 17:10:34 +02:00
Jonas Franz e15f11325a Refactor drawer 2018-09-15 17:01:45 +02:00
Jonas Franz e1d309c97a Fix gradle wrapper 2018-09-15 16:47:37 +02:00
Jonas Franz f25b3ebc72 Add gradle wrapper 2018-09-15 16:12:38 +02:00
Jonas Franz a26489bb3d Add vikunja design
Add drawer menu
2018-09-15 14:18:24 +02:00
Jonas Franz 7b9df7fec0 Add initial flutter project 2018-09-14 18:59:13 +02:00
59 changed files with 2125 additions and 419 deletions

42
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Flutter Build
on:
push:
branches:
- main
pull_request:
jobs:
build-app:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: '12.x'
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
channel: stable
- name: Cache pub dependencies
uses: actions/cache@v2
with:
path: ${{ env.FLUTTER_HOME }}/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: ${{ runner.os }}-pub-
- name: Download pub dependencies
run: flutter pub get
- name: Build Android App Bundle
run: flutter build appbundle
- name: Build Android APK
run: flutter build apk

70
.github/workflows/flutter-release.yml vendored Normal file
View File

@ -0,0 +1,70 @@
# Based on https://medium.com/flutter-community/automating-publishing-your-flutter-apps-to-google-play-using-github-actions-2f67ac582032
name: Flutter release
on:
push:
branches:
- main
release:
types: [published]
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: '12.x'
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
channel: stable
- name: Flutter version
run: flutter --version
- name: Cache pub dependencies
uses: actions/cache@v2
with:
path: ${{ env.FLUTTER_HOME }}/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: ${{ runner.os }}-pub-
- name: Download pub dependencies
run: flutter pub get
- name: Download Android keystore
id: android_keystore
uses: timheuer/base64-to-file@v1.0.3
with:
fileName: key.jks
encodedString: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
- name: Create key.properties
run: |
echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties
echo "storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> android/key.properties
- name: Build Android App Bundle
run: flutter build appbundle
- name: Build Android APK
run: flutter build apk
- name: Upload build artifacts
uses: actions/upload-artifact@v2
with:
name: app-release-bundle
path: |
build/app/outputs/bundle/release/app-release.aab
build/app/outputs/flutter-apk/app-release.apk

68
.gitignore vendored
View File

@ -1,6 +1,5 @@
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
@ -16,62 +15,33 @@
*.iws
.idea/
# Visual Studio Code related
.vscode/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
build/
!pubspec.lock
.flutter-plugins-dependencies
/build/
ios/Flutter/flutter_export_environment.sh
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# Web related
lib/generated_plugin_registrant.dart
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Symbolication related
app.*.symbols
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
ios/fastlane/README.md
ios/fastlane/report.xml
ios/Runner.ipa
ios/Runner.app.dSYM.zip
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@ -4,5 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: 3b309bda072a6b326e8aa4591a5836af600923ce
channel: beta
revision: c5a4b4029c0798f37c4a39b479d7cb75daa7b05c
channel: stable
project_type: app

View File

@ -1,8 +1,8 @@
# Vikunja Cross-Platform app
# Vikunja Cross-Plattform app
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/app/status.svg)](https://drone.kolaente.de/vikunja/app)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.1-brightgreen.svg)](https://storage.kolaente.de/minio/vikunja-app/)
[![TestFlight Beta](https://img.shields.io/badge/TestFlight-Beta-026CBB)](https://testflight.apple.com/join/KxOaAraq)
Vikunja as Flutter cross-platform app.
Vikunja as Flutter cross platform app.

17
android/.gitignore vendored
View File

@ -1,10 +1,11 @@
*.iml
*.class
.gradle
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties

View File

@ -25,8 +25,14 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
compileSdkVersion 28
compileSdkVersion 30
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -39,28 +45,32 @@ android {
defaultConfig {
applicationId "io.vikunja.app"
minSdkVersion 19
targetSdkVersion 28
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
flavorDimensions "deploy"
productFlavors {
fdroid {
dimension "deploy"
signingConfig null
}
unsigned {
dimension "deploy"
signingConfig null
}
main {
dimension "deploy"
signingConfig signingConfigs.debug // TODO add signing key
}
}
//productFlavors {
// fdroid {
// dimension "deploy"
// signingConfig null
// }
// unsigned {
// dimension "deploy"
// signingConfig null
// }
//}
android.applicationVariants.all { variant ->
if (variant.flavorName == "fdroid") {
@ -74,6 +84,9 @@ android {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
debug {
signingConfig signingConfigs.debug
}
}
@ -84,8 +97,5 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -19,16 +19,35 @@
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -2,4 +2,5 @@ package io.vikunja.flutteringvikunja
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -1,8 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '1.2.30'
ext.kotlin_version = '1.4.31'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@ -1,15 +1,11 @@
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

View File

@ -1,18 +0,0 @@
#
# NOTE: This podspec is NOT to be published. It is only used as a local source!
#
Pod::Spec.new do |s|
s.name = 'Flutter'
s.version = '1.0.0'
s.summary = 'High-performance, high-fidelity mobile apps.'
s.description = <<-DESC
Flutter provides an easy and productive way to build and deploy high-performance mobile apps for Android and iOS.
DESC
s.homepage = 'https://flutter.io'
s.license = { :type => 'MIT' }
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
s.ios.deployment_target = '8.0'
s.vendored_frameworks = 'Flutter.framework'
end

View File

@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/components/string_extension.dart';
class Client {
final JsonDecoder _decoder = new JsonDecoder();
@ -25,33 +27,46 @@ class Client {
'Content-Type': 'application/json'
};
Future<dynamic> get(String url) {
return http
.get('${this.base}$url', headers: _headers)
Future<Response> get(String url,
[Map<String, List<String>> queryParameters]) {
// TODO: This could be moved to a seperate function
var uri = Uri.parse('${this.base}$url');
// Because these are all final values, we can't just add the queryParameters and must instead build a new Uri Object every time this method is called.
var newUri = Uri(
scheme: uri.scheme,
userInfo: uri.userInfo,
host: uri.host,
port: uri.port,
path: uri.path,
query: uri.query,
queryParameters: queryParameters,
// Because dart takes a Map<String, String> here, it is only possible to sort by one parameter while the api supports n parameters.
fragment: uri.fragment);
return http.get(newUri, headers: _headers)
.then(_handleResponse);
}
Future<dynamic> delete(String url) {
return http
.delete('${this.base}$url', headers: _headers)
.then(_handleResponse);
Future<Response> delete(String url) {
return http.delete('${this.base}$url'.toUri(),
headers: _headers,
).then(_handleResponse);
}
Future<dynamic> post(String url, {dynamic body}) {
return http
.post('${this.base}$url',
headers: _headers, body: _encoder.convert(body))
.then(_handleResponse);
Future<Response> post(String url, {dynamic body}) {
return http.post('${this.base}$url'.toUri(),
headers: _headers,
body: _encoder.convert(body),
).then(_handleResponse);
}
Future<dynamic> put(String url, {dynamic body}) {
return http
.put('${this.base}$url',
headers: _headers, body: _encoder.convert(body))
.then(_handleResponse);
Future<Response> put(String url, {dynamic body}) {
return http.put('${this.base}$url'.toUri(),
headers: _headers,
body: _encoder.convert(body),
).then(_handleResponse);
}
dynamic _handleResponse(http.Response response) {
Response _handleResponse(http.Response response) {
if (response.statusCode < 200 ||
response.statusCode >= 400 ||
json == null) {
@ -65,12 +80,17 @@ class Client {
throw new ApiException(
response.statusCode, response.request.url.toString());
}
return _decoder.convert(response.body);
return Response(
_decoder.convert(response.body),
response.statusCode,
response.headers
);
}
}
class InvalidRequestApiException extends ApiException {
final String message;
InvalidRequestApiException(int errorCode, String path, this.message)
: super(errorCode, path);
@ -83,6 +103,7 @@ class InvalidRequestApiException extends ApiException {
class ApiException implements Exception {
final int errorCode;
final String path;
ApiException(this.errorCode, this.path);
@override

30
lib/api/label_task.dart Normal file
View File

@ -0,0 +1,30 @@
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/labelTask.dart';
import 'package:vikunja_app/service/services.dart';
class LabelTaskAPIService extends APIService implements LabelTaskService {
LabelTaskAPIService(Client client) : super(client);
@override
Future<Label> create(LabelTask lt) async {
return client.put('/tasks/${lt.task.id}/labels', body: lt.toJSON())
.then((result) => Label.fromJson(result.body));
}
@override
Future<Label> delete(LabelTask lt) async {
return client.delete('/tasks/${lt.task.id}/labels/${lt.label.id}')
.then((result) => Label.fromJson(result.body));
}
@override
Future<List<Label>> getAll(LabelTask lt, {String query}) async {
String params =
query == '' ? null : '?s=' + Uri.encodeQueryComponent(query);
return client.get('/tasks/${lt.task.id}/labels$params').then(
(label) => convertList(label, (result) => Label.fromJson(result)));
}
}

View File

@ -0,0 +1,20 @@
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/labelTaskBulk.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/service/services.dart';
class LabelTaskBulkAPIService extends APIService
implements LabelTaskBulkService {
LabelTaskBulkAPIService(Client client) : super(client);
@override
Future<List<Label>> update(Task task, List<Label> labels) {
return client
.post('/tasks/${task.id}/labels/bulk',
body: LabelTaskBulk(labels: labels).toJSON())
.then((response) =>
convertList(response.body['labels'], (result) => Label.fromJson(result)));
}
}

41
lib/api/labels.dart Normal file
View File

@ -0,0 +1,41 @@
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/service/services.dart';
class LabelAPIService extends APIService implements LabelService {
LabelAPIService(Client client) : super(client);
@override
Future<Label> create(Label label) {
return client.put('/labels', body: label.toJSON())
.then((response) => Label.fromJson(response.body));
}
@override
Future<Label> delete(Label label) {
return client.delete('/labels/${label.id}')
.then((response) => Label.fromJson(response.body));
}
@override
Future<Label> get(int labelID) {
return client.get('/labels/$labelID')
.then((response) => Label.fromJson(response.body));
}
@override
Future<List<Label>> getAll({String query}) {
String params =
query == '' ? null : '?s=' + Uri.encodeQueryComponent(query);
return client.get('/labels$params').then(
(label) => convertList(label, (result) => Label.fromJson(result)));
}
@override
Future<Label> update(Label label) {
return client
.post('/labels/${label.id}', body: label)
.then((response) => Label.fromJson(response.body));
}
}

View File

@ -12,7 +12,7 @@ class ListAPIService extends APIService implements ListService {
Future<TaskList> create(namespaceId, TaskList tl) {
return client
.put('/namespaces/$namespaceId/lists', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
.then((response) => TaskList.fromJson(response.body));
}
@override
@ -22,25 +22,38 @@ class ListAPIService extends APIService implements ListService {
@override
Future<TaskList> get(int listId) {
return client.get('/lists/$listId').then((map) => TaskList.fromJson(map));
/*
return client
.get('/lists/$listId')
.then((response) => TaskList.fromJson(response.body));
*/
return client.get('/lists/$listId').then((response) {
final map = response.body;
if (map.containsKey('id')) {
return client.get("/lists/$listId/tasks")
.then((tasks) => TaskList.fromJson(
map, tasksJson: tasks.body));
}
return TaskList.fromJson(map);
});
}
@override
Future<List<TaskList>> getAll() {
return client.get('/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
return client.get('/lists').then((response) =>
convertList(response.body, (result) => TaskList.fromJson(result)));
}
@override
Future<List<TaskList>> getByNamespace(int namespaceId) {
return client.get('/namespaces/$namespaceId/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
return client.get('/namespaces/$namespaceId/lists').then((response) =>
convertList(response.body, (result) => TaskList.fromJson(result)));
}
@override
Future<TaskList> update(TaskList tl) {
return client
.post('/lists/${tl.id}', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
.then((response) => TaskList.fromJson(response.body));
}
}

View File

@ -12,7 +12,7 @@ class NamespaceAPIService extends APIService implements NamespaceService {
Future<Namespace> create(Namespace ns) {
return client
.put('/namespaces', body: ns.toJSON())
.then((map) => Namespace.fromJson(map));
.then((response) => Namespace.fromJson(response.body));
}
@override
@ -24,19 +24,19 @@ class NamespaceAPIService extends APIService implements NamespaceService {
Future<Namespace> get(int namespaceId) {
return client
.get('/namespaces/$namespaceId')
.then((map) => Namespace.fromJson(map));
.then((response) => Namespace.fromJson(response.body));
}
@override
Future<List<Namespace>> getAll() {
return client.get('/namespaces').then(
(list) => convertList(list, (result) => Namespace.fromJson(result)));
return client.get('/namespaces').then((response) =>
convertList(response.body, (result) => Namespace.fromJson(result)));
}
@override
Future<Namespace> update(Namespace ns) {
return client
.post('/namespaces/${ns.id}', body: ns.toJSON())
.then((map) => Namespace.fromJson(map));
.then((response) => Namespace.fromJson(response.body));
}
}

9
lib/api/response.dart Normal file
View File

@ -0,0 +1,9 @@
// This is a wrapper class to be able to return the headers up to the provider
// to properly handle things like pagination with it.
class Response {
Response(this.body, this.statusCode, this.headers);
final dynamic body;
final int statusCode;
final Map<String, String> headers;
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/service/services.dart';
@ -12,7 +13,7 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> add(int listId, Task task) {
return client
.put('/lists/$listId', body: task.toJSON())
.then((map) => Task.fromJson(map));
.then((response) => Task.fromJson(response.body));
}
@override
@ -24,6 +25,19 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> update(Task task) {
return client
.post('/tasks/${task.id}', body: task.toJSON())
.then((map) => Task.fromJson(map));
.then((response) => Task.fromJson(response.body));
}
@override
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]) {
return client.get('/lists/$listId/tasks', queryParameters).then(
(response) => new Response(
convertList(response.body, (result) => Task.fromJson(result)),
response.statusCode,
response.headers));
}
@override
int get maxPages => throw UnimplementedError();
}

View File

@ -13,7 +13,7 @@ class UserAPIService extends APIService implements UserService {
var token = await client.post('/login', body: {
'username': username,
'password': password
}).then((map) => map['token']);
}).then((response) => response.body['token']);
return UserAPIService(Client(token, client.base))
.getCurrentUser()
.then((user) => UserTokenPair(user, token));
@ -25,12 +25,12 @@ class UserAPIService extends APIService implements UserService {
'username': username,
'email': email,
'password': password
}).then((resp) => resp['username']);
}).then((response) => response.body['username']);
return login(newUser, password);
}
@override
Future<User> getCurrentUser() {
return client.get('/user').then((map) => User.fromJson(map));
return client.get('/user').then((response) => User.fromJson(response.body));
}
}

View File

@ -7,10 +7,15 @@ import 'package:vikunja_app/models/task.dart';
class TaskTile extends StatefulWidget {
final Task task;
final VoidCallback onEdit;
final ValueSetter<bool> onMarkedAsDone;
final bool loading;
const TaskTile(
{Key key, @required this.task, this.onEdit, this.loading = false})
{Key key,
@required this.task,
this.onEdit,
this.loading = false,
this.onMarkedAsDone})
: assert(task != null),
super(key: key);
@ -41,29 +46,28 @@ class TaskTileState extends State<TaskTile> {
strokeWidth: 2.0,
)),
),
title: Text(_currentTask.title),
subtitle:
_currentTask.description == null || _currentTask.description.isEmpty
? null
: Text(_currentTask.description),
title: Text(widget.task.title),
subtitle: widget.task.description == null || widget.task.description.isEmpty
? null
: Text(widget.task.description),
trailing: IconButton(
icon: Icon(Icons.settings),
onPressed: () {
null; // TODO: implement edit task
}),
icon: Icon(Icons.settings),
onPressed: () => widget.onEdit,
),
);
}
return CheckboxListTile(
title: Text(_currentTask.title),
title: Text(widget.task.title),
controlAffinity: ListTileControlAffinity.leading,
value: _currentTask.done ?? false,
subtitle:
_currentTask.description == null || _currentTask.description.isEmpty
? null
: Text(_currentTask.description),
secondary:
IconButton(icon: Icon(Icons.settings), onPressed: widget.onEdit),
onChanged: _change,
value: widget.task.done ?? false,
subtitle: widget.task.description == null || widget.task.description.isEmpty
? null
: Text(widget.task.description),
secondary: IconButton(
icon: Icon(Icons.settings),
onPressed: widget.onEdit,
),
onChanged: widget.onMarkedAsDone,
);
}
@ -71,9 +75,9 @@ class TaskTileState extends State<TaskTile> {
setState(() {
this._loading = true;
});
Task newTask = await _updateTask(_currentTask, value);
Task newTask = await _updateTask(widget.task, value);
setState(() {
this._currentTask = newTask;
//this.widget.task = newTask;
this._loading = false;
});
}
@ -85,7 +89,7 @@ class TaskTileState extends State<TaskTile> {
done: checked,
title: task.title,
description: task.description,
owner: null,
createdBy: null,
));
}
}

View File

@ -0,0 +1,51 @@
import 'package:datetime_picker_formfield/datetime_picker_formfield.dart';
import 'package:flutter/material.dart';
import 'package:vikunja_app/theme/constants.dart';
class VikunjaDateTimePicker extends StatelessWidget {
final String label;
final Function onSaved;
final Function onChanged;
final DateTime initialValue;
final EdgeInsetsGeometry padding;
final Icon icon;
final InputBorder border;
const VikunjaDateTimePicker({
Key key,
@required this.label,
this.onSaved,
this.onChanged,
this.initialValue,
this.padding = const EdgeInsets.symmetric(vertical: 10.0),
this.icon = const Icon(Icons.date_range),
this.border = InputBorder.none,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return DateTimeField(
//dateOnly: false,
//editable: false, // Otherwise editing the date is not possible, this setting affects the underlying text field.
initialValue: initialValue == DateTime.fromMillisecondsSinceEpoch(0)
? null
: initialValue,
format: vDateFormatLong,
decoration: InputDecoration(
labelText: label,
border: border,
icon: icon,
),
onSaved: onSaved,
onChanged: onChanged,
onShowPicker: (context, currentValue) {
return showDatePicker(
context: context,
firstDate: DateTime(1900),
initialDate: currentValue.millisecondsSinceEpoch > 0 ? currentValue : DateTime.now(),
lastDate: DateTime(2100));
},
);
}
}

52
lib/components/label.dart Normal file
View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/theme/constants.dart';
class LabelComponent extends StatefulWidget {
final Label label;
final VoidCallback onDelete;
const LabelComponent({Key key, @required this.label, this.onDelete})
: super(key: key);
@override
State<StatefulWidget> createState() {
return new LabelComponentState();
}
}
class LabelComponentState extends State<LabelComponent> {
@override
Widget build(BuildContext context) {
Color backgroundColor = widget.label.color ?? vLabelDefaultColor;
Color textColor =
backgroundColor.computeLuminance() > 0.5 ? vLabelDark : vLabelLight;
return Chip(
label: Text(
widget.label.title,
style: TextStyle(
color: textColor,
),
),
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(3)),
),
onDeleted: widget.onDelete,
deleteIconColor: textColor,
deleteIcon: Container(
padding: EdgeInsets.all(3),
decoration: BoxDecoration(
color: Color.fromARGB(50, 0, 0, 0),
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: textColor,
size: 15,
),
),
);
}
}

View File

@ -0,0 +1,4 @@
extension StringExtensions on String {
Uri toUri() => Uri.tryParse(this);
}

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/label_task.dart';
import 'package:vikunja_app/api/label_task_bulk.dart';
import 'package:vikunja_app/api/labels.dart';
import 'package:vikunja_app/api/list_implementation.dart';
import 'package:vikunja_app/api/namespace_implementation.dart';
import 'package:vikunja_app/api/task_implementation.dart';
@ -19,8 +22,7 @@ class VikunjaGlobal extends StatefulWidget {
VikunjaGlobalState createState() => VikunjaGlobalState();
static VikunjaGlobalState of(BuildContext context) {
var widget = context.inheritFromWidgetOfExactType(_VikunjaGlobalInherited)
as _VikunjaGlobalInherited;
var widget = context.dependOnInheritedWidgetOfExactType<_VikunjaGlobalInherited>();
return widget.data;
}
}
@ -46,6 +48,13 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
ListService get listService => new ListAPIService(client);
LabelService get labelService => new LabelAPIService(client);
LabelTaskService get labelTaskService => new LabelTaskAPIService(client);
LabelTaskBulkAPIService get labelTaskBulkService =>
new LabelTaskBulkAPIService(client);
@override
void initState() {
super.initState();
@ -85,7 +94,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
_currentUser = null;
});
}).catchError((err) {
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('An error occured while logging out!'),
));
});

View File

@ -3,22 +3,32 @@ import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/pages/home.dart';
import 'package:vikunja_app/pages/user/login.dart';
import 'package:vikunja_app/theme/theme.dart';
//import 'package:alice/alice.dart';
void main() => runApp(VikunjaGlobal(
child: new VikunjaApp(home: HomePage()),
login: new VikunjaApp(home: LoginPage())));
class VikunjaApp extends StatelessWidget {
class VikunjaApp extends StatefulWidget {
final Widget home;
const VikunjaApp({Key key, this.home}) : super(key: key);
VikunjaApp({Key key, this.home}) : super(key: key);
@override
_VikunjaAppState createState() => _VikunjaAppState();
}
class _VikunjaAppState extends State<VikunjaApp> {
//Alice alice = Alice(showNotification: true);
@override
Widget build(BuildContext context) {
return new MaterialApp(
//navigatorKey: alice.getNavigatorKey(),
title: 'Vikunja',
theme: buildVikunjaTheme(),
darkTheme: buildVikunjaDarkTheme(),
home: this.home,
home: this.widget.home,
);
}
}

43
lib/models/label.dart Normal file
View File

@ -0,0 +1,43 @@
import 'dart:ui';
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/user.dart';
class Label {
final int id;
final String title, description;
final DateTime created, updated;
final User createdBy;
final Color color;
Label(
{this.id,
this.title,
this.description,
this.color,
this.created,
this.updated,
this.createdBy});
Label.fromJson(Map<String, dynamic> json)
: id = json['id'],
title = json['title'],
description = json['description'],
color = json['hex_color'] == ''
? null
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
createdBy = User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'title': title,
'description': description,
'hex_color':
color?.value?.toRadixString(16)?.padLeft(8, '0')?.substring(2),
'created_by': createdBy?.toJSON(),
'updated': updated?.millisecondsSinceEpoch,
'created': created?.millisecondsSinceEpoch,
};
}

18
lib/models/labelTask.dart Normal file
View File

@ -0,0 +1,18 @@
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/task.dart';
class LabelTask {
final Label label;
final Task task;
LabelTask({@required this.label, @required this.task});
LabelTask.fromJson(Map<String, dynamic> json)
: label = new Label(id: json['label_id']),
task = null;
toJSON() => {
'label_id': label.id,
};
}

View File

@ -0,0 +1,15 @@
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/label.dart';
class LabelTaskBulk {
final List<Label> labels;
LabelTaskBulk({@required this.labels});
LabelTaskBulk.fromJson(Map<String, dynamic> json)
: labels = json['labels']?.map((label) => Label.fromJson(label));
toJSON() => {
'labels': labels.map((label) => label.toJSON()).toList(),
};
}

View File

@ -9,23 +9,24 @@ class TaskList {
final DateTime created, updated;
final List<Task> tasks;
TaskList(
{@required this.id,
@required this.title,
this.description,
this.owner,
this.created,
this.updated,
this.tasks});
TaskList({
@required this.id,
@required this.title,
this.description,
this.owner,
this.created,
this.updated,
this.tasks,
});
TaskList.fromJson(Map<String, dynamic> json)
TaskList.fromJson(Map<String, dynamic> json, {tasksJson})
: id = json['id'],
owner = User.fromJson(json['owner']),
owner = json['owner'] == null ? null : User.fromJson(json['owner']),
description = json['description'],
title = json['title'],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
tasks = (tasksJson == null ? [] : tasksJson as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();

View File

@ -21,7 +21,7 @@ class Namespace {
id = json['id'],
created = DateTime.parse(json['created']),
updated = DateTime.parse(json['updated']),
owner = User.fromJson(json['owner']);
owner = json['owner'] == null ? null : User.fromJson(json['owner']);
toJSON() => {
"created": created?.toIso8601String(),

View File

@ -1,49 +1,84 @@
import 'package:vikunja_app/models/user.dart';
import 'package:meta/meta.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/user.dart';
import 'package:vikunja_app/utils/datetime_to_unix.dart';
@JsonSerializable()
class Task {
final int id;
final DateTime created, updated, due;
final List<DateTime> reminders;
final int id, parentTaskId, priority;
final DateTime created, updated, dueDate, startDate, endDate;
final List<DateTime> reminderDates;
final String title, description;
final bool done;
final User owner;
final User createdBy;
final Duration repeatAfter;
final List<Task> subtasks;
final List<Label> labels;
Task(
{@required this.id,
this.title,
this.description,
this.done,
this.reminderDates,
this.dueDate,
this.startDate,
this.endDate,
this.parentTaskId,
this.priority,
this.repeatAfter,
this.subtasks,
this.labels,
this.created,
this.updated,
this.reminders,
this.due,
@required this.title,
this.description,
@required this.done,
@required this.owner});
this.createdBy});
Task.fromJson(Map<String, dynamic> json)
: id = json['id'],
title = json['title'],
description = json['description'],
done = json['done'],
reminderDates = (json['reminder_dates'] as List<dynamic>)
?.map((ts) => DateTime.parse(ts))
?.cast<DateTime>()
?.toList(),
dueDate = DateTime.parse(json['due_date']),
startDate = DateTime.parse(json['start_date']),
endDate = DateTime.parse(json['end_date']),
parentTaskId = json['parent_task_id'],
priority = json['priority'],
repeatAfter = Duration(seconds: json['repeat_after']),
labels = (json['labels'] as List<dynamic>)
?.map((label) => Label.fromJson(label))
?.cast<Label>()
?.toList(),
subtasks = (json['subtasks'] as List<dynamic>)
?.map((subtask) => Task.fromJson(subtask))
?.cast<Task>()
?.toList(),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
reminders = (json['reminder_dates'] as List<dynamic>)
?.map((r) => DateTime.parse(r))
?.toList(),
due =
json['due_date'] != null ? DateTime.parse(json['due_date']) : null,
description = json['description'],
title = json['title'],
done = json['done'],
owner = User.fromJson(json['created_by']);
createdBy = json['created_by'] == null ? null : User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'title': title,
'description': description,
'done': done ?? false,
'reminder_dates': reminderDates
?.map((date) => date?.toIso8601String())
?.toList(),
'due_date': dueDate?.toIso8601String(),
'start_date': startDate?.toIso8601String(),
'end_date': endDate?.toIso8601String(),
'priority': priority,
'repeat_after': repeatAfter?.inSeconds,
'labels': labels?.map((label) => label.toJSON())?.toList(),
'subtasks': subtasks?.map((subtask) => subtask.toJSON())?.toList(),
'created_by': createdBy?.toJSON(),
'updated': updated?.toIso8601String(),
'created': created?.toIso8601String(),
'reminder_dates':
reminders?.map((date) => date.toIso8601String())?.toList(),
'due_date': due?.toIso8601String(),
'description': description,
'title': title,
'done': done ?? false,
'created_by': owner?.toJSON()
};
}

View File

@ -15,9 +15,7 @@ class User {
String avatarUrl(BuildContext context) {
return VikunjaGlobal.of(context).client.base +
"/" +
this.username +
"/avatar";
"/avatar/${this.username}";
}
}

View File

@ -1,19 +1,18 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/ErrorDialog.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/pages/namespace/namespace.dart';
import 'package:vikunja_app/pages/namespace/namespace_edit.dart';
import 'package:vikunja_app/pages/placeholder.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => new HomePageState();
State<StatefulWidget> createState() => HomePageState();
}
class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
@ -36,12 +35,13 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
List<Widget> namespacesList = <Widget>[];
_namespaces
.asMap()
.forEach((i, namespace) => namespacesList.add(new ListTile(
.forEach((i, namespace) => namespacesList.add(ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace.title),
title: Text(namespace.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
))
);
return this._loading
? Center(child: CircularProgressIndicator())
@ -49,8 +49,9 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
child: ListView(
padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context, tiles: namespacesList)
.toList()),
context: context,
tiles: namespacesList).toList(),
),
onRefresh: _loadNamespaces,
);
}
@ -71,9 +72,9 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
return new Scaffold(
return Scaffold(
appBar: AppBar(
title: new Text(_currentNamespace?.title ?? 'Vikunja'),
title: Text(_currentNamespace?.title ?? 'Vikunja'),
actions: _currentNamespace == null
? null
: <Widget>[
@ -84,59 +85,63 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
MaterialPageRoute(
builder: (context) => NamespaceEditPage(
namespace: _currentNamespace,
))))
)
)
)
),
],
),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail:
currentUser?.email == null ? null : Text(currentUser.email),
accountName:
currentUser?.username == null ? null : Text(currentUser.username),
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
});
},
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: NetworkImage(currentUser.avatarUrl(context)),
),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(
Theme.of(context).primaryColor, BlendMode.multiply)),
),
),
new Builder(
builder: (BuildContext context) => Expanded(
child: _showUserDetails
? _userDetailsWidget(context)
: _namespacesWidget())),
new Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
builder: (context) => ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespaceDialog(context),
drawer: Drawer(
child: Column(children: <Widget>[
UserAccountsDrawerHeader(
// Removed until we find a way to disable the user email only for some occasions and not everywhere
accountEmail: currentUser?.email == null ? null : Text(currentUser.email),
accountName: currentUser?.username == null ? null : Text(currentUser.username),
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
});
},
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: NetworkImage(currentUser.avatarUrl(context)),
),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.multiply),
),
),
),
),
])),
Builder(
builder: (BuildContext context) =>
Expanded(
child: _showUserDetails ? _userDetailsWidget(context) : _namespacesWidget(),
)
),
Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
builder: (context) => ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespaceDialog(context),
),
),
),
]),
),
body: _getDrawerItemWidget(_selectedDrawerIndex),
);
}
_getDrawerItemWidget(int pos) {
if (pos == -1) {
return new PlaceholderPage();
return PlaceholderPage();
}
return new NamespacePage(namespace: _namespaces[pos]);
return NamespacePage(namespace: _namespaces[pos]);
}
_onSelectItem(int index) {
@ -149,8 +154,10 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addNamespace(name, context),
decoration: new InputDecoration(
labelText: 'Namespace', hintText: 'eg. Personal Namespace'),
decoration: InputDecoration(
labelText: 'Namespace',
hintText: 'eg. Personal Namespace',
),
));
}
@ -160,11 +167,13 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
.create(Namespace(id: null, title: name))
.then((_) {
_loadNamespaces();
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The namespace was created successfully!'),
));
}).catchError((error) => showDialog(
context: context, builder: (context) => ErrorDialog(error: error)));
context: context,
builder: (context) => ErrorDialog(error: error),
));
}
Future<void> _loadNamespaces() {

View File

@ -1,12 +1,16 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/TaskTile.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/pages/list/list_edit.dart';
import 'package:vikunja_app/pages/list/task_edit.dart';
import 'package:vikunja_app/stores/list_store.dart';
class ListPage extends StatefulWidget {
final TaskList taskList;
@ -20,109 +24,163 @@ class ListPage extends StatefulWidget {
class _ListPageState extends State<ListPage> {
TaskList _list;
List<Task> _loadingTasks = [];
bool _loading = true;
int _currentPage = 1;
@override
void initState() {
_list = TaskList(
id: widget.taskList.id, title: widget.taskList.title, tasks: []);
id: widget.taskList.id,
title: widget.taskList.title,
tasks: [],
);
Future.microtask(() => _loadList());
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadList();
}
@override
Widget build(BuildContext context) {
final taskState = Provider.of<ListProvider>(context);
return Scaffold(
appBar: AppBar(
title: new Text(_list.title),
title: Text(_list.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
))))
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
),
)
),
),
],
),
body: !this._loading
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
? RefreshIndicator(
child: _list.tasks.length > 0
? ListView(
child: taskState.tasks.length > 0
? ListenableProvider.value(
value: taskState,
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context, tiles: _listTasks())
.toList(),
)
: Center(child: Text('This list is empty.')),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
if (_loadingTasks.isNotEmpty) {
final loadingTask = _loadingTasks.removeLast();
return _buildLoadingTile(loadingTask);
}
final index = i ~/ 2;
// This handles the case if there are no more elements in the list left which can be provided by the api
if (taskState.maxPages == _currentPage &&
index == taskState.tasks.length - 1) return null;
if (index >= taskState.tasks.length &&
_currentPage < taskState.maxPages) {
_currentPage++;
_loadTasksForPage(_currentPage);
}
return index < taskState.tasks.length
? _buildTile(taskState.tasks[index])
: null;
}
),
)
: Center(child: Text('This list is empty.')),
onRefresh: _loadList,
)
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _addItemDialog(context), child: Icon(Icons.add)),
));
}
List<Widget> _listTasks() {
var tasks = (_list?.tasks?.map(_buildTile) ?? []).toList();
tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
),
);
}
TaskTile _buildTile(Task task) {
return TaskTile(task: task, loading: false);
return TaskTile(
task: task,
loading: false,
onEdit: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TaskEditPage(
task: task,
),
),
),
onMarkedAsDone: (done) {
Provider.of<ListProvider>(context, listen: false).updateTask(
context: context,
id: task.id,
done: done,
);
},
);
}
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true,
onEdit: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TaskEditPage(
task: task,
),
),
),
);
}
Future<void> _loadList() {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((list) {
setState(() {
_loading = false;
_list = list;
});
});
Future<void> _loadList() async {
_loadTasksForPage(1);
}
void _loadTasksForPage(int page) {
Provider.of<ListProvider>(context, listen: false).loadTasks(
context: context,
listId: _list.id,
page: page,
);
}
_addItemDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addItem(name, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
context: context,
builder: (_) => AddDialog(
onAdd: (title) => _addItem(title, context),
decoration: InputDecoration(
labelText: 'Task Name',
hintText: 'eg. Milk',
),
),
);
}
_addItem(String name, BuildContext context) {
_addItem(String title, BuildContext context) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null, title: name, owner: globalState.currentUser, done: false);
id: null,
title: title,
createdBy: globalState.currentUser,
done: false,
);
setState(() => _loadingTasks.add(newTask));
globalState.taskService.add(_list.id, newTask).then((task) {
setState(() {
_list.tasks.add(task);
});
}).then((_) {
_loadList();
setState(() => _loadingTasks.remove(newTask));
Scaffold.of(context).showSnackBar(SnackBar(
Provider.of<ListProvider>(context, listen: false)
.addTask(
context: context,
newTask: newTask,
listId: _list.id,
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'),
));
});
}
}
}

View File

@ -101,12 +101,12 @@ class _ListEditPageState extends State<ListEditPage> {
VikunjaGlobal.of(context).listService.update(updatedList).then((_) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The list was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(

View File

@ -0,0 +1,436 @@
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:vikunja_app/components/datetimePicker.dart';
import 'package:vikunja_app/components/label.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
import 'package:vikunja_app/utils/repeat_after_parse.dart';
class TaskEditPage extends StatefulWidget {
final Task task;
TaskEditPage({this.task}) : super(key: Key(task.toString()));
@override
State<StatefulWidget> createState() => _TaskEditPageState();
}
class _TaskEditPageState extends State<TaskEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
int _priority;
DateTime _dueDate, _startDate, _endDate;
List<DateTime> _reminderDates;
String _title, _description, _repeatAfterType;
Duration _repeatAfter;
List<Label> _labels;
// we use this to find the label object after a user taps on the suggestion, because the typeahead only uses strings, not full objects.
List<Label> _suggestedLabels;
var _reminderInputs = <Widget>[];
final _labelTypeAheadController = TextEditingController();
@override
Widget build(BuildContext ctx) {
// This builds the initial list of reminder inputs only once.
if (_reminderDates == null) {
_reminderDates = widget.task.reminderDates ?? [];
_reminderDates?.asMap()?.forEach((i, time) =>
setState(() => _reminderInputs?.add(VikunjaDateTimePicker(
initialValue: time,
label: 'Reminder',
onSaved: (reminder) => _reminderDates[i] = reminder,
))));
}
if (_labels == null) {
_labels = widget.task.labels ?? [];
}
return Scaffold(
appBar: AppBar(
title: Text('Edit Task'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
child: Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(16.0), children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.task.title,
onSaved: (title) => _title = title,
validator: (title) {
if (title.length < 3 || title.length > 250) {
return 'The title needs to have between 3 and 250 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.task.description,
onSaved: (description) => _description = description,
validator: (description) {
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
),
),
VikunjaDateTimePicker(
icon: Icon(Icons.access_time),
label: 'Due Date',
initialValue: widget.task.dueDate,
onSaved: (duedate) => _dueDate = duedate,
),
VikunjaDateTimePicker(
label: 'Start Date',
initialValue: widget.task.startDate,
onSaved: (startDate) => _startDate = startDate,
),
VikunjaDateTimePicker(
label: 'End Date',
initialValue: widget.task.endDate,
onSaved: (endDate) => _endDate = endDate,
),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
keyboardType: TextInputType.number,
initialValue: getRepeatAfterValueFromDuration(
widget.task.repeatAfter)
?.toString(),
onSaved: (repeatAfter) => _repeatAfter =
getDurationFromType(repeatAfter, _repeatAfterType),
decoration: new InputDecoration(
labelText: 'Repeat after',
border: InputBorder.none,
icon: Icon(Icons.repeat),
),
),
),
Expanded(
child: DropdownButton<String>(
isExpanded: true,
isDense: true,
value: _repeatAfterType ??
getRepeatAfterTypeFromDuration(
widget.task.repeatAfter),
onChanged: (String newValue) {
setState(() {
_repeatAfterType = newValue;
});
},
items: <String>[
'Hours',
'Days',
'Weeks',
'Months',
'Years'
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
],
),
Column(
children: _reminderInputs,
),
GestureDetector(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 15, left: 2),
child: Icon(
Icons.alarm_add,
color: Colors.grey,
)),
Text(
'Add a reminder',
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
],
),
),
onTap: () {
// We add a new entry every time we add a new input, to make sure all inputs have a place where they can put their value.
_reminderDates.add(null);
var currentIndex = _reminderDates.length - 1;
// FIXME: Why does putting this into a row fails?
setState(() => _reminderInputs.add(Row(
children: <Widget>[
VikunjaDateTimePicker(
label: 'Reminder',
onSaved: (reminder) =>
_reminderDates[currentIndex] = reminder,
),
GestureDetector(
onTap: () => print('tapped'),
child: Icon(Icons.close),
)
],
)));
}),
InputDecorator(
isEmpty: _priority == null,
decoration: InputDecoration(
icon: const Icon(Icons.flag),
labelText: 'Priority',
border: InputBorder.none,
),
child: new DropdownButton<String>(
value: _priorityToString(_priority),
isExpanded: true,
isDense: true,
onChanged: (String newValue) {
setState(() {
_priority = _priorityFromString(newValue);
});
},
items: ['Unset', 'Low', 'Medium', 'High', 'Urgent', 'DO NOW']
.map((String value) {
return new DropdownMenuItem(
value: value,
child: new Text(value),
);
}).toList(),
),
),
Wrap(
spacing: 10,
children: _labels.map((Label label) {
return LabelComponent(
label: label,
onDelete: () {
_removeLabel(label);
},
);
}).toList()),
Row(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width - 80,
child: TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _labelTypeAheadController,
decoration:
InputDecoration(labelText: 'Add a new label')),
suggestionsCallback: (pattern) {
return _searchLabel(pattern);
},
itemBuilder: (context, suggestion) {
return ListTile(
title: Text(suggestion),
);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (suggestion) {
_addLabel(suggestion);
},
),
),
IconButton(
onPressed: () =>
_createAndAddLabel(_labelTypeAheadController.text),
icon: Icon(Icons.add),
)
],
),
Builder(
builder: (context) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_saveTask(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),
))),
]),
),
),
),
);
}
_saveTask(BuildContext context) async {
setState(() => _loading = true);
// Removes all reminders with no value set.
_reminderDates.removeWhere((d) => d == null);
Task updatedTask = Task(
id: widget.task.id,
title: _title,
description: _description,
done: widget.task.done,
reminderDates: _reminderDates,
createdBy: widget.task.createdBy,
dueDate: _dueDate,
startDate: _startDate,
endDate: _endDate,
priority: _priority,
repeatAfter: _repeatAfter,
);
// update the labels
VikunjaGlobal.of(context)
.labelTaskBulkService
.update(updatedTask, _labels)
.catchError((err) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
),
);
});
VikunjaGlobal.of(context).taskService.update(updatedTask).then((_) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The task was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: ScaffoldMessenger.of(context).hideCurrentSnackBar),
),
);
});
}
_removeLabel(Label label) {
setState(() {
_labels.removeWhere((l) => l.id == label.id);
});
}
_searchLabel(String query) {
return VikunjaGlobal.of(context)
.labelService
.getAll(query: query)
.then((labels) {
// Only show those labels which aren't already added to the task
labels.removeWhere((labelToRemove) => _labels.contains(labelToRemove));
_suggestedLabels = labels;
return labels.map((label) => label.title).toList();
});
}
_addLabel(String labelTitle) {
// FIXME: This is not an optimal solution...
bool found = false;
_suggestedLabels.forEach((label) {
if (label.title == labelTitle) {
_labels.add(label);
found = true;
}
});
if (found) {
_labelTypeAheadController.clear();
}
}
_createAndAddLabel(String labelTitle) {
// Only add a label if there are none to add
if (labelTitle.isEmpty || (_suggestedLabels?.isNotEmpty ?? false)) {
return;
}
Label newLabel = Label(title: labelTitle);
VikunjaGlobal.of(context)
.labelService
.create(newLabel)
.then((createdLabel) {
setState(() {
_labels.add(createdLabel);
_labelTypeAheadController.clear();
});
});
}
// FIXME: Move the following two functions to an extra class or type.
_priorityFromString(String priority) {
switch (priority) {
case 'Low':
return 1;
case 'Medium':
return 2;
case 'High':
return 3;
case 'Urgent':
return 4;
case 'DO NOW':
return 5;
default:
// unset
return 0;
}
}
_priorityToString(int priority) {
switch (priority) {
case 0:
return 'Unset';
case 1:
return 'Low';
case 2:
return 'Medium';
case 3:
return 'High';
case 4:
return 'Urgent';
case 5:
return 'DO NOW';
default:
return null;
}
}
}

View File

@ -3,12 +3,14 @@ import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/pages/list/list.dart';
import 'package:vikunja_app/stores/list_store.dart';
class NamespacePage extends StatefulWidget {
final Namespace namespace;
@ -26,7 +28,7 @@ class _NamespacePageState extends State<NamespacePage>
@override
void afterFirstLayout(BuildContext context) {
_loadLists();
//_loadLists(); // NOTE: goes right after didChangeDependencies
}
/////
@ -56,11 +58,8 @@ class _NamespacePageState extends State<NamespacePage>
color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
_removeList(ls).then((_) => Scaffold.of(
context)
.showSnackBar(SnackBar(
content:
Text("${ls.title} removed"))));
_removeList(ls).then((_) => ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("${ls.title} removed"))));
},
))).toList(),
)
@ -89,6 +88,7 @@ class _NamespacePageState extends State<NamespacePage>
}
Future<void> _loadLists() {
// FIXME: This is called even when the tasks on a list are loaded - which is not needed at all
return VikunjaGlobal.of(context)
.listService
.getByNamespace(widget.namespace.id)
@ -99,8 +99,15 @@ class _NamespacePageState extends State<NamespacePage>
}
_openList(BuildContext context, TaskList list) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ListPage(taskList: list)));
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<ListProvider>(
create: (_) => new ListProvider(),
child: ListPage(
taskList: list,
),
),
// ListPage(taskList: list)
));
}
_addListDialog(BuildContext context) {
@ -120,7 +127,7 @@ class _NamespacePageState extends State<NamespacePage>
.then((_) {
setState(() {});
_loadLists();
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('The list was successfully created!'),
),

View File

@ -107,17 +107,17 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
.update(updatedNamespace)
.then((_) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(SnackBar(
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The namespace was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: Scaffold.of(context).hideCurrentSnackBar),
onPressed: ScaffoldMessenger.of(context).hideCurrentSnackBar),
),
);
});

View File

@ -4,20 +4,26 @@ class PlaceholderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
padding: EdgeInsets.only(left: 16.0),
child: new Column(
padding: EdgeInsets.all(16),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
new Container(
Container(
padding: EdgeInsets.only(top: 32.0),
child: new Text(
child: Text(
'Welcome to Vikunja',
style: Theme.of(context).textTheme.headline,
style: Theme.of(context).textTheme.headline5,
),
),
new Text('Please select a namespace by tapping the ☰ icon.',
style: Theme.of(context).textTheme.subhead),
Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Text('Please select a namespace by tapping the ☰ icon.',
style: Theme.of(context).textTheme.subtitle1),
)
],
));
),
),
);
}
}

View File

@ -24,7 +24,7 @@ class _LoginPageState extends State<LoginPage> {
padding: const EdgeInsets.all(16.0),
child: Builder(
builder: (BuildContext context) => Form(
autovalidate: true,
autovalidateMode: AutovalidateMode.always,
key: _formKey,
child: Center(
child: Column(
@ -122,7 +122,7 @@ class _LoginPageState extends State<LoginPage> {
'Login failed! Please check your server url and credentials. ' +
ex.toString()),
actions: <Widget>[
FlatButton(
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],

View File

@ -140,7 +140,7 @@ class _RegisterPageState extends State<RegisterPage> {
'Registration failed! Please check your server url and credentials. ' +
ex.toString()),
actions: <Widget>[
FlatButton(
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
@ -39,7 +40,7 @@ var _tasks = {
1: Task(
id: 1,
title: 'Task 1',
owner: _users[1],
createdBy: _users[1],
updated: DateTime.now(),
created: DateTime.now(),
description: 'A descriptive task',
@ -121,7 +122,8 @@ class MockedTaskService implements TaskService {
@override
Future delete(int taskId) {
_lists.forEach(
(_, list) => list.tasks.removeWhere((task) => task.id == taskId));
(_, list) => list.tasks.removeWhere((task) => task.id == taskId)
);
_tasks.remove(taskId);
return Future.value();
}
@ -144,6 +146,15 @@ class MockedTaskService implements TaskService {
_lists[listId].tasks.add(task);
return Future.value(task);
}
@override
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]) {
return Future.value(new Response(_tasks.values.toList(), 200, {}));
}
@override
int get maxPages => 1;
}
class MockedUserService implements UserService {

View File

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/labelTask.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
@ -26,6 +29,10 @@ abstract class TaskService {
Future<Task> update(Task task);
Future delete(int taskId);
Future<Task> add(int listId, Task task);
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]);
// TODO: Avoid having to add this to each abstract class
int get maxPages;
}
abstract class UserService {
@ -33,3 +40,21 @@ abstract class UserService {
Future<UserTokenPair> register(String username, email, password);
Future<User> getCurrentUser();
}
abstract class LabelService {
Future<List<Label>> getAll({String query});
Future<Label> get(int labelID);
Future<Label> create(Label label);
Future<Label> delete(Label label);
Future<Label> update(Label label);
}
abstract class LabelTaskService {
Future<List<Label>> getAll(LabelTask lt, {String query});
Future<Label> create(LabelTask lt);
Future<Label> delete(LabelTask lt);
}
abstract class LabelTaskBulkService {
Future<List<Label>> update(Task task, List<Label> labels);
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/global.dart';
class ListProvider with ChangeNotifier {
bool _isLoading = false;
int _maxPages = 0;
// TODO: Streams
List<Task> _tasks = [];
bool get isLoading => _isLoading;
int get maxPages => _maxPages;
set tasks(List<Task> tasks) {
_tasks = tasks;
notifyListeners();
}
List<Task> get tasks => _tasks;
void loadTasks({BuildContext context, int listId, int page = 1}) {
_tasks = [];
_isLoading = true;
notifyListeners();
VikunjaGlobal.of(context).taskService.getAll(listId, {
"sort_by": ["done", "id"],
"order_by": ["asc", "desc"],
"page": [page.toString()]
}).then((response) {
if (response.headers["x-pagination-total-pages"] != null) {
_maxPages = int.parse(response.headers["x-pagination-total-pages"]);
}
_tasks.addAll(response.body);
_isLoading = false;
notifyListeners();
});
}
Future<void> addTaskByTitle({BuildContext context, String title, int listId}) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null,
title: title,
createdBy: globalState.currentUser,
done: false,
);
_isLoading = true;
notifyListeners();
return globalState.taskService.add(listId, newTask).then((task) {
_tasks.insert(0, task);
_isLoading = false;
notifyListeners();
});
}
Future<void> addTask({BuildContext context, Task newTask, int listId}) {
var globalState = VikunjaGlobal.of(context);
_isLoading = true;
notifyListeners();
return globalState.taskService.add(listId, newTask).then((task) {
_tasks.insert(0, task);
_isLoading = false;
notifyListeners();
});
}
Future<void> updateTask({BuildContext context, int id, bool done}) {
var globalState = VikunjaGlobal.of(context);
globalState.taskService.update(Task(
id: id,
done: done,
)).then((task) {
// FIXME: This is ugly. We should use a redux to not have to do these kind of things.
// This is enough for now (it works) but we should definitly fix it later.
_tasks.asMap().forEach((i, t) {
if (task.id == t.id) {
_tasks[i] = task;
}
});
notifyListeners();
});
}
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
/////////
// Colors
@ -18,9 +19,15 @@ const vButtonTextColor = vWhite;
const vButtonShadowDark = Color(0xFF0b2a4a);
const vButtonShadow = Color(0xFFb2d9ff);
const vLabelLight = Color(0xFFf2f2f2);
const vLabelDark = Color(0xFF4a4a4a);
const vLabelDefaultColor = vGreen;
///////////
// Paddings
////////
const vStandardVerticalPadding = EdgeInsets.symmetric(vertical: 5.0);
const vStandardHorizontalPadding = EdgeInsets.symmetric(horizontal: 5.0);
const vStandardPadding = EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0);
var vDateFormatLong = DateFormat("EEEE, MMMM d, yyyy 'at' H:mm");

View File

@ -27,12 +27,12 @@ ThemeData _buildVikunjaTheme(ThemeData base) {
),
),
textTheme: base.textTheme.copyWith(
headline: base.textTheme.headline.copyWith(
fontFamily: 'Quicksand',
),
title: base.textTheme.title.copyWith(
fontFamily: 'Quicksand',
),
// headline: base.textTheme.headline.copyWith(
// fontFamily: 'Quicksand',
// ),
// title: base.textTheme.title.copyWith(
// fontFamily: 'Quicksand',
// ),
button: base.textTheme.button.copyWith(
color:
vWhite, // This does not work, looks like a bug in Flutter: https://github.com/flutter/flutter/issues/19623

View File

@ -0,0 +1,11 @@
datetimeToUnixTimestamp(DateTime dt) {
return dt?.millisecondsSinceEpoch == null
? null
: (dt.millisecondsSinceEpoch / 1000).round();
}
dateTimeFromUnixTimestamp(int timestamp) {
return timestamp == null
? 0
: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}

View File

@ -0,0 +1,66 @@
getRepeatAfterTypeFromDuration(Duration repeatAfter) {
if (repeatAfter == null || repeatAfter.inSeconds == 0) {
return null;
}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfter.inHours % 24 == 0) {
if (repeatAfter.inDays % 7 == 0) {
return 'Weeks';
} else if (repeatAfter.inDays % 365 == 0) {
return 'Years';
} else if (repeatAfter.inDays % 30 == 0) {
return 'Months';
} else {
return 'Days';
}
}
return 'Hours';
}
getRepeatAfterValueFromDuration(Duration repeatAfter) {
if (repeatAfter == null || repeatAfter.inSeconds == 0) {
return null;
}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfter.inHours % 24 == 0) {
if (repeatAfter.inDays % 7 == 0) {
// Weeks
return (repeatAfter.inDays / 7).round();
} else if (repeatAfter.inDays % 365 == 0) {
// Years
return (repeatAfter.inDays / 365).round();
} else if (repeatAfter.inDays % 30 == 0) {
// Months
return (repeatAfter.inDays / 30).round();
} else {
return repeatAfter.inDays; // Days
}
}
// Otherwise Hours
return repeatAfter.inHours;
}
getDurationFromType(String value, String type) {
// Return an empty duration if either of the values is not set
if (value == null || value == '' || type == null || type == '') {
return Duration();
}
int val = int.parse(value);
switch (type) {
case 'Hours':
return Duration(hours: val);
case 'Days':
return Duration(days: val);
case 'Weeks':
return Duration(days: val * 7);
case 'Months':
return Duration(days: val * 30);
case 'Years':
return Duration(days: val * 365);
}
}

View File

@ -1,252 +1,572 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "22.0.0"
after_layout:
dependency: "direct main"
description:
name: after_layout
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.7+2"
version: "1.1.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.2"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
version: "3.1.2"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
version: "2.2.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
version: "2.8.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
build:
dependency: "direct main"
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.3.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
version: "1.15.0"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
version: "3.0.0"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
version: "3.0.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
version: "1.0.3"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
datetime_picker_formfield:
dependency: "direct main"
description:
name: datetime_picker_formfield
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_keyboard_visibility:
dependency: transitive
description:
name: flutter_keyboard_visibility
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.2"
flutter_keyboard_visibility_platform_interface:
dependency: transitive
description:
name: flutter_keyboard_visibility_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_keyboard_visibility_web:
dependency: transitive
description:
name: flutter_keyboard_visibility_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
version: "0.9.2"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.5"
version: "4.2.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_typeahead:
dependency: "direct main"
description:
name: flutter_typeahead
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
http:
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0+3"
version: "0.13.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
version: "4.0.0"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
version: "3.0.2"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.17.0"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.3"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
json_serializable:
dependency: "direct main"
description:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.4"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
version: "1.8.0"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0+1"
version: "1.11.0"
petitparser:
dependency: transitive
dependency: "direct main"
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
version: "4.1.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.0"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.4"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.10"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
version: "1.8.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.17.10"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
version: "0.4.2"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0"
vm_service:
dependency: transitive
description:
name: vm_service
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
version: "5.1.1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "3.1.0"
sdks:
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.20.0 <2.0.0"
dart: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"

View File

@ -1,23 +1,33 @@
name: vikunja_app
description: Vikunja as Flutter cross platform app
version: 0.1.0
version: 0.2.0+200099
environment:
sdk: ">=2.1.0 <3.0.0"
# TODO: update SDK >=2.12.0 (null-safety enabled)
sdk: ">=2.11.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.3
flutter_secure_storage: 3.3.5
http: 0.12.0+3
after_layout: ^1.0.7
cupertino_icons: 1.0.3
flutter_secure_storage: 4.2.1
http: 0.13.3
after_layout: 1.1.0
#alice: 0.2.1
datetime_picker_formfield: 2.0.0
flutter_typeahead: 3.2.0
build: 2.1.0
json_serializable: 4.1.4
petitparser: 4.1.0
provider: 6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: "^0.8.0"
version: any
test: any
flutter_launcher_icons: 0.9.2
flutter_icons:
image_path: "assets/vikunja_logo.png"

View File

@ -0,0 +1,22 @@
import 'dart:convert';
import 'dart:ui';
import 'package:test/test.dart';
import 'package:vikunja_app/models/label.dart';
void main() {
test('label color from json', () {
final String json = '{"TaskID": 123,"id": 1,"title": "this","description": "","hex_color": "e8e8e8","created_by":{"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325},"created": 1552903790,"updated": 1552903790}';
final JsonDecoder _decoder = new JsonDecoder();
Label label = Label.fromJson(_decoder.convert(json));
expect(label.color, Color(0xFFe8e8e8));
});
test('hex color string from object', () {
Label label = Label(id: 1, color: Color(0xFFe8e8e8));
var json = label.toJSON();
expect(json.toString(), '{id: 1, title: null, description: null, hex_color: e8e8e8, created_by: null, updated: null, created: null}');
});
}

View File

@ -0,0 +1,76 @@
import 'package:test/test.dart';
import 'package:vikunja_app/utils/repeat_after_parse.dart';
void main() {
test('Repeat after hours', () {
Duration testDuration = Duration(hours: 6);
expect(getRepeatAfterTypeFromDuration(testDuration), 'Hours');
expect(getRepeatAfterValueFromDuration(testDuration), 6);
});
test('Repeat after days', () {
Duration testDuration = Duration(days: 6);
expect(getRepeatAfterTypeFromDuration(testDuration), 'Days');
expect(getRepeatAfterValueFromDuration(testDuration), 6);
});
test('Repeat after weeks', () {
Duration testDuration = Duration(days: 6 * 7);
expect(getRepeatAfterTypeFromDuration(testDuration), 'Weeks');
expect(getRepeatAfterValueFromDuration(testDuration), 6);
});
test('Repeat after months', () {
Duration testDuration = Duration(days: 6 * 30);
expect(getRepeatAfterTypeFromDuration(testDuration), 'Months');
expect(getRepeatAfterValueFromDuration(testDuration), 6);
});
test('Repeat after years', () {
Duration testDuration = Duration(days: 6 * 365);
expect(getRepeatAfterTypeFromDuration(testDuration), 'Years');
expect(getRepeatAfterValueFromDuration(testDuration), 6);
});
test('Repeat null value', () {
Duration testDuration = Duration();
expect(getRepeatAfterTypeFromDuration(testDuration), null);
expect(getRepeatAfterValueFromDuration(testDuration), null);
});
test('Hours to duration', () {
Duration parsedDuration = getDurationFromType('6', 'Hours');
expect(parsedDuration, Duration(hours: 6));
});
test('Days to duration', () {
Duration parsedDuration = getDurationFromType('6', 'Days');
expect(parsedDuration, Duration(days: 6));
});
test('Weeks to duration', () {
Duration parsedDuration = getDurationFromType('6', 'Weeks');
expect(parsedDuration, Duration(days: 6 * 7));
});
test('Months to duration', () {
Duration parsedDuration = getDurationFromType('6', 'Months');
expect(parsedDuration, Duration(days: 6 * 30));
});
test('Years to duration', () {
Duration parsedDuration = getDurationFromType('6', 'Years');
expect(parsedDuration, Duration(days: 6 * 365));
});
test('null to duration', () {
Duration parsedDuration = getDurationFromType(null, null);
expect(parsedDuration, Duration());
});
}

View File

@ -0,0 +1,52 @@
import 'dart:convert';
import 'package:vikunja_app/models/task.dart';
import 'package:test/test.dart';
void main() {
test('Check encoding with all values set', () {
final String json = '{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": [1543834800,1544612400],"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
final JsonDecoder _decoder = new JsonDecoder();
final task = Task.fromJson(_decoder.convert(json));
expect(task.id, 1);
expect(task.title, 'test');
expect(task.description, 'Lorem Ipsum');
expect(task.done, true);
expect(task.reminderDates, [
DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000),
DateTime.fromMillisecondsSinceEpoch(1544612400 * 1000),
]);
expect(task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(task.repeatAfter, Duration(seconds: 3600));
expect(task.parentTaskId, 0);
expect(task.priority, 100);
expect(task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
expect(task.labels, null);
expect(task.subtasks, null);
expect(task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
expect(task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
});
test('Check encoding with reminder dates as null', () {
final String json = '{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": null,"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
final JsonDecoder _decoder = new JsonDecoder();
final task = Task.fromJson(_decoder.convert(json));
expect(task.id, 1);
expect(task.title, 'test');
expect(task.description, 'Lorem Ipsum');
expect(task.done, true);
expect(task.reminderDates, null);
expect(task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(task.repeatAfter, Duration(seconds: 3600));
expect(task.parentTaskId, 0);
expect(task.priority, 100);
expect(task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
expect(task.labels, null);
expect(task.subtasks, null);
expect(task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
expect(task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
});
}