Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dd15ed064 | ||
|
|
d169434157 | ||
|
|
6efed72bf2 | ||
|
|
15db311801 | ||
|
|
82d509129b | ||
|
|
939d838e35 | ||
|
|
6de4ce58c4 | ||
|
|
ee66196d9a | ||
|
|
3c13a9e837 | ||
|
|
22b23a9ece | ||
|
|
4ae385b7fc | ||
|
|
d9abc7f0b2 | ||
|
|
1f19b2528a | ||
|
|
1f11c39773 | ||
|
|
b6ccb5927a | ||
|
|
1d465ee318 | ||
|
|
3ab48b1cf5 | ||
|
|
de4ac978bd | ||
|
|
8df5faf4ef | ||
|
|
99a2035fdb | ||
|
|
a0d61728ea | ||
|
|
312bf6102c | ||
|
|
f4885b6f1c | ||
|
|
ca63a0979e | ||
|
|
1f266c3353 | ||
|
|
c7b2a70312 | ||
|
|
255133af4d | ||
|
|
db74531cd4 | ||
|
|
f36d8bfc78 | ||
|
|
3b31ace726 | ||
|
|
6badfef9da | ||
|
|
8c38dd73d8 | ||
|
|
b8682a852a | ||
|
|
8c17929151 | ||
|
|
bb92c9ec28 | ||
|
|
b4aacea2a3 | ||
|
|
8bbe6ebf75 | ||
|
|
a23d28f2cf | ||
|
|
f51e8859ee | ||
|
|
d235dee955 | ||
|
|
2afef4ea3d | ||
|
|
f709d6a566 | ||
|
|
532915b95d | ||
|
|
456e40bedf | ||
|
|
018b7e270f | ||
|
|
f8b8b4c97a | ||
|
|
978d7d0863 | ||
|
|
594a14d6ca | ||
|
|
c628aa9aef | ||
|
|
3060ebf79c | ||
|
|
1a34c7374b | ||
|
|
c262681011 | ||
|
|
f8ba5c46e4 | ||
|
|
b789250cb8 | ||
|
|
5df458739d | ||
|
|
d6fa9a2a03 | ||
|
|
ec15af01dd | ||
|
|
7073336f81 | ||
|
|
3d3380f27e | ||
|
|
c69998c7cb | ||
|
|
1663bfeac7 | ||
|
|
afc3bfb3b8 | ||
|
|
b6bfc25727 | ||
|
|
7fac0fe79f | ||
|
|
5967347b80 | ||
|
|
855a445c1a | ||
|
|
1cc12195a3 | ||
|
|
66d4d39d5b | ||
|
|
a5dbe66fc1 | ||
|
|
4c8be45e28 | ||
|
|
6049a2b834 | ||
|
|
831c014f97 | ||
|
|
5c15a7a64a | ||
|
|
e43992b278 | ||
|
|
5963d2bac5 | ||
|
|
45816a2bcc | ||
|
|
60a5a2aa40 | ||
|
|
04b9b59e14 | ||
|
|
4ff4b52202 | ||
|
|
73c426844a | ||
|
|
25ce236a77 | ||
|
|
9b20175545 | ||
|
|
92a7118d1c | ||
|
|
a9f9e90443 | ||
|
|
31d2067f07 | ||
|
|
dd7e1416b4 | ||
|
|
1d5225de07 | ||
|
|
9c063d59de | ||
|
|
57028987f1 | ||
|
|
9e722ddfb3 | ||
|
|
646a8e0b9f | ||
|
|
f27a2871d7 | ||
|
|
909c44d72a | ||
|
|
53cf595e17 | ||
|
|
2c4eb78c3f | ||
|
|
d7a1325c0b | ||
|
|
30efbcc621 | ||
|
|
628c16eb95 | ||
|
|
d8552c62ff | ||
|
|
df84083536 | ||
|
|
461a4f122b | ||
|
|
c937af8ee7 | ||
|
|
636d4279c8 |
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist_electron
|
||||||
|
.vs
|
||||||
|
.vscode
|
||||||
|
.github
|
||||||
|
.git
|
||||||
|
docs
|
||||||
|
docker
|
||||||
7
.github/workflows/bump-and-release.yaml
vendored
@@ -10,9 +10,8 @@ jobs:
|
|||||||
if: github.event.base_ref == 'refs/heads/master'
|
if: github.event.base_ref == 'refs/heads/master'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
- uses: undergroundwires/bump-everywhere@master
|
||||||
uses: undergroundwires/bump-everywhere@master
|
|
||||||
with:
|
with:
|
||||||
user: undergroundwires-bot
|
user: undergroundwires-bot
|
||||||
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
|
release-token: ${{ secrets.BUMP_GITHUB_PAT }} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
|
||||||
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.
|
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.
|
||||||
|
|||||||
32
.github/workflows/deploy-desktop.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Deploy desktop
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-desktop-app:
|
||||||
|
name: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||||
|
fetch-depth: 0 # fetch all history
|
||||||
|
- name: Checkout to bump commit
|
||||||
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:unit
|
||||||
|
- name: Publish desktop app
|
||||||
|
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Build & deploy
|
name: Deploy site
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created] # will be triggered when a NON-draft release is created and published.
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-deploy:
|
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
node-version: '14.x'
|
node-version: '14.x'
|
||||||
-
|
-
|
||||||
name: "App: Install dependencies"
|
name: "App: Install dependencies"
|
||||||
run: npm install
|
run: npm ci
|
||||||
working-directory: site
|
working-directory: site
|
||||||
-
|
-
|
||||||
name: "App: Run tests"
|
name: "App: Run tests"
|
||||||
@@ -114,4 +114,4 @@ jobs:
|
|||||||
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
|
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
|
||||||
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
|
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
|
||||||
--region us-east-1 \
|
--region us-east-1 \
|
||||||
--profile user --session ${{ env.SESSION_NAME }}
|
--profile user --session ${{ env.SESSION_NAME }}
|
||||||
39
.github/workflows/quality-checks.yaml
vendored
@@ -1,37 +1,26 @@
|
|||||||
name: Quality checks
|
name: Quality checks
|
||||||
|
|
||||||
on:
|
on: [ push, pull_request ]
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
lint-command:
|
||||||
|
- npm run lint:vue
|
||||||
|
- npm run lint:yaml
|
||||||
|
- npm run lint:md
|
||||||
|
- npm run lint:md:relative-urls
|
||||||
|
- npm run lint:md:consistency
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Checkout
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
-
|
- name: Setup node
|
||||||
name: Setup node
|
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 14.x
|
||||||
-
|
- name: Install dependencies
|
||||||
name: Install dependencies
|
|
||||||
run: npm ci
|
run: npm ci
|
||||||
-
|
- name: Lint
|
||||||
name: Lint vue
|
run: ${{ matrix.lint-command }}
|
||||||
run: npm run lint:vue
|
|
||||||
-
|
|
||||||
name: Lint yaml
|
|
||||||
run: npm run lint:yaml
|
|
||||||
-
|
|
||||||
name: 'Validate md: Relative URLs'
|
|
||||||
run: npm run lint:md:relative-urls
|
|
||||||
-
|
|
||||||
name: 'Validate md: Enforce standards'
|
|
||||||
run: npm run lint:md
|
|
||||||
-
|
|
||||||
name: 'Validate md: Ensure consistency'
|
|
||||||
run: npm run lint:md:consistency
|
|
||||||
|
|||||||
3
.github/workflows/security-checks.yaml
vendored
@@ -1,9 +1,8 @@
|
|||||||
name: Security checks
|
name: Security checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0'
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/test.yaml
vendored
@@ -1,9 +1,6 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on:
|
on: [ push, pull_request ]
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
|
|||||||
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
|
#Electron-builder output
|
||||||
|
/dist_electron
|
||||||
185
CHANGELOG.md
@@ -1,31 +1,174 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.4.5 (2020-06-13)
|
## 0.7.4 (2020-09-12)
|
||||||
|
|
||||||
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a27ca65b9eb29f009553e047bfb52f4200c0bd17)
|
* fix checked checkbox has blue border | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0)
|
||||||
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/49249399fe89575ac26b26a2b063c50b29b2f554)
|
* fix spectre protection getting single lined #31 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a)
|
||||||
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ff334704466a1d8e04660ac61da777681ebb93e5)
|
* fix missing reg value in denying app access to account | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb)
|
||||||
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/690f34686f4d35f09e78e5870447b09d7dfaa91b)
|
* fix wrong path in clear all firefox user profile settings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0)
|
||||||
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fb40bc571becf4e97d782af7d0de311cab37cec2)
|
|
||||||
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e07609779b605ce7c80f75f186e4bacff60e8861)
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4)
|
||||||
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7b033f8d5b0e086c6076c1db3391144ae14ad296)
|
|
||||||
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4a91e8ccd8a707bc6bea34ee28cff7fa4f66ee2f)
|
## 0.7.3 (2020-09-12)
|
||||||
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0d2efe5b05aa965458b78b8fa43754ce2f4fe11b)
|
|
||||||
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e2ab124fb799f56ada3570fdc911361cb803e889)
|
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
|
||||||
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/bb98d20637cbf1d524ebb2973e308773006e3153)
|
* fix nvidia tweak error message, categorize and add reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99a2035fdb0766a4dfc2753133eab0d7666516cd)
|
||||||
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c668a97950a1cb7c8bf2a7fd8a72d1101e65e8ce)
|
* improve CPU specific tweaks by conditional platform checks and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8df5faf4ef05a49da63973bd0fbb5c5d07d5bd93)
|
||||||
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aab8f21a8d8dbed54798af581e6e1ad9e86a4be1)
|
* fix wrong path to the main telemetry file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/de4ac978bdda79573b36d355697b8a028d2c0beb)
|
||||||
|
* fix naming of firefox cleanup to mention profiles | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3ab48b1cf5f7f934f07e468ef2318ccee07f530c)
|
||||||
|
* add reversibility and more scripts to denying app access with better structure | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d465ee3189d0e5a827453b3f0eb4361efe23770)
|
||||||
|
* fix comment lines are being detected as duplicate in validation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6ccb5927a20412976a54fd2215eb645092f98a8)
|
||||||
|
* add more detailed error message | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f11c39773c12eccfb3efb898b58c2f6f37ab9ca)
|
||||||
|
* fix typo in a test | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f19b2528a69383e63e579d2885f01cd804abf6c)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.2...0.7.3)
|
||||||
|
|
||||||
|
## 0.7.2 (2020-09-06)
|
||||||
|
|
||||||
|
* update onesync documentation and do not recommend it as it breaks other apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f36d8bfc7848bb65ac0c641e318a689bf3816ccf)
|
||||||
|
* add reversibility for biometric disabling and do not recommend it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/db74531cd4139615c6d595959217d3651f099019)
|
||||||
|
* fix bad highlighting of selected nodes when using keyboard navigation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/255133af4dfae40171406648a3e2920f16d71cb3)
|
||||||
|
* add reversibility to removing bloatware | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c7b2a703128470a05f12c9c6e8002444def37ef8)
|
||||||
|
* fix indeterminate state being lost | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1f266c33535f72b69c65985bf2eff27cd2c5a104)
|
||||||
|
* fix wording in default text in text area | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ca63a0979ef55d07d09d9443e5cea9aa888870a5)
|
||||||
|
* add best practice suggestion to come back | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f4885b6f1c82752f2143934e336d6d1b1af03015)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.1...0.7.2)
|
||||||
|
|
||||||
|
## 0.7.1 (2020-09-04)
|
||||||
|
|
||||||
|
* fix some browsers (including firefox) downloading the script as a text file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c17929151f9c4fa5f48564492bbf400ced95eea)
|
||||||
|
* rename screenshot image file | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b8682a852a14ed6cf49986695d9510b840ac9d3d)
|
||||||
|
* fix new/changed script higlighting not working on production builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8c38dd73d8c7b77d8d341c0389f4d7229f9b97fd)
|
||||||
|
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6badfef9daace0c5de3fd33652a82bfe22261b11)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.0...0.7.1)
|
||||||
|
|
||||||
|
## 0.7.0 (2020-09-02)
|
||||||
|
|
||||||
|
* [search] better (multilined) message when there are no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ec15af01dd020b364c2174fe562fd66227c2320c)
|
||||||
|
* [search] added clear/close button | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d6fa9a2a03c0ebe68b94f0b80cc52b4e200c9213)
|
||||||
|
* move script generation to /generation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5df458739d076719e350ba194c4f3f772884fcdb)
|
||||||
|
* add auto-highlighting of selected/updated code | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b789250cb89e2130b08e1a927df8181cf945dfeb)
|
||||||
|
* prompt admin priviliges automatically | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8ba5c46e4923d9c35f200f8a08aa6437f7c0ecc)
|
||||||
|
* add removal of ghost (default0) telemetry user | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c262681011f39b4412669b6cf233476f676ca550)
|
||||||
|
* add more windows defender tweaks, categorization and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a34c7374ba56bafa0209bbb55c81b233bb419ed)
|
||||||
|
* fix NTP script documentation is on wrong place | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3060ebf79cf242370433495cc3e1878b7581b202)
|
||||||
|
* updated dependencies to latest and audit fixes (#25) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c628aa9aef8ab7c815661d3c1711e7fbc65c69a2)
|
||||||
|
* categorize, fix and extend windows log files cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/594a14d6ca76cbd27a21877b8c373c1930589ca6)
|
||||||
|
* add more OneDrive cleanup scripts and categorize them | [commit](https://github.com/undergroundwires/privacy.sexy/commit/978d7d08638dd161082f239ed088b12302f29458)
|
||||||
|
* add disabling firefox telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f8b8b4c97ab734d5ba7370894b694993924388da)
|
||||||
|
* add disabling ccleaner telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/018b7e270f207aac926cb12f8069ebfcdce193ce)
|
||||||
|
* Add disabling of PowerShell 7+ telemetry (#29) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/456e40bedf9afcc846f9b13f1ea144cef6115cf6)
|
||||||
|
* categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/532915b95da9fecd6b981d91bf489359e4e53caa)
|
||||||
|
* fix "Configure Defender" being in wrong category #28 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f709d6a566ed7846b677b383863deda9680a2a9c)
|
||||||
|
* do not hardcode capability versions and make them reversible | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2afef4ea3d0d3d09aa1fa1eedba8493680bd8f10)
|
||||||
|
* exclude paint, wordpad and notepad from bloatware removal | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d235dee95514a01745aef9479d07f88ffb4b40b8)
|
||||||
|
* add reversibility on category level | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f51e8859eeb32c944126d692cfe03a0320c8b568)
|
||||||
|
* refactor unused imports & variables | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a23d28f2cfa2d64d45460697cf5ee9d6b5920752)
|
||||||
|
* fix search (got broken in b789250) with tests and refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8bbe6ebf750f1a1cbab493fb99b5ea91f4e21609)
|
||||||
|
* update the screenshot to show off highlighting | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b4aacea2a3e0bbcf2d8a79ff67f51c0f19e888a6)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.2...0.7.0)
|
||||||
|
|
||||||
|
## 0.6.2 (2020-08-16)
|
||||||
|
|
||||||
|
* 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b)
|
||||||
|
* 🐛 fixed blank screen and icons on mac | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7fac0fe79f252e8f9dda4f6f83cd6fa4ba2b539f)
|
||||||
|
* 🐛 fixed removing onedrive does not delete scheduled tasks | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6bfc2572740c0cd46d3bc0058fa767dd5fa862e)
|
||||||
|
* ⚙️ enhanced tweak to disable for office telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/afc3bfb3b8896f332c9a196973ded3dce8fd21e4)
|
||||||
|
* ✨ added script to clear dotnet telemery | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1663bfeac7b6580b1335ca5fcf3587b69c080c72)
|
||||||
|
* 🐛 fixed changing time server not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c69998c7cb29ffcf40f0af03b73150736581da69)
|
||||||
|
* 🔥 removed disabling ClickToRun as it breaks office | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3d3380f27ebeea53f17f49974aaa89300ffaf2dd)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.1...0.6.2)
|
||||||
|
|
||||||
|
## 0.6.1 (2020-08-09)
|
||||||
|
|
||||||
|
* updated documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5963d2bac551083f9d16cce6b851abf0e8b88ce7)
|
||||||
|
* fixed typo in footer | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5c15a7a64aaf24578a32713dec491bf494216303)
|
||||||
|
* more scripts can be reverted | [commit](https://github.com/undergroundwires/privacy.sexy/commit/831c014f977515454ee6dc664d77a8c434495501)
|
||||||
|
* moved windows connect now to security & recommended | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6049a2b834d8d17af741f8d8f8b07cd15153b001)
|
||||||
|
* fixed mac / linux download links | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c8be45e287b5ea009d6f828f7f327f37850569e)
|
||||||
|
* tweaks to disable webcam, speech and compatibility telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a5dbe66fc175e39397f296ab2ff703e9b0ab4d7c)
|
||||||
|
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/66d4d39d5bf3db305450514c6b6224654dafbfb2)
|
||||||
|
* fixed removing onedrive does not clean start menu / quick access | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1cc12195a3e9a11c590d3ed64d80299b50f74838)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.0...0.6.1)
|
||||||
|
|
||||||
|
## 0.6.0 (2020-07-26)
|
||||||
|
|
||||||
|
* fixed dead links in documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/25ce236a7737decaf2eb9b8c29a4c4f34d43f770)
|
||||||
|
* runs tests on each push on the repository | [commit](https://github.com/undergroundwires/privacy.sexy/commit/73c426844a0330718a9ab7de12b61ca05e853323)
|
||||||
|
* code area now shows "how" before "why" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ff4b52202b1c5dbfe2b80580bbe7d93132ab05c)
|
||||||
|
* support for desktop versions #20 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/04b9b59e14766ccd251474ad3710baf1f682fd49)
|
||||||
|
* reworked on footer & removed github icon | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60a5a2aa4026d384bef9e6a203f1b7514a269c33)
|
||||||
|
* updated dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45816a2bccb3d11a50e3f2bc19c0a6cc2587deaa)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.5.0...0.6.0)
|
||||||
|
|
||||||
|
## 0.5.0 (2020-07-19)
|
||||||
|
|
||||||
|
* added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9)
|
||||||
|
* search placeholder shows total scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d5225de07186f853f4cf7aedd4998f5d00c107a)
|
||||||
|
* do not collapse card when on "Search" and "Select" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/dd7e1416b4df54bf71b719d4654db88769dc0994)
|
||||||
|
* opening a card scrolls to its content div | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31d2067f076c3159483baec49975617dddbd158d)
|
||||||
|
* all cards in same line now have same height | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a9f9e9044385d9aed3b5551fc6c6823e813fd1e5)
|
||||||
|
* patched loadash vulnerability (#18) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92a7118d1c5013312772e075b9ee5a79c93710b8)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.10...0.5.0)
|
||||||
|
|
||||||
|
## 0.4.10 (2020-07-15)
|
||||||
|
|
||||||
|
* fixed script errors & added tests | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9e722ddfb3825fb29d6298025baaaa033120d017)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10)
|
||||||
|
|
||||||
|
## 0.4.9 (2020-07-14)
|
||||||
|
|
||||||
|
* disable office telemetry Disassembler0/Win10-Initial-Setup-Script#288 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/53cf595e1726ee3de79137fd566978fd512d218f)
|
||||||
|
* updated to may 2020 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/909c44d72a4a602ee8f27d06b6ec706c1e432ce1)
|
||||||
|
* simplified docker builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f27a2871d74e5117fc029be82caef12246e10879)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.8...0.4.9)
|
||||||
|
|
||||||
|
## 0.4.8 (2020-07-11)
|
||||||
|
|
||||||
|
* added more scripts #16 (#17) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d8552c62ffea13ce62abce836c7dd4980eef6bb9)
|
||||||
|
* stopping services before disabling #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/628c16eb952495f5b3f6d794161b355f4b08b819)
|
||||||
|
* can disable features, capabilities & remove onedrive #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/30efbcc621eb83dd5a9c1e66b8f1f5350eb95006)
|
||||||
|
* updated one more typo (#19) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d7a1325c0b7665ce712dc411965d00fc1d6fa384)
|
||||||
|
* more tweaks #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c4eb78c3f156cb0d033977cffbe7464697680f5)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.7...0.4.8)
|
||||||
|
|
||||||
|
## 0.4.7 (2020-06-30)
|
||||||
|
|
||||||
|
* removed HKU tweak as all HKU's are changed #10 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c937af8ee7da9aa95131e56abf7bf24800390fe6)
|
||||||
|
* Fixed types + script in "Clear Windows log files" (#15) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/461a4f122b342369db5cc08c5e30961c64e68cdd)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.6...0.4.7)
|
||||||
|
|
||||||
|
## 0.4.6 (2020-06-16)
|
||||||
|
|
||||||
|
* Fixed Some More Issues (#12) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/52d5713a99422cdf900aba819e49e998abac33cc)
|
||||||
|
* removed failing continuous deployment #14 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/583c5660d6ac934b845a044e013357aa91f61c15)
|
||||||
|
* Updated Some Tweaks (#11) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0fc18459cde57684f00764815062f838f932aed5)
|
||||||
|
* Updated Some More Tweaks (#13) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/019b838925e963b7ec052ac76c6faf5650b9eb67)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.5...0.4.6)
|
||||||
|
|
||||||
|
## 0.4.5 (2020-06-13)
|
||||||
|
|
||||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.4...0.4.5)
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.4...0.4.5)
|
||||||
|
|
||||||
## 0.4.4 (2020-05-24)
|
## 0.4.4 (2020-05-24)
|
||||||
|
|
||||||
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/49249399fe89575ac26b26a2b063c50b29b2f554)
|
* fixed close card button not being visible & cleanup | [commit](https://github.com/undergroundwires/privacy.sexy/commit/0d2efe5b05aa965458b78b8fa43754ce2f4fe11b)
|
||||||
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ff334704466a1d8e04660ac61da777681ebb93e5)
|
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e2ab124fb799f56ada3570fdc911361cb803e889)
|
||||||
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/690f34686f4d35f09e78e5870447b09d7dfaa91b)
|
* one command to lint everything "npm run lint" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/bb98d20637cbf1d524ebb2973e308773006e3153)
|
||||||
* new footer with privacy policy | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fb40bc571becf4e97d782af7d0de311cab37cec2)
|
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c668a97950a1cb7c8bf2a7fd8a72d1101e65e8ce)
|
||||||
* fix "group by" overflows on smaller screens | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e07609779b605ce7c80f75f186e4bacff60e8861)
|
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/aab8f21a8d8dbed54798af581e6e1ad9e86a4be1)
|
||||||
* clicking outside of a card closes it | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7b033f8d5b0e086c6076c1db3391144ae14ad296)
|
|
||||||
|
|
||||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.3...0.4.4)
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.3...0.4.4)
|
||||||
|
|
||||||
@@ -37,7 +180,7 @@
|
|||||||
* reading version from package.json instead of version file #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/691f989682179016ddcbf55a05cded29155288c9)
|
* reading version from package.json instead of version file #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/691f989682179016ddcbf55a05cded29155288c9)
|
||||||
* automatically increases patch number #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3e3bc07576f7c7e74e3e11fc7d197cbb9a9fb8c0)
|
* automatically increases patch number #5 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3e3bc07576f7c7e74e3e11fc7d197cbb9a9fb8c0)
|
||||||
* using deployment operations from aws-static-site-with-cd | [commit](https://github.com/undergroundwires/privacy.sexy/commit/997be7113f676888892ffa35566d9ebb58a3e9ea)
|
* using deployment operations from aws-static-site-with-cd | [commit](https://github.com/undergroundwires/privacy.sexy/commit/997be7113f676888892ffa35566d9ebb58a3e9ea)
|
||||||
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a27ca65b9eb29f009553e047bfb52f4200c0bd17)
|
* automated using bump-everywhere + more quality checks (#8) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4a91e8ccd8a707bc6bea34ee28cff7fa4f66ee2f)
|
||||||
|
|
||||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.2...0.4.3)
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.2...0.4.3)
|
||||||
|
|
||||||
|
|||||||
45
CONTRIBUTING.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||||
|
- Reporting a bug
|
||||||
|
- Discussing the current state of the code
|
||||||
|
- Submitting a fix
|
||||||
|
- Proposing new features
|
||||||
|
- Becoming a maintainer
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
|
||||||
|
- Your pull requests are actively welcomed.
|
||||||
|
- The steps:
|
||||||
|
1. Fork the repo and create your branch from master.
|
||||||
|
2. If you've added code that should be tested, add tests.
|
||||||
|
3. If you've changed APIs, update the documentation.
|
||||||
|
4. Ensure the test suite passes.
|
||||||
|
5. Make sure your code lints.
|
||||||
|
6. Issue that pull request!
|
||||||
|
- 🙏 DO
|
||||||
|
- Document your changes in the pull request
|
||||||
|
- ❗ DON'T
|
||||||
|
- Do not update the versions, current version is only [set by the maintainer](./img/architecture/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
### Extend scripts
|
||||||
|
|
||||||
|
- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
|
||||||
|
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
|
||||||
|
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
|
||||||
|
|
||||||
|
### Handle the state in presentation layer
|
||||||
|
|
||||||
|
- There are two types of components:
|
||||||
|
- **Stateless**, extends `Vue`
|
||||||
|
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
|
||||||
|
- The source of truth for the state lies in application layer (`./src/application/`) and must be updated from the views if they're mutating the state
|
||||||
|
- They mutate or/and reacts to changes in [application state](src/application/State/ApplicationState.ts).
|
||||||
|
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.
|
||||||
12
Dockerfile
@@ -1,20 +1,12 @@
|
|||||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
# Build
|
||||||
# |B|u|i|l|d| |S|t|a|g|e|
|
|
||||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
|
||||||
FROM node:lts-alpine as build-stage
|
FROM node:lts-alpine as build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
# For testing purposes, it's easy to run http-server on lts-alpine such as continuing from here:
|
|
||||||
# RUN npm install -g http-server
|
|
||||||
# EXPOSE 8080
|
|
||||||
# CMD [ "http-server", "dist" ]
|
|
||||||
|
|
||||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
# Production stage
|
||||||
# |P|r|o|d|u|c|t|i|o|n| |S|t|a|g|e|
|
|
||||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
|
||||||
FROM nginx:stable-alpine as production-stage
|
FROM nginx:stable-alpine as production-stage
|
||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
58
README.md
@@ -1,8 +1,8 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy
|
||||||
|
|
||||||
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/issues)
|
[](./CONTRIBUTING.md)
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
@@ -12,54 +12,68 @@
|
|||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
[](https://github.com/undergroundwires/bump-everywhere)
|
[](https://github.com/undergroundwires/bump-everywhere)
|
||||||
|
|
||||||
[https://privacy.sexy](https://privacy.sexy)
|
## Get started
|
||||||
|
|
||||||
|
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||||
|
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.4/privacy.sexy-Setup-0.7.4.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.4/privacy.sexy-0.7.4.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.4/privacy.sexy-0.7.4.dmg)
|
||||||
|
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
|
[](https://privacy.sexy)
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
- You don't need to run any compiled software on your system, just run the generated scripts.
|
- You don't need to run any compiled software that has access to your system, just run the generated scripts.
|
||||||
- It's open source, both application & infrastructure is 100% transparent
|
|
||||||
- Fully automated C/CD pipeline to AWS for provisioning serverless infrastructure using GitHub actions.
|
|
||||||
- Have full visibility into what the tweaks do as you enable them.
|
- Have full visibility into what the tweaks do as you enable them.
|
||||||
|
- Ability to revert applied scripts
|
||||||
- Easily extendable
|
- Easily extendable
|
||||||
|
- Everything is open-sourced including both application and infrastructure
|
||||||
|
- Fully automated CI/CD pipeline using GitHub actions
|
||||||
|
- to AWS for provisioning serverless infrastructure
|
||||||
|
- for building and sharing the desktop applications
|
||||||
|
|
||||||
## Extend scripts
|
## Extend scripts
|
||||||
|
|
||||||
Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
||||||
|
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts)
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- Setup and run
|
- Project setup: `npm install`
|
||||||
- For development:
|
- Testing
|
||||||
- `npm install` to project setup.
|
- Run unit tests: `npm run test:unit`
|
||||||
- `npm run serve` to compile & hot-reload for development.
|
- Lint: `npm run lint`
|
||||||
- Production (using Docker):
|
- **Desktop app**
|
||||||
- Build `docker build -t undergroundwires/privacy.sexy .`
|
- Development: `npm run electron:serve`
|
||||||
- Run `docker run -it -p 8080:8080 --rm --name privacy.sexy-1 undergroundwires/privacy.sexy`
|
- Production: `npm run electron:build` to build an executable
|
||||||
- Prepare for production: `npm run build`
|
- **Webpage**
|
||||||
- Run tests: `npm run test:unit`
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Lint and fix files: `npm run lint`
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
|
- Or run using Docker:
|
||||||
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.4 .`
|
||||||
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.4 undergroundwires/privacy.sexy:0.7.4`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Application
|
### Application
|
||||||
|
|
||||||
- Powered by **TypeScript** + **Vue.js** 💪
|
- Powered by **TypeScript**, **Vue.js** and **Electron** 💪
|
||||||
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
- and driven by **Domain-driven design**, **Event-driven architecture**, **Data-driven programming** concepts.
|
||||||
- Application uses highly decoupled models & services in different DDD layers.
|
- Application uses highly decoupled models & services in different DDD layers.
|
||||||
- **Domain layer** is where the application is modelled with validation logic.
|
- **Domain layer** is where the application is modelled with validation logic.
|
||||||
- **Presentation Layer**
|
- **Presentation Layer**
|
||||||
- Consists of Vue.js components & UI stuff.
|
- Consists of Vue.js components and other UI-related code.
|
||||||
|
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
- Event driven as in components simply listens to events from the state and act accordingly.
|
||||||
- **Application Layer**
|
- **Application Layer**
|
||||||
- Keeps the application state
|
- Keeps the application state
|
||||||
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
|
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
|
||||||
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
|
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### AWS Infrastructure
|
### AWS Infrastructure
|
||||||
|
|
||||||
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
[](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||||
|
|
||||||
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
|
- It uses infrastructure from the following repository: [aws-static-site-with-cd](https://github.com/undergroundwires/aws-static-site-with-cd)
|
||||||
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
|
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
|
||||||
@@ -71,4 +85,4 @@ Fork it & add more scripts in [application.yaml](src/application/application.yam
|
|||||||
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
- Versioning, tagging, creation of `CHANGELOG.md` and releasing is automated using [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) action
|
||||||
- Everything that's merged in the master goes directly to production.
|
- Everything that's merged in the master goes directly to production.
|
||||||
|
|
||||||
[](.github/workflows/)
|
[](.github/workflows/)
|
||||||
|
|||||||
5
build/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# build
|
||||||
|
|
||||||
|
- These are the file that are used by electron.
|
||||||
|
- Logos are created by from the [PNG icon](./../public/icon.png)
|
||||||
|
- by running `npx electron-icon-builder --input=./public/icon.png --output=build --flatten`
|
||||||
BIN
build/icons/1024x1024.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
build/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
build/icons/16x16.png
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
build/icons/24x24.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
build/icons/256x256.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
build/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
build/icons/48x48.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
build/icons/512x512.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
build/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
build/icons/icon.icns
Normal file
BIN
build/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
docs/gitops.png
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
1
img/architecture/gitops.drawio
Normal file
BIN
img/architecture/gitops.png
Normal file
|
After Width: | Height: | Size: 483 KiB |
BIN
img/screenshot.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
6193
package-lock.json
generated
74
package.json
@@ -1,54 +1,66 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.4.5",
|
"version": "0.7.4",
|
||||||
|
"author": "undergroundwires",
|
||||||
|
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
|
||||||
|
"electron:build": "vue-cli-service electron:build",
|
||||||
|
"electron:serve": "vue-cli-service electron:serve",
|
||||||
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:vue": "vue-cli-service lint --no-fix",
|
"lint:vue": "vue-cli-service lint --no-fix",
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent"
|
|
||||||
},
|
},
|
||||||
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.12.0",
|
"@fortawesome/free-brands-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.12.0",
|
"@fortawesome/free-regular-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
"@fortawesome/vue-fontawesome": "^0.1.10",
|
||||||
"ace-builds": "^1.4.7",
|
"ace-builds": "^1.4.12",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"inversify": "^5.0.1",
|
"inversify": "^5.0.1",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"v-tooltip": "^2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.1.0",
|
"vue-class-component": "^7.2.5",
|
||||||
"vue-js-modal": "^2.0.0-rc.3",
|
"vue-js-modal": "^2.0.0-rc.6",
|
||||||
"vue-property-decorator": "^8.3.0"
|
"vue-property-decorator": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.42",
|
"@types/ace": "0.0.43",
|
||||||
"@types/chai": "^4.2.7",
|
"@types/chai": "^4.2.12",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/mocha": "^5.2.7",
|
"@types/mocha": "^8.0.3",
|
||||||
"@vue/cli-plugin-typescript": "^4.4.0",
|
"@vue/cli-plugin-typescript": "^4.5.4",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.1.1",
|
"@vue/cli-plugin-unit-mocha": "^4.5.4",
|
||||||
"@vue/cli-service": "^4.1.1",
|
"@vue/cli-service": "^4.5.4",
|
||||||
"@vue/test-utils": "1.0.0-beta.30",
|
"@vue/test-utils": "1.0.4",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
"electron": "^10.1.0",
|
||||||
|
"electron-devtools-installer": "^3.1.1",
|
||||||
|
"electron-log": "^4.2.4",
|
||||||
|
"electron-updater": "^4.3.4",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.23.1",
|
"markdownlint-cli": "^0.23.2",
|
||||||
"remark-cli": "^8.0.0",
|
"remark-cli": "^8.0.1",
|
||||||
"remark-lint-no-dead-urls": "^1.0.2",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^3.0.0",
|
"remark-preset-lint-consistent": "^3.0.1",
|
||||||
"remark-validate-links": "^10.0.0",
|
"remark-validate-links": "^10.0.2",
|
||||||
"sass": "^1.24.0",
|
"sass": "^1.26.10",
|
||||||
"sass-loader": "^8.0.0",
|
"sass-loader": "^10.0.1",
|
||||||
"typescript": "^3.7.4",
|
"typescript": "^4.0.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
|
||||||
|
"vue-template-compiler": "^2.6.12",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -32,4 +32,4 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -12,10 +12,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from 'vue-property-decorator';
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
import { ApplicationState, IApplicationState } from '@/application/State/ApplicationState';
|
|
||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
import TheHeader from '@/presentation/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/TheFooter.vue';
|
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||||
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
|
||||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||||
|
|||||||
54
src/application/Environment/BrowserOs/BrowserOsDetector.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
import { DetectorBuilder } from './DetectorBuilder';
|
||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
|
private readonly detectors = BrowserDetectors;
|
||||||
|
public detect(userAgent: string): OperatingSystem {
|
||||||
|
if (!userAgent) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
for (const detector of this.detectors) {
|
||||||
|
const os = detector.detect(userAgent);
|
||||||
|
if (os !== OperatingSystem.Unknown) {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||||
|
const BrowserDetectors =
|
||||||
|
[
|
||||||
|
define(OperatingSystem.KaiOS, (b) =>
|
||||||
|
b.mustInclude('KAIOS')),
|
||||||
|
define(OperatingSystem.ChromeOS, (b) =>
|
||||||
|
b.mustInclude('CrOS')),
|
||||||
|
define(OperatingSystem.BlackBerryOS, (b) =>
|
||||||
|
b.mustInclude('BlackBerry')),
|
||||||
|
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
||||||
|
b.mustInclude('RIM Tablet OS')),
|
||||||
|
define(OperatingSystem.BlackBerry, (b) =>
|
||||||
|
b.mustInclude('BB10')),
|
||||||
|
define(OperatingSystem.Android, (b) =>
|
||||||
|
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.Android, (b) =>
|
||||||
|
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.iOS, (b) =>
|
||||||
|
b.mustInclude('like Mac OS X')),
|
||||||
|
define(OperatingSystem.Linux, (b) =>
|
||||||
|
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||||
|
define(OperatingSystem.Windows, (b) =>
|
||||||
|
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.WindowsPhone, (b) =>
|
||||||
|
b.mustInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.macOS, (b) =>
|
||||||
|
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||||
|
];
|
||||||
|
|
||||||
|
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
||||||
|
const builder = new DetectorBuilder(os);
|
||||||
|
applyRules(builder);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
53
src/application/Environment/BrowserOs/DetectorBuilder.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
|
||||||
|
export class DetectorBuilder {
|
||||||
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
|
private readonly notExistingPartsInUserAgent = new Array<string>();
|
||||||
|
|
||||||
|
constructor(private readonly os: OperatingSystem) { }
|
||||||
|
|
||||||
|
public mustInclude(str: string): DetectorBuilder {
|
||||||
|
return this.add(str, this.existingPartsInUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mustNotInclude(str: string): DetectorBuilder {
|
||||||
|
return this.add(str, this.notExistingPartsInUserAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public build(): IBrowserOsDetector {
|
||||||
|
if (!this.existingPartsInUserAgent.length) {
|
||||||
|
throw new Error('Must include at least a part');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
detect: (agent) => this.detect(agent),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private detect(userAgent: string): OperatingSystem {
|
||||||
|
if (!userAgent) {
|
||||||
|
throw new Error('User agent is null or undefined');
|
||||||
|
}
|
||||||
|
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
return this.os;
|
||||||
|
}
|
||||||
|
|
||||||
|
private add(part: string, array: string[]): DetectorBuilder {
|
||||||
|
if (!part) {
|
||||||
|
throw new Error('part is empty or undefined');
|
||||||
|
}
|
||||||
|
if (this.existingPartsInUserAgent.includes(part)) {
|
||||||
|
throw new Error(`part ${part} is already included as existing part`);
|
||||||
|
}
|
||||||
|
if (this.notExistingPartsInUserAgent.includes(part)) {
|
||||||
|
throw new Error(`part ${part} is already included as not existing part`);
|
||||||
|
}
|
||||||
|
array.push(part);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
|
||||||
|
export interface IBrowserOsDetector {
|
||||||
|
detect(userAgent: string): OperatingSystem;
|
||||||
|
}
|
||||||
80
src/application/Environment/Environment.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
|
import { IEnvironment } from './IEnvironment';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
interface IEnvironmentVariables {
|
||||||
|
readonly window: Window & typeof globalThis;
|
||||||
|
readonly process: NodeJS.Process;
|
||||||
|
readonly navigator: Navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Environment implements IEnvironment {
|
||||||
|
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||||
|
window,
|
||||||
|
process,
|
||||||
|
navigator,
|
||||||
|
});
|
||||||
|
public readonly isDesktop: boolean;
|
||||||
|
public readonly os: OperatingSystem;
|
||||||
|
protected constructor(
|
||||||
|
variables: IEnvironmentVariables,
|
||||||
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
||||||
|
if (!variables) {
|
||||||
|
throw new Error('variables is null or empty');
|
||||||
|
}
|
||||||
|
this.isDesktop = isDesktop(variables);
|
||||||
|
this.os = this.isDesktop ?
|
||||||
|
getDesktopOsType(getProcessPlatform(variables))
|
||||||
|
: browserOsDetector.detect(getUserAgent(variables));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||||
|
if (!variables.window || !variables.window.navigator) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return variables.window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||||
|
if (!variables.process || !variables.process.platform) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return variables.process.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesktopOsType(processPlatform: string): OperatingSystem {
|
||||||
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
|
if (processPlatform === 'darwin') {
|
||||||
|
return OperatingSystem.macOS;
|
||||||
|
} else if (processPlatform === 'win32') {
|
||||||
|
return OperatingSystem.Windows;
|
||||||
|
} else if (processPlatform === 'linux') {
|
||||||
|
return OperatingSystem.Linux;
|
||||||
|
}
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
|
// More: https://github.com/electron/electron/issues/2288
|
||||||
|
// Renderer process
|
||||||
|
if (variables.window
|
||||||
|
&& variables.window.process
|
||||||
|
&& variables.window.process.type === 'renderer') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Main process
|
||||||
|
if (variables.process
|
||||||
|
&& variables.process.versions
|
||||||
|
&& Boolean(variables.process.versions.electron)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
|
if (variables.navigator
|
||||||
|
&& variables.navigator.userAgent
|
||||||
|
&& variables.navigator.userAgent.includes('Electron')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
6
src/application/Environment/IEnvironment.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
export interface IEnvironment {
|
||||||
|
isDesktop: boolean;
|
||||||
|
os: OperatingSystem;
|
||||||
|
}
|
||||||
14
src/application/Environment/OperatingSystem.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export enum OperatingSystem {
|
||||||
|
macOS,
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
KaiOS,
|
||||||
|
ChromeOS,
|
||||||
|
BlackBerryOS,
|
||||||
|
BlackBerry,
|
||||||
|
BlackBerryTabletOS,
|
||||||
|
Android,
|
||||||
|
iOS,
|
||||||
|
WindowsPhone,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
import { Category } from '../../domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { Application } from '../../domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import applicationFile from 'js-yaml-loader!./../application.yaml';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
|
|
||||||
export function parseApplication(): Application {
|
export function parseApplication(content: ApplicationYaml): IApplication {
|
||||||
|
validate(content);
|
||||||
const categories = new Array<Category>();
|
const categories = new Array<Category>();
|
||||||
if (!applicationFile.actions || applicationFile.actions.length <= 0) {
|
for (const action of content.actions) {
|
||||||
throw new Error('Application does not define any action');
|
|
||||||
}
|
|
||||||
for (const action of applicationFile.actions) {
|
|
||||||
const category = parseCategory(action);
|
const category = parseCategory(action);
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
}
|
}
|
||||||
const app = new Application(
|
const app = new Application(
|
||||||
applicationFile.name,
|
content.name,
|
||||||
applicationFile.repositoryUrl,
|
content.repositoryUrl,
|
||||||
process.env.VUE_APP_VERSION,
|
process.env.VUE_APP_VERSION,
|
||||||
categories);
|
categories);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate(content: ApplicationYaml): void {
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('application is null or undefined');
|
||||||
|
}
|
||||||
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
|
throw new Error('application does not define any action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '../../domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
import { parseScript } from './ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter: number = 0;
|
||||||
|
|
||||||
|
|
||||||
interface ICategoryChildren {
|
interface ICategoryChildren {
|
||||||
subCategories: Category[];
|
subCategories: Category[];
|
||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCategory(category: YamlCategory): Category {
|
export function parseCategory(category: YamlCategory): Category {
|
||||||
if (!category.children || category.children.length <= 0) {
|
ensureValid(category);
|
||||||
throw Error('Category has no children');
|
|
||||||
}
|
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
@@ -31,6 +29,18 @@ export function parseCategory(category: YamlCategory): Category {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureValid(category: YamlCategory) {
|
||||||
|
if (!category) {
|
||||||
|
throw Error('category is null or undefined');
|
||||||
|
}
|
||||||
|
if (!category.children || category.children.length === 0) {
|
||||||
|
throw Error('category has no children');
|
||||||
|
}
|
||||||
|
if (!category.category || category.category.length === 0) {
|
||||||
|
throw Error('category has no name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
||||||
if (isCategory(categoryOrScript)) {
|
if (isCategory(categoryOrScript)) {
|
||||||
@@ -38,19 +48,14 @@ function parseCategoryChild(
|
|||||||
children.subCategories.push(subCategory);
|
children.subCategories.push(subCategory);
|
||||||
} else if (isScript(categoryOrScript)) {
|
} else if (isScript(categoryOrScript)) {
|
||||||
const yamlScript = categoryOrScript as YamlScript;
|
const yamlScript = categoryOrScript as YamlScript;
|
||||||
const script = new Script(
|
const script = parseScript(yamlScript);
|
||||||
/* name */ yamlScript.name,
|
|
||||||
/* code */ yamlScript.code,
|
|
||||||
/* docs */ parseDocUrls(yamlScript),
|
|
||||||
/* is recommended? */ yamlScript.recommend);
|
|
||||||
children.subScripts.push(script);
|
children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Child element is neither a category or a script.
|
throw new Error(`Child element is neither a category or a script.
|
||||||
Parent: ${parent.category}, element: ${categoryOrScript}`);
|
Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isScript(categoryOrScript: any): boolean {
|
function isScript(categoryOrScript: any): boolean {
|
||||||
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,46 @@
|
|||||||
import { YamlDocumentable } from 'js-yaml-loader!./application.yaml';
|
import { YamlDocumentable, DocumentationUrls } from 'js-yaml-loader!./application.yaml';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
||||||
|
if (!documentable) {
|
||||||
|
throw new Error('documentable is null or undefined');
|
||||||
|
}
|
||||||
const docs = documentable.docs;
|
const docs = documentable.docs;
|
||||||
if (!docs) {
|
if (!docs || !docs.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const result = new DocumentationUrls();
|
let result = new DocumentationUrlContainer();
|
||||||
if (docs instanceof Array) {
|
result = addDocs(docs, result);
|
||||||
for (const doc of docs) {
|
|
||||||
if (typeof doc !== 'string') {
|
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
|
||||||
}
|
|
||||||
result.add(doc);
|
|
||||||
}
|
|
||||||
} else if (typeof docs === 'string') {
|
|
||||||
result.add(docs);
|
|
||||||
} else {
|
|
||||||
throw new Error('Docs field (documentation url) must a string or array of strings');
|
|
||||||
}
|
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentationUrls {
|
function addDocs(docs: DocumentationUrls, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||||
|
if (docs instanceof Array) {
|
||||||
|
urls.addUrls(docs);
|
||||||
|
} else if (typeof docs === 'string') {
|
||||||
|
urls.addUrl(docs);
|
||||||
|
} else {
|
||||||
|
throw new Error('Docs field (documentation url) must a string or array of strings');
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DocumentationUrlContainer {
|
||||||
private readonly urls = new Array<string>();
|
private readonly urls = new Array<string>();
|
||||||
|
|
||||||
public add(url: string) {
|
public addUrl(url: string) {
|
||||||
validateUrl(url);
|
validateUrl(url);
|
||||||
this.urls.push(url);
|
this.urls.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addUrls(urls: any[]) {
|
||||||
|
for (const url of urls) {
|
||||||
|
if (typeof url !== 'string') {
|
||||||
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
|
}
|
||||||
|
this.addUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getAll(): ReadonlyArray<string> {
|
public getAll(): ReadonlyArray<string> {
|
||||||
return this.urls;
|
return this.urls;
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/application/Parser/ScriptParser.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Script } from '@/domain/Script';
|
||||||
|
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
|
||||||
|
export function parseScript(yamlScript: YamlScript): Script {
|
||||||
|
if (!yamlScript) {
|
||||||
|
throw new Error('script is null or undefined');
|
||||||
|
}
|
||||||
|
const script = new Script(
|
||||||
|
/* name */ yamlScript.name,
|
||||||
|
/* code */ yamlScript.code,
|
||||||
|
/* revertCode */ yamlScript.revertCode,
|
||||||
|
/* docs */ parseDocUrls(yamlScript),
|
||||||
|
/* isRecommended */ yamlScript.recommend);
|
||||||
|
return script;
|
||||||
|
}
|
||||||
@@ -3,13 +3,15 @@ import { IUserFilter } from './Filter/IUserFilter';
|
|||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
import { UserSelection } from './Selection/UserSelection';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
import { AsyncLazy } from '../../infrastructure/Threading/AsyncLazy';
|
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||||
import { Signal } from '../../infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { parseApplication } from '../Parser/ApplicationParser';
|
import { parseApplication } from '../Parser/ApplicationParser';
|
||||||
import { IApplicationState } from './IApplicationState';
|
import { IApplicationState } from './IApplicationState';
|
||||||
import { Script } from '../../domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Application } from '../../domain/Application';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
|
||||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
||||||
export class ApplicationState implements IApplicationState {
|
export class ApplicationState implements IApplicationState {
|
||||||
@@ -20,7 +22,7 @@ export class ApplicationState implements IApplicationState {
|
|||||||
|
|
||||||
/** Application instance with all scripts. */
|
/** Application instance with all scripts. */
|
||||||
private static instance = new AsyncLazy<IApplicationState>(() => {
|
private static instance = new AsyncLazy<IApplicationState>(() => {
|
||||||
const application = parseApplication();
|
const application = parseApplication(applicationFile);
|
||||||
const selectedScripts = new Array<Script>();
|
const selectedScripts = new Array<Script>();
|
||||||
const state = new ApplicationState(application, selectedScripts);
|
const state = new ApplicationState(application, selectedScripts);
|
||||||
return Promise.resolve(state);
|
return Promise.resolve(state);
|
||||||
@@ -33,13 +35,11 @@ export class ApplicationState implements IApplicationState {
|
|||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
/** Inner instance of the all scripts */
|
/** Inner instance of the all scripts */
|
||||||
public readonly app: Application,
|
public readonly app: IApplication,
|
||||||
/** Initially selected scripts */
|
/** Initially selected scripts */
|
||||||
public readonly defaultScripts: Script[]) {
|
public readonly defaultScripts: Script[]) {
|
||||||
this.selection = new UserSelection(app, defaultScripts);
|
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
||||||
this.code = new ApplicationCode(this.selection, app.version);
|
this.code = new ApplicationCode(this.selection, app.version);
|
||||||
this.filter = new UserFilter(app);
|
this.filter = new UserFilter(app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IApplicationState, IUserFilter };
|
|
||||||
|
|||||||
@@ -1,27 +1,38 @@
|
|||||||
import { UserScriptGenerator } from './UserScriptGenerator';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { IUserSelection } from './../Selection/IUserSelection';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||||
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new Signal<string>();
|
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||||
public current: string;
|
public current: string;
|
||||||
|
|
||||||
private readonly generator: UserScriptGenerator;
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(userSelection: IUserSelection, private readonly version: string) {
|
constructor(
|
||||||
|
userSelection: IUserSelection,
|
||||||
|
private readonly version: string,
|
||||||
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
if (!version) { throw new Error('version is null or undefined'); }
|
if (!version) { throw new Error('version is null or undefined'); }
|
||||||
this.generator = new UserScriptGenerator();
|
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||||
this.setCode(userSelection.selectedScripts);
|
this.setCode(userSelection.selectedScripts);
|
||||||
userSelection.changed.on((scripts) => {
|
userSelection.changed.on((scripts) => {
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<IScript>) {
|
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||||
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
|
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||||
this.changed.notify(this.current);
|
const code = this.generator.buildCode(scripts, this.version);
|
||||||
|
this.current = code.code;
|
||||||
|
this.scriptPositions = code.scriptPositions;
|
||||||
|
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||||
|
this.changed.notify(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/application/State/Code/Event/CodeChangedEvent.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
|
public readonly code: string;
|
||||||
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
code: string,
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scripts: Map<SelectedScript, ICodePosition>) {
|
||||||
|
ensureAllPositionsExist(code, Array.from(scripts.values()));
|
||||||
|
this.code = code;
|
||||||
|
const newScripts = Array.from(scripts.keys());
|
||||||
|
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||||
|
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||||
|
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||||
|
this.scripts = new Map<IScript, ICodePosition>();
|
||||||
|
scripts.forEach((position, selection) => {
|
||||||
|
this.scripts.set(selection.script, position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty(): boolean {
|
||||||
|
return this.scripts.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
|
return this.scripts.get(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
|
for (const position of positions) {
|
||||||
|
if (position.endLine > totalLines) {
|
||||||
|
throw new Error(`script end line (${position.endLine}) is out of range.` +
|
||||||
|
`(total code lines: ${totalLines}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangedScripts(
|
||||||
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
newScripts: ReadonlyArray<SelectedScript>): ReadonlyArray<IScript> {
|
||||||
|
return newScripts
|
||||||
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
|
&& oldScript.revert !== newScript.revert ))
|
||||||
|
.map((selection) => selection.script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectIfNotExists(
|
||||||
|
selectableContainer: ReadonlyArray<SelectedScript>,
|
||||||
|
test: ReadonlyArray<SelectedScript>) {
|
||||||
|
return selectableContainer
|
||||||
|
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
|
||||||
|
.map((selection) => selection.script);
|
||||||
|
}
|
||||||
11
src/application/State/Code/Event/ICodeChangedEvent.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface ICodeChangedEvent {
|
||||||
|
readonly code: string;
|
||||||
|
addedScripts: ReadonlyArray<IScript>;
|
||||||
|
removedScripts: ReadonlyArray<IScript>;
|
||||||
|
changedScripts: ReadonlyArray<IScript>;
|
||||||
|
isEmpty(): boolean;
|
||||||
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
|
}
|
||||||
@@ -4,8 +4,20 @@ const TotalFunctionSeparatorChars = 58;
|
|||||||
export class CodeBuilder {
|
export class CodeBuilder {
|
||||||
private readonly lines = new Array<string>();
|
private readonly lines = new Array<string>();
|
||||||
|
|
||||||
|
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||||
|
public get currentLine(): number {
|
||||||
|
return this.lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
public appendLine(code?: string): CodeBuilder {
|
public appendLine(code?: string): CodeBuilder {
|
||||||
this.lines.push(code);
|
if (!code) {
|
||||||
|
this.lines.push('');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
|
for (const line of lines) {
|
||||||
|
this.lines.push(line);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
7
src/application/State/Code/Generation/IUserScript.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface IUserScript {
|
||||||
|
code: string;
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
export interface IUserScriptGenerator {
|
||||||
|
buildCode(
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
version: string): IUserScript;
|
||||||
|
}
|
||||||
68
src/application/State/Code/Generation/UserScriptGenerator.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import { CodeBuilder } from './CodeBuilder';
|
||||||
|
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
|
export const adminRightsScript = {
|
||||||
|
name: 'Ensure admin privileges',
|
||||||
|
code: 'fltmc >nul 2>&1 || (\n' +
|
||||||
|
' echo Administrator privileges are required.\n' +
|
||||||
|
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
|
||||||
|
' echo Right-click on the script and select "Run as administrator".\n' +
|
||||||
|
' pause & exit 1\n' +
|
||||||
|
' )\n' +
|
||||||
|
' exit 0\n' +
|
||||||
|
')',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
|
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
||||||
|
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||||
|
if (!version) { throw new Error('version is undefined'); }
|
||||||
|
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||||
|
if (!selectedScripts.length) {
|
||||||
|
return { code: '', scriptPositions };
|
||||||
|
}
|
||||||
|
const builder = initializeCode(version);
|
||||||
|
for (const selection of selectedScripts) {
|
||||||
|
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||||
|
}
|
||||||
|
const code = finalizeCode(builder);
|
||||||
|
return { code, scriptPositions };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCode(version: string): CodeBuilder {
|
||||||
|
return new CodeBuilder()
|
||||||
|
.appendLine('@echo off')
|
||||||
|
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||||
|
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
||||||
|
.appendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeCode(builder: CodeBuilder): string {
|
||||||
|
return builder.appendLine()
|
||||||
|
.appendLine('pause')
|
||||||
|
.appendLine('exit /b 0')
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSelection(
|
||||||
|
selection: SelectedScript,
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
|
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||||
|
const startPosition = builder.currentLine + 1;
|
||||||
|
appendCode(selection, builder);
|
||||||
|
const endPosition = builder.currentLine - 1;
|
||||||
|
builder.appendLine();
|
||||||
|
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
||||||
|
return scriptPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||||
|
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||||
|
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
|
||||||
|
builder.appendFunction(name, scriptCode);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
|
||||||
export interface IApplicationCode {
|
export interface IApplicationCode {
|
||||||
readonly changed: ISignal<string>;
|
readonly changed: ISignal<ICodeChangedEvent>;
|
||||||
readonly current: string;
|
readonly current: string;
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/application/State/Code/Position/CodePosition.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { ICodePosition } from './ICodePosition';
|
||||||
|
export class CodePosition implements ICodePosition {
|
||||||
|
|
||||||
|
public get totalLines(): number {
|
||||||
|
return this.endLine - this.startLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly startLine: number,
|
||||||
|
public readonly endLine: number) {
|
||||||
|
if (startLine < 0) {
|
||||||
|
throw new Error('Code cannot start in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine < 0) {
|
||||||
|
throw new Error('Code cannot end in a negative line');
|
||||||
|
}
|
||||||
|
if (endLine === startLine) {
|
||||||
|
throw new Error('Empty code');
|
||||||
|
}
|
||||||
|
if (endLine < startLine) {
|
||||||
|
throw new Error('End line cannot be less than start line');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/application/State/Code/Position/ICodePosition.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ICodePosition {
|
||||||
|
readonly startLine: number;
|
||||||
|
readonly endLine: number;
|
||||||
|
readonly totalLines: number;
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { CodeBuilder } from './CodeBuilder';
|
|
||||||
import { Script } from '@/domain/Script';
|
|
||||||
|
|
||||||
const adminRightsScript = {
|
|
||||||
name: 'Ensure admin privileges',
|
|
||||||
code: 'fltmc >nul 2>&1 || (\n' +
|
|
||||||
' echo This batch script requires administrator privileges. Right-click on\n' +
|
|
||||||
' echo the script and select "Run as administrator".\n' +
|
|
||||||
' pause\n' +
|
|
||||||
' exit 1\n' +
|
|
||||||
')',
|
|
||||||
};
|
|
||||||
|
|
||||||
export class UserScriptGenerator {
|
|
||||||
public buildCode(scripts: ReadonlyArray<Script>, version: string): string {
|
|
||||||
if (!scripts) { throw new Error('scripts is undefined'); }
|
|
||||||
if (!scripts.length) { throw new Error('scripts are empty'); }
|
|
||||||
if (!version) { throw new Error('version is undefined'); }
|
|
||||||
const builder = new CodeBuilder()
|
|
||||||
.appendLine('@echo off')
|
|
||||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
|
||||||
.appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine();
|
|
||||||
for (const script of scripts) {
|
|
||||||
builder.appendFunction(script.name, script.code).appendLine();
|
|
||||||
}
|
|
||||||
return builder.appendLine()
|
|
||||||
.appendLine('pause')
|
|
||||||
.appendLine('exit /b 0')
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { IScript } from '@/domain/Script';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
export class FilterResult implements IFilterResult {
|
export class FilterResult implements IFilterResult {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IFilterResult } from './IFilterResult';
|
|||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IUserFilter {
|
||||||
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
readonly filtered: ISignal<IFilterResult>;
|
readonly filtered: ISignal<IFilterResult>;
|
||||||
readonly filterRemoved: ISignal<void>;
|
readonly filterRemoved: ISignal<void>;
|
||||||
setFilter(filter: string): void;
|
setFilter(filter: string): void;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
import { FilterResult } from './FilterResult';
|
import { FilterResult } from './FilterResult';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { Application } from '../../../domain/Application';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IUserFilter } from './IUserFilter';
|
import { IUserFilter } from './IUserFilter';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
|
|
||||||
export class UserFilter implements IUserFilter {
|
export class UserFilter implements IUserFilter {
|
||||||
public readonly filtered = new Signal<IFilterResult>();
|
public readonly filtered = new Signal<IFilterResult>();
|
||||||
public readonly filterRemoved = new Signal<void>();
|
public readonly filterRemoved = new Signal<void>();
|
||||||
|
public currentFilter: IFilterResult | undefined;
|
||||||
|
|
||||||
constructor(private application: Application) {
|
constructor(private application: IApplication) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,22 +20,34 @@ export class UserFilter implements IUserFilter {
|
|||||||
}
|
}
|
||||||
const filterLowercase = filter.toLocaleLowerCase();
|
const filterLowercase = filter.toLocaleLowerCase();
|
||||||
const filteredScripts = this.application.getAllScripts().filter(
|
const filteredScripts = this.application.getAllScripts().filter(
|
||||||
(script) =>
|
(script) => isScriptAMatch(script, filterLowercase));
|
||||||
script.name.toLowerCase().includes(filterLowercase) ||
|
|
||||||
script.code.toLowerCase().includes(filterLowercase));
|
|
||||||
const filteredCategories = this.application.getAllCategories().filter(
|
const filteredCategories = this.application.getAllCategories().filter(
|
||||||
(script) => script.name.toLowerCase().includes(filterLowercase));
|
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||||
|
|
||||||
const matches = new FilterResult(
|
const matches = new FilterResult(
|
||||||
filteredScripts,
|
filteredScripts,
|
||||||
filteredCategories,
|
filteredCategories,
|
||||||
filter,
|
filter,
|
||||||
);
|
);
|
||||||
|
this.currentFilter = matches;
|
||||||
this.filtered.notify(matches);
|
this.filtered.notify(matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFilter(): void {
|
public removeFilter(): void {
|
||||||
|
this.currentFilter = undefined;
|
||||||
this.filterRemoved.notify();
|
this.filterRemoved.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isScriptAMatch(script: IScript, filterLowercase: string) {
|
||||||
|
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (script.code.toLowerCase().includes(filterLowercase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (script.revertCode) {
|
||||||
|
return script.revertCode.toLowerCase().includes(filterLowercase);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IUserSelection {
|
||||||
readonly changed: ISignal<ReadonlyArray<IScript>>;
|
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<IScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
readonly totalSelected: number;
|
||||||
addSelectedScript(scriptId: string): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
|
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
removeSelectedScript(scriptId: string): void;
|
removeSelectedScript(scriptId: string): void;
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
||||||
isSelected(script: IScript): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/application/State/Selection/SelectedScript.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
export class SelectedScript extends BaseEntity<string> {
|
||||||
|
constructor(
|
||||||
|
public readonly script: IScript,
|
||||||
|
public readonly revert: boolean,
|
||||||
|
) {
|
||||||
|
super(script.id);
|
||||||
|
if (revert && !script.canRevert()) {
|
||||||
|
throw new Error('cannot revert an irreversible script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { IUserSelection } from './IUserSelection';
|
import { IUserSelection } from './IUserSelection';
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/Script';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
|
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export class UserSelection implements IUserSelection {
|
||||||
public readonly changed = new Signal<ReadonlyArray<IScript>>();
|
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||||
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
private readonly scripts = new InMemoryRepository<string, IScript>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: IApplication,
|
private readonly app: IApplication,
|
||||||
/** Initially selected scripts */
|
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||||
selectedScripts: ReadonlyArray<IScript>) {
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
if (selectedScripts && selectedScripts.length > 0) {
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
@@ -20,28 +21,64 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a script to users application */
|
public removeAllInCategory(categoryId: number): void {
|
||||||
public addSelectedScript(scriptId: string): void {
|
const category = this.app.findCategory(categoryId);
|
||||||
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
|
if (!scriptsToRemove.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scriptsToRemove) {
|
||||||
|
this.scripts.removeItem(script.id);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
||||||
|
const category = this.app.findCategory(categoryId);
|
||||||
|
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||||
|
.filter((script) =>
|
||||||
|
!this.scripts.exists(script.id)
|
||||||
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
|
);
|
||||||
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
}
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.app.findScript(scriptId);
|
const script = this.app.findScript(scriptId);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||||
}
|
}
|
||||||
this.scripts.addItem(script);
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
|
||||||
|
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
|
const script = this.app.findScript(scriptId);
|
||||||
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove a script from users application */
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
public removeSelectedScript(scriptId: string): void {
|
||||||
this.scripts.removeItem(scriptId);
|
this.scripts.removeItem(scriptId);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSelected(script: IScript): boolean {
|
public isSelected(scriptId: string): boolean {
|
||||||
return this.scripts.exists(script);
|
return this.scripts.exists(scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
/** Get users scripts based on his/her selections */
|
||||||
public get selectedScripts(): ReadonlyArray<IScript> {
|
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
||||||
return this.scripts.getItems();
|
return this.scripts.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +88,9 @@ export class UserSelection implements IUserSelection {
|
|||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.app.getAllScripts()) {
|
for (const script of this.app.getAllScripts()) {
|
||||||
if (!this.scripts.exists(script)) {
|
if (!this.scripts.exists(script.id)) {
|
||||||
this.scripts.addItem(script);
|
const selection = new SelectedScript(script, false);
|
||||||
|
this.scripts.addItem(selection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
@@ -78,9 +116,11 @@ export class UserSelection implements IUserSelection {
|
|||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||||
}
|
}
|
||||||
// Select from unselected scripts
|
// Select from unselected scripts
|
||||||
scripts
|
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
||||||
.filter((script) => !this.scripts.exists(script))
|
for (const toSelect of unselectedScripts) {
|
||||||
.forEach((script) => this.scripts.addItem(script));
|
const selection = new SelectedScript(toSelect, false);
|
||||||
|
this.scripts.addItem(selection);
|
||||||
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/application/application.yaml.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
declare module 'js-yaml-loader!*' {
|
declare module 'js-yaml-loader!*' {
|
||||||
type CategoryOrScript = YamlCategory | YamlScript;
|
export type CategoryOrScript = YamlCategory | YamlScript;
|
||||||
type DocumentationUrls = ReadonlyArray<string> | string;
|
export type DocumentationUrls = ReadonlyArray<string> | string;
|
||||||
|
|
||||||
export interface YamlDocumentable {
|
export interface YamlDocumentable {
|
||||||
docs?: DocumentationUrls;
|
docs?: DocumentationUrls;
|
||||||
@@ -9,6 +9,7 @@ declare module 'js-yaml-loader!*' {
|
|||||||
export interface YamlScript extends YamlDocumentable {
|
export interface YamlScript extends YamlDocumentable {
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
|
revertCode: string;
|
||||||
recommend: boolean;
|
recommend: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ declare module 'js-yaml-loader!*' {
|
|||||||
category: string;
|
category: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApplicationYaml {
|
export interface ApplicationYaml {
|
||||||
name: string;
|
name: string;
|
||||||
repositoryUrl: string;
|
repositoryUrl: string;
|
||||||
actions: ReadonlyArray<YamlCategory>;
|
actions: ReadonlyArray<YamlCategory>;
|
||||||
|
|||||||
133
src/background.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { app, protocol, BrowserWindow, shell } from 'electron';
|
||||||
|
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||||
|
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||||
|
import path from 'path';
|
||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
||||||
|
|
||||||
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
let win: BrowserWindow | null;
|
||||||
|
|
||||||
|
// Scheme must be registered before the app is ready
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
|
autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging
|
||||||
|
log.transports.file.level = 'silly';
|
||||||
|
if (!process.env.IS_TEST) {
|
||||||
|
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 1350,
|
||||||
|
height: 955,
|
||||||
|
webPreferences: {
|
||||||
|
// Use pluginOptions.nodeIntegration, leave this alone
|
||||||
|
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
|
||||||
|
nodeIntegration: (process.env
|
||||||
|
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
|
||||||
|
},
|
||||||
|
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
|
||||||
|
icon: path.join(__static, 'icon.png'),
|
||||||
|
});
|
||||||
|
|
||||||
|
win.setMenuBarVisibility(false);
|
||||||
|
configureExternalsUrlsOpenBrowser(win);
|
||||||
|
loadApplication(win);
|
||||||
|
|
||||||
|
win.on('closed', () => {
|
||||||
|
win = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit when all windows are closed.
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// On macOS it is common for applications and their menu bar
|
||||||
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
// On macOS it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (win === null) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.on('ready', async () => {
|
||||||
|
if (isDevelopment && !process.env.IS_TEST) {
|
||||||
|
// Install Vue Devtools
|
||||||
|
try {
|
||||||
|
await installExtension(VUEJS_DEVTOOLS);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vue Devtools failed to install:', e.toString()); // tslint:disable-line:no-console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
// See electron-builder issue "checkForUpdatesAndNotify updates but does not notify on Windows 10"
|
||||||
|
// https://github.com/electron-userland/electron-builder/issues/2700
|
||||||
|
// https://github.com/electron/electron/issues/10864
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/shell/appid#how-to-form-an-application-defined-appusermodelid
|
||||||
|
app.setAppUserModelId('Undergroundwires.PrivacySexy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit cleanly on request from parent process in development mode.
|
||||||
|
if (isDevelopment) {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
process.on('message', (data) => {
|
||||||
|
if (data === 'graceful-exit') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadApplication(window: BrowserWindow) {
|
||||||
|
if (process.env.WEBPACK_DEV_SERVER_URL) {
|
||||||
|
// Load the url of the dev server if in development mode
|
||||||
|
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
|
||||||
|
if (!process.env.IS_TEST) {
|
||||||
|
win.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createProtocol('app');
|
||||||
|
// Load the index.html when not in development
|
||||||
|
win.loadURL('app://./index.html');
|
||||||
|
// tslint:disable-next-line:max-line-length
|
||||||
|
autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
|
||||||
|
window.webContents.on('new-window', (event, url) => { // handle redirect
|
||||||
|
if (url !== win.webContents.getURL()) {
|
||||||
|
event.preventDefault();
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,20 +13,12 @@ export class Application implements IApplication {
|
|||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly repositoryUrl: string,
|
public readonly repositoryUrl: string,
|
||||||
public readonly version: string,
|
public readonly version: string,
|
||||||
public readonly categories: ReadonlyArray<ICategory>) {
|
public readonly actions: ReadonlyArray<ICategory>) {
|
||||||
if (!name) { throw Error('Application has no name'); }
|
if (!name) { throw Error('Application has no name'); }
|
||||||
if (!repositoryUrl) { throw Error('Application has no repository url'); }
|
if (!repositoryUrl) { throw Error('Application has no repository url'); }
|
||||||
if (!version) { throw Error('Version cannot be empty'); }
|
if (!version) { throw Error('Version cannot be empty'); }
|
||||||
this.flattened = flatten(categories);
|
this.flattened = flatten(actions);
|
||||||
if (this.flattened.allCategories.length === 0) {
|
ensureValid(this.flattened);
|
||||||
throw new Error('Application must consist of at least one category');
|
|
||||||
}
|
|
||||||
if (this.flattened.allScripts.length === 0) {
|
|
||||||
throw new Error('Application must consist of at least one script');
|
|
||||||
}
|
|
||||||
if (this.flattened.allScripts.filter((script) => script.isRecommended).length === 0) {
|
|
||||||
throw new Error('Application must consist of at least one recommended script');
|
|
||||||
}
|
|
||||||
ensureNoDuplicates(this.flattened.allCategories);
|
ensureNoDuplicates(this.flattened.allCategories);
|
||||||
ensureNoDuplicates(this.flattened.allScripts);
|
ensureNoDuplicates(this.flattened.allScripts);
|
||||||
}
|
}
|
||||||
@@ -75,30 +67,50 @@ interface IFlattenedApplication {
|
|||||||
allScripts: IScript[];
|
allScripts: IScript[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenRecursive(
|
function ensureValid(application: IFlattenedApplication) {
|
||||||
|
if (!application.allCategories || application.allCategories.length === 0) {
|
||||||
|
throw new Error('Application must consist of at least one category');
|
||||||
|
}
|
||||||
|
if (!application.allScripts || application.allScripts.length === 0) {
|
||||||
|
throw new Error('Application must consist of at least one script');
|
||||||
|
}
|
||||||
|
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
|
||||||
|
throw new Error('Application must consist of at least one recommended script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenCategories(
|
||||||
categories: ReadonlyArray<ICategory>,
|
categories: ReadonlyArray<ICategory>,
|
||||||
flattened: IFlattenedApplication) {
|
flattened: IFlattenedApplication): IFlattenedApplication {
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
flattened.allCategories.push(category);
|
flattened.allCategories.push(category);
|
||||||
if (category.scripts) {
|
flattened = flattenScripts(category.scripts, flattened);
|
||||||
for (const script of category.scripts) {
|
flattened = flattenCategories(category.subCategories, flattened);
|
||||||
flattened.allScripts.push(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (category.subCategories && category.subCategories.length > 0) {
|
|
||||||
flattenRecursive(
|
|
||||||
category.subCategories as ReadonlyArray<ICategory>,
|
|
||||||
flattened);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenScripts(
|
||||||
|
scripts: ReadonlyArray<IScript>,
|
||||||
|
flattened: IFlattenedApplication): IFlattenedApplication {
|
||||||
|
if (!scripts) {
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
for (const script of scripts) {
|
||||||
|
flattened.allScripts.push(script);
|
||||||
|
}
|
||||||
|
return flattened;
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatten(
|
function flatten(
|
||||||
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
|
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
|
||||||
const flattened: IFlattenedApplication = {
|
let flattened: IFlattenedApplication = {
|
||||||
allCategories: new Array<ICategory>(),
|
allCategories: new Array<ICategory>(),
|
||||||
allScripts: new Array<IScript>(),
|
allScripts: new Array<IScript>(),
|
||||||
};
|
};
|
||||||
flattenRecursive(categories, flattened);
|
flattened = flattenCategories(categories, flattened);
|
||||||
return flattened;
|
return flattened;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,7 @@ import { IScript } from './IScript';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
|
|
||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private static validate(category: ICategory) {
|
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
||||||
if (!category.name) {
|
|
||||||
throw new Error('name is null or empty');
|
|
||||||
}
|
|
||||||
if ((!category.subCategories || category.subCategories.length === 0) &&
|
|
||||||
(!category.scripts || category.scripts.length === 0)) {
|
|
||||||
throw new Error('A category must have at least one sub-category or scripts');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
@@ -20,6 +12,27 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
public readonly scripts?: ReadonlyArray<IScript>) {
|
||||||
super(id);
|
super(id);
|
||||||
Category.validate(this);
|
validateCategory(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
|
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||||
|
return [
|
||||||
|
...category.scripts,
|
||||||
|
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCategory(category: ICategory) {
|
||||||
|
if (!category.name) {
|
||||||
|
throw new Error('undefined or empty name');
|
||||||
|
}
|
||||||
|
if ((!category.subCategories || category.subCategories.length === 0) &&
|
||||||
|
(!category.scripts || category.scripts.length === 0)) {
|
||||||
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ export interface IApplication {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly repositoryUrl: string;
|
readonly repositoryUrl: string;
|
||||||
readonly version: string;
|
readonly version: string;
|
||||||
readonly categories: ReadonlyArray<ICategory>;
|
|
||||||
readonly totalScripts: number;
|
readonly totalScripts: number;
|
||||||
readonly totalCategories: number;
|
readonly totalCategories: number;
|
||||||
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
getRecommendedScripts(): ReadonlyArray<IScript>;
|
getRecommendedScripts(): ReadonlyArray<IScript>;
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
findCategory(categoryId: number): ICategory | undefined;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts?: ReadonlyArray<IScript>;
|
||||||
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IEntity } from '../infrastructure/Entity/IEntity';
|
export { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { IEntity } from './../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { IDocumentable } from './IDocumentable';
|
import { IDocumentable } from './IDocumentable';
|
||||||
|
|
||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly code: string;
|
|
||||||
readonly isRecommended: boolean;
|
readonly isRecommended: boolean;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,59 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
|||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
|
|
||||||
export class Script extends BaseEntity<string> implements IScript {
|
export class Script extends BaseEntity<string> implements IScript {
|
||||||
private static ensureNoEmptyLines(name: string, code: string): void {
|
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`Script has empty lines "${name}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ensureCodeHasUniqueLines(name: string, code: string): void {
|
|
||||||
const lines = code.split('\n')
|
|
||||||
.filter((line) => this.mayBeUniqueLine(line));
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static mayBeUniqueLine(codeLine: string): boolean {
|
|
||||||
const trimmed = codeLine.trim();
|
|
||||||
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public name: string,
|
public readonly name: string,
|
||||||
public code: string,
|
public readonly code: string,
|
||||||
public documentationUrls: ReadonlyArray<string>,
|
public readonly revertCode: string,
|
||||||
public isRecommended: boolean) {
|
public readonly documentationUrls: ReadonlyArray<string>,
|
||||||
|
public readonly isRecommended: boolean) {
|
||||||
super(name);
|
super(name);
|
||||||
if (code == null || code.length === 0) {
|
validateCode(name, code);
|
||||||
throw new Error('Code is empty or null');
|
if (revertCode) {
|
||||||
|
validateCode(name, revertCode);
|
||||||
|
if (code === revertCode) {
|
||||||
|
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Script.ensureCodeHasUniqueLines(name, code);
|
}
|
||||||
Script.ensureNoEmptyLines(name, code);
|
public canRevert(): boolean {
|
||||||
|
return Boolean(this.revertCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IScript } from './IScript';
|
function validateCode(name: string, code: string): void {
|
||||||
|
if (!code || code.length === 0) {
|
||||||
|
throw new Error(`Code of ${name} is empty or null`);
|
||||||
|
}
|
||||||
|
ensureCodeHasUniqueLines(name, code);
|
||||||
|
ensureNoEmptyLines(name, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoEmptyLines(name: string, code: string): void {
|
||||||
|
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||||
|
throw Error(`Script has empty lines "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mayBeUniqueLine(codeLine: string): boolean {
|
||||||
|
const trimmed = codeLine.trim();
|
||||||
|
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||||
|
const lines = code.split('\n')
|
||||||
|
.filter((line) => mayBeUniqueLine(line));
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
|
if (duplicateLines.length !== 0) {
|
||||||
|
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { IEntity } from '../Entity/IEntity';
|
|||||||
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
readonly length: number;
|
readonly length: number;
|
||||||
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
|
||||||
|
getById(id: TKey): TEntity | undefined;
|
||||||
addItem(item: TEntity): void;
|
addItem(item: TEntity): void;
|
||||||
|
addOrUpdateItem(item: TEntity): void;
|
||||||
removeItem(id: TKey): void;
|
removeItem(id: TKey): void;
|
||||||
exists(item: TEntity): boolean;
|
exists(id: TKey): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,34 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
|
|||||||
return predicate ? this.items.filter(predicate) : this.items;
|
return predicate ? this.items.filter(predicate) : this.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getById(id: TKey): TEntity | undefined {
|
||||||
|
const items = this.getItems((entity) => entity.id === id);
|
||||||
|
if (!items.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return items[0];
|
||||||
|
}
|
||||||
|
|
||||||
public addItem(item: TEntity): void {
|
public addItem(item: TEntity): void {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('Item is null');
|
throw new Error('item is null or undefined');
|
||||||
}
|
}
|
||||||
if (this.exists(item)) {
|
if (this.exists(item.id)) {
|
||||||
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
|
||||||
}
|
}
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public addOrUpdateItem(item: TEntity): void {
|
||||||
|
if (!item) {
|
||||||
|
throw new Error('item is null or undefined');
|
||||||
|
}
|
||||||
|
if (this.exists(item.id)) {
|
||||||
|
this.removeItem(item.id);
|
||||||
|
}
|
||||||
|
this.items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
public removeItem(id: TKey): void {
|
public removeItem(id: TKey): void {
|
||||||
const index = this.items.findIndex((item) => item.id === id);
|
const index = this.items.findIndex((item) => item.id === id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
@@ -34,8 +52,8 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
|
|||||||
this.items.splice(index, 1);
|
this.items.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exists(entity: TEntity): boolean {
|
public exists(id: TKey): boolean {
|
||||||
const index = this.items.findIndex((item) => item.id === entity.id);
|
const index = this.items.findIndex((item) => item.id === id);
|
||||||
return index !== -1;
|
return index !== -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import fileSaver from 'file-saver';
|
import fileSaver from 'file-saver';
|
||||||
|
|
||||||
|
export enum FileType {
|
||||||
|
BatchFile,
|
||||||
|
}
|
||||||
export class SaveFileDialog {
|
export class SaveFileDialog {
|
||||||
public static saveText(text: string, fileName: string): void {
|
public static saveFile(text: string, fileName: string, type: FileType): void {
|
||||||
this.saveBlob(text, 'text/plain;charset=utf-8', fileName);
|
const mimeType = this.mimeTypes.get(type);
|
||||||
|
this.saveBlob(text, mimeType, fileName);
|
||||||
}
|
}
|
||||||
|
private static readonly mimeTypes = new Map<FileType, string>([
|
||||||
|
// Some browsers (including firefox + IE) require right mime type
|
||||||
|
// otherwise they ignore extension and save the file as text.
|
||||||
|
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
||||||
|
]);
|
||||||
|
|
||||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
||||||
|
import 'core-js/fn/array/flat-map'; // Here until Vue 3 & CLI v4 https://github.com/vuejs/vue-cli/issues/3834
|
||||||
|
|
||||||
new ApplicationBootstrapper()
|
new ApplicationBootstrapper()
|
||||||
.bootstrap(Vue);
|
.bootstrap(Vue);
|
||||||
|
|||||||
@@ -4,15 +4,20 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
|||||||
/** BRAND ICONS (PREFIX: fab) */
|
/** BRAND ICONS (PREFIX: fab) */
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
/** REGULAR ICONS (PREFIX: far) */
|
/** REGULAR ICONS (PREFIX: far) */
|
||||||
import { faFolderOpen, faFolder } from '@fortawesome/free-regular-svg-icons';
|
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, faTag, faGlobe } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
|
||||||
export class IconBootstrapper implements IVueBootstrapper {
|
export class IconBootstrapper implements IVueBootstrapper {
|
||||||
public bootstrap(vue: VueConstructor): void {
|
public bootstrap(vue: VueConstructor): void {
|
||||||
library.add(
|
library.add(
|
||||||
faGithub,
|
faGithub,
|
||||||
|
faUserSecret,
|
||||||
|
faSmile,
|
||||||
|
faDesktop,
|
||||||
|
faGlobe,
|
||||||
|
faTag,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faFolder,
|
faFolder,
|
||||||
faTimes,
|
faTimes,
|
||||||
|
|||||||
@@ -8,10 +8,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
|
import { Component, Prop, Emit } from 'vue-property-decorator';
|
||||||
import { StatefulVue, IApplicationState } from './StatefulVue';
|
import { StatefulVue } from './StatefulVue';
|
||||||
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
|
|
||||||
import { Clipboard } from './../infrastructure/Clipboard';
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class IconButton extends StatefulVue {
|
export default class IconButton extends StatefulVue {
|
||||||
|
|||||||
@@ -16,10 +16,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import CardListItem from './CardListItem.vue';
|
import CardListItem from './CardListItem.vue';
|
||||||
import { StatefulVue, IApplicationState } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { hasDirective } from './NonCollapsingDirective';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -32,8 +33,11 @@ export default class CardList extends StatefulVue {
|
|||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
this.setCategories(state.app.categories);
|
this.setCategories(state.app.actions);
|
||||||
this.onOutsideOfActiveCardClicked(() => {
|
this.onOutsideOfActiveCardClicked((element) => {
|
||||||
|
if (hasDirective(element)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.activeCategoryId = null;
|
this.activeCategoryId = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -46,14 +50,14 @@ export default class CardList extends StatefulVue {
|
|||||||
this.categoryIds = categories.map((category) => category.id);
|
this.categoryIds = categories.map((category) => category.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onOutsideOfActiveCardClicked(callback) {
|
private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
|
||||||
const outsideClickListener = (event) => {
|
const outsideClickListener = (event) => {
|
||||||
if (!this.activeCategoryId) {
|
if (!this.activeCategoryId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`);
|
const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`);
|
||||||
if (!element.contains(event.target)) {
|
if (element && !element.contains(event.target)) {
|
||||||
callback();
|
callback(event.target);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('click', outsideClickListener);
|
document.addEventListener('click', outsideClickListener);
|
||||||
|
|||||||
@@ -4,25 +4,27 @@
|
|||||||
v-bind:class="{
|
v-bind:class="{
|
||||||
'is-collapsed': !isExpanded,
|
'is-collapsed': !isExpanded,
|
||||||
'is-inactive': activeCategoryId && activeCategoryId != categoryId,
|
'is-inactive': activeCategoryId && activeCategoryId != categoryId,
|
||||||
'is-expanded': isExpanded}">
|
'is-expanded': isExpanded
|
||||||
<div class="card__inner">
|
}"
|
||||||
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
|
ref="cardElement">
|
||||||
<span v-else>Oh no 😢</span>
|
<div class="card__inner">
|
||||||
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
|
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
|
||||||
|
<span v-else>Oh no 😢</span>
|
||||||
|
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
|
||||||
|
</div>
|
||||||
|
<div class="card__expander" v-on:click.stop>
|
||||||
|
<div class="card__expander__content">
|
||||||
|
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
|
||||||
|
</div>
|
||||||
|
<div class="card__expander__close-button">
|
||||||
|
<font-awesome-icon :icon="['fas', 'times']" v-on:click="onSelected(false)"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card__expander" v-on:click.stop>
|
|
||||||
<div class="card__expander__content">
|
|
||||||
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
|
|
||||||
</div>
|
|
||||||
<div class="card__expander__close-button">
|
|
||||||
<font-awesome-icon :icon="['fas', 'times']" v-on:click="onSelected(false)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator';
|
import { Component, Prop, Watch, Emit } from 'vue-property-decorator';
|
||||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
|
||||||
@@ -41,11 +43,18 @@ export default class CardListItem extends StatefulVue {
|
|||||||
public onSelected(isExpanded: boolean) {
|
public onSelected(isExpanded: boolean) {
|
||||||
this.isExpanded = isExpanded;
|
this.isExpanded = isExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch('activeCategoryId')
|
@Watch('activeCategoryId')
|
||||||
public async onActiveCategoryChanged(value: |number) {
|
public async onActiveCategoryChanged(value: |number) {
|
||||||
this.isExpanded = value === this.categoryId;
|
this.isExpanded = value === this.categoryId;
|
||||||
}
|
}
|
||||||
|
@Watch('isExpanded')
|
||||||
|
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
||||||
|
if (!oldValue && newValue) {
|
||||||
|
await new Promise((r) => setTimeout(r, 400));
|
||||||
|
const focusElement = this.$refs.cardElement as HTMLElement;
|
||||||
|
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async mounted() {
|
public async mounted() {
|
||||||
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
|
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
|
||||||
@@ -66,32 +75,39 @@ export default class CardListItem extends StatefulVue {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "@/presentation/styles/colors.scss";
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
@import "@/presentation/styles/media.scss";
|
||||||
|
|
||||||
$big-screen-width: 991px;
|
$card-padding: 30px;
|
||||||
$medium-screen-width: 767px;
|
$card-margin: 15px;
|
||||||
$small-screen-width: 380px;
|
$card-line-break-width: 30px;
|
||||||
|
$arrow-size: 15px;
|
||||||
|
$expanded-margin-top: 30px;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin: 15px;
|
margin: 15px;
|
||||||
width: calc((100% / 3) - 30px);
|
width: calc((100% / 3) - #{$card-line-break-width});
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
// Media queries for stacking cards
|
// Media queries for stacking cards
|
||||||
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - 30px); }
|
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - #{$card-line-break-width}); }
|
||||||
@media screen and (max-width: $medium-screen-width) { width: 100%; }
|
@media screen and (max-width: $medium-screen-width) { width: 100%; }
|
||||||
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
@media screen and (max-width: $small-screen-width) { width: 90%; }
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
padding: 30px;
|
padding: $card-padding;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: $gray;
|
background-color: $gray;
|
||||||
color: $light-gray;
|
color: $light-gray;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
|
height: 100%;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
|
||||||
|
display:flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $accent;
|
background-color: $accent;
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
@@ -151,16 +167,17 @@ $small-screen-width: 380px;
|
|||||||
|
|
||||||
&.is-expanded {
|
&.is-expanded {
|
||||||
.card__inner {
|
.card__inner {
|
||||||
|
height: auto;
|
||||||
background-color: $accent;
|
background-color: $accent;
|
||||||
&:after{
|
&:after { // arrow
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -30px;
|
bottom: calc(-1 * #{$expanded-margin-top});
|
||||||
left: calc(50% - 15px);
|
left: calc(50% - #{$arrow-size});
|
||||||
border-left: 15px solid transparent;
|
border-left: #{$arrow-size} solid transparent;
|
||||||
border-right: 15px solid transparent;
|
border-right: #{$arrow-size} solid transparent;
|
||||||
border-bottom: 15px solid #333a45;
|
border-bottom: #{$arrow-size} solid #333a45;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +185,7 @@ $small-screen-width: 380px;
|
|||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
// max-height: 1000px;
|
// max-height: 1000px;
|
||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
margin-top: 30px;
|
margin-top: $expanded-margin-top;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +199,9 @@ $small-screen-width: 380px;
|
|||||||
&.is-inactive {
|
&.is-inactive {
|
||||||
.card__inner {
|
.card__inner {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
height: auto;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -196,28 +215,28 @@ $small-screen-width: 380px;
|
|||||||
|
|
||||||
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
|
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row
|
||||||
.card:nth-of-type(3n+2) .card__expander {
|
.card:nth-of-type(3n+2) .card__expander {
|
||||||
margin-left: calc(-100% - 30px);
|
margin-left: calc(-100% - #{$card-line-break-width});
|
||||||
}
|
}
|
||||||
.card:nth-of-type(3n+3) .card__expander {
|
.card:nth-of-type(3n+3) .card__expander {
|
||||||
margin-left: calc(-200% - 60px);
|
margin-left: calc(-200% - (#{$card-line-break-width} * 2));
|
||||||
}
|
}
|
||||||
.card:nth-of-type(3n+4) {
|
.card:nth-of-type(3n+4) {
|
||||||
clear: left;
|
clear: left;
|
||||||
}
|
}
|
||||||
.card__expander {
|
.card__expander {
|
||||||
width: calc(300% + 60px);
|
width: calc(300% + (#{$card-line-break-width} * 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
|
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
|
||||||
.card:nth-of-type(2n+2) .card__expander {
|
.card:nth-of-type(2n+2) .card__expander {
|
||||||
margin-left: calc(-100% - 30px);
|
margin-left: calc(-100% - #{$card-line-break-width});
|
||||||
}
|
}
|
||||||
.card:nth-of-type(2n+3) {
|
.card:nth-of-type(2n+3) {
|
||||||
clear: left;
|
clear: left;
|
||||||
}
|
}
|
||||||
.card__expander {
|
.card__expander {
|
||||||
width: calc(200% + 30px);
|
width: calc(200% + #{$card-line-break-width});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
17
src/presentation/Scripts/Cards/NonCollapsingDirective.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { DirectiveOptions } from 'vue';
|
||||||
|
|
||||||
|
const attributeName = 'data-interactionDoesNotCollapse';
|
||||||
|
|
||||||
|
export function hasDirective(el: Element): boolean {
|
||||||
|
if (el.hasAttribute(attributeName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const parent = el.closest(`[${attributeName}]`);
|
||||||
|
return !!parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NonCollapsing: DirectiveOptions = {
|
||||||
|
inserted(el: HTMLElement) {
|
||||||
|
el.setAttribute(attributeName, '');
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -15,9 +15,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
import { Component } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
|
||||||
import { Grouping } from './Grouping';
|
import { Grouping } from './Grouping';
|
||||||
|
|
||||||
const DefaultGrouping = Grouping.Cards;
|
const DefaultGrouping = Grouping.Cards;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { IApplication } from './../../../domain/IApplication';
|
import { IApplication } from './../../../domain/IApplication';
|
||||||
import { ICategory, IScript } from '@/domain/ICategory';
|
import { ICategory, IScript } from '@/domain/ICategory';
|
||||||
import { INode } from './SelectableTree/INode';
|
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||||
|
|
||||||
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
||||||
const nodes = new Array<INode>();
|
const nodes = new Array<INode>();
|
||||||
for (const category of app.categories) {
|
for (const category of app.actions) {
|
||||||
const children = parseCategoryRecursively(category);
|
const children = parseCategoryRecursively(category);
|
||||||
nodes.push(convertCategoryToNode(category, children));
|
nodes.push(convertCategoryToNode(category, children));
|
||||||
}
|
}
|
||||||
@@ -23,25 +23,45 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
|
|||||||
export function getScriptNodeId(script: IScript): string {
|
export function getScriptNodeId(script: IScript): string {
|
||||||
return script.id;
|
return script.id;
|
||||||
}
|
}
|
||||||
|
export function getScriptId(nodeId: string): string {
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
export function getCategoryId(nodeId: string): number {
|
||||||
|
return +nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCategoryNodeId(category: ICategory): string {
|
export function getCategoryNodeId(category: ICategory): string {
|
||||||
return `Category${category.id}`;
|
return `${category.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryRecursively(
|
function parseCategoryRecursively(
|
||||||
parentCategory: ICategory): INode[] {
|
parentCategory: ICategory): INode[] {
|
||||||
if (!parentCategory) { throw new Error('parentCategory is undefined'); }
|
if (!parentCategory) {
|
||||||
|
throw new Error('parentCategory is undefined');
|
||||||
const nodes = new Array<INode>();
|
|
||||||
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
|
|
||||||
for (const subCategory of parentCategory.subCategories) {
|
|
||||||
const subCategoryNodes = parseCategoryRecursively(subCategory);
|
|
||||||
nodes.push(convertCategoryToNode(subCategory, subCategoryNodes));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
|
let nodes = new Array<INode>();
|
||||||
for (const script of parentCategory.scripts) {
|
nodes = addCategories(parentCategory.subCategories, nodes);
|
||||||
nodes.push(convertScriptToNode(script));
|
nodes = addScripts(parentCategory.scripts, nodes);
|
||||||
}
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScripts(scripts: ReadonlyArray<IScript>, nodes: INode[]): INode[] {
|
||||||
|
if (!scripts || scripts.length === 0) {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
for (const script of scripts) {
|
||||||
|
nodes.push(convertScriptToNode(script));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCategories(categories: ReadonlyArray<ICategory>, nodes: INode[]): INode[] {
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
for (const category of categories) {
|
||||||
|
const subCategoryNodes = parseCategoryRecursively(category);
|
||||||
|
nodes.push(convertCategoryToNode(category, subCategoryNodes));
|
||||||
}
|
}
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
@@ -50,17 +70,21 @@ function convertCategoryToNode(
|
|||||||
category: ICategory, children: readonly INode[]): INode {
|
category: ICategory, children: readonly INode[]): INode {
|
||||||
return {
|
return {
|
||||||
id: getCategoryNodeId(category),
|
id: getCategoryNodeId(category),
|
||||||
|
type: NodeType.Category,
|
||||||
text: category.name,
|
text: category.name,
|
||||||
children,
|
children,
|
||||||
documentationUrls: category.documentationUrls,
|
documentationUrls: category.documentationUrls,
|
||||||
|
isReversible: children && children.every((child) => child.isReversible),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertScriptToNode(script: IScript): INode {
|
function convertScriptToNode(script: IScript): INode {
|
||||||
return {
|
return {
|
||||||
id: getScriptNodeId(script),
|
id: getScriptNodeId(script),
|
||||||
|
type: NodeType.Script,
|
||||||
text: script.name,
|
text: script.name,
|
||||||
children: undefined,
|
children: undefined,
|
||||||
documentationUrls: script.documentationUrls,
|
documentationUrls: script.documentationUrls,
|
||||||
|
isReversible: script.canRevert(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
:selectedNodeIds="selectedNodeIds"
|
:selectedNodeIds="selectedNodeIds"
|
||||||
:filterPredicate="filterPredicate"
|
:filterPredicate="filterPredicate"
|
||||||
:filterText="filterText"
|
:filterText="filterText"
|
||||||
v-on:nodeSelected="checkNodeAsync($event)">
|
v-on:nodeSelected="toggleNodeSelectionAsync($event)"
|
||||||
|
>
|
||||||
</SelectableTree>
|
</SelectableTree>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>Nooo 😢</span>
|
<span v-else>Nooo 😢</span>
|
||||||
@@ -14,17 +15,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
import { Category } from '@/domain/Category';
|
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { ICategory } from '@/domain/ICategory';
|
||||||
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
|
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
|
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
||||||
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
|
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||||
import { INode } from './SelectableTree/INode';
|
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -48,17 +49,20 @@
|
|||||||
state.filter.filtered.on(this.handleFiltered);
|
state.filter.filtered.on(this.handleFiltered);
|
||||||
// Update initial state
|
// Update initial state
|
||||||
await this.initializeNodesAsync(this.categoryId);
|
await this.initializeNodesAsync(this.categoryId);
|
||||||
|
await this.initializeFilter(state.filter.currentFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkNodeAsync(node: INode) {
|
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||||
if (node.children != null && node.children.length > 0) {
|
|
||||||
return; // only interested in script nodes
|
|
||||||
}
|
|
||||||
const state = await this.getCurrentStateAsync();
|
const state = await this.getCurrentStateAsync();
|
||||||
if (!this.selectedNodeIds.some((id) => id === node.id)) {
|
switch (event.node.type) {
|
||||||
state.selection.addSelectedScript(node.id);
|
case NodeType.Category:
|
||||||
} else {
|
toggleCategoryNodeSelection(event, state);
|
||||||
state.selection.removeSelectedScript(node.id);
|
break;
|
||||||
|
case NodeType.Script:
|
||||||
|
toggleScriptNodeSelection(event, state);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown node type: ${event.node.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +75,7 @@
|
|||||||
this.nodes = parseAllCategories(state.app);
|
this.nodes = parseAllCategories(state.app);
|
||||||
}
|
}
|
||||||
this.selectedNodeIds = state.selection.selectedScripts
|
this.selectedNodeIds = state.selection.selectedScripts
|
||||||
.map((script) => getScriptNodeId(script));
|
.map((selected) => getScriptNodeId(selected.script));
|
||||||
}
|
}
|
||||||
|
|
||||||
public filterPredicate(node: INode): boolean {
|
public filterPredicate(node: INode): boolean {
|
||||||
@@ -81,7 +85,15 @@
|
|||||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<IScript>): void {
|
private initializeFilter(currentFilter: IFilterResult | undefined) {
|
||||||
|
if (!currentFilter) {
|
||||||
|
this.handleFilterRemoved();
|
||||||
|
} else {
|
||||||
|
this.handleFiltered(currentFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||||
this.selectedNodeIds = selectedScripts
|
this.selectedNodeIds = selectedScripts
|
||||||
.map((node) => node.id);
|
.map((node) => node.id);
|
||||||
}
|
}
|
||||||
@@ -96,6 +108,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleCategoryNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||||
|
const categoryId = getCategoryId(event.node.id);
|
||||||
|
if (event.isSelected) {
|
||||||
|
state.selection.addOrUpdateAllInCategory(categoryId, false);
|
||||||
|
} else {
|
||||||
|
state.selection.removeAllInCategory(categoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function toggleScriptNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||||
|
const scriptId = getScriptId(event.node.id);
|
||||||
|
const actualToggleState = state.selection.isSelected(scriptId);
|
||||||
|
const targetToggleState = event.isSelected;
|
||||||
|
if (targetToggleState && !actualToggleState) {
|
||||||
|
state.selection.addSelectedScript(scriptId, false);
|
||||||
|
} else if (!targetToggleState && actualToggleState) {
|
||||||
|
state.selection.removeSelectedScript(scriptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { INode } from './Node/INode';
|
||||||
|
|
||||||
|
export interface INodeSelectedEvent {
|
||||||
|
isSelected: boolean;
|
||||||
|
node: INode;
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
// Two ways of typing other libraries: https://stackoverflow.com/a/53070501
|
|
||||||
|
|
||||||
declare module 'liquor-tree' {
|
declare module 'liquor-tree' {
|
||||||
import { PluginObject } from 'vue';
|
import { PluginObject } from 'vue';
|
||||||
@@ -10,32 +9,44 @@ declare module 'liquor-tree' {
|
|||||||
filter(query: string): void;
|
filter(query: string): void;
|
||||||
clearFilter(): void;
|
clearFilter(): void;
|
||||||
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
setModel(nodes: ReadonlyArray<ILiquorTreeNewNode>): void;
|
||||||
|
// getNodeById(id: string): ILiquorTreeExistingNode;
|
||||||
|
recurseDown(fn: (node: ILiquorTreeExistingNode) => void): void;
|
||||||
}
|
}
|
||||||
interface ICustomLiquorTreeData {
|
export interface ICustomLiquorTreeData {
|
||||||
|
type: number;
|
||||||
documentationUrls: ReadonlyArray<string>;
|
documentationUrls: ReadonlyArray<string>;
|
||||||
|
isReversible: boolean;
|
||||||
|
}
|
||||||
|
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||||
|
export interface ILiquorTreeNodeState {
|
||||||
|
checked: boolean;
|
||||||
|
indeterminate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ILiquorTreeNode {
|
||||||
|
id: string;
|
||||||
|
data: ICustomLiquorTreeData;
|
||||||
|
children: ReadonlyArray<ILiquorTreeNode> | undefined;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Returned from Node tree view events.
|
* Returned from Node tree view events.
|
||||||
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||||
*/
|
*/
|
||||||
export interface ILiquorTreeExistingNode {
|
export interface ILiquorTreeExistingNode extends ILiquorTreeNode {
|
||||||
id: string;
|
|
||||||
data: ILiquorTreeNodeData;
|
data: ILiquorTreeNodeData;
|
||||||
states: ILiquorTreeNodeState | undefined;
|
states: ILiquorTreeNodeState | undefined;
|
||||||
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
|
||||||
|
// expand(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sent to liquor tree to define of new nodes.
|
* Sent to liquor tree to define of new nodes.
|
||||||
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
||||||
*/
|
*/
|
||||||
export interface ILiquorTreeNewNode {
|
export interface ILiquorTreeNewNode extends ILiquorTreeNode {
|
||||||
id: string;
|
|
||||||
text: string;
|
text: string;
|
||||||
state: ILiquorTreeNodeState | undefined;
|
state: ILiquorTreeNodeState | undefined;
|
||||||
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
|
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
|
||||||
data: ICustomLiquorTreeData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://amsik.github.io/liquor-tree/#Component-Options
|
// https://amsik.github.io/liquor-tree/#Component-Options
|
||||||
@@ -46,29 +57,16 @@ declare module 'liquor-tree' {
|
|||||||
autoCheckChildren: boolean;
|
autoCheckChildren: boolean;
|
||||||
parentSelect: boolean;
|
parentSelect: boolean;
|
||||||
keyboardNavigation: boolean;
|
keyboardNavigation: boolean;
|
||||||
deletion: (node: ILiquorTreeExistingNode) => void;
|
|
||||||
filter: ILiquorTreeFilter;
|
filter: ILiquorTreeFilter;
|
||||||
|
deletion(node: ILiquorTreeNode): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
|
export interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
||||||
interface ILiquorTreeNodeState {
|
|
||||||
checked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
|
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
||||||
interface ILiquorTreeOptions {
|
export interface ILiquorTreeFilter {
|
||||||
checkbox: boolean;
|
|
||||||
checkOnSelect: boolean;
|
|
||||||
filter: ILiquorTreeFilter;
|
|
||||||
deletion(node: ILiquorTreeNewNode): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
|
|
||||||
interface ILiquorTreeFilter {
|
|
||||||
emptyText: string;
|
emptyText: string;
|
||||||
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ILiquorTreeOptions, ILiquorTreeFilter, ILiquorTreeNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||||
|
|
||||||
|
export class LiquorTreeOptions implements ILiquorTreeOptions {
|
||||||
|
public readonly multiple = true;
|
||||||
|
public readonly checkbox = true;
|
||||||
|
public readonly checkOnSelect = true;
|
||||||
|
/* For checkbox mode only. Children will have the same checked state as their parent.
|
||||||
|
⚠️ Setting this false, does not update indeterminate state of nodes.
|
||||||
|
This is false as it's handled manually to be able to batch select for performance + highlighting */
|
||||||
|
public readonly autoCheckChildren = false;
|
||||||
|
public readonly parentSelect = true;
|
||||||
|
public readonly keyboardNavigation = true;
|
||||||
|
public readonly filter = { // Wrap this in an arrow function as setting filter directly does not work JS APIs
|
||||||
|
emptyText: this.liquorTreeFilter.emptyText,
|
||||||
|
matcher: (query: string, node: ILiquorTreeExistingNode) => {
|
||||||
|
return this.liquorTreeFilter.matcher(query, node);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
constructor(private readonly liquorTreeFilter: ILiquorTreeFilter) { }
|
||||||
|
public deletion(node: ILiquorTreeNode): boolean {
|
||||||
|
return false; // no op
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { ILiquorTreeFilter, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||||
|
import { convertExistingToNode } from './NodeTranslator';
|
||||||
|
import { INode } from './../../Node/INode';
|
||||||
|
|
||||||
|
export type FilterPredicate = (node: INode) => boolean;
|
||||||
|
|
||||||
|
export class NodePredicateFilter implements ILiquorTreeFilter {
|
||||||
|
public emptyText = ''; // Does not matter as a custom mesage is shown
|
||||||
|
constructor(private readonly filterPredicate: FilterPredicate) {
|
||||||
|
if (!filterPredicate) {
|
||||||
|
throw new Error('filterPredicate is undefined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public matcher(query: string, node: ILiquorTreeExistingNode): boolean {
|
||||||
|
return this.filterPredicate(convertExistingToNode(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
|
||||||
|
import { NodeType } from './../../Node/INode';
|
||||||
|
|
||||||
|
export function getNewState(
|
||||||
|
node: ILiquorTreeNode,
|
||||||
|
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
|
||||||
|
const checked = getNewCheckedState(node, selectedNodeIds);
|
||||||
|
const indeterminate = !checked && getNewIndeterminateState(node, selectedNodeIds);
|
||||||
|
return {
|
||||||
|
checked, indeterminate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNewIndeterminateState(
|
||||||
|
node: ILiquorTreeNode,
|
||||||
|
selectedNodeIds: ReadonlyArray<string>): boolean {
|
||||||
|
switch (node.data.type) {
|
||||||
|
case NodeType.Script:
|
||||||
|
return false;
|
||||||
|
case NodeType.Category:
|
||||||
|
return parseAllSubScriptIds(node).some((id) => selectedNodeIds.includes(id));
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown node type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNewCheckedState(
|
||||||
|
node: ILiquorTreeNode,
|
||||||
|
selectedNodeIds: ReadonlyArray<string>): boolean {
|
||||||
|
switch (node.data.type) {
|
||||||
|
case NodeType.Script:
|
||||||
|
return selectedNodeIds.some((id) => id === node.id);
|
||||||
|
case NodeType.Category:
|
||||||
|
return parseAllSubScriptIds(node).every((id) => selectedNodeIds.includes(id));
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown node type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAllSubScriptIds(categoryNode: ILiquorTreeNode): ReadonlyArray<string> {
|
||||||
|
if (categoryNode.data.type !== NodeType.Category) {
|
||||||
|
throw new Error('Not a category node');
|
||||||
|
}
|
||||||
|
if (!categoryNode.children) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return categoryNode
|
||||||
|
.children
|
||||||
|
.flatMap((child) => getNodeIds(child));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeIds(node: ILiquorTreeNode): ReadonlyArray<string> {
|
||||||
|
switch (node.data.type) {
|
||||||
|
case NodeType.Script:
|
||||||
|
return [ node.id ];
|
||||||
|
case NodeType.Category:
|
||||||
|
return parseAllSubScriptIds(node);
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown node type');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
|
||||||
import { INode } from './INode';
|
import { INode } from './../../Node/INode';
|
||||||
|
|
||||||
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
|
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
|
||||||
|
|
||||||
@@ -7,11 +7,12 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
|
|||||||
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
|
||||||
return {
|
return {
|
||||||
id: liquorTreeNode.id,
|
id: liquorTreeNode.id,
|
||||||
|
type: liquorTreeNode.data.type,
|
||||||
text: liquorTreeNode.data.text,
|
text: liquorTreeNode.data.text,
|
||||||
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
|
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
|
||||||
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
|
children: convertChildren(liquorTreeNode.children, convertExistingToNode),
|
||||||
? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)),
|
|
||||||
documentationUrls: liquorTreeNode.data.documentationUrls,
|
documentationUrls: liquorTreeNode.data.documentationUrls,
|
||||||
|
isReversible : liquorTreeNode.data.isReversible,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,11 +23,22 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
|
|||||||
text: node.text,
|
text: node.text,
|
||||||
state: {
|
state: {
|
||||||
checked: false,
|
checked: false,
|
||||||
|
indeterminate: false,
|
||||||
},
|
},
|
||||||
children: (!node.children || node.children.length === 0) ? [] :
|
children: convertChildren(node.children, toNewLiquorTreeNode),
|
||||||
node.children.map((childNode) => toNewLiquorTreeNode(childNode)),
|
|
||||||
data: {
|
data: {
|
||||||
documentationUrls: node.documentationUrls,
|
documentationUrls: node.documentationUrls,
|
||||||
|
isReversible: node.isReversible,
|
||||||
|
type: node.type,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertChildren<TOldNode, TNewNode>(
|
||||||
|
oldChildren: readonly TOldNode[],
|
||||||
|
callback: (value: TOldNode) => TNewNode): TNewNode[] {
|
||||||
|
if (!oldChildren || oldChildren.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return oldChildren.map((childNode) => callback(childNode));
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="node">
|
|
||||||
<div>{{ this.data.text }}</div>
|
|
||||||
<div
|
|
||||||
v-for="url of this.data.documentationUrls"
|
|
||||||
v-bind:key="url">
|
|
||||||
<a :href="url"
|
|
||||||
:alt="url"
|
|
||||||
target="_blank" class="docs"
|
|
||||||
v-tooltip.top-center="url"
|
|
||||||
v-on:click.stop>
|
|
||||||
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
|
||||||
import { INode } from './INode';
|
|
||||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
|
||||||
@Component
|
|
||||||
export default class Node extends Vue {
|
|
||||||
@Prop() public data: INode;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import "@/presentation/styles/colors.scss";
|
|
||||||
#node {
|
|
||||||
display:flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
.docs {
|
|
||||||
color: $gray;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left:5px;
|
|
||||||
&:hover {
|
|
||||||
color: $slate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="documentationUrls">
|
||||||
|
<a v-for="url of this.documentationUrls"
|
||||||
|
v-bind:key="url"
|
||||||
|
:href="url"
|
||||||
|
:alt="url"
|
||||||
|
target="_blank" class="documentationUrl"
|
||||||
|
v-tooltip.top-center="url"
|
||||||
|
v-on:click.stop>
|
||||||
|
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||||
|
@Component
|
||||||
|
export default class DocumentationUrls extends Vue {
|
||||||
|
@Prop() public documentationUrls: string[];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
.documentationUrls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.documentationUrl {
|
||||||
|
display: flex;
|
||||||
|
color: $gray;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
&:hover {
|
||||||
|
color: $slate;
|
||||||
|
}
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
|
export enum NodeType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
|
|
||||||
export interface INode {
|
export interface INode {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly text: string;
|
readonly text: string;
|
||||||
|
readonly isReversible: boolean;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
readonly children?: ReadonlyArray<INode>;
|
readonly children?: ReadonlyArray<INode>;
|
||||||
|
readonly type: NodeType;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div id="node">
|
||||||
|
<div class="item text">{{ this.data.text }}</div>
|
||||||
|
<RevertToggle
|
||||||
|
class="item"
|
||||||
|
v-if="data.isReversible"
|
||||||
|
:node="data" />
|
||||||
|
<DocumentationUrls
|
||||||
|
class="item"
|
||||||
|
v-if="data.documentationUrls && data.documentationUrls.length > 0"
|
||||||
|
:documentationUrls="this.data.documentationUrls" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
import { INode } from './INode';
|
||||||
|
import RevertToggle from './RevertToggle.vue';
|
||||||
|
import DocumentationUrls from './DocumentationUrls.vue';
|
||||||
|
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
RevertToggle,
|
||||||
|
DocumentationUrls,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class Node extends Vue {
|
||||||
|
@Prop() public data: INode;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
#node {
|
||||||
|
display:flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.item:not(:first-child) {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="checkbox-switch" >
|
||||||
|
<input type="checkbox" class="input-checkbox"
|
||||||
|
v-model="isReverted"
|
||||||
|
@change="onRevertToggledAsync()"
|
||||||
|
v-on:click.stop>
|
||||||
|
<div class="checkbox-animate">
|
||||||
|
<span class="checkbox-off">revert</span>
|
||||||
|
<span class="checkbox-on">revert</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||||
|
import { IReverter } from './Reverter/IReverter';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import { INode } from './INode';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { getReverter } from './Reverter/ReverterFactory';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class RevertToggle extends StatefulVue {
|
||||||
|
@Prop() public node: INode;
|
||||||
|
public isReverted = false;
|
||||||
|
|
||||||
|
private handler: IReverter;
|
||||||
|
|
||||||
|
public async mounted() {
|
||||||
|
await this.onNodeChangedAsync(this.node);
|
||||||
|
const state = await this.getCurrentStateAsync();
|
||||||
|
this.updateState(state.selection.selectedScripts);
|
||||||
|
state.selection.changed.on((scripts) => this.updateState(scripts));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch('node') public async onNodeChangedAsync(node: INode) {
|
||||||
|
const state = await this.getCurrentStateAsync();
|
||||||
|
this.handler = getReverter(node, state.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onRevertToggledAsync() {
|
||||||
|
const state = await this.getCurrentStateAsync();
|
||||||
|
this.handler.selectWithRevertState(this.isReverted, state.selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateState(scripts: ReadonlyArray<SelectedScript>) {
|
||||||
|
this.isReverted = this.handler.getState(scripts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
$width: 85px;
|
||||||
|
$height: 30px;
|
||||||
|
// https://www.designlabthemes.com/css-toggle-switch/
|
||||||
|
.checkbox-switch {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: $width;
|
||||||
|
height: $height;
|
||||||
|
-webkit-border-radius: $height;
|
||||||
|
border-radius: $height;
|
||||||
|
line-height: $height;
|
||||||
|
font-size: $height / 2;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
input.input-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: $width;
|
||||||
|
height: $height;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-animate {
|
||||||
|
position: relative;
|
||||||
|
width: $width;
|
||||||
|
height: $height;
|
||||||
|
background-color: $gray;
|
||||||
|
-webkit-transition: background-color 0.25s ease-out 0s;
|
||||||
|
transition: background-color 0.25s ease-out 0s;
|
||||||
|
|
||||||
|
// Circle
|
||||||
|
&:before {
|
||||||
|
$circle-size: $height * 0.66;
|
||||||
|
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: $circle-size;
|
||||||
|
height: $circle-size;
|
||||||
|
border-radius: $circle-size * 2;
|
||||||
|
-webkit-border-radius: $circle-size * 2;
|
||||||
|
background-color: $slate;
|
||||||
|
top: $height * 0.16;
|
||||||
|
left: $width * 0.05;
|
||||||
|
-webkit-transition: left 0.3s ease-out 0s;
|
||||||
|
transition: left 0.3s ease-out 0s;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.input-checkbox:checked {
|
||||||
|
+ .checkbox-animate {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
+ .checkbox-animate:before {
|
||||||
|
left: ($width - $width/3.5);
|
||||||
|
background-color: $light-gray;
|
||||||
|
}
|
||||||
|
+ .checkbox-animate .checkbox-off {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
+ .checkbox-animate .checkbox-on {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-off, .checkbox-on {
|
||||||
|
float: left;
|
||||||
|
color: $white;
|
||||||
|
font-weight: 700;
|
||||||
|
-webkit-transition: all 0.3s ease-out 0s;
|
||||||
|
transition: all 0.3s ease-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-off {
|
||||||
|
margin-left: $width / 3;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-on {
|
||||||
|
display: none;
|
||||||
|
float: right;
|
||||||
|
margin-right: $width / 3;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { IReverter } from './IReverter';
|
||||||
|
import { getCategoryId } from '../../../ScriptNodeParser';
|
||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ScriptReverter } from './ScriptReverter';
|
||||||
|
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||||
|
|
||||||
|
export class CategoryReverter implements IReverter {
|
||||||
|
private readonly categoryId: number;
|
||||||
|
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
|
||||||
|
constructor(nodeId: string, app: IApplication) {
|
||||||
|
this.categoryId = getCategoryId(nodeId);
|
||||||
|
this.scriptReverters = getAllSubScriptReverters(this.categoryId, app);
|
||||||
|
}
|
||||||
|
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||||
|
return this.scriptReverters.every((script) => script.getState(selectedScripts));
|
||||||
|
}
|
||||||
|
public selectWithRevertState(newState: boolean, selection: IUserSelection): void {
|
||||||
|
selection.addOrUpdateAllInCategory(this.categoryId, newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllSubScriptReverters(categoryId: number, app: IApplication) {
|
||||||
|
const category = app.findCategory(categoryId);
|
||||||
|
if (!category) {
|
||||||
|
throw new Error(`Category with id "${categoryId}" does not exist`);
|
||||||
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
return scripts.map((script) => new ScriptReverter(script.id));
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
import { IUserSelection } from '@/application/State/IApplicationState';
|
||||||
|
|
||||||
|
export interface IReverter {
|
||||||
|
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
|
||||||
|
selectWithRevertState(newState: boolean, selection: IUserSelection): void;
|
||||||
|
}
|
||||||