├── .editorconfig
├── .env.example
├── .eslintrc
├── .github
└── dependabot.yml
├── .gitignore
├── .gitlab-ci.yml
├── .nvmrc
├── .sentry.json
├── .yarn
├── plugins
│ └── @yarnpkg
│ │ ├── plugin-interactive-tools.cjs
│ │ └── plugin-version.cjs
└── releases
│ └── yarn-3.2.1.cjs
├── .yarnrc.yml
├── LICENSE
├── README.md
├── app
├── assets
│ └── icons
│ │ ├── app
│ │ ├── icon.png
│ │ ├── icon@2x.png
│ │ ├── icon@4x.png
│ │ └── icon@8x.png
│ │ ├── dock
│ │ ├── icon-idle.png
│ │ ├── icon-loading.png
│ │ └── icon-tracking.png
│ │ └── tray
│ │ ├── icon-idle.png
│ │ ├── icon-idle@1.25x.png
│ │ ├── icon-idle@1.5x.png
│ │ ├── icon-idle@2.5x.png
│ │ ├── icon-idle@2x.png
│ │ ├── icon-idle@3x.png
│ │ ├── icon-idle@4x.png
│ │ ├── icon-loading.png
│ │ ├── icon-loading@1.25x.png
│ │ ├── icon-loading@1.5x.png
│ │ ├── icon-loading@2.5x.png
│ │ ├── icon-loading@2x.png
│ │ ├── icon-loading@3x.png
│ │ ├── icon-loading@4x.png
│ │ ├── icon-tracking.png
│ │ ├── icon-tracking@1.25x.png
│ │ ├── icon-tracking@1.5x.png
│ │ ├── icon-tracking@2.5x.png
│ │ ├── icon-tracking@2x.png
│ │ ├── icon-tracking@3x.png
│ │ └── icon-tracking@4x.png
├── renderer
│ ├── app.html
│ ├── fonts
│ │ ├── Rubik
│ │ │ ├── Rubik-Bold.ttf
│ │ │ └── Rubik-Regular.ttf
│ │ └── Source_Sans_Pro
│ │ │ ├── SourceSansPro-Bold.ttf
│ │ │ └── SourceSansPro-Regular.ttf
│ ├── js
│ │ ├── app.js
│ │ ├── components
│ │ │ ├── App.vue
│ │ │ ├── Loader.vue
│ │ │ ├── Message.vue
│ │ │ ├── auth
│ │ │ │ └── Login.vue
│ │ │ ├── controls
│ │ │ │ └── WindowControl.vue
│ │ │ ├── inactivity
│ │ │ │ └── Modal.vue
│ │ │ └── user
│ │ │ │ ├── ControlBar.vue
│ │ │ │ ├── Navigation.vue
│ │ │ │ ├── User.vue
│ │ │ │ ├── intervals
│ │ │ │ └── IntervalViewBig.vue
│ │ │ │ ├── pages
│ │ │ │ ├── IntervalsQueue.vue
│ │ │ │ ├── OfflineSync.vue
│ │ │ │ ├── Project.vue
│ │ │ │ └── Settings.vue
│ │ │ │ └── tasks
│ │ │ │ ├── Create.vue
│ │ │ │ ├── Info.vue
│ │ │ │ ├── List.vue
│ │ │ │ ├── Task.vue
│ │ │ │ └── Tracker.vue
│ │ ├── helpers
│ │ │ ├── colors.helper.js
│ │ │ ├── time-between.helper.js
│ │ │ └── time-format.helper.js
│ │ ├── router
│ │ │ ├── index.js
│ │ │ └── routes.js
│ │ ├── storage
│ │ │ ├── index.js
│ │ │ └── store.js
│ │ └── utils
│ │ │ └── screenshot.js
│ ├── screen-notie.html
│ └── scss
│ │ ├── app.scss
│ │ ├── imports
│ │ ├── _buttons.scss
│ │ ├── _fonts.scss
│ │ ├── _forms.scss
│ │ ├── _grid.scss
│ │ ├── _helpers.scss
│ │ ├── _icons.scss
│ │ ├── _main.scss
│ │ ├── _nav.scss
│ │ └── _variables.scss
│ │ └── misc
│ │ └── tasks-style-misc.scss
└── src
│ ├── app.js
│ ├── base
│ ├── active-window.js
│ ├── api.js
│ ├── authentication.js
│ ├── config.js
│ ├── deferred-handler.js
│ ├── offline-mode.js
│ ├── os-integration.js
│ ├── task-tracker.js
│ ├── translation.js
│ ├── update.js
│ └── user-preferences.js
│ ├── components
│ ├── application-menu.js
│ ├── disable-production-hotkeys.js
│ ├── index.js
│ ├── log-rotate.js
│ ├── os-inactivity-handler.js
│ ├── power-manager.js
│ ├── relaunch-on-logout.js
│ ├── screen-notie.js
│ ├── tray.js
│ └── usage-statistic.js
│ ├── constants
│ ├── ScreenshotsState.js
│ ├── empty-screenshot.js
│ ├── log-rotate.js
│ └── url.js
│ ├── controller
│ ├── offline-user.js
│ ├── projects.js
│ ├── tasks.js
│ ├── time-intervals.js
│ ├── time.js
│ └── tracking-features.js
│ ├── migrations
│ ├── 20190401162647-create-project.js
│ ├── 20190401185842-create-task.js
│ ├── 20190401210750-create-interval.js
│ ├── 20190401213740-create-track.js
│ ├── 20190529213233-intervals-table-structure-changed-to-boundaries.js
│ ├── 20190816003133-intervals-table-userid-added-.js
│ ├── 20190816163400-create-property.js
│ ├── 20200330060000-projects-source-prop.js
│ ├── 20200709112600-change-interval-properties.js
│ ├── 20200715105642-tasks-pinorder-property-added.js
│ ├── 20210114201900-add-synced-flag-to-interval.js
│ ├── 20210115220000-add-remoteid-to-interval.js
│ ├── 20240000000001-change-interval-uuid-type.js
│ └── 20240000000002-projects-add-screenshots-state.js
│ ├── models
│ ├── index.js
│ ├── interval.js
│ ├── project.js
│ ├── property.js
│ ├── task.js
│ └── track.js
│ ├── routes
│ ├── authentication.js
│ ├── index.js
│ ├── intervals.js
│ ├── misc.js
│ ├── offline-mode.js
│ ├── projects.js
│ ├── task-tracking.js
│ ├── tasks.js
│ ├── time.js
│ ├── translation.js
│ └── user-preferences.js
│ ├── translations
│ ├── en.json
│ └── ru.json
│ └── utils
│ ├── crypto-random.js
│ ├── errors.js
│ ├── event-counter.js
│ ├── heartbeat-monitor.js
│ ├── icons.js
│ ├── jwt.js
│ ├── keychain.js
│ ├── log.js
│ ├── notifier.js
│ ├── screenshot.js
│ ├── sentry.js
│ ├── ticker.js
│ ├── time.js
│ └── translations.js
├── docker-desktop.png
├── package.json
├── resources
├── appx
│ ├── LargeTile.png
│ ├── SmallTile.png
│ ├── Square150x150Logo.png
│ ├── Square44x44Logo.png
│ ├── StoreLogo.png
│ └── Wide310x150Logo.png
├── entitlements.mac.inherit.plist
├── entitlements.mas.plist
├── icon.icns
├── icon.ico
├── icon.png
└── icons
│ ├── 1024x1024.png
│ ├── 128x128.png
│ ├── 16x16.png
│ ├── 256x256.png
│ ├── 32x32.png
│ ├── 48x48.png
│ ├── 512x512.png
│ └── 64x64.png
├── tools
├── artifact-manifest.js
├── clean-development.js
├── macos-notarization.js
└── rimraf.js
├── webpack.mix.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | [*.{js,json,yml}]
8 | charset = utf-8
9 | indent_style = space
10 | indent_size = 2
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # API key & issuer exported from AppStore Connect
2 | # Required for Apple Notarization for app distribution
3 | APPLE_API_KEY=1234XXXXZZ
4 | APPLE_API_ISSUER=issuer-id-uuid-should-be-here
5 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "linebreak-style": ["error", "unix"]
4 | },
5 | "parserOptions": {
6 | "ecmaVersion": "latest"
7 | },
8 |
9 | "env": {
10 | "es6": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 |
24 | # nyc test coverage
25 | .nyc_output
26 |
27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
28 | .grunt
29 |
30 | # Bower dependency directory (https://bower.io/)
31 | bower_components
32 |
33 | # node-waf configuration
34 | .lock-wscript
35 |
36 | # Compiled binary addons (https://nodejs.org/api/addons.html)
37 | build/Release
38 |
39 | # Dependency directories
40 | node_modules/
41 | jspm_packages/
42 |
43 | # TypeScript v1 declaration files
44 | typings/
45 |
46 | # Optional npm cache directory
47 | .npm
48 |
49 | # Optional eslint cache
50 | .eslintcache
51 |
52 | # Optional REPL history
53 | .node_repl_history
54 |
55 | # Output of 'npm pack'
56 | *.tgz
57 |
58 | # Yarn Integrity file
59 | .yarn-integrity
60 |
61 | # dotenv environment variables file
62 | .env
63 | .env.test
64 |
65 | # parcel-bundler cache (https://parceljs.org/)
66 | .cache
67 |
68 | # next.js build output
69 | .next
70 |
71 | # nuxt.js build output
72 | .nuxt
73 |
74 | # vuepress build output
75 | .vuepress/dist
76 |
77 | # Serverless directories
78 | .serverless/
79 |
80 | # FuseBox cache
81 | .fusebox/
82 |
83 | # DynamoDB Local files
84 | .dynamodb/
85 |
86 | # Ignore builds
87 | native/*.node
88 |
89 | # macOS
90 | .DS_Store
91 |
92 | # Idea
93 | .idea/
94 |
95 | # Theming toolkit
96 | theming/
97 |
98 | # Binary distribution
99 | target
100 |
101 | # Built frontend
102 | build
103 |
104 | .yarn/*
105 | !.yarn/patches
106 | !.yarn/plugins
107 | !.yarn/releases
108 | !.yarn/sdks
109 | !.yarn/versions
110 | .pnp.*
111 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - build
3 | - upload
4 | - deploy
5 |
6 | variables:
7 | PACKAGE_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/binaries/"
8 |
9 | build-linux:
10 | stage: build
11 | tags:
12 | - linux
13 | only:
14 | - tags
15 | image: node:fermium-slim
16 | artifacts:
17 | untracked: false
18 | expire_in: 30 days
19 | paths:
20 | - "target/*.AppImage"
21 | - "target/*.tar.gz"
22 | - "target/*.deb"
23 | - "target/latest-linux.yml"
24 | before_script:
25 | - apt update
26 | - apt install -y build-essential python3 pkg-config libsecret-1-0 libsecret-1-dev ca-certificates openssh-client dpkg-dev dpkg-sig
27 | - export RELEASE_VERSION=$(echo "${CI_COMMIT_TAG}" | sed 's/v//')
28 | - yarn
29 | - npm config set git-tag-version false
30 | - npm version $RELEASE_VERSION
31 | script:
32 | - yarn build-production
33 |
34 | - yarn package-linux
35 |
36 | build-mac:
37 | stage: build
38 | tags:
39 | - macos
40 | only:
41 | - tags
42 | artifacts:
43 | untracked: false
44 | expire_in: 30 days
45 | paths:
46 | - "target/*.dmg"
47 | - "target/latest-mac.yml"
48 | before_script:
49 | - source ~/.zshrc
50 | - security unlock-keychain -p "$APPLE_CI_HOST_PASSWORD"
51 | - export RELEASE_VERSION=$(echo "${CI_COMMIT_TAG}" | sed 's/v//')
52 | - yarn
53 | - npm config set git-tag-version false
54 | - npm version $RELEASE_VERSION
55 | script:
56 | - yarn build-production
57 |
58 | - yarn package-mac-unsigned
59 |
60 | build-windows:
61 | stage: build
62 | image: electronuserland/builder:14-wine
63 | tags:
64 | - docker
65 | only:
66 | - tags
67 | artifacts:
68 | untracked: false
69 | expire_in: 30 days
70 | paths:
71 | - "target/*.exe"
72 | - "target/*.appx"
73 | - "target/latest*.yml"
74 | before_script:
75 | - export RELEASE_VERSION=$(echo "${CI_COMMIT_TAG}" | sed 's/v//')
76 | - yarn
77 | - npm config set git-tag-version false
78 | - npm version $RELEASE_VERSION
79 | script:
80 | - yarn build-production
81 | - yarn package-windows
82 |
83 | upload:
84 | stage: upload
85 | only:
86 | - tags
87 | image: curlimages/curl:latest
88 | before_script:
89 | - export RELEASE_VERSION=$(echo "${CI_COMMIT_TAG}" | sed 's/v//')
90 | script:
91 | - 'cd target && find . -maxdepth 1 -type f | while read -r line; do curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file "${line}" "${PACKAGE_URL}${RELEASE_VERSION}/${result}"; done'
92 |
93 | release:
94 | stage: deploy
95 | only:
96 | - tags
97 | when: manual
98 | image: registry.gitlab.com/gitlab-org/release-cli:latest
99 | before_script:
100 | - export RELEASE_VERSION=$(echo "${CI_COMMIT_TAG}" | sed 's/v//')
101 | script:
102 | - |
103 | release-cli create --name $CI_COMMIT_TAG --tag-name $CI_COMMIT_TAG \
104 | --assets-link "{\"name\":\"dmg\",\"filepath\":\"/dmg\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr.dmg\", \"link_type\":\"package\"}" \
105 | --assets-link "{\"name\":\"exe\",\"filepath\":\"/exe\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr.exe\", \"link_type\":\"package\"}" \
106 | --assets-link "{\"name\":\"nsis\",\"filepath\":\"/nsis\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr_Setup.exe\", \"link_type\":\"package\"}" \
107 | --assets-link "{\"name\":\"appx\",\"filepath\":\"/appx\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr.appx\", \"link_type\":\"package\"}" \
108 | --assets-link "{\"name\":\"tar\",\"filepath\":\"/tar\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr.tar.gz\", \"link_type\":\"package\"}" \
109 | --assets-link "{\"name\":\"deb\",\"filepath\":\"/deb\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr.deb\", \"link_type\":\"package\"}" \
110 | --assets-link "{\"name\":\"appimage\",\"filepath\":\"/appimage\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/Cattr.AppImage\", \"link_type\":\"package\"}" \
111 | --assets-link "{\"name\":\"windows-yml\",\"filepath\":\"/latest.yml\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/latest.yml\", \"link_type\":\"other\"}" \
112 | --assets-link "{\"name\":\"mac-yml\",\"filepath\":\"/latest-mac.yml\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/latest-mac.yml\", \"link_type\":\"other\"}" \
113 | --assets-link "{\"name\":\"linux-yml\",\"filepath\":\"/latest-linux.yml\",\"url\":\"${PACKAGE_URL}${RELEASE_VERSION}/latest-linux.yml\", \"link_type\":\"other\"}"
114 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 14.19.0
2 |
--------------------------------------------------------------------------------
/.sentry.json:
--------------------------------------------------------------------------------
1 | {
2 | "url": "https://sentry.amazingcat.net",
3 | "org": "amazingcat",
4 | "frontend": {
5 | "project": "cattr-desktop-vue"
6 | },
7 | "backend": {
8 | "project": "cattr-desktop-electron"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | npmScopes:
4 | cattr:
5 | npmRegistryServer: "https://git.amazingcat.net/api/v4/packages/npm/"
6 |
7 | plugins:
8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
9 | spec: "@yarnpkg/plugin-interactive-tools"
10 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs
11 | spec: "@yarnpkg/plugin-version"
12 |
13 | yarnPath: .yarn/releases/yarn-3.2.1.cjs
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Cattr Desktop App
2 | ==========
3 | Electron desktop application for Cattr
4 |
5 | Minimum system requirements to build the app
6 | - MacOS: Monterey 12.3.1
7 | - Windows: 22H2 10.0.19045, 11.0.22621
8 | - Debian: bullseye+kde 11
9 | - Ubuntu: LTS 22.04
10 | - Alt linux: kworkstation 10
11 | - Astra linux: orel 2.12
12 | - CPU: amd64
13 |
14 | ### For build to work, you need to have following dependencies:
15 | #### MacOS
16 | You need to install xcode from [official website](https://developer.apple.com/xcode/)
17 |
18 | #### Linux (apt based)
19 | ```bash
20 | apt-get update
21 | apt-get install -y git cmake curl python3 build-essential pkg-config libsecret-1-0 libsecret-1-dev ca-certificates openssh-client dpkg-dev dpkg-sig
22 | ```
23 | ##### Installl nodejs 14.19.0 (MacOS & Linux)
24 | Easiest way to do so is by using nvm, here is the [official guide on how to install it](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script).
25 |
26 | Now we can use it to install nodejs.
27 | ```bash
28 | nvm install 14.19.0
29 | nvm use 14.19.0
30 | ```
31 | Install yarn
32 | ```bash
33 | npm install -g yarn
34 | ```
35 |
36 | You can verify the installation like so:
37 | ```bash
38 | node -v # v14.19.0
39 | yarn -v # 3.2.1
40 | ```
41 |
42 | #### Windows
43 | ##### Download and install Docker Desktop from the [official site](https://www.docker.com/).
44 |
45 | 
46 |
47 | For Docker to work in Windows you may need to enable virtualization in BIOS and [install WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install). The installation process is described in details [in the Docker user manual](https://docs.docker.com/desktop/setup/install/windows-install/).
48 |
49 |
50 | ## Launch development version (Linux & MacOS only)
51 | 1. Clone this repository and open it's directory
52 | 2. Install dependencies via `yarn`
53 | 3. Specify version, for example `v1.0.0"`
54 | ```bash
55 | npm config set git-tag-version false
56 | npm version v1.0.0
57 | ```
58 | 4. Run webpack via `yarn build-development` for development version
59 | 5. When build completes, run `yarn dev` to launch client in development mode
60 |
61 | ## Development mode
62 | Development installation uses different keychain service name and application folder path (with "-develop" suffix).
63 |
64 | ## Build production version
65 | 1. Clone this repository and open its directory
66 | 2. (Windows only) run in PowerShell `docker run -it -v ${PWD}:/project electronuserland/builder:14-wine` next commands should be executed inside running container.
67 | 3. Install dependencies via `yarn`
68 | 4. Specify version, for example `v1.0.0`
69 | ```bash
70 | npm config set git-tag-version false
71 | npm version v1.0.0
72 | ```
73 | 5. Build application in production mode via `yarn build-production`
74 | 6. Build executable for your favourite platform (output directory is `/target`).
75 |
76 |
77 | How to build executable?
78 | - **macOS:** `yarn package-mac` will produce signed & notarized DMG
79 | - **Linux:** `yarn package-linux` will produce Tarball, DPKG and AppImage
80 | - **Windows:** `yarn package-windows` will produce installer and portable executables
81 |
82 | Compatibility sheet:
83 | - **Host with macOS:** can produce builds only for macOS
84 | - **Host with Linux:** can produce builds for Linux and Windows (using Wine)
85 | - **Host with Windows:** can produce builds only for Windows
86 |
--------------------------------------------------------------------------------
/app/assets/icons/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/app/icon.png
--------------------------------------------------------------------------------
/app/assets/icons/app/icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/app/icon@2x.png
--------------------------------------------------------------------------------
/app/assets/icons/app/icon@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/app/icon@4x.png
--------------------------------------------------------------------------------
/app/assets/icons/app/icon@8x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/app/icon@8x.png
--------------------------------------------------------------------------------
/app/assets/icons/dock/icon-idle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/dock/icon-idle.png
--------------------------------------------------------------------------------
/app/assets/icons/dock/icon-loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/dock/icon-loading.png
--------------------------------------------------------------------------------
/app/assets/icons/dock/icon-tracking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/dock/icon-tracking.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle@1.25x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle@1.25x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle@1.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle@1.5x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle@2.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle@2.5x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle@2x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle@3x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-idle@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-idle@4x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading@1.25x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading@1.25x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading@1.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading@1.5x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading@2.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading@2.5x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading@2x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading@3x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-loading@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-loading@4x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking@1.25x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking@1.25x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking@1.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking@1.5x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking@2.5x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking@2.5x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking@2x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking@3x.png
--------------------------------------------------------------------------------
/app/assets/icons/tray/icon-tracking@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/assets/icons/tray/icon-tracking@4x.png
--------------------------------------------------------------------------------
/app/renderer/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Cattr
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/renderer/fonts/Rubik/Rubik-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/renderer/fonts/Rubik/Rubik-Bold.ttf
--------------------------------------------------------------------------------
/app/renderer/fonts/Rubik/Rubik-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/renderer/fonts/Rubik/Rubik-Regular.ttf
--------------------------------------------------------------------------------
/app/renderer/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/renderer/fonts/Source_Sans_Pro/SourceSansPro-Bold.ttf
--------------------------------------------------------------------------------
/app/renderer/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cattr-app/desktop-application/c9846bde80a519633c4ed24c9657b7f75ac5a0df/app/renderer/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf
--------------------------------------------------------------------------------
/app/renderer/js/app.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron';
2 | import Vue from 'vue';
3 | import IPCRouter from '@amazingcat/electron-ipc-router';
4 | import * as Sentry from '@sentry/browser';
5 | import * as Integrations from '@sentry/integrations';
6 | import VueI18n from 'vue-i18n';
7 | import Element from 'element-ui';
8 |
9 | import store from './storage';
10 | import App from './components/App.vue';
11 | import router from './router';
12 |
13 | // Comment it out to use remote devtools
14 | if (process.env.NODE_ENV === 'development' && process.env.REMOTE_DEVTOOLS_ENABLE) {
15 |
16 | try {
17 |
18 | // eslint-disable-next-line global-require
19 | // const devtools = require('@vue/devtools');
20 | // devtools.connect();
21 |
22 | // eslint-disable-next-line no-console
23 | console.log('vue-devtools package is disabled due to the maintaining issues');
24 |
25 | } catch (err) {
26 |
27 | // eslint-disable-next-line no-console
28 | console.error('Error occured during Vue Devtools init', err);
29 |
30 | }
31 |
32 | }
33 |
34 | Vue.use(VueI18n);
35 | Vue.use(Element);
36 |
37 | (async () => {
38 |
39 | // Initialize IPC
40 | Vue.prototype.$ipc = new IPCRouter(ipcRenderer);
41 |
42 | // Initialise Sentry
43 | (() => {
44 |
45 | // eslint-disable-next-line no-restricted-globals
46 | let sentryConfig = new URL(location.href);
47 | if (!sentryConfig.searchParams.has('sentry'))
48 | return;
49 |
50 | // Extract Sentry configuration from query params
51 | sentryConfig = JSON.parse(sentryConfig.searchParams.get('sentry'));
52 |
53 | if (!sentryConfig.enabled)
54 | return;
55 |
56 | Sentry.init({
57 | dsn: sentryConfig.dsnFrontend,
58 | release: sentryConfig.release,
59 | integrations: [new Integrations.Vue({ Vue, attachProps: true })],
60 |
61 | // Patching error report right before sending to normalize frontend paths
62 | beforeSend: data => {
63 |
64 | // Filter only requests with known structure which we can modify
65 | if (!data || !data.exception || !data.exception.values)
66 | return data;
67 |
68 | // Iterate over exceptions
69 | // eslint-disable-next-line no-param-reassign
70 | data.exception.values = data.exception.values.map(exception => {
71 |
72 | // Filter only exceptions we can modify
73 | if (!exception.stacktrace || !exception.stacktrace.frames)
74 | return exception;
75 |
76 | // Rewrite exception file path to "build/app.js" since we always
77 | // building frontend into the single bundle
78 | // eslint-disable-next-line no-param-reassign
79 | exception.stacktrace.frames = exception.stacktrace.frames.map(frame => {
80 |
81 | // Modify only exceptions with the "filename" property
82 | if (frame.filename)
83 | frame.filename = 'build/app.js'; // eslint-disable-line no-param-reassign
84 |
85 | return frame;
86 |
87 | });
88 |
89 | return exception;
90 |
91 | });
92 |
93 | return data;
94 |
95 | },
96 | });
97 |
98 | // Populate Sentry error reports with company identifier
99 | Vue.prototype.$ipc.serve(
100 | 'auth/company-instance-fetched',
101 | req => Sentry.configureScope(scope => scope.setTag('companyIdentifier', req.packet.body.cid)),
102 | );
103 |
104 |
105 | })();
106 |
107 | // Initialise translations
108 | const i18n = await (async () => {
109 |
110 | const { body } = await Vue.prototype.$ipc.request('translation/get-configuration', {});
111 | const { lng, resources } = body.configuration;
112 | const messages = {};
113 |
114 | Object.entries(resources).forEach(([key, value]) => {
115 |
116 | messages[key] = value.translation;
117 |
118 | });
119 |
120 | return new VueI18n({ locale: lng, silentTranslationWarn: true, messages });
121 |
122 | })();
123 |
124 | new Vue({
125 | router,
126 | store,
127 | i18n,
128 | render: h => h(App),
129 | }).$mount('#app');
130 |
131 | })();
132 |
133 |
--------------------------------------------------------------------------------
/app/renderer/js/components/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
13 |
14 |
15 |
16 |
29 |
30 |
47 |
--------------------------------------------------------------------------------
/app/renderer/js/components/Message.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ this.title }}
5 |
{{ this.message }}
6 |
7 |
8 |
9 |
10 |
23 |
24 |
27 |
--------------------------------------------------------------------------------
/app/renderer/js/components/controls/WindowControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
44 |
45 |
66 |
--------------------------------------------------------------------------------
/app/renderer/js/components/user/Navigation.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 | Tasks
10 |
11 |
12 | Settings
13 |
14 |
15 |
16 |
17 |
27 |
28 |
31 |
--------------------------------------------------------------------------------
/app/renderer/js/components/user/pages/IntervalsQueue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
15 |
19 | {{ $t('Nothing there') }} 🤷
20 |
21 |
22 |
23 |
24 |
25 |
26 |
88 |
89 |
138 |
--------------------------------------------------------------------------------
/app/renderer/js/components/user/pages/Project.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 |
13 |
14 |
94 |
95 |
188 |
--------------------------------------------------------------------------------
/app/renderer/js/components/user/tasks/Create.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
13 |
17 |
18 |
22 |
28 |
29 |
34 |
41 |
42 |
50 | {{ $t('Create a new task') }}
51 |
52 |
53 |
54 |
55 |
56 |
57 |
175 |
--------------------------------------------------------------------------------
/app/renderer/js/helpers/colors.helper.js:
--------------------------------------------------------------------------------
1 | export function addLight(color, amount) {
2 |
3 | const cc = parseInt(color, 16) + amount;
4 | let c = (cc > 255) ? 255 : (cc);
5 | c = (c.toString(16).length > 1) ? c.toString(16) : `0${c.toString(16)}`;
6 | return c;
7 |
8 | }
9 |
10 | export function lighten(rawColor, rawAmount) {
11 |
12 | const color = (rawColor.includes('#')) ? rawColor.substring(1, rawColor.length) : rawColor;
13 | const amount = parseInt((255 * rawAmount) / 100, 10);
14 | return `#${addLight(color.substring(0, 2), amount)}${addLight(color.substring(2, 4), amount)}${addLight(color.substring(4, 6), amount)}`;
15 |
16 | }
17 |
18 | export function subtractLight(color, amount) {
19 |
20 | const cc = parseInt(color, 16) - amount;
21 | let c = (cc < 0) ? 0 : (cc);
22 | c = (c.toString(16).length > 1) ? c.toString(16) : `0${c.toString(16)}`;
23 | return c;
24 |
25 | }
26 |
27 | export function darken(rawColor, rawAmount) {
28 |
29 | const color = (rawColor.includes('#')) ? rawColor.substring(1, rawColor.length) : rawColor;
30 | const amount = parseInt((255 * rawAmount) / 100, 10);
31 | return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight(color.substring(2, 4), amount)}${subtractLight(color.substring(4, 6), amount)}`;
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/renderer/js/helpers/time-between.helper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 |
3 | /**
4 | * Delta between dates in seconds
5 | * @param {Date} dateA
6 | * @param {Date} dateB
7 | * @returns {Number} Delta in seconds
8 | */
9 | export function secondsBetween(dateA, dateB) {
10 |
11 | return Math.ceil(Math.abs(new Date(dateA).getTime() - new Date(dateB).getTime()) / 1000);
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/app/renderer/js/helpers/time-format.helper.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 |
3 | /**
4 | * Adding a leading zero
5 | * @param {Number} number Input number
6 | * @return {String} String with lead zero if neccessary
7 | */
8 | const leftpad = number => ((number >= 10) ? String(number) : `0${number}`);
9 |
10 | /**
11 | * Converts amount of seconds into HH:mm:ss format
12 | * @param {Number} seconds Amount of seconds
13 | * @returns {String} Formatted amount of seconds
14 | */
15 | export function formatSeconds(seconds) {
16 |
17 | // Parse input argument and perform type-safety checks
18 | let secs = Number(seconds);
19 | if (Number.isNaN(secs))
20 | throw new TypeError('Incorrect seconds amount');
21 | else if (secs < 0)
22 | throw new TypeError('Expect positive number of seconds, but negative is given');
23 | else if (secs === 0)
24 | return '00:00:00';
25 |
26 | // Getting amount of hours
27 | const hours = Math.floor(secs / 3600);
28 | secs %= 3600;
29 |
30 | // Getting amount of seconds
31 | const minutes = Math.floor(secs / 60);
32 | secs %= 60;
33 |
34 | return `${leftpad(hours)}:${leftpad(minutes)}:${leftpad(secs)}`;
35 |
36 | }
37 |
38 | /**
39 | * Splits seconds into hours-minutes-seconds
40 | * @param {Number} seconds
41 | * @returns {Object}
42 | */
43 | export function splitSecondsIntoHMS(seconds) {
44 |
45 | let secs = Number(seconds);
46 |
47 | // Getting amount of hours
48 | const hours = Math.floor(secs / 3600);
49 | secs %= 3600;
50 |
51 | // Getting amount of seconds
52 | const minutes = Math.floor(secs / 60);
53 | secs %= 60;
54 |
55 | return { hours, minutes, seconds: secs };
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/app/renderer/js/router/index.js:
--------------------------------------------------------------------------------
1 | import Router from 'vue-router';
2 | import Vue from 'vue';
3 | import routes from './routes';
4 |
5 | Vue.use(Router);
6 |
7 | export default new Router({
8 | mode: 'history',
9 | routes,
10 | });
11 |
--------------------------------------------------------------------------------
/app/renderer/js/router/routes.js:
--------------------------------------------------------------------------------
1 | import Login from '../components/auth/Login.vue';
2 | import User from '../components/user/User.vue';
3 | import TaskList from '../components/user/tasks/List.vue';
4 | import UserSettings from '../components/user/pages/Settings.vue';
5 | import OfflineSync from "../components/user/pages/OfflineSync.vue";
6 | import Info from '../components/user/tasks/Info.vue';
7 | import TaskCreate from '../components/user/tasks/Create.vue';
8 | import Project from '../components/user/pages/Project.vue';
9 | import IntervalsQueue from '../components/user/pages/IntervalsQueue.vue';
10 |
11 | export default [
12 | {
13 | name: 'auth.login',
14 | path: '/auth/login',
15 | component: Login,
16 | },
17 | {
18 | name: 'user',
19 | path: '/user',
20 | component: User,
21 | beforeEnter: (to, from, next) => next(), // TODO
22 | children: [
23 | {
24 | name: 'user.settings',
25 | path: 'settings',
26 | component: UserSettings,
27 | },
28 | {
29 | name: 'user.offline-sync',
30 | path: 'offline-sync',
31 | component: OfflineSync,
32 | },
33 | {
34 | name: 'user.tasks',
35 | path: 'tasks',
36 | component: TaskList,
37 | },
38 | {
39 | name: 'user.task',
40 | path: 'task/:id',
41 | component: Info,
42 | },
43 | {
44 | name: 'user.project',
45 | path: 'project/:id',
46 | component: Project,
47 | },
48 | {
49 | name: 'user.createTask',
50 | path: 'create',
51 | component: TaskCreate,
52 | },
53 | {
54 | name: 'user.intervalsQueue',
55 | path: 'queue',
56 | component: IntervalsQueue,
57 | },
58 | ],
59 | },
60 | ];
61 |
--------------------------------------------------------------------------------
/app/renderer/js/storage/index.js:
--------------------------------------------------------------------------------
1 | import Vuex from 'vuex';
2 | import Vue from 'vue';
3 | import store from './store';
4 |
5 | Vue.use(Vuex);
6 |
7 | export default new Vuex.Store(store);
8 |
--------------------------------------------------------------------------------
/app/renderer/js/utils/screenshot.js:
--------------------------------------------------------------------------------
1 | const { desktopCapturer } = require('electron');
2 |
3 | /**
4 | * Returns available "display" MediaDevices
5 | * @return {Promise|Error>} Return array of MediaDevice, or error
6 | */
7 | const getMediaDevices = () => desktopCapturer.getSources({ types: ['screen'] });
8 |
9 | /**
10 | * Captures screen
11 | * @param {DOMElement} canvas Canvas element
12 | * @return {Promise|null>} Captured screenshots in base64 or null
13 | */
14 | export default async canvas => {
15 |
16 | // Getting available MediaDevices
17 | const mediaDevices = await getMediaDevices();
18 |
19 | // Getting MediaStream from all fetched devices
20 | let userMediaStreams = mediaDevices.map(device => {
21 |
22 | // Fix
23 | if (device.stream)
24 | device.stream.stop();
25 |
26 | return navigator.mediaDevices.getUserMedia({
27 | audio: false,
28 | video: {
29 | mandatory: {
30 | chromeMediaSource: 'desktop',
31 | chromeMediaSourceId: device.id,
32 | },
33 | },
34 | });
35 |
36 | });
37 |
38 | // Filter all unsuccessfull media streams
39 | userMediaStreams = userMediaStreams.filter(stream => typeof stream !== 'undefined');
40 |
41 | // Resolve all MediaStreams inits
42 | userMediaStreams = await Promise.all(userMediaStreams);
43 |
44 | // Grabbing frams for all MediaStreams
45 | let userMediaGrabbers = userMediaStreams.map(mediaStream => {
46 |
47 | const mediaStreamTrack = mediaStream.getVideoTracks()[0];
48 | return new ImageCapture(mediaStreamTrack).grabFrame();
49 |
50 | });
51 |
52 | // Making screenshots
53 | userMediaGrabbers = await Promise.all(userMediaGrabbers);
54 |
55 | // Closing MediaStreams
56 | userMediaStreams.forEach(mediaStream => mediaStream.getVideoTracks()[0].stop());
57 |
58 | // Getting canvas size for splitted screenshots
59 | const canvasSize = userMediaGrabbers
60 |
61 | // Get W&H for each snap
62 | .map(snap => ([snap.width, snap.height]))
63 |
64 | // Sum them
65 | .reduce((dimensions, [width, height]) => {
66 |
67 | // Selecting max height over each screen
68 | if (height > dimensions.height)
69 | dimensions.height = height; // eslint-disable-line no-param-reassign
70 |
71 | // Summarize width
72 | dimensions.width += width; // eslint-disable-line no-param-reassign
73 |
74 | return dimensions;
75 |
76 | }, { width: 0, height: 0 });
77 |
78 | // Creating canvas with the same image size
79 | canvas.width = canvasSize.width; // eslint-disable-line no-param-reassign
80 | canvas.height = canvasSize.height; // eslint-disable-line no-param-reassign
81 |
82 | // Obtaining BitmapRenderer context
83 | const ctx = canvas.getContext('2d');
84 |
85 | // Placing snaps as ImageBitmaps on BitmapRenderer context
86 | let lastSnapEndedX = 0;
87 | userMediaGrabbers = userMediaGrabbers.forEach(snap => {
88 |
89 | ctx.drawImage(snap, lastSnapEndedX, 0);
90 | lastSnapEndedX += snap.width;
91 |
92 | });
93 |
94 | // Return rendered canvas as JPEG image with 50% quality in DataURL (base64)
95 | return canvas.toDataURL('image/jpeg', 0.5);
96 |
97 | };
98 |
--------------------------------------------------------------------------------
/app/renderer/screen-notie.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Cattr
8 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
190 |
191 |
192 |
193 |
--------------------------------------------------------------------------------
/app/renderer/scss/app.scss:
--------------------------------------------------------------------------------
1 | @import "imports/variables";
2 | @import "~element-ui/packages/theme-chalk/src/index";
3 | @import "imports/fonts";
4 | @import "imports/main";
5 | @import "imports/icons";
6 | // It's just a hook for the clickable tasks and projects buttons
7 | @import "misc/tasks-style-misc";
8 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_buttons.scss:
--------------------------------------------------------------------------------
1 | .btn {
2 | color: $font-color-primary;
3 | font-size: $font-size-base;
4 | padding: .5em 1.5em;
5 | cursor: pointer;
6 |
7 | font-family: $font-family-headings;
8 |
9 | border-radius: .25em;
10 | transition: ease .09s;
11 | border: 0;
12 |
13 | &:active {
14 | outline: none;
15 | }
16 |
17 | &:focus {
18 | outline: none;
19 | }
20 |
21 | &.btn-primary {
22 | background: $btn-color-primary;
23 |
24 | &:active {
25 | background: darken($btn-color-primary, 15%);
26 | }
27 | }
28 |
29 | &.btn-secondary {
30 | background: $btn-color-secondary;
31 |
32 | &:active {
33 | background: darken($btn-color-secondary, 10%);
34 | }
35 | }
36 |
37 | &.btn-outlined-primary {
38 | background: transparent;
39 | border: 2px solid $btn-color-primary;
40 |
41 | &:active {
42 | border-color: darken($btn-color-primary, 15%);
43 | }
44 | }
45 |
46 | &.btn-outlined-secondary {
47 | background: transparent;
48 | border: 2px solid $btn-color-secondary;
49 |
50 | &:active {
51 | border-color: darken($btn-color-secondary, 15%);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_fonts.scss:
--------------------------------------------------------------------------------
1 | /* cyrillic */
2 | @font-face {
3 | font-family: 'Rubik';
4 | font-style: normal;
5 | font-weight: 400;
6 | font-display: swap;
7 | src: local('Rubik'), local('Rubik-Regular'), url('fonts/Rubik-Regular.ttf') format('truetype');
8 | }
9 |
10 | /* cyrillic */
11 | @font-face {
12 | font-family: 'Rubik';
13 | font-style: normal;
14 | font-weight: 700;
15 | font-display: swap;
16 | src: local('Rubik Bold'), local('Rubik-Bold'), url('fonts/Rubik-Bold.ttf') format('truetype');
17 | }
18 |
19 | /* cyrillic */
20 | @font-face {
21 | font-family: 'Source Sans Pro';
22 | font-style: normal;
23 | font-weight: 400;
24 | font-display: swap;
25 | src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url('fonts/SourceSansPro-Regular.ttf') format('truetype');
26 | }
27 |
28 | /* cyrillic */
29 | @font-face {
30 | font-family: 'Source Sans Pro';
31 | font-style: normal;
32 | font-weight: 700;
33 | font-display: swap;
34 | src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url('fonts/SourceSansPro-Bold.ttf') format('truetype');
35 | }
36 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_forms.scss:
--------------------------------------------------------------------------------
1 | .form-row {
2 | display: flex;
3 | flex-direction: column;
4 |
5 | .form-input {
6 | padding: .5em .5em;
7 | font-size: 1em;
8 | /*border-radius: 1em;*/
9 | margin-bottom: 1em;
10 | border: 0;
11 | border-bottom: 1px solid $border-color-primary;
12 | background: transparent;
13 | color: #fafafa;
14 |
15 | /*box-shadow: 0 5px .3em lighten(rgba(24, 24, 38, .3), 15%);*/
16 |
17 | &:focus {
18 | // border: 0;
19 | outline: 0
20 | }
21 |
22 | &::placeholder {
23 | color: darken(#fafafa, 15%);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_grid.scss:
--------------------------------------------------------------------------------
1 | .row {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | margin-bottom: 1em;
5 | &:last-child {
6 | margin: 0;
7 | }
8 |
9 | .col {
10 | flex-basis: auto;
11 |
12 | &-1 {
13 | flex-basis: 8.33333333333%;
14 | max-width: 8.33333333333%;
15 | }
16 |
17 | &-2 {
18 | flex-basis: 16.6666666667%;
19 | max-width: 16.6666666667%;
20 | }
21 |
22 | &-3 {
23 | flex-basis: 25%;
24 | max-width: 25%;
25 |
26 | }
27 |
28 | &-4 {
29 | flex-basis: 33.3333333333%;
30 | max-width: 33.3333333333%;
31 |
32 | }
33 |
34 | &-5 {
35 | flex-basis: 41.6666666667%;
36 | max-width: 41.6666666667%;
37 |
38 | }
39 |
40 | &-6 {
41 | flex-basis: 50%;
42 | max-width: 50%
43 |
44 | }
45 |
46 | &-7 {
47 | flex-basis: 58.3333333333%;
48 | max-width: 58.3333333333%;
49 |
50 | }
51 |
52 | &-8 {
53 | flex-basis: 66.6666666667%;
54 | max-width: 66.6666666667%;
55 |
56 | }
57 |
58 | &-9 {
59 | flex-basis: 75%;
60 | max-width: 75%;
61 |
62 | }
63 |
64 | &-10 {
65 | flex-basis: 83.3333333333%;
66 | max-width: 83.3333333333%;
67 |
68 | }
69 |
70 | &-11 {
71 | flex-basis: 91.6666666667%;
72 | max-width: 91.6666666667%;
73 |
74 | }
75 |
76 | &-12 {
77 | flex-basis: 100%;
78 | max-width: 100%;
79 | }
80 |
81 | &-1, &-2, &-3, &-4, &-5, &-6, &-7, &-8, &-9, &-10, &-11, &-12 {
82 | box-sizing: border-box;
83 | padding: 0 .5em;
84 |
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_helpers.scss:
--------------------------------------------------------------------------------
1 | .text {
2 | &-center {
3 | text-align: center;
4 | }
5 |
6 | &-right {
7 | text-align: right;
8 | }
9 |
10 | &-left {
11 | text-align: left;
12 | }
13 | }
14 |
15 | .justify-content {
16 | &-left {
17 | justify-content: flex-start;
18 | }
19 |
20 | &-center {
21 | justify-content: center;
22 | }
23 |
24 | &-right {
25 | justify-content: flex-end;
26 | }
27 | }
28 |
29 | .align-items {
30 | &-top {
31 | align-items: flex-start;
32 | }
33 |
34 | &-center {
35 | align-items: center;
36 | }
37 |
38 | &-top {
39 | align-items: flex-end;
40 | }
41 | }
42 |
43 | .d {
44 | &-flex {
45 | display: flex;
46 | }
47 |
48 | &-inline-flex {
49 | display: inline-flex;
50 | }
51 | }
52 |
53 | .p {
54 | &-1 {
55 | padding: .5em !important;
56 | }
57 |
58 | &-2 {
59 | padding: 1em !important;
60 | }
61 |
62 | &-3 {
63 | padding: 1.5em !important;
64 | }
65 |
66 | &-4 {
67 | padding: 1.75em !important;
68 | }
69 |
70 | &-5 {
71 | padding: 2em !important;
72 | }
73 |
74 | &t {
75 | &-1 {
76 | padding-top: .5em !important;
77 | }
78 |
79 | &-2 {
80 | padding-top: 1em !important;
81 | }
82 |
83 | &-3 {
84 | padding-top: 1.5em !important;
85 | }
86 |
87 | &-4 {
88 | padding-top: 1.75em !important;
89 | }
90 |
91 | &-5 {
92 | padding-top: 2em !important;
93 | }
94 | }
95 | }
96 |
97 | .underlined {
98 | padding-bottom: .5em;
99 | border-bottom: 1px solid $border-color-primary;
100 | }
101 |
102 | .label {
103 | margin-bottom: .5em;
104 | }
105 |
106 | .text-muted {
107 | color: darken($font-color-primary, 15%) !important;
108 | }
109 |
110 | .w {
111 | &-25 {
112 | width: 25%;
113 | }
114 |
115 | &-50 {
116 | width: 50%;
117 | }
118 |
119 | &-75 {
120 | width: 75%
121 | }
122 |
123 | &-100 {
124 | width: 100%;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_icons.scss:
--------------------------------------------------------------------------------
1 | .el-icon-refresh.animated {
2 | animation: rotating-clockwise 2s linear infinite;
3 | }
4 |
5 | @keyframes rotating-clockwise {
6 | 0% {
7 | transform: rotateZ(360deg);
8 | }
9 | 100% {
10 | transform: rotateZ(0deg);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_main.scss:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | display: flex;
4 | flex-direction: column;
5 | width: 100vw;
6 | max-height: 100vh;
7 | height: 100vh;
8 |
9 | * {
10 | font-family: BlinkMacSystemFont, "Rubik", sans-serif;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/renderer/scss/imports/_nav.scss:
--------------------------------------------------------------------------------
1 | a {
2 | color: darken($font-color-primary, 10%);
3 |
4 | &:active {
5 | color: darken($font-color-primary, 30%);
6 | }
7 |
8 | &:visited {
9 | color: $font-color-primary;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/renderer/scss/misc/tasks-style-misc.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Used for clickable projects and tasks
3 | */
4 | .clickable {
5 | cursor: pointer;
6 | transition: $--all-transition;
7 |
8 | &:hover {
9 | color: $--color-primary-light-1;
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/app.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: 0 */
2 | const { app, shell, BrowserWindow } = require('electron');
3 | const fs = require('fs');
4 | const path = require('path');
5 | const config = require('./base/config');
6 | const appIcons = require('./utils/icons');
7 |
8 | const { WEBCONTENTS_ALLOWED_PROTOCOLS } = require('./constants/url');
9 | const userPreferences = require('./base/user-preferences');
10 |
11 | /**
12 | * Object, containing Electron browser window
13 | * @type {Object}
14 | */
15 | let window = {};
16 |
17 | // Second instance protection
18 | if (!config.isDeveloperModeEnabled) {
19 |
20 | // If single instance lock successfully set — we are the first running instance
21 | if (app.requestSingleInstanceLock()) {
22 |
23 | app.on('second-instance', () => {
24 |
25 | // Reveal window then focus on it if it's exists
26 | if (!window)
27 | return;
28 | window.restore();
29 | window.focus();
30 |
31 | });
32 |
33 | } else
34 | app.quit();
35 |
36 | }
37 |
38 | // Register custom protocol for SSO
39 | app.setAsDefaultProtocolClient('cattr');
40 |
41 | // Wait until Electron comes ready
42 | app.once('ready', async () => {
43 |
44 | // Load database
45 | await require('./models').init();
46 |
47 | const router = require('./routes');
48 | const osIntegration = require('./base/os-integration');
49 |
50 | // Creates new window with specified parameters
51 | window = new BrowserWindow({
52 |
53 | minWidth: 480,
54 | minHeight: 580,
55 | width: 480,
56 | height: 580,
57 | maxWidth: 640,
58 | maxHeight: 700,
59 | frame: true,
60 | show: false,
61 | center: true,
62 | maximizable: false,
63 | webPreferences: {
64 | nodeIntegration: true,
65 | enableRemoteModule: false,
66 | contextIsolation: false,
67 | nativeWindowOpen: true,
68 | },
69 | icon: appIcons.DEFAULT,
70 |
71 | });
72 |
73 | // Pass the window into OS integration module
74 | osIntegration.init(window);
75 |
76 | // Load components
77 | require('./components')();
78 |
79 | /**
80 | * Loads a page into browser window + passing Sentry configuration
81 | * @param {String} page Page to load
82 | * @return {Boolean} Returns true if succeed
83 | */
84 | const loadPage = page => {
85 |
86 | // Building absolute path to the requested page
87 | const pathToPage = path.resolve(__dirname, '../../build', page);
88 |
89 | // Is this page exists?
90 | if (!fs.existsSync(pathToPage))
91 | throw new Error(`Requested page ${page} was not found`);
92 |
93 | // Encoding the Sentry configuration
94 | let sentryConfiguration = {};
95 |
96 | // Cloning the original configuration
97 | Object.assign(sentryConfiguration, config.sentry);
98 |
99 | // Removing large but useless field which can potentialy broke Sentry on renderer
100 | delete sentryConfiguration.defaultIntegrations;
101 |
102 | // Apply user's decision on error reporting
103 | sentryConfiguration.enabled = config.sentry.enabled && userPreferences.get('errorReporting');
104 |
105 | // Encode to JSON, then encode to URIEncoded
106 | sentryConfiguration = encodeURIComponent(JSON.stringify(sentryConfiguration));
107 |
108 | // Load this page
109 | window.loadURL(`file://${pathToPage}?sentry=${sentryConfiguration}`);
110 | return true;
111 |
112 | };
113 |
114 | // Intercept external links navigation
115 | window.webContents.on('will-navigate', (event, url) => {
116 |
117 | // Preventing default behavior, so the link wouldn't be loaded
118 | // inside the tracker's window
119 | event.preventDefault();
120 |
121 | // Check is target protocol allowed
122 | const targetUrl = new URL(url);
123 | if (!WEBCONTENTS_ALLOWED_PROTOCOLS.has(targetUrl.protocol))
124 | return;
125 |
126 | // Open link in external browser
127 | shell.openExternal(url);
128 |
129 | });
130 |
131 | // Generate and inject CSP policy
132 | window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
133 |
134 | // Build a CSP and apply the default policy
135 | let cspValue = "default-src 'self';";
136 |
137 | // Apply assets policy
138 | cspValue += "style-src 'self' data: 'unsafe-inline'; font-src 'self' data:; img-src 'self' data:;";
139 |
140 | // Scripts: allow unsafe-eval in dev mode, otherwise Chrome DevTools wouldn't work
141 | if (config.isDeveloperModeEnabled)
142 | cspValue += "script-src 'self' 'unsafe-eval';";
143 | else
144 | cspValue += "script-src 'self';";
145 |
146 | // If Sentry is enabled, inject also a connect-src CSP allowing requests to Sentry host
147 | if (config.sentry.enabled && userPreferences.get('errorReporting')) {
148 |
149 | // Parse frontend's DSN to extract the host
150 | const frontendDsnUrl = new URL(config.sentry.dsnFrontend);
151 |
152 | // Inject connect-src policy allowing connections to self and Sentry hostname
153 | cspValue += `connect-src 'self' ${frontendDsnUrl.origin};`;
154 |
155 | }
156 |
157 | // Returning injection by callback
158 | callback({
159 | responseHeaders: {
160 | ...details.responseHeaders,
161 | 'Content-Security-Policy': cspValue,
162 | },
163 | });
164 |
165 | });
166 |
167 | // Re-render new page each time when it comes ready
168 | window.on('ready-to-show', () => {
169 |
170 | // Pass webContents to IPC
171 | router.setWebContents(window.webContents);
172 |
173 | // Show the main window
174 | window.show();
175 |
176 | });
177 |
178 | // Weird workaround for Linux (see https://github.com/electron/electron/issues/4544)
179 | window.setMenuBarVisibility(false);
180 |
181 | // Load frontend entry point
182 | loadPage('app.html');
183 |
184 | // Open DevTools if we're in development mode
185 | if (config.isDeveloperModeEnabled && !process.env.DISABLE_DEVTOOLS)
186 | window.webContents.openDevTools('detached');
187 |
188 |
189 | });
190 |
--------------------------------------------------------------------------------
/app/src/base/active-window.js:
--------------------------------------------------------------------------------
1 | const { EventEmitter } = require('events');
2 | const activeWindow = require('active-win');
3 | const Log = require('../utils/log');
4 |
5 | const log = new Log('ActiveWindow');
6 |
7 | /**
8 | * Polling interval
9 | * @type {number} Delay between polls in milliseconds
10 | */
11 | const ACTIVE_WINDOW_POLLING_INTERVAL = 5000;
12 |
13 | class ActiveWindow extends EventEmitter {
14 |
15 | constructor() {
16 |
17 | super();
18 |
19 | /**
20 | * Interval ID of polling timer
21 | * @type {Number|null}
22 | */
23 | this.pollingTimerId = null;
24 |
25 | /**
26 | * Current application parameters
27 | * @type {Object}
28 | */
29 | this.currentApplication = {
30 | executable: null,
31 | title: null,
32 | url: null,
33 | };
34 |
35 | }
36 |
37 | /**
38 | * Timer status
39 | * @type {boolean}
40 | */
41 | get active() {
42 |
43 | return this.pollingTimerId !== null;
44 |
45 | }
46 |
47 | /**
48 | * Starts active window polling
49 | * @returns {boolean} True if successfully started, False otherwise
50 | */
51 | start() {
52 |
53 | if (this.active)
54 | return false;
55 |
56 | this.pollingTimerId = setInterval(async () => {
57 |
58 | try {
59 |
60 | const window = await activeWindow();
61 |
62 | // Detect changes
63 | if (
64 | window && window.owner
65 | && (window.owner.path !== this.currentApplication.executable
66 | || window.title !== this.currentApplication.title
67 | || window.url !== this.currentApplication.url)
68 | )
69 | this.applyNewWindow(window);
70 |
71 | } catch (err) {
72 |
73 | log.error('Error occured during active window poll', err);
74 |
75 | }
76 |
77 | }, ACTIVE_WINDOW_POLLING_INTERVAL);
78 | return true;
79 |
80 | }
81 |
82 | /**
83 | * Stops the active window polling
84 | */
85 | stop() {
86 |
87 | if (!this.pollingTimerId)
88 | return;
89 |
90 | clearInterval(this.pollingTimerId);
91 | this.pollingTimerId = null;
92 |
93 | }
94 |
95 | /**
96 | * Update current window
97 | * @private
98 | * @param {Object} window New window definition
99 | */
100 | applyNewWindow(window) {
101 |
102 | this.currentApplication.executable = window.owner.path;
103 | this.currentApplication.title = window.title;
104 | this.currentApplication.url = window.url;
105 | this.emit('updated', this.currentApplication);
106 |
107 | }
108 |
109 | }
110 |
111 | module.exports = new ActiveWindow();
112 |
--------------------------------------------------------------------------------
/app/src/base/api.js:
--------------------------------------------------------------------------------
1 | const Cattr = require('@cattr/node');
2 | const keychain = require('../utils/keychain');
3 |
4 | const api = new Cattr();
5 |
6 | api.tokenProvider = {
7 |
8 | get: keychain.getSavedToken,
9 | set: keychain.saveToken,
10 |
11 | };
12 |
13 | api.credentialsProvider = {
14 |
15 | get: keychain.getSavedCredentials,
16 | set: keychain.saveCredentials,
17 |
18 | };
19 |
20 | module.exports = api;
21 |
--------------------------------------------------------------------------------
/app/src/base/config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const { resolve } = require('path');
3 | const { app } = require('electron');
4 | const argv = require('minimist')(process.argv.slice(2));
5 | const packageManifest = require('../../../package.json');
6 |
7 | // Development mode rules
8 | const isDeveloperModeEnabled = (
9 |
10 | // Specific environment variable
11 | process.env.AT_DEVMODE === 'meow'
12 |
13 | );
14 |
15 | /**
16 | * Identifier of the package used in the filesystem and keychain
17 | * @type {String}
18 | */
19 | const packageId = isDeveloperModeEnabled ? 'cattr-develop' : 'cattr';
20 |
21 | /**
22 | * Version of this Cattr package
23 | * @type {String}
24 | */
25 | const packageVersion = packageManifest.version;
26 |
27 | // Basic configuration
28 | const configuration = {
29 |
30 | packageId,
31 | packageVersion,
32 |
33 | // Application data directory
34 | appdata: isDeveloperModeEnabled ? app.getPath('userData').concat('-develop') : app.getPath('userData'),
35 |
36 | // Application codebase path
37 | apppath: app.getAppPath(),
38 |
39 | // Is we're currently in development mode?
40 | isDeveloperModeEnabled,
41 |
42 | };
43 |
44 | // Sentry error handling
45 | configuration.sentry = {
46 |
47 | // Is Sentry enabled?
48 | enabled: !isDeveloperModeEnabled || process.env.AT_SENTRY === 'force',
49 |
50 | // Main application DSN
51 | dsn: 'https://b0ab7e30102244948431ecf5b1eb9c9a@sentry.amazingcat.net/15',
52 |
53 | // Frontend application DSN
54 | dsnFrontend: 'https://00bd1ee1db824310812252bb96e96945@sentry.amazingcat.net/14',
55 |
56 | // Setting the current release
57 | release: `cattr@${packageVersion}`,
58 |
59 | };
60 |
61 | // Ensure that application data directory actually exists
62 | if (!fs.existsSync(configuration.appdata))
63 | fs.mkdirSync(configuration.appdata);
64 |
65 | // Ensure that database directory actually exists
66 | if (!fs.existsSync(resolve(configuration.appdata, 'db')))
67 | fs.mkdirSync(resolve(configuration.appdata, 'db'));
68 |
69 | // Database configuration
70 | configuration.localDB = {
71 |
72 | // Sequelize options
73 | opts: {
74 |
75 | // Set SQLite as dialect
76 | dialect: 'sqlite',
77 |
78 | // Set database path
79 | storage: resolve(configuration.appdata, 'db', 'main.db'),
80 |
81 | // Disable SQL queries logging by default, enable via flag
82 | logging: (argv['debug-sql-logging'] === 'true'),
83 |
84 | },
85 |
86 | };
87 |
88 | // Logger settings
89 | configuration.logger = {
90 |
91 | // Logs directory
92 | directory: argv['logs-directory'] || resolve(configuration.appdata, 'logs'),
93 |
94 | };
95 |
96 | // Credentials storage
97 | configuration.credentialsStore = {
98 |
99 | // Service identifier
100 | service: `amazingcat/${packageId}`,
101 |
102 | };
103 |
104 | // User Preferences file
105 | configuration.userPreferences = {
106 |
107 | file: `${configuration.appdata}/at-user-preferences.json`,
108 |
109 | };
110 |
111 | // Usage statistics reporter
112 | configuration.usageStatistics = {
113 |
114 | /**
115 | * Report usage statistics
116 | * Notice that even if reporting is enabled here, priority is on User Preferences parameter.
117 | * In other words, even if stats is enabled here, user's decision in Settings is more important.
118 | */
119 | enabled: true,
120 |
121 | /**
122 | * Base URL of statistics collector
123 | */
124 | collectorUrl: 'https://stats.cattr.app',
125 |
126 | };
127 |
128 | // New release available notification
129 | configuration.updateNotification = {
130 |
131 | /**
132 | * Enables update notification mechanism
133 | * Notice that user preference "DISABLE" is overriding this value
134 | * @type {Boolean}
135 | */
136 | enabled: (!process.windowsStore && !process.mas),
137 |
138 | /**
139 | * Base URL to retrieve releases manifest
140 | * @type {String}
141 | */
142 | manifestBaseUrl: 'https://git.amazingcat.net/api/v4/projects/353/releases/permalink/latest',
143 |
144 | /**
145 | * URL to downloads page
146 | * @type {String}
147 | */
148 | downloadsPageUrl: 'https://cattr.app/desktop',
149 |
150 | };
151 |
152 | // Export configuration
153 | module.exports = configuration;
154 |
--------------------------------------------------------------------------------
/app/src/base/deferred-handler.js:
--------------------------------------------------------------------------------
1 | const IntervalsController = require('../controller/time-intervals');
2 | const TimeIntervalModel = require('../models').db.models.Interval;
3 | const Log = require('../utils/log');
4 | const OfflineMode = require('./offline-mode');
5 | const TaskTracker = require('./task-tracker');
6 |
7 | const log = new Log('DeferredHandler');
8 |
9 | /**
10 | * If deferred intervals push procedure is already running,
11 | * we should lock this from more executions to avoid collisions
12 | * @type {Boolean}
13 | */
14 | let threadLock = false;
15 |
16 | /**
17 | * Pushes deferred intervals
18 | * @return {Promise} True, if everything is pushed, error otherwise
19 | */
20 | const deferredIntervalsPush = async () => {
21 |
22 | // Check thread lock
23 | if (threadLock)
24 | return;
25 |
26 | // Locking thread
27 | threadLock = true;
28 |
29 | // Getting all deferred intervals
30 | const deferredIntervals = await TimeIntervalModel.findAll({ where: { synced: false } });
31 |
32 | // Skip sync routine if there are no deffered intervals
33 | if (deferredIntervals.length === 0) {
34 |
35 | threadLock = false;
36 | return;
37 |
38 | }
39 |
40 | // Catching all the deferred intervals errors
41 | try {
42 |
43 | // Iterating over deferred intervals
44 | // eslint-disable-next-line no-restricted-syntax
45 | for await (const rawInterval of deferredIntervals) {
46 |
47 | // Prepare interval structure
48 | /* eslint camelcase: 0 */
49 | const preparedInterval = {
50 |
51 | _isDeferred: true,
52 | task_id: rawInterval.taskId,
53 | start_at: rawInterval.startAt,
54 | end_at: rawInterval.endAt,
55 | user_id: rawInterval.userId,
56 | activity_fill: rawInterval.systemActivity,
57 |
58 | };
59 |
60 | if (rawInterval.mouseActivity)
61 | preparedInterval.mouse_fill = rawInterval.mouseActivity;
62 |
63 | if (rawInterval.keyboardActivity)
64 | preparedInterval.keyboard_fill = rawInterval.keyboardActivity;
65 |
66 | // Push deferred interval
67 | let res = null;
68 |
69 | if (rawInterval.screenshot)
70 | res = await IntervalsController.pushTimeInterval(preparedInterval);
71 | else
72 | res = await IntervalsController.pushTimeInterval(preparedInterval, rawInterval.screenshot);
73 |
74 | log.debug(`Deferred interval (${res.response.data.id}) has been pushed`);
75 |
76 | // Update interval status
77 | rawInterval.synced = true;
78 | rawInterval.remoteId = res.response.data.id;
79 | await rawInterval.save();
80 |
81 | // Will trigger 'Not Synced Intervals' count
82 | TaskTracker.emit('interval-pushed', {
83 | deferred: true
84 | });
85 |
86 | // Remove old intervals from queue
87 | await IntervalsController.reduceSyncedIntervalQueue();
88 |
89 | }
90 |
91 | // That's all
92 | log.debug('Deferred screenshots queue is empty, nice work');
93 |
94 | } catch (error) {
95 |
96 | // Handle connectivity errors
97 | if (error.request && !error.response) {
98 |
99 | // Trigger offline mode then exit
100 | OfflineMode.trigger();
101 |
102 | // Unlocking thread
103 | threadLock = false;
104 |
105 | // Ignore this error
106 | return;
107 |
108 | }
109 |
110 | // Log other errors
111 | log.warning(`Error occured during deferred intervals push: ${error}`);
112 | throw error;
113 |
114 | }
115 |
116 | // Unlocking thread
117 | threadLock = false;
118 |
119 | };
120 |
121 | // Push deferred intervals if connection is restored
122 | OfflineMode.on('connection-restored', deferredIntervalsPush);
123 | OfflineMode.once('connection-ok', deferredIntervalsPush);
124 |
--------------------------------------------------------------------------------
/app/src/base/offline-mode.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events');
2 | const auth = require('./authentication');
3 | const heartbeatMonitor = require('../utils/heartbeat-monitor');
4 | const Log = require('../utils/log');
5 |
6 | const log = new Log('OfflineMode');
7 |
8 | class OfflineModeHandler extends EventEmitter {
9 |
10 | /**
11 | * Returns status of offline mode (true if enabled)
12 | * @return {Boolean} Is offline mode enabled?
13 | */
14 | get enabled() {
15 |
16 | return this._isEnabled;
17 |
18 | }
19 |
20 | /**
21 | * Builds the class
22 | */
23 | constructor() {
24 |
25 | super();
26 |
27 | /**
28 | * Status of the offline mode
29 | * @type {Boolean}
30 | */
31 | this._isEnabled = false;
32 |
33 | /**
34 | * Identifier of the server ping timer
35 | * @type {Number|null}
36 | */
37 | this._pingTimer = null;
38 |
39 | /**
40 | * Check server connectivity each 30 sec
41 | * @type {Number}
42 | * @constant
43 | */
44 | this._PING_TIMER_INTERVAL = 30 * 1000;
45 |
46 | /**
47 | * Is tracker active right now?
48 | * @type {Boolean}
49 | */
50 | this.trackingActive = false;
51 |
52 | }
53 |
54 | /**
55 | * Set current tracker status
56 | * @param {Boolean} status
57 | */
58 | setTrackerStatus(status) {
59 |
60 | this.trackingActive = Boolean(status);
61 |
62 | }
63 |
64 | /**
65 | * Triggers offline mode on
66 | */
67 | trigger() {
68 |
69 | // Ignore repeats
70 | if (this._isEnabled)
71 | return;
72 |
73 | // Turn offline mode on
74 | log.debug(`Offline mode is triggered, checking connectivity each ${this._PING_TIMER_INTERVAL} ms`);
75 | this._isEnabled = true;
76 |
77 | // Notify that offline mode is enabled
78 | this.emit('offline');
79 |
80 | // Stop heartbeating to backend when tracker is paused
81 | heartbeatMonitor.stop();
82 |
83 | // Set connectivity check timer
84 | this._pingTimer = setInterval(async () => {
85 |
86 | // Check connectivity
87 | const serverStatus = await auth.ping();
88 |
89 | // Connection is not restored
90 | if (!serverStatus)
91 | return log.debug('Connection still not restored');
92 |
93 | // Call offline mode restore routine
94 | return this.restore();
95 |
96 | }, this._PING_TIMER_INTERVAL);
97 |
98 | }
99 |
100 |
101 | /**
102 | * Cancels offline mode
103 | */
104 | async restore() {
105 |
106 | // Notify about connection OK status
107 | this.emit('connection-ok');
108 |
109 | if (!this._isEnabled)
110 | return;
111 |
112 | // Connection restored
113 | log.debug('Connection restored');
114 |
115 | // Reset timer
116 | clearInterval(this._pingTimer);
117 | this._pingTimer = null;
118 |
119 | // Reset state
120 | this._isEnabled = false;
121 |
122 | // Emit reconnection event
123 | this.emit('connection-restored');
124 |
125 | }
126 |
127 | /**
128 | * Disarm offline mode if server is available
129 | */
130 | async restoreWithCheck() {
131 |
132 | const connectivityEstablished = await auth.ping();
133 |
134 | // Notify about connection OK status
135 | if (connectivityEstablished)
136 | this.emit('connection-ok');
137 |
138 | // Restore OfflineMode status if we're offline,
139 | // but connection is established during the latest ping
140 | if (this._isEnabled && connectivityEstablished)
141 | this.restore();
142 |
143 | // If the tracking is enabled and the connection is restored, we start the heartbeat again
144 | if (this.heartbeatEnabled && connectivityEstablished)
145 | heartbeatMonitor.start();
146 |
147 | }
148 |
149 | }
150 |
151 | /**
152 | * Exports single instance of OfflineModeHander
153 | * @type {OfflineModeHandler}
154 | */
155 | module.exports = new OfflineModeHandler();
156 |
--------------------------------------------------------------------------------
/app/src/base/translation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Translations support
3 | */
4 |
5 | const { EventEmitter } = require('events');
6 | const Log = require('../utils/log');
7 | const translationsLoader = require('../utils/translations');
8 | const userPreferences = require('./user-preferences');
9 |
10 | const log = new Log('Translation');
11 |
12 | /**
13 | * Translations
14 | */
15 | class Translation extends EventEmitter {
16 |
17 | /**
18 | * Initialises translations for i18next
19 | */
20 | constructor() {
21 |
22 | super();
23 |
24 | /**
25 | * Available translation resources
26 | * @type {Object}
27 | */
28 | this._resources = translationsLoader.resources;
29 |
30 | /**
31 | * Available languages
32 | * @type {Object}
33 | */
34 | this._languages = translationsLoader.languages;
35 |
36 | /**
37 | * Default fallback language
38 | * @type {String}
39 | */
40 | this.DEFAULT_FALLBACK_LANG = 'en';
41 |
42 | /**
43 | * Currently selected language
44 | * @type {String}
45 | */
46 | this._currentLanguage = userPreferences.get('language');
47 |
48 | // Check availability of the selected language
49 | if (typeof this._resources[this._currentLanguage] === 'undefined') {
50 |
51 | // Check availability for the fallback language
52 | if (typeof this._resources[this.DEFAULT_FALLBACK_LANG] === 'undefined') {
53 |
54 | // Fallback language is not available, select first available lang
55 | [this._currentLanguage] = Object.keys(this._resources);
56 |
57 | // Leave a message
58 | log.error('TRS01', `Falling back to first available language, because selected & fallback are not available: ${this._currentLanguage}`);
59 |
60 | } else {
61 |
62 | // Log entry
63 | log.error('TRS02', `Falling back to fallback language because selected language is not available: ${this._currentLanguage}`);
64 |
65 | // Select fallback language
66 | this._currentLanguage = this.DEFAULT_FALLBACK_LANG;
67 |
68 | }
69 |
70 | }
71 |
72 | // Subscribe user preferences "commited" event
73 | userPreferences.on('commited', () => {
74 |
75 | // Update current language if user preferences changed & saved
76 | this.updateLanguage();
77 |
78 | });
79 |
80 | }
81 |
82 | /**
83 | * Get translation by the key
84 | * @param {String} key Translation Resource key
85 | * @return {String} Translation
86 | */
87 | translate(key) {
88 |
89 | if (Object.prototype.hasOwnProperty.call(this._resources[this._currentLanguage].translation, key))
90 | return this._resources[this._currentLanguage].translation[key];
91 | return key;
92 |
93 | }
94 |
95 | /**
96 | * Returns map of available languages
97 | * @return {Object} Map between full language title and code
98 | */
99 | getLanguages() {
100 |
101 | return this._languages;
102 |
103 | }
104 |
105 | /**
106 | * Return configuration for i18next
107 | * @return {Object} i18next configuration
108 | */
109 | getConfiguration() {
110 |
111 | return { lng: this._currentLanguage, resources: this._resources };
112 |
113 | }
114 |
115 | /**
116 | * Updates current language
117 | * @param {String|null} language Language to set
118 | * If null / empty argument passed, language will be pulled from user preferences
119 | */
120 | updateLanguage(language = null) {
121 |
122 | // Get new language from args or userPreferences
123 | const newLanguage = language || userPreferences.get('language');
124 | if (this._currentLanguage !== newLanguage) {
125 |
126 | // Update current language
127 | this._currentLanguage = newLanguage;
128 |
129 | // Emit "lanuage-changed" event
130 | this.emit('language-changed', { newLanguage, translation: this });
131 |
132 | }
133 |
134 | }
135 |
136 | }
137 |
138 | // Initialize Translation module and export ready-to-use instance
139 | module.exports = new Translation();
140 |
--------------------------------------------------------------------------------
/app/src/base/update.js:
--------------------------------------------------------------------------------
1 | const { EventEmitter } = require('events');
2 | const axios = require('axios');
3 | const semver = require('semver');
4 | const Log = require('../utils/log');
5 | const configuration = require('./config');
6 | const userPreferences = require('./user-preferences');
7 |
8 | const log = new Log('Update');
9 |
10 | /**
11 | * @typedef {Object} UpdateData
12 | * @property {String} version Update version
13 | * @property {String} current Current installed version
14 | * @property {String} downloadsPageUrl URL of downloads page
15 | */
16 |
17 | /**
18 | * Self-update actions
19 | */
20 | class Update extends EventEmitter {
21 |
22 | /**
23 | * Retrieve update data from DL server
24 | * @async
25 | * @returns {Promise.} Object with update info if newer version is available, null otherwise
26 | */
27 | async retrieveUpdate() {
28 |
29 | // Check is update notifier functionality is enabled in App configuration
30 | if (!configuration.updateNotification.enabled)
31 | return null;
32 |
33 | // Check is update notification is not disabled by user
34 | if (!userPreferences.get('updateNotification'))
35 | return null;
36 |
37 | // Obtain platform
38 | let platform = null;
39 | switch (process.platform) {
40 |
41 | case 'win32': platform = 'windows'; break;
42 | case 'darwin': platform = 'mac'; break;
43 | case 'linux': platform = 'linux'; break;
44 | default:
45 | log.debug(`unsupported architecture: ${process.platform}`);
46 | return null;
47 |
48 | }
49 |
50 | try {
51 |
52 | // Retrieve manifest, then check its structure
53 | const manifestReq = await axios.get(configuration.updateNotification.manifestBaseUrl);
54 | if (manifestReq.status !== 200 || !manifestReq.data?.name)
55 | return null;
56 |
57 | const manifestVersion = manifestReq.data.name.replace('v', '');
58 |
59 | // Filer incorrect, older, or current version
60 | if (configuration.packageVersion === 'dev' || !semver.valid(manifestVersion) || semver.lte(manifestVersion, configuration.packageVersion))
61 | return null;
62 |
63 | const updateData = {
64 | version: manifestVersion,
65 | current: configuration.packageVersion,
66 | downloadsPageUrl: configuration.updateNotification.downloadsPageUrl,
67 | };
68 |
69 | this.emit('update-available', updateData);
70 | return updateData;
71 |
72 | } catch (err) {
73 |
74 | log.error('Error occured during update manifest retrival', err);
75 | return null;
76 |
77 | }
78 |
79 | }
80 |
81 | }
82 |
83 | module.exports = new Update();
84 |
--------------------------------------------------------------------------------
/app/src/components/application-menu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Application menu
3 | */
4 |
5 | const { Menu } = require('electron');
6 | const osIntegration = require('../base/os-integration');
7 |
8 | /**
9 | * Template buffer
10 | * @type {Array