Compare commits

...

51 Commits

Author SHA1 Message Date
HammyHavoc 47ae5ea8c7 Fixed a typo in title
:- )
2022-01-27 08:06:36 +00:00
JonasFranz fd33172250 Upgrade android embedding to v2 (#60)
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: vikunja/app#60
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: JonasFranz <email@jfdev.de>
Co-committed-by: JonasFranz <email@jfdev.de>
2021-01-18 13:20:32 +00:00
JonasFranz 5d0b424217 Fix badges (#59)
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: vikunja/app#59
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: JonasFranz <email@jfdev.de>
Co-committed-by: JonasFranz <email@jfdev.de>
2021-01-17 15:03:10 +00:00
Jonas Franz 09b15a249c
Re-enable dependencies for iOS build 2021-01-17 15:52:04 +01:00
Jonas Franz 7fa0835063
Remove depenedncy 2021-01-16 19:07:30 +01:00
Jonas Franz dbc3a77105
rbenv fix 2021-01-16 19:07:10 +01:00
Jonas Franz 50cb3318d4
Add rbenv 2021-01-16 19:01:36 +01:00
Jonas Franz f422b73414
Use local bundle path 2021-01-16 18:15:34 +01:00
Jonas Franz 89f790c38c
Use bundle install 2021-01-16 18:10:34 +01:00
JonasFranz 5ffa78c97c Fix iOS build (#58)
Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: vikunja/app#58
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: JonasFranz <email@jfdev.de>
Co-committed-by: JonasFranz <email@jfdev.de>
2021-01-10 20:41:44 +00:00
renovate 3adfe3c860 Update dependency gradle to v4.10.3 (#54)
Update dependency gradle to v4.10.3

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

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

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

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

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

Remove test branch from pipeline

Add push

Add beta dist

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

Add white logo in dark mode

Make button shadow dark

Format

Add dark mode

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

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

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

Fix CI

Add keychain pw

ensure keychain

Disable code signing while building

Fix CI

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

Add keychain password

Merge branch 'master' into feature/ios

Add compliance

Add secrets

Remove build app step

Use other version

Set Utf8

Fix CI

Fix CI

Fix beta deployment

Add deploy step

Fix CI

Fix CI

Fix ci

fix ci

fix ci

test ci

fix ci

Fix keychain

Fix keychain

Use custom keychain

Add security unlock

Add MATCH_PASSWORD

Add match

Add fastlane

Add ios pipeline to build

Add ios pipeline to build

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

Use correct import

Add sentry

Co-authored-by: Jonas Franz <info@jonasfranz.software>
Reviewed-on: vikunja/app#43
Reviewed-by: konrad <konrad@kola-entertainments.de>
2020-06-17 15:37:42 +00:00
kolaente 4adee85d62
Don't try to run tests in pipeline 2020-06-16 00:18:09 +02:00
kolaente 020cfa2bde
Format 2020-06-15 23:48:15 +02:00
kolaente 9973816dde
Fix all json fields being snake_case 2020-06-15 23:46:10 +02:00
kolaente 68d3ffd30d
Rename namespace name and task text to title 2020-06-15 23:42:12 +02:00
kolaente 46fc580b7b
Format 2020-04-27 17:09:33 +02:00
kolaente 361057aa9f
Fix date format 2020-04-27 17:02:55 +02:00
kolaente 14300266ef
Change release bucket 2020-03-01 22:56:05 +01:00
kolaente a2f53ce711
Use the same image for building and testing in ci 2020-01-15 23:38:02 +01:00
konrad 0f23c4d0f3 Make it build again (#38)
Fix parsing of user model if email is not present

Use user avatar hash instead of calculating it from the email

Format

Replace GravatarImageProvider

Set min sdk version to 19

Change target api version to 28

Limit drone pipeline execution to master or pr

Remove drone debug

Use username instead of id

Format

"Fix" clone permissions

Drone debug

Fix drone permissions with different flutter build docker image

Switch CI build image

Bump Gradle sdk version

Fix formatting

Update packages for support for androidX

Update gitignore

AndroidX

Make GravatarImageProvider work again

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/app#38
2020-01-12 12:59:28 +00:00
konrad 144f55af6d
Updated dockerimage to use the vikunja one for building apps 2019-04-25 19:35:52 +02:00
konrad 8278340242 Show a message if a list or namespace is empty (#29) 2019-03-18 17:05:32 +00:00
konrad 2121b831a0 Namespace edit (#36) 2019-03-18 17:00:34 +00:00
konrad 1e3518554b Logout (#35)
:C
2019-03-18 16:56:15 +00:00
konrad 9848c462f8 Fixed namespaces loading every time a widget was loaded (#34) 2019-03-18 15:30:54 +00:00
konrad 75f6608863 Refactor (#31) 2019-03-16 13:29:00 +00:00
konrad fa05e0e6a3 Fix the build and update drone (#33) 2019-03-15 22:14:37 +00:00
konrad 67e47b03cd Snackbars for all actions (#30) 2019-03-15 06:52:50 +00:00
konrad cc25bba0c7 Reload items after adding it (#28) 2019-03-14 21:27:13 +00:00
konrad 893e59b00e List edit design improvements (#27) 2019-03-14 21:25:57 +00:00
konrad ae197684ac Login page design improvements (#26) 2019-03-14 21:12:02 +00:00
konrad cb360f656f Theme update (#23) 2019-03-11 20:38:05 +00:00
konrad b904379af7 Added better error messages to login and register (#25) 2019-03-11 20:37:25 +00:00
konrad c285120034 List edit (#21) 2019-03-11 20:29:15 +00:00
konrad d30ea0f91c Cleanup pubspec (#24) 2019-03-10 21:41:55 +00:00
konrad 563fe1fc4d Fixed app not working with the newest api change which has multiple reminders (#19) 2018-12-03 21:26:00 +00:00
konrad 62d7261a63 Fix build (#18) 2018-11-05 15:43:30 +00:00
konrad 7755b9c812 Added formatting check to makefile and ci (#17) 2018-10-15 17:16:47 +00:00
61 changed files with 1959 additions and 918 deletions

View File

@ -1,57 +1,189 @@
kind: pipeline
type: docker
name: testing
workspace:
base: /app
base: /home/cirrus/app
clone:
git:
image: plugins/git
depth: 50
tags: true
depth: 50
pipeline:
test:
image: nathansamson/flutter-builder-docker:v0.6.0
pull: true
commands:
- make test
when:
event: [ push, tag, pull_request ]
build:
image: nathansamson/flutter-builder-docker:v0.6.0
pull: true
commands:
- flutter packages get
- make build-all
- mkdir apks
- mv build/app/outputs/apk/*/*/*.apk apks
when:
event: [ push, tag ]
# Push the releases to our pseudo-s3-bucket
release:
# Only run on prs or pushes to master
trigger:
branch:
include:
- master
event:
include:
- push
- pull_request
steps:
- name: build
image: cirrusci/flutter:stable
pull: true
commands:
- flutter packages get
- make format-check
- make build-debug
# Don't run tests until we have to not break the pipeline
# - name: test
# image: cirrusci/flutter:stable
# pull: true
# commands:
# - flutter packages get
# - make test
---
kind: pipeline
type: docker
name: release-latest
depends_on:
- testing
trigger:
branch:
- master
event:
- push
workspace:
base: /home/cirrus/app
clone:
depth: 50
steps:
# Because drone separates the pipelines, we have to add the build step to this pipeline. This is double code, we should change it at some point if possible.
- name: build
image: cirrusci/flutter:stable
pull: true
commands:
- flutter packages get
- make build-all
- mkdir apks
- mv build/app/outputs/apk/*/*/*.apk apks
# Push the releases to our pseudo-s3-bucket
- name: release
image: plugins/s3:1
pull: true
secrets: [ aws_access_key_id, aws_secret_access_key ]
bucket: vikunja-app
endpoint: https://storage.kolaente.de
path_style: true
strip_prefix: apks/
source: apks/*
target: /${DRONE_TAG##v}
when:
event: [ tag ]
settings:
bucket: vikunja-releases
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
strip_prefix: apks/
source: apks/*
target: /app/master
# Push the releases to our pseudo-s3-bucket
release:
---
kind: pipeline
type: docker
name: release-version
depends_on:
- testing
trigger:
event:
- tag
workspace:
base: /home/cirrus/app
clone:
depth: 50
steps:
# Because drone separates the pipelines, we have to add the build step to this pipeline. This is double code, we should change it at some point if possible.
- name: build
image: cirrusci/flutter:stable
pull: true
commands:
- flutter packages get
- make build-all
- mkdir apks
- mv build/app/outputs/apk/*/*/*.apk apks
# Push the releases to our pseudo-s3-bucket
- name: release
image: plugins/s3:1
pull: true
secrets: [ aws_access_key_id, aws_secret_access_key ]
bucket: vikunja-app
endpoint: https://storage.kolaente.de
path_style: true
strip_prefix: apks/
source: apks/*
target: /master
when:
event: [ push ]
branch: [ master ]
settings:
bucket: vikunja-releases
access_key:
from_secret: aws_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
strip_prefix: apks/
source: apks/*
target: /app/${DRONE_TAG##v}
---
kind: pipeline
type: exec
name: release-ios
trigger:
event:
- push
branch:
- master
platform:
os: darwin
arch: amd64
steps:
- name: build
commands:
- make build-ios
environment:
HOME: /Users/buildslave
- name: deploy
environment:
HOME: /Users/buildslave
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
FASTLANE_SKIP_UPDATE_CHECK: true
FASTLANE_HIDE_CHANGELOG: true
MATCH_PASSWORD:
from_secret: match_password
FASTLANE_PASSWORD:
from_secret: fastlane_password
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD:
from_secret: fastlane_apple_password
FASTLANE_SESSION:
from_secret: fastlane_session
KEYCHAIN_PASSWORD:
from_secret: keychain_password
CONTACT_EMAIL:
from_secret: contact_email
CONTACT_FIRST_NAME:
from_secret: contact_first_name
CONTACT_LAST_NAME:
from_secret: contact_last_name
CONTACT_PHONE:
from_secret: contact_phone
commands:
- eval "$(rbenv init -)"
- rbenv shell 2.5.0
- cd ios
- bundle config set --local path '.vendor/bundle'
- bundle install
- bundle exec fastlane ios signing
- bundle exec fastlane ios beta
depends_on:
- testing

8
.gitignore vendored
View File

@ -27,6 +27,8 @@
.pub-cache/
.pub/
build/
!pubspec.lock
.flutter-plugins-dependencies
# Android related
**/android/**/gradle-wrapper.jar
@ -68,4 +70,8 @@ build/
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/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

@ -1,30 +1,45 @@
GIT_LAST_COMMIT := $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
FLUTTER ?= flutter
ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG))-$(GIT_LAST_COMMIT)
ifneq ($(DRONE_BUILD_NUMBER),)
VERSION ?= $(DRONE_BUILD_NUMBER)
else
ifneq ($(DRONE_BRANCH),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH))-$(GIT_LAST_COMMIT)
else
VERSION ?= master-$(GIT_LAST_COMMIT)
endif
VERSION ?= 1
endif
.PHONY: test
test:
flutter test
$(FLUTTER) test
.PHONY: build-all
build-all: build-release build-debug build-profile
.PHONY: build-release
build-release:
flutter build apk --release --build-name=$(VERSION) --flavor main
$(FLUTTER) build apk --release --build-number=$(VERSION) --flavor main
.PHONY: build-debug
build-debug:
flutter build apk --debug --build-name=$(VERSION) --flavor unsigned
$(FLUTTER) build apk --debug --build-number=$(VERSION) --flavor unsigned
.PHONY: build-profile
build-profile:
flutter build apk --profile --build-name=$(VERSION) --flavor unsigned
$(FLUTTER) build apk --profile --build-number=$(VERSION) --flavor unsigned
.PHONY: build-ios
build-ios:
$(FLUTTER) build ios --release --build-number=$(VERSION) --no-codesign
.PHONY: format
format:
$(FLUTTER) format lib
.PHONY: format-check
format-check:
@diff=$$(flutter format -n lib); \
if [ -n "$$diff" ]; then \
echo "The following files are not formatted correctly:"; \
echo "$${diff}"; \
echo "Please run 'make format' and commit the result."; \
exit 1; \
fi;

View File

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

View File

@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 27
compileSdkVersion 28
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -37,14 +37,12 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.vikunja.app"
minSdkVersion 18
targetSdkVersion 27
minSdkVersion 19
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
flavorDimensions "deploy"

View File

@ -13,7 +13,6 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="Vikunja"
android:icon="@mipmap/ic_launcher">
<activity
@ -23,17 +22,13 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<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>
</application>
</manifest>

View File

@ -1,13 +1,5 @@
package io.vikunja.flutteringvikunja
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
class MainActivity(): FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
}
}
class MainActivity : FlutterActivity()

View File

@ -21,8 +21,6 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}

View File

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

View File

@ -0,0 +1 @@
include ':app'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

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

3
ios/Gemfile Normal file
View File

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

41
ios/Podfile Normal file
View File

@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@ -8,13 +8,8 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@ -29,8 +24,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -40,21 +33,21 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; };
1CD140BF817CCDA821C9152F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BD0C880424A0CB4300291E83 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
C102A622A93B95B5704BDD24 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CA78C9A9831542FDDB6FB31E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -62,8 +55,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
ACA854A11123D371B9168194 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -82,6 +73,8 @@
7CACC4C503C5D851EB73C215 /* Pods */ = {
isa = PBXGroup;
children = (
CA78C9A9831542FDDB6FB31E /* Pods-Runner.debug.xcconfig */,
1CD140BF817CCDA821C9152F /* Pods-Runner.release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
@ -89,10 +82,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
2D5378251FAA1A9400D5DBA9 /* flutter_assets */,
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
@ -122,6 +112,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
BD0C880424A0CB4300291E83 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@ -157,7 +148,6 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
85082CB7F9EE2F3E7985BDB9 /* [CP] Embed Pods Frameworks */,
7C22F5DE30AEBAB42040EB3F /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -181,6 +171,7 @@
CreatedOnToolsVersion = 7.3.1;
DevelopmentTeam = Z48VLBM2R7;
LastSwiftMigration = 0910;
ProvisioningStyle = Manual;
};
};
};
@ -189,6 +180,7 @@
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
English,
en,
Base,
);
@ -211,7 +203,6 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -249,22 +240,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
};
7C22F5DE30AEBAB42040EB3F /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
85082CB7F9EE2F3E7985BDB9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
@ -272,8 +248,8 @@
files = (
);
inputPaths = (
"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
"${PODS_ROOT}/../Flutter/Flutter.framework",
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
);
name = "[CP] Embed Pods Frameworks";
@ -283,7 +259,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
@ -336,7 +312,6 @@
/* Begin XCBuildConfiguration section */
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -390,7 +365,6 @@
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -443,6 +417,8 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = Z48VLBM2R7;
ENABLE_BITCODE = NO;
@ -458,6 +434,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = io.vikunja.flutteringVikunja;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match Development io.vikunja.flutteringVikunja 1592303885";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = On;
@ -472,6 +449,9 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = Z48VLBM2R7;
ENABLE_BITCODE = NO;
@ -487,6 +467,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = io.vikunja.flutteringVikunja;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.vikunja.flutteringVikunja 1592303767";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_SWIFT3_OBJC_INFERENCE = On;
SWIFT_VERSION = 4.0;

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
</dict>
</plist>

8
ios/fastlane/Appfile Normal file
View File

@ -0,0 +1,8 @@
app_identifier("io.vikunja.flutteringVikunja") # The bundle identifier of your app
apple_id("email@jfdev.de") # Your Apple email address
itc_team_id("117734679") # App Store Connect Team ID
team_id("Z48VLBM2R7") # Developer Portal Team ID
# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile

45
ios/fastlane/Fastfile Normal file
View File

@ -0,0 +1,45 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
match(type: "appstore", readonly: true)
gym
upload_to_testflight(
beta_app_feedback_email: "hello@vikunja.io",
beta_app_description: "Automated Vikunja App Build",
demo_account_required: true,
distribute_external: true,
groups: ["PublicBeta"],
changelog: "Automated Vikunja Build",
beta_app_review_info: {
contact_email: ENV["CONTACT_EMAIL"],
contact_first_name: ENV["CONTACT_FIRST_NAME"],
contact_last_name: ENV["CONTACT_LAST_NAME"],
contact_phone: ENV["CONTACT_PHONE"],
demo_account_name: "demo",
demo_account_password: "demo",
notes: "Please use https://try.vikunja.io as URL"
}
)
end
lane :signing do
match(type: "appstore", readonly: true)
match(type: "development", readonly: true)
end
end

13
ios/fastlane/Matchfile Normal file
View File

@ -0,0 +1,13 @@
git_url("git@git.jfdev.de:JonasFranzDEV/match.git")
storage_mode("git")
type("development") # The default type, can be: appstore, adhoc, enterprise or development
# app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
# username("user@fastlane.tools") # Your Apple Developer Portal username
# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options
# The docs are available on https://docs.fastlane.tools/actions/match

View File

@ -53,8 +53,15 @@ class Client {
dynamic _handleResponse(http.Response response) {
if (response.statusCode < 200 ||
response.statusCode > 400 ||
response.statusCode >= 400 ||
json == null) {
if (response.statusCode ~/ 100 == 4) {
Map<String, dynamic> error = _decoder.convert(response.body);
throw new InvalidRequestApiException(
response.statusCode,
response.request.url.toString(),
error["message"] ?? "Unknown Error");
}
throw new ApiException(
response.statusCode, response.request.url.toString());
}
@ -62,6 +69,17 @@ class Client {
}
}
class InvalidRequestApiException extends ApiException {
final String message;
InvalidRequestApiException(int errorCode, String path, this.message)
: super(errorCode, path);
@override
String toString() {
return this.message;
}
}
class ApiException implements Exception {
final int errorCode;
final String path;

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/service/services.dart';
class ListAPIService extends APIService implements ListService {
@ -40,7 +40,7 @@ class ListAPIService extends APIService implements ListService {
@override
Future<TaskList> update(TaskList tl) {
return client
.put('/lists/${tl.id}', body: tl.toJSON())
.post('/lists/${tl.id}', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
}
}

View File

@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
class ErrorDialog extends StatelessWidget {
final dynamic error;
ErrorDialog({this.error});
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Text(error.toString()),
actions: <Widget>[
FlatButton(
child: Text('Close'),
onPressed: () => Navigator.of(context).maybePop(),
)
],
);
}
}

View File

@ -1,9 +0,0 @@
import 'package:flutter/material.dart';
import 'package:crypto/crypto.dart';
class GravatarImageProvider extends NetworkImage {
GravatarImageProvider(String email)
: super("https://secure.gravatar.com/avatar/" +
md5.convert(email.trim().toLowerCase().codeUnits).toString());
}

View File

@ -41,16 +41,20 @@ class TaskTileState extends State<TaskTile> {
strokeWidth: 2.0,
)),
),
title: Text(_currentTask.text),
title: Text(_currentTask.title),
subtitle:
_currentTask.description == null || _currentTask.description.isEmpty
? null
: Text(_currentTask.description),
trailing: IconButton(icon: Icon(Icons.settings), onPressed: null),
trailing: IconButton(
icon: Icon(Icons.settings),
onPressed: () {
null; // TODO: implement edit task
}),
);
}
return CheckboxListTile(
title: Text(_currentTask.text),
title: Text(_currentTask.title),
controlAffinity: ListTileControlAffinity.leading,
value: _currentTask.done ?? false,
subtitle:
@ -79,7 +83,7 @@ class TaskTileState extends State<TaskTile> {
return VikunjaGlobal.of(context).taskService.update(Task(
id: task.id,
done: checked,
text: task.text,
title: task.title,
description: task.description,
owner: null,
));

View File

@ -1,106 +0,0 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/pages/list_page.dart';
class NamespaceFragment extends StatefulWidget {
final Namespace namespace;
NamespaceFragment({this.namespace})
: super(key: Key(namespace.id.toString()));
@override
_NamespaceFragmentState createState() => new _NamespaceFragmentState();
}
class _NamespaceFragmentState extends State<NamespaceFragment> {
List<TaskList> _lists = [];
bool _loading = true;
@override
Widget build(BuildContext context) {
return Scaffold(
body: !this._loading
? RefreshIndicator(
child: new ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context,
tiles: _lists.map((ls) => Dismissible(
key: Key(ls.id.toString()),
direction: DismissDirection.startToEnd,
child: ListTile(
title: new Text(ls.title),
onTap: () => _openList(context, ls),
trailing: Icon(Icons.arrow_right),
),
background: Container(
color: Colors.red,
child: const ListTile(
leading: Icon(Icons.delete,
color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
_removeList(ls).then((_) => Scaffold.of(context)
.showSnackBar(SnackBar(
content: Text("${ls.title} removed"))));
},
))).toList(),
),
onRefresh: _updateLists,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: FloatingActionButton(
onPressed: () => _addListDialog(), child: const Icon(Icons.add)),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateLists();
}
Future _removeList(TaskList list) {
return VikunjaGlobal.of(context)
.listService
.delete(list.id)
.then((_) => _updateLists());
}
Future<void> _updateLists() {
return VikunjaGlobal.of(context)
.listService
.getByNamespace(widget.namespace.id)
.then((lists) => setState(() {
this._lists = lists;
this._loading = false;
}));
}
_openList(BuildContext context, TaskList list) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ListPage(taskList: list)));
}
_addListDialog() {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: _addList,
decoration: new InputDecoration(
labelText: 'List Name', hintText: 'eg. Shopping List')),
);
}
_addList(String name) {
VikunjaGlobal.of(context)
.listService
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: []))
.then((_) => setState(() {}));
}
}

View File

@ -7,7 +7,6 @@ import 'package:vikunja_app/api/task_implementation.dart';
import 'package:vikunja_app/api/user_implementation.dart';
import 'package:vikunja_app/managers/user.dart';
import 'package:vikunja_app/models/user.dart';
import 'package:vikunja_app/service/mocked_services.dart';
import 'package:vikunja_app/service/services.dart';
class VikunjaGlobal extends StatefulWidget {
@ -34,12 +33,17 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
bool _loading = true;
User get currentUser => _currentUser;
Client get client => _client;
UserManager get userManager => new UserManager(_storage);
UserService newUserService(base) => new UserAPIService(Client(null, base));
NamespaceService get namespaceService => new NamespaceAPIService(client);
TaskService get taskService => new TaskAPIService(client);
ListService get listService => new ListAPIService(client);
@override
@ -73,6 +77,20 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
});
}
void logoutUser(BuildContext context) {
_storage.deleteAll().then((_) {
Navigator.pop(context);
setState(() {
_client = null;
_currentUser = null;
});
}).catchError((err) {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('An error occured while logging out!'),
));
});
}
void _loadCurrentUser() async {
var currentUser = await _storage.read(key: 'currentUser');
if (currentUser == null) {

View File

@ -1,10 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/pages/home_page.dart';
import 'package:vikunja_app/pages/login_page.dart';
import 'package:vikunja_app/style.dart';
import 'package:vikunja_app/pages/home.dart';
import 'package:vikunja_app/pages/user/login.dart';
import 'package:vikunja_app/theme/theme.dart';
void main() => runApp(VikunjaGlobal(
child: new VikunjaApp(home: HomePage()),
@ -14,12 +12,12 @@ class VikunjaApp extends StatelessWidget {
final Widget home;
const VikunjaApp({Key key, this.home}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Vikunja',
theme: buildVikunjaTheme(),
darkTheme: buildVikunjaDarkTheme(),
home: this.home,
);
}

42
lib/models/list.dart Normal file
View File

@ -0,0 +1,42 @@
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';
class TaskList {
final int id;
final String title, description;
final User owner;
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.fromJson(Map<String, dynamic> json)
: id = json['id'],
owner = User.fromJson(json['owner']),
description = json['description'],
title = json['title'],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();
toJSON() {
return {
"id": this.id,
"title": this.title,
"description": this.description,
"owner": this.owner?.toJSON(),
"created": this.created?.toIso8601String(),
"updated": this.updated?.toIso8601String(),
};
}
}

View File

@ -4,29 +4,29 @@ import 'package:meta/meta.dart';
class Namespace {
final int id;
final DateTime created, updated;
final String name, description;
final String title, description;
final User owner;
Namespace(
{@required this.id,
this.created,
this.updated,
@required this.name,
@required this.title,
this.description,
this.owner});
Namespace.fromJson(Map<String, dynamic> json)
: name = json['name'],
: title = json['title'],
description = json['description'],
id = json['id'],
created = DateTime.fromMillisecondsSinceEpoch(json['created']),
updated = DateTime.fromMillisecondsSinceEpoch(json['updated']),
created = DateTime.parse(json['created']),
updated = DateTime.parse(json['updated']),
owner = User.fromJson(json['owner']);
toJSON() => {
"created": created?.millisecondsSinceEpoch,
"updated": updated?.millisecondsSinceEpoch,
"name": name,
"created": created?.toIso8601String(),
"updated": updated?.toIso8601String(),
"title": title,
"owner": owner?.toJSON(),
"description": description
};

View File

@ -3,8 +3,9 @@ import 'package:meta/meta.dart';
class Task {
final int id;
final DateTime created, updated, reminder, due;
final String text, description;
final DateTime created, updated, due;
final List<DateTime> reminders;
final String title, description;
final bool done;
final User owner;
@ -12,71 +13,37 @@ class Task {
{@required this.id,
this.created,
this.updated,
this.reminder,
this.reminders,
this.due,
@required this.text,
@required this.title,
this.description,
@required this.done,
@required this.owner});
Task.fromJson(Map<String, dynamic> json)
: id = json['id'],
updated = DateTime.fromMillisecondsSinceEpoch(json['updated']),
created = DateTime.fromMillisecondsSinceEpoch(json['created']),
reminder = DateTime.fromMillisecondsSinceEpoch(json['reminderDate']),
due = DateTime.fromMillisecondsSinceEpoch(json['dueDate']),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
reminders = (json['reminder_dates'] as List<dynamic>)
?.map((r) => DateTime.parse(r))
?.toList(),
due =
json['due_date'] != null ? DateTime.parse(json['due_date']) : null,
description = json['description'],
text = json['text'],
title = json['title'],
done = json['done'],
owner = User.fromJson(json['createdBy']);
owner = User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'updated': updated?.millisecondsSinceEpoch,
'created': created?.millisecondsSinceEpoch,
'reminderDate': reminder?.millisecondsSinceEpoch,
'dueDate': due?.millisecondsSinceEpoch,
'updated': updated?.toIso8601String(),
'created': created?.toIso8601String(),
'reminder_dates':
reminders?.map((date) => date.toIso8601String())?.toList(),
'due_date': due?.toIso8601String(),
'description': description,
'text': text,
'title': title,
'done': done ?? false,
'createdBy': owner?.toJSON()
'created_by': owner?.toJSON()
};
}
class TaskList {
final int id;
final String title, description;
final User owner;
final DateTime created, updated;
final List<Task> tasks;
TaskList(
{@required this.id,
@required this.title,
this.description,
this.owner,
this.created,
this.updated,
@required this.tasks});
TaskList.fromJson(Map<String, dynamic> json)
: id = json['id'],
owner = User.fromJson(json['owner']),
description = json['description'],
title = json['title'],
updated = DateTime.fromMillisecondsSinceEpoch(json['updated']),
created = DateTime.fromMillisecondsSinceEpoch(json['created']),
tasks = (json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();
toJSON() {
return {
"created": this.created?.millisecondsSinceEpoch,
"updated": this.updated?.millisecondsSinceEpoch,
"id": this.id,
"title": this.title,
"owner": this.owner?.toJSON()
};
}
}

View File

@ -1,4 +1,5 @@
import 'package:meta/meta.dart';
import 'package:flutter/cupertino.dart';
import 'package:vikunja_app/global.dart';
class User {
final int id;
@ -7,10 +8,17 @@ class User {
User(this.id, this.email, this.username);
User.fromJson(Map<String, dynamic> json)
: id = json['id'],
email = json['email'],
email = json.containsKey('email') ? json['email'] : '',
username = json['username'];
toJSON() => {"id": this.id, "email": this.email, "username": this.username};
String avatarUrl(BuildContext context) {
return VikunjaGlobal.of(context).client.base +
"/" +
this.username +
"/avatar";
}
}
class UserTokenPair {

178
lib/pages/home.dart Normal file
View File

@ -0,0 +1,178 @@
import 'dart:async';
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/pages/namespace/namespace.dart';
import 'package:vikunja_app/pages/namespace/namespace_edit.dart';
import 'package:vikunja_app/pages/placeholder.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => new HomePageState();
}
class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
List<Namespace> _namespaces = [];
Namespace get _currentNamespace =>
_selectedDrawerIndex >= 0 && _selectedDrawerIndex < _namespaces.length
? _namespaces[_selectedDrawerIndex]
: null;
int _selectedDrawerIndex = -1;
bool _loading = true;
bool _showUserDetails = false;
@override
void afterFirstLayout(BuildContext context) {
_loadNamespaces();
}
Widget _namespacesWidget() {
List<Widget> namespacesList = <Widget>[];
_namespaces
.asMap()
.forEach((i, namespace) => namespacesList.add(new ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
return this._loading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
child: ListView(
padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context, tiles: namespacesList)
.toList()),
onRefresh: _loadNamespaces,
);
}
Widget _userDetailsWidget(BuildContext context) {
return ListView(padding: EdgeInsets.zero, children: <Widget>[
ListTile(
title: Text('Logout'),
leading: Icon(Icons.exit_to_app),
onTap: () {
VikunjaGlobal.of(context).logoutUser(context);
},
),
]);
}
@override
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
return new Scaffold(
appBar: AppBar(
title: new Text(_currentNamespace?.title ?? 'Vikunja'),
actions: _currentNamespace == null
? null
: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NamespaceEditPage(
namespace: _currentNamespace,
))))
],
),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail:
currentUser?.email == null ? null : Text(currentUser.email),
accountName:
currentUser?.username == null ? null : Text(currentUser.username),
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
});
},
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: NetworkImage(currentUser.avatarUrl(context)),
),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(
Theme.of(context).primaryColor, BlendMode.multiply)),
),
),
new Builder(
builder: (BuildContext context) => Expanded(
child: _showUserDetails
? _userDetailsWidget(context)
: _namespacesWidget())),
new Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
builder: (context) => ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespaceDialog(context),
),
),
),
])),
body: _getDrawerItemWidget(_selectedDrawerIndex),
);
}
_getDrawerItemWidget(int pos) {
if (pos == -1) {
return new PlaceholderPage();
}
return new NamespacePage(namespace: _namespaces[pos]);
}
_onSelectItem(int index) {
setState(() => _selectedDrawerIndex = index);
Navigator.of(context).pop();
}
_addNamespaceDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addNamespace(name, context),
decoration: new InputDecoration(
labelText: 'Namespace', hintText: 'eg. Personal Namespace'),
));
}
_addNamespace(String name, BuildContext context) {
VikunjaGlobal.of(context)
.namespaceService
.create(Namespace(id: null, title: name))
.then((_) {
_loadNamespaces();
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The namespace was created successfully!'),
));
}).catchError((error) => showDialog(
context: context, builder: (context) => ErrorDialog(error: error)));
}
Future<void> _loadNamespaces() {
return VikunjaGlobal.of(context).namespaceService.getAll().then((result) {
setState(() {
_loading = false;
_namespaces = result;
});
});
}
}

View File

@ -1,126 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/GravatarImage.dart';
import 'package:vikunja_app/fragments/namespace.dart';
import 'package:vikunja_app/fragments/placeholder.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => new HomePageState();
}
class HomePageState extends State<HomePage> {
List<Namespace> _namespaces = [];
Namespace get _currentNamespace =>
_selectedDrawerIndex >= 0 && _selectedDrawerIndex < _namespaces.length
? _namespaces[_selectedDrawerIndex]
: null;
int _selectedDrawerIndex = -1;
bool _loading = true;
_getDrawerItemWidget(int pos) {
if (pos == -1) {
return new PlaceholderFragment();
}
return new NamespaceFragment(namespace: _namespaces[pos]);
}
_onSelectItem(int index) {
setState(() => _selectedDrawerIndex = index);
Navigator.of(context).pop();
}
_addNamespaceDialog() {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: _addNamespace,
decoration: new InputDecoration(
labelText: 'Namespace', hintText: 'eg. Personal Namespace'),
));
}
_addNamespace(String name) {
VikunjaGlobal.of(context)
.namespaceService
.create(Namespace(id: null, name: name))
.then((_) => _updateNamespaces());
}
Future<void> _updateNamespaces() {
return VikunjaGlobal.of(context).namespaceService.getAll().then((result) {
setState(() {
_loading = false;
_namespaces = result;
});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateNamespaces();
}
@override
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
List<Widget> drawerOptions = <Widget>[];
_namespaces
.asMap()
.forEach((i, namespace) => drawerOptions.add(new ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace.name),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
return new Scaffold(
appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vikunja')),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail: currentUser == null ? null : Text(currentUser.email),
accountName: currentUser == null ? null : Text(currentUser.username),
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: GravatarImageProvider(currentUser.username)),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(
Theme.of(context).primaryColor, BlendMode.multiply)),
),
),
new Expanded(
child: this._loading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
child: ListView(
padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context, tiles: drawerOptions)
.toList()),
onRefresh: _updateNamespaces,
)),
new Align(
alignment: FractionalOffset.bottomCenter,
child: new ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespaceDialog(),
),
),
])),
body: _getDrawerItemWidget(_selectedDrawerIndex),
);
}
}

128
lib/pages/list/list.dart Normal file
View File

@ -0,0 +1,128 @@
import 'dart:async';
import 'package:flutter/material.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';
class ListPage extends StatefulWidget {
final TaskList taskList;
ListPage({this.taskList}) : super(key: Key(taskList.id.toString()));
@override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
TaskList _list;
List<Task> _loadingTasks = [];
bool _loading = true;
@override
void initState() {
_list = TaskList(
id: widget.taskList.id, title: widget.taskList.title, tasks: []);
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(_list.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
))))
],
),
body: !this._loading
? RefreshIndicator(
child: _list.tasks.length > 0
? ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
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);
}
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true,
);
}
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: (name) => _addItem(name, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
}
_addItem(String name, BuildContext context) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null, title: name, owner: globalState.currentUser, done: false);
setState(() => _loadingTasks.add(newTask));
globalState.taskService.add(_list.id, newTask).then((task) {
setState(() {
_list.tasks.add(task);
});
}).then((_) {
_loadList();
setState(() => _loadingTasks.remove(newTask));
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'),
));
});
}
}

View File

@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
class ListEditPage extends StatefulWidget {
final TaskList list;
ListEditPage({this.list}) : super(key: Key(list.toString()));
@override
State<StatefulWidget> createState() => _ListEditPageState();
}
class _ListEditPageState extends State<ListEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String _title, _description;
@override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('Edit List'),
),
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.list.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.list.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(),
),
),
),
Builder(
builder: (context) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_saveList(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),
))),
]),
),
),
),
);
}
_saveList(BuildContext context) async {
setState(() => _loading = true);
// FIXME: is there a way we can update the list without creating a new list object?
// aka updating the existing list we got from context (setters?)
TaskList updatedList =
TaskList(id: widget.list.id, title: _title, description: _description);
VikunjaGlobal.of(context).listService.update(updatedList).then((_) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The list was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: Scaffold.of(context).hideCurrentSnackBar),
),
);
});
}
}

View File

@ -1,114 +0,0 @@
import 'dart:async';
import 'package:flutter/material.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/task.dart';
class ListPage extends StatefulWidget {
final TaskList taskList;
ListPage({this.taskList}) : super(key: Key(taskList.id.toString()));
@override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
TaskList _items;
List<Task> _loadingTasks = [];
bool _loading = true;
@override
void initState() {
_items = TaskList(
id: widget.taskList.id, title: widget.taskList.title, tasks: []);
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(_items.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => {/* TODO add edit list functionality */},
)
],
),
body: !this._loading
? RefreshIndicator(
child: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children:
ListTile.divideTiles(context: context, tiles: _listTasks())
.toList(),
),
onRefresh: _updateList,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: FloatingActionButton(
onPressed: () => _addItemDialog(), child: Icon(Icons.add)),
);
}
List<Widget> _listTasks() {
var tasks = (_items?.tasks?.map(_buildTile) ?? []).toList();
tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
}
TaskTile _buildTile(Task task) {
return TaskTile(task: task, loading: false);
}
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true,
);
}
Future<void> _updateList() {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((tasks) {
setState(() {
_loading = false;
_items = tasks;
});
});
}
_addItemDialog() {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: _addItem,
decoration: new InputDecoration(
labelText: 'List Item', hintText: 'eg. Milk')));
}
_addItem(String name) {
var globalState = VikunjaGlobal.of(context);
var newTask =
Task(id: null, text: name, owner: globalState.currentUser, done: false);
setState(() => _loadingTasks.add(newTask));
globalState.taskService.add(_items.id, newTask).then((task) {
setState(() {
_items.tasks.add(task);
});
}).then((_) => _updateList()
.then((_) => setState(() => _loadingTasks.remove(newTask))));
}
}

View File

@ -1,129 +0,0 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/pages/register_page.dart';
import 'package:vikunja_app/utils/validator.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
String _server, _username, _password;
bool _loading = false;
@override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('Login to Vikunja'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
top: false,
bottom: false,
child: Form(
autovalidate: true,
key: _formKey,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Image(
image: AssetImage('assets/vikunja_logo.png'),
height: 128.0,
semanticLabel: 'Vikunja Logo',
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
onSaved: (serverAddress) => _server = serverAddress,
validator: (address) {
return isUrl(address) ? null : 'Invalid URL';
},
decoration: new InputDecoration(
labelText: 'Server Address'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
onSaved: (username) => _username = username,
decoration:
new InputDecoration(labelText: 'Username'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
onSaved: (password) => _password = password,
decoration:
new InputDecoration(labelText: 'Password'),
obscureText: true,
),
),
Builder(
builder: (context) => ButtonTheme(
height: _loading ? 55.0 : 36.0,
child: RaisedButton(
onPressed: !_loading
? () {
if (_formKey.currentState
.validate()) {
Form.of(context).save();
_loginUser(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: Text('Login'),
))),
Builder(
builder: (context) => ButtonTheme(
height: _loading ? 55.0 : 36.0,
child: RaisedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
RegisterPage())),
child: _loading
? CircularProgressIndicator()
: Text('Register'),
))),
],
)),
),
));
}
_loginUser(BuildContext context) async {
setState(() => _loading = true);
try {
var vGlobal = VikunjaGlobal.of(context);
var newUser =
await vGlobal.newUserService(_server).login(_username, _password);
vGlobal.changeUser(newUser.user, token: newUser.token, base: _server);
} catch (ex) {
showDialog(
context: context,
builder: (context) => new AlertDialog(
title:
const Text('Login failed! Please check you credentials.'),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text('CLOSE'))
],
));
} finally {
setState(() {
_loading = false;
});
}
}
}

View File

@ -0,0 +1,130 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.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';
class NamespacePage extends StatefulWidget {
final Namespace namespace;
NamespacePage({this.namespace}) : super(key: Key(namespace.id.toString()));
@override
_NamespacePageState createState() => new _NamespacePageState();
}
class _NamespacePageState extends State<NamespacePage>
with AfterLayoutMixin<NamespacePage> {
List<TaskList> _lists = [];
bool _loading = true;
@override
void afterFirstLayout(BuildContext context) {
_loadLists();
}
/////
// This essentially shows the lists.
@override
Widget build(BuildContext context) {
return Scaffold(
body: !this._loading
? RefreshIndicator(
child: _lists.length > 0
? new ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context,
tiles: _lists.map((ls) => Dismissible(
key: Key(ls.id.toString()),
direction: DismissDirection.startToEnd,
child: ListTile(
title: new Text(ls.title),
onTap: () => _openList(context, ls),
trailing: Icon(Icons.arrow_right),
),
background: Container(
color: Colors.red,
child: const ListTile(
leading: Icon(Icons.delete,
color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
_removeList(ls).then((_) => Scaffold.of(
context)
.showSnackBar(SnackBar(
content:
Text("${ls.title} removed"))));
},
))).toList(),
)
: Center(child: Text('This namespace is empty.')),
onRefresh: _loadLists,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _addListDialog(context),
child: const Icon(Icons.add))),
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadLists();
}
Future _removeList(TaskList list) {
return VikunjaGlobal.of(context)
.listService
.delete(list.id)
.then((_) => _loadLists());
}
Future<void> _loadLists() {
return VikunjaGlobal.of(context)
.listService
.getByNamespace(widget.namespace.id)
.then((lists) => setState(() {
this._lists = lists;
this._loading = false;
}));
}
_openList(BuildContext context, TaskList list) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ListPage(taskList: list)));
}
_addListDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addList(name, context),
decoration: new InputDecoration(
labelText: 'List Name', hintText: 'eg. Shopping List')),
);
}
_addList(String name, BuildContext context) {
VikunjaGlobal.of(context)
.listService
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: []))
.then((_) {
setState(() {});
_loadLists();
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('The list was successfully created!'),
),
);
});
}
}

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
class NamespaceEditPage extends StatefulWidget {
final Namespace namespace;
NamespaceEditPage({this.namespace}) : super(key: Key(namespace.toString()));
@override
State<StatefulWidget> createState() => _NamespaceEditPageState();
}
class _NamespaceEditPageState extends State<NamespaceEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String _name, _description;
@override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('Edit Namespace'),
),
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.namespace.title,
onSaved: (name) => _name = name,
validator: (name) {
if (name.length < 3 || name.length > 250) {
return 'The name needs to have between 3 and 250 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.namespace.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(),
),
),
),
Builder(
builder: (context) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_saveNamespace(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),
))),
]),
),
),
),
);
}
_saveNamespace(BuildContext context) async {
setState(() => _loading = true);
// FIXME: is there a way we can update the namespace without creating a new namespace object?
// aka updating the existing namespace we got from context (setters?)
Namespace updatedNamespace = Namespace(
id: widget.namespace.id,
title: _name,
description: _description,
owner: widget.namespace.owner);
VikunjaGlobal.of(context)
.namespaceService
.update(updatedNamespace)
.then((_) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The namespace was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: Scaffold.of(context).hideCurrentSnackBar),
),
);
});
}
}

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
class PlaceholderFragment extends StatelessWidget {
class PlaceholderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Container(
@ -15,7 +15,7 @@ class PlaceholderFragment extends StatelessWidget {
style: Theme.of(context).textTheme.headline,
),
),
new Text('Please select a namespace by clicking the ☰ icon.',
new Text('Please select a namespace by tapping the ☰ icon.',
style: Theme.of(context).textTheme.subhead),
],
));

View File

@ -1,152 +0,0 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/utils/validator.dart';
class RegisterPage extends StatefulWidget {
@override
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final passwordController = TextEditingController();
String _server, _username, _email, _password;
bool _loading = false;
@override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('Register to Vikunja'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
top: false,
bottom: false,
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Image(
image: AssetImage('assets/vikunja_logo.png'),
height: 128.0,
semanticLabel: 'Vikunja Logo',
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: TextFormField(
onSaved: (serverAddress) => _server = serverAddress,
validator: (address) {
return isUrl(address) ? null : 'Invalid URL';
},
decoration: new InputDecoration(
labelText: 'Server Address'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
onSaved: (username) => _username = username.trim(),
validator: (username) {
return username.trim().isNotEmpty ? null : 'Please specify a username';
},
decoration:
new InputDecoration(labelText: 'Username'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
onSaved: (email) => _email = email,
validator: (email) {
return isEmail(email)
? null
: 'Email adress is invalid';
},
decoration:
new InputDecoration(labelText: 'Email Address'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: passwordController,
onSaved: (password) => _password = password,
validator: (password) {
return password.length >= 8 ? null : 'Please use at least 8 characters';
},
decoration:
new InputDecoration(labelText: 'Password'),
obscureText: true,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
validator: (password) {
return passwordController.text == password
? null
: 'Passwords don\'t match.';
},
decoration: new InputDecoration(
labelText: 'Repeat Password'),
obscureText: true,
),
),
Builder(
builder: (context) => ButtonTheme(
height: _loading ? 55.0 : 36.0,
child: RaisedButton(
onPressed: !_loading
? () {
if (_formKey.currentState
.validate()) {
Form.of(context).save();
_registerUser(context);
} else {
print("awhat");
}
}
: null,
child: _loading
? CircularProgressIndicator()
: Text('Register'),
))),
],
)),
),
));
}
_registerUser(BuildContext context) async {
setState(() => _loading = true);
try {
var vGlobal = VikunjaGlobal.of(context);
var newUserLoggedIn = await vGlobal
.newUserService(_server)
.register(_username, _email, _password);
vGlobal.changeUser(newUserLoggedIn.user,
token: newUserLoggedIn.token, base: _server);
} catch (ex) {
showDialog(
context: context,
builder: (context) => new AlertDialog(
title: const Text(
'Registration failed! Please check your server url and credentials.'),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text('CLOSE'))
],
));
} finally {
setState(() {
_loading = false;
});
}
}
}

136
lib/pages/user/login.dart Normal file
View File

@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/pages/user/register.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
import 'package:vikunja_app/theme/constants.dart';
import 'package:vikunja_app/utils/validator.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
String _server, _username, _password;
bool _loading = false;
@override
Widget build(BuildContext ctx) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Builder(
builder: (BuildContext context) => Form(
autovalidate: true,
key: _formKey,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 30),
child: Image(
image: Theme.of(context).brightness == Brightness.dark
? AssetImage('assets/vikunja_logo_full_white.png')
: AssetImage('assets/vikunja_logo_full.png'),
height: 85.0,
semanticLabel: 'Vikunja Logo',
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
enabled: !_loading,
onSaved: (serverAddress) => _server = serverAddress,
validator: (address) {
return isUrl(address) ? null : 'Invalid URL';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Server Address'),
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
enabled: !_loading,
onSaved: (username) => _username = username,
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username'),
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
enabled: !_loading,
onSaved: (password) => _password = password,
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password'),
obscureText: true,
),
),
Builder(
builder: (context) => FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_loginUser(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Login'),
)),
Builder(
builder: (context) => FancyButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RegisterPage())),
child: VikunjaButtonText('Register'),
)),
],
),
),
),
),
),
),
);
}
_loginUser(BuildContext context) async {
setState(() => _loading = true);
try {
var vGlobal = VikunjaGlobal.of(context);
var newUser =
await vGlobal.newUserService(_server).login(_username, _password);
vGlobal.changeUser(newUser.user, token: newUser.token, base: _server);
} catch (ex) {
showDialog(
context: context,
builder: (context) => new AlertDialog(
title: Text(
'Login failed! Please check your server url and credentials. ' +
ex.toString()),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],
));
} finally {
setState(() {
_loading = false;
});
}
}
}

View File

@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
import 'package:vikunja_app/theme/constants.dart';
import 'package:vikunja_app/utils/validator.dart';
class RegisterPage extends StatefulWidget {
@override
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final passwordController = TextEditingController();
String _server, _username, _email, _password;
bool _loading = false;
@override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('Register'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
top: false,
bottom: false,
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
onSaved: (serverAddress) => _server = serverAddress,
validator: (address) {
return isUrl(address) ? null : 'Invalid URL';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Server Address'),
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
onSaved: (username) => _username = username.trim(),
validator: (username) {
return username.trim().isNotEmpty
? null
: 'Please specify a username';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username'),
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
onSaved: (email) => _email = email,
validator: (email) {
return isEmail(email)
? null
: 'Email adress is invalid';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email Address'),
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
controller: passwordController,
onSaved: (password) => _password = password,
validator: (password) {
return password.length >= 8
? null
: 'Please use at least 8 characters';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password'),
obscureText: true,
),
),
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
validator: (password) {
return passwordController.text == password
? null
: 'Passwords don\'t match.';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
labelText: 'Repeat Password'),
obscureText: true,
),
),
Builder(
builder: (context) => FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_registerUser(context);
} else {
print("awhat");
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Register'),
)),
],
)),
),
));
}
_registerUser(BuildContext context) async {
setState(() => _loading = true);
try {
var vGlobal = VikunjaGlobal.of(context);
var newUserLoggedIn = await vGlobal
.newUserService(_server)
.register(_username, _email, _password);
vGlobal.changeUser(newUserLoggedIn.user,
token: newUserLoggedIn.token, base: _server);
} catch (ex) {
showDialog(
context: context,
builder: (context) => new AlertDialog(
title: Text(
'Registration failed! Please check your server url and credentials. ' +
ex.toString()),
actions: <Widget>[
FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'))
],
));
} finally {
setState(() {
_loading = false;
});
}
}
}

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';
@ -11,7 +12,7 @@ var _users = {1: User(1, 'test@testuser.org', 'test1')};
var _namespaces = {
1: Namespace(
id: 1,
name: 'Test 1',
title: 'Test 1',
created: DateTime.now(),
updated: DateTime.now(),
description: 'A namespace for testing purposes',
@ -37,7 +38,7 @@ var _lists = {
var _tasks = {
1: Task(
id: 1,
text: 'Task 1',
title: 'Task 1',
owner: _users[1],
updated: DateTime.now(),
created: DateTime.now(),

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';

View File

@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
const vBlue = Color(0xFF455486);
const vBlueLight = Color(0xFF7480b7);
const vBlueDark = Color(0xFF152c5a);
const vBlack = Color(0xFFFFFFFF);
ThemeData buildVikunjaTheme() {
var base = ThemeData.light();
return base.copyWith(
primaryColor: vBlue,
primaryColorLight: vBlueLight,
primaryColorDark: vBlueDark,
textTheme: base.textTheme.copyWith(
headline: base.textTheme.headline.copyWith(
fontFamily: 'Quicksand',
fontSize: 72.0,
),
title: base.textTheme.title.copyWith(
fontFamily: 'Quicksand',
),
));
}

45
lib/theme/button.dart Normal file
View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/theme/constants.dart';
class FancyButton extends StatelessWidget {
final double width;
final double height;
final Function onPressed;
final Widget child;
const FancyButton({
Key key,
@required this.child,
this.width = double.infinity,
this.height = 35,
this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: vStandardVerticalPadding,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Theme.of(context).brightness == Brightness.dark
? vButtonShadowDark
: vButtonShadow,
offset: Offset(-5, 5),
blurRadius: 10,
),
]),
child: Material(
borderRadius: BorderRadius.circular(3),
color: vButtonColor,
child: InkWell(
onTap: onPressed,
child: Center(
child: child,
)),
),
));
}
}

19
lib/theme/buttonText.dart Normal file
View File

@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/theme/constants.dart';
class VikunjaButtonText extends StatelessWidget {
final String text;
const VikunjaButtonText(
this.text, {
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(
text,
style: TextStyle(color: vButtonTextColor, fontWeight: FontWeight.w600),
);
}
}

26
lib/theme/constants.dart Normal file
View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
/////////
// Colors
//////
const vBlue = Color(0xFF7F23FF);
const vBlueLight = Color(0xFF7480b7);
const vBlueDark = Color(0xFF152c5a);
const vPrimary = Color(0xFF0c86ff);
const vPrimaryDark = Color(0xFF1A73E8);
const vRed = Color(0xFFff4136);
const vOrange = Color(0xFFff851b);
const vWhite = Color(0xFFffffff);
const vGreen = Color(0xFF00CE6E);
const vButtonColor = vPrimary;
const vButtonTextColor = vWhite;
const vButtonShadowDark = Color(0xFF0b2a4a);
const vButtonShadow = Color(0xFFb2d9ff);
///////////
// 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);

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class FancyAppBar extends StatelessWidget {
final String title;
final double barHeight = 80.0;
FancyAppBar(this.title);
@override
Widget build(BuildContext context) {
return new Container(
height: barHeight,
width: double.infinity,
decoration: new BoxDecoration(color: Colors.blue),
child: new Padding(
padding: EdgeInsets.symmetric(vertical: 38, horizontal: 10),
child: new Text(title,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontFamily: 'Quicksand',
fontSize: 21)),
),
);
}
}
/*
Usage:
return new Scaffold(
body: new Column(
children: <Widget>[
new FancyAppBar('Login to Vikunja'),
],
),
);
*/

42
lib/theme/theme.dart Normal file
View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/theme/constants.dart';
ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light());
ThemeData buildVikunjaDarkTheme() {
ThemeData base = _buildVikunjaTheme(ThemeData.dark());
return base.copyWith(
accentColor: vWhite,
);
}
ThemeData _buildVikunjaTheme(ThemeData base) {
return base.copyWith(
errorColor: vRed,
primaryColor: vPrimaryDark,
primaryColorLight: vPrimary,
primaryColorDark: vBlueDark,
buttonTheme: base.buttonTheme.copyWith(
buttonColor: vPrimary,
textTheme: ButtonTextTheme.normal,
colorScheme: base.buttonTheme.colorScheme.copyWith(
// Why does this not work?
onSurface: vWhite,
onSecondary: vWhite,
background: vBlue,
),
),
textTheme: base.textTheme.copyWith(
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
),
),
);
}

252
pubspec.lock Normal file
View File

@ -0,0 +1,252 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
after_layout:
dependency: "direct main"
description:
name: after_layout
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.7+2"
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.13"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
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-nullsafety.1"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.4"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.5"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: "direct main"
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0+3"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.12"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0+1"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
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-nullsafety.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
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-nullsafety.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
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-nullsafety.3"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.1"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
sdks:
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.20.0 <2.0.0"

View File

@ -1,25 +1,23 @@
name: vikunja_app
description: Vikunja as Flutter cross platform app
version: 0.0.1
version: 0.1.0
environment:
sdk: ">=2.0.0-dev.63.0 <3.0.0"
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
flutter_secure_storage: 3.1.1
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
flutter_launcher_icons: "^0.6.1"
flutter_launcher_icons: "^0.8.0"
flutter_icons:
image_path: "assets/vikunja_logo.png"
@ -29,54 +27,14 @@ flutter_icons:
adaptive_icon_background: "#FF455486"
adaptive_icon_foreground: "assets/vikunja_logo_adaptive.png"
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.io/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.io/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.io/custom-fonts/#from-packages
assets:
- assets/graphics/background.jpg
- assets/graphics/hypnotize.png
- assets/vikunja_logo.png
- assets/vikunja_logo_full.png
- assets/vikunja_logo_full_white.png
fonts:
- family: Quicksand
fonts:
- asset: assets/fonts/quicksand-v7-latin-regular.ttf

6
renovate.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}