├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .npmrc
├── .stylelintrc.json
├── App_Resources
├── Android
│ ├── app.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ ├── drawable-hdpi
│ │ ├── background.png
│ │ ├── icon.png
│ │ └── logo.png
│ │ ├── drawable-ldpi
│ │ ├── background.png
│ │ ├── icon.png
│ │ └── logo.png
│ │ ├── drawable-mdpi
│ │ ├── background.png
│ │ ├── icon.png
│ │ └── logo.png
│ │ ├── drawable-nodpi
│ │ └── splash_screen.xml
│ │ ├── drawable-xhdpi
│ │ ├── background.png
│ │ ├── icon.png
│ │ └── logo.png
│ │ ├── drawable-xxhdpi
│ │ ├── background.png
│ │ ├── icon.png
│ │ └── logo.png
│ │ ├── drawable-xxxhdpi
│ │ ├── background.png
│ │ ├── icon.png
│ │ └── logo.png
│ │ ├── values-v21
│ │ ├── colors.xml
│ │ └── styles.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
└── iOS
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon-1024.png
│ │ ├── icon-29.png
│ │ ├── icon-29@2x.png
│ │ ├── icon-29@3x.png
│ │ ├── icon-40.png
│ │ ├── icon-40@2x.png
│ │ ├── icon-40@3x.png
│ │ ├── icon-60@2x.png
│ │ ├── icon-60@3x.png
│ │ ├── icon-76.png
│ │ ├── icon-76@2x.png
│ │ └── icon-83.5@2x.png
│ ├── Contents.json
│ ├── LaunchImage.launchimage
│ │ ├── Contents.json
│ │ ├── Default-1125h.png
│ │ ├── Default-568h@2x.png
│ │ ├── Default-667h@2x.png
│ │ ├── Default-736h@3x.png
│ │ ├── Default-Landscape-X.png
│ │ ├── Default-Landscape.png
│ │ ├── Default-Landscape@2x.png
│ │ ├── Default-Landscape@3x.png
│ │ ├── Default-Portrait.png
│ │ ├── Default-Portrait@2x.png
│ │ ├── Default.png
│ │ └── Default@2x.png
│ ├── LaunchScreen.AspectFill.imageset
│ │ ├── Contents.json
│ │ ├── LaunchScreen-AspectFill.png
│ │ └── LaunchScreen-AspectFill@2x.png
│ └── LaunchScreen.Center.imageset
│ │ ├── Contents.json
│ │ ├── LaunchScreen-Center.png
│ │ └── LaunchScreen-Center@2x.png
│ ├── Info.plist
│ ├── LaunchScreen.storyboard
│ └── build.xcconfig
├── CHANGELOG.md
├── LICENSE
├── README.md
├── angular.json
├── artwork
├── feature.png
├── feature.xcf
├── icon-circle.png
├── icon-rounded.png
├── icon.png
└── icon.xcf
├── metadata
├── en-US
│ └── images
│ │ ├── featureGraphic.png
│ │ └── phoneScreenshots
│ │ ├── screenshot_add_task.png
│ │ └── screenshot_tasks.png
└── todo.txt
├── nativescript.config.ts
├── package-lock.json
├── package.json
├── plugins
├── .gitkeep
├── README.md
├── nativescript-foss-sidedrawer-2.0.0.tgz
└── nativescript-imagepicker-7.1.0.tgz
├── reference.d.ts
├── scripts
├── bump-version.js
├── check-changelog.js
├── copy-fonts.js
├── ios-unsigned.sh
└── web-backend.js
├── src
├── _app-common.scss
├── _app-variables.scss
├── _app-web.scss
├── app.android.scss
├── app.ios.scss
├── app.tns.scss
├── app
│ ├── about
│ │ ├── about.component.html
│ │ ├── about.component.scss
│ │ ├── about.component.spec.ts
│ │ ├── about.component.tns.html
│ │ ├── about.component.tns.scss
│ │ └── about.component.ts
│ ├── app-routing.module.tns.ts
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.tns.html
│ ├── app.component.tns.scss
│ ├── app.component.ts
│ ├── app.constants.ts
│ ├── app.module.tns.ts
│ ├── app.module.ts
│ ├── app.routes.ts
│ ├── nav
│ │ ├── nav-modal.component.scss
│ │ ├── nav-modal.component.tns.html
│ │ ├── nav-modal.component.tns.ts
│ │ ├── nav.ts
│ │ ├── navigation.component.html
│ │ ├── navigation.component.scss
│ │ ├── navigation.component.spec.ts
│ │ ├── navigation.component.ts
│ │ ├── sidedrawer.service.tns.ts
│ │ └── sidedrawer.service.ts
│ ├── plaintext
│ │ ├── plaintext.component.html
│ │ ├── plaintext.component.scss
│ │ ├── plaintext.component.tns.html
│ │ ├── plaintext.component.tns.scss
│ │ └── plaintext.component.ts
│ ├── settings
│ │ ├── settings.component.html
│ │ ├── settings.component.scss
│ │ ├── settings.component.spec.ts
│ │ ├── settings.component.tns.html
│ │ ├── settings.component.tns.scss
│ │ └── settings.component.ts
│ ├── shared
│ │ ├── dialog.component.ts
│ │ ├── dialog.service.tns.ts
│ │ ├── dialog.service.ts
│ │ ├── file.guard.ts
│ │ ├── file.service.spec.ts
│ │ ├── file.service.tns.ts
│ │ ├── file.service.ts
│ │ ├── helpers
│ │ │ ├── date-picker.tns.ts
│ │ │ ├── date-picker.ts
│ │ │ ├── file-picker.tns.ts
│ │ │ ├── file-picker.ts
│ │ │ ├── input.tns.ts
│ │ │ ├── input.ts
│ │ │ ├── page.tns.ts
│ │ │ ├── page.ts
│ │ │ ├── platform.tns.ts
│ │ │ ├── platform.ts
│ │ │ ├── pullrefresh.tns.ts
│ │ │ ├── pullrefresh.ts
│ │ │ ├── storage.tns.ts
│ │ │ ├── storage.ts
│ │ │ ├── toast.tns.ts
│ │ │ ├── toast.ts
│ │ │ ├── version.tns.ts
│ │ │ └── version.ts
│ │ ├── misc.spec.ts
│ │ ├── misc.ts
│ │ ├── router.service.spec.ts
│ │ ├── router.service.tns.ts
│ │ ├── router.service.ts
│ │ ├── settings.service.spec.ts
│ │ ├── settings.service.ts
│ │ ├── settings.ts
│ │ ├── task.spec.ts
│ │ ├── task.ts
│ │ ├── todo-file.service.spec.ts
│ │ ├── todo-file.service.ts
│ │ └── validators.ts
│ ├── tag-list
│ │ ├── tag-list.component.html
│ │ ├── tag-list.component.scss
│ │ ├── tag-list.component.spec.ts
│ │ ├── tag-list.component.tns.html
│ │ ├── tag-list.component.tns.scss
│ │ └── tag-list.component.ts
│ ├── task-form
│ │ ├── task-form-autocomplete.component.html
│ │ ├── task-form-autocomplete.component.scss
│ │ ├── task-form-autocomplete.component.spec.ts
│ │ ├── task-form-autocomplete.component.tns.html
│ │ ├── task-form-autocomplete.component.tns.scss
│ │ ├── task-form-autocomplete.component.ts
│ │ ├── task-form.component.html
│ │ ├── task-form.component.scss
│ │ ├── task-form.component.spec.ts
│ │ ├── task-form.component.tns.html
│ │ ├── task-form.component.tns.scss
│ │ └── task-form.component.ts
│ ├── task-list
│ │ ├── task-list.component.html
│ │ ├── task-list.component.scss
│ │ ├── task-list.component.spec.ts
│ │ ├── task-list.component.tns.html
│ │ ├── task-list.component.tns.scss
│ │ └── task-list.component.ts
│ └── welcome
│ │ ├── welcome.component.html
│ │ ├── welcome.component.scss
│ │ ├── welcome.component.spec.ts
│ │ ├── welcome.component.tns.html
│ │ ├── welcome.component.tns.scss
│ │ ├── welcome.component.ts
│ │ └── welcome.guard.ts
├── assets
│ └── .gitkeep
├── browserslist
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── karma.conf.js
├── main.tns.ts
├── main.ts
├── polyfills.ts
├── styles.scss
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── tslint.json
├── tsconfig.json
├── tsconfig.tns.json
├── tslint.json
├── webpack-tns.config.js
└── webpack-web.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 | charset = utf-8
8 | indent_style = space
9 | indent_size = 4
10 |
11 | [*.html]
12 | indent_size = 2
13 |
14 | [*.gradle]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": [
4 | "@typescript-eslint",
5 | "jasmine"
6 | ],
7 | "env": {
8 | "browser": true,
9 | "es6": true,
10 | "jasmine": true
11 | },
12 | "extends": [
13 | "standard",
14 | "plugin:@typescript-eslint/recommended"
15 | ],
16 | "parserOptions": {
17 | "ecmaVersion": 2018,
18 | "sourceType": "module"
19 | },
20 | "rules": {
21 | "indent": [
22 | "error",
23 | 4
24 | ],
25 | "comma-dangle": [
26 | "error",
27 | "always-multiline"
28 | ],
29 | "no-useless-constructor": "off",
30 | "space-before-function-paren": [
31 | "error",
32 | {
33 | "named": "never"
34 | }
35 | ],
36 | "padded-blocks": "off",
37 | "object-curly-spacing": "off",
38 | "@typescript-eslint/explicit-function-return-type": "off",
39 | "@typescript-eslint/no-explicit-any": "off",
40 | "@typescript-eslint/no-empty-function": "off"
41 | },
42 | "globals": {
43 | "android": true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # NativeScript
2 | hooks/
3 | node_modules/
4 | platforms/
5 | src/fonts/
6 | webpack.config.js
7 |
8 | # General
9 | Vagrantfile
10 | vagrant/
11 | .vagrant/
12 | .vagrant_ssh_config
13 | todo.txt
14 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | message="Misc: Version %s"
2 | save-prefix=~
3 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-sass-guidelines",
3 | "rules": {
4 | "indentation": 4,
5 | "color-hex-case": null,
6 | "color-hex-length": null,
7 | "selector-max-id": 1,
8 | "max-nesting-depth": 2,
9 | "property-no-unknown": [
10 | true,
11 | {
12 | "ignoreProperties": [
13 | "link-color",
14 | "horizontal-align",
15 | "separator-color",
16 | "ripple-color"
17 | ]
18 | }
19 | ],
20 | "selector-pseudo-element-no-unknown": [
21 | true,
22 | {
23 | "ignorePseudoElements": [
24 | "ng-deep"
25 | ]
26 | }
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/App_Resources/Android/app.gradle:
--------------------------------------------------------------------------------
1 | // https://stackoverflow.com/a/39040755/1868395
2 | def packageJsonFile = file("./../../../package.json")
3 | def packageJson = new groovy.json.JsonSlurper().parseText(packageJsonFile.text)
4 | def version = packageJson.version
5 | def (major, minor, patch) = version.split(/\./).collect{it.toInteger()}
6 | def versionInt = (major * 1000000) + (minor * 10000) + (patch * 100)
7 |
8 | android {
9 | defaultConfig {
10 | versionCode versionInt
11 | versionName "$version"
12 | minSdkVersion = 17
13 |
14 | generatedDensities = []
15 | }
16 |
17 | // Read signing config from gradle properties instead of CLI args
18 | signingConfigs {
19 | release {
20 | if (project.hasProperty("release")) {
21 | if (project.hasProperty("MINDSTREAM_RELEASE_STORE_FILE") &&
22 | project.hasProperty("MINDSTREAM_RELEASE_STORE_PASSWORD") &&
23 | project.hasProperty("MINDSTREAM_RELEASE_KEY_ALIAS") &&
24 | project.hasProperty("MINDSTREAM_RELEASE_KEY_PASSWORD")) {
25 |
26 | storeFile file(MINDSTREAM_RELEASE_STORE_FILE)
27 | storePassword MINDSTREAM_RELEASE_STORE_PASSWORD
28 | keyAlias MINDSTREAM_RELEASE_KEY_ALIAS
29 | keyPassword MINDSTREAM_RELEASE_KEY_PASSWORD
30 | }
31 | }
32 | }
33 | }
34 |
35 | aaptOptions {
36 | additionalParameters "--no-version-vectors"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
30 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-hdpi/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-hdpi/background.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-hdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-hdpi/icon.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-hdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-hdpi/logo.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-ldpi/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-ldpi/background.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-ldpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-ldpi/icon.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-ldpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-ldpi/logo.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-mdpi/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-mdpi/background.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-mdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-mdpi/icon.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-mdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-mdpi/logo.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-nodpi/splash_screen.xml:
--------------------------------------------------------------------------------
1 |
2 | -
3 |
4 |
5 | -
6 |
7 |
8 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xhdpi/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xhdpi/background.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xhdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xhdpi/icon.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xhdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xhdpi/logo.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xxhdpi/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xxhdpi/background.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xxhdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xxhdpi/icon.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xxhdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xxhdpi/logo.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xxxhdpi/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xxxhdpi/background.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xxxhdpi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xxxhdpi/icon.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/drawable-xxxhdpi/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/Android/src/main/res/drawable-xxxhdpi/logo.png
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/values-v21/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #643900
4 | #FBFCF0
5 |
6 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
15 |
16 |
17 |
20 |
21 |
24 |
25 |
28 |
29 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #F5F5F5
4 | #757575
5 | #643900
6 | #272734
7 |
8 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Mindstream
4 | Mindstream
5 |
6 |
--------------------------------------------------------------------------------
/App_Resources/Android/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
21 |
22 |
23 |
31 |
32 |
34 |
35 |
36 |
42 |
43 |
45 |
46 |
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "29x29",
5 | "idiom" : "iphone",
6 | "filename" : "icon-29.png",
7 | "scale" : "1x"
8 | },
9 | {
10 | "size" : "29x29",
11 | "idiom" : "iphone",
12 | "filename" : "icon-29@2x.png",
13 | "scale" : "2x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "icon-29@3x.png",
19 | "scale" : "3x"
20 | },
21 | {
22 | "size" : "40x40",
23 | "idiom" : "iphone",
24 | "filename" : "icon-40@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "icon-40@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "60x60",
35 | "idiom" : "iphone",
36 | "filename" : "icon-60@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "icon-60@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "29x29",
47 | "idiom" : "ipad",
48 | "filename" : "icon-29.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "size" : "29x29",
53 | "idiom" : "ipad",
54 | "filename" : "icon-29@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "40x40",
59 | "idiom" : "ipad",
60 | "filename" : "icon-40.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "40x40",
65 | "idiom" : "ipad",
66 | "filename" : "icon-40@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "76x76",
71 | "idiom" : "ipad",
72 | "filename" : "icon-76.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "76x76",
77 | "idiom" : "ipad",
78 | "filename" : "icon-76@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "83.5x83.5",
83 | "idiom" : "ipad",
84 | "filename" : "icon-83.5@2x.png",
85 | "scale" : "2x"
86 | },
87 | {
88 | "size" : "1024x1024",
89 | "idiom" : "ios-marketing",
90 | "filename" : "icon-1024.png",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-1024.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "extent" : "full-screen",
5 | "idiom" : "iphone",
6 | "subtype" : "2436h",
7 | "filename" : "Default-1125h.png",
8 | "minimum-system-version" : "11.0",
9 | "orientation" : "portrait",
10 | "scale" : "3x"
11 | },
12 | {
13 | "orientation" : "landscape",
14 | "idiom" : "iphone",
15 | "extent" : "full-screen",
16 | "filename" : "Default-Landscape-X.png",
17 | "minimum-system-version" : "11.0",
18 | "subtype" : "2436h",
19 | "scale" : "3x"
20 | },
21 | {
22 | "extent" : "full-screen",
23 | "idiom" : "iphone",
24 | "subtype" : "736h",
25 | "filename" : "Default-736h@3x.png",
26 | "minimum-system-version" : "8.0",
27 | "orientation" : "portrait",
28 | "scale" : "3x"
29 | },
30 | {
31 | "extent" : "full-screen",
32 | "idiom" : "iphone",
33 | "subtype" : "736h",
34 | "filename" : "Default-Landscape@3x.png",
35 | "minimum-system-version" : "8.0",
36 | "orientation" : "landscape",
37 | "scale" : "3x"
38 | },
39 | {
40 | "extent" : "full-screen",
41 | "idiom" : "iphone",
42 | "subtype" : "667h",
43 | "filename" : "Default-667h@2x.png",
44 | "minimum-system-version" : "8.0",
45 | "orientation" : "portrait",
46 | "scale" : "2x"
47 | },
48 | {
49 | "orientation" : "portrait",
50 | "idiom" : "iphone",
51 | "filename" : "Default@2x.png",
52 | "extent" : "full-screen",
53 | "minimum-system-version" : "7.0",
54 | "scale" : "2x"
55 | },
56 | {
57 | "extent" : "full-screen",
58 | "idiom" : "iphone",
59 | "subtype" : "retina4",
60 | "filename" : "Default-568h@2x.png",
61 | "minimum-system-version" : "7.0",
62 | "orientation" : "portrait",
63 | "scale" : "2x"
64 | },
65 | {
66 | "orientation" : "portrait",
67 | "idiom" : "ipad",
68 | "filename" : "Default-Portrait.png",
69 | "extent" : "full-screen",
70 | "minimum-system-version" : "7.0",
71 | "scale" : "1x"
72 | },
73 | {
74 | "orientation" : "landscape",
75 | "idiom" : "ipad",
76 | "filename" : "Default-Landscape.png",
77 | "extent" : "full-screen",
78 | "minimum-system-version" : "7.0",
79 | "scale" : "1x"
80 | },
81 | {
82 | "orientation" : "portrait",
83 | "idiom" : "ipad",
84 | "filename" : "Default-Portrait@2x.png",
85 | "extent" : "full-screen",
86 | "minimum-system-version" : "7.0",
87 | "scale" : "2x"
88 | },
89 | {
90 | "orientation" : "landscape",
91 | "idiom" : "ipad",
92 | "filename" : "Default-Landscape@2x.png",
93 | "extent" : "full-screen",
94 | "minimum-system-version" : "7.0",
95 | "scale" : "2x"
96 | },
97 | {
98 | "orientation" : "portrait",
99 | "idiom" : "iphone",
100 | "filename" : "Default.png",
101 | "extent" : "full-screen",
102 | "scale" : "1x"
103 | },
104 | {
105 | "orientation" : "portrait",
106 | "idiom" : "iphone",
107 | "filename" : "Default@2x.png",
108 | "extent" : "full-screen",
109 | "scale" : "2x"
110 | },
111 | {
112 | "orientation" : "portrait",
113 | "idiom" : "iphone",
114 | "filename" : "Default-568h@2x.png",
115 | "extent" : "full-screen",
116 | "subtype" : "retina4",
117 | "scale" : "2x"
118 | },
119 | {
120 | "orientation" : "portrait",
121 | "idiom" : "ipad",
122 | "extent" : "to-status-bar",
123 | "scale" : "1x"
124 | },
125 | {
126 | "orientation" : "portrait",
127 | "idiom" : "ipad",
128 | "filename" : "Default-Portrait.png",
129 | "extent" : "full-screen",
130 | "scale" : "1x"
131 | },
132 | {
133 | "orientation" : "landscape",
134 | "idiom" : "ipad",
135 | "extent" : "to-status-bar",
136 | "scale" : "1x"
137 | },
138 | {
139 | "orientation" : "landscape",
140 | "idiom" : "ipad",
141 | "filename" : "Default-Landscape.png",
142 | "extent" : "full-screen",
143 | "scale" : "1x"
144 | },
145 | {
146 | "orientation" : "portrait",
147 | "idiom" : "ipad",
148 | "extent" : "to-status-bar",
149 | "scale" : "2x"
150 | },
151 | {
152 | "orientation" : "portrait",
153 | "idiom" : "ipad",
154 | "filename" : "Default-Portrait@2x.png",
155 | "extent" : "full-screen",
156 | "scale" : "2x"
157 | },
158 | {
159 | "orientation" : "landscape",
160 | "idiom" : "ipad",
161 | "extent" : "to-status-bar",
162 | "scale" : "2x"
163 | },
164 | {
165 | "orientation" : "landscape",
166 | "idiom" : "ipad",
167 | "filename" : "Default-Landscape@2x.png",
168 | "extent" : "full-screen",
169 | "scale" : "2x"
170 | }
171 | ],
172 | "info" : {
173 | "version" : 1,
174 | "author" : "xcode"
175 | }
176 | }
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-1125h.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-1125h.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-568h@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-667h@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-736h@3x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape-X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape-X.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Landscape@3x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default-Portrait@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchImage.launchimage/Default@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchScreen-AspectFill.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchScreen-AspectFill@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchScreen.AspectFill.imageset/LaunchScreen-AspectFill@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchScreen-Center.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchScreen-Center@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/App_Resources/iOS/Assets.xcassets/LaunchScreen.Center.imageset/LaunchScreen-Center@2x.png
--------------------------------------------------------------------------------
/App_Resources/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | ${PRODUCT_NAME}
9 | CFBundleExecutable
10 | ${EXECUTABLE_NAME}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.6.1
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.6.1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiresFullScreen
28 |
29 | UIRequiredDeviceCapabilities
30 |
31 | armv7
32 |
33 | UISupportedInterfaceOrientations
34 |
35 | UIInterfaceOrientationPortrait
36 | UIInterfaceOrientationLandscapeLeft
37 | UIInterfaceOrientationLandscapeRight
38 |
39 | UISupportedInterfaceOrientations~ipad
40 |
41 | UIInterfaceOrientationPortrait
42 | UIInterfaceOrientationPortraitUpsideDown
43 | UIInterfaceOrientationLandscapeLeft
44 | UIInterfaceOrientationLandscapeRight
45 |
46 |
47 | UIFileSharingEnabled
48 |
49 | LSSupportsOpeningDocumentsInPlace
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/App_Resources/iOS/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/App_Resources/iOS/build.xcconfig:
--------------------------------------------------------------------------------
1 | // You can add custom settings here
2 | // for example you can uncomment the following line to force distribution code signing
3 | // CODE_SIGN_IDENTITY = iPhone Distribution
4 | // To build for device with Xcode 8 you need to specify your development team. More info: https://developer.apple.com/library/prerelease/content/releasenotes/DeveloperTools/RN-Xcode/Introduction.html
5 | // DEVELOPMENT_TEAM = YOUR_TEAM_ID;
6 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
7 | ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unreleased
4 |
5 | - Mobile, Web: Added support for yearly tasks.
6 | - Web: Showing alert if server connection fails.
7 |
8 | ## 1.6.1
9 |
10 | - Mobile, Web: Fix task duplication during completion of recurrent task.
11 | - Mobile: Upgraded datepicker plugin.
12 | - Mobile, Web: Upgraded jsTodoTxt to version 0.10.0.
13 | - Web: Fixed styles for chrome browser.
14 | - Web: Changed hotkey for adding new task from {Alt+A} to {A}.
15 | - Web: Fixed "Payload too large" error occuring when todo.txt file is large.
16 |
17 | ## 1.6.0
18 |
19 | - Android: Created todo.txt file in user-accessible location.
20 | - Android: Made text on plaintext page selectable.
21 |
22 | ## 1.5.0
23 |
24 | - Mobile, Web: Upgraded to NativeScript 7.0.
25 | - Mobile, Web: Added backslash trick to usage tips.
26 | - Mobile, Web: Redirecting to welcome page if file is not set.
27 | - Web: Enabled autocomplete in todo file path field.
28 |
29 | ## 1.4.0
30 |
31 | - Mobile, Web: Allowed more recurrence options (#6 by @oli-ver).
32 | - Mobile: Enabled horizontal scrolling on plain text page.
33 |
34 | ## 1.3.2
35 |
36 | - Android: Fixed filesystem access errors on Android 10.
37 |
38 | ## 1.3.1
39 |
40 | - Web: Added recurrence picker to task form.
41 | - Android: Fixed file-picking from 'SDCARD' location.
42 | - Android: Showing error message when file picker can't retrieve the file path.
43 | - iOS: Fixed wrong background color on 'About' page.
44 |
45 | ## 1.3.0
46 |
47 | - Mobile, Web: Added usage tips to 'About' page.
48 | - Mobile, Web: Disabled auto-focusing on text field when editing a task.
49 | - Mobile: Notifying of file access errors with a toast.
50 | - iOS: Fixed wrong behaviour of menu button on 'About' page.
51 |
52 | ## 1.2.0
53 |
54 | - Mobile, Web: Renamed 'Projects' page to 'Tags', added contexts to it.
55 | - Mobile, Web: Not showing empty task when creating new todo.txt file.
56 | - Mobile, Web: Sorting tasks by project first, then by context.
57 | - Mobile, Web: Added support for 'color' extension.
58 | - Mobile, Web: Fixed parsing of task text on update.
59 | - Mobile, Web: Filling 'contexts' field on task form from current filter.
60 | - Mobile, Web: Not showing an empty task if todo.txt file ends with a newline.
61 |
62 | ## 1.1.0
63 |
64 | - Mobile, Web: Added 'About' page.
65 | - Mobile, Web: Added shortcut for priority D to task form.
66 | - Mobile, Web: Enabled autocompletion for contexts.
67 | - Mobile, Web: Trimming whitespace from projects and contexts in task form.
68 | - Mobile, Web: Showing current due date in calendar when editing task.
69 | - Mobile, Web: Fixed task postponement bug.
70 | - Mobile: Added 'tomorrow' shortcut to due date field.
71 | - Mobile: Enabled switching between tasks and projects with swipe gesture.
72 | - Mobile: Moved 'save' button to action bar at task form page.
73 | - Android: Moving cursor to the end of line when removing item from autocomplete field.
74 | - Web: Added Alt+A hotkey for adding new task.
75 | - Web: Allowed to submit task form by pressing 'Enter'.
76 | - Web: Allowed to close task form by pressing 'Esc'.
77 | - Web: Showing app version in navigation bar.
78 | - Web: Fixed switching with 'Tab' between projects and contexts in task form.
79 | - Web: Allowed to postpone task by ctrl-clicking on checkbox.
80 |
81 | ## 1.0.0
82 |
83 | - Mobile, Web: Added priority selection buttons to task form.
84 | - Mobile, Web: Added support for contexts.
85 | - Mobile, Web: Fixed project search bug at task form.
86 | - Mobile, Web: Fixed bug where task IDs become wrong.
87 | - Mobile, Web: Fixed 'hidden' extension.
88 | - Web: Created plaintext page.
89 | - Web: Added task sorting dialog.
90 | - Web: Added task menu.
91 | - Web: Allowed text selection in task list.
92 | - Web: Enabled project suggestions in task form.
93 | - Web: Allowed to select project with keyboard.
94 | - Web: Fixed error on app reloading.
95 | - Web: Added 'tomorrow' button to task form.
96 | - Web: Added datepicker to task form.
97 |
98 | ## 0.6.0
99 |
100 | - Mobile: Enabled automatic reloading of todo.txt file on changes.
101 | - Mobile: Added ability to sort tasks by due date and priority.
102 | - IOS: Fixed incorrect size of action bar icons.
103 | - Web: Created task list, task form and project list.
104 |
105 | ## 0.5.0
106 |
107 | - Allowed to add mutiple projects to task.
108 | - Fixed bug in task removal.
109 | - Added support for hidden tasks.
110 | - Prevented opening of task form after tapping on link in Android app.
111 | - Enabled long-tap text selection on Android.
112 | - Changed project list design.
113 | - Show task menu after long press on checkbox.
114 |
115 | ## 0.4.2
116 |
117 | - Fixed bug where monthly tasks were not completed properly.
118 | - Improved app performance.
119 |
120 | ## 0.4.1
121 |
122 | - Removed camera and microphone permission requirements.
123 |
124 | ## 0.4.0
125 |
126 | - Added welcome screen.
127 | - Use current project filter to prefill project field in task form.
128 | - Change app icon on iOS.
129 | - Render task text as markdown.
130 | - Enabled coloring of due date tag, similar to priorities.
131 | - Changed datepicker type to calendar on Android.
132 |
133 | ## 0.3.0
134 |
135 | - Initial release.
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mindstream
2 |
3 | [](https://github.com/xuhcc/mindstream/releases)
4 | [](https://github.com/xuhcc/mindstream/blob/HEAD/LICENSE)
5 |
6 | Task management app that uses [todo.txt](http://todotxt.org/) format.
7 |
8 | ## Features
9 |
10 | - Projects, contexts, priorities, due dates.
11 | - Recurring tasks.
12 | - Filtering by project, context or due date.
13 | - Sorting by due date or priority.
14 | - Markdown support.
15 |
16 | ### Supported todo.txt extensions
17 |
18 | - Tasks with due date: `due:2019-01-01`.
19 | - Recurrent tasks: `rec:1d` (`d` = day, `w` = week, `m` = month, `y` = year).
20 | - Colored tasks: `color:#e9dce5`.
21 | - Hidden tasks: `h:1`.
22 |
23 | See [example](metadata/todo.txt).
24 |
25 |
26 |
27 | ## Changelog
28 |
29 | See [CHANGELOG](CHANGELOG.md).
30 |
31 | ## Usage
32 |
33 | ### Android
34 |
35 | Latest APK can be downloaded from [releases page](https://github.com/xuhcc/mindstream/releases).
36 |
37 | ### iOS (unmaintained)
38 |
39 | Build unsigned iOS package from source (only on MacOS):
40 |
41 | ```
42 | npm install
43 | npm run ios-unsigned
44 | ```
45 |
46 | ### Web
47 |
48 | Build from source:
49 |
50 | ```
51 | npm install
52 | npm run web-release
53 | ```
54 |
55 | Run the web app (it will be available at `http://localhost:8080/`):
56 |
57 | ```
58 | cd platforms/web/
59 | PORT=8080 node index.js
60 | ```
61 |
62 | ## Development
63 |
64 | Prerequisites:
65 |
66 | * Node.js & NPM
67 | * Note: Known to be broken on Node.js v20.18.0, but works on v12.22.6. YMMV. Use `nvm` to switch.
68 | * [NativeScript CLI](https://v7.docs.nativescript.org/angular/start/quick-setup#step-2-install-the-nativescript-cli) 7.0
69 |
70 | Install required packages:
71 |
72 | ```
73 | npm install
74 | ```
75 |
76 | ### Mobile
77 |
78 | Run in Android emulator:
79 |
80 | ```
81 | npm run android
82 | ```
83 |
84 | Run in iOS emulator:
85 |
86 | ```
87 | npm run ios
88 | ```
89 |
90 | ### Web
91 |
92 | Run in browser:
93 |
94 | ```
95 | npm start
96 | ```
97 |
98 | ### Testing
99 |
100 | ```
101 | npm run lint
102 | npm run test
103 | ```
104 |
105 | ## License
106 |
107 | [GPL v3](LICENSE)
108 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "cli": {
6 | "defaultCollection": "@nativescript/schematics"
7 | },
8 | "projects": {
9 | "mindstream": {
10 | "root": "",
11 | "sourceRoot": "src",
12 | "projectType": "application",
13 | "prefix": "ms",
14 | "schematics": {
15 | "@nativescript/schematics:component": {
16 | "styleext": "scss"
17 | }
18 | },
19 | "architect": {
20 | "build": {
21 | "builder": "@angular-builders/custom-webpack:browser",
22 | "options": {
23 | "customWebpackConfig": {
24 | "path": "./webpack-web.config.js"
25 | },
26 | "outputPath": "platforms/web/static",
27 | "index": "src/index.html",
28 | "main": "src/main.ts",
29 | "polyfills": "src/polyfills.ts",
30 | "tsConfig": "src/tsconfig.app.json",
31 | "assets": [
32 | "src/favicon.ico",
33 | "src/assets"
34 | ],
35 | "styles": [
36 | "src/styles.scss"
37 | ],
38 | "scripts": []
39 | },
40 | "configurations": {
41 | "production": {
42 | "fileReplacements": [
43 | {
44 | "replace": "src/environments/environment.ts",
45 | "with": "src/environments/environment.prod.ts"
46 | }
47 | ],
48 | "optimization": true,
49 | "outputHashing": "all",
50 | "sourceMap": false,
51 | "extractCss": true,
52 | "namedChunks": false,
53 | "aot": true,
54 | "extractLicenses": true,
55 | "vendorChunk": false,
56 | "buildOptimizer": true
57 | }
58 | }
59 | },
60 | "serve": {
61 | "builder": "@angular-builders/custom-webpack:dev-server",
62 | "options": {
63 | "browserTarget": "mindstream:build"
64 | },
65 | "configurations": {
66 | "production": {
67 | "browserTarget": "mindstream:build:production"
68 | }
69 | }
70 | },
71 | "extract-i18n": {
72 | "builder": "@angular-devkit/build-angular:extract-i18n",
73 | "options": {
74 | "browserTarget": "mindstream:build"
75 | }
76 | },
77 | "test": {
78 | "builder": "@angular-builders/custom-webpack:karma",
79 | "options": {
80 | "customWebpackConfig": {
81 | "path": "./webpack-web.config.js"
82 | },
83 | "main": "src/test.ts",
84 | "polyfills": "src/polyfills.ts",
85 | "tsConfig": "src/tsconfig.spec.json",
86 | "karmaConfig": "src/karma.conf.js",
87 | "styles": [
88 | "src/styles.scss"
89 | ],
90 | "scripts": [],
91 | "assets": [
92 | "src/favicon.ico",
93 | "src/assets"
94 | ],
95 | "sourceMap": false
96 | }
97 | },
98 | "lint": {
99 | "builder": "@angular-devkit/build-angular:tslint",
100 | "options": {
101 | "tsConfig": [
102 | "src/tsconfig.app.json",
103 | "src/tsconfig.spec.json"
104 | ],
105 | "exclude": [
106 | "**/node_modules/**"
107 | ]
108 | }
109 | }
110 | }
111 | }
112 | },
113 | "defaultProject": "mindstream"
114 | }
115 |
--------------------------------------------------------------------------------
/artwork/feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/artwork/feature.png
--------------------------------------------------------------------------------
/artwork/feature.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/artwork/feature.xcf
--------------------------------------------------------------------------------
/artwork/icon-circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/artwork/icon-circle.png
--------------------------------------------------------------------------------
/artwork/icon-rounded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/artwork/icon-rounded.png
--------------------------------------------------------------------------------
/artwork/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/artwork/icon.png
--------------------------------------------------------------------------------
/artwork/icon.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/artwork/icon.xcf
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/metadata/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot_add_task.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/metadata/en-US/images/phoneScreenshots/screenshot_add_task.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/screenshot_tasks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/metadata/en-US/images/phoneScreenshots/screenshot_tasks.png
--------------------------------------------------------------------------------
/metadata/todo.txt:
--------------------------------------------------------------------------------
1 | (A) Task 1 +project1 due:2019-11-13
2 | (B) Task 2 due:2021-11-13 rec:1m
3 | (C) Task 3
4 | Task 4 @context1 color:#e9dce5
5 | Task 5 +project2 +project3 @context2
6 | Task 6 color:#feefd6
7 |
--------------------------------------------------------------------------------
/nativescript.config.ts:
--------------------------------------------------------------------------------
1 | import { NativeScriptConfig } from '@nativescript/core'
2 |
3 | export default {
4 | id: 'im.mindstream.mobile',
5 | appResourcesPath: 'App_Resources',
6 | android: {
7 | v8Flags: '--expose_gc',
8 | markingMode: 'none',
9 | codeCache: true,
10 | },
11 | appPath: 'src',
12 | nsext: '.tns',
13 | webext: '',
14 | shared: true,
15 | webpackConfigPath: 'webpack-tns.config.js',
16 | } as NativeScriptConfig
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mindstream",
3 | "version": "1.6.2",
4 | "license": "GPL-3.0-or-later",
5 | "repository": "https://github.com/xuhcc/mindstream",
6 | "private": true,
7 | "main": "main.js",
8 | "scripts": {
9 | "postinstall": "./scripts/copy-fonts.js",
10 | "ng": "ng",
11 | "web": "./scripts/web-backend.js & ng serve",
12 | "web-release": "ng build --prod && cp scripts/web-backend.js platforms/web/index.js",
13 | "start": "npm run web",
14 | "build-icons": "tns resources generate icons artwork/icon.png",
15 | "build-splashes": "tns resources generate splashes artwork/feature.png --background \"#333333\"",
16 | "android": "tns debug android --no-hmr",
17 | "android-release": "tns build android --release --clean --key-store-path /dev/null --key-store-alias NULL --key-store-password NULL --key-store-alias-password NULL",
18 | "ios": "tns debug ios",
19 | "ios-unsigned": "./scripts/ios-unsigned.sh",
20 | "lint": "eslint 'src/**/*.ts' && stylelint 'src/**/*.scss'",
21 | "test": "ng test --browsers jsdom",
22 | "clean": "rm -rf platforms",
23 | "version": "./scripts/check-changelog.js && ./scripts/bump-version.js && git add -A"
24 | },
25 | "dependencies": {
26 | "@angular/animations": "~10.1.0",
27 | "@angular/common": "~10.1.0",
28 | "@angular/compiler": "~10.1.0",
29 | "@angular/core": "~10.1.0",
30 | "@angular/forms": "~10.1.0",
31 | "@angular/platform-browser": "~10.1.0",
32 | "@angular/platform-browser-dynamic": "~10.1.0",
33 | "@angular/router": "~10.1.0",
34 | "@nativescript/angular": "~10.1.0",
35 | "@nativescript/core": "~7.2.1",
36 | "@nativescript/imagepicker": "~1.0.0",
37 | "@nativescript/iqkeyboardmanager": "~2.0.0",
38 | "@nativescript/webpack": "~3.0.8",
39 | "@nstudio/nativescript-floatingactionbutton": "~3.0.4",
40 | "@nstudio/nativescript-pulltorefresh": "~3.0.1",
41 | "@openfonts/pt-sans_all": "1.44.0",
42 | "@openfonts/vollkorn_all": "1.43.0",
43 | "@triniwiz/nativescript-toasty": "~4.0.3",
44 | "angular-mydatepicker": "~0.10.3",
45 | "core-js": "~2.6.12",
46 | "express": "~4.17.1",
47 | "jstodotxt": "0.10.0",
48 | "markdown-it": "~10.0.0",
49 | "markdown-it-link-attributes": "~3.0.0",
50 | "material-design-icons-iconfont": "~5.0.1",
51 | "moment": "~2.24.0",
52 | "nativescript-appversion": "~1.4.2",
53 | "nativescript-foss-sidedrawer": "file:plugins/nativescript-foss-sidedrawer-2.0.0.tgz",
54 | "nativescript-mediafilepicker": "~2.0.16",
55 | "nativescript-modal-datetimepicker": "~2.1.5",
56 | "nativescript-permissions": "~1.3.7",
57 | "ngx-smart-modal": "~7.2.1",
58 | "normalize.css": "~8.0.1",
59 | "reflect-metadata": "~0.1.12",
60 | "rxjs": "~6.6.3",
61 | "thenby": "~1.3.0",
62 | "tslib": "1.10.0",
63 | "zone.js": "~0.10.2"
64 | },
65 | "devDependencies": {
66 | "@angular-builders/custom-webpack": "~8.2.0",
67 | "@angular-devkit/build-angular": "~0.1002.0",
68 | "@angular/cli": "~10.2.0",
69 | "@angular/compiler-cli": "~10.2.3",
70 | "@nativescript/android": "7.0.1",
71 | "@nativescript/schematics": "~10.0.2",
72 | "@nativescript/types": "~7.0.4",
73 | "@types/jasmine": "~3.5.0",
74 | "@types/node": "~12.19.8",
75 | "@typescript-eslint/eslint-plugin": "~2.18.0",
76 | "@typescript-eslint/parser": "~2.18.0",
77 | "eslint": "~6.3.0",
78 | "eslint-config-standard": "~14.1.0",
79 | "eslint-plugin-import": "~2.18.2",
80 | "eslint-plugin-jasmine": "~2.10.1",
81 | "eslint-plugin-node": "~9.2.0",
82 | "eslint-plugin-promise": "~4.2.1",
83 | "eslint-plugin-standard": "~4.0.1",
84 | "jsdom": "~15.1.1",
85 | "karma": "~6.2.0",
86 | "karma-jasmine": "~3.0.1",
87 | "karma-jsdom-launcher": "~8.0.2",
88 | "karma-mocha-reporter": "~2.2.5",
89 | "node-sass": "~4.14.1",
90 | "stylelint": "~13.12.0",
91 | "stylelint-config-sass-guidelines": "~8.0.0",
92 | "ts-node": "~8.3.0",
93 | "tslint": "~6.1.0",
94 | "typescript": "~3.9.0",
95 | "webpack-cli": "~3.3.12"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/plugins/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/plugins/.gitkeep
--------------------------------------------------------------------------------
/plugins/README.md:
--------------------------------------------------------------------------------
1 | # Plugins
2 |
3 | ### nativescript-foss-sidedrawer-2.0.0.tgz
4 |
5 | Contains fix from https://gitlab.com/burke-software/nativescript-foss-sidedrawer/-/merge_requests/4
6 |
7 | ### nativescript-imagepicker-7.1.0.tgz
8 |
9 | Contains fix from https://github.com/NativeScript/nativescript-imagepicker/pull/326
10 |
--------------------------------------------------------------------------------
/plugins/nativescript-foss-sidedrawer-2.0.0.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/plugins/nativescript-foss-sidedrawer-2.0.0.tgz
--------------------------------------------------------------------------------
/plugins/nativescript-imagepicker-7.1.0.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/plugins/nativescript-imagepicker-7.1.0.tgz
--------------------------------------------------------------------------------
/reference.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/scripts/bump-version.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { exec } = require('child_process');
4 | const shell = require('shelljs');
5 |
6 | // Temporary solution for
7 | // https://github.com/NativeScript/nativescript-cli/issues/1118
8 | shell.sed(
9 | '-i',
10 | new RegExp(process.env.npm_old_version),
11 | process.env.npm_new_version,
12 | 'App_Resources/iOS/Info.plist',
13 | )
14 | shell.sed(
15 | '-i',
16 | new RegExp(process.env.npm_old_version),
17 | process.env.npm_new_version,
18 | 'src/environments/environment.ts',
19 | )
20 | shell.sed(
21 | '-i',
22 | new RegExp(process.env.npm_old_version),
23 | process.env.npm_new_version,
24 | 'src/environments/environment.prod.ts',
25 | )
26 |
--------------------------------------------------------------------------------
/scripts/check-changelog.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { exec } = require('child_process');
4 |
5 | exec(`grep ${process.env.npm_package_version} CHANGELOG.md`, (error, stdout) => {
6 | if (error) {
7 | throw error;
8 | }
9 | });
10 |
--------------------------------------------------------------------------------
/scripts/copy-fonts.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 |
6 | const FONT_FIR = 'src/fonts';
7 | const FONTS = [
8 | 'node_modules/material-design-icons-iconfont/dist/fonts/MaterialIcons-Regular.ttf',
9 | 'node_modules/@openfonts/vollkorn_all/files/vollkorn-all-400.woff2',
10 | 'node_modules/@openfonts/vollkorn_all/files/vollkorn-all-400-italic.woff2',
11 | 'node_modules/@openfonts/vollkorn_all/files/vollkorn-all-700.woff2',
12 | 'node_modules/@openfonts/pt-sans_all/files/pt-sans-all-400.woff2',
13 | 'node_modules/@openfonts/pt-sans_all/files/pt-sans-all-700.woff2',
14 | ];
15 |
16 | if (!fs.existsSync(FONT_FIR)){
17 | fs.mkdirSync(FONT_FIR);
18 | }
19 |
20 | FONTS.forEach((filePath) => {
21 | let fileName = path.basename(filePath);
22 | fs.copyFile(filePath, path.join(FONT_FIR, fileName), (error) => {
23 | if (error) {
24 | throw error;
25 | } else {
26 | console.info(`copied font ${fileName}`);
27 | }
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/scripts/ios-unsigned.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 | set -x
5 |
6 | tns prepare ios --release
7 | cd platforms/ios
8 |
9 | xcodebuild -scheme mindstream -workspace mindstream.xcworkspace \
10 | -configuration Release clean archive \
11 | -archivePath "build/mindstream.xcarchive" \
12 | CODE_SIGN_IDENTITY="" \
13 | CODE_SIGNING_REQUIRED=NO \
14 | CODE_SIGNING_ALLOWED=NO
15 | mkdir Payload
16 | cp -R build/mindstream.xcarchive/Products/Applications/mindstream.app Payload/
17 | zip -r build/mindstream.ipa Payload
18 | rm -rf Payload
19 |
--------------------------------------------------------------------------------
/scripts/web-backend.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | /*
3 | * We need this service to interact with the file system
4 | * until some standard emerges for doing it from browser.
5 | * For example: https://github.com/WICG/native-file-system
6 | */
7 | const fs = require('fs');
8 | const express = require('express');
9 |
10 | const app = express();
11 | app.use(express.static('static'));
12 | app.use(express.json({ limit: '10mb' }));
13 | app.use((request, response, next) => {
14 | response.header('Access-Control-Allow-Origin', '*');
15 | response.header(
16 | 'Access-Control-Allow-Headers',
17 | 'Origin, X-Requested-With, Content-Type, Accept',
18 | );
19 | next();
20 | });
21 |
22 | app.get('/file/:path', (request, response) => {
23 | const filePath = decodeURIComponent(request.params.path);
24 | fs.readFile(filePath, {encoding: 'utf-8'}, (error, data) => {
25 | if (error) {
26 | console.warn(`file not loaded from ${filePath}: ${error}`);
27 | response.status(400).json({error: error.toString()});
28 | } else {
29 | console.info(`file loaded from ${filePath}`);
30 | response.json({content: data});
31 | }
32 | });
33 | });
34 |
35 | app.post('/file/:path', (request, response) => {
36 | const filePath = decodeURIComponent(request.params.path);
37 | const content = request.body.content;
38 | fs.writeFile(filePath, content, (error) => {
39 | if (error) {
40 | console.warn(`file not saved to ${filePath}: ${error}`);
41 | response.status(400).json({error: error.toString()});
42 | } else {
43 | console.info(`file saved to ${filePath}`);
44 | response.json({});
45 | }
46 | });
47 | });
48 |
49 | app.get('/*', (request, response) => {
50 | // Handle angular routes
51 | response.sendFile(__dirname + '/static/index.html');
52 | });
53 |
54 | const PORT = process.env.PORT || 8080;
55 |
56 | app.listen(PORT, () => {
57 | console.log(`file manager running at http://localhost:${PORT}/`);
58 | });
59 |
--------------------------------------------------------------------------------
/src/_app-common.scss:
--------------------------------------------------------------------------------
1 | // Common variables to be shared between web, iOS and Android
2 | $text-color: #000000;
3 | $link-color: #643900; // Also defined as ns_accent in values-v21/colors.xml
4 | $link-hover-color: #995700;
5 |
6 | $header-color: #333333;
7 | $header-text-color: #FBFCF0; // Also defined as header_text in values-v21/colors.xml
8 | $menu-color: #DFE6B8;
9 | $page-color: #FBFCF0;
10 |
11 | $button-color: #EDEEE2;
12 | $button-border-color: #E3E6CF;
13 | $button-text-color: $link-color;
14 | $button-disabled-color: #EEEEEE;
15 | $button-disabled-border-color: #EEEEEE;
16 | $button-disabled-text-color: #B5B5B5;
17 |
18 | $field-color: #FFFFFF;
19 | $field-border-color: #EBEBEB;
20 |
21 | $task-color: #FEFFF3;
22 | $task-border-color: #E5E6DA;
23 | $task-checkbox-color: #FFFFFF;
24 | $task-checkbox-text-color: #333333;
25 | $task-completed-color: #999999;
26 |
27 | $tag-color: $button-color;
28 | $tag-text-color: $link-color;
29 | $tag-hover-color: #DFE6B8;
30 |
31 | $priority-a: #EEAAA7;
32 | $priority-b: #F0D3A8;
33 | $priority-c: #F2EAAA;
34 | $priority-d: #DEE6B8;
35 |
--------------------------------------------------------------------------------
/src/_app-variables.scss:
--------------------------------------------------------------------------------
1 | // Common variables to be shared between iOS and Android
2 | $main-padding: 7;
3 |
4 | $field-border: 2;
5 | $field-border-radius: 3;
6 |
7 | %task {
8 | background-color: $task-color;
9 | border-color: $task-border-color;
10 | border-radius: 5;
11 | border-style: solid;
12 | border-width: 1;
13 | color: $text-color;
14 | font-family: serif;
15 | font-size: 16;
16 | margin-bottom: 5;
17 | padding: $main-padding;
18 | }
19 |
20 | %tag {
21 | background-color: $tag-color;
22 | color: $tag-text-color;
23 | padding: 2 5;
24 | }
25 |
--------------------------------------------------------------------------------
/src/_app-web.scss:
--------------------------------------------------------------------------------
1 | // Common variables for web app
2 | $main-padding: 7px;
3 |
4 | $field-border: 2px;
5 | $field-border-radius: 3px;
6 |
7 | $main-font: Tahoma, Geneva, sans-serif;
8 | $header-font: 'PT Sans', sans-serif;
9 |
10 | %tag {
11 | background-color: $tag-color;
12 | color: $tag-text-color;
13 | padding: 2px 5px;
14 | white-space: nowrap;
15 | }
16 |
--------------------------------------------------------------------------------
/src/app.android.scss:
--------------------------------------------------------------------------------
1 | // Common styles for Android
2 | @import './app.tns';
3 |
4 | .action-bar .icon {
5 | font-size: 10;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app.ios.scss:
--------------------------------------------------------------------------------
1 | // Common styles for iOS
2 | @import './app.tns';
3 |
4 | .action-bar .icon {
5 | font-size: 24;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app.tns.scss:
--------------------------------------------------------------------------------
1 | // Common styles for iOS and Android
2 | @import 'app-common';
3 | @import 'app-variables';
4 |
5 | .icon {
6 | font-family: 'Material Icons', 'MaterialIcons-Regular';
7 | }
8 |
9 | .action-bar {
10 | background-color: $header-color;
11 | color: $header-text-color;
12 | font-size: 20;
13 | font-weight: bold;
14 |
15 | .disabled {
16 | color: #666666;
17 | }
18 | }
19 |
20 | .page {
21 | background-color: $page-color;
22 | color: $text-color;
23 | padding: $main-padding;
24 | }
25 |
26 | .btn {
27 | background-color: $button-color;
28 | border-color: $button-border-color;
29 | border-radius: 3;
30 | border-width: 1;
31 | color: $button-text-color;
32 | padding: $main-padding;
33 | }
34 |
35 | .btn-disabled {
36 | background-color: $button-disabled-color;
37 | border-color: $button-disabled-border-color;
38 | color: $button-disabled-text-color;
39 | }
40 |
41 | .field {
42 | background-color: $field-color;
43 | border-color: $field-border-color;
44 | border-radius: $field-border-radius;
45 | border-width: $field-border;
46 | font-size: 16;
47 | height: 40;
48 | margin: 5 0;
49 | padding: $main-padding;
50 | }
51 |
52 | .field-with-addon {
53 | border-radius: $field-border-radius 0 0 $field-border-radius;
54 | border-width: $field-border 0 $field-border $field-border;
55 | }
56 |
57 | .field-addon {
58 | background-color: $field-color;
59 | border-color: $field-border-color;
60 | border-radius: 0;
61 | border-width: $field-border 0;
62 | color: $link-color;
63 | height: 40;
64 | margin: 5 0;
65 | padding: $main-padding $main-padding $main-padding 0;
66 |
67 | &.icon {
68 | font-size: 28;
69 | padding: 3 3 0 0;
70 | }
71 | }
72 |
73 | .field-addon-last {
74 | border-radius: 0 $field-border-radius $field-border-radius 0;
75 | border-width: $field-border $field-border $field-border 0;
76 | }
77 |
78 | .item-list {
79 | separator-color: transparent;
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/about/about.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
{{ appName }} v{{ appVersion }}
6 |
7 |
12 | Report bug
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/about/about.component.scss:
--------------------------------------------------------------------------------
1 | #app-name {
2 | font-weight: bold;
3 | }
4 |
5 | #report-bug {
6 | margin-top: 10px;
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/about/about.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 |
3 | import { AboutComponent } from './about.component'
4 |
5 | describe('AboutComponent', () => {
6 | let component: AboutComponent
7 | let fixture: ComponentFixture
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [
12 | AboutComponent,
13 | ],
14 | }).compileComponents()
15 | }))
16 |
17 | beforeEach(() => {
18 | fixture = TestBed.createComponent(AboutComponent)
19 | component = fixture.componentInstance
20 | fixture.detectChanges()
21 | })
22 |
23 | it('should create', () => {
24 | expect(component).toBeTruthy()
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/app/about/about.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
18 |
19 |
20 |
24 |
28 |
29 |
33 |
34 |
--------------------------------------------------------------------------------
/src/app/about/about.component.tns.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-variables';
3 |
4 | .page {
5 | font-size: 16;
6 | }
7 |
8 | HtmlView {
9 | // For iOS
10 | background-color: $page-color;
11 | }
12 |
13 | #app-name {
14 | font-weight: bold;
15 | }
16 |
17 | #report-bug {
18 | margin-top: 10;
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/about/about.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewContainerRef } from '@angular/core'
2 |
3 | import { APP_NAME } from '../app.constants'
4 | import { SideDrawerService } from '../nav/sidedrawer.service'
5 | import { isAndroid, isIOS, isWeb } from '../shared/helpers/platform'
6 | import { getVersion } from '../shared/helpers/version'
7 |
8 | const APP_DESCRIPTION = `
9 | Task management app, built on todo.txt.
10 | Tips:
11 |
12 | ${isAndroid || isIOS ? '- Press and hold on a task checkbox to see additional actions.
' : ''}
13 | ${isWeb ? '- Press A while on a task list to add a new task; press T to add one due today, or W to add one due tomorrow.
' : ''}
14 | ${isWeb ? '- Ctrl-click a task\'s checkbox to postpone it by one day.
' : ''}
15 | - Add
h:1
tag to create a hidden task. You can define new projects, contexts and colors in it.
16 | - Use backslash to prevent words starting with
+
or @
from being parsed as projects or contexts: \\+test \\@test
.
17 |
18 | `
19 |
20 | @Component({
21 | selector: 'ms-about',
22 | templateUrl: './about.component.html',
23 | styleUrls: ['./about.component.scss'],
24 | })
25 | export class AboutComponent {
26 |
27 | title = 'About';
28 | appName = APP_NAME;
29 | appVersion = getVersion();
30 | description = APP_DESCRIPTION;
31 | bugTracker = 'https://github.com/xuhcc/mindstream/issues';
32 |
33 | constructor(
34 | private sideDrawer: SideDrawerService,
35 | private view: ViewContainerRef,
36 | ) {}
37 |
38 | openDrawer() {
39 | this.sideDrawer.open(this.view)
40 | }
41 |
42 | getBugTrackerLink(): string {
43 | return `Report bug`
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.tns.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core'
2 | import { NativeScriptRouterModule } from '@nativescript/angular'
3 | import { routes } from './app.routes'
4 |
5 | @NgModule({
6 | imports: [NativeScriptRouterModule.forRoot(routes)],
7 | exports: [NativeScriptRouterModule],
8 | })
9 | export class AppRoutingModule { }
10 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core'
2 | import { RouterModule } from '@angular/router'
3 | import { routes } from './app.routes'
4 |
5 | @NgModule({
6 | imports: [RouterModule.forRoot(routes)],
7 | exports: [RouterModule],
8 | })
9 | export class AppRoutingModule { }
10 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | @import '../app-common';
2 | @import '../app-web';
3 |
4 | #wrapper {
5 | background-color: $page-color;
6 | display: flex;
7 | min-height: 100vh;
8 | width: 100%;
9 | }
10 |
11 | #content {
12 | flex-grow: 1;
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/app.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app.component.tns.scss:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core'
2 |
3 | @Component({
4 | selector: 'ms-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss'],
7 | })
8 | export class AppComponent {}
9 |
--------------------------------------------------------------------------------
/src/app/app.constants.ts:
--------------------------------------------------------------------------------
1 | export const APP_NAME = 'Mindstream'
2 |
--------------------------------------------------------------------------------
/src/app/app.module.tns.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core'
2 | import { ReactiveFormsModule } from '@angular/forms'
3 | import {
4 | NativeScriptModule,
5 | NativeScriptFormsModule,
6 | registerElement,
7 | } from '@nativescript/angular'
8 |
9 | import { Fab } from '@nstudio/nativescript-floatingactionbutton'
10 | import { PullToRefresh } from '@nstudio/nativescript-pulltorefresh'
11 |
12 | import { AppRoutingModule } from './app-routing.module'
13 | import { AppComponent } from './app.component'
14 | import { NavModalComponent } from './nav/nav-modal.component'
15 | import { isIOS } from './shared/helpers/platform'
16 | import { PlainTextComponent } from './plaintext/plaintext.component'
17 | import { SettingsComponent } from './settings/settings.component'
18 | import { TaskFormComponent } from './task-form/task-form.component'
19 | import { TaskFormAutocompleteComponent } from './task-form/task-form-autocomplete.component'
20 | import { TaskListComponent } from './task-list/task-list.component'
21 | import { TagListComponent } from './tag-list/tag-list.component'
22 | import { WelcomeComponent } from './welcome/welcome.component'
23 | import { AboutComponent } from './about/about.component'
24 |
25 | @NgModule({
26 | declarations: [
27 | AppComponent,
28 | NavModalComponent,
29 | PlainTextComponent,
30 | SettingsComponent,
31 | TaskFormComponent,
32 | TaskFormAutocompleteComponent,
33 | TaskListComponent,
34 | TagListComponent,
35 | WelcomeComponent,
36 | AboutComponent,
37 | ],
38 | entryComponents: [
39 | NavModalComponent,
40 | ],
41 | imports: [
42 | NativeScriptModule,
43 | AppRoutingModule,
44 | ReactiveFormsModule,
45 | NativeScriptFormsModule,
46 | ],
47 | providers: [],
48 | bootstrap: [AppComponent],
49 | schemas: [NO_ERRORS_SCHEMA],
50 | })
51 | export class AppModule {
52 |
53 | constructor() {
54 | registerElement('Fab', () => Fab)
55 | registerElement('PullToRefresh', () => PullToRefresh)
56 | if (isIOS) {
57 | const iqKeyboard = IQKeyboardManager.sharedManager() // eslint-disable-line no-undef
58 | iqKeyboard.shouldResignOnTouchOutside = true
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core'
2 | import { ReactiveFormsModule } from '@angular/forms'
3 | import { HttpClientModule } from '@angular/common/http'
4 | import { BrowserModule } from '@angular/platform-browser'
5 |
6 | import { AngularMyDatePickerModule } from 'angular-mydatepicker'
7 | import { NgxSmartModalModule } from 'ngx-smart-modal'
8 |
9 | import { AppRoutingModule } from './app-routing.module'
10 | import { AppComponent } from './app.component'
11 | import { NavigationComponent } from './nav/navigation.component'
12 | import { DialogComponent } from './shared/dialog.component'
13 | import { PlainTextComponent } from './plaintext/plaintext.component'
14 | import { SettingsComponent } from './settings/settings.component'
15 | import { TaskFormComponent } from './task-form/task-form.component'
16 | import { TaskFormAutocompleteComponent } from './task-form/task-form-autocomplete.component'
17 | import { TaskListComponent } from './task-list/task-list.component'
18 | import { TagListComponent } from './tag-list/tag-list.component'
19 | import { WelcomeComponent } from './welcome/welcome.component'
20 | import { AboutComponent } from './about/about.component'
21 |
22 | @NgModule({
23 | declarations: [
24 | AppComponent,
25 | NavigationComponent,
26 | DialogComponent,
27 | PlainTextComponent,
28 | SettingsComponent,
29 | TaskFormComponent,
30 | TaskFormAutocompleteComponent,
31 | TaskListComponent,
32 | TagListComponent,
33 | WelcomeComponent,
34 | AboutComponent,
35 | ],
36 | entryComponents: [
37 | DialogComponent,
38 | ],
39 | imports: [
40 | HttpClientModule,
41 | ReactiveFormsModule,
42 | BrowserModule,
43 | AngularMyDatePickerModule,
44 | NgxSmartModalModule.forRoot(),
45 | AppRoutingModule,
46 | ],
47 | providers: [],
48 | bootstrap: [AppComponent],
49 | })
50 | export class AppModule { }
51 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router'
2 |
3 | import { AboutComponent } from './about/about.component'
4 | import { PlainTextComponent } from './plaintext/plaintext.component'
5 | import { TagListComponent } from './tag-list/tag-list.component'
6 | import { SettingsComponent } from './settings/settings.component'
7 | import { TaskFormComponent } from './task-form/task-form.component'
8 | import { TaskListComponent } from './task-list/task-list.component'
9 | import { WelcomeComponent } from './welcome/welcome.component'
10 |
11 | import { FileGuard } from './shared/file.guard'
12 | import { WelcomeGuard } from './welcome/welcome.guard'
13 |
14 | export const routes: Routes = [
15 | {
16 | path: '',
17 | redirectTo: '/welcome',
18 | pathMatch: 'full',
19 | },
20 | {
21 | path: 'welcome',
22 | component: WelcomeComponent,
23 | canActivate: [WelcomeGuard],
24 | },
25 | {
26 | path: 'plaintext',
27 | component: PlainTextComponent,
28 | canActivate: [FileGuard],
29 | },
30 | {
31 | path: 'settings',
32 | component: SettingsComponent,
33 | canActivate: [FileGuard],
34 | },
35 | {
36 | path: 'tasks',
37 | component: TaskListComponent,
38 | canActivate: [FileGuard],
39 | },
40 | {
41 | path: 'task-detail/:dueDaysFromNow',
42 | component: TaskFormComponent,
43 | canActivate: [FileGuard],
44 | },
45 | {
46 | path: 'task-detail',
47 | component: TaskFormComponent,
48 | canActivate: [FileGuard],
49 | },
50 | {
51 | path: 'tags',
52 | component: TagListComponent,
53 | canActivate: [FileGuard],
54 | },
55 | {
56 | path: 'about',
57 | component: AboutComponent,
58 | canActivate: [FileGuard],
59 | },
60 | ]
61 |
--------------------------------------------------------------------------------
/src/app/nav/nav-modal.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 |
3 | .modal {
4 | background-color: $menu-color;
5 | border-radius: 20 20 0 0;
6 | text-align: center;
7 | width: 250;
8 | }
9 |
10 | .modal-header {
11 | font-size: 18;
12 | font-weight: 500;
13 | margin: 10 0 0;
14 | padding: 0;
15 | }
16 |
17 | .modal-subheader {
18 | font-size: 14;
19 | font-weight: 500;
20 | margin: 0 0 20;
21 | }
22 |
23 | .nav-item {
24 | margin: 10 0;
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/nav/nav-modal.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/src/app/nav/nav-modal.component.tns.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core'
2 | import { ModalDialogParams } from '@nativescript/angular'
3 |
4 | import { APP_NAME } from '../app.constants'
5 | import { getVersion } from '../shared/helpers/version'
6 |
7 | @Component({
8 | selector: 'ms-nav-modal',
9 | templateUrl: './nav-modal.component.html',
10 | styleUrls: ['./nav-modal.component.scss'],
11 | })
12 | export class NavModalComponent {
13 |
14 | title = APP_NAME;
15 | subtitle: string;
16 | pages: {title: string; url: string}[];
17 |
18 | constructor(
19 | private modalParams: ModalDialogParams,
20 | ) {
21 | this.subtitle = `v${getVersion()}`
22 | this.pages = modalParams.context
23 | }
24 |
25 | navigateTo(url: string): void {
26 | this.modalParams.closeCallback(url)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/nav/nav.ts:
--------------------------------------------------------------------------------
1 | export const NAVIGATION_MENU = [
2 | {
3 | title: 'Tags',
4 | url: '/tags',
5 | },
6 | {
7 | title: 'Tasks',
8 | url: '/tasks',
9 | },
10 | {
11 | title: 'Plain text',
12 | url: '/plaintext',
13 | },
14 | {
15 | title: 'Settings',
16 | url: '/settings',
17 | },
18 | {
19 | title: 'About',
20 | url: '/about',
21 | },
22 | ]
23 |
--------------------------------------------------------------------------------
/src/app/nav/navigation.component.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/app/nav/navigation.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | $nav-color: #DFE6B8;
5 | $nav-text-color: #000000;
6 |
7 | nav {
8 | background-color: $nav-color;
9 | color: $nav-text-color;
10 | font-family: $header-font;
11 | font-size: 20px;
12 | font-weight: bold;
13 | height: 100%;
14 | min-width: 200px;
15 |
16 | a {
17 | color: $nav-text-color;
18 | }
19 | }
20 |
21 | .nav-header {
22 | background: linear-gradient(90deg, $header-color 50%, #252622 100%);
23 | margin: 0;
24 | }
25 |
26 | .version {
27 | color: #5a5a5a;
28 | }
29 |
30 | .nav-item {
31 | padding: 10px $main-padding 0;
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/nav/navigation.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 | import { RouterModule } from '@angular/router'
3 |
4 | import { NavigationComponent } from './navigation.component'
5 |
6 | describe('NavigationComponent', () => {
7 | let component: NavigationComponent
8 | let fixture: ComponentFixture
9 |
10 | beforeEach(async(() => {
11 | TestBed.configureTestingModule({
12 | declarations: [
13 | NavigationComponent,
14 | ],
15 | imports: [
16 | RouterModule.forRoot([]),
17 | ],
18 | }).compileComponents()
19 | }))
20 |
21 | beforeEach(() => {
22 | fixture = TestBed.createComponent(NavigationComponent)
23 | component = fixture.componentInstance
24 | fixture.detectChanges()
25 | })
26 |
27 | it('should create', () => {
28 | expect(component).toBeTruthy()
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/app/nav/navigation.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core'
2 |
3 | import { APP_NAME } from '../app.constants'
4 | import { NAVIGATION_MENU } from './nav'
5 | import { getVersion } from '../shared/helpers/version'
6 |
7 | @Component({
8 | selector: 'ms-navigation',
9 | templateUrl: './navigation.component.html',
10 | styleUrls: ['./navigation.component.scss'],
11 | })
12 | export class NavigationComponent {
13 |
14 | appName = APP_NAME;
15 | appVersion = getVersion();
16 | navigationMenu = NAVIGATION_MENU;
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/nav/sidedrawer.service.tns.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NgZone, ViewContainerRef } from '@angular/core'
2 | import { ModalDialogOptions, ModalDialogService, RouterExtensions } from '@nativescript/angular'
3 |
4 | import { TnsSideDrawerClass } from 'nativescript-foss-sidedrawer'
5 | import { Color } from '@nativescript/core/color'
6 | import { isAndroid, isIOS } from '@nativescript/core/platform'
7 |
8 | import { NavModalComponent } from './nav-modal.component'
9 | import { NAVIGATION_MENU } from './nav'
10 | import { APP_NAME } from '../app.constants'
11 | import { getVersion } from '../shared/helpers/version'
12 |
13 | // https://developer.android.com/reference/android/support/v4/widget/DrawerLayout.html
14 | const LOCK_MODE_LOCKED_CLOSED = 1
15 | const LOCK_MODE_UNDEFINED = 3
16 |
17 | @Injectable({
18 | providedIn: 'root',
19 | })
20 | export class SideDrawerService {
21 |
22 | private drawer: TnsSideDrawerClass;
23 | loaded: Promise;
24 |
25 | constructor(
26 | private ngZone: NgZone,
27 | private router: RouterExtensions,
28 | private modalService: ModalDialogService,
29 | ) {
30 | if (isAndroid) {
31 | this.createAndroidDrawer()
32 | }
33 | }
34 |
35 | open(viewContainerRef?: ViewContainerRef) {
36 | if (isAndroid) {
37 | this.drawer.open()
38 | } else if (isIOS) {
39 | this.openModalNav(viewContainerRef)
40 | }
41 | }
42 |
43 | private createAndroidDrawer() {
44 | this.drawer = new TnsSideDrawerClass()
45 |
46 | const config = {
47 | title: APP_NAME,
48 | subtitle: `v${getVersion()}`,
49 | templates: NAVIGATION_MENU,
50 | headerTextColor: new Color('#FBFCF0'), // $header-text-color
51 | textColor: new Color('#000000'), // $text-color
52 | headerBackgroundColor: new Color('#333333'), // $header-color
53 | backgroundColor: new Color('#FBFCF0'), // $page-color
54 | listener: (index: number) => {
55 | const url = NAVIGATION_MENU[index].url
56 | // Use NgZone because this is a callback from external JS library
57 | this.ngZone.run(() => {
58 | this.router.navigateByUrl(url)
59 | })
60 | },
61 | }
62 | this.loaded = new Promise((resolve) => {
63 | // https://gitlab.com/burke-software/nativescript-foss-sidedrawer/issues/2
64 | setTimeout(() => {
65 | this.drawer.build(config)
66 | resolve()
67 | }, 0)
68 | })
69 | }
70 |
71 | private openModalNav(viewContainerRef: ViewContainerRef) {
72 | const options: ModalDialogOptions = {
73 | viewContainerRef: viewContainerRef,
74 | context: NAVIGATION_MENU,
75 | }
76 | this.modalService.showModal(NavModalComponent, options).then((url: string) => {
77 | // Navigation is not working in callback
78 | // https://github.com/NativeScript/nativescript-angular/issues/1380
79 | setTimeout(() => {
80 | this.router.navigateByUrl(url)
81 | }, 50)
82 | })
83 | }
84 |
85 | async lock() {
86 | if (isAndroid) {
87 | await this.loaded
88 | const layout = (this.drawer as any).drawer.getDrawerLayout()
89 | layout.setDrawerLockMode(LOCK_MODE_LOCKED_CLOSED)
90 | }
91 | }
92 |
93 | async unlock() {
94 | if (isAndroid) {
95 | await this.loaded
96 | const layout = (this.drawer as any).drawer.getDrawerLayout()
97 | layout.setDrawerLockMode(LOCK_MODE_UNDEFINED)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/app/nav/sidedrawer.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 |
3 | @Injectable({
4 | providedIn: 'root',
5 | })
6 | export class SideDrawerService {
7 |
8 | open(viewContainerRef?: any) {
9 | console.log(viewContainerRef)
10 | }
11 |
12 | lock() {} // eslint-disable-line @typescript-eslint/no-empty-function
13 |
14 | unlock() {} // eslint-disable-line @typescript-eslint/no-empty-function
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/plaintext/plaintext.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
{{ todoFile.content }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/app/plaintext/plaintext.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | #file-content {
5 | font-family: monospace;
6 |
7 | pre {
8 | font-size: 14px;
9 | margin: 0;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/plaintext/plaintext.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/app/plaintext/plaintext.component.tns.scss:
--------------------------------------------------------------------------------
1 | #file-content {
2 | font-family: monospace;
3 | vertical-align: top;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/plaintext/plaintext.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ViewContainerRef, ViewChild, ElementRef } from '@angular/core'
2 |
3 | import { SideDrawerService } from '../nav/sidedrawer.service'
4 | import { RouterService } from '../shared/router.service'
5 | import { TodoFileService } from '../shared/todo-file.service'
6 | import { isAndroid } from '../shared/helpers/platform'
7 |
8 | @Component({
9 | selector: 'ms-plaintext',
10 | templateUrl: './plaintext.component.html',
11 | styleUrls: ['./plaintext.component.scss'],
12 | })
13 | export class PlainTextComponent {
14 |
15 | title = 'Plain text';
16 |
17 | @ViewChild('fileContent', {static: false})
18 | fileContent: ElementRef
19 |
20 | constructor(
21 | private router: RouterService,
22 | public todoFile: TodoFileService,
23 | private sideDrawer: SideDrawerService,
24 | private viewContainerRef: ViewContainerRef,
25 | ) { }
26 |
27 | ngAfterViewInit(): void {
28 | setTimeout(() => {
29 | if (isAndroid) {
30 | this.fileContent.nativeElement.android.setTextIsSelectable(true)
31 | }
32 | }, 100)
33 | }
34 |
35 | openDrawer() {
36 | this.sideDrawer.open(this.viewContainerRef)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.html:
--------------------------------------------------------------------------------
1 |
4 |
20 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | #save-btn {
5 | margin-top: 5px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 | import { ReactiveFormsModule } from '@angular/forms'
3 |
4 | import { SettingsComponent } from './settings.component'
5 | import { FileService } from '../shared/file.service'
6 | import { RouterService } from '../shared/router.service'
7 |
8 | describe('SettingsComponent', () => {
9 | let component: SettingsComponent
10 | let fixture: ComponentFixture
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [SettingsComponent],
15 | imports: [ReactiveFormsModule],
16 | providers: [
17 | {provide: FileService, useValue: {}},
18 | {provide: RouterService, useValue: {}},
19 | ],
20 | }).compileComponents()
21 | }))
22 |
23 | beforeEach(() => {
24 | fixture = TestBed.createComponent(SettingsComponent)
25 | component = fixture.componentInstance
26 | fixture.detectChanges()
27 | })
28 |
29 | it('should create', () => {
30 | expect(component).toBeTruthy()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
31 |
37 |
38 |
39 |
40 |
44 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.tns.scss:
--------------------------------------------------------------------------------
1 | .field {
2 | margin: 5 0 0;
3 | }
4 |
5 | .btn {
6 | margin-top: 10;
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewContainerRef } from '@angular/core'
2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'
3 |
4 | import { SideDrawerService } from '../nav/sidedrawer.service'
5 | import { RouterService } from '../shared/router.service'
6 | import { SettingsService } from '../shared/settings.service'
7 | import { TodoFileService } from '../shared/todo-file.service'
8 | import { FilePathValidator } from '../shared/validators'
9 | import { openFilePicker } from '../shared/helpers/file-picker'
10 | import { showToast } from '../shared/helpers/toast'
11 |
12 | @Component({
13 | selector: 'ms-settings',
14 | templateUrl: './settings.component.html',
15 | styleUrls: ['./settings.component.scss'],
16 | })
17 | export class SettingsComponent implements OnInit {
18 |
19 | title = 'Settings';
20 | form: FormGroup;
21 |
22 | constructor(
23 | private formBuilder: FormBuilder,
24 | private router: RouterService,
25 | private settings: SettingsService,
26 | private sideDrawer: SideDrawerService,
27 | private todoFile: TodoFileService,
28 | private viewContainerRef: ViewContainerRef,
29 | ) { }
30 |
31 | ngOnInit() {
32 | this.form = this.formBuilder.group({
33 | filePath: [
34 | this.settings.path,
35 | [Validators.required, FilePathValidator()],
36 | ],
37 | })
38 | }
39 |
40 | openDrawer() {
41 | this.sideDrawer.open(this.viewContainerRef)
42 | }
43 |
44 | openPicker() {
45 | openFilePicker().then((filePath) => {
46 | if (filePath) {
47 | this.form.controls.filePath.setValue(filePath)
48 | }
49 | }).catch((error) => {
50 | console.warn(error)
51 | showToast(error.toString(), true)
52 | })
53 | }
54 |
55 | goBack() {
56 | this.router.backToPreviousPage()
57 | }
58 |
59 | save() {
60 | const filePath = this.form.value.filePath
61 | this.settings.path = filePath
62 | this.todoFile.initialLoad().then(() => {
63 | this.router.navigate(['/tasks'])
64 | })
65 | }
66 |
67 | reset() {
68 | this.settings.reset()
69 | this.form.reset()
70 | this.router.navigate(['/welcome'], {
71 | clearHistory: true,
72 | })
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/shared/dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ChangeDetectorRef } from '@angular/core'
2 |
3 | import { NgxSmartModalComponent, NgxSmartModalService } from 'ngx-smart-modal'
4 |
5 | @Component({
6 | template: `
7 | {{ title }}
8 | {{ message }}
9 |
12 |
15 |
18 | `,
19 | })
20 | export class DialogComponent {
21 |
22 | loader: any;
23 | modal: NgxSmartModalComponent;
24 |
25 | title: string;
26 | message: string;
27 | actions: string[];
28 | showCancel: boolean;
29 | showOK: boolean;
30 |
31 | constructor(
32 | private modalService: NgxSmartModalService,
33 | private changeDetector: ChangeDetectorRef,
34 | ) {
35 | // Workaround for
36 | // https://github.com/biig-io/ngx-smart-modal/issues/235
37 | this.loader = setInterval(() => {
38 | try {
39 | this.modal = this.modalService.getModal('dialog')
40 | } catch (error) {
41 | // Not loaded yet
42 | return
43 | }
44 | clearInterval(this.loader)
45 | this.onInit(this.modal.getData())
46 | }, 100)
47 | }
48 |
49 | onInit(data: any) {
50 | this.title = data.title
51 | this.message = data.message
52 | this.actions = data.actions || []
53 | this.showCancel = data.showCancel
54 | this.showOK = data.showOK
55 | this.changeDetector.detectChanges()
56 | }
57 |
58 | close(value: any) {
59 | this.modal.setData({result: value}, true)
60 | this.modal.close()
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/shared/dialog.service.tns.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 |
3 | import { action, confirm } from '@nativescript/core/ui/dialogs'
4 |
5 | @Injectable({
6 | providedIn: 'root',
7 | })
8 | export class DialogService {
9 |
10 | action(
11 | title: string,
12 | message: string,
13 | actions: string[],
14 | ): Promise {
15 | const options = {
16 | title: title,
17 | message: message,
18 | actions: actions,
19 | cancelButtonText: 'Cancel',
20 | }
21 | return action(options)
22 | }
23 |
24 | confirm(title: string, message: string): Promise {
25 | const options = {
26 | title: title,
27 | message: message,
28 | okButtonText: 'Yes',
29 | cancelButtonText: 'No',
30 | neutralButtonText: 'Cancel',
31 | }
32 | return confirm(options)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/shared/dialog.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 |
3 | import { NgxSmartModalService } from 'ngx-smart-modal'
4 | import { first } from 'rxjs/operators'
5 |
6 | import { DialogComponent } from './dialog.component'
7 |
8 | @Injectable({
9 | providedIn: 'root',
10 | })
11 | export class DialogService {
12 |
13 | constructor(private modalService: NgxSmartModalService) { }
14 |
15 | private createDialog(params: {}): Promise {
16 | return new Promise((resolve) => {
17 | const modal = this.modalService
18 | .create('dialog', DialogComponent)
19 | .setData(params)
20 | .open()
21 | modal.onClose.pipe(first()).subscribe(() => {
22 | const data = modal.getData()
23 | this.modalService.removeModal('dialog')
24 | resolve(data.result)
25 | })
26 | modal.onDismiss.pipe(first()).subscribe(() => {
27 | this.modalService.removeModal('dialog')
28 | resolve(null)
29 | })
30 | })
31 | }
32 |
33 | action(
34 | title: string,
35 | message: string,
36 | actions: string[],
37 | ): Promise {
38 | return this.createDialog({
39 | title: title,
40 | message: message,
41 | actions: actions,
42 | })
43 | }
44 |
45 | confirm(title: string, message: string): Promise {
46 | return this.createDialog({
47 | title: title,
48 | message: message,
49 | showCancel: true,
50 | showOK: true,
51 | })
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/shared/file.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { CanActivate } from '@angular/router'
3 |
4 | import { RouterService } from '../shared/router.service'
5 | import { SettingsService } from '../shared/settings.service'
6 |
7 | @Injectable({
8 | providedIn: 'root',
9 | })
10 | export class FileGuard implements CanActivate {
11 |
12 | constructor(
13 | private settings: SettingsService,
14 | private router: RouterService,
15 | ) {}
16 |
17 | canActivate(): boolean {
18 | if (this.settings.path) {
19 | return true
20 | } else {
21 | // Go back to welcome screen
22 | this.router.navigate(['/welcome'])
23 | return false
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/shared/file.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing'
2 | import { HttpClientTestingModule } from '@angular/common/http/testing'
3 |
4 | import { FileService } from './file.service'
5 |
6 | describe('FileService', () => {
7 | beforeEach(() => TestBed.configureTestingModule({
8 | imports: [HttpClientTestingModule],
9 | }))
10 |
11 | it('should be created', () => {
12 | const service: FileService = TestBed.get(FileService)
13 | expect(service).toBeTruthy()
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/app/shared/file.service.tns.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 |
3 | import { File, Folder, path } from '@nativescript/core/file-system'
4 | import { isAndroid } from '@nativescript/core/platform'
5 |
6 | import { getAppId } from 'nativescript-appversion'
7 | import * as permissions from 'nativescript-permissions'
8 |
9 | export function isValidPath(path: string): boolean {
10 | if (path && File.exists(path) && !Folder.exists(path)) {
11 | return true
12 | }
13 | return false
14 | }
15 |
16 | @Injectable({
17 | providedIn: 'root',
18 | })
19 | export class FileService {
20 |
21 | private async checkPermission(): Promise {
22 | let hasPermission = true
23 | if (isAndroid) {
24 | const permissionName = 'android.permission.WRITE_EXTERNAL_STORAGE'
25 | hasPermission = permissions.hasPermission(permissionName)
26 | if (!hasPermission) {
27 | try {
28 | const result = await permissions.requestPermission(
29 | permissionName,
30 | 'Your permission is required.',
31 | )
32 | hasPermission = result[permissionName]
33 | } catch (error) {
34 | hasPermission = false
35 | }
36 | }
37 | }
38 | return hasPermission
39 | }
40 |
41 | async read(path: string): Promise {
42 | let content
43 | const hasPermission = await this.checkPermission()
44 | if (hasPermission) {
45 | const file = File.fromPath(path)
46 | content = await file.readText()
47 | } else {
48 | throw new Error('permission denied')
49 | }
50 | return content
51 | }
52 |
53 | async write(path: string, content: string): Promise {
54 | const file = File.fromPath(path)
55 | await file.writeText(content)
56 | }
57 |
58 | async create(name: string): Promise {
59 | const hasPermission = await this.checkPermission()
60 | if (!hasPermission) {
61 | throw new Error('permission denied')
62 | }
63 | if (!isAndroid) {
64 | throw new Error('not implemented')
65 | }
66 | // WARNING: deprecated in Android 10
67 | const externalPath = android.os.Environment.getExternalStorageDirectory().getAbsolutePath().toString()
68 | const appId = await getAppId()
69 | const appFolderPath = path.join(externalPath, 'data', appId)
70 | const appFolder = Folder.fromPath(appFolderPath)
71 | const file = appFolder.getFile(name)
72 | if (isValidPath(file.path)) {
73 | console.warn('file already exists on default path')
74 | } else {
75 | // Write empty string to create a file
76 | await file.writeText('')
77 | }
78 | return file.path
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/shared/file.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { HttpClient } from '@angular/common/http'
3 |
4 | import { environment } from '../../environments/environment'
5 |
6 | export function isValidPath(path: string): boolean { // eslint-disable-line @typescript-eslint/no-unused-vars
7 | return true
8 | }
9 |
10 | @Injectable({
11 | providedIn: 'root',
12 | })
13 | export class FileService {
14 |
15 | constructor(private http: HttpClient) { }
16 |
17 | async read(path: string): Promise {
18 | const encodedPath = encodeURIComponent(path)
19 | const url = `${environment.backendUrl}/file/${encodedPath}`
20 | const response = await this.http.get(url).toPromise()
21 | return (response as any).content
22 | }
23 |
24 | async write(path: string, content: string): Promise {
25 | const encodedPath = encodeURIComponent(path)
26 | const url = `${environment.backendUrl}/file/${encodedPath}`
27 | await this.http.post(url, {content: content}).toPromise()
28 | }
29 |
30 | create(name: string): Promise {
31 | return new Promise(resolve => resolve(name))
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/date-picker.tns.ts:
--------------------------------------------------------------------------------
1 | import { ModalDatetimepicker, PickerOptions } from 'nativescript-modal-datetimepicker'
2 |
3 | import { dateToString, stringToDate } from '../misc'
4 |
5 | export function openDatePicker(initialDate: string, datepicker: void): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars
6 | const picker = new ModalDatetimepicker()
7 | const options: PickerOptions = {
8 | theme: 'overlay',
9 | startingDate: initialDate ? stringToDate(initialDate) : new Date(),
10 | }
11 | return picker.pickDate(options).then((result) => {
12 | if (!result) {
13 | throw new Error('Picker cancelled')
14 | }
15 | const date = new Date(Date.UTC(result.year, result.month - 1, result.day))
16 | return dateToString(date)
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/date-picker.ts:
--------------------------------------------------------------------------------
1 | import { AngularMyDatePickerDirective, IMyDateModel } from 'angular-mydatepicker'
2 | import { first } from 'rxjs/operators'
3 |
4 | import { stringToDate } from '../misc'
5 |
6 | export function openDatePicker(initialDate: string, datepicker: AngularMyDatePickerDirective): Promise {
7 | const initialValue: IMyDateModel = {
8 | isRange: false,
9 | singleDate: {
10 | jsDate: initialDate ? stringToDate(initialDate) : new Date(),
11 | },
12 | }
13 | return new Promise((resolve, reject) => {
14 | datepicker.writeValue(initialValue)
15 | const opened = datepicker.toggleCalendar()
16 | if (!opened) {
17 | reject(new Error('Picker cancelled'))
18 | return
19 | }
20 | datepicker.dateChanged.pipe(first()).subscribe((result: IMyDateModel) => {
21 | resolve(result.singleDate.formatted)
22 | })
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/file-picker.tns.ts:
--------------------------------------------------------------------------------
1 | import { knownFolders, File, path as pathUtils } from '@nativescript/core/file-system'
2 | import { ImageAsset } from '@nativescript/core/image-asset'
3 | import { isAndroid, isIOS } from '@nativescript/core/platform'
4 |
5 | import { ImagePicker, ImagePickerMediaType } from '@nativescript/imagepicker'
6 | import { Mediafilepicker, FilePickerOptions } from 'nativescript-mediafilepicker'
7 |
8 | class AndroidFilePicker extends ImagePicker {
9 |
10 | get mimeTypes() {
11 | const mimeTypes = Array.create(java.lang.String, 1) // eslint-disable-line no-undef
12 | mimeTypes[0] = '*/*'
13 | return mimeTypes
14 | }
15 | }
16 |
17 | /**
18 | * Resolves with the file path on success or with null if cancelled
19 | */
20 | export function openFilePicker(): Promise {
21 | if (isAndroid) {
22 | const androidFilePicker = new AndroidFilePicker({
23 | mode: 'single',
24 | mediaType: ImagePickerMediaType.Any,
25 | showAdvanced: true,
26 | })
27 | return androidFilePicker.authorize()
28 | .then(() => androidFilePicker.present())
29 | .then((selection: ImageAsset[]) => {
30 | const filePath = selection[0].android
31 | if (!filePath) {
32 | throw new Error('Can not get file path')
33 | }
34 | return filePath
35 | })
36 | .catch((error) => {
37 | if (error.message === 'Image picker activity result code 0') {
38 | // Picker has been cancelled
39 | return null
40 | } else {
41 | throw error
42 | }
43 | })
44 |
45 | } else if (isIOS) {
46 | const options: FilePickerOptions = {
47 | ios: {
48 | extensions: [kUTTypeText], // eslint-disable-line no-undef
49 | multipleSelection: false,
50 | },
51 | }
52 | const iosFilePicker = new Mediafilepicker()
53 | iosFilePicker.openFilePicker(options)
54 | return new Promise((resolve, reject) => {
55 | iosFilePicker.on('getFiles', (res) => {
56 | const files = res.object.get('results')
57 | const fileUri = files[0].file
58 | const filePath = fileUri.slice(7, fileUri.length)
59 | // Copy file from temp dir to documents
60 | File.fromPath(filePath).readText().then((content) => {
61 | const docDir = knownFolders.documents()
62 | const newFilePath = pathUtils.join(docDir.path, 'todo.txt')
63 | File.fromPath(newFilePath).writeText(content).then(() => {
64 | console.info(`file copied from ${filePath} to ${newFilePath}`)
65 | resolve(newFilePath)
66 | })
67 | })
68 | })
69 | iosFilePicker.on('error', (res) => {
70 | const message = res.object.get('msg')
71 | reject(message)
72 | })
73 | iosFilePicker.on('cancel', () => {
74 | resolve(null)
75 | })
76 | })
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/file-picker.ts:
--------------------------------------------------------------------------------
1 | export function openFilePicker(): Promise {
2 | return new Promise((resolve) => {
3 | resolve('')
4 | })
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/input.tns.ts:
--------------------------------------------------------------------------------
1 | import { ElementRef } from '@angular/core'
2 |
3 | import { isAndroid } from '@nativescript/core/platform'
4 |
5 | export function focusOnInput(input: ElementRef) {
6 | input.nativeElement.focus()
7 | }
8 |
9 | export function enableInputSuggestions(input: ElementRef) {
10 | // Fix autosuggestion bug on Android
11 | if (isAndroid) {
12 | const inputType = input.nativeElement.android.getInputType()
13 | input.nativeElement.android.setInputType(
14 | inputType ^ android.text.InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE,
15 | )
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/input.ts:
--------------------------------------------------------------------------------
1 | import { ElementRef } from '@angular/core'
2 |
3 | export function focusOnInput(input: ElementRef) {
4 | input.nativeElement.focus()
5 | }
6 |
7 | export function enableInputSuggestions(input: ElementRef) { // eslint-disable-line @typescript-eslint/no-unused-vars
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/page.tns.ts:
--------------------------------------------------------------------------------
1 | import { ViewContainerRef, NgZone } from '@angular/core'
2 |
3 | import { Page } from '@nativescript/core/ui/page'
4 |
5 | export function onNavigatedTo(container: ViewContainerRef, handler: () => void) {
6 | const page = container.injector.get(Page)
7 | const ngZone = container.injector.get(NgZone)
8 | page.on('navigatedTo', () => {
9 | // Trigger change detection
10 | ngZone.run(handler)
11 | })
12 | }
13 |
14 | export function onNavigatingFrom(container: ViewContainerRef, handler: () => void) {
15 | const page = container.injector.get(Page)
16 | page.on('navigatingFrom', handler)
17 | }
18 |
19 | export function hideActionBar(container: ViewContainerRef) {
20 | const page = container.injector.get(Page)
21 | page.actionBarHidden = true
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/page.ts:
--------------------------------------------------------------------------------
1 | export function onNavigatedTo(container, handler: () => void) { // eslint-disable-line @typescript-eslint/no-unused-vars
2 | // Execute immediately
3 | handler()
4 | }
5 |
6 | export function onNavigatingFrom(container, handler: () => void) { // eslint-disable-line @typescript-eslint/no-unused-vars
7 | // Do nothing
8 | }
9 |
10 | export function hideActionBar(container) { // eslint-disable-line @typescript-eslint/no-unused-vars
11 | // Do nothing
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/platform.tns.ts:
--------------------------------------------------------------------------------
1 | export { isAndroid, isIOS } from '@nativescript/core/platform'
2 |
3 | export const isWeb = false
4 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/platform.ts:
--------------------------------------------------------------------------------
1 | export const isWeb = true
2 | export const isAndroid = false
3 | export const isIOS = false
4 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/pullrefresh.tns.ts:
--------------------------------------------------------------------------------
1 | import { PullToRefresh } from '@nstudio/nativescript-pulltorefresh'
2 |
3 | export function onPullRefresh(event, callback) {
4 | const pullRefresh = event.object as PullToRefresh
5 | setTimeout(() => {
6 | callback()
7 | pullRefresh.refreshing = false
8 | }, 0)
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/pullrefresh.ts:
--------------------------------------------------------------------------------
1 | export function onPullRefresh(event, callback) {
2 | console.log(event)
3 | console.log(callback)
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/storage.tns.ts:
--------------------------------------------------------------------------------
1 | import * as appSettings from '@nativescript/core/application-settings'
2 |
3 | export function setValue(key: string, value: string): void {
4 | appSettings.setString(key, value)
5 | }
6 |
7 | export function getValue(key: string): string {
8 | return appSettings.getString(key)
9 | }
10 |
11 | export function removeValue(key: string): void {
12 | appSettings.remove(key)
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/storage.ts:
--------------------------------------------------------------------------------
1 | export function setValue(key: string, value: string): void {
2 | localStorage.setItem(key, value)
3 | }
4 |
5 | export function getValue(key: string): string {
6 | return localStorage.getItem(key)
7 | }
8 |
9 | export function removeValue(key: string): void {
10 | localStorage.removeItem(key)
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/toast.tns.ts:
--------------------------------------------------------------------------------
1 | import { Toasty, ToastPosition, ToastDuration } from '@triniwiz/nativescript-toasty'
2 |
3 | export function showToast(text: string) {
4 | try {
5 | const toast = new Toasty({
6 | text: text,
7 | position: ToastPosition.BOTTOM,
8 | duration: ToastDuration.LONG,
9 | yAxisOffset: 10,
10 | })
11 | toast.show()
12 | } catch (error) {
13 | // Log error if view is not ready
14 | console.warn(error)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/toast.ts:
--------------------------------------------------------------------------------
1 | export function showToast(text: string, isError: boolean) {
2 | if (isError) {
3 | alert(text)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/version.tns.ts:
--------------------------------------------------------------------------------
1 | import { getVersionNameSync } from 'nativescript-appversion'
2 |
3 | export const getVersion = getVersionNameSync
4 |
--------------------------------------------------------------------------------
/src/app/shared/helpers/version.ts:
--------------------------------------------------------------------------------
1 | import { environment } from '../../../environments/environment'
2 |
3 | export function getVersion(): string {
4 | return environment.version
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/shared/misc.spec.ts:
--------------------------------------------------------------------------------
1 | import { compareEmptyGreater } from './misc'
2 |
3 | describe('compareEmptyGreater function', () => {
4 | it('should correctly compare non-empty values', () => {
5 | expect(compareEmptyGreater('A', 'B')).toBe(-1)
6 | expect(compareEmptyGreater('B', 'A')).toBe(1)
7 | expect(compareEmptyGreater('A', 'A')).toBe(0)
8 | })
9 |
10 | it('should correctly compare empty values', () => {
11 | expect(compareEmptyGreater('A', '')).toBe(-1)
12 | expect(compareEmptyGreater('', 'A')).toBe(1)
13 | expect(compareEmptyGreater('', '')).toBe(0)
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/src/app/shared/misc.ts:
--------------------------------------------------------------------------------
1 | export function dateToString(date: Date): string {
2 | // Compatible with jsTodoTxt
3 | const year = date.getFullYear()
4 | const month = ((date.getMonth() + 1 < 10) ? '0' : '') + (date.getMonth() + 1)
5 | const day = ((date.getDate() < 10) ? '0' : '') + date.getDate()
6 | return `${year}-${month}-${day}`
7 | }
8 |
9 | export function stringToDate(dateStr: string): Date {
10 | // Compatible with jsTodoTxt
11 | const datePieces = dateStr.split('-')
12 | return new Date(
13 | parseInt(datePieces[0]),
14 | parseInt(datePieces[1]) - 1,
15 | parseInt(datePieces[2]),
16 | )
17 | }
18 |
19 | function compare(v1: any, v2: any): number {
20 | // Default cmp function from the thenBy module
21 | // https://github.com/Teun/thenBy.js/blob/da9ec2149e530a4c491492cc127dbf13b1c0ae36/thenBy.js#L33
22 | return v1 < v2 ? -1 : v1 > v2 ? 1 : 0
23 | }
24 |
25 | export function compareEmptyGreater(v1: any, v2: any): number {
26 | // This cmp function assumes that empty value is greater then non-empty
27 | if (v1 && v2) {
28 | return compare(v1, v2)
29 | } else {
30 | return v2 ? 1 : v1 ? -1 : 0
31 | }
32 | }
33 |
34 | export function escapeRegExp(value: string) {
35 | return value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') // eslint-disable-line no-useless-escape
36 | }
37 |
38 | export function splitStringWithSpace(value: string): string[] {
39 | return value.trim().split(/\s+/)
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/shared/router.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Location } from '@angular/common'
2 | import { TestBed } from '@angular/core/testing'
3 | import { Router } from '@angular/router'
4 |
5 | import { RouterService } from './router.service'
6 |
7 | describe('RouterService', () => {
8 | beforeEach(() => TestBed.configureTestingModule({
9 | providers: [
10 | {provide: Router, useValue: {}},
11 | {provide: Location, useValue: {}},
12 | ],
13 | }))
14 |
15 | it('should be created', () => {
16 | const service: RouterService = TestBed.get(RouterService)
17 | expect(service).toBeTruthy()
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/app/shared/router.service.tns.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { RouterExtensions } from '@nativescript/angular'
3 |
4 | @Injectable({
5 | providedIn: 'root',
6 | })
7 | export class RouterService {
8 |
9 | constructor(private router: RouterExtensions) { }
10 |
11 | navigate(parameters: any[], extras?: any) {
12 | this.router.navigate(parameters, extras)
13 | }
14 |
15 | backToPreviousPage() {
16 | this.router.backToPreviousPage()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/shared/router.service.ts:
--------------------------------------------------------------------------------
1 | import { Location } from '@angular/common'
2 | import { Injectable } from '@angular/core'
3 | import { Router } from '@angular/router'
4 |
5 | @Injectable({
6 | providedIn: 'root',
7 | })
8 | export class RouterService {
9 |
10 | constructor(
11 | private location: Location,
12 | private router: Router,
13 | ) { }
14 |
15 | navigate(parameters: any[], extras?: any) {
16 | this.router.navigate(parameters, extras)
17 | }
18 |
19 | backToPreviousPage() {
20 | this.location.back()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/shared/settings.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing'
2 |
3 | import { SettingsService } from './settings.service'
4 |
5 | describe('SettingsService', () => {
6 | beforeEach(() => TestBed.configureTestingModule({}))
7 |
8 | it('should be created', () => {
9 | const service: SettingsService = TestBed.get(SettingsService)
10 | expect(service).toBeTruthy()
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/src/app/shared/settings.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 |
3 | import { Settings, TaskFilter } from './settings'
4 | import {
5 | setValue,
6 | getValue,
7 | removeValue,
8 | } from './helpers/storage'
9 |
10 | @Injectable({
11 | providedIn: 'root',
12 | })
13 | export class SettingsService {
14 |
15 | get path(): string {
16 | return getValue(Settings.Path)
17 | }
18 |
19 | set path(path: string) {
20 | if (!path) {
21 | throw Error('Path can not be empty.')
22 | }
23 | setValue(Settings.Path, path)
24 | }
25 |
26 | get filter(): TaskFilter {
27 | const filterStr = getValue(Settings.TaskFilter)
28 | if (!filterStr) {
29 | return {}
30 | }
31 | const filter = JSON.parse(filterStr)
32 | if (filter.dueDate) {
33 | filter.dueDate = new Date(filter.dueDate)
34 | }
35 | return filter
36 | }
37 |
38 | set filter(filter: TaskFilter) {
39 | const filterStr = JSON.stringify(filter)
40 | setValue(Settings.TaskFilter, filterStr)
41 | }
42 |
43 | get ordering(): string[] {
44 | const orderingStr = getValue(Settings.TaskOrdering)
45 | if (!orderingStr) {
46 | return []
47 | }
48 | return JSON.parse(orderingStr)
49 | }
50 |
51 | set ordering(ordering: string[]) {
52 | const orderingStr = JSON.stringify(ordering)
53 | setValue(Settings.TaskOrdering, orderingStr)
54 | }
55 |
56 | reset() {
57 | removeValue(Settings.TaskFilter)
58 | removeValue(Settings.TaskOrdering)
59 | removeValue(Settings.Path)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/shared/settings.ts:
--------------------------------------------------------------------------------
1 | export enum Settings {
2 | Path = 'todoFilePath',
3 | TaskFilter = 'taskFilter',
4 | TaskOrdering = 'taskOrdering',
5 | }
6 |
7 | export interface TaskFilter {
8 | project?: string;
9 | context?: string;
10 | dueDate?: Date;
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/shared/todo-file.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing'
2 |
3 | import { FileService } from './file.service'
4 | import { RouterService } from './router.service'
5 | import { TodoFileService } from './todo-file.service'
6 |
7 | describe('TodoFileService', () => {
8 | let fileContent
9 |
10 | beforeEach(() => TestBed.configureTestingModule({
11 | providers: [
12 | {
13 | provide: FileService,
14 | useValue: {
15 | read: () => fileContent,
16 | write: () => {},
17 | },
18 | },
19 | {provide: RouterService, useValue: {}},
20 | ],
21 | }))
22 |
23 | it('should be created', () => {
24 | const service: TodoFileService = TestBed.get(TodoFileService)
25 | expect(service).toBeTruthy()
26 | expect(service.content).toBe('')
27 | expect(service.fileChanged).toBeDefined()
28 | // Private property
29 | expect(service['todoItems']).toEqual([]) // eslint-disable-line dot-notation
30 | })
31 |
32 | it('should get tasks', async () => {
33 | const service: TodoFileService = TestBed.get(TodoFileService)
34 | fileContent = 'task1\ntask2\ntask3'
35 | await service.load()
36 | const tasks = service.getTasks()
37 | expect(tasks.length).toBe(3)
38 | // IDs are starting with 1
39 | expect(tasks[0].id).toBe(1)
40 | expect(tasks[0].text).toBe('task1')
41 | })
42 |
43 | it('should get projects', async () => {
44 | const service: TodoFileService = TestBed.get(TodoFileService)
45 | fileContent = (
46 | '(A) task1 +project1 +project2\n' +
47 | 'task2 +project3 +project1\n' +
48 | 'x 2019-09-01 task3 +project4\n')
49 | await service.load()
50 | const projects = service.getProjects()
51 | expect(projects).toEqual([
52 | 'project1',
53 | 'project2',
54 | 'project3',
55 | 'project4',
56 | ])
57 | })
58 |
59 | it('should get colors', async () => {
60 | const service: TodoFileService = TestBed.get(TodoFileService)
61 | fileContent = (
62 | '(A) task1 color:#cccccc\n' +
63 | 'task2 color:#ffffff\n' +
64 | 'x 2019-09-01 task3 color:#cccccc\n')
65 | await service.load()
66 | const colors = service.getColors()
67 | expect(colors).toEqual(['#cccccc', '#ffffff'])
68 | })
69 |
70 | it('should remove task', async () => {
71 | const service: TodoFileService = TestBed.get(TodoFileService)
72 | fileContent = 'task1\ntask2\ntask3'
73 | await service.load()
74 | // Get private property
75 | const todoItems = service['todoItems'] // eslint-disable-line dot-notation
76 | expect(todoItems.length).toBe(3)
77 | await service.removeTask(2)
78 | expect(todoItems.length).toBe(3)
79 | expect(todoItems[1]).toBeUndefined()
80 | expect(service.content).toEqual('task1\ntask3')
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/src/app/shared/todo-file.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, OnDestroy } from '@angular/core'
2 |
3 | import { TodoTxt, TodoTxtItem } from 'jstodotxt'
4 | import { Subject, Subscription, interval } from 'rxjs'
5 |
6 | import { FileService } from './file.service'
7 | import { RouterService } from './router.service'
8 | import { SettingsService } from './settings.service'
9 | import { Task, TaskData, getExtensions } from './task'
10 | import { showToast } from './helpers/toast'
11 |
12 | const FILE_WATCH_INTERVAL = 60 * 1000
13 |
14 | @Injectable({
15 | providedIn: 'root',
16 | })
17 | export class TodoFileService implements OnDestroy {
18 |
19 | content = '';
20 | fileLoaded: Promise;
21 | fileChanged: Subject;
22 | private watcher: Subscription;
23 | private todoItems: TodoTxtItem[] = [];
24 |
25 | constructor(
26 | private file: FileService,
27 | private router: RouterService,
28 | private settings: SettingsService,
29 | ) {
30 | this.fileChanged = new Subject()
31 | // Initial load
32 | if (settings.path) {
33 | this.initialLoad()
34 | }
35 | // Periodic reload
36 | this.watcher = interval(FILE_WATCH_INTERVAL).subscribe(() => {
37 | if (!settings.path) {
38 | // No path to watch
39 | return
40 | }
41 | this.load(true)
42 | })
43 | }
44 |
45 | initialLoad(): Promise {
46 | // Called on init and every time the file is switched
47 | this.fileLoaded = this.load()
48 | return this.fileLoaded
49 | }
50 |
51 | ngOnDestroy(): void {
52 | // Stop file watcher when service is destroyed
53 | this.watcher.unsubscribe()
54 | }
55 |
56 | getTask(taskId: number): Task {
57 | return new Task(this.todoItems[taskId - 1])
58 | }
59 |
60 | getTasks(): Task[] {
61 | return this.todoItems.map((todoItem, index) => {
62 | const task = new Task(todoItem)
63 | // Set IDs (line number, starting with 1, similar to todo.sh)
64 | // TODO: index can change if file has been updated from another device
65 | // TODO: use UUIDs?
66 | task.id = index + 1
67 | return task
68 | })
69 | }
70 |
71 | getProjects(): string[] {
72 | const projects = new Set()
73 | this.todoItems.forEach((todoItem: TodoTxtItem) => {
74 | (todoItem.projects || []).forEach((project: string) => {
75 | projects.add(project)
76 | })
77 | })
78 | return Array.from(projects).sort()
79 | }
80 |
81 | getContexts(): string[] {
82 | const contexts = new Set()
83 | this.todoItems.forEach((todoItem: TodoTxtItem) => {
84 | (todoItem.contexts || []).forEach((context: string) => {
85 | contexts.add(context)
86 | })
87 | })
88 | return Array.from(contexts).sort()
89 | }
90 |
91 | getColors(): string[] {
92 | const colors = new Set()
93 | this.todoItems.forEach((todoItem: TodoTxtItem) => {
94 | if (todoItem.color) {
95 | colors.add(todoItem.color)
96 | }
97 | })
98 | return Array.from(colors)
99 | }
100 |
101 | async createTask(taskData: TaskData): Promise {
102 | const task = Task.create(taskData)
103 | await this.appendTask(task)
104 | }
105 |
106 | async appendTask(task: Task): Promise {
107 | // Append to the end of file
108 | this.todoItems.push(task.todoItem)
109 | await this.save()
110 | }
111 |
112 | async updateTask(taskId: number, taskData: TaskData): Promise {
113 | const task = new Task(this.todoItems[taskId - 1])
114 | task.update(taskData)
115 | await this.save()
116 | }
117 |
118 | async replaceTask(taskId: number, task: Task): Promise {
119 | this.todoItems[taskId - 1] = task.todoItem
120 | await this.save()
121 | }
122 |
123 | async removeTask(taskId: number): Promise {
124 | delete this.todoItems[taskId - 1] // Keeps task IDs intact
125 | await this.save()
126 | }
127 |
128 | async load(watch = false): Promise {
129 | let content
130 | try {
131 | content = (await this.file.read(this.settings.path)).trim()
132 | } catch (error) {
133 | if (!watch) {
134 | this.router.navigate(['/settings'])
135 | }
136 | console.error(error)
137 | showToast(error.toString(), true)
138 | return
139 | }
140 | if (watch && this.content && this.content === content) {
141 | // No changes for watcher
142 | return
143 | }
144 | this.content = content
145 | if (this.content === '') {
146 | this.todoItems = []
147 | } else {
148 | this.todoItems = TodoTxt.parse(this.content, getExtensions())
149 | }
150 | this.fileChanged.next(true) // true = IDs are probably changed
151 | showToast('File loaded', false)
152 | }
153 |
154 | private async save(): Promise {
155 | this.content = TodoTxt.render(this.todoItems)
156 | try {
157 | await this.file.write(this.settings.path, this.content)
158 | } catch (error) {
159 | console.error(error)
160 | showToast(error.toString(), true)
161 | return
162 | }
163 | this.fileChanged.next(false) // false => IDs are not changed
164 | }
165 |
166 | async create(): Promise {
167 | return this.file.create('todo.txt')
168 | }
169 |
170 | }
171 |
--------------------------------------------------------------------------------
/src/app/shared/validators.ts:
--------------------------------------------------------------------------------
1 | import { AbstractControl, ValidatorFn } from '@angular/forms'
2 |
3 | import { isValidPath } from './file.service'
4 |
5 | export function FilePathValidator(): ValidatorFn {
6 | return (control: AbstractControl): {[key: string]: any} | null => {
7 | const path = control.value
8 | if (!path) {
9 | return
10 | }
11 | if (!isValidPath(path)) {
12 | return {
13 | invalidPath: {
14 | value: control.value,
15 | },
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/tag-list/tag-list.component.html:
--------------------------------------------------------------------------------
1 |
4 |
5 |
0"
7 | class="subheader"
8 | >Projects
9 |
14 | {{ '+' + project }}
15 |
16 |
0"
18 | class="subheader"
19 | >Contexts
20 |
25 | {{ '@' + context }}
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/app/tag-list/tag-list.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | .page {
5 | width: 33%;
6 | }
7 |
8 | .subheader {
9 | margin: 0 0 $main-padding;
10 | }
11 |
12 | .tag {
13 | @extend %tag;
14 | display: inline-block;
15 | font-size: 16px;
16 | margin: 0 $main-padding $main-padding 0;
17 |
18 | &:hover {
19 | background-color: $tag-hover-color;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/tag-list/tag-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 |
3 | import { TagListComponent } from './tag-list.component'
4 | import { FileService } from '../shared/file.service'
5 | import { RouterService } from '../shared/router.service'
6 | import { SettingsService } from '../shared/settings.service'
7 |
8 | describe('TagListComponent', () => {
9 | let component: TagListComponent
10 | let fixture: ComponentFixture
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [TagListComponent],
15 | providers: [
16 | {provide: FileService, useValue: {read: () => 'test'}},
17 | {provide: RouterService, useValue: {}},
18 | {provide: SettingsService, useValue: {path: 'test'}},
19 | ],
20 | }).compileComponents()
21 | }))
22 |
23 | beforeEach(() => {
24 | fixture = TestBed.createComponent(TagListComponent)
25 | component = fixture.componentInstance
26 | fixture.detectChanges()
27 | })
28 |
29 | it('should create', () => {
30 | expect(component).toBeTruthy()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/app/tag-list/tag-list.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
18 |
19 |
23 |
24 |
29 |
33 |
40 |
41 |
46 |
50 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/app/tag-list/tag-list.component.tns.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-variables';
3 |
4 | .subheader {
5 | font-size: 16;
6 | margin: $main-padding $main-padding 0;
7 | }
8 |
9 | .tag {
10 | @extend %tag;
11 | font-size: 16;
12 | margin: $main-padding 0 0 $main-padding;
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/tag-list/tag-list.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, OnDestroy, ViewContainerRef } from '@angular/core'
2 |
3 | import { Subscription } from 'rxjs'
4 |
5 | import { RouterService } from '../shared/router.service'
6 | import { TaskFilter } from '../shared/settings'
7 | import { SettingsService } from '../shared/settings.service'
8 | import { SideDrawerService } from '../nav/sidedrawer.service'
9 | import { TodoFileService } from '../shared/todo-file.service'
10 | import { onNavigatedTo, onNavigatingFrom } from '../shared/helpers/page'
11 |
12 | @Component({
13 | selector: 'ms-tag-list',
14 | templateUrl: './tag-list.component.html',
15 | styleUrls: ['./tag-list.component.scss'],
16 | })
17 | export class TagListComponent implements OnInit, OnDestroy {
18 |
19 | title = 'Tags';
20 | projects: string[] = [];
21 | contexts: string[] = [];
22 | private fileSubscription: Subscription;
23 |
24 | constructor(
25 | private router: RouterService,
26 | private settings: SettingsService,
27 | private sideDrawer: SideDrawerService,
28 | private todoFile: TodoFileService,
29 | private view: ViewContainerRef,
30 | ) { }
31 |
32 | ngOnInit() {
33 | this.todoFile.fileLoaded.then(() => this.updatePage())
34 | onNavigatedTo(this.view, () => {
35 | this.fileSubscribe()
36 | })
37 | onNavigatingFrom(this.view, () => {
38 | this.fileUnsubscribe()
39 | })
40 | }
41 |
42 | ngOnDestroy() {
43 | this.fileUnsubscribe()
44 | }
45 |
46 | private updatePage() {
47 | this.projects = this.todoFile.getProjects()
48 | this.contexts = this.todoFile.getContexts()
49 | }
50 |
51 | private fileSubscribe() {
52 | this.fileSubscription = this.todoFile.fileChanged.subscribe(() => {
53 | this.updatePage()
54 | })
55 | }
56 |
57 | private fileUnsubscribe() {
58 | this.fileSubscription.unsubscribe()
59 | }
60 |
61 | openDrawer() {
62 | this.sideDrawer.open(this.view)
63 | }
64 |
65 | showTaskList(filter: TaskFilter) {
66 | this.settings.filter = filter
67 | this.router.navigate(['/tasks'])
68 | }
69 |
70 | switchToTasks(event: any): void {
71 | if (event.direction !== 1) {
72 | return
73 | }
74 | this.router.navigate(['/tasks'])
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form-autocomplete.component.html:
--------------------------------------------------------------------------------
1 |
11 |
12 | - {{ suggestion }}
18 |
19 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form-autocomplete.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | .suggestions {
5 | box-sizing: border-box;
6 | list-style-type: none;
7 | margin: 0 0 5px;
8 | max-height: 200px;
9 | min-width: 300px;
10 | overflow-y: auto;
11 | padding: 0 2px;
12 | width: 33%;
13 | }
14 |
15 | .suggestion {
16 | background-color: #FFF;
17 | cursor: pointer;
18 | font-size: 16px;
19 | padding: $main-padding;
20 |
21 | &:hover,
22 | &.highlighted {
23 | background-color: $menu-color;
24 | }
25 |
26 | &:last-child {
27 | border-radius: 0 0 $field-border-radius $field-border-radius;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form-autocomplete.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 | import { ReactiveFormsModule, FormControl } from '@angular/forms'
3 |
4 | import { TaskFormAutocompleteComponent } from './task-form-autocomplete.component'
5 |
6 | describe('TaskFormAutocompleteComponent', () => {
7 | let component: TaskFormAutocompleteComponent
8 | let fixture: ComponentFixture
9 |
10 | beforeEach(async(() => {
11 | TestBed.configureTestingModule({
12 | declarations: [
13 | TaskFormAutocompleteComponent,
14 | ],
15 | imports: [ReactiveFormsModule],
16 | }).compileComponents()
17 | }))
18 |
19 | beforeEach(() => {
20 | fixture = TestBed.createComponent(TaskFormAutocompleteComponent)
21 | component = fixture.componentInstance
22 | component.items = ['pro1', 'pro2']
23 | component.inputControl = new FormControl('')
24 | fixture.detectChanges()
25 | })
26 |
27 | it('should create', () => {
28 | expect(component).toBeTruthy()
29 | })
30 |
31 | it('should highlight project', () => {
32 | component.items = ['pro1', 'pro2']
33 | component.inputControl.setValue('pro')
34 | component.isVisible = true
35 | expect(component.highlightedItem).toBeUndefined()
36 | component.navigate({key: 'ArrowDown'})
37 | expect(component.highlightedItem).toEqual('pro1')
38 | component.navigate({key: 'ArrowDown'})
39 | expect(component.highlightedItem).toEqual('pro2')
40 | component.navigate({key: 'ArrowDown'})
41 | expect(component.highlightedItem).toEqual('pro2')
42 | component.navigate({key: 'ArrowUp'})
43 | expect(component.highlightedItem).toEqual('pro1')
44 | component.navigate({key: 'ArrowUp'})
45 | expect(component.highlightedItem).toEqual('pro1')
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form-autocomplete.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
15 |
16 |
17 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form-autocomplete.component.tns.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-variables';
3 |
4 | .field-with-suggestions {
5 | margin-bottom: 0;
6 | }
7 |
8 | .clear-btn {
9 | margin: 5 0 0;
10 | }
11 |
12 | .suggestions {
13 | margin: 0 2 5;
14 | }
15 |
16 | .suggestion {
17 | background-color: #FFF;
18 | font-size: 16;
19 | padding: $main-padding;
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form-autocomplete.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core'
2 | import { FormControl } from '@angular/forms'
3 |
4 | import { escapeRegExp } from '../shared/misc'
5 | import { isAndroid, isIOS, isWeb } from '../shared/helpers/platform'
6 |
7 | @Component({
8 | selector: 'task-form-autocomplete',
9 | templateUrl: './task-form-autocomplete.component.html',
10 | styleUrls: ['./task-form-autocomplete.component.scss'],
11 | })
12 | export class TaskFormAutocompleteComponent {
13 |
14 | @Input() items: string[];
15 | @Input() inputControl: FormControl;
16 |
17 | isVisible = false;
18 | isLocked = false;
19 | highlightedItem: string;
20 |
21 | @ViewChild('inputField', {static: false})
22 | inputField: ElementRef;
23 |
24 | getSuggestions(): string[] {
25 | if (!this.isVisible) {
26 | return []
27 | }
28 | const value = this.inputControl.value
29 | if (!value) {
30 | return []
31 | }
32 | const pieces = value.split(/\s+/)
33 | if (pieces.length === 0) {
34 | return []
35 | }
36 | const search = pieces[pieces.length - 1]
37 | const searchRegexp = new RegExp(escapeRegExp(search), 'iu')
38 | return this.items.filter((item: string) => {
39 | return item.search(searchRegexp) !== -1
40 | }).sort()
41 | }
42 |
43 | select(item: string, lock = false): void {
44 | if (lock && (isIOS || isWeb)) {
45 | // Prevent suggestions list from hiding on blur event
46 | this.isLocked = true
47 | }
48 | const pieces = this.inputControl.value.split(/\s+/)
49 | pieces[pieces.length - 1] = item
50 | const newValue = pieces.join(' ')
51 | this.inputControl.setValue(newValue)
52 | if (isAndroid) {
53 | // Move cursor to the end of string
54 | this.inputField.nativeElement.android.setSelection(newValue.length)
55 | }
56 | }
57 |
58 | removeItem(): void {
59 | const pieces = this.inputControl.value.split(/\s+/)
60 | pieces.pop()
61 | const newValue = pieces.join(' ')
62 | this.inputControl.setValue(newValue)
63 | if (isAndroid) {
64 | // Move cursor to the end of string
65 | this.inputField.nativeElement.android.setSelection(newValue.length)
66 | }
67 | }
68 |
69 | show(): void {
70 | this.isVisible = true
71 | }
72 |
73 | hide(): void {
74 | // On iOS and Web the "blur" event is emitted before "tap" event,
75 | // so we need to delay hiding
76 | setTimeout(() => {
77 | if (this.isLocked) {
78 | // Unlock suggestion list and move focus back to input field
79 | this.isLocked = false
80 | this.inputField.nativeElement.focus()
81 | } else {
82 | // Hide suggestion list
83 | this.isVisible = false
84 | }
85 | }, 100)
86 |
87 | }
88 |
89 | navigate(event: any): void {
90 | if (isWeb) {
91 | // Select project from keyboard
92 | const key = event.key
93 | if (key === 'ArrowUp' || key === 'ArrowDown') {
94 | const suggestions = this.getSuggestions()
95 | let highlightedIndex = suggestions.indexOf(this.highlightedItem)
96 | if (key === 'ArrowDown' && highlightedIndex < suggestions.length - 1) {
97 | // Select next project
98 | highlightedIndex++
99 | } else if (key === 'ArrowUp' && highlightedIndex > 0) {
100 | // Select previous project
101 | highlightedIndex--
102 | }
103 | this.highlightedItem = suggestions[highlightedIndex]
104 | }
105 | if (key === 'Enter' && this.highlightedItem) {
106 | event.preventDefault()
107 | this.select(this.highlightedItem)
108 | delete this.highlightedItem
109 | }
110 | }
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form.component.html:
--------------------------------------------------------------------------------
1 |
4 |
136 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | .priority {
5 | @extend %tag;
6 |
7 | &.priority-a {
8 | background-color: $priority-a;
9 | }
10 |
11 | &.priority-b {
12 | background-color: $priority-b;
13 | }
14 |
15 | &.priority-c {
16 | background-color: $priority-c;
17 | }
18 |
19 | &.priority-d {
20 | background-color: $priority-d;
21 | }
22 | }
23 |
24 | #due-date-wrapper {
25 | position: relative;
26 | }
27 |
28 | .field-wrapper .material-icons {
29 | font-size: 20px;
30 | }
31 |
32 | .color {
33 | border: 1px solid $field-border-color;
34 | border-radius: $field-border-radius;
35 | box-sizing: border-box;
36 | display: inline-block;
37 | height: 20px;
38 | width: 20px;
39 | }
40 |
41 | #save-btn {
42 | margin-top: 5px;
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 | import { ReactiveFormsModule } from '@angular/forms'
3 | import { ActivatedRoute } from '@angular/router'
4 |
5 | import { AngularMyDatePickerModule } from 'angular-mydatepicker'
6 |
7 | import { TaskFormComponent } from './task-form.component'
8 | import { TaskFormAutocompleteComponent } from './task-form-autocomplete.component'
9 | import { DialogService } from '../shared/dialog.service'
10 | import { FileService } from '../shared/file.service'
11 | import { RouterService } from '../shared/router.service'
12 | import { SettingsService } from '../shared/settings.service'
13 |
14 | describe('TaskFormComponent', () => {
15 | let component: TaskFormComponent
16 | let fixture: ComponentFixture
17 |
18 | beforeEach(async(() => {
19 | TestBed.configureTestingModule({
20 | declarations: [
21 | TaskFormComponent,
22 | TaskFormAutocompleteComponent,
23 | ],
24 | imports: [ReactiveFormsModule, AngularMyDatePickerModule],
25 | providers: [
26 | {provide: DialogService, useValue: {}},
27 | {provide: FileService, useValue: {read: () => 'test'}},
28 | {provide: RouterService, useValue: {}},
29 | {provide: ActivatedRoute, useValue: {snapshot: {params: {}}}},
30 | {
31 | provide: SettingsService,
32 | useValue: {path: 'test', filter: {}, ordering: []},
33 | },
34 | ],
35 | }).compileComponents()
36 | }))
37 |
38 | beforeEach(() => {
39 | fixture = TestBed.createComponent(TaskFormComponent)
40 | component = fixture.componentInstance
41 | fixture.detectChanges()
42 | })
43 |
44 | it('should create', () => {
45 | expect(component).toBeTruthy()
46 | })
47 | })
48 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
22 |
23 |
24 |
25 |
26 |
27 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
55 |
59 |
64 |
65 |
66 |
67 |
68 |
69 |
75 |
80 |
84 |
89 |
93 |
98 |
99 |
100 |
101 |
102 |
107 |
112 |
113 |
114 |
115 |
116 |
121 |
125 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/src/app/task-form/task-form.component.tns.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-variables';
3 |
4 | .priority {
5 | @extend %tag;
6 |
7 | &.priority-a {
8 | background-color: $priority-a;
9 | }
10 |
11 | &.priority-b {
12 | background-color: $priority-b;
13 | }
14 |
15 | &.priority-c {
16 | background-color: $priority-c;
17 | }
18 |
19 | &.priority-d {
20 | background-color: $priority-d;
21 | }
22 | }
23 |
24 | .color {
25 | border-color: $field-border-color;
26 | border-radius: $field-border-radius;
27 | border-width: 1;
28 | height: 22;
29 | width: 22;
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/task-list/task-list.component.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
21 |
26 |
27 |
32 |
33 |
34 |
39 |
71 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/app/task-list/task-list.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | .header {
5 | display: flex;
6 |
7 | .sort {
8 | margin-left: auto;
9 | }
10 | }
11 |
12 | .page {
13 | align-items: flex-start;
14 | display: flex;
15 | flex-direction: column;
16 | }
17 |
18 | #add-task-btn {
19 | break-after: always;
20 | margin-bottom: $main-padding;
21 | }
22 |
23 | .task {
24 | background-color: $task-color;
25 | border-color: $task-border-color;
26 | border-radius: 5px;
27 | border-style: solid;
28 | border-width: 1px;
29 | box-sizing: border-box;
30 | color: $text-color;
31 | font-family: 'Vollkorn', serif;
32 | font-size: 17px;
33 | margin-bottom: 5px;
34 | min-width: 33%;
35 | padding: $main-padding;
36 |
37 | &:hover {
38 | border: 1px solid $menu-color;
39 | }
40 | }
41 |
42 | .task-body {
43 | display: flex;
44 | }
45 |
46 | $task-checkbox-size: 20px;
47 |
48 | .task-checkbox {
49 | background-color: $task-checkbox-color;
50 | border-color: $task-checkbox-text-color;
51 | border-radius: 3px;
52 | border-style: solid;
53 | border-width: 1px;
54 | color: $task-checkbox-text-color;
55 | cursor: pointer;
56 | height: $task-checkbox-size;
57 | margin-right: $main-padding;
58 | min-width: $task-checkbox-size;
59 | text-align: center;
60 | width: $task-checkbox-size;
61 |
62 | i {
63 | font-size: 19px;
64 | font-weight: bold;
65 | line-height: $task-checkbox-size;
66 | }
67 |
68 | &:hover {
69 | border-color: #999;
70 | color: #999;
71 | }
72 | }
73 |
74 | .task-text {
75 | flex-grow: 1;
76 | margin-right: 15px;
77 | padding-top: 2px;
78 |
79 | ::ng-deep code {
80 | font-size: 14px;
81 | }
82 |
83 | &.completed {
84 | color: $task-completed-color;
85 | text-decoration: line-through;
86 |
87 | ::ng-deep a {
88 | color: $task-completed-color;
89 | }
90 | }
91 | }
92 |
93 | .tags {
94 | display: flex;
95 | font-family: $main-font;
96 | font-size: 12px;
97 | height: $task-checkbox-size;
98 | padding-top: 1px;
99 | text-align: right;
100 | }
101 |
102 | .tag {
103 | @extend %tag;
104 | line-height: $task-checkbox-size - 2 * 2px;
105 | margin-left: 5px;
106 |
107 | &:first-child {
108 | margin-left: 0;
109 | }
110 |
111 | &.priority-a {
112 | background-color: $priority-a;
113 | }
114 |
115 | &.priority-b {
116 | background-color: $priority-b;
117 | }
118 |
119 | &.priority-c {
120 | background-color: $priority-c;
121 | }
122 |
123 | &.priority-d {
124 | background-color: $priority-d;
125 | }
126 |
127 | &.overdue {
128 | background-color: $priority-a;
129 | }
130 | }
131 |
132 | .project,
133 | .context,
134 | .due-date {
135 | cursor: pointer;
136 |
137 | &:hover {
138 | background-color: $tag-hover-color;
139 | }
140 | }
141 |
142 | .task-menu {
143 | color: #999;
144 | font-size: 22px;
145 |
146 | &:hover {
147 | color: $link-color;
148 | }
149 | }
150 |
151 | @media (max-width: 1000px) {
152 | .task {
153 | width: 100%;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/app/task-list/task-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 |
3 | import { TaskListComponent } from './task-list.component'
4 | import { DialogService } from '../shared/dialog.service'
5 | import { FileService } from '../shared/file.service'
6 | import { RouterService } from '../shared/router.service'
7 | import { SettingsService } from '../shared/settings.service'
8 |
9 | describe('TaskListComponent', () => {
10 | let component: TaskListComponent
11 | let fixture: ComponentFixture
12 |
13 | beforeEach(async(() => {
14 | TestBed.configureTestingModule({
15 | declarations: [TaskListComponent],
16 | providers: [
17 | {provide: DialogService, useValue: {}},
18 | {provide: FileService, useValue: {read: () => 'test'}},
19 | {
20 | provide: RouterService,
21 | useValue: {
22 | onNavigatedTo: () => {},
23 | onNavigatingFrom: () => {},
24 | },
25 | },
26 | {
27 | provide: SettingsService,
28 | useValue: {
29 | path: 'test',
30 | filter: {},
31 | ordering: [],
32 | },
33 | },
34 | ],
35 | }).compileComponents()
36 | }))
37 |
38 | beforeEach(() => {
39 | fixture = TestBed.createComponent(TaskListComponent)
40 | component = fixture.componentInstance;
41 | (component as any).fileSubscription = {unsubscribe: () => {}}
42 | fixture.detectChanges()
43 | })
44 |
45 | it('should create', () => {
46 | expect(component).toBeTruthy()
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/src/app/task-list/task-list.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
9 |
17 |
26 |
34 |
35 |
36 |
42 |
43 |
49 |
50 |
51 |
52 |
56 |
61 |
67 |
73 |
79 |
84 |
89 |
90 |
91 |
98 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
115 |
116 |
--------------------------------------------------------------------------------
/src/app/task-list/task-list.component.tns.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-variables';
3 |
4 | .task {
5 | @extend %task;
6 | }
7 |
8 | .tags {
9 | font-family: sans-serif;
10 | font-size: 14;
11 | text-align: right;
12 | }
13 |
14 | .tag {
15 | @extend %tag;
16 | margin-left: 5;
17 |
18 | &.priority-a {
19 | background-color: $priority-a;
20 | }
21 |
22 | &.priority-b {
23 | background-color: $priority-b;
24 | }
25 |
26 | &.priority-c {
27 | background-color: $priority-c;
28 | }
29 |
30 | &.priority-d {
31 | background-color: $priority-d;
32 | }
33 |
34 | &.overdue {
35 | background-color: $priority-a;
36 | }
37 | }
38 |
39 | .task-checkbox {
40 | background-color: $task-checkbox-color;
41 | border-color: $task-checkbox-text-color;
42 | border-radius: 3;
43 | border-width: 1;
44 | color: $task-checkbox-text-color;
45 | font-size: 24;
46 | font-weight: bold;
47 | height: 30;
48 | margin-right: $main-padding;
49 | max-height: 30;
50 | min-width: 30;
51 | padding: 1 0;
52 | text-align: center;
53 | vertical-align: middle;
54 | width: 30;
55 | }
56 |
57 | .task-text {
58 | background-color: transparent; /* required on iOS */
59 | link-color: $link-color;
60 | margin-top: 3;
61 | }
62 |
63 | .task-text.completed {
64 | color: $task-completed-color;
65 | link-color: $task-completed-color;
66 | text-decoration: line-through;
67 | }
68 |
69 | #add-task-btn {
70 | background-color: #C2CC87;
71 | color: $text-color;
72 | font-size: 12;
73 | height: 70;
74 | horizontal-align: right;
75 | margin: 0 20 20 0;
76 | ripple-color: $text-color;
77 | vertical-align: bottom;
78 | width: 70;
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.component.html:
--------------------------------------------------------------------------------
1 |
2 |
Welcome!
3 |
4 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-web';
3 |
4 | #save-btn {
5 | margin-top: 10px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'
2 | import { ReactiveFormsModule } from '@angular/forms'
3 |
4 | import { FileService } from '../shared/file.service'
5 | import { RouterService } from '../shared/router.service'
6 | import { WelcomeComponent } from './welcome.component'
7 |
8 | describe('WelcomeComponent', () => {
9 | let component: WelcomeComponent
10 | let fixture: ComponentFixture
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [WelcomeComponent],
15 | imports: [ReactiveFormsModule],
16 | providers: [
17 | {provide: FileService, useValue: {}},
18 | {provide: RouterService, useValue: {}},
19 | ],
20 | }).compileComponents()
21 | }))
22 |
23 | beforeEach(() => {
24 | fixture = TestBed.createComponent(WelcomeComponent)
25 | component = fixture.componentInstance
26 | fixture.detectChanges()
27 | })
28 |
29 | it('should create', () => {
30 | expect(component).toBeTruthy()
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.component.tns.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
12 |
13 |
17 |
21 |
22 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.component.tns.scss:
--------------------------------------------------------------------------------
1 | @import '../../app-common';
2 | @import '../../app-variables';
3 |
4 | .page {
5 | background: linear-gradient(to bottom, $page-color, #FDFDF9);
6 | font-size: 16;
7 | }
8 |
9 | #welcome-header {
10 | font-size: 46;
11 | font-weight: 300;
12 | margin-bottom: 16;
13 | }
14 |
15 | #welcome-text {
16 | background-color: transparent; /* required on iOS */
17 | font-size: 16;
18 | link-color: $link-color;
19 | margin-bottom: 32;
20 | }
21 |
22 | .field {
23 | margin: 5 0 0;
24 | }
25 |
26 | .btn {
27 | margin-top: 10;
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit, ViewContainerRef } from '@angular/core'
2 | import { FormBuilder, FormGroup } from '@angular/forms'
3 |
4 | import { RouterService } from '../shared/router.service'
5 | import { SettingsService } from '../shared/settings.service'
6 | import { SideDrawerService } from '../nav/sidedrawer.service'
7 | import { TodoFileService } from '../shared/todo-file.service'
8 | import { FilePathValidator } from '../shared/validators'
9 | import { openFilePicker } from '../shared/helpers/file-picker'
10 | import { hideActionBar } from '../shared/helpers/page'
11 | import { showToast } from '../shared/helpers/toast'
12 |
13 | @Component({
14 | selector: 'ms-welcome',
15 | templateUrl: './welcome.component.html',
16 | styleUrls: ['./welcome.component.scss'],
17 | })
18 | export class WelcomeComponent implements OnInit {
19 |
20 | welcomeText = 'Mindstream is a task management app that uses ' +
21 | 'todo.txt file format.';
22 |
23 | form: FormGroup;
24 |
25 | constructor(
26 | private formBuilder: FormBuilder,
27 | private router: RouterService,
28 | private settings: SettingsService,
29 | private sideDrawer: SideDrawerService,
30 | private todoFile: TodoFileService,
31 | private view: ViewContainerRef,
32 | ) {
33 | hideActionBar(view)
34 | }
35 |
36 | ngOnInit() {
37 | this.sideDrawer.lock()
38 | this.form = this.formBuilder.group({
39 | filePath: [
40 | this.settings.path,
41 | FilePathValidator(),
42 | ],
43 | })
44 | }
45 |
46 | openPicker() {
47 | openFilePicker().then((filePath) => {
48 | if (filePath) {
49 | this.form.controls.filePath.setValue(filePath)
50 | }
51 | }).catch((error) => {
52 | console.warn(error)
53 | showToast(error.toString(), true)
54 | })
55 | }
56 |
57 | save() {
58 | if (!this.form.valid) {
59 | return
60 | }
61 | const filePath = this.form.value.filePath
62 | let filePromise
63 | if (!filePath) {
64 | // Create empty todo.txt
65 | filePromise = this.todoFile.create()
66 | } else {
67 | filePromise = new Promise((resolve) => resolve(filePath))
68 | }
69 | filePromise.then((path: string) => {
70 | this.settings.path = path
71 | this.todoFile.initialLoad().then(() => {
72 | this.sideDrawer.unlock()
73 | this.router.navigate(['/tasks'], {clearHistory: true})
74 | })
75 | })
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/app/welcome/welcome.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core'
2 | import { CanActivate } from '@angular/router'
3 |
4 | import { RouterService } from '../shared/router.service'
5 | import { SettingsService } from '../shared/settings.service'
6 |
7 | @Injectable({
8 | providedIn: 'root',
9 | })
10 | export class WelcomeGuard implements CanActivate {
11 |
12 | constructor(
13 | private settings: SettingsService,
14 | private router: RouterService,
15 | ) {}
16 |
17 | canActivate(): boolean {
18 | if (this.settings.path) {
19 | // Skip welcome screen
20 | this.router.navigate(['/tasks'])
21 | return false
22 | } else {
23 | return true
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/browserslist:
--------------------------------------------------------------------------------
1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed
5 | > 0.5%
6 | last 2 versions
7 | Firefox ESR
8 | not dead
9 | # IE 9-11
10 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | backendUrl: '',
4 | version: '1.6.1',
5 | }
6 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | declare const PORT
2 |
3 | export const environment = {
4 | production: false,
5 | backendUrl: `http://localhost:${PORT}`,
6 | version: '1.6.2-dev',
7 | }
8 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xuhcc/mindstream/34d5c57214a57f96acf88c1c71de90f42210b6a0/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mindstream
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | config.set({
3 | basePath: '',
4 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
5 | plugins: [
6 | require('karma-jasmine'),
7 | require('karma-jsdom-launcher'),
8 | require('karma-mocha-reporter'),
9 | require('@angular-devkit/build-angular/plugins/karma'),
10 | ],
11 | reporters: ['mocha'],
12 | port: 9876,
13 | colors: true,
14 | logLevel: config.LOG_INFO,
15 | autoWatch: false,
16 | browsers: ['jsdom'],
17 | singleRun: true,
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/src/main.tns.ts:
--------------------------------------------------------------------------------
1 | // this import should be first in order to load some required settings (like globals and reflect-metadata)
2 | import { platformNativeScriptDynamic } from '@nativescript/angular'
3 |
4 | import { AppModule } from './app/app.module'
5 |
6 | // A traditional NativeScript application starts by initializing global objects, setting up global CSS rules, creating, and navigating to the main page.
7 | // Angular applications need to take care of their own initialization: modules, components, directives, routes, DI providers.
8 | // A NativeScript Angular app needs to make both paradigms work together, so we provide a wrapper platform object, platformNativeScriptDynamic,
9 | // that sets up a NativeScript application and can bootstrap the Angular framework.
10 | platformNativeScriptDynamic().bootstrapModule(AppModule)
11 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core'
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
3 |
4 | import { AppModule } from './app/app.module'
5 | import { environment } from './environments/environment'
6 |
7 | if (environment.production) {
8 | enableProdMode()
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.log(err))
13 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/weak-map';
35 | // import 'core-js/es6/set';
36 |
37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
38 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
39 |
40 | /** IE10 and IE11 requires the following for the Reflect API. */
41 | // import 'core-js/es6/reflect';
42 |
43 | /** Evergreen browsers require these. **/
44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
45 | import 'core-js/es7/reflect'
46 |
47 | /**
48 | * Web Animations `@angular/platform-browser/animations`
49 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
50 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
51 | **/
52 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
53 |
54 | /**
55 | * By default, zone.js will patch all possible macroTask and DomEvents
56 | * user can disable parts of macroTask/DomEvents patch by setting following flags
57 | */
58 |
59 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
60 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
61 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
62 |
63 | /*
64 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
65 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
66 | */
67 | // (window as any).__Zone_enable_cross_context_check = true;
68 |
69 | /***************************************************************************************************
70 | * Zone JS is required by default for Angular itself.
71 | */
72 | import 'zone.js/dist/zone' // Included with Angular CLI.
73 |
74 | /***************************************************************************************************
75 | * APPLICATION IMPORTS
76 | */
77 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | // Styles for the web app
2 | @import '~normalize.css';
3 | @import '~ngx-smart-modal/ngx-smart-modal';
4 | $material-design-icons-font-directory-path: '~material-design-icons-iconfont/dist/fonts/';
5 | @import '~material-design-icons-iconfont/src/material-design-icons';
6 |
7 | @import 'app-common';
8 | @import 'app-web';
9 |
10 | @font-face {
11 | font-display: swap;
12 | font-family: 'Vollkorn';
13 | font-style: normal;
14 | font-weight: 400;
15 | src:
16 | local('Vollkorn Regular'),
17 | local('Vollkorn-Regular'),
18 | url('fonts/vollkorn-all-400.woff2') format('woff2');
19 | }
20 |
21 | @font-face {
22 | font-display: swap;
23 | font-family: 'Vollkorn';
24 | font-style: italic;
25 | font-weight: 400;
26 | src:
27 | local('Vollkorn Italic'),
28 | local('Vollkorn-Italic'),
29 | url('fonts/vollkorn-all-400-italic.woff2') format('woff2');
30 | }
31 |
32 | @font-face {
33 | font-display: swap;
34 | font-family: 'Vollkorn';
35 | font-style: normal;
36 | font-weight: 700;
37 | src:
38 | local('Vollkorn Bold'),
39 | local('Vollkorn-Bold'),
40 | url('fonts/vollkorn-all-700.woff2') format('woff2');
41 | }
42 |
43 | @font-face {
44 | font-display: swap;
45 | font-family: 'PT Sans';
46 | font-style: normal;
47 | font-weight: 400;
48 | src:
49 | local('PT Sans'),
50 | local('PTSans-Regular'),
51 | url('fonts/pt-sans-all-400.woff2') format('woff2');
52 | }
53 |
54 | @font-face {
55 | font-display: swap;
56 | font-family: 'PT Sans';
57 | font-style: normal;
58 | font-weight: 700;
59 | src:
60 | local('PT Sans Bold'),
61 | local('PTSans-Bold'),
62 | url('fonts/pt-sans-all-700.woff2') format('woff2');
63 | }
64 |
65 | body {
66 | font-family: $main-font;
67 | }
68 |
69 | a {
70 | color: $link-color;
71 | cursor: pointer;
72 | text-decoration: none;
73 | }
74 |
75 | a:hover {
76 | color: $link-hover-color;
77 | }
78 |
79 | .nsm-content {
80 | /* ngx-smart-modal */
81 | background-color: $button-color;
82 | border-radius: 3px;
83 | font-size: 16px;
84 | text-align: center;
85 |
86 | .dialog-title {
87 | font-size: 20px;
88 | word-wrap: break-word;
89 | }
90 |
91 | .dialog-message {
92 | margin: 10px;
93 | }
94 |
95 | a {
96 | display: block;
97 | margin: 10px;
98 | }
99 | }
100 |
101 | .header {
102 | background-color: $header-color;
103 | color: $header-text-color;
104 | font-family: $header-font;
105 | font-size: 20px;
106 | font-weight: bold;
107 | padding: 20px $main-padding;
108 |
109 | a {
110 | color: $header-text-color;
111 | }
112 |
113 | .material-icons {
114 | font-size: 20px;
115 | font-weight: bold;
116 | margin-left: $main-padding;
117 | padding-top: 3px;
118 | }
119 | }
120 |
121 | .page {
122 | background-color: $page-color;
123 | color: $text-color;
124 | padding: $main-padding;
125 | }
126 |
127 | .btn {
128 | background-color: $button-color;
129 | border: 1px solid $button-border-color;
130 | border-radius: 3px;
131 | color: $button-text-color;
132 | cursor: pointer;
133 | font-size: 14px;
134 | padding: $main-padding;
135 |
136 | &:hover {
137 | background: #EDEED9;
138 | border: 1px solid #E2E6C6;
139 | color: #995700;
140 | }
141 |
142 | &[disabled] {
143 | background-color: $button-disabled-color;
144 | border-color: $button-disabled-border-color;
145 | color: $button-disabled-text-color;
146 | }
147 | }
148 |
149 | .field-label {
150 | display: block;
151 | font-size: 14px;
152 | margin: 5px 0;
153 | }
154 |
155 | .field,
156 | .field-addon {
157 | background-color: $field-color;
158 | border: $field-border solid $field-border-color;
159 | border-radius: $field-border-radius;
160 | box-sizing: border-box;
161 | display: block;
162 | font-size: 16px;
163 | height: 38px;
164 | padding: $main-padding;
165 | }
166 |
167 | .field {
168 | min-width: 300px;
169 | outline: none;
170 | width: 33%;
171 | }
172 |
173 | .field-wrapper {
174 | /* Flex wrapper for field with addons */
175 | display: flex;
176 | min-width: 300px;
177 | width: 33%;
178 |
179 | .field {
180 | border-bottom-right-radius: 0;
181 | border-right-width: 0;
182 | border-top-right-radius: 0;
183 | flex-grow: 1;
184 | min-width: 0;
185 | width: auto;
186 | }
187 |
188 | .field-addon {
189 | border-left-width: 0;
190 | border-radius: 0;
191 | border-right-width: 0;
192 | color: $link-color;
193 | padding-left: 0;
194 | }
195 |
196 | .field-addon:last-child {
197 | border-bottom-right-radius: $field-border-radius;
198 | border-right-width: $field-border;
199 | border-top-right-radius: $field-border-radius;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing'
4 | import { getTestBed } from '@angular/core/testing'
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting,
8 | } from '@angular/platform-browser-dynamic/testing'
9 |
10 | declare const require: any
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | )
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/)
19 | // And load the modules.
20 | context.keys().map(context)
21 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "module": "es2015",
6 | "types": []
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/spec",
5 | "module": "commonjs",
6 | "types": [
7 | "jasmine",
8 | "node"
9 | ]
10 | },
11 | "files": [
12 | "test.ts",
13 | "polyfills.ts"
14 | ],
15 | "include": [
16 | "**/*.spec.ts",
17 | "**/*.d.ts"
18 | ],
19 | "exclude": [
20 | "**/*.tns.ts",
21 | "**/*.android.ts",
22 | "**/*.ios.ts"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/src/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tslint.json",
3 | "rules": {
4 | "directive-selector": [
5 | true,
6 | "attribute",
7 | "ms",
8 | "camelCase"
9 | ],
10 | "component-selector": [
11 | true,
12 | "element",
13 | "ms",
14 | "kebab-case"
15 | ]
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es2017",
11 | "typeRoots": [
12 | "node_modules/@types"
13 | ],
14 | "lib": [
15 | "es2017",
16 | "dom"
17 | ],
18 | "baseUrl": ".",
19 | "module": "esnext",
20 | "removeComments": false
21 | },
22 | "exclude": [
23 | "**/*.tns.ts",
24 | "**/*.android.ts",
25 | "**/*.ios.ts",
26 | "**/*.spec.ts"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.tns.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "es2015",
5 | "moduleResolution": "node"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-shadowed-variable": true,
69 | "no-string-literal": false,
70 | "no-string-throw": true,
71 | "no-switch-case-fall-through": true,
72 | "no-trailing-whitespace": true,
73 | "no-unnecessary-initializer": true,
74 | "no-unused-expression": true,
75 | "no-var-keyword": true,
76 | "object-literal-sort-keys": false,
77 | "one-line": [
78 | true,
79 | "check-open-brace",
80 | "check-catch",
81 | "check-else",
82 | "check-whitespace"
83 | ],
84 | "prefer-const": true,
85 | "quotemark": [
86 | true,
87 | "single"
88 | ],
89 | "radix": true,
90 | "semicolon": [
91 | true,
92 | "always"
93 | ],
94 | "triple-equals": [
95 | true,
96 | "allow-null-check"
97 | ],
98 | "typedef-whitespace": [
99 | true,
100 | {
101 | "call-signature": "nospace",
102 | "index-signature": "nospace",
103 | "parameter": "nospace",
104 | "property-declaration": "nospace",
105 | "variable-declaration": "nospace"
106 | }
107 | ],
108 | "unified-signatures": true,
109 | "variable-name": false,
110 | "whitespace": [
111 | true,
112 | "check-branch",
113 | "check-decl",
114 | "check-operator",
115 | "check-separator",
116 | "check-type"
117 | ],
118 | "no-output-on-prefix": true,
119 | "no-input-rename": true,
120 | "no-output-rename": true,
121 | "use-pipe-transform-interface": true,
122 | "component-class-suffix": true,
123 | "directive-class-suffix": true
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/webpack-tns.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const baseConfig = require('./webpack.config');
3 |
4 | module.exports = env => {
5 | const config = baseConfig(env);
6 | return config;
7 | };
8 |
--------------------------------------------------------------------------------
/webpack-web.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = {
4 | plugins: [
5 | // Allow to specify app port
6 | new webpack.DefinePlugin({
7 | PORT: process.env.PORT || 8080,
8 | }),
9 | ],
10 | };
11 |
--------------------------------------------------------------------------------