├── .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 | [![GitHub release](https://img.shields.io/github/release/xuhcc/mindstream)](https://github.com/xuhcc/mindstream/releases) 4 | [![License: GPL v3](https://img.shields.io/github/license/xuhcc/mindstream)](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 |
2 |
{{ title }}
3 |
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 |
2 | 3 |
4 | 5 |
6 |
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 |
2 |
{{ title }}
3 |
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 |
2 |
{{ title }}
3 |
4 |
5 | 8 | 13 | 19 |
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 |
10 | {{ action }} 11 |
12 |
13 | Cancel 14 |
15 |
16 | OK 17 |
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 |
2 |
{{ title }}
3 |
4 | 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 |
2 |
{{ title }}
3 |
4 |
9 | 12 | 18 | 21 | 25 | 26 | 27 | 30 | 34 | 35 | 36 | 39 |
40 | 45 | 56 |
57 | 58 | 59 |
60 | 66 | today 67 | | 68 | tomorrow 69 | | 70 | 88 | 92 |
93 | 94 | 95 |
96 | 101 | 105 |
106 | 107 | 108 |
109 | 114 |
118 | 123 |
124 |
125 | 126 | 135 |
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 |
2 |
{{ title }}
3 | 8 | 12 |
13 |
14 | 21 |
26 |
27 |
32 | 33 |
34 |
39 |
40 |
45 | {{ '+' + project }} 46 |
47 |
52 | {{ '@' + context }} 53 |
54 |
59 | {{ getDateDisplay(task.due) }} 60 |
61 |
62 | {{ task.rec.display() }} 63 |
64 |
68 | {{ task.priority }} 69 |
70 |
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 |
5 | 8 | 15 | 22 |
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 | --------------------------------------------------------------------------------