Compare commits

..

118 Commits
0.6.0 ... 0.8.2

Author SHA1 Message Date
undergroundwires
8cd3352017 rename "disable" to "uninstall" for removing capabilities #47 2020-12-26 16:13:50 +01:00
undergroundwires
c4ec6a1445 refactor capabilities to use a shared function #41 #47 2020-12-25 22:38:06 +01:00
undergroundwires
b3117c27f2 rename app launch tracking tweak to make it more clear #44 2020-12-24 23:39:47 +01:00
undergroundwires
54ba4dbb0b in ci/cd, do not run security checks if PRs do not change dependencies #48 2020-12-23 22:42:30 +01:00
Marc05
a744415eb2 correct typos (#48)
Co-authored-by: Marc05 <git@marc05.net>
2020-12-23 22:23:42 +01:00
undergroundwires
55f936fee9 fix type assignment error after typescript upgrade 2020-12-22 23:12:48 +01:00
undergroundwires
d9e44e2574 update dependencies to latest #46 2020-12-21 22:20:49 +01:00
greaterthanstar
52d4313156 replace ampersand in "Movies & TV app" with "and" to prevent batch file from misinterpreting it (#45) 2020-12-20 21:48:52 +01:00
undergroundwires-bot
c2b531e968 ⬆️ bump everywhere to 0.8.1 2020-11-16 18:38:23 +00:00
undergroundwires
ab7d617886 replace deprecated github ::set-env command 2020-11-16 19:37:28 +01:00
undergroundwires
b247b12c3f move Microsoft.Appconnector to right category 2020-11-16 18:38:00 +01:00
undergroundwires
c26bc209eb fix errors when file already exists 2020-11-14 19:55:37 +01:00
undergroundwires
ad1872e7cd fix not being able to rename paths including brackets 2020-11-14 19:55:37 +01:00
undergroundwires
29c7704e0b unrecommend some system apps and document more 2020-11-14 19:51:21 +01:00
undergroundwires
e41e40c5bf fix wrong app names caused by wrong Microsoft docs 2020-11-11 16:18:35 +01:00
undergroundwires
31e08d231d fix not being able to uninstall system apps 2020-11-10 17:15:53 +01:00
undergroundwires
45b8dd972b refactor unused imports 2020-11-08 15:33:31 +01:00
undergroundwires
4e72673373 fix reinstalling store apps by searching appx for all users 2020-11-07 14:03:08 +01:00
undergroundwires
92c3dd9232 fix clearing jump lists causing os to break and user pin removal #37 2020-11-07 14:01:02 +01:00
undergroundwires
2c5ab3ea7d fix reinstalling store apps by searching appx for all users 2020-11-06 18:31:01 +01:00
undergroundwires
ffa279f3df refactor removing bloatware to use functions #41 2020-11-05 20:27:18 +01:00
undergroundwires-bot
89dddfbb23 ⬆️ bump everywhere to 0.8.0 2020-11-01 18:00:39 +00:00
undergroundwires
cfedcd724c update screenshot 2020-11-01 18:55:40 +01:00
undergroundwires
fd28eaad06 hide scrollbars on code area when not overflowing 2020-11-01 18:49:27 +01:00
undergroundwires
8ce06facbd add support for shared functions #41 2020-11-01 18:36:55 +01:00
undergroundwires
1a9db31c77 add all dist folders in gitignore because of files auto-generated by vscode 2020-11-01 01:53:27 +01:00
undergroundwires
ac70b063b8 rework disabling metadata retrieval 2020-10-28 22:32:05 +01:00
undergroundwires
d0019c2c9b update recommendations to be safer and consistent 2020-10-27 16:41:56 +01:00
sopla4ever
4c68408f1e add scripts to increase cryptography, enable camera notifications and remove todo app (#36)
Co-authored-by: undergroundwires <undergroundwires@users.noreply.github.com>
2020-10-26 11:52:48 +01:00
undergroundwires
1072505219 show icons on cards during indeterminate and fully selected states 2020-10-25 12:55:40 +01:00
undergroundwires
07fc555324 change "download" button to "save" on desktop 2020-10-23 17:11:44 +01:00
undergroundwires
50fb29038a switch places of download and copy buttons 2020-10-22 17:02:38 +01:00
Charles Zwicker
3785c623f8 Add GroupMe and Spotify removal option (#34)
Co-authored-by: undergroundwires <undergroundwires@users.noreply.github.com>
Co-authored-by: Charles Zwicker <czwicker54@gmail.com>
2020-10-20 16:51:30 +02:00
undergroundwires
14be3017c5 add support for different recommendation levels: strict and standard 2020-10-19 15:12:03 +01:00
undergroundwires-bot
978bab0b81 ⬆️ bump everywhere to 0.7.6 2020-10-17 22:58:06 +00:00
undergroundwires
d9d7f62d81 run tests on all operating systems: macos, ubuntu, windows 2020-10-18 02:10:20 +01:00
undergroundwires
11e0613165 update dependencies to latest 2020-10-17 23:17:44 +01:00
undergroundwires
77c3d2bbb8 simplify "why" section 2020-09-23 20:42:05 +01:00
undergroundwires
784a67afff refactor to read more from package.json 2020-09-22 20:41:12 +01:00
undergroundwires
19a092dd31 add more reversibility 2020-09-21 23:05:31 +01:00
undergroundwires
4c2f74949b add robots.txt to explicitly allow indexing 2020-09-19 01:23:33 +01:00
undergroundwires
a3fc3782ef add docs for default0 pointing to github discussion (#30) 2020-09-20 13:58:19 +01:00
undergroundwires-bot
cdc93f032a ⬆️ bumped to 0.7.5 2020-09-19 13:41:58 +00:00
undergroundwires
7dd15ed064 fix typo 2020-09-19 15:39:48 +01:00
undergroundwires
d169434157 fix pasting in search bar after page load showing no results 2020-09-18 20:07:03 +01:00
undergroundwires
6efed72bf2 fix rendering issue in older edge/IE 2020-09-17 15:41:46 +01:00
Clayton Errington
15db311801 fix the recycling bin option (#32)
* update the recycling bin option

Powershell has a module in PS 5.1+ called Clear-Recyclebin that works better than the CMD method - rd /s %systemdrive%\$Recycle.bin

* update recycling bin delete command

one liner of ComObject in powershell instead of cmd asking for confirmation. adds better backwards compatibility.
2020-09-16 20:22:33 +00:00
undergroundwires
82d509129b fix tests and checks are not running on PRs 2020-09-16 19:10:25 +01:00
undergroundwires
939d838e35 fix reverting (reinstalling) capabilities not working 2020-09-16 02:09:23 +01:00
undergroundwires-bot
6de4ce58c4 ⬆️ bumped to 0.7.4 2020-09-14 13:57:39 +00:00
undergroundwires
ee66196d9a fix wrong path in clear all firefox user profile settings 2020-09-14 16:04:38 +01:00
undergroundwires
3c13a9e837 fix missing reg value in denying app access to account 2020-09-14 16:03:03 +01:00
undergroundwires
22b23a9ece fix spectre protection getting single lined #31 2020-09-14 16:00:20 +01:00
undergroundwires
4ae385b7fc fix checked checkbox has blue border 2020-09-13 18:42:19 +01:00
undergroundwires-bot
d9abc7f0b2 ⬆️ bumped to 0.7.3 2020-09-12 13:14:13 +00:00
undergroundwires
1f19b2528a fix typo in a test 2020-09-12 14:42:27 +01:00
undergroundwires
1f11c39773 add more detailed error message 2020-09-12 00:14:27 +01:00
undergroundwires
b6ccb5927a fix comment lines are being detected as duplicate in validation 2020-09-12 00:13:58 +01:00
undergroundwires
1d465ee318 add reversibility and more scripts to denying app access with better structure 2020-09-12 00:11:10 +01:00
undergroundwires
3ab48b1cf5 fix naming of firefox cleanup to mention profiles 2020-09-11 14:26:32 +01:00
undergroundwires
de4ac978bd fix wrong path to the main telemetry file 2020-09-10 12:52:29 +01:00
undergroundwires
8df5faf4ef improve CPU specific tweaks by conditional platform checks and reversibility 2020-09-09 13:55:21 +01:00
undergroundwires
99a2035fdb fix nvidia tweak error message, categorize and add reversibility 2020-09-08 19:41:03 +01:00
undergroundwires
a0d61728ea fix vscode settings file override and add more configs 2020-09-07 13:42:59 +01:00
undergroundwires-bot
312bf6102c ⬆️ bumped to 0.7.2 2020-09-06 18:10:12 +00:00
undergroundwires
f4885b6f1c add best practice suggestion to come back 2020-09-06 02:41:11 +01:00
undergroundwires
ca63a0979e fix wording in default text in text area 2020-09-06 02:37:47 +01:00
undergroundwires
1f266c3353 fix indeterminate state being lost 2020-09-06 15:26:19 +01:00
undergroundwires
c7b2a70312 add reversibility to removing bloatware 2020-09-06 19:03:36 +01:00
undergroundwires
255133af4d fix bad highlighting of selected nodes when using keyboard navigation 2020-09-04 01:24:35 +01:00
undergroundwires
db74531cd4 add reversibility for biometric disabling and do not recommend it 2020-09-06 16:39:48 +01:00
undergroundwires
f36d8bfc78 update onesync documentation and do not recommend it as it breaks other apps 2020-09-05 23:31:31 +01:00
undergroundwires-bot
3b31ace726 ⬆️ bumped to 0.7.1 2020-09-04 11:42:03 +00:00
undergroundwires
6badfef9da refactor unused imports 2020-09-04 13:29:42 +01:00
undergroundwires
8c38dd73d8 fix new/changed script higlighting not working on production builds 2020-09-04 13:26:35 +01:00
undergroundwires
b8682a852a rename screenshot image file 2020-09-04 13:25:36 +01:00
undergroundwires
8c17929151 fix some browsers (including firefox) downloading the script as a text file 2020-09-04 12:20:41 +01:00
undergroundwires-bot
bb92c9ec28 ⬆️ bumped to 0.7.0 2020-09-02 21:26:40 +00:00
undergroundwires
b4aacea2a3 update the screenshot to show off highlighting 2020-09-02 19:12:24 +01:00
undergroundwires
8bbe6ebf75 fix search (got broken in b789250) with tests and refactorings 2020-09-02 22:44:20 +01:00
undergroundwires
a23d28f2cf refactor unused imports & variables 2020-09-01 21:32:31 +01:00
undergroundwires
f51e8859ee add reversibility on category level 2020-09-01 21:18:16 +01:00
undergroundwires
d235dee955 exclude paint, wordpad and notepad from bloatware removal 2020-09-01 21:03:14 +01:00
undergroundwires
2afef4ea3d do not hardcode capability versions and make them reversible 2020-08-26 00:58:52 +01:00
undergroundwires
f709d6a566 fix "Configure Defender" being in wrong category #28 2020-09-01 20:06:44 +01:00
undergroundwires
532915b95d categorize, fix, make scripts reversible in "UI for privacy", "security improvements" and "configure browsers" 2020-09-01 20:03:43 +01:00
Francesco Saltori
456e40bedf Add disabling of PowerShell 7+ telemetry (#29) 2020-09-01 19:57:43 +02:00
undergroundwires
018b7e270f add disabling ccleaner telemetry 2020-08-20 00:29:35 +01:00
undergroundwires
f8b8b4c97a add disabling firefox telemetry 2020-08-30 17:42:05 +01:00
undergroundwires
978d7d0863 add more OneDrive cleanup scripts and categorize them 2020-08-30 16:05:02 +01:00
undergroundwires
594a14d6ca categorize, fix and extend windows log files cleanup 2020-08-24 21:21:21 +01:00
undergroundwires
c628aa9aef updated dependencies to latest and audit fixes (#25) 2020-08-28 16:46:09 +01:00
undergroundwires
3060ebf79c fix NTP script documentation is on wrong place 2020-08-28 14:16:11 +01:00
undergroundwires
1a34c7374b add more windows defender tweaks, categorization and reversibility 2020-08-27 20:32:19 +01:00
undergroundwires
c262681011 add removal of ghost (default0) telemetry user 2020-08-26 21:31:12 +01:00
undergroundwires
f8ba5c46e4 prompt admin priviliges automatically 2020-08-26 01:58:29 +01:00
undergroundwires
b789250cb8 add auto-highlighting of selected/updated code 2020-08-25 16:52:38 +01:00
undergroundwires
5df458739d move script generation to /generation 2020-08-25 16:44:47 +01:00
undergroundwires
d6fa9a2a03 [search] added clear/close button 2020-08-24 02:50:47 +01:00
undergroundwires
ec15af01dd [search] better (multilined) message when there are no results 2020-08-24 02:50:47 +01:00
undergroundwires-bot
7073336f81 ⬆️ bumped to 0.6.2 2020-08-16 16:16:50 +00:00
undergroundwires
3d3380f27e 🔥 removed disabling ClickToRun as it breaks office 2020-08-16 16:58:05 +02:00
undergroundwires
c69998c7cb 🐛 fixed changing time server not working 2020-08-16 16:48:37 +01:00
undergroundwires
1663bfeac7 added script to clear dotnet telemery 2020-08-16 14:19:44 +02:00
undergroundwires
afc3bfb3b8 ⚙️ enhanced tweak to disable for office telemetry 2020-08-16 14:18:20 +02:00
undergroundwires
b6bfc25727 🐛 fixed removing onedrive does not delete scheduled tasks 2020-08-10 19:12:04 +02:00
undergrounwdires
7fac0fe79f 🐛 fixed blank screen and icons on mac 2020-08-10 18:40:16 +01:00
undergrounwdires
5967347b80 🐛 fixed disabling error reporting for november 2019 update 2020-08-09 16:23:28 +02:00
undergroundwires-bot
855a445c1a ⬆️ bumped to 0.6.1 2020-08-09 01:03:43 +00:00
undergroundwires
1cc12195a3 fixed removing onedrive does not clean start menu / quick access 2020-08-09 03:00:19 +01:00
undergroundwires
66d4d39d5b refactorings 2020-08-09 03:00:18 +01:00
undergroundwires
a5dbe66fc1 tweaks to disable webcam, speech and compatibility telemetry 2020-08-09 03:00:18 +01:00
undergroundwires
4c8be45e28 fixed mac / linux download links 2020-08-09 03:00:18 +01:00
undergroundwires
6049a2b834 moved windows connect now to security & recommended 2020-08-09 03:00:18 +01:00
undergroundwires
831c014f97 more scripts can be reverted 2020-08-09 03:00:18 +01:00
undergroundwires
5c15a7a64a fixed typo in footer 2020-08-09 03:00:18 +01:00
dependabot[bot]
e43992b278 Bump elliptic from 6.5.2 to 6.5.3 (#23)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-09 03:00:18 +01:00
undergroundwires
5963d2bac5 updated documentation 2020-08-09 03:00:18 +01:00
155 changed files with 10194 additions and 3816 deletions

View File

@@ -23,7 +23,7 @@ jobs:
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \ --secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
--region us-east-1 \ --region us-east-1 \
&& \ && \
echo "::set-env name=SESSION_NAME::${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)" echo "SESSION_NAME=${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)" >> $GITHUB_ENV
working-directory: aws working-directory: aws
- -
name: "Infrastructure: Deploy IAM stack" name: "Infrastructure: Deploy IAM stack"

View File

@@ -1,145 +0,0 @@
name: Build & deploy
on:
release:
types: [created] # will be triggered when a NON-draft release is created and published.
jobs:
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
runs-on: ubuntu-latest
steps:
-
name: "Infrastructure: Checkout"
uses: actions/checkout@v2
with:
path: aws
repository: undergroundwires/aws-static-site-with-cd
-
name: "Infrastructure: Create AWS user profile & session name"
run: >-
bash "scripts/configure/create-user-profile.sh" \
--profile user \
--access-key-id ${{secrets.AWS_DEPLOYMENT_USER_ACCESS_KEY_ID}} \
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
--region us-east-1 \
&& \
echo "::set-env name=SESSION_NAME::${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)"
working-directory: aws
-
name: "Infrastructure: Deploy IAM stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/iam-stack.yaml \
--stack-name privacysexy-iam-stack \
--capabilities CAPABILITY_IAM \
--parameter-overrides "WebStackName=privacysexy-web-stack DnsStackName=privacysexy-dns-stack \
CertificateStackName=privacysexy-cert-stack RootDomainName=privacy.sexy" \
--region us-east-1 --role-arn ${{secrets.AWS_IAM_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "Infrastructure: Deploy DNS stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/dns-stack.yaml \
--stack-name privacysexy-dns-stack \
--parameter-overrides "RootDomainName=privacy.sexy" \
--region us-east-1 \
--role-arn ${{secrets.AWS_DNS_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "Infrastructure: Deploy certificate stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/certificate-stack.yaml \
--stack-name privacysexy-cert-stack \
--capabilities CAPABILITY_IAM \
--parameter-overrides "IamStackName=privacysexy-iam-stack RootDomainName=privacy.sexy DnsStackName=privacysexy-dns-stack" \
--region us-east-1 \
--role-arn ${{secrets.AWS_CERTIFICATE_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "Infrastructure: Deploy web stack"
run: >-
bash "scripts/deploy/deploy-stack.sh" \
--template-file stacks/web-stack.yaml \
--stack-name privacysexy-web-stack \
--parameter-overrides "CertificateStackName=privacysexy-cert-stack DnsStackName=privacysexy-dns-stack \
RootDomainName=privacy.sexy UseDeepLinks=true" \
--capabilities CAPABILITY_IAM \
--region us-east-1 \
--role-arn ${{secrets.AWS_WEB_STACK_DEPLOYMENT_ROLE_ARN}} \
--profile user --session ${{ env.SESSION_NAME }}
working-directory: aws
-
name: "App: Checkout"
uses: actions/checkout@v2
with:
path: site
ref: master # otherwise we don't get version bump commit
-
name: "App: Setup node"
uses: actions/setup-node@v1
with:
node-version: '14.x'
-
name: "App: Install dependencies"
run: npm ci
working-directory: site
-
name: "App: Run tests"
run: npm run test:unit
working-directory: site
-
name: "App: Build"
run: npm run build
working-directory: site
-
name: "App: Deploy to S3"
run: >-
bash "aws/scripts/deploy/deploy-to-s3.sh" \
--folder site/dist \
--web-stack-name privacysexy-web-stack --web-stack-s3-name-output-name S3BucketName \
--storage-class ONEZONE_IA \
--role-arn ${{secrets.AWS_S3_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \
--profile user --session ${{ env.SESSION_NAME }}
-
name: "App: Invalidate CloudFront cache"
run: >-
bash "aws/scripts/deploy/invalidate-cloudfront-cache.sh" \
--paths "/*" \
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \
--profile user --session ${{ env.SESSION_NAME }}
desktop-deploy:
runs-on: windows-latest
steps:
-
name: Set GitHub PAT token # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#github-personal-access-token
run: set GH_TOKEN=TOKEN-GOES-HERE
-
name: Checkout
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: Upload Release to GitHub # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
run: npm run electron:build -- -p always

View File

@@ -1,6 +1,6 @@
name: Quality checks name: Quality checks
on: push on: [ push, pull_request ]
jobs: jobs:
lint: lint:

View File

@@ -2,6 +2,8 @@ name: Security checks
on: on:
push: push:
pull_request:
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
schedule: schedule:
- cron: '0 0 * * 0' - cron: '0 0 * * 0'

View File

@@ -1,10 +1,13 @@
name: Test name: Test
on: push on: [ push, pull_request ]
jobs: jobs:
run-tests: run-tests:
runs-on: ubuntu-latest strategy:
matrix:
os: [macos, ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
steps: steps:
- -
name: Checkout name: Checkout

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
node_modules node_modules
/dist dist/
.vs .vs
.vscode .vscode
#Electron-builder output #Electron-builder output

View File

@@ -1,5 +1,169 @@
# Changelog # Changelog
## 0.8.1 (2020-11-16)
* refactor removing bloatware to use functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ffa279f3dfe51db564f0a3859543eb212170e173)
* fix reinstalling store apps by searching appx for all users | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c5ab3ea7da159cfb9fbfbbb7cdd28afbee965ea)
* fix clearing jump lists causing os to break and user pin removal #37 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92c3dd923257ac940eab6cbab858698ed55a09b7)
* fix reinstalling store apps by searching appx for all users | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4e7267337301fe4a0480ba0603218fca25c2d096)
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45b8dd972b1edf9e263858c23b27e7a1d2e07077)
* fix not being able to uninstall system apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31e08d231d52e2a691400468b7c599c142a29448)
* fix wrong app names caused by wrong Microsoft docs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e41e40c5bf01e2971d3054fcd3a48f8465a96622)
* unrecommend some system apps and document more | [commit](https://github.com/undergroundwires/privacy.sexy/commit/29c7704e0bd38f6e9923cde84accb569b02d2dd6)
* fix not being able to rename paths including brackets | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ad1872e7cd4ad7ef9facf33fadfa8c6a55065dd3)
* fix errors when file already exists | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c26bc209eb167aa71cad10b7f3ea02d0dedd97b0)
* move Microsoft.Appconnector to right category | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b247b12c3f009aab4350e33f4779fd193e570050)
* replace deprecated github ::set-env command | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ab7d617886a65fe4f3c2daa929168e5678ccae60)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.0...0.8.1)
## 0.8.0 (2020-11-01)
* add support for different recommendation levels: strict and standard | [commit](https://github.com/undergroundwires/privacy.sexy/commit/14be3017c55ed5e0d9bdecb63fcc4e1131e79ab0)
* Add GroupMe and Spotify removal option (#34) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3785c623f837b182d82fa383dfe7709722a67248)
* switch places of download and copy buttons | [commit](https://github.com/undergroundwires/privacy.sexy/commit/50fb29038ae19b17ec006093db02cf1e568d53c3)
* change "download" button to "save" on desktop | [commit](https://github.com/undergroundwires/privacy.sexy/commit/07fc555324d8bf4fa3594a9701daaa124a873153)
* show icons on cards during indeterminate and fully selected states | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1072505219edc47d82a91f148d1f310f32869fea)
* add scripts to increase cryptography, enable camera notifications and remove todo app (#36) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c68408f1ec339dc8d39c7ab044f825a7f7185cb)
* update recommendations to be safer and consistent | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d0019c2c9b1eea620e2e8e02b586903ce62b80e3)
* rework disabling metadata retrieval | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ac70b063b8a15bc528256185792939685be6b36f)
* add all dist folders in gitignore because of files auto-generated by vscode | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a9db31c7778c3269a71c0bd9665827efda70a02)
* add support for shared functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8ce06facbd54184402a4b1af3c7303e64db85b8a)
* hide scrollbars on code area when not overflowing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fd28eaad061c75ea1aa7e0f0d60ea37a7e52f8c4)
* update screenshot | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cfedcd724cad7708b30c7390a7bca3b6313b6726)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.6...0.8.0)
## 0.7.6 (2020-10-18)
* add docs for default0 pointing to github discussion (#30) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a3fc3782efd346b4c99d2a0b40df2eb0229f5b36)
* add robots.txt to explicitly allow indexing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c2f74949b0758d33049bdfa4f0124a28958f8ea)
* add more reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/19a092dd31fb3588277f1ab3120b409d98506752)
* refactor to read more from package.json | [commit](https://github.com/undergroundwires/privacy.sexy/commit/784a67afff681bc19147d03c947de0e165d97e87)
* simplify "why" section | [commit](https://github.com/undergroundwires/privacy.sexy/commit/77c3d2bbb8d13db86bb82ed0b5cbeaacfdea3db9)
* update dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/11e06131655398db08faeeacff62062e46e0dddd)
* run tests on all operating systems: macos, ubuntu, windows | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d9d7f62d81d4d8f95104d33211e82641884d711f)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.5...0.7.6)
## 0.7.5 (2020-09-14)
* fix reverting (reinstalling) capabilities not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/939d838e3535bb1c9b00c8ea9dacb735ae41d700)
* fix tests and checks are not running on PRs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/82d509129b4e4a5df4b84786a0d6842a7d26e888)
* fix the recycling bin option (#32) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/15db3118012a172a2191a2afad57084a65b34642)
* fix rendering issue in older edge/IE | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6efed72bf25c2ddf0901caab7f22966ca13cd47a)
* fix pasting in search bar after page load showing no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d1694341578288eeaf8b80caf9296a38d76789f0)
* fix typo | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7dd15ed06433e0e6583ab0fa46a683ce6554bbea)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.4...0.7.5)
## 0.7.4 (2020-09-12)
* fix checked checkbox has blue border | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0)
* fix spectre protection getting single lined #31 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a)
* fix missing reg value in denying app access to account | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb)
* fix wrong path in clear all firefox user profile settings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4)
## 0.7.3 (2020-09-12)
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
* fix nvidia tweak error message, categorize and add reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/99a2035fdb0766a4dfc2753133eab0d7666516cd)
* improve CPU specific tweaks by conditional platform checks and reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8df5faf4ef05a49da63973bd0fbb5c5d07d5bd93)
* 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) ## 0.5.0 (2020-07-19)
* added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9) * added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9)

View File

@@ -21,16 +21,10 @@
- 🙏 DO - 🙏 DO
- Document your changes in the pull request - Document your changes in the pull request
- ❗ DON'T - ❗ DON'T
- Do not update the versions, current version is only [set by the maintainer](./docs/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere) - 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 ## 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 ### Handle the state in presentation layer
- There are two types of components: - There are two types of components:

View File

@@ -15,23 +15,26 @@
## Get started ## Get started
- Online version: [https://privacy.sexy](https://privacy.sexy) - Online version: [https://privacy.sexy](https://privacy.sexy)
- Or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy-sexy/releases/download/0.5.0/privacy.sexy-Setup-0.5.0.exe), [Linux](https://github.com/undergroundwires/privacy-sexy/releases/download/0.5.0/privacy.sexy-0.5.0.dmg), [macOS](https://github.com/undergroundwires/privacy-sexy/releases/download/0.5.0/privacy.sexy-0.5.0-mac.zip) - or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-Setup-0.8.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-0.8.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-0.8.1.dmg)
- 💡 Come back regularly to apply latest version for stronger privacy and security.
[![privacy.sexy application](img/screenshot.png)](https://privacy.sexy)
## Why ## Why
- You don't need to run any compiled software that has access to your system, just run the generated scripts. - Rich tweak pool to harden security & privacy of the OS and other software on it
- Have full visibility into what the tweaks do as you enable them. - Free (both free as in beer and free as in speech)
- Ability to revert applied scripts - No need to run any compiled software that has access to your system, just run the generated scripts
- Have full visibility into what the tweaks do as you enable them
- Ability to revert (undo) applied scripts
- Everything is transparent: both application and its infrastructure are open-source and automated
- 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) - 📖 If you're unsure about the syntax you can refer to the [application file | documentation](docs/application-file.md).
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
## Commands ## Commands
@@ -46,14 +49,14 @@
- Development: `npm run serve` to compile & hot-reload for development. - Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution. - Production: `npm run build` to prepare files for distribution.
- Or run using Docker: - Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.5.0 .` 1. Build: `docker build -t undergroundwires/privacy.sexy:0.8.1 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.5.0 undergroundwires/privacy.sexy:0.5.0` 2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.8.1 undergroundwires/privacy.sexy:0.8.1`
## 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.
@@ -66,11 +69,11 @@
- 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))
![DDD + vue.js](docs/app-ddd.png) ![DDD + vue.js](img/architecture/app-ddd.png)
### AWS Infrastructure ### AWS Infrastructure
[![AWS solution](docs/aws-solution.png)](https://github.com/undergroundwires/aws-static-site-with-cd) [![AWS solution](img/architecture/aws-solution.png)](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/).
@@ -82,4 +85,4 @@
- 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.
[![CI/CD to AWS with GitHub Actions](docs/gitops.png)](.github/workflows/) [![CI/CD to AWS with GitHub Actions](img/architecture/gitops.png)](.github/workflows/)

5
build/README.md Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

BIN
build/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
build/icons/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

BIN
build/icons/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
build/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
build/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
build/icons/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
build/icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
build/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
build/icons/icon.icns Normal file

Binary file not shown.

BIN
build/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

139
docs/application-file.md Normal file
View File

@@ -0,0 +1,139 @@
# Application file
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from [`application.yaml`](./../src/application/application.yaml)
- 💡 Best practices
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
- 📖 Types in code: [`application.d.ts`](./../src/application/application.yaml.d.ts)
## Objects
### `Application`
- Application file simply defines different categories and their scripts in a tree structure.
- Application file also allows defining common [function](#function)s to be used throughout the application if you'd like different scripts to share same code.
#### `Application` syntax
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
- Each [category](#category) is rendered as different cards in card presentation.
- ❗ Application must consist of at least one category.
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
- Functions are optionally defined to re-use the same code throughout different scripts.
### `Category`
- Category has a parent that has tree-like structure where it can have subcategories or subscripts.
- It's a logical grouping of different scripts and other categories.
#### `Category` syntax
- `category:` *`string`* (**required**)
- Name of the category
- ❗ Must be unique throughout the application
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
- ❗ Category must consist of at least one subcategory or script.
- Children can be combination of scripts and subcategories.
### `Script`
- Script represents a single tweak.
- A script must include either:
- A `code` and `revertCode`
- Or `call` to call YAML-defined functions
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
#### `Script` syntax
- `name`: *`string`* (**required**)
- Name of the script
- ❗ Must be unique throughout the application
- E.g. `Disable targeted ads`
- `code`: *`string`* (may be **required**)
- Batch file commands that will be executed
- 💡 If defined, best practice to also define `revertCode`
- ❗ If not defined `call` must be defined, do not define if `call` is defined.
- `revertCode`: `string`
- Code that'll undo the change done by `code` property.
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
- ❗ Do not define if `call` is defined.
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
- A shared function or sequence of functions to call (called in order)
- ❗ If not defined `code` must be defined
- `docs`: *`string`* | `[`*`string`*`, ... ]`
- Single documentation URL or list of URLs for those who wants to learn more about the script
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
- If not defined then the script will not be recommended
- If defined it can be either
- `standard`: Only non-breaking scripts without limiting OS functionality
- `strict`: Scripts that can break certain functionality in favor of privacy and security
### `FunctionCall`
- Describes a single call to a function by optionally providing values to its parameters.
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
#### `FunctionCall` syntax
- `function`: *`string`* (**required**)
- Name of the function to call.
- ❗ Function with same name must defined in `functions` property of [Application](#application)
- `parameters`: `[ parameterName:` *`parameterValue`*`, ... ]`
- Defines key value dictionary for each parameter and its value
- E.g.
```yaml
parameters:
userDefinedParameterName: parameterValue
# ...
appName: Microsoft.WindowsFeedbackHub
```
### `Function`
- Functions allow re-usable code throughout the defined scripts.
- Functions are templates compiled by privacy.sexy and uses special expressions.
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
#### Parameter substitution
A simple function example
```yaml
function: EchoArgument
parameters: [ 'argument' ]
code: Hello {{ $argument }} !
```
It would print "Hello world" if it's called in a [script](#script) as following:
```yaml
script: Echo script
call:
function: EchoArgument
parameters:
argument: World
```
#### `Function` syntax
- `name`: *`string`* (**required**)
- Name of the function that scripts will use.
- Convention is to use camelCase, and be verbs.
- E.g. `uninstallStoreApp`
- ❗ Function names must be unique
- `parameters`: `[` *`string`* `, ... ]`
- Name of the parameters that the function has.
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#functioncall)
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
- ❗ Parameter names must be unique
`code`: *`string`* (**required**)
- Batch file commands that will be executed
- 💡 If defined, best practice to also define `revertCode`
- `revertCode`: *`string`*
- Code that'll undo the change done by `code` property.
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 460 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

File diff suppressed because one or more lines are too long

BIN
img/architecture/gitops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

BIN
img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

3618
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.5.0", "version": "0.8.1",
"author": "undergroundwires", "author": "undergroundwires",
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆", "description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
"homepage": "https://privacy.sexy",
"private": true, "private": true,
"repository": {
"type": "git",
"url": "https://github.com/undergroundwires/privacy.sexy.git"
},
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@@ -21,47 +26,46 @@
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.30", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-brands-svg-icons": "^5.14.0", "@fortawesome/free-brands-svg-icons": "^5.15.1",
"@fortawesome/free-regular-svg-icons": "^5.14.0", "@fortawesome/free-regular-svg-icons": "^5.15.1",
"@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/vue-fontawesome": "^0.1.10", "@fortawesome/vue-fontawesome": "^2.0.2",
"ace-builds": "^1.4.12", "ace-builds": "^1.4.12",
"file-saver": "^2.0.2", "file-saver": "^2.0.5",
"inversify": "^5.0.1", "inversify": "^5.0.5",
"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.2.5", "vue-class-component": "^7.2.6",
"vue-js-modal": "^2.0.0-rc.6", "vue-js-modal": "^2.0.0-rc.6",
"vue-property-decorator": "^9.0.0" "vue-property-decorator": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/ace": "0.0.43", "@types/ace": "0.0.44",
"@types/chai": "^4.2.12", "@types/chai": "^4.2.14",
"@types/file-saver": "^2.0.1", "@types/file-saver": "^2.0.1",
"@types/mocha": "^8.0.0", "@types/mocha": "^8.2.0",
"@types/node": "12.0.0", "@vue/cli-plugin-typescript": "^4.5.9",
"@vue/cli-plugin-typescript": "^4.4.6", "@vue/cli-plugin-unit-mocha": "^4.5.9",
"@vue/cli-plugin-unit-mocha": "^4.4.6", "@vue/cli-service": "^4.5.9",
"@vue/cli-service": "^4.4.6", "@vue/test-utils": "1.1.2",
"@vue/test-utils": "1.0.3",
"chai": "^4.2.0", "chai": "^4.2.0",
"electron": "^9.1.1", "electron": "^11.1.0",
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-log": "^4.2.2", "electron-log": "^4.3.1",
"electron-updater": "^4.3.4", "electron-updater": "^4.3.5",
"js-yaml-loader": "^1.2.2", "js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.23.2", "markdownlint-cli": "^0.26.0",
"remark-cli": "^8.0.1", "remark-cli": "^9.0.0",
"remark-lint-no-dead-urls": "^1.1.0", "remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^3.0.1", "remark-preset-lint-consistent": "^4.0.0",
"remark-validate-links": "^10.0.2", "remark-validate-links": "^10.0.2",
"sass": "^1.26.10", "sass": "^1.30.0",
"sass-loader": "^9.0.2", "sass-loader": "^10.1.0",
"typescript": "^3.9.7", "typescript": "^4.1.3",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4", "vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.12",
"yaml-lint": "^1.2.4" "yaml-lint": "^1.2.4"
} }
} }

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow:

View File

@@ -12,8 +12,7 @@
</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 } from '@/application/State/ApplicationState';
import TheHeader from '@/presentation/TheHeader.vue'; import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter/TheFooter.vue'; import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
import TheCodeArea from '@/presentation/TheCodeArea.vue'; import TheCodeArea from '@/presentation/TheCodeArea.vue';

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '../OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { DetectorBuilder } from './DetectorBuilder'; import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector'; import { IBrowserOsDetector } from './IBrowserOsDetector';

View File

@@ -1,5 +1,5 @@
import { IBrowserOsDetector } from './IBrowserOsDetector'; import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '../OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export class DetectorBuilder { export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>(); private readonly existingPartsInUserAgent = new Array<string>();
@@ -8,19 +8,11 @@ export class DetectorBuilder {
constructor(private readonly os: OperatingSystem) { } constructor(private readonly os: OperatingSystem) { }
public mustInclude(str: string): DetectorBuilder { public mustInclude(str: string): DetectorBuilder {
if (!str) { return this.add(str, this.existingPartsInUserAgent);
throw new Error('part to include is empty or undefined');
}
this.existingPartsInUserAgent.push(str);
return this;
} }
public mustNotInclude(str: string): DetectorBuilder { public mustNotInclude(str: string): DetectorBuilder {
if (!str) { return this.add(str, this.notExistingPartsInUserAgent);
throw new Error('part to not include is empty or undefined');
}
this.notExistingPartsInUserAgent.push(str);
return this;
} }
public build(): IBrowserOsDetector { public build(): IBrowserOsDetector {
@@ -28,22 +20,34 @@ export class DetectorBuilder {
throw new Error('Must include at least a part'); throw new Error('Must include at least a part');
} }
return { return {
detect: (userAgent) => { detect: (agent) => this.detect(agent),
if (!userAgent) {
throw new Error('User agent is null or undefined');
}
for (const exitingPart of this.existingPartsInUserAgent) {
if (!userAgent.includes(exitingPart)) {
return OperatingSystem.Unknown;
}
}
for (const notExistingPart of this.notExistingPartsInUserAgent) {
if (userAgent.includes(notExistingPart)) {
return OperatingSystem.Unknown;
}
}
return this.os;
},
}; };
} }
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;
}
} }

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from '../OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector { export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem; detect(userAgent: string): OperatingSystem;

View File

@@ -1,7 +1,7 @@
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment'; import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
interface IEnvironmentVariables { interface IEnvironmentVariables {
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;

View File

@@ -1,4 +1,4 @@
import { OperatingSystem } from './OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment { export interface IEnvironment {
isDesktop: boolean; isDesktop: boolean;

View File

@@ -1,24 +1,37 @@
import { Category } from '@/domain/Category'; import { Category } from '@/domain/Category';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml'; import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
import { parseCategory } from './CategoryParser'; import { parseCategory } from './CategoryParser';
import { ProjectInformation } from '@/domain/ProjectInformation';
import { ScriptCompiler } from './Compiler/ScriptCompiler';
export function parseApplication(content: ApplicationYaml): IApplication {
export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEnv = process.env): IApplication {
validate(content); validate(content);
const compiler = new ScriptCompiler(content.functions);
const categories = new Array<Category>(); const categories = new Array<Category>();
for (const action of content.actions) { for (const action of content.actions) {
const category = parseCategory(action); const category = parseCategory(action, compiler);
categories.push(category); categories.push(category);
} }
const info = readAppInformation(env);
const app = new Application( const app = new Application(
content.name, info,
content.repositoryUrl,
process.env.VUE_APP_VERSION,
categories); categories);
return app; return app;
} }
function readAppInformation(environment: NodeJS.ProcessEnv): IProjectInformation {
return new ProjectInformation(
environment.VUE_APP_NAME,
environment.VUE_APP_VERSION,
environment.VUE_APP_REPOSITORY_URL,
environment.VUE_APP_HOMEPAGE_URL,
);
}
function validate(content: ApplicationYaml): void { function validate(content: ApplicationYaml): void {
if (!content) { if (!content) {
throw new Error('application is null or undefined'); throw new Error('application is null or undefined');

View File

@@ -3,6 +3,7 @@ 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'; import { parseScript } from './ScriptParser';
import { IScriptCompiler } from './Compiler/IScriptCompiler';
let categoryIdCounter: number = 0; let categoryIdCounter: number = 0;
@@ -11,14 +12,17 @@ interface ICategoryChildren {
subScripts: Script[]; subScripts: Script[];
} }
export function parseCategory(category: YamlCategory): Category { export function parseCategory(category: YamlCategory, compiler: IScriptCompiler): Category {
if (!compiler) {
throw new Error('undefined compiler');
}
ensureValid(category); ensureValid(category);
const children: ICategoryChildren = { const children: ICategoryChildren = {
subCategories: new Array<Category>(), subCategories: new Array<Category>(),
subScripts: new Array<Script>(), subScripts: new Array<Script>(),
}; };
for (const categoryOrScript of category.children) { for (const categoryOrScript of category.children) {
parseCategoryChild(categoryOrScript, children, category); parseCategoryChild(categoryOrScript, children, category, compiler);
} }
return new Category( return new Category(
/*id*/ categoryIdCounter++, /*id*/ categoryIdCounter++,
@@ -42,22 +46,26 @@ function ensureValid(category: YamlCategory) {
} }
function parseCategoryChild( function parseCategoryChild(
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) { categoryOrScript: any,
children: ICategoryChildren,
parent: YamlCategory,
compiler: IScriptCompiler) {
if (isCategory(categoryOrScript)) { if (isCategory(categoryOrScript)) {
const subCategory = parseCategory(categoryOrScript as YamlCategory); const subCategory = parseCategory(categoryOrScript as YamlCategory, compiler);
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 = parseScript(yamlScript); const script = parseScript(yamlScript, compiler);
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)
|| categoryOrScript.call;
} }
function isCategory(categoryOrScript: any): boolean { function isCategory(categoryOrScript: any): boolean {

View File

@@ -0,0 +1,7 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { YamlScript } from 'js-yaml-loader!./application.yaml';
export interface IScriptCompiler {
canCompile(script: YamlScript): boolean;
compile(script: YamlScript): IScriptCode;
}

View File

@@ -0,0 +1,200 @@
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
import { YamlScript, YamlFunction, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
import { IScriptCompiler } from './IScriptCompiler';
interface ICompiledCode {
readonly code: string;
readonly revertCode: string;
}
export class ScriptCompiler implements IScriptCompiler {
constructor(private readonly functions: readonly YamlFunction[]) {
ensureValidFunctions(functions);
}
public canCompile(script: YamlScript): boolean {
if (!script.call) {
return false;
}
return true;
}
public compile(script: YamlScript): IScriptCode {
this.ensureCompilable(script.call);
const compiledCodes = new Array<ICompiledCode>();
const calls = getCallSequence(script.call);
calls.forEach((currentCall, currentCallIndex) => {
ensureValidCall(currentCall, script.name);
const commonFunction = this.getFunctionByName(currentCall.function);
let functionCode = compileCode(commonFunction, currentCall.parameters);
if (currentCallIndex !== calls.length - 1) {
functionCode = appendLine(functionCode);
}
compiledCodes.push(functionCode);
});
const scriptCode = merge(compiledCodes);
return new ScriptCode(script.name, scriptCode.code, scriptCode.revertCode);
}
private getFunctionByName(name: string): YamlFunction {
const func = this.functions.find((f) => f.name === name);
if (!func) {
throw new Error(`called function is not defined "${name}"`);
}
return func;
}
private ensureCompilable(call: ScriptFunctionCall) {
if (!this.functions || this.functions.length === 0) {
throw new Error('cannot compile without shared functions');
}
if (typeof call !== 'object') {
throw new Error('called function(s) must be an object');
}
}
}
function getDuplicates(texts: readonly string[]): string[] {
return texts.filter((item, index) => texts.indexOf(item) !== index);
}
function printList(list: readonly string[]): string {
return `"${list.join('","')}"`;
}
function ensureNoDuplicatesInFunctionNames(functions: readonly YamlFunction[]) {
const duplicateFunctionNames = getDuplicates(functions
.map((func) => func.name.toLowerCase()));
if (duplicateFunctionNames.length) {
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
}
}
function ensureNoDuplicatesInParameterNames(functions: readonly YamlFunction[]) {
const functionsWithParameters = functions
.filter((func) => func.parameters && func.parameters.length > 0);
for (const func of functionsWithParameters) {
const duplicateParameterNames = getDuplicates(func.parameters);
if (duplicateParameterNames.length) {
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
}
}
}
function ensureNoDuplicateCode(functions: readonly YamlFunction[]) {
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
if (duplicateCodes.length > 0) {
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
}
const duplicateRevertCodes = getDuplicates(functions
.filter((func) => func.revertCode)
.map((func) => func.revertCode));
if (duplicateRevertCodes.length > 0) {
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
}
}
function ensureValidFunctions(functions: readonly YamlFunction[]) {
if (!functions) {
return;
}
ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicatesInParameterNames(functions);
ensureNoDuplicateCode(functions);
}
function appendLine(code: ICompiledCode): ICompiledCode {
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
return {
code: appendLineIfNotEmpty(code.code),
revertCode: appendLineIfNotEmpty(code.revertCode),
};
}
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
return {
code: codes.map((code) => code.code).join(''),
revertCode: codes.map((code) => code.revertCode).join(''),
};
}
function compileCode(func: YamlFunction, parameters: FunctionCallParameters): ICompiledCode {
return {
code: compileExpressions(func.code, parameters),
revertCode: compileExpressions(func.revertCode, parameters),
};
}
function compileExpressions(code: string, parameters: FunctionCallParameters): string {
let intermediateCode = compileToIL(code);
intermediateCode = substituteParameters(intermediateCode, parameters);
ensureNoExpressionLeft(intermediateCode);
return intermediateCode;
}
function substituteParameters(intermediateCode: string, parameters: FunctionCallParameters): string {
const parameterNames = getUniqueParameterNamesFromIL(intermediateCode);
if (parameterNames.length && !parameters) {
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
}
for (const parameterName of parameterNames) {
const parameterValue = parameters[parameterName];
intermediateCode = substituteParameter(intermediateCode, parameterName, parameterValue);
}
return intermediateCode;
}
function ensureValidCall(call: FunctionCall, scriptName: string) {
if (!call) {
throw new Error(`undefined function call in script "${scriptName}"`);
}
if (!call.function) {
throw new Error(`empty function name called in script "${scriptName}"`);
}
}
function getCallSequence(call: ScriptFunctionCall): FunctionCall[] {
if (call instanceof Array) {
return call as FunctionCall[];
}
return [ call as FunctionCall ];
}
function getDistinctValues(values: readonly string[]): string[] {
return values.filter((value, index, self) => {
return self.indexOf(value) === index;
});
}
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
function compileToIL(code: string) {
return code.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
return `\{\{exp|${match.trim()}\}\}`;
});
}
// Parses all distinct usages of {{exp|$parameterName}}
function getUniqueParameterNamesFromIL(ilCode: string) {
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
const uniqueParameterNames = getDistinctValues(allParameters);
return uniqueParameterNames;
}
// substitutes {{exp|$parameterName}} to value of the parameter
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
if (!parameterValue) {
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
}
const pattern = `{{exp|$${parameterName}}}`;
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
}
// finds all "{{exp|..}} left"
function ensureNoExpressionLeft(ilCode: string) {
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
const uniqueExpressions = getDistinctValues(allMatches);
if (uniqueExpressions.length > 0) {
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
}
}

View File

@@ -1,37 +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) { if (!documentable) {
throw new Error('documentable is null or undefined'); 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: readonly 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;
} }

View File

@@ -1,16 +1,61 @@
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { YamlScript } from 'js-yaml-loader!./application.yaml'; import { YamlScript } from 'js-yaml-loader!./application.yaml';
import { parseDocUrls } from './DocumentationParser'; import { parseDocUrls } from './DocumentationParser';
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
import { IScriptCompiler } from './Compiler/IScriptCompiler';
import { IScriptCode } from '@/domain/IScriptCode';
import { ScriptCode } from '@/domain/ScriptCode';
export function parseScript(yamlScript: YamlScript): Script { export function parseScript(yamlScript: YamlScript, compiler: IScriptCompiler): Script {
if (!yamlScript) { validateScript(yamlScript);
throw new Error('script is null or undefined'); if (!compiler) {
throw new Error('undefined compiler');
} }
const script = new Script( const script = new Script(
/* name */ yamlScript.name, /* name */ yamlScript.name,
/* code */ yamlScript.code, /* code */ parseCode(yamlScript, compiler),
/* revertCode */ yamlScript.revertCode,
/* docs */ parseDocUrls(yamlScript), /* docs */ parseDocUrls(yamlScript),
/* isRecommended */ yamlScript.recommend); /* level */ getLevel(yamlScript.recommend));
return script; return script;
} }
function getLevel(level: string): RecommendationLevel | undefined {
if (!level) {
return undefined;
}
if (typeof level !== 'string') {
throw new Error(`level must be a string but it was ${typeof level}`);
}
const typedLevel = RecommendationLevelNames
.find((l) => l.toLowerCase() === level.toLowerCase());
if (!typedLevel) {
throw new Error(`unknown level: \"${level}\"`);
}
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
}
function parseCode(yamlScript: YamlScript, compiler: IScriptCompiler): IScriptCode {
if (compiler.canCompile(yamlScript)) {
return compiler.compile(yamlScript);
}
return new ScriptCode(yamlScript.name, yamlScript.code, yamlScript.revertCode);
}
function ensureNotBothCallAndCode(yamlScript: YamlScript) {
if (yamlScript.code && yamlScript.call) {
throw new Error('cannot define both "call" and "code"');
}
if (yamlScript.revertCode && yamlScript.call) {
throw new Error('cannot define "revertCode" if "call" is defined');
}
}
function validateScript(yamlScript: YamlScript) {
if (!yamlScript) {
throw new Error('undefined script');
}
if (!yamlScript.code && !yamlScript.call) {
throw new Error('must define either "call" or "code"');
}
ensureNotBothCallAndCode(yamlScript);
}

View File

@@ -11,6 +11,7 @@ import { Script } from '@/domain/Script';
import { IApplication } from '@/domain/IApplication'; 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 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 {
@@ -37,8 +38,8 @@ export class ApplicationState implements IApplicationState {
public readonly app: IApplication, 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.info.version);
this.filter = new UserFilter(app); this.filter = new UserFilter(app);
} }
} }

View File

@@ -1,30 +1,38 @@
import { CodeChangedEvent } from './Event/CodeChangedEvent';
import { CodePosition } from './Position/CodePosition';
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { SelectedScript } from '@/application/State/Selection/SelectedScript'; import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/State/Selection/IUserSelection'; import { IUserSelection } from '@/application/State/Selection/IUserSelection';
import { UserScriptGenerator } from './UserScriptGenerator'; 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 { IUserScriptGenerator } from './IUserScriptGenerator'; 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: IUserScriptGenerator = new UserScriptGenerator(); private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor( constructor(
userSelection: IUserSelection, userSelection: IUserSelection,
private readonly version: string) { 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<SelectedScript>) { 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);
} }
} }

View 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);
}

View 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;
}

View File

@@ -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;
} }

View 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>;
}

View File

@@ -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;
}

View 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.code.revert : selection.script.code.execute;
builder.appendFunction(name, scriptCode);
}

View File

@@ -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;
} }

View File

@@ -1,5 +0,0 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
export interface IUserScriptGenerator {
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
}

View 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');
}
}
}

View File

@@ -0,0 +1,5 @@
export interface ICodePosition {
readonly startLine: number;
readonly endLine: number;
readonly totalLines: number;
}

View File

@@ -1,34 +0,0 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { CodeBuilder } from './CodeBuilder';
export 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 implements IUserScriptGenerator {
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string {
if (!selectedScripts) { throw new Error('scripts is undefined'); }
if (!selectedScripts.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 selection of selectedScripts) {
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
const code = selection.revert ? selection.script.revertCode : selection.script.code;
builder.appendFunction(name, code).appendLine();
}
return builder.appendLine()
.appendLine('pause')
.appendLine('exit /b 0')
.toString();
}
}

View File

@@ -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;

View File

@@ -8,6 +8,7 @@ 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: IApplication) { constructor(private application: IApplication) {
@@ -22,17 +23,17 @@ export class UserFilter implements IUserFilter {
(script) => isScriptAMatch(script, filterLowercase)); (script) => isScriptAMatch(script, filterLowercase));
const filteredCategories = this.application.getAllCategories().filter( const filteredCategories = this.application.getAllCategories().filter(
(category) => category.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();
} }
} }
@@ -41,11 +42,11 @@ function isScriptAMatch(script: IScript, filterLowercase: string) {
if (script.name.toLowerCase().includes(filterLowercase)) { if (script.name.toLowerCase().includes(filterLowercase)) {
return true; return true;
} }
if (script.code.toLowerCase().includes(filterLowercase)) { if (script.code.execute.toLowerCase().includes(filterLowercase)) {
return true; return true;
} }
if (script.revertCode) { if (script.code.revert) {
return script.revertCode.toLowerCase().includes(filterLowercase); return script.code.revert.toLowerCase().includes(filterLowercase);
} }
return false; return false;
} }

View File

@@ -1,16 +1,21 @@
import { SelectedScript } from './SelectedScript'; 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';
import { ICategory } from '@/domain/ICategory';
export interface IUserSelection { export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<SelectedScript>>; readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number; readonly totalSelected: number;
areAllSelected(category: ICategory): boolean;
isAnySelected(category: ICategory): boolean;
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void; addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(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;
} }

View File

@@ -1,5 +1,5 @@
import { SelectedScript } from './SelectedScript'; import { SelectedScript } from './SelectedScript';
import { IApplication } from '@/domain/IApplication'; import { IApplication, ICategory } 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/IScript'; import { IScript } from '@/domain/IScript';
@@ -8,20 +8,67 @@ import { IRepository } from '@/infrastructure/Repository/IRepository';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>(); public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>(); private readonly scripts: IRepository<string, SelectedScript>;
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) {
const selected = new SelectedScript(script, false); this.scripts.addItem(script);
this.scripts.addItem(selected);
} }
} }
} }
public areAllSelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
const scripts = category.getAllScriptsRecursively();
if (this.selectedScripts.length < scripts.length) {
return false;
}
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
}
public isAnySelected(category: ICategory): boolean {
if (this.selectedScripts.length === 0) {
return false;
}
return this.selectedScripts.some((s) => category.includes(s.script));
}
public removeAllInCategory(categoryId: number): 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 { public addSelectedScript(scriptId: string, revert: boolean): void {
const script = this.app.findScript(scriptId); const script = this.app.findScript(scriptId);
if (!script) { if (!script) {
@@ -44,8 +91,8 @@ export class UserSelection implements IUserSelection {
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.id); return this.scripts.exists(scriptId);
} }
/** Get users scripts based on his/her selections */ /** Get users scripts based on his/her selections */

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,7 @@
declare module 'js-yaml-loader!*' { declare module 'js-yaml-loader!*' {
export type CategoryOrScript = YamlCategory | YamlScript; export interface ApplicationYaml {
type DocumentationUrls = ReadonlyArray<string> | string; actions: ReadonlyArray<YamlCategory>;
functions: ReadonlyArray<YamlFunction> | undefined;
export interface YamlDocumentable {
docs?: DocumentationUrls;
}
export interface YamlScript extends YamlDocumentable {
name: string;
code: string;
revertCode: string;
recommend: boolean;
} }
export interface YamlCategory extends YamlDocumentable { export interface YamlCategory extends YamlDocumentable {
@@ -18,10 +9,37 @@ declare module 'js-yaml-loader!*' {
category: string; category: string;
} }
export interface ApplicationYaml { export type CategoryOrScript = YamlCategory | YamlScript;
export type DocumentationUrls = ReadonlyArray<string> | string;
export interface YamlDocumentable {
docs?: DocumentationUrls;
}
export interface YamlFunction {
name: string; name: string;
repositoryUrl: string; code: string;
actions: ReadonlyArray<YamlCategory>; revertCode?: string;
parameters?: readonly string[];
}
export interface FunctionCallParameters {
[index: string]: string;
}
export interface FunctionCall {
function: string;
parameters?: FunctionCallParameters;
}
export type ScriptFunctionCall = readonly FunctionCall[] | FunctionCall | undefined;
export interface YamlScript extends YamlDocumentable {
name: string;
code: string | undefined;
revertCode: string | undefined;
call: ScriptFunctionCall;
recommend: string | undefined;
} }
const content: ApplicationYaml; const content: ApplicationYaml;

View File

@@ -35,12 +35,12 @@ function createWindow() {
height: 955, height: 955,
webPreferences: { webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone // Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info // See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
nodeIntegration: (process.env nodeIntegration: (process.env
.ELECTRON_NODE_INTEGRATION as unknown) as boolean, .ELECTRON_NODE_INTEGRATION as unknown) as boolean,
}, },
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#icons // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
icon: path.join(__static, `favicon.ico`), icon: path.join(__static, 'icon.png'),
}); });
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);

View File

@@ -2,53 +2,51 @@ import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory'; import { ICategory } from './ICategory';
import { IScript } from './IScript'; import { IScript } from './IScript';
import { IApplication } from './IApplication'; import { IApplication } from './IApplication';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel, RecommendationLevelNames, RecommendationLevels } from './RecommendationLevel';
export class Application implements IApplication { export class Application implements IApplication {
public get totalScripts(): number { return this.flattened.allScripts.length; } public get totalScripts(): number { return this.queryable.allScripts.length; }
public get totalCategories(): number { return this.flattened.allCategories.length; } public get totalCategories(): number { return this.queryable.allCategories.length; }
private readonly flattened: IFlattenedApplication; private readonly queryable: IQueryableApplication;
constructor( constructor(
public readonly name: string, public readonly info: IProjectInformation,
public readonly repositoryUrl: string,
public readonly version: string,
public readonly actions: ReadonlyArray<ICategory>) { public readonly actions: ReadonlyArray<ICategory>) {
if (!name) { throw Error('Application has no name'); } if (!info) {
if (!repositoryUrl) { throw Error('Application has no repository url'); } throw new Error('info is undefined');
if (!version) { throw Error('Version cannot be empty'); }
this.flattened = flatten(actions);
if (this.flattened.allCategories.length === 0) {
throw new Error('Application must consist of at least one category');
} }
if (this.flattened.allScripts.length === 0) { this.queryable = makeQueryable(actions);
throw new Error('Application must consist of at least one script'); ensureValid(this.queryable);
} ensureNoDuplicates(this.queryable.allCategories);
if (this.flattened.allScripts.filter((script) => script.isRecommended).length === 0) { ensureNoDuplicates(this.queryable.allScripts);
throw new Error('Application must consist of at least one recommended script');
}
ensureNoDuplicates(this.flattened.allCategories);
ensureNoDuplicates(this.flattened.allScripts);
} }
public findCategory(categoryId: number): ICategory | undefined { public findCategory(categoryId: number): ICategory | undefined {
return this.flattened.allCategories.find((category) => category.id === categoryId); return this.queryable.allCategories.find((category) => category.id === categoryId);
} }
public getRecommendedScripts(): readonly IScript[] { public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
return this.flattened.allScripts.filter((script) => script.isRecommended); if (isNaN(level)) {
throw new Error('undefined level');
}
if (!(level in RecommendationLevel)) {
throw new Error(`invalid level: ${level}`);
}
return this.queryable.scriptsByLevel.get(level);
} }
public findScript(scriptId: string): IScript | undefined { public findScript(scriptId: string): IScript | undefined {
return this.flattened.allScripts.find((script) => script.id === scriptId); return this.queryable.allScripts.find((script) => script.id === scriptId);
} }
public getAllScripts(): IScript[] { public getAllScripts(): IScript[] {
return this.flattened.allScripts; return this.queryable.allScripts;
} }
public getAllCategories(): ICategory[] { public getAllCategories(): ICategory[] {
return this.flattened.allCategories; return this.queryable.allCategories;
} }
} }
@@ -70,35 +68,85 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
} }
} }
interface IFlattenedApplication { interface IQueryableApplication {
allCategories: ICategory[]; allCategories: ICategory[];
allScripts: IScript[]; allScripts: IScript[];
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
} }
function flattenRecursive( function ensureValid(application: IQueryableApplication) {
categories: ReadonlyArray<ICategory>, ensureValidCategories(application.allCategories);
flattened: IFlattenedApplication) { ensureValidScripts(application.allScripts);
for (const category of categories) { }
flattened.allCategories.push(category);
if (category.scripts) { function ensureValidCategories(allCategories: readonly ICategory[]) {
for (const script of category.scripts) { if (!allCategories || allCategories.length === 0) {
flattened.allScripts.push(script); throw new Error('Application must consist of at least one category');
} }
} }
if (category.subCategories && category.subCategories.length > 0) {
flattenRecursive( function ensureValidScripts(allScripts: readonly IScript[]) {
category.subCategories as ReadonlyArray<ICategory>, if (!allScripts || allScripts.length === 0) {
flattened); throw new Error('Application must consist of at least one script');
}
for (const level of RecommendationLevels) {
if (allScripts.every((script) => script.level !== level)) {
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
} }
} }
} }
function flatten( function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
categories: ReadonlyArray<ICategory>): IFlattenedApplication { const allCategories = new Array<ICategory>();
const flattened: IFlattenedApplication = { const allScripts = new Array<IScript>();
allCategories: new Array<ICategory>(), flattenCategories(categories, allCategories, allScripts);
allScripts: new Array<IScript>(), return [
}; allCategories,
flattenRecursive(categories, flattened); allScripts,
return flattened; ];
}
function flattenCategories(
categories: ReadonlyArray<ICategory>,
allCategories: ICategory[],
allScripts: IScript[]): IQueryableApplication {
if (!categories || categories.length === 0) {
return;
}
for (const category of categories) {
allCategories.push(category);
flattenScripts(category.scripts, allScripts);
flattenCategories(category.subCategories, allCategories, allScripts);
}
}
function flattenScripts(
scripts: ReadonlyArray<IScript>,
allScripts: IScript[]): IScript[] {
if (!scripts) {
return;
}
for (const script of scripts) {
allScripts.push(script);
}
}
function makeQueryable(
actions: ReadonlyArray<ICategory>): IQueryableApplication {
const flattened = flattenApplication(actions);
return {
allCategories: flattened[0],
allScripts: flattened[1],
scriptsByLevel: groupByLevel(flattened[1]),
};
}
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
const map = new Map<RecommendationLevel, readonly IScript[]>();
for (const levelName of RecommendationLevelNames) {
const level = RecommendationLevel[levelName];
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
map.set(level, scripts);
}
return map;
} }

View File

@@ -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,31 @@ 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 includes(script: IScript): boolean {
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
}
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');
} }
} }

View File

@@ -1,15 +1,15 @@
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel } from './RecommendationLevel';
export interface IApplication { export interface IApplication {
readonly name: string; readonly info: IProjectInformation;
readonly repositoryUrl: string;
readonly version: string;
readonly totalScripts: number; readonly totalScripts: number;
readonly totalCategories: number; readonly totalCategories: number;
readonly actions: ReadonlyArray<ICategory>; readonly actions: ReadonlyArray<ICategory>;
getRecommendedScripts(): ReadonlyArray<IScript>; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
findCategory(categoryId: number): ICategory | undefined; findCategory(categoryId: number): ICategory | undefined;
findScript(scriptId: string): IScript | undefined; findScript(scriptId: string): IScript | undefined;
getAllScripts(): ReadonlyArray<IScript>; getAllScripts(): ReadonlyArray<IScript>;

View File

@@ -7,6 +7,8 @@ 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>;
includes(script: IScript): boolean;
getAllScriptsRecursively(): ReadonlyArray<IScript>;
} }
export { IEntity } from '../infrastructure/Entity/IEntity'; export { IEntity } from '../infrastructure/Entity/IEntity';

View File

@@ -0,0 +1,11 @@
import { OperatingSystem } from './OperatingSystem';
export interface IProjectInformation {
readonly name: string;
readonly version: string;
readonly repositoryUrl: string;
readonly homepage: string;
readonly feedbackUrl: string;
readonly releaseUrl: string;
readonly repositoryWebUrl: string;
getDownloadUrl(os: OperatingSystem): string;
}

View File

@@ -1,11 +1,12 @@
import { IEntity } from '../infrastructure/Entity/IEntity'; import { IEntity } from '../infrastructure/Entity/IEntity';
import { IDocumentable } from './IDocumentable'; import { IDocumentable } from './IDocumentable';
import { RecommendationLevel } from './RecommendationLevel';
import { IScriptCode } from './IScriptCode';
export interface IScript extends IEntity<string>, IDocumentable { export interface IScript extends IEntity<string>, IDocumentable {
readonly name: string; readonly name: string;
readonly isRecommended: boolean; readonly level?: RecommendationLevel;
readonly documentationUrls: ReadonlyArray<string>; readonly documentationUrls: ReadonlyArray<string>;
readonly code: string; readonly code: IScriptCode;
readonly revertCode: string;
canRevert(): boolean; canRevert(): boolean;
} }

View File

@@ -0,0 +1,4 @@
export interface IScriptCode {
readonly execute: string;
readonly revert: string;
}

View File

@@ -0,0 +1,55 @@
import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem';
export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string;
constructor(
public readonly name: string,
public readonly version: string,
public readonly repositoryUrl: string,
public readonly homepage: string,
) {
if (!name) {
throw new Error('name is undefined');
}
if (!version || +version <= 0) {
throw new Error('version should be higher than zero');
}
if (!repositoryUrl) {
throw new Error('repositoryUrl is undefined');
}
if (!homepage) {
throw new Error('homepage is undefined');
}
this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
}
public getDownloadUrl(os: OperatingSystem): string {
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
}
public get feedbackUrl(): string {
return `${this.repositoryWebUrl}/issues`;
}
public get releaseUrl(): string {
return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
}
}
function getWebUrl(gitUrl: string) {
if (gitUrl.endsWith('.git')) {
return gitUrl.substring(0, gitUrl.length - 4);
}
return gitUrl;
}
function getFileName(os: OperatingSystem, version: string): string {
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}.dmg`;
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}

View File

@@ -0,0 +1,11 @@
export enum RecommendationLevel {
Standard = 0,
Strict = 1,
}
export const RecommendationLevelNames = Object
.values(RecommendationLevel)
.filter((level) => typeof level === 'string') as string[];
export const RecommendationLevels = RecommendationLevelNames
.map((level) => RecommendationLevel[level]) as RecommendationLevel[];

View File

@@ -1,57 +1,27 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from './IScript'; import { IScript } from './IScript';
import { RecommendationLevel } from './RecommendationLevel';
import { IScriptCode } from './IScriptCode';
export class Script extends BaseEntity<string> implements IScript { export class Script extends BaseEntity<string> implements IScript {
constructor( constructor(
public readonly name: string, public readonly name: string,
public readonly code: string, public readonly code: IScriptCode,
public readonly revertCode: string,
public readonly documentationUrls: ReadonlyArray<string>, public readonly documentationUrls: ReadonlyArray<string>,
public readonly isRecommended: boolean) { public readonly level?: RecommendationLevel) {
super(name); super(name);
validateCode(name, code); if (!code) {
if (revertCode) { throw new Error(`undefined code (script: ${name})`);
validateCode(name, revertCode);
if (code === revertCode) {
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
}
} }
validateLevel(level);
} }
public canRevert(): boolean { public canRevert(): boolean {
return Boolean(this.revertCode); return Boolean(this.code.revert);
} }
} }
function validateCode(name: string, code: string): void { function validateLevel(level?: RecommendationLevel) {
if (!code || code.length === 0) { if (level !== undefined && !(level in RecommendationLevel)) {
throw new Error(`Code of ${name} is empty or null`); throw new Error(`invalid level: ${level}`);
}
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;
}
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')}`);
} }
} }

57
src/domain/ScriptCode.ts Normal file
View File

@@ -0,0 +1,57 @@
import { IScriptCode } from './IScriptCode';
export class ScriptCode implements IScriptCode {
constructor(
scriptName: string,
public readonly execute: string,
public readonly revert: string) {
if (!scriptName) {
throw new Error('script name is undefined');
}
validateCode(scriptName, execute);
if (revert) {
scriptName = `${scriptName} (revert)`;
validateCode(scriptName, revert);
if (execute === revert) {
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
}
}
}
}
function validateCode(name: string, code: string): void {
if (!code || code.length === 0) {
throw new Error(`code of ${name} is empty or undefined`);
}
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 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')}`);
}
}
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;
}

View File

@@ -3,6 +3,7 @@ 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; addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void; removeItem(id: TKey): void;

View File

@@ -16,6 +16,14 @@ 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 or undefined'); throw new Error('item is null or undefined');

View File

@@ -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 {

View File

@@ -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);

View File

@@ -4,9 +4,10 @@ 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, faComment, faSmile } 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, faUserSecret, faDesktop, faTag, faGlobe } from '@fortawesome/free-solid-svg-icons'; import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
export class IconBootstrapper implements IVueBootstrapper { export class IconBootstrapper implements IVueBootstrapper {
@@ -21,9 +22,10 @@ export class IconBootstrapper implements IVueBootstrapper {
faFolderOpen, faFolderOpen,
faFolder, faFolder,
faTimes, faTimes,
faFileDownload, faFileDownload, faSave,
faCopy, faCopy,
faSearch, faSearch,
faBatteryFull, faBatteryHalf,
faInfoCircle); faInfoCircle);
vue.component('font-awesome-icon', FontAwesomeIcon); vue.component('font-awesome-icon', FontAwesomeIcon);
} }

View File

@@ -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 } 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 {

View File

@@ -16,7 +16,7 @@
</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 } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';

View File

@@ -8,9 +8,17 @@
}" }"
ref="cardElement"> ref="cardElement">
<div class="card__inner"> <div class="card__inner">
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span> <span v-if="cardTitle && cardTitle.length > 0">
<span>{{cardTitle}}</span>
</span>
<span v-else>Oh no 😢</span> <span v-else>Oh no 😢</span>
<!-- Expand icon -->
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" /> <font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
<!-- Indeterminate and full states -->
<div class="card__inner__state-icons">
<font-awesome-icon v-if="isAnyChildSelected && !areAllChildrenSelected" :icon="['fa', 'battery-half']" />
<font-awesome-icon v-if="areAllChildrenSelected" :icon="['fa', 'battery-full']" />
</div>
</div> </div>
<div class="card__expander" v-on:click.stop> <div class="card__expander" v-on:click.stop>
<div class="card__expander__content"> <div class="card__expander__content">
@@ -24,7 +32,7 @@
</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';
@@ -36,17 +44,21 @@ import { StatefulVue } from '@/presentation/StatefulVue';
export default class CardListItem extends StatefulVue { export default class CardListItem extends StatefulVue {
@Prop() public categoryId!: number; @Prop() public categoryId!: number;
@Prop() public activeCategoryId!: number; @Prop() public activeCategoryId!: number;
public cardTitle?: string = ''; public cardTitle = '';
public isExpanded: boolean = false; public isExpanded = false;
public isAnyChildSelected = false;
public areAllChildrenSelected = false;
@Emit('selected') @Emit('selected')
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') @Watch('isExpanded')
public async onExpansionChangedAsync(newValue: number, oldValue: number) { public async onExpansionChangedAsync(newValue: number, oldValue: number) {
if (!oldValue && newValue) { if (!oldValue && newValue) {
@@ -57,20 +69,23 @@ export default class CardListItem extends StatefulVue {
} }
public async mounted() { public async mounted() {
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined; const state = await this.getCurrentStateAsync();
state.selection.changed.on(() => {
this.updateStateAsync(this.categoryId);
});
this.updateStateAsync(this.categoryId);
} }
@Watch('categoryId') @Watch('categoryId')
public async onCategoryIdChanged(value: |number) { public async updateStateAsync(value: |number) {
this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined; const state = await this.getCurrentStateAsync();
} const category = !value ? undefined : state.app.findCategory(this.categoryId);
this.cardTitle = category ? category.name : undefined;
private async getCardTitleAsync(categoryId: number): Promise<string | undefined> { this.isAnyChildSelected = category ? state.selection.isAnySelected(category) : false;
const state = await this.getCurrentStateAsync(); this.areAllChildrenSelected = category ? state.selection.areAllSelected(category) : false;
const category = state.app.findCategory(this.categoryId);
return category ? category.name : undefined;
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -93,7 +108,7 @@ $expanded-margin-top: 30px;
@media screen and (max-width: $small-screen-width) { width: 90%; } @media screen and (max-width: $small-screen-width) { width: 90%; }
&__inner { &__inner {
padding: $card-padding; padding: $card-padding $card-padding 0 $card-padding;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
background-color: $gray; background-color: $gray;
@@ -115,13 +130,21 @@ $expanded-margin-top: 30px;
&:after { &:after {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
&__state-icons {
height: $card-padding;
margin-right: -$card-padding;
padding-right: 10px;
display: flex;
justify-content: flex-end;
}
&__expand-icon { &__expand-icon {
width: 100%; width: 100%;
margin-top: .25em; margin-top: .25em;
vertical-align: middle;
} }
} }
&__expander { &__expander {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
position: relative; position: relative;

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
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>();
@@ -23,26 +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;
} }
@@ -51,16 +70,18 @@ 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: false, 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,

View File

@@ -7,7 +7,6 @@
:filterPredicate="filterPredicate" :filterPredicate="filterPredicate"
:filterText="filterText" :filterText="filterText"
v-on:nodeSelected="toggleNodeSelectionAsync($event)" v-on:nodeSelected="toggleNodeSelectionAsync($event)"
v-on:nodeRevertToggled="handleNodeRevertToggleAsync($event)"
> >
</SelectableTree> </SelectableTree>
</span> </span>
@@ -16,18 +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 { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
@Component({ @Component({
components: { components: {
@@ -51,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 toggleNodeSelectionAsync(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, false); 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}`);
} }
} }
@@ -84,6 +85,14 @@
(category: ICategory) => node.id === getCategoryNodeId(category)); (category: ICategory) => node.id === getCategoryNodeId(category));
} }
private initializeFilter(currentFilter: IFilterResult | undefined) {
if (!currentFilter) {
this.handleFilterRemoved();
} else {
this.handleFiltered(currentFilter);
}
}
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void { private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
this.selectedNodeIds = selectedScripts this.selectedNodeIds = selectedScripts
.map((node) => node.id); .map((node) => node.id);
@@ -99,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">

View File

@@ -0,0 +1,6 @@
import { INode } from './Node/INode';
export interface INodeSelectedEvent {
isSelected: boolean;
node: INode;
}

View File

@@ -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,33 +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; 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
@@ -47,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;
} }

View File

@@ -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
}
}

View File

@@ -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));
}
}

View File

@@ -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');
}
}

View File

@@ -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,10 +7,10 @@ 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, isReversible : liquorTreeNode.data.isReversible,
}; };
@@ -23,12 +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, 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));
}

View File

@@ -1,7 +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 isReversible: boolean;
readonly documentationUrls: ReadonlyArray<string>; readonly documentationUrls: ReadonlyArray<string>;
readonly children?: ReadonlyArray<INode>; readonly children?: ReadonlyArray<INode>;
readonly type: NodeType;
} }

View File

@@ -4,7 +4,7 @@
<RevertToggle <RevertToggle
class="item" class="item"
v-if="data.isReversible" v-if="data.isReversible"
:scriptId="data.id" /> :node="data" />
<DocumentationUrls <DocumentationUrls
class="item" class="item"
v-if="data.documentationUrls && data.documentationUrls.length > 0" v-if="data.documentationUrls && data.documentationUrls.length > 0"

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More