Merge remote-tracking branch 'upstream/master' into build/angular-10-upgrade

* upstream/master: (83 commits)
  docs: update contributing stuff
  docs: update contributing stuff
  feat: make AppDataForProjects non optional
  5.9.0
  fix: lint
  feat(autoRepair): make stray backup stuff translateable
  feat(autoRepair): re-enable stray backup check
  feat(autoRepair): add a little bit of logging
  feat(autoRepair): make restoring orphaned tasks work
  feat(autoRepair): make restore from archive work
  feat(autoRepair): make _removeMissingIdsFromLists work
  feat(autoRepair): make _removeDuplicatesFromArchive work
  feat(autoRepair): make fix duplicate tasks work
  refactor(autoRepair): make dataRepair non async
  test(autoRepair): prepare more tests
  test(autoRepair): make app data mock available
  test(autoRepair): outline tests
  feat(autoRepair): trigger for data import and data init if data is broken
  fix: another read-only error #538
  test: make new error cases work
  ...
This commit is contained in:
Dominik Broj 2020-09-26 13:14:44 +02:00
commit 5f3870537a
78 changed files with 1976 additions and 151 deletions

View file

@ -56,7 +56,7 @@ script:
bash ${TRAVIS_BUILD_DIR}/build/docker/run-linux-win.sh ${PUB}
else
yarn install && yarn add 7zip-bin-mac
yarn run buildAllElectronNoTests
yarn run buildAllElectron:noTests
travis_wait 30 yarn dist:mac:dl -p $PUB
fi

View file

@ -1,3 +1,159 @@
# [5.9.0](https://github.com/johannesjo/super-productivity/compare/v5.8.2...v5.9.0) (2020-09-24)
### Bug Fixes
* another read-only error [#538](https://github.com/johannesjo/super-productivity/issues/538) ([8f2afcc](https://github.com/johannesjo/super-productivity/commit/8f2afcc171901d09b4af2e5281ddb044a3329743))
* disabling done sound not working [#534](https://github.com/johannesjo/super-productivity/issues/534) ([2179118](https://github.com/johannesjo/super-productivity/commit/2179118f9468962876f647199dc0d1e93bcfbd13))
* lint ([763e991](https://github.com/johannesjo/super-productivity/commit/763e99158211c977e7e1b621eb175698998a66bd))
* use encodeURIComponent instead of encodeURI [#523](https://github.com/johannesjo/super-productivity/issues/523) ([ba06b0b](https://github.com/johannesjo/super-productivity/commit/ba06b0b60057fd510de68d3622762bf9035f9a0b))
### Features
* **autoRepair:** add a little bit of logging ([70e4e20](https://github.com/johannesjo/super-productivity/commit/70e4e20943d97beb02227b1b627f2875e5be41c6))
* **autoRepair:** make _removeDuplicatesFromArchive work ([6a111d9](https://github.com/johannesjo/super-productivity/commit/6a111d92ce31e94ac86a0a69aa899d918df6b692))
* **autoRepair:** make _removeMissingIdsFromLists work ([192ef5a](https://github.com/johannesjo/super-productivity/commit/192ef5ad8679b012aee75bbd593cf61014d8e95d))
* **autoRepair:** make fix duplicate tasks work ([002ec09](https://github.com/johannesjo/super-productivity/commit/002ec096f2466e9c0a4651aff8a046379437bf49))
* **autoRepair:** make restore from archive work ([c706f26](https://github.com/johannesjo/super-productivity/commit/c706f26586badb6a4c1a5e6fe03ffc822a5402d1))
* **autoRepair:** make restoring orphaned tasks work ([5fe3fbe](https://github.com/johannesjo/super-productivity/commit/5fe3fbe139eb67850a51521598f252784b83573d))
* **autoRepair:** make stray backup stuff translateable ([35cd773](https://github.com/johannesjo/super-productivity/commit/35cd7736995cfc7ffcabad35887f5b2829cc0f54))
* **autoRepair:** re-enable stray backup check ([ec6c844](https://github.com/johannesjo/super-productivity/commit/ec6c8449861981bfb8ed7c8c13426b4820f2e4a5))
* **autoRepair:** trigger for data import and data init if data is broken ([a80054a](https://github.com/johannesjo/super-productivity/commit/a80054a2fc47866e7c05fe14964022715ba70778))
* add missing null checks for is valid app data ([577383b](https://github.com/johannesjo/super-productivity/commit/577383bb44c469eb43654ca31eb83fa7bd150c70))
* also check for miss-matched ids in entity states ([81ca47c](https://github.com/johannesjo/super-productivity/commit/81ca47c0d9047c399a45de7eb058890f5c638490))
## [5.8.2](https://github.com/johannesjo/super-productivity/compare/v5.8.1...v5.8.2) (2020-09-22)
### Bug Fixes
* private policy link [#531](https://github.com/johannesjo/super-productivity/issues/531) ([8ccf7b1](https://github.com/johannesjo/super-productivity/commit/8ccf7b1ea4619f08ab71e6544d950c14b5ad3c98))
## [5.8.1](https://github.com/johannesjo/super-productivity/compare/v5.8.0...v5.8.1) (2020-09-21)
# [5.8.0](https://github.com/johannesjo/super-productivity/compare/v5.7.7...v5.8.0) (2020-09-20)
### Bug Fixes
* **task:** undone done when marking a lot of tasks as done in fast succession ([59574c5](https://github.com/johannesjo/super-productivity/commit/59574c55a4ce271842d3177ace25761dff297a73))
### Features
* **startTrackingReminder:** make configurable and add translations for it [#507](https://github.com/johannesjo/super-productivity/issues/507) ([4ad922c](https://github.com/johannesjo/super-productivity/commit/4ad922c5c2b50aca18c0831bb182895b792aab87))
* make dark mode default ([b6c72f5](https://github.com/johannesjo/super-productivity/commit/b6c72f57c077bfe2f5214831d06502a02fd37539))
* **startTrackingReminder:** implement timer as real timer [#507](https://github.com/johannesjo/super-productivity/issues/507) ([71d18a6](https://github.com/johannesjo/super-productivity/commit/71d18a67c320f628e506296723c062656de9bcaf))
* **startTrackingReminder:** make dialog work and translations [#507](https://github.com/johannesjo/super-productivity/issues/507) ([2cac5c7](https://github.com/johannesjo/super-productivity/commit/2cac5c72023bfa020555839a0038957aadb2819c))
* **startTrackingReminder:** make reset timer work [#507](https://github.com/johannesjo/super-productivity/issues/507) ([7f6cb61](https://github.com/johannesjo/super-productivity/commit/7f6cb61d859b375af36d3c869d5c68f9ee818edb))
* **startTrackingReminder:** make timer work [#507](https://github.com/johannesjo/super-productivity/issues/507) ([fc37243](https://github.com/johannesjo/super-productivity/commit/fc37243e4355a861481971eafad4c856db631f29))
* **startTrackingReminder:** outline service [#507](https://github.com/johannesjo/super-productivity/issues/507) ([455c2b3](https://github.com/johannesjo/super-productivity/commit/455c2b322bab3793b7c6abbdb76f30e4394f8e99))
* **startTrackingReminder:** prepare banner [#507](https://github.com/johannesjo/super-productivity/issues/507) ([68e802e](https://github.com/johannesjo/super-productivity/commit/68e802eeb32781b1b884ee51154b652c28830f5b))
* **task:** improve current/focus border styling ([8308862](https://github.com/johannesjo/super-productivity/commit/83088629750dd8b6f884bfe2054689439b27a5a7))
* **task:** use solid border for task additional info panel as well ([934843a](https://github.com/johannesjo/super-productivity/commit/934843a58215028aece5e8ad6d17c6ee473ce52f))
## [5.7.7](https://github.com/johannesjo/super-productivity/compare/v5.7.6...v5.7.7) (2020-09-18)
### Bug Fixes
* allow deletion of multiple tasks in fast succession ([8b41fb9](https://github.com/johannesjo/super-productivity/commit/8b41fb9fe9532f54653e04596a75ed0f050952ae))
* persistence request for mobile ([113dafd](https://github.com/johannesjo/super-productivity/commit/113dafdc5b600259ba62e7fa664ca9b6c5e9d647))
* weird error for reminders ([c4c2a33](https://github.com/johannesjo/super-productivity/commit/c4c2a33289d8b68854f29e5aa3e4f86e4613ceb6))
* weird reminder error ([260a80c](https://github.com/johannesjo/super-productivity/commit/260a80c2c6239cffec10312c908b35ca843f1c48))
* **task:** case when adding task via short syntax with project id to today ([2e96c0f](https://github.com/johannesjo/super-productivity/commit/2e96c0fa52270232c3238667ab6401c15bcb613b))
### Features
* add additional debug info for undo task delete meta reducer ([01ea6d9](https://github.com/johannesjo/super-productivity/commit/01ea6d9343a897681c721039d7e9cf9f6df467be))
* add debugging actions for persistence ([dfe459e](https://github.com/johannesjo/super-productivity/commit/dfe459e65f7846a4bdc6f2125a419ad26e3f7f68))
* add error alert ([389063a](https://github.com/johannesjo/super-productivity/commit/389063aaeb4ab7562b034cb3d533d904d754457e))
* limit inMemoryComplete$ to valid only ([5021969](https://github.com/johannesjo/super-productivity/commit/502196951d7ad98a34b35c62322c5ac9fe42ec05))
* make adjustments for stage behaviour ([75042ee](https://github.com/johannesjo/super-productivity/commit/75042ee4c4a1cd76968047927e695e227e83ff7a))
* remove load from db action because it's too much clutter ([ea05fda](https://github.com/johannesjo/super-productivity/commit/ea05fda38069ab09f975a4be4146b24f49a052d7))
## [5.7.6](https://github.com/johannesjo/super-productivity/compare/v5.7.5...v5.7.6) (2020-09-17)
### Features
* improve error handling ([6892605](https://github.com/johannesjo/super-productivity/commit/68926057ec43716e467381350ba569bf7ec9294e))
## [5.7.5](https://github.com/johannesjo/super-productivity/compare/v5.7.4...v5.7.5) (2020-09-17)
### Bug Fixes
* prevent dropbox from syncing invalid ([6ddc711](https://github.com/johannesjo/super-productivity/commit/6ddc7115c60f8ad2516c6e38d0bbe4fc802a34d3))
## [5.7.4](https://github.com/johannesjo/super-productivity/compare/v5.7.3...v5.7.4) (2020-09-17)
### Bug Fixes
* stray import stuff being wrong ([b2fae30](https://github.com/johannesjo/super-productivity/commit/b2fae30bc7eafcca5b5449f11dd19d6b0440a007))
## [5.7.3](https://github.com/johannesjo/super-productivity/compare/v5.7.2...v5.7.3) (2020-09-17)
### Features
* **i18n:** add missing translations ([a229186](https://github.com/johannesjo/super-productivity/commit/a22918681274ffdd576184b3f281ae07f2e5b45b))
* improve pre-check ([ed9fc7a](https://github.com/johannesjo/super-productivity/commit/ed9fc7a8b681cca2d7acfc2ea0718427cbe06187))
* improve warning for empty data sync ([5f27d43](https://github.com/johannesjo/super-productivity/commit/5f27d4397750c3272a164f36b6d15aff904104ee))
* make dropbox sync confirms translatable ([e8c97ee](https://github.com/johannesjo/super-productivity/commit/e8c97ee32f8271db89bd0ab312d96a92514fbce1))
## [5.7.2](https://github.com/johannesjo/super-productivity/compare/v5.7.1...v5.7.2) (2020-09-17)
### Features
* add pre-check for invalid local data before saving ([955aed7](https://github.com/johannesjo/super-productivity/commit/955aed7e0c3edf1c656d0ddc2307a939f86f73e2))
## [5.7.1](https://github.com/johannesjo/super-productivity/compare/v5.7.0...v5.7.1) (2020-09-17)
### Bug Fixes
* editing sub task tags should not be possible [#522](https://github.com/johannesjo/super-productivity/issues/522) ([eb11530](https://github.com/johannesjo/super-productivity/commit/eb11530193f91232fe2e109a8ad792a2180e258d))
* model check for empty data ([4a8a784](https://github.com/johannesjo/super-productivity/commit/4a8a7841f8db6cd23541caf74bf7a955ddc1eecb))
* note reminder from tag view [#524](https://github.com/johannesjo/super-productivity/issues/524) ([f9e508f](https://github.com/johannesjo/super-productivity/commit/f9e508fb13f5d3a44d284da166af8c84893cad7e))
### Features
* add auto import backup when something went wrong with data import previously ([f281470](https://github.com/johannesjo/super-productivity/commit/f281470f193f93692a3bd37b0f59c34a2c894bb8))
* alert on indexeddb error ([09f8a22](https://github.com/johannesjo/super-productivity/commit/09f8a22ad018a9052bea3ded77a4a48d2528d07b))
* allow drag & drop attachments on task additional info ([9377b80](https://github.com/johannesjo/super-productivity/commit/9377b80742e1fe3e9fb4b02f3b4059e06174775c))
* improve persistence permission request ([0e8cb34](https://github.com/johannesjo/super-productivity/commit/0e8cb34219dbf464c30ee5d1c2c8c2cdb2f90bb7))
* **doneSound:** make pitching work for a bigger amount of tasks ([51489d2](https://github.com/johannesjo/super-productivity/commit/51489d263df6338e2f12c62a62697784a28bdf9c))
* **style:** add separators to worklog ([11127f1](https://github.com/johannesjo/super-productivity/commit/11127f10c4f4e6924a3257a9dca2a59c4d2fc656))
* **style:** use checkmark for bookmark-bar edit mode ([46d4ccb](https://github.com/johannesjo/super-productivity/commit/46d4ccb4bed5aad65135d0206b03665638cc3ff0))
# [5.7.0](https://github.com/johannesjo/super-productivity/compare/v5.6.5...v5.7.0) (2020-09-07)

25
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,25 @@
:hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts:
Welcome you awesome human being! You want to contribute to this repository? This is gonna be great!
The best way to start is to open up a [new issue](https://github.com/johannesjo/super-productivity/issues/new) or [to search for already existing ones](https://github.com/johannesjo/super-productivity/issues) covering your topic.
If you're contributing code, please try to use the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary) commit message conventions.
If you want to discuss something private, please write an email to contact@super-productivity.com.
### Code of conduct
Don't be a d***!
### Options for contributing
In case you want to contribute, but you wouldn't know how, here are some suggestions:
1. **Spread the word:** More users means more people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on [Slant](https://www.slant.co/topics/14021/viewpoints/7/~productivity-tools-for-linux~super-productivity), [Product Hunt](https://www.producthunt.com/posts/super-productivity), [Softpedia](https://www.softpedia.com/get/Office-tools/Diary-Organizers-Calendar/Super-Productivity.shtml) or on [AlternativeTo](https://alternativeto.net/software/super-productivity/), you can [tweet about it](https://twitter.com/intent/tweet?text=I%20like%20Super%20Productivity%20%20https%3A%2F%2Fsuper-productivity.com), share it on [LinkedIn](http://www.linkedin.com/shareArticle?mini=true&url=https://super-productivity.com&title=I%20like%20Super%20Productivity&), [reddit](http://www.reddit.com/submit?url=https%3A%2F%2Fsuper-productivity.com&title=I%20like%20Super%20Productivity) or any of your favorite social media platforms. Every little bit helps!
2. **[Make a feature or improvement request](https://github.com/johannesjo/super-productivity/issues/new)**: Something can be be done better? Something essential missing? Let us know!
3. **[Report bugs](https://github.com/johannesjo/super-productivity/issues/new)**
4. **Contribute**: You don't have to be programmer to help. Some of the icons really need improvement and many of the translations could use some love.
:hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts:

View file

@ -110,9 +110,11 @@ brew cask install superproductivity
There is a [very early(!) Android version available](https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity&hl=gsw). The sources can be [found here](https://github.com/johannesjo/super-productivity-android).
## :hearts: Contributing
Please check out the [CONTRIBUTING.md](CONTRIBUTING.md)
There are several ways to help.
1. **Spread the word:** More users means more possible people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on [Product Hunt](https://www.producthunt.com/posts/super-productivity), [Softpedia](https://www.softpedia.com/get/Office-tools/Diary-Organizers-Calendar/Super-Productivity.shtml) or on [AlternativeTo](https://alternativeto.net/software/super-productivity/), you can [tweet about it](https://twitter.com/intent/tweet?text=I%20like%20Super%20Productivity%20%20https%3A%2F%2Fsuper-productivity.com), share it on [LinkedIn](http://www.linkedin.com/shareArticle?mini=true&url=https://super-productivity.com&title=I%20like%20Super%20Productivity&) or [reddit](http://www.reddit.com/submit?url=https%3A%2F%2Fsuper-productivity.com&title=I%20like%20Super%20Productivity). Every little bit helps !
1. **Spread the word:** More users means more people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on [Slant](https://www.slant.co/topics/14021/viewpoints/7/~productivity-tools-for-linux~super-productivity), [Product Hunt](https://www.producthunt.com/posts/super-productivity), [Softpedia](https://www.softpedia.com/get/Office-tools/Diary-Organizers-Calendar/Super-Productivity.shtml) or on [AlternativeTo](https://alternativeto.net/software/super-productivity/), you can [tweet about it](https://twitter.com/intent/tweet?text=I%20like%20Super%20Productivity%20%20https%3A%2F%2Fsuper-productivity.com), share it on [LinkedIn](http://www.linkedin.com/shareArticle?mini=true&url=https://super-productivity.com&title=I%20like%20Super%20Productivity&), [reddit](http://www.reddit.com/submit?url=https%3A%2F%2Fsuper-productivity.com&title=I%20like%20Super%20Productivity) or any of your favorite social media platforms. Every little bit helps!
2. **[Make a feature or improvement request](https://github.com/johannesjo/super-productivity/issues/new)**: Something can be be done better? Something essential missing? Let us know!

View file

@ -67,6 +67,55 @@
"vendorChunk": false,
"buildOptimizer": true,
"serviceWorker": true
},
"productionWeb": {
"baseHref": "",
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": true,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"serviceWorker": true
},
"stage": {
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": true,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"serviceWorker": true
}
}
},

View file

@ -0,0 +1,20 @@
import { BASE } from '../e2e.const';
import { NBrowser } from '../n-browser-interface';
const TASK = 'task';
const TASK_TAGS = 'task tag';
const WORK_VIEW_URL = `${BASE}/`;
const READY_TO_WORK_BTN = '.ready-to-work-btn';
module.exports = {
'@tags': ['work-view', 'task', 'short-syntax'],
'should add task with project via short syntax': (browser: NBrowser) => browser
.url(WORK_VIEW_URL)
.waitForElementVisible(READY_TO_WORK_BTN)
.addTask('0 test task koko +s')
.waitForElementVisible(TASK)
.assert.visible(TASK)
.assert.containsText(TASK_TAGS, 'Super Productivity')
.end(),
};

View file

@ -1,6 +1,6 @@
{
"name": "superProductivity",
"version": "5.7.0",
"version": "5.9.0",
"description": "Personal Task Management App to help you with your daily struggle with JIRA etc.",
"main": "./electron/main.js",
"author": "johannesjo <contact@super-productivity.com> (http://super-productivity.com)",
@ -22,13 +22,13 @@
"start": "yarn electron:build && cross-env NODE_ENV=DEV electron .",
"startFrontend": "ng serve",
"serveProd": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng serve --prod",
"buildFrontend": "yarn preCheck && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod",
"buildFrontendElectron": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --base-href=''",
"buildFrontendElectron:preCheck": "yarn preCheck && yarn buildFrontendElectron",
"buildFrontendElectron:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --base-href='' --watch",
"buildAllElectron": "yarn buildFrontendElectron:preCheck && yarn electron:build",
"buildAllElectronNoTests": "yarn lint && yarn buildFrontendElectron && yarn electron:build",
"buildApp": "yarn preCheck && yarn buildAllElectron && yarn electron-builder",
"buildFrontend:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod",
"buildFrontend:stage": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --configuration stage",
"buildFrontend:prod:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --watch",
"buildAllElectron:prod": "yarn preCheck && yarn buildFrontend:prod && yarn electron:build",
"buildAllElectron:stage": "yarn preCheck && yarn buildFrontend:stage && yarn electron:build",
"buildAllElectron:noTests": "yarn lint && yarn buildFrontend:prod && yarn electron:build",
"buildApp": "yarn preCheck && yarn buildAllElectron:prod && yarn electron-builder",
"test": "ng test --watch=false",
"test:watch": "ng test --browsers ChromeHeadless",
"lint": "ng lint",
@ -41,15 +41,16 @@
"electron:watch": "tsc -p electron/tsconfig.electron.json --watch",
"electronBuilderOnly": "electron-builder",
"pack": "electron-builder --dir",
"localInstall": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildAllElectron && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb",
"localInstall:quick": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildFrontendElectron && yarn electron:build && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb",
"localInstall:mac": "sudo echo 'Starting local install. Don`t forget APPLEID & APPLEIDPASS !!' && yarn buildAllElectron && sudo echo '' && electron-builder && sudo cp -rf app-builds/mac/superProductivity.app/ /Applications/superProductivity.app",
"dist": "yarn buildAllElectron && electron-builder",
"localInstall": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildAllElectron:stage && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb",
"localInstall:prod": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildAllElectron:prod && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb",
"localInstall:quick": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildFrontend:stage && yarn electron:build && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb",
"localInstall:mac": "sudo echo 'Starting local install. Don`t forget APPLEID & APPLEIDPASS !!' && yarn buildAllElectron:prod && sudo echo '' && electron-builder && sudo cp -rf app-builds/mac/superProductivity.app/ /Applications/superProductivity.app",
"dist": "yarn buildAllElectron:prod && electron-builder",
"dist:only": "electron-builder",
"dist:linuxAndWin": "yarn buildAllElectron && electron-builder --linux --win",
"dist:win": "yarn buildAllElectronNoTests && electron-builder --win",
"dist:linuxAndWin": "yarn buildAllElectron:prod && electron-builder --linux --win",
"dist:win": "yarn buildAllElectron:noTests && electron-builder --win",
"dist:win:only": "electron-builder --win",
"dist:win:appx": "yarn buildAllElectron && electron-builder --win --config=build/electron-builder.appx.yaml",
"dist:win:appx": "yarn buildAllElectron:prod && electron-builder --win --config=build/electron-builder.appx.yaml",
"dist:win:store": "git stash && git pull && yarn && git stash pop && yarn dist:win",
"dist:mac:dl": "cp dl.provisionprofile embedded.provisionprofile && electron-builder --mac",
"dist:mac:mas": "cp mas.provisionprofile embedded.provisionprofile; electron-builder --mac mas --config=build/electron-builder.mas.yaml",
@ -130,7 +131,7 @@
"codelyzer": "^6.0.0",
"conventional-changelog-cli": "^2.0.21",
"cross-env": "^7.0.2",
"electron": "^10.1.0",
"electron": "^10.1.2",
"electron-builder": "^22.7.0",
"electron-notarize": "^1.0.0",
"electron-reload": "^1.2.5",
@ -138,8 +139,8 @@
"hammerjs": "^2.0.8",
"helpful-decorators": "^2.1.0",
"husky": "^4.2.5",
"idb": "^5.0.3",
"jasmine-core": "~3.6.0",
"idb": "^5.0.6",
"jasmine-core": "^3.4.0",
"jasmine-marbles": "^0.6.0",
"jasmine-spec-reporter": "~5.0.0",
"jira2md": "git+https://github.com/johannesjo/J2M.git",

View file

@ -39,6 +39,7 @@ import { SyncService } from './imex/sync/sync.service';
import { environment } from '../environments/environment';
import { RouterOutlet } from '@angular/router';
import { ipcRenderer } from 'electron';
import { TrackingReminderService } from './features/time-tracking/tracking-reminder/tracking-reminder.service';
const w = window as any;
const productivityTip: string[] = w.productivityTips && w.productivityTips[w.randomIndex];
@ -81,6 +82,7 @@ export class AppComponent implements OnDestroy {
private _androidService: AndroidService,
private _initialDialogService: InitialDialogService,
private _bookmarkService: BookmarkService,
private _startTrackingReminderService: TrackingReminderService,
public readonly syncService: SyncService,
public readonly imexMetaService: ImexMetaService,
public readonly workContextService: WorkContextService,
@ -91,19 +93,6 @@ export class AppComponent implements OnDestroy {
document.dir = this.isRTL ? 'rtl' : 'ltr';
});
// try to avoid data-loss
if (navigator.storage && navigator.storage.persist) {
navigator.storage.persist().then(granted => {
if (granted) {
console.log('Persistent store granted');
} else {
const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED;
console.warn('Persistence not allowed');
this._snackService.open({msg});
}
});
}
// check for dialog
this._initialDialogService.showDialogIfNecessary$().subscribe();
@ -113,6 +102,10 @@ export class AppComponent implements OnDestroy {
// init offline banner in lack of a better place for it
this._initOfflineBanner();
// basically init
this._startTrackingReminderService.init();
this._requestPersistence();
this._checkAvailableStorage();
if (IS_ANDROID_WEB_VIEW) {
@ -245,6 +238,29 @@ export class AppComponent implements OnDestroy {
});
}
private _requestPersistence() {
if (navigator.storage) {
// try to avoid data-loss
Promise.all([
navigator.storage.persisted(),
]).then(([persisted]) => {
if (!persisted) {
navigator.storage.persist().then(granted => {
if (granted) {
console.log('Persistent store granted');
} else {
const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED;
console.warn('Persistence not allowed');
this._snackService.open({msg});
}
});
} else {
console.log('Persistence already allowed');
}
});
}
}
private _checkAvailableStorage() {
if (environment.production) {
if ('storage' in navigator && 'estimate' in navigator.storage) {

View file

@ -102,7 +102,7 @@ export function createTranslateLoader(http: HttpClient) {
}
),
EffectsModule.forRoot([]),
!environment.production ? StoreDevtoolsModule.instrument() : [],
(!environment.production && !environment.stage) ? StoreDevtoolsModule.instrument() : [],
ReactiveFormsModule,
FormlyModule.forRoot({
extras: {
@ -112,7 +112,7 @@ export function createTranslateLoader(http: HttpClient) {
{name: 'pattern', message: 'Invalid input'},
],
}),
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}),
ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production || environment.stage}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,

View file

@ -1,5 +1,6 @@
export enum BannerId {
TakeABreak = 'TakeABreak',
StartTrackingReminder = 'StartTrackingReminder',
GoogleLogin = 'GoogleLogin',
JiraUnblock = 'JiraUnblock',
InstallWebApp = 'InstallWebApp',

View file

@ -9,6 +9,8 @@ import { PersistenceService } from '../persistence/persistence.service';
import { ProjectState } from '../../features/project/store/project.reducer';
import { MigrationService } from '../migration/migration.service';
import { loadAllData } from '../../root-store/meta/load-all-data.action';
import { isValidAppData } from '../../imex/sync/is-valid-app-data.util';
import { DataRepairService } from '../data-repair/data-repair.service';
@Injectable({providedIn: 'root'})
export class DataInitService {
@ -33,6 +35,7 @@ export class DataInitService {
private _projectService: ProjectService,
private _workContextService: WorkContextService,
private _store$: Store<any>,
private _dataRepairService: DataRepairService,
) {
// TODO better construction than this
this.isAllDataLoadedInitially$.pipe(
@ -47,9 +50,17 @@ export class DataInitService {
// because the data load is triggered, but not necessarily already reflected inside the store
async reInit(isOmitTokens: boolean = false): Promise<void> {
const appDataComplete = await this._persistenceService.loadComplete();
// if (!environment.production) {
// const isValid = isValidAppData(appDataComplete);
// }
this._store$.dispatch(loadAllData({appDataComplete, isOmitTokens}));
const isValid = isValidAppData(appDataComplete);
if (isValid) {
this._store$.dispatch(loadAllData({appDataComplete, isOmitTokens}));
} else {
if (this._dataRepairService.isRepairConfirmed()) {
const fixedData = this._dataRepairService.repairData(appDataComplete);
this._store$.dispatch(loadAllData({
appDataComplete: fixedData,
isOmitTokens,
}));
}
}
}
}

View file

@ -0,0 +1,16 @@
// import { TestBed } from '@angular/core/testing';
//
// import { DataRepairService } from './data-repair.service';
//
// describe('DataRepairService', () => {
// let service: DataRepairService;
//
// beforeEach(() => {
// TestBed.configureTestingModule({});
// service = TestBed.inject(DataRepairService);
// });
//
// it('should be created', () => {
// expect(service).toBeTruthy();
// });
// });

View file

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { AppDataComplete } from '../../imex/sync/sync.model';
import { T } from '../../t.const';
import { TranslateService } from '@ngx-translate/core';
import { dataRepair } from './data-repair.util';
@Injectable({
providedIn: 'root'
})
export class DataRepairService {
constructor(
private _translateService: TranslateService,
) {
}
repairData(dataIn: AppDataComplete): AppDataComplete {
return dataRepair(dataIn);
}
isRepairConfirmed(): boolean {
return confirm(this._translateService.instant(T.CONFIRM.AUTO_FIX));
}
}

View file

@ -0,0 +1,407 @@
import { AppDataComplete } from '../../imex/sync/sync.model';
import { createAppDataCompleteMock } from '../../util/app-data-mock';
import { dataRepair } from './data-repair.util';
import { fakeEntityStateFromArray } from '../../util/fake-entity-state-from-array';
import { DEFAULT_TASK, Task } from '../../features/tasks/task.model';
import { createEmptyEntity } from '../../util/create-empty-entity';
import { Tag, TagState } from '../../features/tag/tag.model';
import { ProjectState } from '../../features/project/store/project.reducer';
import { Project } from '../../features/project/project.model';
describe('dataRepair()', () => {
let mock: AppDataComplete;
beforeEach(() => {
mock = createAppDataCompleteMock();
});
it('should delete tasks with same id in "task" and "taskArchive" from taskArchive', () => {
const taskState = {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'TEST',
title: 'TEST',
}])
} as any;
expect(dataRepair({
...mock,
task: taskState,
taskArchive: fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'TEST',
title: 'TEST',
}]),
})).toEqual({
...mock,
task: taskState,
taskArchive: {
...createEmptyEntity()
},
});
});
it('should delete missing tasks for tags today list', () => {
const taskState = {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'TEST',
title: 'TEST',
}])
} as any;
const tagState: TagState = {
...fakeEntityStateFromArray([{
title: 'TEST_TAG',
id: 'TEST_ID_TAG',
taskIds: ['goneTag', 'TEST', 'noneExisting'],
}] as Partial<Tag> []),
};
expect(dataRepair({
...mock,
tag: tagState,
task: taskState,
})).toEqual({
...mock,
task: taskState as any,
tag: {
...tagState,
entities: {
TEST_ID_TAG: {
title: 'TEST_TAG',
id: 'TEST_ID_TAG',
taskIds: ['TEST'],
},
} as any
}
});
});
it('should delete missing tasks for projects today list', () => {
const taskState = {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'TEST',
title: 'TEST',
}])
} as any;
const projectState: ProjectState = {
...fakeEntityStateFromArray([{
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: ['goneProject', 'TEST', 'noneExisting'],
backlogTaskIds: [],
}] as Partial<Project> []),
};
expect(dataRepair({
...mock,
project: projectState,
task: taskState,
})).toEqual({
...mock,
task: taskState as any,
project: {
...projectState,
entities: {
TEST_ID_PROJECT: {
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: ['TEST'],
backlogTaskIds: [],
},
} as any
}
});
});
it('should delete missing tasks for projects backlog list', () => {
const taskState = {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'TEST',
title: 'TEST',
}])
} as any;
const projectState: ProjectState = {
...fakeEntityStateFromArray([{
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: [],
backlogTaskIds: ['goneProject', 'TEST', 'noneExisting'],
}] as Partial<Project> []),
};
expect(dataRepair({
...mock,
project: projectState,
task: taskState,
})).toEqual({
...mock,
task: taskState as any,
project: {
...projectState,
entities: {
TEST_ID_PROJECT: {
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: [],
backlogTaskIds: ['TEST'],
},
} as any
}
});
});
describe('should fix duplicate entities for', () => {
it('task', () => {
expect(dataRepair({
...mock,
task: {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'DUPE',
title: 'DUPE',
}, {
...DEFAULT_TASK,
id: 'DUPE',
title: 'DUPE',
}, {
...DEFAULT_TASK,
id: 'NO_DUPE',
title: 'NO_DUPE',
}])
} as any,
})).toEqual({
...mock,
task: {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'DUPE',
title: 'DUPE',
}, {
...DEFAULT_TASK,
id: 'NO_DUPE',
title: 'NO_DUPE',
}])
} as any,
});
});
it('taskArchive', () => {
expect(dataRepair({
...mock,
taskArchive: {
...mock.taskArchive,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'DUPE',
title: 'DUPE',
}, {
...DEFAULT_TASK,
id: 'DUPE',
title: 'DUPE',
}, {
...DEFAULT_TASK,
id: 'NO_DUPE',
title: 'NO_DUPE',
}])
} as any,
})).toEqual({
...mock,
taskArchive: {
...mock.taskArchive,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'DUPE',
title: 'DUPE',
}, {
...DEFAULT_TASK,
id: 'NO_DUPE',
title: 'NO_DUPE',
}])
} as any,
});
});
});
describe('should fix inconsistent entity states for', () => {
it('task', () => {
expect(dataRepair({
...mock,
task: {
ids: ['AAA, XXX', 'YYY'],
entities: {
AAA: {},
CCC: {},
}
} as any,
})).toEqual({
...mock,
task: {
ids: ['AAA', 'CCC'],
entities: {
AAA: {},
CCC: {},
}
} as any,
});
});
it('taskArchive', () => {
expect(dataRepair({
...mock,
taskArchive: {
ids: ['AAA, XXX', 'YYY'],
entities: {
AAA: {},
CCC: {},
}
} as any,
})).toEqual({
...mock,
taskArchive: {
ids: ['AAA', 'CCC'],
entities: {
AAA: {},
CCC: {},
}
} as any,
});
});
});
it('should restore missing tasks from taskArchive if available', () => {
const taskArchiveState = {
...mock.taskArchive,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'goneToArchiveToday',
title: 'goneToArchiveToday',
projectId: 'TEST_ID_PROJECT',
}, {
...DEFAULT_TASK,
id: 'goneToArchiveBacklog',
title: 'goneToArchiveBacklog',
projectId: 'TEST_ID_PROJECT',
}])
} as any;
const projectState: ProjectState = {
...fakeEntityStateFromArray([{
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: ['goneToArchiveToday', 'GONE'],
backlogTaskIds: ['goneToArchiveBacklog', 'GONE'],
}] as Partial<Project> []),
};
expect(dataRepair({
...mock,
project: projectState,
taskArchive: taskArchiveState,
task: {
...mock.task,
...createEmptyEntity()
} as any,
})).toEqual({
...mock,
task: {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'goneToArchiveToday',
title: 'goneToArchiveToday',
projectId: 'TEST_ID_PROJECT',
}, {
...DEFAULT_TASK,
id: 'goneToArchiveBacklog',
title: 'goneToArchiveBacklog',
projectId: 'TEST_ID_PROJECT',
}])
} as any,
project: {
...projectState,
entities: {
TEST_ID_PROJECT: {
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: ['goneToArchiveToday'],
backlogTaskIds: ['goneToArchiveBacklog'],
},
} as any
}
});
});
it('should add orphan tasks to their project list', () => {
const taskState = {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
id: 'orphanedTask',
title: 'orphanedTask',
projectId: 'TEST_ID_PROJECT',
parentId: null,
}, {
...DEFAULT_TASK,
id: 'orphanedTaskOtherProject',
title: 'orphanedTaskOtherProject',
projectId: 'TEST_ID_PROJECT_OTHER',
parentId: null,
}, {
...DEFAULT_TASK,
id: 'regularTaskOtherProject',
title: 'regularTaskOtherProject',
projectId: 'TEST_ID_PROJECT_OTHER',
parentId: null,
}])
} as any;
const projectState: ProjectState = {
...fakeEntityStateFromArray([{
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: ['GONE'],
backlogTaskIds: [],
}, {
title: 'TEST_PROJECT_OTHER',
id: 'TEST_ID_PROJECT_OTHER',
taskIds: ['regularTaskOtherProject'],
backlogTaskIds: [],
}] as Partial<Project> []),
};
expect(dataRepair({
...mock,
project: projectState,
task: taskState,
})).toEqual({
...mock,
task: taskState,
project: {
...projectState,
entities: {
TEST_ID_PROJECT: {
title: 'TEST_PROJECT',
id: 'TEST_ID_PROJECT',
taskIds: ['orphanedTask'],
backlogTaskIds: [],
},
TEST_ID_PROJECT_OTHER: {
title: 'TEST_PROJECT_OTHER',
id: 'TEST_ID_PROJECT_OTHER',
taskIds: ['regularTaskOtherProject', 'orphanedTaskOtherProject'],
backlogTaskIds: [],
},
} as any
}
});
});
});

View file

@ -0,0 +1,127 @@
import { AppBaseDataEntityLikeStates, AppDataComplete } from '../../imex/sync/sync.model';
import { TagCopy } from '../../features/tag/tag.model';
import { ProjectCopy } from '../../features/project/project.model';
const ENTITY_STATE_KEYS: (keyof AppDataComplete)[] = ['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter'];
export const dataRepair = (data: AppDataComplete): AppDataComplete => {
// console.time('dataRepair');
let dataOut: AppDataComplete = data;
// let dataOut: AppDataComplete = dirtyDeepCopy(data);
dataOut = _fixEntityStates(dataOut);
dataOut = _removeMissingTasksFromListsOrRestoreFromArchive(dataOut);
dataOut = _removeDuplicatesFromArchive(dataOut);
dataOut = _addOrphanedTasksToProjectLists(dataOut);
// console.timeEnd('dataRepair');
return dataOut;
};
const _fixEntityStates = (data: AppDataComplete): AppDataComplete => {
ENTITY_STATE_KEYS.forEach((key) => {
data[key] = _resetEntityIdsFromObjects(data[key] as AppBaseDataEntityLikeStates) as any;
});
return data;
};
const _removeDuplicatesFromArchive = (data: AppDataComplete): AppDataComplete => {
const taskIds = data.task.ids as string[];
const archiveTaskIds = data.taskArchive.ids as string[];
const duplicateIds = taskIds.filter((id) => archiveTaskIds.includes(id));
if (duplicateIds.length) {
data.taskArchive.ids = archiveTaskIds.filter(id => !duplicateIds.includes(id));
duplicateIds.forEach(id => {
if (data.taskArchive.entities[id]) {
delete data.taskArchive.entities[id];
}
});
if (duplicateIds.length > 0) {
console.log(duplicateIds.length + ' duplicates removed from archive.');
}
}
return data;
};
const _removeMissingTasksFromListsOrRestoreFromArchive = (data: AppDataComplete): AppDataComplete => {
const {task, project, tag, taskArchive} = data;
const taskIds: string[] = task.ids;
const taskArchiveIds: string[] = taskArchive.ids as string[];
const taskIdsToRestoreFromArchive: string[] = [];
project.ids.forEach((pId: string | number) => {
const projectItem = project.entities[pId] as ProjectCopy;
projectItem.taskIds = projectItem.taskIds.filter((id: string): boolean => {
if (taskArchiveIds.includes(id)) {
taskIdsToRestoreFromArchive.push(id);
return true;
}
return taskIds.includes(id);
});
projectItem.backlogTaskIds = projectItem.backlogTaskIds.filter((id: string): boolean => {
if (taskArchiveIds.includes(id)) {
taskIdsToRestoreFromArchive.push(id);
return true;
}
return taskIds.includes(id);
});
});
tag.ids.forEach((tId: string | number) => {
const tagItem = tag.entities[tId] as TagCopy;
tagItem.taskIds = tagItem.taskIds.filter(id => taskIds.includes(id));
});
taskIdsToRestoreFromArchive.forEach(id => {
task.entities[id] = taskArchive.entities[id];
delete taskArchive.entities[id];
});
task.ids = [...taskIds, ...taskIdsToRestoreFromArchive];
taskArchive.ids = taskArchiveIds.filter(id => !taskIdsToRestoreFromArchive.includes(id));
if (taskIdsToRestoreFromArchive.length > 0) {
console.log(taskIdsToRestoreFromArchive.length + ' missing tasks restored from archive.');
}
return data;
};
const _resetEntityIdsFromObjects = <T>(data: AppBaseDataEntityLikeStates): AppBaseDataEntityLikeStates => {
return {
...data,
ids: Object.keys(data.entities)
};
};
const _addOrphanedTasksToProjectLists = (data: AppDataComplete): AppDataComplete => {
const {task, project} = data;
let allTaskIdsOnProjectLists: string[] = [];
project.ids.forEach((pId: string | number) => {
const projectItem = project.entities[pId] as ProjectCopy;
allTaskIdsOnProjectLists = allTaskIdsOnProjectLists.concat(projectItem.taskIds, projectItem.backlogTaskIds);
});
const orphanedTaskIds: string[] = task.ids.filter(tid => {
const taskItem = task.entities[tid];
if (!taskItem) {
throw new Error('Missing task');
}
return !taskItem.parentId && !allTaskIdsOnProjectLists.includes(tid) && taskItem.projectId;
});
orphanedTaskIds.forEach(tid => {
const taskItem = task.entities[tid];
if (!taskItem) {
throw new Error('Missing task');
}
project.entities[taskItem.projectId as string]?.taskIds.push(tid);
});
if (orphanedTaskIds.length > 0) {
console.log(orphanedTaskIds.length + ' orphaned tasks found & restored.');
}
return data;
};

View file

@ -68,7 +68,7 @@ export class MigrationService {
lastLocalSyncModelChange: legacyAppDataComplete.lastActiveTime,
archivedProjects: legacyAppDataComplete.archivedProjects,
globalConfig: legacyAppDataComplete.globalConfig,
reminders: legacyAppDataComplete.reminders,
reminders: legacyAppDataComplete.reminders || [],
// new
tag: initialTagState,
simpleCounter: initialSimpleCounterState,
@ -82,6 +82,13 @@ export class MigrationService {
task: this._mTaskState(legacyAppDataComplete),
taskArchive: this._mTaskArchiveState(legacyAppDataComplete),
// NEW PROJECT MODELS
note: {},
metric: {},
improvement: {},
obstruction: {},
bookmark: {},
};
console.log('DATA AFTER MIGRATIONS', newAppData);

View file

@ -88,6 +88,6 @@ export class NotifyService {
}
private _isServiceWorkerAvailable(): boolean {
return 'serviceWorker' in navigator && environment.production && !IS_ELECTRON;
return 'serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON;
}
}

View file

@ -77,6 +77,7 @@ export class DatabaseService {
} catch (e) {
console.error('Database initialization failed');
console.error('_lastParams', this._lastParams);
alert('IndexedDB Error');
throw new Error(e);
}

View file

@ -43,6 +43,8 @@ export const LS_DROPBOX_LOCAL_LAST_SYNC_CHECK = LS_PREFIX + 'DROPBOX_LOCAL_LAST_
export const LS_ACTION_LOG = LS_PREFIX + 'ACTION_LOG';
export const LS_ACTION_BEFORE_LAST_ERROR_LOG = LS_PREFIX + 'LAST_ERROR_ACTION_LOG';
export const LS_CHECK_STRAY_PERSISTENCE_BACKUP = LS_PREFIX + 'CHECK_STRAY_PERSISTENCE_BACKUP';
export const LS_NO_PERSISTENCE_NOTE = LS_PREFIX + 'NO_PERSISTENCE_NOTE';
// SESSION STORAGE
const SS_PREFIX = 'SUP_SS_';

View file

@ -0,0 +1,17 @@
import { createAction, props } from '@ngrx/store';
// log only
export const saveToDb = createAction(
'[Persistence] Save to DB',
props<{ dbKey: string; data: any }>(),
);
export const removeFromDb = createAction(
'[Persistence] Remove from DB',
props<{ dbKey: string; }>(),
);
export const loadFromDb = createAction(
'[Persistence] Load from DB',
props<{ dbKey: string; }>(),
);

View file

@ -6,6 +6,7 @@ import { PersistenceService } from './persistence.service';
import { TestScheduler } from 'rxjs/testing';
import { of } from 'rxjs';
import { createEmptyEntity } from '../../util/create-empty-entity';
import { provideMockStore } from '@ngrx/store/testing';
const testScheduler = new TestScheduler((actual, expected) => {
// asserting the two objects are equal
@ -16,6 +17,7 @@ describe('PersistenceService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideMockStore({initialState: {}}),
{
provide: SnackService, useValue: {
open: () => false,

View file

@ -48,7 +48,7 @@ import { Obstruction, ObstructionState } from '../../features/metric/obstruction
import { TaskRepeatCfg, TaskRepeatCfgState } from '../../features/task-repeat-cfg/task-repeat-cfg.model';
import { Bookmark } from '../../features/bookmark/bookmark.model';
import { Note } from '../../features/note/note.model';
import { Action } from '@ngrx/store';
import { Action, Store } from '@ngrx/store';
import { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer';
import { Tag, TagState } from '../../features/tag/tag.model';
import { migrateProjectState } from '../../features/project/migrate-projects-state.util';
@ -62,8 +62,10 @@ import { checkFixEntityStateConsistency } from '../../util/check-fix-entity-stat
import { SimpleCounter, SimpleCounterState } from '../../features/simple-counter/simple-counter.model';
import { simpleCounterReducer } from '../../features/simple-counter/store/simple-counter.reducer';
import { from, merge, Observable, Subject } from 'rxjs';
import { concatMap, shareReplay } from 'rxjs/operators';
import { concatMap, shareReplay, skipWhile } from 'rxjs/operators';
import { devError } from '../../util/dev-error';
import { isValidAppData } from '../../imex/sync/is-valid-app-data.util';
import { removeFromDb, saveToDb } from './persistence.actions';
@Injectable({
providedIn: 'root',
@ -147,7 +149,7 @@ export class PersistenceService {
this.onAfterSave$.pipe(
concatMap(() => this.loadComplete()),
// TODO maybe not necessary
// skipWhile(complete => !isValidAppData(complete)),
skipWhile(complete => !isValidAppData(complete)),
),
).pipe(
shareReplay(1),
@ -159,6 +161,7 @@ export class PersistenceService {
constructor(
private _databaseService: DatabaseService,
private _compressionService: CompressionService,
private _store: Store<any>,
) {
// this.inMemoryComplete$.subscribe((v) => console.log('inMemoryComplete$', v));
}
@ -285,6 +288,10 @@ export class PersistenceService {
return this._saveToDb({dbKey: LS_BACKUP, data, isDataImport: true, isSyncModelChange: true});
}
async clearBackup(): Promise<unknown> {
return this._removeFromDb({dbKey: LS_BACKUP});
}
// NOTE: not including backup
async loadComplete(): Promise<AppDataComplete> {
let r;
@ -520,6 +527,7 @@ export class PersistenceService {
}): Promise<any> {
if (!this._isBlockSaving || isDataImport === true) {
const idbKey = this._getIDBKey(dbKey, projectId);
this._store.dispatch(saveToDb({dbKey, data}));
const r = await this._databaseService.save(idbKey, data);
this._updateInMemory({
@ -547,6 +555,7 @@ export class PersistenceService {
}): Promise<any> {
const idbKey = this._getIDBKey(dbKey, projectId);
if (!this._isBlockSaving || isDataImport === true) {
this._store.dispatch(removeFromDb({dbKey}));
return this._databaseService.remove(idbKey);
} else {
console.warn('BLOCKED SAVING for ', dbKey);
@ -560,6 +569,8 @@ export class PersistenceService {
projectId?: string,
}): Promise<any> {
const idbKey = this._getIDBKey(dbKey, projectId);
// NOTE: too much clutter
// this._store.dispatch(loadFromDb({dbKey}));
// TODO remove legacy stuff
return await this._databaseService.load(idbKey) || await this._databaseService.load(legacyDBKey) || undefined;
}

View file

@ -6,7 +6,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
lng: null
},
misc: {
isDarkMode: false,
isDarkMode: true,
isConfirmBeforeExit: false,
isNotifyWhenTimeEstimateExceeded: false,
isAutMarkParentAsDone: false,
@ -112,5 +112,9 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = {
isPlayDoneSound: true,
isIncreaseDoneSoundPitch: true,
doneSound: 'done2.mp3',
},
trackingReminder: {
isEnabled: true,
minTime: minute * 2,
}
};

View file

@ -0,0 +1,25 @@
// tslint:disable:max-line-length
import { ConfigFormSection, TrackingReminderConfig } from '../global-config.model';
import { T } from '../../../t.const';
export const TRACKING_REMINDER_FORM_CFG: ConfigFormSection<TrackingReminderConfig> = {
title: T.GCF.TRACKING_REMINDER.TITLE,
help: T.GCF.TRACKING_REMINDER.HELP,
key: 'trackingReminder',
items: [
{
key: 'isEnabled',
type: 'checkbox',
templateOptions: {
label: T.GCF.TRACKING_REMINDER.L_IS_ENABLED,
},
},
{
key: 'minTime',
type: 'duration',
templateOptions: {
label: T.GCF.TRACKING_REMINDER.L_MIN_TIME
},
},
]
};

View file

@ -12,12 +12,14 @@ import { EVALUATION_SETTINGS_FORM_CFG } from './form-cfgs/evaluation-settings-fo
import { SIMPLE_COUNTER_FORM } from './form-cfgs/simple-counter-form.const';
import { DROPBOX_SYNC_FORM } from './form-cfgs/dropbox-sync-form.const';
import { SOUND_FORM_CFG } from './form-cfgs/sound-form.const';
import { TRACKING_REMINDER_FORM_CFG } from './form-cfgs/tracking-reminder-form.const';
export const GLOBAL_CONFIG_FORM_CONFIG: ConfigFormConfig = [
(LANGUAGE_SELECTION_FORM_FORM as GenericConfigFormSection),
(MISC_SETTINGS_FORM_CFG as GenericConfigFormSection),
(IDLE_FORM_CFG as GenericConfigFormSection),
(KEYBOARD_SETTINGS_FORM_CFG as GenericConfigFormSection),
(TRACKING_REMINDER_FORM_CFG as GenericConfigFormSection),
(SOUND_FORM_CFG as GenericConfigFormSection),
];

View file

@ -125,6 +125,11 @@ export type SoundConfig = Readonly<{
volume: number;
}>;
export type TrackingReminderConfig = Readonly<{
isEnabled: boolean;
minTime: number;
}>;
export type GlobalConfigState = Readonly<{
lang: LanguageConfig;
misc: MiscConfig;
@ -137,6 +142,7 @@ export type GlobalConfigState = Readonly<{
keyboard: KeyboardConfig;
localBackup: LocalBackupConfig;
sound: SoundConfig;
trackingReminder: TrackingReminderConfig;
[MODEL_VERSION_KEY]?: number;
}>;

View file

@ -3,7 +3,7 @@ import { DEFAULT_GLOBAL_CONFIG } from './default-global-config.const';
import { MODEL_VERSION_KEY } from '../../app.constants';
import { isMigrateModel } from '../../util/model-version';
const MODEL_VERSION = 1.2;
const MODEL_VERSION = 1.3;
export const migrateGlobalConfigState = (globalConfigState: GlobalConfigState): GlobalConfigState => {
if (!isMigrateModel(globalConfigState, MODEL_VERSION, 'GlobalConfig')) {

View file

@ -22,6 +22,8 @@ import { DialogDbxSyncConflictComponent } from './dialog-dbx-sync-conflict/dialo
import { SnackService } from '../../core/snack/snack.service';
import { environment } from '../../../environments/environment';
import { T } from '../../t.const';
import { isValidAppData } from '../../imex/sync/is-valid-app-data.util';
import { TranslateService } from '@ngx-translate/core';
@Injectable({providedIn: 'root'})
export class DropboxSyncService {
@ -58,6 +60,7 @@ export class DropboxSyncService {
private _dataInitService: DataInitService,
private _snackService: SnackService,
private _matDialog: MatDialog,
private _translateService: TranslateService,
) {
// TODO initial syncing (do with immediate triggers)
}
@ -79,7 +82,7 @@ export class DropboxSyncService {
if (isAxiosError && e.response.data && e.response.data.error_summary === 'path/not_found/..') {
dbxLog('DBX: File not found => ↑↑↑ Initial Upload ↑↑↑');
local = await this._syncService.inMemory$.pipe(take(1)).toPromise();
local = await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise();
return await this._uploadAppData(local);
} else if (isAxiosError && e.response.status === 401) {
this._snackService.open({msg: T.F.DROPBOX.S.AUTH_ERROR, type: 'ERROR'});
@ -109,7 +112,7 @@ export class DropboxSyncService {
if (rev && rev === localRev) {
dbxLog('DBX PRE1: ↔ Same Rev', rev);
// NOTE: same rev, doesn't mean. that we can't have local changes
local = await this._syncService.inMemory$.pipe(take(1)).toPromise();
local = await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise();
if (lastSync === local.lastLocalSyncModelChange) {
dbxLog('DBX PRE1: No local changes to sync');
return;
@ -120,9 +123,10 @@ export class DropboxSyncService {
// simple check based on file meta data
// ------------------------------------
// if not defined yet
local = local || await this._syncService.inMemory$.pipe(take(1)).toPromise();
local = local || await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise();
if (local.lastLocalSyncModelChange === 0) {
if (!confirm('lastLocalSyncModelChange is 0. Which means data has been deleted or something is wrong. Proceed with Dropbox sync?')) {
console.log(local);
if (!(this._c(T.F.DROPBOX.C.EMPTY_SYNC))) {
return;
}
}
@ -169,7 +173,7 @@ export class DropboxSyncService {
case UpdateCheckResult.RemoteNotUpToDateDespiteSync: {
dbxLog('DBX: X Remote not up to date despite sync');
if (confirm('Try to re-load data from remote once again?')) {
if (this._c(T.F.DROPBOX.C.TRY_LOAD_REMOTE_AGAIN)) {
return this.sync();
} else {
return this._handleConflict({remote, local, lastSync, downloadMeta: r.meta});
@ -192,11 +196,11 @@ export class DropboxSyncService {
case UpdateCheckResult.ErrorLastSyncNewerThanLocal: {
dbxLog('DBX: XXX Wrong Data');
if (local.lastLocalSyncModelChange > remote.lastLocalSyncModelChange) {
if (confirm('Upload local data anyway?')) {
if (this._c(T.F.DROPBOX.C.FORCE_UPLOAD)) {
return await this._uploadAppData(local, true);
}
} else {
if (confirm('Import remote data anyway?')) {
if (this._c(T.F.DROPBOX.C.FORCE_IMPORT)) {
return await this._importData(remote, r.meta.rev);
}
}
@ -239,6 +243,12 @@ export class DropboxSyncService {
}
private async _uploadAppData(data: AppDataComplete, isForceOverwrite: boolean = false): Promise<DropboxFileMetadata | undefined> {
if (!isValidAppData(data)) {
console.log(data);
alert('The data you are trying to upload is invalid');
throw new Error('The data you are trying to upload is invalid');
}
try {
const r = await this._dropboxApiService.upload({
path: DROPBOX_SYNC_FILE_PATH,
@ -254,7 +264,7 @@ export class DropboxSyncService {
} catch (e) {
console.error(e);
dbxLog('DBX: X Upload Request Error');
if (confirm('An Error occurred while uploading your local data. Try to force the update?')) {
if (this._c(T.F.DROPBOX.C.FORCE_UPLOAD_AFTER_ERROR)) {
return this._uploadAppData(data, true);
}
}
@ -329,4 +339,9 @@ export class DropboxSyncService {
}
}).afterClosed();
}
private _c(str: string): boolean {
return confirm(this._translateService.instant(str));
};
}

View file

@ -53,7 +53,7 @@ export class GithubApiService {
`+repo:${cfg.repo}`;
return this._sendRequest$({
url: `${BASE}search/issues?q=${encodeURI(searchText + repoQuery)}`
url: `${BASE}search/issues?q=${encodeURIComponent(searchText + repoQuery)}`
}, cfg)
.pipe(
map((res: GithubIssueSearchResult) => {

View file

@ -109,7 +109,7 @@ export class JiraApiService {
issuePicker$(searchTerm: string, cfg: JiraCfg): Observable<SearchResultItem[]> {
const searchStr = `${searchTerm}`;
const jql = (cfg.searchJqlQuery ? `${encodeURI(cfg.searchJqlQuery)}` : '');
const jql = (cfg.searchJqlQuery ? `${encodeURIComponent(cfg.searchJqlQuery)}` : '');
return this._sendRequest$({
jiraReqCfg: {

View file

@ -126,6 +126,8 @@ export class ReminderService {
throw new Error('A reminder for this ' + type + ' already exists');
}
// TODO find out why we need to do this
this._reminders = dirtyDeepCopy(this._reminders);
this._reminders.push({
id,
workContextId: this._workContextService.activeWorkContextId as string,
@ -148,6 +150,8 @@ export class ReminderService {
updateReminder(reminderId: string, reminderChanges: Partial<Reminder>) {
const i = this._reminders.findIndex(reminder => reminder.id === reminderId);
if (i > -1) {
// TODO find out why we need to do this
this._reminders = dirtyDeepCopy(this._reminders);
this._reminders[i] = Object.assign({}, this._reminders[i], reminderChanges);
}
this._saveModel(this._reminders);
@ -157,6 +161,8 @@ export class ReminderService {
const i = this._reminders.findIndex(reminder => reminder.id === reminderIdToRemove);
if (i > -1) {
// TODO find out why we need to do this
this._reminders = dirtyDeepCopy(this._reminders);
this._reminders.splice(i, 1);
this._saveModel(this._reminders);
} else {

View file

@ -128,10 +128,6 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy {
this._subs.add(this.activatedIssueTask$.subscribe((v) => this.activatedIssueTask = v));
this._subs.add(this.shortSyntaxTags$.subscribe((v) => this.shortSyntaxTags = v));
this._subs.add(this.inputVal$.subscribe((v) => this.inputVal = v));
// TODO remove
this._subs.add(this.shortSyntaxTags$.subscribe((v) => console.log('shortSyntaxTags$', v)));
this._subs.add(this.filteredIssueSuggestions$.subscribe((val) => console.log('filteredIssueSuggestions$', val)));
}
ngAfterViewInit(): void {

View file

@ -67,6 +67,7 @@ export class TaskDbEffects {
private _persistenceService: PersistenceService) {
}
// @debounce(50)
private _saveToLs(taskState: TaskState, isSyncModelChange: boolean = false) {
this._persistenceService.task.saveState({
...taskState,

View file

@ -10,7 +10,7 @@ import {
UpdateTask,
UpdateTaskTags
} from './task.actions';
import { concatMap, filter, first, map, mapTo, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { concatMap, delay, filter, first, map, mapTo, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { PersistenceService } from '../../../core/persistence/persistence.service';
import { Task, TaskArchive, TaskWithSubTasks } from '../task.model';
import { ReminderService } from '../../reminder/reminder.service';
@ -154,6 +154,8 @@ export class TaskRelatedModelEffects {
// we only want to execute this for task title updates
return (changeProps.length === 1 && changeProps[0] === 'title');
}),
// dirty fix to execute this after setDefaultProjectId$ effect
delay(20),
concatMap((action: AddTask | UpdateTask): Observable<any> => {
return this._taskService.getByIdOnce$(action.payload.task.id as string);
}),
@ -162,8 +164,6 @@ export class TaskRelatedModelEffects {
this._projectService.list$,
),
mergeMap(([task, tags, projects]) => {
console.log('ADDED TASK', task);
const r = shortSyntax(task, tags, projects);
if (!r) {
return EMPTY;
@ -197,14 +197,17 @@ export class TaskRelatedModelEffects {
}
if (tagIds && tagIds.length) {
const isEqualTags = (JSON.stringify(tagIds) === JSON.stringify(task.tagIds));
if (!task.tagIds) {
throw new Error('Task Old TagIds need to be passed');
}
actions.push(new UpdateTaskTags({
task,
newTagIds: unique(tagIds),
oldTagIds: task.tagIds,
}));
if (!isEqualTags) {
actions.push(new UpdateTaskTags({
task,
newTagIds: unique(tagIds),
oldTagIds: task.tagIds,
}));
}
}
return actions;

View file

@ -74,6 +74,7 @@ export class TaskUiEffects {
),
filter(({payload: {task: {changes}}}: UpdateTask) => !!changes.isDone),
withLatestFrom(this._workContextService.flatDoneTodayNr$, this._globalConfigService.sound$),
filter(([, , soundCfg]) => soundCfg.isPlayDoneSound),
tap(([, doneToday, soundCfg]) => playDoneSound(soundCfg, doneToday)),
);

View file

@ -22,7 +22,7 @@
//overflow: visible;
:host:focus & {
border: 1px dashed $c-focus-border;
border: 1px solid $c-focus-border;
border-color: $c-focus-border !important;
}

View file

@ -103,13 +103,14 @@
// NOTE: somehow ng build messes up, if we don't include it like that
border-color: $task-c-focus;
border-width: 1px;
border-style: dashed !important;
border-style: solid !important;
}
}
:host.isCurrent.isCurrent.isCurrent.isCurrent.isCurrent & {
border-color: $task-c-current !important;
border-width: 1px !important;
border-style: dashed;
border-radius: 12px !important;
bottom: -2px;

View file

@ -38,6 +38,7 @@ import { TODAY_TAG } from '../../tag/tag.const';
import { DialogEditTagsForTaskComponent } from '../../tag/dialog-edit-tags/dialog-edit-tags-for-task.component';
import { WorkContextService } from '../../work-context/work-context.service';
import { environment } from '../../../../environments/environment';
import { throttle } from 'helpful-decorators';
@Component({
selector: 'task',
@ -59,7 +60,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
ShowSubTasksMode: typeof ShowSubTasksMode = ShowSubTasksMode;
contextMenuPosition: { x: string; y: string } = {x: '0px', y: '0px'};
progress: number = 0;
isDev: boolean = !environment.production;
isDev: boolean = !(environment.production || environment.stage);
@ViewChild('contentEditableOnClickEl', {static: true}) contentEditableOnClickEl?: ElementRef;
@ViewChild('blockLeftEl') blockLeftElRef?: ElementRef;
@ViewChild('blockRightEl') blockRightElRef?: ElementRef;
@ -91,6 +92,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
private _dragEnterTarget?: HTMLElement;
private _destroy$: Subject<boolean> = new Subject<boolean>();
private _currentPanTimeout?: number;
private _isTaskDeleteTriggered: boolean = false;
constructor(
private readonly _taskService: TaskService,
@ -236,6 +238,11 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
}
deleteTask() {
if (this._isTaskDeleteTriggered) {
return;
}
this._isTaskDeleteTriggered = true;
this._taskService.remove(this.task);
this.focusNext(true);
}
@ -284,6 +291,14 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
this._taskService.addSubTaskTo(this.task.parentId || this.task.id);
}
@throttle(200, {leading: true, trailing: false})
toggleDoneKeyboard() {
this.toggleTaskDone();
if (!this.task.parentId) {
this.focusNext(true);
}
}
toggleTaskDone() {
if (this.task.parentId) {
this.focusNext(true);
@ -601,14 +616,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit {
this.editReminder();
}
if (checkKeyCombo(ev, keys.taskToggleDone)) {
this.toggleTaskDone();
if (!this.task.parentId) {
if (this.task.isDone) {
this.focusPrevious(true);
} else {
this.focusNext(true);
}
}
this.toggleDoneKeyboard();
}
if (checkKeyCombo(ev, keys.taskAddSubTask)) {
this.addSubTask();

View file

@ -125,12 +125,13 @@ export class IdleService {
if (task) {
if (typeof task === 'string') {
this._taskService.add(task, false, {
const currId = this._taskService.add(task, false, {
timeSpent,
timeSpentOnDay: {
[getWorklogStr()]: timeSpent
}
});
this._taskService.setCurrentId(currId);
} else {
this._taskService.addTimeSpent(task, timeSpent);
this._taskService.setCurrentId(task.id);

View file

@ -6,6 +6,7 @@ import { UiModule } from '../../ui/ui.module';
import { FormsModule } from '@angular/forms';
import { TakeABreakModule } from './take-a-break/take-a-break.module';
import { TasksModule } from '../tasks/tasks.module';
import { DialogTrackingReminderComponent } from './tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component';
@NgModule({
imports: [
@ -15,7 +16,8 @@ import { TasksModule } from '../tasks/tasks.module';
TasksModule,
],
declarations: [
DialogIdleComponent
DialogIdleComponent,
DialogTrackingReminderComponent
],
exports: [
TakeABreakModule,

View file

@ -0,0 +1,37 @@
<form (submit)="track()">
<div class="dialog-content"
mat-dialog-content>
<p>{{T.F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME|translate}}</p>
<div class="time">{{data.remindCounter$|async|msToString:true}}</div>
<select-task (taskChange)="onTaskChange($event)"
[initialTask]="selectedTask"></select-task>
<div class="track-to-label">
<span *ngIf="!isCreate">{{T.F.TIME_TRACKING.D_TRACKING_REMINDER.TRACK_TO|translate}}</span>
<span *ngIf="isCreate"
[innerHTML]="T.F.TIME_TRACKING.D_TRACKING_REMINDER.CREATE_AND_TRACK|translate"></span>
</div>
</div>
<div align="center"
mat-dialog-actions>
<button (click)="cancel()"
color=""
mat-button
type="button">
<mat-icon>close</mat-icon>
{{T.G.CANCEL|translate}}
</button>
<button [disabled]="!(selectedTask||newTaskTitle)"
color="primary"
mat-stroked-button
type="submit">
<mat-icon *ngIf="!isCreate">track_changes</mat-icon>
<mat-icon *ngIf="isCreate">add</mat-icon>
{{T.F.TIME_TRACKING.D_TRACKING_REMINDER.TASK|translate}}
</button>
</div>
</form>

View file

@ -0,0 +1,21 @@
@import "../../../../../variables";
.dialog-content {
p,
.track-to-label,
.time {
text-align: center;
}
}
.time {
font-size: 20px;
font-weight: bold;
margin-bottom: $s;
}
.track-to-label {
margin-bottom: $s;
font-size: $s*2;
font-weight: bold;
}

View file

@ -0,0 +1,25 @@
// import { async, ComponentFixture, TestBed } from '@angular/core/testing';
//
// import { DialogIdleComponent } from './dialog-idle.component';
//
// describe('DialogIdleComponent', () => {
// let component: DialogIdleComponent;
// let fixture: ComponentFixture<DialogIdleComponent>;
//
// beforeEach(async(() => {
// TestBed.configureTestingModule({
// declarations: [DialogIdleComponent]
// })
// .compileComponents();
// }));
//
// beforeEach(() => {
// fixture = TestBed.createComponent(DialogIdleComponent);
// component = fixture.componentInstance;
// fixture.detectChanges();
// });
//
// it('should create', () => {
// expect(component).toBeTruthy();
// });
// });

View file

@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { TaskService } from '../../../tasks/task.service';
import { Observable } from 'rxjs';
import { Task } from '../../../tasks/task.model';
import { T } from '../../../../t.const';
@Component({
selector: 'dialog-tracking-reminder',
templateUrl: './dialog-tracking-reminder.component.html',
styleUrls: ['./dialog-tracking-reminder.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DialogTrackingReminderComponent implements OnInit {
T: typeof T = T;
lastCurrentTask$: Observable<Task> = this._taskService.getByIdOnce$(this.data.lastCurrentTaskId);
selectedTask: Task | null = null;
newTaskTitle?: string;
isCreate?: boolean;
constructor(
private _taskService: TaskService,
private _matDialogRef: MatDialogRef<DialogTrackingReminderComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
_matDialogRef.disableClose = true;
}
ngOnInit() {
this.lastCurrentTask$.subscribe((task) => {
this.selectedTask = task;
this.isCreate = false;
});
}
onTaskChange(taskOrTaskTitle: Task | string) {
this.isCreate = (typeof taskOrTaskTitle === 'string');
if (this.isCreate) {
this.newTaskTitle = taskOrTaskTitle as string;
this.selectedTask = null;
} else {
this.selectedTask = taskOrTaskTitle as Task;
this.newTaskTitle = undefined;
}
}
cancel() {
this._matDialogRef.close();
}
track() {
this._matDialogRef.close({
task: this.selectedTask || this.newTaskTitle,
});
}
}

View file

@ -0,0 +1,16 @@
// import { TestBed } from '@angular/core/testing';
//
// import { StartTimerReminderService } from './start-timer-reminder.service';
//
// describe('StartTimerReminderService', () => {
// let service: StartTimerReminderService;
//
// beforeEach(() => {
// TestBed.configureTestingModule({});
// service = TestBed.inject(StartTimerReminderService);
// });
//
// it('should be created', () => {
// expect(service).toBeTruthy();
// });
// });

View file

@ -0,0 +1,128 @@
import { Injectable } from '@angular/core';
import { IdleService } from '../idle.service';
import { TaskService } from '../../tasks/task.service';
import { GlobalConfigService } from '../../config/global-config.service';
import { combineLatest, EMPTY, merge, Observable, of, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { realTimer$ } from '../../../util/real-timer';
import { BannerService } from '../../../core/banner/banner.service';
import { BannerId } from '../../../core/banner/banner.model';
import { msToString } from '../../../ui/duration/ms-to-string.pipe';
import { MatDialog } from '@angular/material/dialog';
import { DialogTrackingReminderComponent } from './dialog-tracking-reminder/dialog-tracking-reminder.component';
import { Task } from '../../tasks/task.model';
import { getWorklogStr } from '../../../util/get-work-log-str';
import { T } from '../../../t.const';
import { TranslateService } from '@ngx-translate/core';
import { TrackingReminderConfig } from '../../config/global-config.model';
@Injectable({
providedIn: 'root'
})
export class TrackingReminderService {
_cfg$: Observable<TrackingReminderConfig> = this._globalConfigService.cfg$.pipe(map(cfg => cfg?.trackingReminder));
_counter$: Observable<number> = realTimer$(1000);
_manualReset$: Subject<void> = new Subject();
_resetableCounter$: Observable<number> = merge(
of(true),
this._manualReset$,
).pipe(
switchMap(() => this._counter$),
);
remindCounter$: Observable<number> = this._cfg$.pipe(
switchMap((cfg) => !cfg.isEnabled
? EMPTY
: combineLatest([
this._taskService.currentTaskId$,
this._idleService.isIdle$,
]).pipe(
map(([currentTaskId, isIdle]) => !currentTaskId && !isIdle),
distinctUntilChanged(),
switchMap((isEnabled) => isEnabled
? this._resetableCounter$
: of(0)
),
filter(time => time > cfg.minTime),
)
),
shareReplay(),
);
constructor(
private _idleService: IdleService,
private _taskService: TaskService,
private _globalConfigService: GlobalConfigService,
private _bannerService: BannerService,
private _matDialog: MatDialog,
private _translateService: TranslateService,
) {
}
init() {
this.remindCounter$.subscribe((count) => {
this._triggerBanner(count, {});
});
}
private _triggerBanner(duration: number, cfg: any) {
// don't update if this or other dialogs are open
if (this._matDialog.openDialogs.length !== 0) {
return;
}
const durationStr = msToString(duration);
this._bannerService.open({
id: BannerId.StartTrackingReminder,
ico: 'timer',
msg: this._translateService.instant(T.F.TIME_TRACKING.B_TTR.MSG, {time: durationStr}),
action: {
label: T.F.TIME_TRACKING.B_TTR.ADD_TO_TASK,
fn: () => this._openDialog(),
},
action2: {
label: T.G.DISMISS,
fn: () => this._dismissBanner(),
},
});
}
private _openDialog() {
this._matDialog.open(DialogTrackingReminderComponent, {
data: {
remindCounter$: this.remindCounter$,
}
}).afterClosed()
.pipe(
withLatestFrom(this.remindCounter$),
)
.subscribe(async ([{task} = {task: undefined}, remindCounter]: [{ task: Task | string | undefined }, number]): Promise<void> => {
this._manualReset$.next();
const timeSpent = remindCounter;
if (task) {
if (typeof task === 'string') {
const currId = this._taskService.add(task, false, {
timeSpent,
timeSpentOnDay: {
[getWorklogStr()]: timeSpent
}
});
this._taskService.setCurrentId(currId);
} else {
this._taskService.addTimeSpent(task, timeSpent);
this._taskService.setCurrentId(task.id);
}
}
this._dismissBanner();
});
}
private _dismissBanner() {
this._bannerService.dismiss(BannerId.StartTrackingReminder);
this._manualReset$.next();
}
}

View file

@ -8,6 +8,9 @@ import { T } from '../../t.const';
import { MigrationService } from '../../core/migration/migration.service';
import { DataInitService } from '../../core/data-init/data-init.service';
import { isValidAppData } from './is-valid-app-data.util';
import { DataRepairService } from '../../core/data-repair/data-repair.service';
import { LS_CHECK_STRAY_PERSISTENCE_BACKUP } from '../../core/persistence/ls-keys.const';
import { TranslateService } from '@ngx-translate/core';
// TODO some of this can be done in a background script
@ -23,19 +26,26 @@ export class DataImportService {
private _imexMetaService: ImexMetaService,
private _migrationService: MigrationService,
private _dataInitService: DataInitService,
private _dataRepairService: DataRepairService,
private _translateService: TranslateService,
) {
this._isCheckForStrayBackupAndImport();
}
async getCompleteSyncData(): Promise<AppDataComplete> {
return await this._persistenceService.loadComplete();
}
async importCompleteSyncData(data: AppDataComplete, isBackupReload: boolean = false) {
async importCompleteSyncData(data: AppDataComplete, isBackupReload: boolean = false, isSkipStrayBackupCheck: boolean = false) {
this._snackService.open({msg: T.S.SYNC.IMPORTING, ico: 'cloud_download'});
this._imexMetaService.setDataImportInProgress(true);
// get rid of outdated project data
if (!isBackupReload) {
if (!isSkipStrayBackupCheck && await this._isCheckForStrayBackupAndImport()) {
return;
}
await this._persistenceService.saveBackup();
await this._persistenceService.clearDatabaseExceptBackup();
}
@ -46,6 +56,7 @@ export class DataImportService {
// save data to database first then load to store from there
await this._persistenceService.importComplete(migratedData);
await this._loadAllFromDatabaseToStore();
await this._persistenceService.clearBackup();
this._imexMetaService.setDataImportInProgress(false);
this._snackService.open({type: 'SUCCESS', msg: T.S.SYNC.SUCCESS});
@ -55,9 +66,12 @@ export class DataImportService {
msg: T.S.SYNC.ERROR_FALLBACK_TO_BACKUP,
});
console.error(e);
await this._loadBackup();
await this._importBackup();
this._imexMetaService.setDataImportInProgress(false);
}
} else if (this._dataRepairService.isRepairConfirmed()) {
const fixedData = this._dataRepairService.repairData(data);
await this.importCompleteSyncData(fixedData, isBackupReload, true);
} else {
this._snackService.open({type: 'ERROR', msg: T.S.SYNC.ERROR_INVALID_DATA});
console.error(data);
@ -73,8 +87,30 @@ export class DataImportService {
]);
}
private async _loadBackup(): Promise<any> {
private async _importBackup(): Promise<any> {
const data = await this._persistenceService.loadBackup();
return this.importCompleteSyncData(data, true);
}
private async _isCheckForStrayBackupAndImport(): Promise<boolean> {
const backup = await this._persistenceService.loadBackup();
if (!localStorage.getItem(LS_CHECK_STRAY_PERSISTENCE_BACKUP)) {
if (backup) {
await this._persistenceService.clearBackup();
}
localStorage.setItem(LS_CHECK_STRAY_PERSISTENCE_BACKUP, 'true');
}
if (backup) {
if (confirm(this._translateService.instant(T.CONFIRM.RESTORE_STRAY_BACKUP))) {
await this._importBackup();
return true;
} else {
if (confirm(this._translateService.instant(T.CONFIRM.DELETE_STRAY_BACKUP))) {
await this._persistenceService.clearBackup();
}
}
}
return false;
}
}

View file

@ -0,0 +1,135 @@
import { AppDataComplete } from './sync.model';
import { isValidAppData } from './is-valid-app-data.util';
import { MODEL_VERSION_KEY } from '../../app.constants';
import { DEFAULT_TASK, Task } from '../../features/tasks/task.model';
import { fakeEntityStateFromArray } from '../../util/fake-entity-state-from-array';
import { Project } from '../../features/project/project.model';
import { Tag } from '../../features/tag/tag.model';
import { createAppDataCompleteMock } from '../../util/app-data-mock';
// const BASE_STATE_KEYS: (keyof AppBaseData)[] = [
// 'task',
// 'taskArchive',
// 'tag',
// 'project',
// ];
// const PROJECT_STATE_KEYS: (keyof AppDataForProjects)[] = [
// 'note',
// 'bookmark',
// 'metric',
// 'improvement',
// 'obstruction',
// ];
describe('isValidAppData()', () => {
let mock: AppDataComplete;
beforeEach(() => {
mock = createAppDataCompleteMock();
spyOn(window, 'alert').and.stub();
});
it('should work for valid data', () => {
expect(isValidAppData(mock)).toBe(true);
});
describe('should return false for', () => {
['note', 'bookmark', 'improvement', 'obstruction', 'metric', 'task', 'tag', 'globalConfig', 'taskArchive'].forEach((prop) => {
it('missing prop ' + prop, () => {
expect(isValidAppData({
...mock,
[prop]: null,
})).toBe(false);
});
});
});
describe('should error for', () => {
describe('inconsistent entity state', () => {
['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter'].forEach(prop => {
it(prop, () => {
expect(() => isValidAppData({
...mock,
[prop]: {
...mock[prop],
entities: {},
ids: ['asasdasd']
},
})).toThrowError(`Inconsistent entity state "${prop}"`);
});
});
});
it('inconsistent task state', () => {
expect(() => isValidAppData({
...mock,
task: {
...mock.task,
entities: {'A asdds': DEFAULT_TASK},
ids: ['asasdasd']
},
})).toThrowError(`Inconsistent entity state "task"`);
});
it('missing today task data for projects', () => {
expect(() => isValidAppData({
...mock,
// NOTE: it's empty
task: mock.task,
project: {
...fakeEntityStateFromArray([{
title: 'TEST_T',
id: 'TEST_ID',
taskIds: ['gone'],
}] as Partial<Project> []),
[MODEL_VERSION_KEY]: 5
},
})).toThrowError(`Inconsistent Task State: Missing task id gone for Project/Tag TEST_T`);
});
it('missing backlog task data for projects', () => {
expect(() => isValidAppData({
...mock,
// NOTE: it's empty
task: mock.task,
project: {
...fakeEntityStateFromArray([{
title: 'TEST_T',
id: 'TEST_ID',
taskIds: [],
backlogTaskIds: ['goneBL'],
}] as Partial<Project> []),
[MODEL_VERSION_KEY]: 5
},
})).toThrowError(`Inconsistent Task State: Missing task id goneBL for Project/Tag TEST_T`);
});
it('missing today task data for tags', () => {
expect(() => isValidAppData({
...mock,
// NOTE: it's empty
task: mock.task,
tag: {
...fakeEntityStateFromArray([{
title: 'TEST_TAG',
id: 'TEST_ID_TAG',
taskIds: ['goneTag'],
}] as Partial<Tag> []),
[MODEL_VERSION_KEY]: 5
},
})).toThrowError(`Inconsistent Task State: Missing task id goneTag for Project/Tag TEST_TAG`);
});
xit('missing tag for task', () => {
expect(() => isValidAppData({
...mock,
task: {
...mock.task,
...fakeEntityStateFromArray<Task>([{
...DEFAULT_TASK,
tagIds: ['Non existent']
}])
} as any,
})).toThrowError(`No tagX`);
});
});
});

View file

@ -5,40 +5,42 @@ import { devError } from '../../util/dev-error';
import { Tag } from '../../features/tag/tag.model';
import { Project } from '../../features/project/project.model';
// TODO unit test this
export const isValidAppData = (data: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => {
export const isValidAppData = (d: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => {
const dAny: any = d;
// TODO remove this later on
const isCapableModelVersion = data.project
&& data.project[MODEL_VERSION_KEY]
&& typeof data.project[MODEL_VERSION_KEY] === 'number'
&& (data.project[MODEL_VERSION_KEY] as number) >= 5;
const isCapableModelVersion =
(typeof dAny === 'object')
&& d.project
&& d.project[MODEL_VERSION_KEY]
&& typeof d.project[MODEL_VERSION_KEY] === 'number'
&& (d.project[MODEL_VERSION_KEY] as number) >= 5;
// console.time('time isValidAppData');
const isValid = (isCapableModelVersion)
? (typeof (data as any) === 'object')
&& typeof (data as any).note === 'object'
&& typeof (data as any).bookmark === 'object'
&& typeof (data as any).improvement === 'object'
&& typeof (data as any).obstruction === 'object'
&& typeof (data as any).metric === 'object'
&& typeof (data as any).task === 'object'
&& typeof (data as any).tag === 'object'
&& typeof (data as any).globalConfig === 'object'
&& typeof (data as any).taskArchive === 'object'
&& typeof (data as any).project === 'object'
&& Array.isArray(data.reminders)
&& _isEntityStatesConsistent(data)
&& _isTaskIdsConsistent(data, isSkipInconsistentTaskStateError)
? (typeof dAny === 'object') && dAny !== null
&& typeof dAny.note === 'object' && dAny.note !== null
&& typeof dAny.bookmark === 'object' && dAny.bookmark !== null
&& typeof dAny.improvement === 'object' && dAny.improvement !== null
&& typeof dAny.obstruction === 'object' && dAny.obstruction !== null
&& typeof dAny.metric === 'object' && dAny.metric !== null
&& typeof dAny.task === 'object' && dAny.task !== null
&& typeof dAny.tag === 'object' && dAny.tag !== null
&& typeof dAny.globalConfig === 'object' && dAny.globalConfig !== null
&& typeof dAny.taskArchive === 'object' && dAny.taskArchive !== null
&& typeof dAny.project === 'object' && dAny.project !== null
&& Array.isArray(d.reminders)
&& _isEntityStatesConsistent(d)
&& (isSkipInconsistentTaskStateError || _isAllTasksAvailable(d))
: typeof (data as any) === 'object'
: typeof dAny === 'object'
;
// console.timeEnd('time isValidAppData');
return isValid;
};
const _isTaskIdsConsistent = (data: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => {
const _isAllTasksAvailable = (data: AppDataComplete): boolean => {
let allIds: string [] = [];
(data.tag.ids as string[])
@ -65,8 +67,7 @@ const _isTaskIdsConsistent = (data: AppDataComplete, isSkipInconsistentTaskState
);
const notFound = allIds.find(id => !(data.task.ids.includes(id)));
if (notFound && !isSkipInconsistentTaskStateError) {
if (notFound) {
const tag = (data.tag.ids as string[])
.map(id => data.tag.entities[id])
.find(tagI => (tagI as Tag).taskIds.includes(notFound));
@ -84,8 +85,10 @@ const _isEntityStatesConsistent = (data: AppDataComplete): boolean => {
const baseStateKeys: (keyof AppBaseData)[] = [
'task',
'taskArchive',
'taskRepeatCfg',
'tag',
'project',
'simpleCounter',
];
const projectStateKeys: (keyof AppDataForProjects)[] = [
'note',

View file

@ -28,7 +28,7 @@ export interface AppBaseData {
project: ProjectState;
archivedProjects: ProjectArchive;
globalConfig: GlobalConfigState;
reminders?: Reminder[];
reminders: Reminder[];
task: TaskState;
tag: TagState;
@ -40,21 +40,28 @@ export interface AppBaseData {
taskAttachment?: TaskAttachmentState;
}
export type AppBaseDataEntityLikeStates =
ProjectState
| TaskState
| TaskRepeatCfgState
| TaskArchive
| SimpleCounterState;
// NOTE: [key:string] always refers to projectId
export interface AppDataForProjects {
note?: {
note: {
[key: string]: NoteState;
};
bookmark?: {
bookmark: {
[key: string]: BookmarkState;
};
metric?: {
metric: {
[key: string]: MetricState;
};
improvement?: {
improvement: {
[key: string]: ImprovementState;
};
obstruction?: {
obstruction: {
[key: string]: ObstructionState;
};
}

View file

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { combineLatest, EMPTY, fromEvent, merge, Observable, of, ReplaySubject } from 'rxjs';
import { combineLatest, EMPTY, fromEvent, merge, Observable, of, ReplaySubject, throwError } from 'rxjs';
import {
auditTime,
auditTime, catchError,
concatMap,
debounceTime,
distinctUntilChanged,
@ -14,7 +14,7 @@ import {
switchMap,
take,
tap,
throttleTime
throttleTime, timeout
} from 'rxjs/operators';
import { GlobalConfigService } from '../../features/config/global-config.service';
import { SyncProvider } from './sync-provider';
@ -40,7 +40,10 @@ import { IPC } from '../../../../electron/ipc-events.const';
providedIn: 'root',
})
export class SyncService {
inMemory$: Observable<AppDataComplete> = this._persistenceService.inMemoryComplete$;
inMemoryComplete$: Observable<AppDataComplete> = this._persistenceService.inMemoryComplete$.pipe(
timeout(5000),
catchError(() => throwError('Error while trying to get inMemoryComplete$')),
);
private _onUpdateLocalDataTrigger$: Observable<{ appDataKey: AllowedDBKeys, data: any, isDataImport: boolean, projectId?: string }> =
this._persistenceService.onAfterSave$.pipe(

View file

@ -46,6 +46,6 @@
<footer class="version-footer">
Super Productivity <a href="https://github.com/johannesjo/super-productivity/blob/master/CHANGELOG.md"
target="_blank">{{appVersion}}</a>
<a href="https://super-productivity.com/private-policy.html"
<a href="https://super-productivity.com/private-policy"
target="_blank">{{T.PS.PRIVATE_POLICY|translate}}</a>
</footer>

View file

@ -141,6 +141,10 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT
taskIdsForProjectBacklog = (project as Project).backlogTaskIds;
taskIdsForProject = (project as Project).taskIds;
if (!taskIdsForProject || !taskIdsForProjectBacklog || (!taskIdsForProjectBacklog.length && !taskIdsForProject.length)) {
console.log('------ERR_ADDITIONAL_INFO------');
console.log('project', project);
console.log('taskIdsForProject', taskIdsForProject);
console.log('taskIdsForProjectBacklog', taskIdsForProjectBacklog);
throw new Error('Invalid project data');
}
}
@ -149,6 +153,10 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT
const tagTaskIdMap = (task.tagIds).reduce((acc, id) => {
const tag = tagState.entities[id];
if (!tag) {
console.log('------ERR_ADDITIONAL_INFO------');
console.log('id', id);
console.log('tagState', tagState);
console.log('tagTaskIdMap', tagTaskIdMap);
throw new Error('Task Restore Error: Missing tag');
}

View file

@ -16,6 +16,11 @@ export const T = {
'BL': {
'NO_TASKS': 'BL.NO_TASKS'
},
'CONFIRM': {
'AUTO_FIX': 'CONFIRM.AUTO_FIX',
'DELETE_STRAY_BACKUP': 'CONFIRM.DELETE_STRAY_BACKUP',
'RESTORE_STRAY_BACKUP': 'CONFIRM.RESTORE_STRAY_BACKUP'
},
'DATETIME_INPUT': {
'IN': 'DATETIME_INPUT.IN',
'TOMORROW': 'DATETIME_INPUT.TOMORROW'
@ -77,6 +82,13 @@ export const T = {
}
},
'DROPBOX': {
'C': {
'EMPTY_SYNC': 'F.DROPBOX.C.EMPTY_SYNC',
'FORCE_IMPORT': 'F.DROPBOX.C.FORCE_IMPORT',
'FORCE_UPLOAD': 'F.DROPBOX.C.FORCE_UPLOAD',
'FORCE_UPLOAD_AFTER_ERROR': 'F.DROPBOX.C.FORCE_UPLOAD_AFTER_ERROR',
'TRY_LOAD_REMOTE_AGAIN': 'F.DROPBOX.C.TRY_LOAD_REMOTE_AGAIN'
},
'D_CONFLICT': {
'LAST_CHANGE': 'F.DROPBOX.D_CONFLICT.LAST_CHANGE',
'LAST_SYNC': 'F.DROPBOX.D_CONFLICT.LAST_SYNC',
@ -742,6 +754,10 @@ export const T = {
'ALREADY_DID': 'F.TIME_TRACKING.B.ALREADY_DID',
'SNOOZE': 'F.TIME_TRACKING.B.SNOOZE'
},
'B_TTR': {
'ADD_TO_TASK': 'F.TIME_TRACKING.B_TTR.ADD_TO_TASK',
'MSG': 'F.TIME_TRACKING.B_TTR.MSG'
},
'D_IDLE': {
'BREAK': 'F.TIME_TRACKING.D_IDLE.BREAK',
'CREATE_AND_TRACK': 'F.TIME_TRACKING.D_IDLE.CREATE_AND_TRACK',
@ -750,6 +766,13 @@ export const T = {
'TASK': 'F.TIME_TRACKING.D_IDLE.TASK',
'TASK_BREAK': 'F.TIME_TRACKING.D_IDLE.TASK_BREAK',
'TRACK_TO': 'F.TIME_TRACKING.D_IDLE.TRACK_TO'
},
'D_TRACKING_REMINDER': {
'UNTRACKED_TIME': 'F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME',
'TRACK_TO': 'F.TIME_TRACKING.D_TRACKING_REMINDER.TRACK_TO',
'CREATE_AND_TRACK': 'F.TIME_TRACKING.D_TRACKING_REMINDER.CREATE_AND_TRACK',
'IDLE_FOR': 'F.TIME_TRACKING.D_TRACKING_REMINDER.IDLE_FOR',
'TASK': 'F.TIME_TRACKING.D_TRACKING_REMINDER.TASK'
}
},
'WORKLOG': {
@ -951,10 +974,10 @@ export const T = {
'TITLE': 'GCF.POMODORO.TITLE'
},
'SOUND': {
'TITLE': 'GCF.SOUND.TITLE',
'DONE_SOUND': 'GCF.SOUND.DONE_SOUND',
'IS_INCREASE_DONE_PITCH': 'GCF.SOUND.IS_INCREASE_DONE_PITCH',
'IS_PLAY_DONE_SOUND': 'GCF.SOUND.IS_PLAY_DONE_SOUND',
'TITLE': 'GCF.SOUND.TITLE',
'VOLUME': 'GCF.SOUND.VOLUME'
},
'TAKE_A_BREAK': {
@ -966,6 +989,12 @@ export const T = {
'MIN_WORKING_TIME': 'GCF.TAKE_A_BREAK.MIN_WORKING_TIME',
'MOTIVATIONAL_IMG': 'GCF.TAKE_A_BREAK.MOTIVATIONAL_IMG',
'TITLE': 'GCF.TAKE_A_BREAK.TITLE'
},
'TRACKING_REMINDER': {
'HELP': 'GCF.TRACKING_REMINDER.HELP',
'TITLE': 'GCF.TRACKING_REMINDER.TITLE',
'L_IS_ENABLED': 'GCF.TRACKING_REMINDER.L_IS_ENABLED',
'L_MIN_TIME': 'GCF.TRACKING_REMINDER.L_MIN_TIME'
}
},
'GLOBAL_SNACK': {

View file

@ -1,7 +1,7 @@
import { loadFromRealLs, saveToRealLs } from '../core/persistence/local-storage';
import { LS_ACTION_BEFORE_LAST_ERROR_LOG, LS_ACTION_LOG } from '../core/persistence/ls-keys.const';
const NUMBER_OF_ACTIONS_TO_SAVE = 25;
const NUMBER_OF_ACTIONS_TO_SAVE = 30;
const getActionLog = (): string[] => {
const current = loadFromRealLs(LS_ACTION_LOG);

View file

@ -0,0 +1,39 @@
import { AppDataComplete } from '../imex/sync/sync.model';
import { MODEL_VERSION_KEY } from '../app.constants';
import { DEFAULT_GLOBAL_CONFIG } from '../features/config/default-global-config.const';
import { createEmptyEntity } from './create-empty-entity';
export const createAppDataCompleteMock = (): AppDataComplete => ({
project: {
...createEmptyEntity(),
[MODEL_VERSION_KEY]: 5
},
archivedProjects: {},
globalConfig: DEFAULT_GLOBAL_CONFIG,
task: {
...createEmptyEntity(),
ids: [],
currentTaskId: null,
selectedTaskId: null,
taskAdditionalInfoTargetPanel: null,
lastCurrentTaskId: null,
isDataLoaded: false,
},
tag: createEmptyEntity(),
simpleCounter: {
...createEmptyEntity(),
ids: []
},
taskArchive: createEmptyEntity(),
taskRepeatCfg: createEmptyEntity(),
lastLocalSyncModelChange: 0,
// OPTIONAL though they are really not
reminders: [],
note: {},
bookmark: {},
metric: {},
improvement: {},
obstruction: {},
});

View file

@ -0,0 +1,6 @@
export const arrayEquals = (arr1: string[], arr2: string[]): boolean => {
for (let i = 0; i < arr1.length; ++i) {
if (arr1[i] !== arr2[i]) return false;
}
return true;
};

View file

@ -1,4 +1,5 @@
import { devError } from './dev-error';
import { arrayEquals } from './array-equals';
export const checkFixEntityStateConsistency = (data: any, additionalStr = ''): any => {
if (!isEntityStateConsistent(data, additionalStr)) {
@ -23,7 +24,8 @@ export const isEntityStateConsistent = (data: any, additionalStr = ''): boolean
if (!data
|| !data.entities
|| !data.ids
|| Object.keys(data.entities).length !== data.ids.length) {
|| Object.keys(data.entities).length !== data.ids.length
|| !arrayEquals(Object.keys(data.entities).sort(), [...data.ids].sort())) {
console.log(data);
devError(`Inconsistent entity state "${additionalStr}"`);
return false;

View file

@ -0,0 +1,13 @@
import { lazySetInterval } from '../../../electron/lazy-set-interval';
import { Observable } from 'rxjs';
export const realTimer$ = (intervalDuration: number): Observable<number> => {
return new Observable(subscriber => {
const idleStart = Date.now();
// subscriber.next(0);
lazySetInterval(() => {
const delta = Date.now() - idleStart;
subscriber.next(delta);
}, intervalDuration);
});
};

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "أنت تحاول مزامنة كائن بيانات فارغ. هل هذه أول مزامنة لك مع تطبيق جديد (تقريبًا)؟",
"FORCE_IMPORT": "هل تريد استيراد البيانات عن بُعد على أي حال؟",
"FORCE_UPLOAD": "تحميل البيانات المحلية على أي حال؟",
"FORCE_UPLOAD_AFTER_ERROR": "حدث خطأ أثناء تحميل البيانات المحلية الخاصة بك. حاول فرض التحديث؟",
"TRY_LOAD_REMOTE_AGAIN": "حاول إعادة تحميل البيانات من جهاز التحكم عن بعد مرة أخرى؟"
},
"D_CONFLICT": {
"LAST_CHANGE": "اخر تغير:",
"LAST_SYNC": "المزامنة الأخيرة:",
@ -607,8 +614,12 @@
"TIME": "زمن"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "إضافة مهمة موجودة \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "إضافة العدد رقم{{issueNr}} من {{issueType}}",
"ADD_TASK": "إضافة مهمة",
"ADD_TASK_TO_BACKLOG": "إضافة مهمة إلى تراكم",
"CREATE_TASK": "إنشاء مهمة جديدة",
"EXAMPLE": "مثال: \"بعض عناوين المهمة + اسم المشروع # علامة بعض # علامة أخرى بعض 10 م / 3 س\"",
"START": "اضغط على إدخال مرة أخرى للبدء"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "مدة الاستراحات الطويلة",
"TITLE": "إعدادات بومودورو"
},
"SOUND": {
"DONE_SOUND": "المهمة تمت الصوت",
"IS_INCREASE_DONE_PITCH": "زيادة الملعب لكل مهمة يتم القيام بها",
"IS_PLAY_DONE_SOUND": "تشغيل الصوت عندما يتم وضع علامة على المهمة \"تم\"",
"TITLE": "صوت",
"VOLUME": "الصوت"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>يتيح لك تكوين تذكير متكرر عندما تعمل لفترة زمنية محددة دون أخذ استراحة.</p> <p>يمكنك تعديل الرسالة المعروضة. سيتم استبدال ${duration} بالوقت الذي يستغرقه دون انقطاع.</p> </div>",
"IS_ENABLED": "تمكين التذكير باخذ استراحة",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "نسخ إلى الحافظة",
"ERR_COMPRESSION": "خطأ لواجهة الضغط",
"PERSISTENCE_DISALLOWED": "لن يتم استمرار البيانات بشكل دائم. أن تدرك أن هذا يمكن أن يؤدي إلى فقدان البيانات!",
"RUNNING_X": "تشغيل \"{{str}}\"."
"RUNNING_X": "تشغيل \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": " تم الضغط على{{keyCombo}} ، ولكن لا يتوفر اختصار الإشارات المرجعية المفتوح إلا في سياق المشروع.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": " تم الضغط على{{keyCombo}} ، ولكن فتح اختصار الملاحظات متاح فقط في سياق المشروع."
},
"GPB": {
"ASSETS": "جارٍ تحميل الأصول ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Sie versuchen, ein leeres Datenobjekt zu synchronisieren. Ist dies Ihre allererste Synchronisierung einer (fast) jungfräulichen App?",
"FORCE_IMPORT": "Remote-Daten trotzdem importieren?",
"FORCE_UPLOAD": "Lokale Daten trotzdem hochladen?",
"FORCE_UPLOAD_AFTER_ERROR": "Beim Hochladen Ihrer lokalen Daten ist ein Fehler aufgetreten. Versuchen Sie das Update zu erzwingen?",
"TRY_LOAD_REMOTE_AGAIN": "Versuchen Sie erneut, Daten von der Fernbedienung neu zu laden?"
},
"D_CONFLICT": {
"LAST_CHANGE": "Letzte Änderung:",
"LAST_SYNC": "letzte Synchronisierung:",
@ -607,8 +614,12 @@
"TIME": "Zeit"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Vorhandene Aufgabe hinzufügen \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "Fügen Sie das Problem #{{issueNr}} von {{issueType}}hinzu",
"ADD_TASK": "Aufgabe hinzufügen",
"ADD_TASK_TO_BACKLOG": "Aufgabe zum Rückstand hinzufügen",
"CREATE_TASK": "Neue Aufgabe erstellen",
"EXAMPLE": "Beispiel: \"Einige Aufgabentitel + Projektname #Einiges Tag #Einiges anderes Tag 10m / 3h\"",
"START": "Drücken Sie die Eingabetaste noch einmal, um zu beginnen"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Dauer längerer Pausen",
"TITLE": "Pomodoro TImer"
},
"SOUND": {
"DONE_SOUND": "Aufgabe erledigt Ton",
"IS_INCREASE_DONE_PITCH": "Erhöhen Sie die Tonhöhe für jede erledigte Aufgabe",
"IS_PLAY_DONE_SOUND": "Ton abspielen, wenn die Aufgabe als erledigt markiert ist",
"TITLE": "Klingen",
"VOLUME": "Volumen"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Ermöglicht die Konfiguration einer wiederkehrenden Erinnerung, wenn Sie eine bestimmte Zeit ohne Pause gearbeitet haben.</p> <p>Sie können die angezeigte Nachricht ändern. ${duration} wird durch die ohne Unterbrechung verbrachte Zeit ersetzt.</p> </div>",
"IS_ENABLED": "Aktivieren Sie die Erinnerungsfunktion für eine Pause",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "In die Zwischenablage kopiert",
"ERR_COMPRESSION": "Fehler bei der Komprimierung",
"PERSISTENCE_DISALLOWED": "Daten werden nicht dauerhaft gespeichert. Beachten Sie, dass dies zu Datenverlust führen kann !!",
"RUNNING_X": "Wird gestartet: \"{{str}}\"."
"RUNNING_X": "Wird gestartet: \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} gedrückt, aber die Verknüpfung zum Öffnen von Lesezeichen ist nur im Projektkontext verfügbar.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} gedrückt, aber die Verknüpfung für offene Notizen ist nur im Projektkontext verfügbar."
},
"GPB": {
"ASSETS": "Laden von Assets ...",

View file

@ -16,6 +16,11 @@
"BL": {
"NO_TASKS": "There are currently no tasks in your backlog"
},
"CONFIRM": {
"AUTO_FIX": "Your data seems to be damaged. Do you want to try to automatically fix it? This might result in partial data loss.",
"DELETE_STRAY_BACKUP": "Do you want to delete the back to avoid seeing this dialog?",
"RESTORE_STRAY_BACKUP": "During last sync there might have been some error. Do you want to restore the last backup?"
},
"DATETIME_INPUT": {
"IN": "in {{time}}",
"TOMORROW": "tomorrow {{time}}"
@ -77,6 +82,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "You're trying to sync an empty data object. Is this your very first sync of a (almost) virgin app?",
"FORCE_IMPORT": "Import remote data anyway?",
"FORCE_UPLOAD": "Upload local data anyway?",
"FORCE_UPLOAD_AFTER_ERROR": "An Error occurred while uploading your local data. Try to force the update?",
"TRY_LOAD_REMOTE_AGAIN": "Try to re-load data from remote once again?"
},
"D_CONFLICT": {
"LAST_CHANGE": "last change:",
"LAST_SYNC": "last sync:",
@ -742,14 +754,25 @@
"ALREADY_DID": "I already did",
"SNOOZE": "Snooze {{time}}"
},
"B_TTR": {
"ADD_TO_TASK": "Add to Task",
"MSG": "You have not been tracking time for {{time}}"
},
"D_IDLE": {
"BREAK": "Break",
"CREATE_AND_TRACK": "<em>Create</em> and track to:",
"CREATE_AND_TRACK": "<em>Create</em> and track to",
"IDLE_FOR": "You have been idle for:",
"SKIP": "Skip",
"TASK": "Task",
"TASK_BREAK": "Task+Break",
"TRACK_TO": "Track to:"
"TRACK_TO": "Track to"
},
"D_TRACKING_REMINDER": {
"UNTRACKED_TIME": "Untracked time:",
"TRACK_TO": "Track to:",
"CREATE_AND_TRACK": "<em>Create</em> and track to",
"IDLE_FOR": "You have been idle for:",
"TASK": "Task"
}
},
"WORKLOG": {
@ -951,10 +974,10 @@
"TITLE": "Pomodoro Timer"
},
"SOUND": {
"TITLE": "Sound",
"DONE_SOUND": "Task done sound",
"IS_INCREASE_DONE_PITCH": "Increase pitch for every task done",
"IS_PLAY_DONE_SOUND": "Play sound when task is marked done",
"TITLE": "Sound",
"VOLUME": "Volume"
},
"TAKE_A_BREAK": {
@ -966,6 +989,12 @@
"MIN_WORKING_TIME": "Trigger take a break notification after X working without one",
"MOTIVATIONAL_IMG": "Motivational image (web url)",
"TITLE": "Break Reminder"
},
"TRACKING_REMINDER": {
"HELP": "Configure a banner to show up in case you forgot to start time tracking.",
"TITLE": "Time Tracking Reminder",
"L_IS_ENABLED": "Enabled",
"L_MIN_TIME": "Time to wait before showing Banner"
}
},
"GLOBAL_SNACK": {

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Estás intentando sincronizar un objeto de datos vacío. ¿Es esta tu primera sincronización de una aplicación (casi) virgen?",
"FORCE_IMPORT": "¿Importar datos remotos de todos modos?",
"FORCE_UPLOAD": "¿Subir datos locales de todos modos?",
"FORCE_UPLOAD_AFTER_ERROR": "Se produjo un error al cargar sus datos locales. ¿Intentas forzar la actualización?",
"TRY_LOAD_REMOTE_AGAIN": "¿Intentar volver a cargar datos desde el control remoto?"
},
"D_CONFLICT": {
"LAST_CHANGE": "ultimo cambio:",
"LAST_SYNC": "Última sincronización:",
@ -607,8 +614,12 @@
"TIME": "hora"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Agregar tarea existente \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "Agregar problema n. °{{issueNr}} de {{issueType}}",
"ADD_TASK": "Agregar tarea",
"ADD_TASK_TO_BACKLOG": "Agregar tarea al registro de trabajo",
"CREATE_TASK": "Crear nueva tarea",
"EXAMPLE": "Ejemplo: \"Un título de tarea + nombre de proyecto # alguna etiqueta # alguna otra etiqueta 10m / 3h\"",
"START": "Presiona entrar una vez más para comenzar"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Duración de los descansos más largos",
"TITLE": "Pomodoro Timer"
},
"SOUND": {
"DONE_SOUND": "Sonido de tarea realizada",
"IS_INCREASE_DONE_PITCH": "Aumente el tono para cada tarea realizada",
"IS_PLAY_DONE_SOUND": "Reproducir sonido cuando la tarea esté marcada como terminada",
"TITLE": "Sonar",
"VOLUME": "Volumen"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Le permite configurar un recordatorio recurrente cuando ha trabajado durante un período de tiempo específico sin tomar un descanso.</p> <p>Puede modificar el mensaje que se muestra. ${duration} será reemplazado con el tiempo dedicado sin descanso.</p> </div>",
"IS_ENABLED": "Habilitar recordatorio para tomar un descanso",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Copiado al portapapeles",
"ERR_COMPRESSION": "Error para la interfaz de compresión",
"PERSISTENCE_DISALLOWED": "Los datos no se conservarán de forma permanente. ¡Tenga en cuenta que esto puede conducir a la pérdida de datos!",
"RUNNING_X": "Ejecutando \"{{str}}\"."
"RUNNING_X": "Ejecutando \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} presionado, pero el acceso directo a marcadores abiertos solo está disponible en el contexto del proyecto.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} presionado, pero el acceso directo de notas abiertas solo está disponible en el contexto del proyecto."
},
"GPB": {
"ASSETS": "Cargando activos ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "شما در حال تلاش برای همگام سازی یک شی empty داده خالی هستید. آیا این اولین همگام سازی شما با یک برنامه (تقریبا) بکر است؟",
"FORCE_IMPORT": "به هر حال داده از راه دور وارد می شود؟",
"FORCE_UPLOAD": "به هر حال داده های محلی بارگذاری می شوند؟",
"FORCE_UPLOAD_AFTER_ERROR": "هنگام بارگذاری داده های محلی شما خطایی روی داد. سعی کنید به زودی بروزرسانی کنید؟",
"TRY_LOAD_REMOTE_AGAIN": "سعی می کنید بار دیگر داده ها را از راه دور بارگیری کنید؟"
},
"D_CONFLICT": {
"LAST_CHANGE": "آخرین تغییر:",
"LAST_SYNC": "آخرین همگام سازی:",
@ -607,8 +614,12 @@
"TIME": "زمان"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "افزودن کار موجود \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "شماره شماره{{issueNr}} را از {{issueType}}اضافه کنید",
"ADD_TASK": "اضافه کردن کار",
"ADD_TASK_TO_BACKLOG": "اضافه کردن کار به بک لاگ",
"CREATE_TASK": "ایجاد کار جدید",
"EXAMPLE": "مثال: \"برخی از عنوان های کار + پروژه نام # برخی از برچسب ها # برخی دیگر از برچسب ها 10m / 3h\"",
"START": "جهت شروع یکبار دیگر دکمه اینتر را بزنید"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Duration of longer breaks",
"TITLE": "Pomodoro Timer"
},
"SOUND": {
"DONE_SOUND": "وظیفه تمام شد صدا",
"IS_INCREASE_DONE_PITCH": "برای انجام هر کاری سطح صدا را افزایش دهید",
"IS_PLAY_DONE_SOUND": "وقتی کار به پایان رسید ، صدا را پخش کنید",
"TITLE": "صدا",
"VOLUME": "جلد"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Allows you to configure a reoccurring reminder when you have worked for a specified amount of time without taking a break.</p> <p>You can modify the message displayed. ${duration} will be replaced with the time spent without a break.</p> </div>",
"IS_ENABLED": "Enable take a break reminder",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "در کلیپ برد کپی شد",
"ERR_COMPRESSION": "خطایی برای رابط فشرده سازی",
"PERSISTENCE_DISALLOWED": "Data will be not persisted permanently. Be aware that this can lead to data loss!!",
"RUNNING_X": "Running \"{{str}}\"."
"RUNNING_X": "Running \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} فشرده شده است ، اما میانبر نشانک های باز فقط در متن پروژه در دسترس است.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} میانبر فشرده شده است ، اما باز کردن یادداشت های باز فقط وقتی در متن پروژه باشد ، در دسترس است."
},
"GPB": {
"ASSETS": "در حال بارگیری دارایی ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Vous essayez de synchroniser un objet de données vide. Est-ce votre toute première synchronisation d'une application (presque) vierge?",
"FORCE_IMPORT": "Importer quand même des données distantes?",
"FORCE_UPLOAD": "Télécharger quand même des données locales?",
"FORCE_UPLOAD_AFTER_ERROR": "Une erreur s'est produite lors du téléchargement de vos données locales. Essayez de forcer la mise à jour?",
"TRY_LOAD_REMOTE_AGAIN": "Essayez à nouveau de recharger les données depuis la télécommande?"
},
"D_CONFLICT": {
"LAST_CHANGE": "dernier changement:",
"LAST_SYNC": "Dernière synchronisation:",
@ -607,8 +614,12 @@
"TIME": "Temps"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Ajouter la tâche existante \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "Ajouter le problème n °{{issueNr}} de {{issueType}}",
"ADD_TASK": "Ajouter une tâche",
"ADD_TASK_TO_BACKLOG": "Ajouter une tâche au backlog",
"CREATE_TASK": "Créer une nouvelle tâche",
"EXAMPLE": "Exemple: \"Un titre de tâche + projectName # une balise # une autre balise 10m / 3h\"",
"START": "Appuyez sur Entrée une fois de plus pour démarrer"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Durée des pauses plus longues",
"TITLE": "Pomodoro Timer"
},
"SOUND": {
"DONE_SOUND": "Tâche terminée son",
"IS_INCREASE_DONE_PITCH": "Augmentez la hauteur pour chaque tâche effectuée",
"IS_PLAY_DONE_SOUND": "Jouer le son lorsque la tâche est marquée comme terminée",
"TITLE": "Du son",
"VOLUME": "Le volume"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>vous permet de configurer un rappel récurrent lorsque vous avez travaillé pendant une durée déterminée sans prendre de pause.</p> <p>Vous pouvez modifier le message affiché. ${duration} sera remplacé par le temps passé sans pause.</p> </div>",
"IS_ENABLED": "Activer le rappel de pause",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Copié dans le presse-papier",
"ERR_COMPRESSION": "Erreur pour l'interface de compression",
"PERSISTENCE_DISALLOWED": "Les données ne seront pas conservées de manière permanente. Sachez que cela peut entraîner une perte de données!",
"RUNNING_X": "\"{{str}}\" en cours."
"RUNNING_X": "\"{{str}}\" en cours.",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} enfoncé, mais le raccourci vers les favoris ouverts n'est disponible que dans le contexte du projet.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} enfoncé, mais le raccourci Notes ouvertes n'est disponible que dans le contexte du projet."
},
"GPB": {
"ASSETS": "Chargement des éléments ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Stai tentando di sincronizzare un oggetto dati vuoto. Questa è la tua prima sincronizzazione di un'app (quasi) vergine?",
"FORCE_IMPORT": "Importare comunque i dati remoti?",
"FORCE_UPLOAD": "Caricare comunque i dati locali?",
"FORCE_UPLOAD_AFTER_ERROR": "Si è verificato un errore durante il caricamento dei dati locali. Prova a forzare l'aggiornamento?",
"TRY_LOAD_REMOTE_AGAIN": "Vuoi provare a ricaricare i dati da remoto ancora una volta?"
},
"D_CONFLICT": {
"LAST_CHANGE": "Ultima modifica:",
"LAST_SYNC": "ultima sincronizzazione:",
@ -607,8 +614,12 @@
"TIME": "Tempo"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Aggiungi attività esistente \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "Aggiungi problema n.{{issueNr}} da {{issueType}}",
"ADD_TASK": "Aggiungi task",
"ADD_TASK_TO_BACKLOG": "Aggiungi task al backlog",
"CREATE_TASK": "Crea nuova attività",
"EXAMPLE": "Esempio: \"Qualche titolo di attività + nomeProgetto #some tag #some altro tag 10m / 3h\"",
"START": "Premi ancora invio per iniziare"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Durata delle pause più lunghe",
"TITLE": "Impostazioni del pomodoro"
},
"SOUND": {
"DONE_SOUND": "Compito fatto suono",
"IS_INCREASE_DONE_PITCH": "Aumenta il tono per ogni attività svolta",
"IS_PLAY_DONE_SOUND": "Riproduci un suono quando l'attività è contrassegnata come completata",
"TITLE": "Suono",
"VOLUME": "Volume"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Ti permette di configurare un promemoria ricorrente quando hai lavorato per uno specifica quantità di tempo senza prenderti una pausa.</p> <p>Puoi modificare il messaggio mostrato. ${duration} sarà rimpiazzata con il tempo speso senza una pausa.</p> </div>",
"IS_ENABLED": "Abilita il promemoria di fare una pausa",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Copiato negli appunti",
"ERR_COMPRESSION": "Errore per l'interfaccia di compressione",
"PERSISTENCE_DISALLOWED": "I dati non saranno salvati permanentemente. Fai attenzione che questo può portare a perdita dei dati!!",
"RUNNING_X": "Eseguendo \"{{str}}\"."
"RUNNING_X": "Eseguendo \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} premuto, ma la scorciatoia Apri segnalibri è disponibile solo nel contesto del progetto.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} premuto, ma la scorciatoia per le note aperte è disponibile solo nel contesto del progetto."
},
"GPB": {
"ASSETS": "Caricamento risorse in corso ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "空のデータオブジェクトを同期しようとしています。これは(ほぼ)未使用のアプリの最初の同期ですか?",
"FORCE_IMPORT": "とにかくリモートデータをインポートしますか?",
"FORCE_UPLOAD": "とにかくローカルデータをアップロードしますか?",
"FORCE_UPLOAD_AFTER_ERROR": "ローカルデータのアップロード中にエラーが発生しました。強制的に更新しますか?",
"TRY_LOAD_REMOTE_AGAIN": "リモートからもう一度データを再ロードしてみますか?"
},
"D_CONFLICT": {
"LAST_CHANGE": "最終変更:",
"LAST_SYNC": "最終同期:",
@ -607,8 +614,12 @@
"TIME": "時間"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "既存のタスク「{{taskTitle}}」を追加",
"ADD_ISSUE_TASK": " {{issueType}}から問題番号{{issueNr}} を追加",
"ADD_TASK": "タスクを追加",
"ADD_TASK_TO_BACKLOG": "バックログにタスクを追加",
"CREATE_TASK": "新しいタスクを作成する",
"EXAMPLE": "例「Some task title + projectName #some tag #some other tag 10m / 3h」",
"START": "もう一度Enterキーを押して開始します"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "長い休憩時間",
"TITLE": "ポモドーロタイマー"
},
"SOUND": {
"DONE_SOUND": "タスク完了音",
"IS_INCREASE_DONE_PITCH": "実行するすべてのタスクのピッチを上げる",
"IS_PLAY_DONE_SOUND": "タスクが完了とマークされたときに音を鳴らす",
"TITLE": "音",
"VOLUME": "ボリューム"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>休憩せずに指定した時間作業したときに繰り返し発生するアラームを設定できます。</p> <p>表示されたメッセージを変更できます。 ${duration} は中断せずに費やした時間に置き換えられます。</p> </div>",
"IS_ENABLED": "休憩のお知らせを有効にする",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "クリップボードにコピー",
"ERR_COMPRESSION": "圧縮インターフェースのエラー",
"PERSISTENCE_DISALLOWED": "データは永続的に保持されません。これはデータの損失につながる可能性があることに注意してください!!",
"RUNNING_X": "\"{{str}}\"を実行しています。"
"RUNNING_X": "\"{{str}}\"を実行しています。",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} が押されましたが、ブックマークを開くショートカットはプロジェクトコンテキストでのみ使用できます。",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} が押されましたが、メモを開くショートカットはプロジェクトコンテキストでのみ使用できます。"
},
"GPB": {
"ASSETS": "アセットを読み込んでいます...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "빈 데이터 개체를 동기화하려고합니다. 이것이 (거의) 처녀 앱의 첫 번째 동기화입니까?",
"FORCE_IMPORT": "그래도 원격 데이터를 가져 오시겠습니까?",
"FORCE_UPLOAD": "그래도 로컬 데이터를 업로드 하시겠습니까?",
"FORCE_UPLOAD_AFTER_ERROR": "로컬 데이터를 업로드하는 동안 오류가 발생했습니다. 강제로 업데이트 하시겠습니까?",
"TRY_LOAD_REMOTE_AGAIN": "원격에서 데이터를 다시로드 하시겠습니까?"
},
"D_CONFLICT": {
"LAST_CHANGE": "마지막 변경:",
"LAST_SYNC": "마지막 동기화 :",
@ -607,8 +614,12 @@
"TIME": "시각"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "기존 작업 \"{{taskTitle}}\"추가",
"ADD_ISSUE_TASK": " {{issueType}}의{{issueNr}} 호 추가",
"ADD_TASK": "작업 추가",
"ADD_TASK_TO_BACKLOG": "백 로그에 작업 추가",
"CREATE_TASK": "새 할 일 만들기",
"EXAMPLE": "예 : \"일부 작업 제목 + projectName #some tag #some other tag 10m / 3h\"",
"START": "시작하려면 한 번 더 입력하십시오."
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "긴 휴식 시간",
"TITLE": "포모 도로 타이머"
},
"SOUND": {
"DONE_SOUND": "작업 완료 소리",
"IS_INCREASE_DONE_PITCH": "모든 작업에 대한 피치 높이기",
"IS_PLAY_DONE_SOUND": "작업이 완료로 표시되면 소리 재생",
"TITLE": "소리",
"VOLUME": "음량"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>중단하지 않고 지정된 시간 동안 작업 한 경우 다시 알림을 구성 할 수 있습니다.</p> <p>표시된 메시지를 수정할 수 있습니다. ${duration} 은 휴식 시간없이 바뀝니다.</p> </div>",
"IS_ENABLED": "휴식 알림을 사용하도록 설정",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "클립 보드에 복사 됨",
"ERR_COMPRESSION": "압축 인터페이스 오류",
"PERSISTENCE_DISALLOWED": "데이터는 영구적으로 유지되지 않습니다. 이로 인해 데이터가 손실 될 수 있습니다 !!",
"RUNNING_X": "\"{{str}}\"실행 중."
"RUNNING_X": "\"{{str}}\"실행 중.",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} 을 눌렀지만 북마크 열기 바로 가기는 프로젝트 컨텍스트에서만 사용할 수 있습니다.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} 을 눌렀지만 노트 열기 단축키는 프로젝트 컨텍스트에서만 사용할 수 있습니다."
},
"GPB": {
"ASSETS": "자산로드 중 ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "U probeert een leeg gegevensobject te synchroniseren. Is dit uw allereerste synchronisatie van een (bijna) nieuwe app?",
"FORCE_IMPORT": "Toch externe gegevens importeren?",
"FORCE_UPLOAD": "Lokale gegevens toch uploaden?",
"FORCE_UPLOAD_AFTER_ERROR": "Er is een fout opgetreden tijdens het uploaden van uw lokale gegevens. Probeer de update te forceren?",
"TRY_LOAD_REMOTE_AGAIN": "Wilt u nogmaals gegevens van de afstandsbediening opnieuw laden?"
},
"D_CONFLICT": {
"LAST_CHANGE": "laatste wijziging:",
"LAST_SYNC": "laatste gesynchroniseerd:",
@ -607,8 +614,12 @@
"TIME": "Tijd"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Voeg bestaande taak \"{{taskTitle}}\" toe",
"ADD_ISSUE_TASK": "Voeg probleem #{{issueNr}} van {{issueType}}toe",
"ADD_TASK": "Voeg taak toe",
"ADD_TASK_TO_BACKLOG": "Taak toevoegen aan achterstand",
"CREATE_TASK": "Maak een nieuwe taak",
"EXAMPLE": "Voorbeeld: \"Een taaktitel + projectnaam # een tag # een andere tag 10 m / 3 uur\"",
"START": "Druk nogmaals op Enter om te beginnen"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Duur van langere pauzes",
"TITLE": "Pomodoro Timer"
},
"SOUND": {
"DONE_SOUND": "Taak gedaan geluid",
"IS_INCREASE_DONE_PITCH": "Verhoog de toon voor elke uitgevoerde taak",
"IS_PLAY_DONE_SOUND": "Speel geluid af wanneer de taak is gemarkeerd als voltooid",
"TITLE": "Geluid",
"VOLUME": "Volume"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Hiermee kunt u een terugkerende herinnering configureren wanneer u een bepaalde tijd hebt gewerkt zonder een pauze te nemen.</p> <p>U kunt het weergegeven bericht wijzigen. $ {duration} wordt vervangen door de tijd die u zonder pauze doorbrengt.</p> </div>",
"IS_ENABLED": "Schakel een pauzeherinnering in",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Gekopieerd naar het klembord",
"ERR_COMPRESSION": "Fout voor compressie-interface",
"PERSISTENCE_DISALLOWED": "Gegevens worden niet permanent bewaard. Houd er rekening mee dat dit kan leiden tot gegevensverlies !!",
"RUNNING_X": "\"{{Str}}\" wordt uitgevoerd."
"RUNNING_X": "\"{{Str}}\" wordt uitgevoerd.",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} ingedrukt, maar de snelkoppeling naar bladwijzers openen is alleen beschikbaar in projectcontext.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} ingedrukt, maar de snelkoppeling voor open notities is alleen beschikbaar in projectcontext."
},
"GPB": {
"ASSETS": "Activa laden ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Você está tentando sincronizar um objeto de dados vazio. Esta é a sua primeira sincronização de um aplicativo (quase) virgem?",
"FORCE_IMPORT": "Importar dados remotos mesmo assim?",
"FORCE_UPLOAD": "Enviar dados locais mesmo assim?",
"FORCE_UPLOAD_AFTER_ERROR": "Ocorreu um erro ao enviar seus dados locais. Tentar forçar a atualização?",
"TRY_LOAD_REMOTE_AGAIN": "Tentar recarregar os dados remotamente mais uma vez?"
},
"D_CONFLICT": {
"LAST_CHANGE": "última mudança:",
"LAST_SYNC": "última sincronização:",
@ -607,8 +614,12 @@
"TIME": "tempo"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Adicionar tarefa existente \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "Adicione a edição nº{{issueNr}} de {{issueType}}",
"ADD_TASK": "Adicionar tarefa",
"ADD_TASK_TO_BACKLOG": "Adicionar tarefa ao backlog",
"CREATE_TASK": "Criar nova tarefa",
"EXAMPLE": "Exemplo: \"Algum título da tarefa + nome do projeto # alguma tag # alguma outra tag 10m / 3h\"",
"START": "Pressione enter mais uma vez para iniciar"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Duração de pausas mais longas",
"TITLE": "Configurações do Pomodoro"
},
"SOUND": {
"DONE_SOUND": "Som de tarefa concluída",
"IS_INCREASE_DONE_PITCH": "Aumente o tom para cada tarefa realizada",
"IS_PLAY_DONE_SOUND": "Tocar som quando a tarefa for marcada como concluída",
"TITLE": "Som",
"VOLUME": "Volume"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Permite configurar um lembrete recorrente quando você trabalha por um período de tempo especificado sem fazer uma pausa.</p> <p>Você pode modificar a mensagem exibida. ${duration} será substituído pelo tempo gasto sem interrupção.</p> </div>",
"IS_ENABLED": "Ativar lembrete de pausa",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Copiado para a área de transferência",
"ERR_COMPRESSION": "Erro para interface de compactação",
"PERSISTENCE_DISALLOWED": "Os dados não serão mantidos permanentemente. Esteja ciente de que isso pode levar à perda de dados!",
"RUNNING_X": "Rodando \"{{str}}\"."
"RUNNING_X": "Rodando \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} pressionado, mas o atalho para abrir favoritos só está disponível no contexto do projeto.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} pressionado, mas o atalho para abrir notas só está disponível no contexto do projeto."
},
"GPB": {
"ASSETS": "Carregando recursos...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Вы пытаетесь синхронизировать пустой объект данных. Это ваша самая первая синхронизация (почти) девственного приложения?",
"FORCE_IMPORT": "Все равно импортировать удаленные данные?",
"FORCE_UPLOAD": "Все равно загрузить локальные данные?",
"FORCE_UPLOAD_AFTER_ERROR": "Произошла ошибка при загрузке ваших локальных данных. Пробовать принудительно обновить?",
"TRY_LOAD_REMOTE_AGAIN": "Попробовать еще раз перезагрузить данные с пульта?"
},
"D_CONFLICT": {
"LAST_CHANGE": "Последнее изменение:",
"LAST_SYNC": "последняя синхронизация:",
@ -607,8 +614,12 @@
"TIME": "время"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Добавить существующую задачу \"{{taskTitle}}\"",
"ADD_ISSUE_TASK": "Добавить выпуск №{{issueNr}} из {{issueType}}",
"ADD_TASK": "Добавить задачу",
"ADD_TASK_TO_BACKLOG": "Добавить задачу в отставание",
"CREATE_TASK": "Создать новую задачу",
"EXAMPLE": "Пример: \"Название задачи + название проекта # какой-то тег # какой-то другой тег 10 м / 3 ч\"",
"START": "Нажмите ввод еще раз, чтобы начать"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Продолжительность более длительных перерывов",
"TITLE": "Помодоро Таймер"
},
"SOUND": {
"DONE_SOUND": "Звук выполненной задачи",
"IS_INCREASE_DONE_PITCH": "Увеличивайте высоту звука для каждой выполненной задачи",
"IS_PLAY_DONE_SOUND": "Воспроизвести звук, когда задача отмечена как выполненная",
"TITLE": "звук",
"VOLUME": "объем"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Позволяет настроить повторяющееся напоминание, если вы работали в течение определенного времени без перерыва.</p> <p>Вы можете изменить отображаемое сообщение. ${duration} будет заменено временем, проведенным без перерыва.</p> </div>",
"IS_ENABLED": "Включить напоминание о перерыве",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Скопировано в буфер обмена",
"ERR_COMPRESSION": "Ошибка для интерфейса сжатия",
"PERSISTENCE_DISALLOWED": "Данные не будут сохранены постоянно. Имейте в виду, что это может привести к потере данных!",
"RUNNING_X": "Запуск \"{{str}}\"."
"RUNNING_X": "Запуск \"{{str}}\".",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} нажата, но ярлык для открытия закладок доступен только в контексте проекта.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} нажата, но ярлык для открытия заметок доступен только в контексте проекта."
},
"GPB": {
"ASSETS": "Загрузка активов ...",

View file

@ -77,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "Boş bir veri nesnesini senkronize etmeye çalışıyorsunuz. Bu, (neredeyse) bakir bir uygulamanın ilk senkronizasyonunuz mu?",
"FORCE_IMPORT": "Yine de uzak veriler içe aktarılsın mı?",
"FORCE_UPLOAD": "Yine de yerel veriler yüklensin mi?",
"FORCE_UPLOAD_AFTER_ERROR": "Yerel verilerinizi yüklerken bir hata oluştu. Güncellemeyi zorlamaya mı çalışıyorsunuz?",
"TRY_LOAD_REMOTE_AGAIN": "Verileri bir kez daha uzaktan kumandadan yeniden yüklemeyi mi deneyin?"
},
"D_CONFLICT": {
"LAST_CHANGE": "son değişiklik:",
"LAST_SYNC": "son senkronizasyon:",
@ -607,8 +614,12 @@
"TIME": "zaman"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "Mevcut \"{{taskTitle}}\" görevini ekle",
"ADD_ISSUE_TASK": " {{issueType}}kaynaklı{{issueNr}} numaralı sorunu ekleyin",
"ADD_TASK": "Görev ekle",
"ADD_TASK_TO_BACKLOG": "İş gününe görev ekle",
"CREATE_TASK": "Yeni görev oluştur",
"EXAMPLE": "Örnek: \"Bazı görev başlığı + projeAdı # bazı etiket # bazı diğer etiket 10a / 3sa\"",
"START": "Başlamak için bir kez daha enter tuşuna basın"
},
"B": {
@ -946,6 +957,13 @@
"LONGER_BREAK_DURATION": "Uzun molaların süresi",
"TITLE": "Pomodoro Zamanlayıcı"
},
"SOUND": {
"DONE_SOUND": "Görev tamamlandı sesi",
"IS_INCREASE_DONE_PITCH": "Yapılan her görev için perdeyi artırın",
"IS_PLAY_DONE_SOUND": "Görev tamamlandı olarak işaretlendiğinde ses çal",
"TITLE": "Ses",
"VOLUME": "hacim"
},
"TAKE_A_BREAK": {
"HELP": "<div> <p>Mola vermeden belirli bir süre çalıştığınızda tekrar eden bir hatırlatıcı yapılandırmanıza izin verir.</p> <p>Görüntülenen mesajı değiştirebilirsiniz. ${duration} , ara vermeden harcadığınız zamanla değiştirilecektir.</p> </div>",
"IS_ENABLED": "Ara ver hatırlatma yapmayı etkinleştir",
@ -961,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "Panoya kopyalandı",
"ERR_COMPRESSION": "Sıkıştırma arayüzü için hata",
"PERSISTENCE_DISALLOWED": "Veri kalıcı olarak sürdürülmeyecek. Bunun veri kaybına neden olabileceğini unutmayın!",
"RUNNING_X": "\"{{str}}\" çalıştırılıyor."
"RUNNING_X": "\"{{str}}\" çalıştırılıyor.",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} basıldı, ancak yer imlerini aç kısayolu yalnızca proje bağlamındayken kullanılabilir.",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} basılı, ancak açık notlar kısayolu yalnızca proje bağlamındayken kullanılabilir."
},
"GPB": {
"ASSETS": "Varlıklar yükleniyor ...",

View file

@ -14,10 +14,7 @@
"UPDATE_WEB_APP": "新版本可用,是否加载新版本?"
},
"BL": {
"FROM_OTHER_PROJECTS": "来自其他项目",
"NO_TASKS": "目前您的待办事项中没有任务",
"SCHEDULED": "已计划",
"TITLE": "待办事项"
"NO_TASKS": "目前您的待办事项中没有任务"
},
"DATETIME_INPUT": {
"IN": "在 {{time}}",
@ -80,6 +77,13 @@
}
},
"DROPBOX": {
"C": {
"EMPTY_SYNC": "您正在尝试同步一个空的数据对象。这是您(几乎)原始应用程序的第一次同步吗?",
"FORCE_IMPORT": "反正导入远程数据?",
"FORCE_UPLOAD": "仍要上传本地数据吗?",
"FORCE_UPLOAD_AFTER_ERROR": "上载本地数据时发生错误。尝试强制更新?",
"TRY_LOAD_REMOTE_AGAIN": "尝试再次从远程重新加载数据?"
},
"D_CONFLICT": {
"LAST_CHANGE": "最后一次变更:",
"LAST_SYNC": "上次同步:",
@ -610,8 +614,12 @@
"TIME": "时间"
},
"ADD_TASK_BAR": {
"ADD_EXISTING_TASK": "添加现有任务“{{taskTitle}}”",
"ADD_ISSUE_TASK": "从 {{issueType}}添加问题#{{issueNr}} ",
"ADD_TASK": "添加任务",
"ADD_TASK_TO_BACKLOG": "将任务添加到待办事项中",
"CREATE_TASK": "创建新任务",
"EXAMPLE": "示例:“某些任务标题+ projectName一些标签一些其他标签10m / 3h”",
"START": "再次按回车键开始"
},
"B": {
@ -949,6 +957,13 @@
"LONGER_BREAK_DURATION": "长休息时长",
"TITLE": "番茄钟"
},
"SOUND": {
"DONE_SOUND": "任务完成声音",
"IS_INCREASE_DONE_PITCH": "为完成的每项任务增加音调",
"IS_PLAY_DONE_SOUND": "任务标记为完成时播放声音",
"TITLE": "声音",
"VOLUME": "体积"
},
"TAKE_A_BREAK": {
"HELP": "<div><p>允许您在工作了指定的时间而不休息时配置重复提醒。</p> <p>您可以修改显示的消息。 ${duration}将被替换为连续工作时长。</p></div>",
"IS_ENABLED": "启用休息提醒",
@ -964,7 +979,9 @@
"COPY_TO_CLIPPBOARD": "复制到剪贴板",
"ERR_COMPRESSION": "压缩界面出错",
"PERSISTENCE_DISALLOWED": "数据将不会永久保存。请注意,这可能会导致数据丢失!!",
"RUNNING_X": "运行“{{str}}”。"
"RUNNING_X": "运行“{{str}}”。",
"SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": " 按下了{{keyCombo}} ,但是打开书签快捷方式仅在项目上下文中可用。",
"SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": " 按下了{{keyCombo}} ,但是仅在项目上下文中,打开注释快捷方式才可用。"
},
"GPB": {
"ASSETS": "正在加载数据...",

View file

@ -2,5 +2,6 @@ import { version } from '../../package.json';
export const environment = {
production: true,
stage: false,
version,
};

View file

@ -0,0 +1,7 @@
import { version } from '../../package.json';
export const environment = {
production: false,
stage: true,
version,
};

View file

@ -5,6 +5,7 @@ import { version } from '../../package.json';
export const environment = {
production: false,
stage: false,
version,
};

View file

@ -7,13 +7,13 @@ import { IS_ELECTRON } from './app/app.constants';
import { IS_ANDROID_WEB_VIEW } from './app/util/is-android-web-view';
import { androidInterface } from './app/core/android/android-interface';
if (environment.production) {
if (environment.production || environment.stage) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule).then(() => {
// TODO make asset caching work for electron
if ('serviceWorker' in navigator && environment.production && !IS_ELECTRON) {
if ('serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON) {
console.log('Registering Service worker');
return navigator.serviceWorker.register('ngsw-worker.js');
}
@ -27,7 +27,7 @@ platformBrowserDynamic().bootstrapModule(AppModule).then(() => {
window.addEventListener('touchmove', () => {
});
if (!environment.production && IS_ANDROID_WEB_VIEW) {
if (!(environment.production || environment.stage) && IS_ANDROID_WEB_VIEW) {
setTimeout(() => {
androidInterface.showToast('Android DEV works');
console.log(androidInterface);

View file

@ -4552,10 +4552,10 @@ electron-window-state@^5.0.3:
jsonfile "^4.0.0"
mkdirp "^0.5.1"
electron@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.0.tgz#3559182545dc76e20d9764d183d555415d55c250"
integrity sha512-DyS6WhQ59+ZXQsI1EkpsYkOXFt0Xbp+mbxPTJS9A7O21r3JDzaTC+1Jxz7g6J+Sbi9Y7UFdRs0tn/vqhHJx2gA==
electron@^10.1.2:
version "10.1.2"
resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.2.tgz#30b6fd7669f8daf08c56219a61dfa053fa2b0c70"
integrity sha512-SvN8DcKCmPZ0UcQSNAJBfaUu+LGACqtRhUn1rW0UBLHgdbbDM76L0GU5/XGQEllH5pu5bwlCZwax3srzIl+Aeg==
dependencies:
"@electron/get" "^1.0.1"
"@types/node" "^12.0.12"
@ -6256,7 +6256,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
idb@^5.0.3:
idb@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/idb/-/idb-5.0.6.tgz#8c94624f5a8a026abe3bef3c7166a5febd1cadc1"
integrity sha512-/PFvOWPzRcEPmlDt5jEvzVZVs0wyd/EvGvkDIcbBpGuMMLQKrTPG0TxvE2UJtgZtCQCmOtM2QD7yQJBVEjKGOw==
@ -6907,7 +6907,7 @@ jake@^10.6.1:
filelist "^1.0.1"
minimatch "^3.0.4"
jasmine-core@^3.6.0, jasmine-core@~3.6.0:
jasmine-core@^3.4.0, jasmine-core@^3.6.0, jasmine-core@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==