Compare commits

..

78 Commits

Author SHA1 Message Date
88bf3f9bc7 Fixed a typo in title (#75)
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #75
Co-authored-by: HammyHavoc <hammy@splitanatom.com>
Co-committed-by: HammyHavoc <hammy@splitanatom.com>
2022-01-27 20:12:47 +00:00
fd33172250 Upgrade android embedding to v2 (#60)
All checks were successful
continuous-integration/drone/push Build is passing
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
5d0b424217 Fix badges (#59)
All checks were successful
continuous-integration/drone/push Build is passing
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
09b15a249c
Re-enable dependencies for iOS build
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-17 15:52:04 +01:00
7fa0835063
Remove depenedncy
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-16 19:07:30 +01:00
dbc3a77105
rbenv fix
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-01-16 19:07:10 +01:00
50cb3318d4
Add rbenv
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-16 19:01:36 +01:00
f422b73414
Use local bundle path
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-16 18:15:34 +01:00
89f790c38c
Use bundle install
Some checks reported errors
continuous-integration/drone/push Build was killed
2021-01-16 18:10:34 +01:00
5ffa78c97c Fix iOS build (#58)
Some checks failed
continuous-integration/drone/push Build is failing
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
3adfe3c860 Update dependency gradle to v4.10.3 (#54)
Some checks failed
continuous-integration/drone/push Build is failing
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
442251f636 Update dependency flutter_secure_storage to v3.3.5 (#53)
Some checks reported errors
continuous-integration/drone/push Build was killed
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
2fc3aabae3
Fix s3 release bucket credentials
Some checks reported errors
continuous-integration/drone/push Build was killed
2020-12-26 18:42:53 +01:00
648e9c5748 Update dependency flutter_launcher_icons to ^0.8.0 (#52)
Some checks reported errors
continuous-integration/drone/push Build was killed
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
99f1e1aed4 Configure Renovate (#51)
Some checks reported errors
continuous-integration/drone/push Build was killed
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
23c048b3f4 Remove chown to fix ci (#49)
Some checks failed
continuous-integration/drone/push Build is failing
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
0bbc552945
Use the updated logo
Some checks failed
continuous-integration/drone/push Build is failing
2020-09-03 21:29:23 +02:00
de77bd2565
Update dependencies
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-10 10:34:58 +02:00
8f595c340f
Revert "Add sentry (#43)"
This reverts commit 9c2622e7
2020-07-10 10:32:16 +02:00
2abdcc4ca9 Publish iOS Version into Testers group automatically (#47)
All checks were successful
continuous-integration/drone/push Build is passing
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
3ecf6cd9dd Add dark mode (#46)
All checks were successful
continuous-integration/drone/push Build is passing
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
8993999a68 Improve error handling (#45)
All checks were successful
continuous-integration/drone/push Build is passing
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
6c0762d458 Provide build for iOS (#42)
All checks were successful
continuous-integration/drone/push Build is passing
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
9c2622e77f Add sentry (#43)
All checks were successful
continuous-integration/drone/push Build is passing
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
4adee85d62
Don't try to run tests in pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2020-06-16 00:18:09 +02:00
020cfa2bde
Format
Some checks failed
continuous-integration/drone/push Build is failing
2020-06-15 23:48:15 +02:00
9973816dde
Fix all json fields being snake_case
Some checks failed
continuous-integration/drone/push Build is failing
2020-06-15 23:46:10 +02:00
68d3ffd30d
Rename namespace name and task text to title
Some checks failed
continuous-integration/drone/push Build is failing
2020-06-15 23:42:12 +02:00
46fc580b7b
Format
All checks were successful
continuous-integration/drone/push Build is passing
2020-04-27 17:09:33 +02:00
361057aa9f
Fix date format
Some checks failed
continuous-integration/drone/push Build is failing
2020-04-27 17:02:55 +02:00
14300266ef
Change release bucket
All checks were successful
continuous-integration/drone/push Build is passing
2020-03-01 22:56:05 +01:00
a2f53ce711
Use the same image for building and testing in ci
All checks were successful
continuous-integration/drone/push Build is passing
2020-01-15 23:38:02 +01:00
0f23c4d0f3 Make it build again (#38)
All checks were successful
continuous-integration/drone/push Build is passing
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
144f55af6d
Updated dockerimage to use the vikunja one for building apps
All checks were successful
continuous-integration/drone/push Build is passing
2019-04-25 19:35:52 +02:00
8278340242 Show a message if a list or namespace is empty (#29)
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-18 17:05:32 +00:00
2121b831a0 Namespace edit (#36)
Some checks reported errors
continuous-integration/drone/push Build was killed
2019-03-18 17:00:34 +00:00
1e3518554b Logout (#35)
All checks were successful
continuous-integration/drone/push Build is passing
:C
2019-03-18 16:56:15 +00:00
9848c462f8 Fixed namespaces loading every time a widget was loaded (#34)
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-18 15:30:54 +00:00
75f6608863 Refactor (#31)
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-16 13:29:00 +00:00
fa05e0e6a3 Fix the build and update drone (#33)
All checks were successful
continuous-integration/drone/push Build is passing
2019-03-15 22:14:37 +00:00
67e47b03cd Snackbars for all actions (#30)
Some checks failed
the build failed
2019-03-15 06:52:50 +00:00
cc25bba0c7 Reload items after adding it (#28)
Some checks failed
the build failed
2019-03-14 21:27:13 +00:00
893e59b00e List edit design improvements (#27)
Some checks failed
the build failed
2019-03-14 21:25:57 +00:00
ae197684ac Login page design improvements (#26)
Some checks failed
the build failed
2019-03-14 21:12:02 +00:00
cb360f656f Theme update (#23)
Some checks failed
the build failed
2019-03-11 20:38:05 +00:00
b904379af7 Added better error messages to login and register (#25)
Some checks failed
the build failed
2019-03-11 20:37:25 +00:00
c285120034 List edit (#21)
Some checks failed
the build failed
2019-03-11 20:29:15 +00:00
d30ea0f91c Cleanup pubspec (#24)
Some checks failed
the build failed
2019-03-10 21:41:55 +00:00
563fe1fc4d Fixed app not working with the newest api change which has multiple reminders (#19)
All checks were successful
the build was successful
2018-12-03 21:26:00 +00:00
62d7261a63 Fix build (#18)
All checks were successful
the build was successful
2018-11-05 15:43:30 +00:00
7755b9c812 Added formatting check to makefile and ci (#17)
Some checks failed
the build failed
2018-10-15 17:16:47 +00:00
abf0196de3 Added register (#13)
All checks were successful
the build was successful
2018-10-08 14:26:01 +00:00
301997f32b Fix Typo (#14)
All checks were successful
the build was successful
2018-10-02 15:53:57 +00:00
8e90f9b170 Improved regex (#12)
All checks were successful
the build was successful
2018-09-27 16:04:37 +00:00
2e04969689 Add improved loading indicators (#9)
Some checks failed
the build failed
2018-09-27 15:55:56 +00:00
3c52931538 Fixed url regex (#11)
Some checks failed
the build failed
2018-09-25 12:06:41 +00:00
d70863a752 Fixed version (#8)
All checks were successful
the build was successful
2018-09-23 16:39:20 +00:00
3cb375dafe Removed .idea folder (#7)
All checks were successful
the build was successful
2018-09-23 16:23:53 +00:00
cabac017d9 Fixed build (#4)
All checks were successful
the build was successful
2018-09-23 16:04:00 +00:00
4a29480ded added fdroid flavour (#3)
Some checks failed
the build failed
2018-09-23 15:38:02 +00:00
a5b8618d49 Added drone (#2)
All checks were successful
the build was successful
2018-09-23 10:41:28 +00:00
35db3db988 Cleanup (#1) 2018-09-22 20:56:16 +00:00
f7db5324aa
Add API implementations of List, Namespace, Task, User
Use Services in order to retrieve data
2018-09-17 18:16:50 +02:00
1994892b63
Add working login implementation 2018-09-17 15:35:57 +02:00
4bfd13536e
Fix a bug causing to not-start at new devices 2018-09-16 22:19:43 +02:00
502fd62dd9
Add mocked services 2018-09-16 22:13:50 +02:00
2bb5f7c6a2
Reformatted code 2018-09-16 21:47:53 +02:00
4c986b85df
Add login view
Add services and models
Add mocks
2018-09-16 21:47:33 +02:00
0e6d7778ea
Add list support 2018-09-15 19:40:59 +02:00
08df1bf2c2
Fix alert for cupertino OS 2018-09-15 18:33:41 +02:00
9dc281317b
Add ListPage 2018-09-15 18:21:48 +02:00
7e681bd16b
Reformat code
Add "Add namespace" functionality
2018-09-15 17:22:28 +02:00
9acc0ff750
Add namespace 2018-09-15 17:10:34 +02:00
d40fdab947
Refactor drawer 2018-09-15 17:01:45 +02:00
6fd7bcd88b
Fix gradle wrapper 2018-09-15 16:47:37 +02:00
16a0e50875
Add gradle wrapper 2018-09-15 16:12:38 +02:00
879a24ad22
Add vikunja design
Add drawer menu
2018-09-15 14:18:24 +02:00
f3fda094f3
Add initial flutter project 2018-09-14 18:59:13 +02:00
59 changed files with 419 additions and 2125 deletions

View File

@ -1,42 +0,0 @@
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

View File

@ -1,70 +0,0 @@
# 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,5 +1,6 @@
# Miscellaneous
*.class
*.lock
*.log
*.pyc
*.swp
@ -15,33 +16,62 @@
*.iws
.idea/
# 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/
# Visual Studio Code related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
ios/Flutter/flutter_export_environment.sh
build/
!pubspec.lock
.flutter-plugins-dependencies
# Web related
lib/generated_plugin_registrant.dart
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# Symbolication related
app.*.symbols
# 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.*
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# 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

View File

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

View File

@ -1,8 +1,8 @@
# Vikunja Cross-Plattform app
# Vikunja Cross-Platform 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,11 +1,10 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
*.iml
*.class
.gradle
/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,14 +25,8 @@ 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 30
compileSdkVersion 28
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -45,32 +39,28 @@ android {
defaultConfig {
applicationId "io.vikunja.app"
minSdkVersion 19
targetSdkVersion 30
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
flavorDimensions "deploy"
//productFlavors {
// fdroid {
// dimension "deploy"
// signingConfig null
// }
// unsigned {
// dimension "deploy"
// signingConfig null
// }
//}
productFlavors {
fdroid {
dimension "deploy"
signingConfig null
}
unsigned {
dimension "deploy"
signingConfig null
}
main {
dimension "deploy"
signingConfig signingConfigs.debug // TODO add signing key
}
}
android.applicationVariants.all { variant ->
if (variant.flavorName == "fdroid") {
@ -84,9 +74,6 @@ 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
}
}
@ -97,5 +84,8 @@ flutter {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
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'
}

View File

@ -19,35 +19,16 @@
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
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,5 +2,4 @@ package io.vikunja.flutteringvikunja
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity() {
}
class MainActivity : FlutterActivity()

View File

@ -1,12 +0,0 @@
<?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,18 +1,8 @@
<?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.4.31'
ext.kotlin_version = '1.2.30'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.android.tools.build:gradle:3.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -1,3 +1,4 @@
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-6.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip

View File

@ -1,11 +1,15 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
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"
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

View File

@ -0,0 +1,18 @@
#
# 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,8 +1,6 @@
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();
@ -27,46 +25,33 @@ class Client {
'Content-Type': 'application/json'
};
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)
Future<dynamic> get(String url) {
return http
.get('${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> delete(String url) {
return http
.delete('${this.base}$url', headers: _headers)
.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> post(String url, {dynamic body}) {
return http
.post('${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);
Future<dynamic> put(String url, {dynamic body}) {
return http
.put('${this.base}$url',
headers: _headers, body: _encoder.convert(body))
.then(_handleResponse);
}
Response _handleResponse(http.Response response) {
dynamic _handleResponse(http.Response response) {
if (response.statusCode < 200 ||
response.statusCode >= 400 ||
json == null) {
@ -80,17 +65,12 @@ class Client {
throw new ApiException(
response.statusCode, response.request.url.toString());
}
return Response(
_decoder.convert(response.body),
response.statusCode,
response.headers
);
return _decoder.convert(response.body);
}
}
class InvalidRequestApiException extends ApiException {
final String message;
InvalidRequestApiException(int errorCode, String path, this.message)
: super(errorCode, path);
@ -103,7 +83,6 @@ class InvalidRequestApiException extends ApiException {
class ApiException implements Exception {
final int errorCode;
final String path;
ApiException(this.errorCode, this.path);
@override

View File

@ -1,30 +0,0 @@
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

@ -1,20 +0,0 @@
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)));
}
}

View File

@ -1,41 +0,0 @@
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((response) => TaskList.fromJson(response.body));
.then((map) => TaskList.fromJson(map));
}
@override
@ -22,38 +22,25 @@ class ListAPIService extends APIService implements ListService {
@override
Future<TaskList> get(int listId) {
/*
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);
});
return client.get('/lists/$listId').then((map) => TaskList.fromJson(map));
}
@override
Future<List<TaskList>> getAll() {
return client.get('/lists').then((response) =>
convertList(response.body, (result) => TaskList.fromJson(result)));
return client.get('/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
}
@override
Future<List<TaskList>> getByNamespace(int namespaceId) {
return client.get('/namespaces/$namespaceId/lists').then((response) =>
convertList(response.body, (result) => TaskList.fromJson(result)));
return client.get('/namespaces/$namespaceId/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
}
@override
Future<TaskList> update(TaskList tl) {
return client
.post('/lists/${tl.id}', body: tl.toJSON())
.then((response) => TaskList.fromJson(response.body));
.then((map) => TaskList.fromJson(map));
}
}

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

View File

@ -1,9 +0,0 @@
// 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,7 +1,6 @@
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';
@ -13,7 +12,7 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> add(int listId, Task task) {
return client
.put('/lists/$listId', body: task.toJSON())
.then((response) => Task.fromJson(response.body));
.then((map) => Task.fromJson(map));
}
@override
@ -25,19 +24,6 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> update(Task task) {
return client
.post('/tasks/${task.id}', body: task.toJSON())
.then((response) => Task.fromJson(response.body));
.then((map) => Task.fromJson(map));
}
@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((response) => response.body['token']);
}).then((map) => map['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((response) => response.body['username']);
}).then((resp) => resp['username']);
return login(newUser, password);
}
@override
Future<User> getCurrentUser() {
return client.get('/user').then((response) => User.fromJson(response.body));
return client.get('/user').then((map) => User.fromJson(map));
}
}

View File

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

View File

@ -1,51 +0,0 @@
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));
},
);
}
}

View File

@ -1,52 +0,0 @@
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

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

View File

@ -1,9 +1,6 @@
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';
@ -22,7 +19,8 @@ class VikunjaGlobal extends StatefulWidget {
VikunjaGlobalState createState() => VikunjaGlobalState();
static VikunjaGlobalState of(BuildContext context) {
var widget = context.dependOnInheritedWidgetOfExactType<_VikunjaGlobalInherited>();
var widget = context.inheritFromWidgetOfExactType(_VikunjaGlobalInherited)
as _VikunjaGlobalInherited;
return widget.data;
}
}
@ -48,13 +46,6 @@ 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();
@ -94,7 +85,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
_currentUser = null;
});
}).catchError((err) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('An error occured while logging out!'),
));
});

View File

@ -3,32 +3,22 @@ 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 StatefulWidget {
class VikunjaApp extends StatelessWidget {
final Widget home;
VikunjaApp({Key key, this.home}) : super(key: key);
@override
_VikunjaAppState createState() => _VikunjaAppState();
}
class _VikunjaAppState extends State<VikunjaApp> {
//Alice alice = Alice(showNotification: true);
const VikunjaApp({Key key, this.home}) : super(key: key);
@override
Widget build(BuildContext context) {
return new MaterialApp(
//navigatorKey: alice.getNavigatorKey(),
title: 'Vikunja',
theme: buildVikunjaTheme(),
darkTheme: buildVikunjaDarkTheme(),
home: this.widget.home,
home: this.home,
);
}
}

View File

@ -1,43 +0,0 @@
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,
};
}

View File

@ -1,18 +0,0 @@
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

@ -1,15 +0,0 @@
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,24 +9,23 @@ 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, {tasksJson})
TaskList.fromJson(Map<String, dynamic> json)
: id = json['id'],
owner = json['owner'] == null ? null : User.fromJson(json['owner']),
owner = User.fromJson(json['owner']),
description = json['description'],
title = json['title'],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
tasks = (tasksJson == null ? [] : tasksJson as List<dynamic>)
tasks = (json['tasks'] == null ? [] : json['tasks'] 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 = json['owner'] == null ? null : User.fromJson(json['owner']);
owner = User.fromJson(json['owner']);
toJSON() => {
"created": created?.toIso8601String(),

View File

@ -1,84 +1,49 @@
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';
import 'package:meta/meta.dart';
@JsonSerializable()
class Task {
final int id, parentTaskId, priority;
final DateTime created, updated, dueDate, startDate, endDate;
final List<DateTime> reminderDates;
final int id;
final DateTime created, updated, due;
final List<DateTime> reminders;
final String title, description;
final bool done;
final User createdBy;
final Duration repeatAfter;
final List<Task> subtasks;
final List<Label> labels;
final User owner;
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.createdBy});
this.reminders,
this.due,
@required this.title,
this.description,
@required this.done,
@required this.owner});
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']),
createdBy = json['created_by'] == null ? null : User.fromJson(json['created_by']);
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']);
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,7 +15,9 @@ class User {
String avatarUrl(BuildContext context) {
return VikunjaGlobal.of(context).client.base +
"/avatar/${this.username}";
"/" +
this.username +
"/avatar";
}
}

View File

@ -1,18 +1,19 @@
import 'dart:async';
import 'package:after_layout/after_layout.dart';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.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() => HomePageState();
State<StatefulWidget> createState() => new HomePageState();
}
class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
@ -35,13 +36,12 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
List<Widget> namespacesList = <Widget>[];
_namespaces
.asMap()
.forEach((i, namespace) => namespacesList.add(ListTile(
.forEach((i, namespace) => namespacesList.add(new ListTile(
leading: const Icon(Icons.folder),
title: Text(namespace.title),
title: new Text(namespace.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
))
);
)));
return this._loading
? Center(child: CircularProgressIndicator())
@ -49,9 +49,8 @@ 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,
);
}
@ -72,9 +71,9 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
return Scaffold(
return new Scaffold(
appBar: AppBar(
title: Text(_currentNamespace?.title ?? 'Vikunja'),
title: new Text(_currentNamespace?.title ?? 'Vikunja'),
actions: _currentNamespace == null
? null
: <Widget>[
@ -85,63 +84,59 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
MaterialPageRoute(
builder: (context) => NamespaceEditPage(
namespace: _currentNamespace,
)
)
)
),
))))
],
),
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),
),
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),
),
),
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 PlaceholderPage();
return new PlaceholderPage();
}
return NamespacePage(namespace: _namespaces[pos]);
return new NamespacePage(namespace: _namespaces[pos]);
}
_onSelectItem(int index) {
@ -154,10 +149,8 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addNamespace(name, context),
decoration: InputDecoration(
labelText: 'Namespace',
hintText: 'eg. Personal Namespace',
),
decoration: new InputDecoration(
labelText: 'Namespace', hintText: 'eg. Personal Namespace'),
));
}
@ -167,13 +160,11 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
.create(Namespace(id: null, title: name))
.then((_) {
_loadNamespaces();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
Scaffold.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,16 +1,12 @@
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;
@ -24,163 +20,109 @@ class ListPage extends StatefulWidget {
class _ListPageState extends State<ListPage> {
TaskList _list;
List<Task> _loadingTasks = [];
int _currentPage = 1;
bool _loading = true;
@override
void initState() {
_list = TaskList(
id: widget.taskList.id,
title: widget.taskList.title,
tasks: [],
);
Future.microtask(() => _loadList());
id: widget.taskList.id, title: widget.taskList.title, tasks: []);
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadList();
}
@override
Widget build(BuildContext context) {
final taskState = Provider.of<ListProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(_list.title),
title: new 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,
))))
],
),
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
body: !this._loading
? RefreshIndicator(
child: taskState.tasks.length > 0
? ListenableProvider.value(
value: taskState,
child: ListView.builder(
child: _list.tasks.length > 0
? ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
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.')),
children: ListTile.divideTiles(
context: context, tiles: _listTasks())
.toList(),
)
: 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,
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,
);
},
);
return TaskTile(task: task, loading: false);
}
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true,
onEdit: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TaskEditPage(
task: task,
),
),
),
);
}
Future<void> _loadList() async {
_loadTasksForPage(1);
}
void _loadTasksForPage(int page) {
Provider.of<ListProvider>(context, listen: false).loadTasks(
context: context,
listId: _list.id,
page: page,
);
Future<void> _loadList() {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((list) {
setState(() {
_loading = false;
_list = list;
});
});
}
_addItemDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: (title) => _addItem(title, context),
decoration: InputDecoration(
labelText: 'Task Name',
hintText: 'eg. Milk',
),
),
);
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addItem(name, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
}
_addItem(String title, BuildContext context) {
_addItem(String name, BuildContext context) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null,
title: title,
createdBy: globalState.currentUser,
done: false,
);
id: null, title: name, owner: globalState.currentUser, done: false);
setState(() => _loadingTasks.add(newTask));
Provider.of<ListProvider>(context, listen: false)
.addTask(
context: context,
newTask: newTask,
listId: _list.id,
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
globalState.taskService.add(_list.id, newTask).then((task) {
setState(() {
_list.tasks.add(task);
});
}).then((_) {
_loadList();
setState(() => _loadingTasks.remove(newTask));
Scaffold.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);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The list was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(

View File

@ -1,436 +0,0 @@
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,14 +3,12 @@ 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;
@ -28,7 +26,7 @@ class _NamespacePageState extends State<NamespacePage>
@override
void afterFirstLayout(BuildContext context) {
//_loadLists(); // NOTE: goes right after didChangeDependencies
_loadLists();
}
/////
@ -58,8 +56,11 @@ class _NamespacePageState extends State<NamespacePage>
color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
_removeList(ls).then((_) => ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("${ls.title} removed"))));
_removeList(ls).then((_) => Scaffold.of(
context)
.showSnackBar(SnackBar(
content:
Text("${ls.title} removed"))));
},
))).toList(),
)
@ -88,7 +89,6 @@ 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,15 +99,8 @@ class _NamespacePageState extends State<NamespacePage>
}
_openList(BuildContext context, TaskList list) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<ListProvider>(
create: (_) => new ListProvider(),
child: ListPage(
taskList: list,
),
),
// ListPage(taskList: list)
));
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ListPage(taskList: list)));
}
_addListDialog(BuildContext context) {
@ -127,7 +120,7 @@ class _NamespacePageState extends State<NamespacePage>
.then((_) {
setState(() {});
_loadLists();
ScaffoldMessenger.of(context).showSnackBar(
Scaffold.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);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The namespace was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: ScaffoldMessenger.of(context).hideCurrentSnackBar),
onPressed: Scaffold.of(context).hideCurrentSnackBar),
),
);
});

View File

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

View File

@ -24,7 +24,7 @@ class _LoginPageState extends State<LoginPage> {
padding: const EdgeInsets.all(16.0),
child: Builder(
builder: (BuildContext context) => Form(
autovalidateMode: AutovalidateMode.always,
autovalidate: true,
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>[
TextButton(
FlatButton(
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>[
TextButton(
FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],

View File

@ -1,6 +1,5 @@
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';
@ -40,7 +39,7 @@ var _tasks = {
1: Task(
id: 1,
title: 'Task 1',
createdBy: _users[1],
owner: _users[1],
updated: DateTime.now(),
created: DateTime.now(),
description: 'A descriptive task',
@ -122,8 +121,7 @@ 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();
}
@ -146,15 +144,6 @@ 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,8 +1,5 @@
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';
@ -29,10 +26,6 @@ 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 {
@ -40,21 +33,3 @@ 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

@ -1,88 +0,0 @@
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,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
/////////
// Colors
@ -19,15 +18,9 @@ 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

@ -1,11 +0,0 @@
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

@ -1,66 +0,0 @@
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,572 +1,252 @@
# 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.1.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.2"
version: "1.0.7+2"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.2"
version: "2.0.13"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.8.1"
version: "2.5.0-nullsafety.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
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"
version: "2.1.0-nullsafety.1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.0-nullsafety.3"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
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"
version: "1.2.0-nullsafety.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.0-nullsafety.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
version: "1.15.0-nullsafety.3"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
coverage:
dependency: transitive
description:
name: coverage
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "2.1.4"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
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"
version: "0.1.3"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
version: "1.2.0-nullsafety.1"
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.9.2"
version: "0.8.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.1"
version: "3.3.5"
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.13.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "0.12.0+3"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
version: "3.1.3"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
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"
version: "2.1.12"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10"
version: "0.12.10-nullsafety.1"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
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"
version: "1.3.0-nullsafety.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
version: "1.8.0-nullsafety.1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.0"
version: "1.8.0+1"
petitparser:
dependency: "direct main"
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
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"
version: "2.4.0"
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.1"
version: "1.8.0-nullsafety.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0"
version: "1.10.0-nullsafety.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
version: "2.1.0-nullsafety.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.1.0-nullsafety.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.17.10"
version: "1.2.0-nullsafety.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.2"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
version: "0.2.19-nullsafety.2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
version: "1.3.0-nullsafety.3"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
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"
version: "2.1.0-nullsafety.3"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
version: "3.6.1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
version: "2.2.0"
sdks:
dart: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.20.0 <2.0.0"

View File

@ -1,33 +1,23 @@
name: vikunja_app
description: Vikunja as Flutter cross platform app
version: 0.2.0+200099
version: 0.1.0
environment:
# TODO: update SDK >=2.12.0 (null-safety enabled)
sdk: ">=2.11.0 <3.0.0"
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
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
cupertino_icons: ^0.1.3
flutter_secure_storage: 3.3.5
http: 0.12.0+3
after_layout: ^1.0.7
dev_dependencies:
flutter_test:
sdk: flutter
version: any
test: any
flutter_launcher_icons: 0.9.2
flutter_launcher_icons: "^0.8.0"
flutter_icons:
image_path: "assets/vikunja_logo.png"

View File

@ -1,22 +0,0 @@
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

@ -1,76 +0,0 @@
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

@ -1,52 +0,0 @@
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));
});
}