Compare commits

..

47 Commits
0.5.0 ... 0.7.0

Author SHA1 Message Date
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
undergroundwires
45816a2bcc updated dependencies to latest 2020-08-09 03:00:17 +01:00
undergroundwires
60a5a2aa40 reworked on footer & removed github icon 2020-08-09 03:00:17 +01:00
undergroundwires
04b9b59e14 support for desktop versions #20 2020-08-09 03:00:13 +01:00
undergroundwires
4ff4b52202 code area now shows "how" before "why" 2020-07-24 15:43:19 +01:00
undergroundwires
73c426844a runs tests on each push on the repository 2020-07-19 15:14:23 +01:00
undergroundwires
25ce236a77 fixed dead links in documentation 2020-07-19 02:37:54 +01:00
undergroundwires-bot
9b20175545 ⬆️ bumped to 0.5.0 2020-07-19 00:32:13 +00:00
130 changed files with 9428 additions and 2663 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
dist
dist_electron
.vs
.vscode
.github
.git
docs
docker

View File

@@ -10,9 +10,8 @@ jobs:
if: github.event.base_ref == 'refs/heads/master'
runs-on: ubuntu-latest
steps:
-
uses: undergroundwires/bump-everywhere@master
- uses: undergroundwires/bump-everywhere@master
with:
user: undergroundwires-bot
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.
release-token: ${{ secrets.BUMP_GITHUB_PAT }} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.

32
.github/workflows/deploy-desktop.yaml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Deploy desktop
on:
release:
types: [created] # will be triggered when a NON-draft release is created and published.
jobs:
publish-desktop-app:
name: ${{ matrix.os }}
strategy:
matrix:
os: [macos, ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v2
with:
ref: master # otherwise it defaults to the version tag missing bump commit
fetch-depth: 0 # fetch all history
- name: Checkout to bump commit
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
- name: Setup node
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:unit
- name: Publish desktop app
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,11 +1,11 @@
name: Build & deploy
name: Deploy site
on:
release:
types: [created] # will be triggered when a NON-draft release is created and published.
jobs:
build-and-deploy:
aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
runs-on: ubuntu-latest
steps:
-
@@ -86,7 +86,7 @@ jobs:
node-version: '14.x'
-
name: "App: Install dependencies"
run: npm install
run: npm ci
working-directory: site
-
name: "App: Run tests"
@@ -114,4 +114,4 @@ jobs:
--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 }}
--profile user --session ${{ env.SESSION_NAME }}

View File

@@ -1,37 +1,26 @@
name: Quality checks
on:
pull_request:
branches:
- master
on: push
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
lint-command:
- npm run lint:vue
- npm run lint:yaml
- npm run lint:md
- npm run lint:md:relative-urls
- npm run lint:md:consistency
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v2
-
name: Setup node
- name: Setup node
uses: actions/setup-node@v1
with:
node-version: 14.x
-
name: Install dependencies
- name: Install dependencies
run: npm ci
-
name: Lint vue
run: npm run lint:vue
-
name: Lint yaml
run: npm run lint:yaml
-
name: 'Validate md: Relative URLs'
run: npm run lint:md:relative-urls
-
name: 'Validate md: Enforce standards'
run: npm run lint:md
-
name: 'Validate md: Ensure consistency'
run: npm run lint:md:consistency
- name: Lint
run: ${{ matrix.lint-command }}

View File

@@ -1,9 +1,7 @@
name: Security checks
on:
pull_request:
branches:
- master
push:
schedule:
- cron: '0 0 * * 0'

View File

@@ -1,9 +1,6 @@
name: Test
on:
pull_request:
branches:
- master
on: push
jobs:
run-tests:

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
node_modules
/dist
.vs
.vscode
.vscode
#Electron-builder output
/dist_electron

View File

@@ -1,8 +1,55 @@
# Changelog
## 0.6.2 (2020-08-16)
* 🐛 fixed disabling error reporting for november 2019 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5967347b80976a519f6f4eb1972a62f3e600df2b)
* 🐛 fixed blank screen and icons on mac | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7fac0fe79f252e8f9dda4f6f83cd6fa4ba2b539f)
* 🐛 fixed removing onedrive does not delete scheduled tasks | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b6bfc2572740c0cd46d3bc0058fa767dd5fa862e)
* ⚙️ enhanced tweak to disable for office telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/afc3bfb3b8896f332c9a196973ded3dce8fd21e4)
* ✨ added script to clear dotnet telemery | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1663bfeac7b6580b1335ca5fcf3587b69c080c72)
* 🐛 fixed changing time server not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c69998c7cb29ffcf40f0af03b73150736581da69)
* 🔥 removed disabling ClickToRun as it breaks office | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3d3380f27ebeea53f17f49974aaa89300ffaf2dd)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.1...0.6.2)
## 0.6.1 (2020-08-09)
* updated documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5963d2bac551083f9d16cce6b851abf0e8b88ce7)
* fixed typo in footer | [commit](https://github.com/undergroundwires/privacy.sexy/commit/5c15a7a64aaf24578a32713dec491bf494216303)
* more scripts can be reverted | [commit](https://github.com/undergroundwires/privacy.sexy/commit/831c014f977515454ee6dc664d77a8c434495501)
* moved windows connect now to security & recommended | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6049a2b834d8d17af741f8d8f8b07cd15153b001)
* fixed mac / linux download links | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c8be45e287b5ea009d6f828f7f327f37850569e)
* tweaks to disable webcam, speech and compatibility telemetry | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a5dbe66fc175e39397f296ab2ff703e9b0ab4d7c)
* refactorings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/66d4d39d5bf3db305450514c6b6224654dafbfb2)
* fixed removing onedrive does not clean start menu / quick access | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1cc12195a3e9a11c590d3ed64d80299b50f74838)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.6.0...0.6.1)
## 0.6.0 (2020-07-26)
* fixed dead links in documentation | [commit](https://github.com/undergroundwires/privacy.sexy/commit/25ce236a7737decaf2eb9b8c29a4c4f34d43f770)
* runs tests on each push on the repository | [commit](https://github.com/undergroundwires/privacy.sexy/commit/73c426844a0330718a9ab7de12b61ca05e853323)
* code area now shows "how" before "why" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ff4b52202b1c5dbfe2b80580bbe7d93132ab05c)
* support for desktop versions #20 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/04b9b59e14766ccd251474ad3710baf1f682fd49)
* reworked on footer & removed github icon | [commit](https://github.com/undergroundwires/privacy.sexy/commit/60a5a2aa4026d384bef9e6a203f1b7514a269c33)
* updated dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45816a2bccb3d11a50e3f2bc19c0a6cc2587deaa)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.5.0...0.6.0)
## 0.5.0 (2020-07-19)
* added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9)
* search placeholder shows total scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d5225de07186f853f4cf7aedd4998f5d00c107a)
* do not collapse card when on "Search" and "Select" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/dd7e1416b4df54bf71b719d4654db88769dc0994)
* opening a card scrolls to its content div | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31d2067f076c3159483baec49975617dddbd158d)
* all cards in same line now have same height | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a9f9e9044385d9aed3b5551fc6c6823e813fd1e5)
* patched loadash vulnerability (#18) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92a7118d1c5013312772e075b9ee5a79c93710b8)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.10...0.5.0)
## 0.4.10 (2020-07-15)
* fixed script validation errors | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a34ae139d7fc1ba05b8ab9eb962da4ca0231ed5c)
* fixed script errors & added tests | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9e722ddfb3825fb29d6298025baaaa033120d017)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10)

View File

@@ -20,25 +20,24 @@
6. Issue that pull request!
- 🙏 DO
- Document your changes in the pull request
- 💡 Check [developer notes](./docs/developer-notes.md) if you need help
- ❗ 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
### Extend scripts
- Create a [pull request](./../CONTRIBUTING.md#Pull+Request+Process) for [application.yaml](./../src/application/application.yaml)
- 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.
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
### Handle the state in presentation layer
- There are two types of components:
- **Stateless**, extends `Vue`
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
- The source of truth for the state lies in [application layer](./src/application/state) and must be updated from the views if they're mutating the state
- They mutate or/and reacts to changes in [application state](./src/application/state).
- The source of truth for the state lies in application layer (`./src/application/`) and must be updated from the views if they're mutating the state
- They mutate or/and reacts to changes in [application state](src/application/State/ApplicationState.ts).
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
## License

View File

@@ -1,6 +1,6 @@
# privacy.sexy
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](./CONTRIBUTING.md)
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/undergroundwires/privacy.sexy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
@@ -12,16 +12,23 @@
[![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
[![Auto-versioned by bump-everywhere](https://github.com/undergroundwires/bump-everywhere/blob/master/badge.svg?raw=true)](https://github.com/undergroundwires/bump-everywhere)
[https://privacy.sexy](https://privacy.sexy)
## Get started
- Online version: [https://privacy.sexy](https://privacy.sexy)
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-Setup-0.6.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-0.6.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.6.2/privacy.sexy-0.6.2.dmg)
[![privacy.sexy application](img/app.png)](https://privacy.sexy)
## Why
- You don't need to run any compiled software that has access to your system, just run the generated scripts.
- It's open source, both application & infrastructure is 100% transparent
- Fully automated C/CD pipeline to AWS for provisioning serverless infrastructure using GitHub actions.
- Have full visibility into what the tweaks do as you enable them.
- Ability to revert applied scripts
- 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
@@ -34,34 +41,38 @@
- Testing
- Run unit tests: `npm run test:unit`
- Lint: `npm run lint`
- **Desktop app**
- Development: `npm run electron:serve`
- Production: `npm run electron:build` to build an executable
- **Webpage**
- Development: `npm run serve` to compile & hot-reload for development.
- Production: `npm run build` to prepare files for distribution.
- Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.4.10 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.4.10 undergroundwires/privacy.sexy:0.4.10`
1. Build: `docker build -t undergroundwires/privacy.sexy:0.6.2 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.6.2 undergroundwires/privacy.sexy:0.6.2`
## Architecture
### 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.
- Application uses highly decoupled models & services in different DDD layers.
- **Domain layer** is where the application is modelled with validation logic.
- **Presentation Layer**
- Consists of Vue.js components & UI stuff.
- Consists of Vue.js components and other UI-related code.
- Desktop application is created using [Electron](https://www.electronjs.org/).
- Event driven as in components simply listens to events from the state and act accordingly.
- **Application Layer**
- Keeps the application state
- 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))
![DDD + vue.js](docs/app-ddd.png)
![DDD + vue.js](img/architecture/app-ddd.png)
### 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)
- Runs on AWS 100% serverless and automatically provisioned using [GitHub Actions](.github/workflows/).
@@ -73,4 +84,4 @@
- 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.
[![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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

BIN
img/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 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

6187
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,66 @@
{
"name": "privacy.sexy",
"version": "0.4.10",
"version": "0.6.2",
"author": "undergroundwires",
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "npm run lint:vue && npm run lint:yaml && npm run lint:md && npm run lint:md:relative-urls && npm run lint:md:consistency",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:vue": "vue-cli-service lint --no-fix",
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
"lint:md": "markdownlint **/*.md --ignore node_modules",
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent"
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-brands-svg-icons": "^5.12.0",
"@fortawesome/free-regular-svg-icons": "^5.12.0",
"@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"ace-builds": "^1.4.7",
"@fortawesome/fontawesome-svg-core": "^1.2.30",
"@fortawesome/free-brands-svg-icons": "^5.14.0",
"@fortawesome/free-regular-svg-icons": "^5.14.0",
"@fortawesome/free-solid-svg-icons": "^5.14.0",
"@fortawesome/vue-fontawesome": "^0.1.10",
"ace-builds": "^1.4.12",
"file-saver": "^2.0.2",
"inversify": "^5.0.1",
"liquor-tree": "^0.2.70",
"v-tooltip": "^2.0.2",
"vue": "^2.6.11",
"vue-class-component": "^7.1.0",
"vue-js-modal": "^2.0.0-rc.3",
"vue-property-decorator": "^8.3.0"
"v-tooltip": "2.0.2",
"vue": "^2.6.12",
"vue-class-component": "^7.2.5",
"vue-js-modal": "^2.0.0-rc.6",
"vue-property-decorator": "^9.0.0"
},
"devDependencies": {
"@types/ace": "0.0.42",
"@types/chai": "^4.2.7",
"@types/ace": "0.0.43",
"@types/chai": "^4.2.12",
"@types/file-saver": "^2.0.1",
"@types/mocha": "^5.2.7",
"@vue/cli-plugin-typescript": "^4.4.0",
"@vue/cli-plugin-unit-mocha": "^4.1.1",
"@vue/cli-service": "^4.1.1",
"@vue/test-utils": "1.0.0-beta.30",
"@types/mocha": "^8.0.3",
"@vue/cli-plugin-typescript": "^4.5.4",
"@vue/cli-plugin-unit-mocha": "^4.5.4",
"@vue/cli-service": "^4.5.4",
"@vue/test-utils": "1.0.4",
"chai": "^4.2.0",
"electron": "^10.1.0",
"electron-devtools-installer": "^3.1.1",
"electron-log": "^4.2.4",
"electron-updater": "^4.3.4",
"js-yaml-loader": "^1.2.2",
"markdownlint-cli": "^0.23.1",
"remark-cli": "^8.0.0",
"remark-lint-no-dead-urls": "^1.0.2",
"remark-preset-lint-consistent": "^3.0.0",
"remark-validate-links": "^10.0.0",
"sass": "^1.24.0",
"sass-loader": "^8.0.0",
"typescript": "^3.7.4",
"vue-template-compiler": "^2.6.11",
"markdownlint-cli": "^0.23.2",
"remark-cli": "^8.0.1",
"remark-lint-no-dead-urls": "^1.1.0",
"remark-preset-lint-consistent": "^3.0.1",
"remark-validate-links": "^10.0.2",
"sass": "^1.26.10",
"sass-loader": "^10.0.1",
"typescript": "^4.0.2",
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
"vue-template-compiler": "^2.6.12",
"yaml-lint": "^1.2.4"
}
}

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -32,4 +32,4 @@
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
</html>

View File

@@ -12,10 +12,9 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import { ApplicationState } from '@/application/State/ApplicationState';
import { Component, Vue } from 'vue-property-decorator';
import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter.vue';
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
import TheCodeArea from '@/presentation/TheCodeArea.vue';
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
import TheSearchBar from '@/presentation/TheSearchBar.vue';

View File

@@ -0,0 +1,54 @@
import { OperatingSystem } from '../OperatingSystem';
import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem {
if (!userAgent) {
return OperatingSystem.Unknown;
}
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== OperatingSystem.Unknown) {
return os;
}
}
return OperatingSystem.Unknown;
}
}
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
const BrowserDetectors =
[
define(OperatingSystem.KaiOS, (b) =>
b.mustInclude('KAIOS')),
define(OperatingSystem.ChromeOS, (b) =>
b.mustInclude('CrOS')),
define(OperatingSystem.BlackBerryOS, (b) =>
b.mustInclude('BlackBerry')),
define(OperatingSystem.BlackBerryTabletOS, (b) =>
b.mustInclude('RIM Tablet OS')),
define(OperatingSystem.BlackBerry, (b) =>
b.mustInclude('BB10')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Android').mustNotInclude('Windows Phone')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
define(OperatingSystem.iOS, (b) =>
b.mustInclude('like Mac OS X')),
define(OperatingSystem.Linux, (b) =>
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
define(OperatingSystem.Windows, (b) =>
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
define(OperatingSystem.WindowsPhone, (b) =>
b.mustInclude('Windows Phone')),
define(OperatingSystem.macOS, (b) =>
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
];
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
const builder = new DetectorBuilder(os);
applyRules(builder);
return builder.build();
}

View File

@@ -0,0 +1,53 @@
import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '../OperatingSystem';
export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { }
public mustInclude(str: string): DetectorBuilder {
return this.add(str, this.existingPartsInUserAgent);
}
public mustNotInclude(str: string): DetectorBuilder {
return this.add(str, this.notExistingPartsInUserAgent);
}
public build(): IBrowserOsDetector {
if (!this.existingPartsInUserAgent.length) {
throw new Error('Must include at least a part');
}
return {
detect: (agent) => this.detect(agent),
};
}
private detect(userAgent: string): OperatingSystem {
if (!userAgent) {
throw new Error('User agent is null or undefined');
}
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return OperatingSystem.Unknown;
}
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return OperatingSystem.Unknown;
}
return this.os;
}
private add(part: string, array: string[]): DetectorBuilder {
if (!part) {
throw new Error('part is empty or undefined');
}
if (this.existingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as existing part`);
}
if (this.notExistingPartsInUserAgent.includes(part)) {
throw new Error(`part ${part} is already included as not existing part`);
}
array.push(part);
return this;
}
}

View File

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

View File

@@ -0,0 +1,80 @@
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from './OperatingSystem';
interface IEnvironmentVariables {
readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process;
readonly navigator: Navigator;
}
export class Environment implements IEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment({
window,
process,
navigator,
});
public readonly isDesktop: boolean;
public readonly os: OperatingSystem;
protected constructor(
variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
if (!variables) {
throw new Error('variables is null or empty');
}
this.isDesktop = isDesktop(variables);
this.os = this.isDesktop ?
getDesktopOsType(getProcessPlatform(variables))
: browserOsDetector.detect(getUserAgent(variables));
}
}
function getUserAgent(variables: IEnvironmentVariables): string {
if (!variables.window || !variables.window.navigator) {
return undefined;
}
return variables.window.navigator.userAgent;
}
function getProcessPlatform(variables: IEnvironmentVariables): string {
if (!variables.process || !variables.process.platform) {
return undefined;
}
return variables.process.platform;
}
function getDesktopOsType(processPlatform: string): OperatingSystem {
// https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') {
return OperatingSystem.macOS;
} else if (processPlatform === 'win32') {
return OperatingSystem.Windows;
} else if (processPlatform === 'linux') {
return OperatingSystem.Linux;
}
return OperatingSystem.Unknown;
}
function isDesktop(variables: IEnvironmentVariables): boolean {
// More: https://github.com/electron/electron/issues/2288
// Renderer process
if (variables.window
&& variables.window.process
&& variables.window.process.type === 'renderer') {
return true;
}
// Main process
if (variables.process
&& variables.process.versions
&& Boolean(variables.process.versions.electron)) {
return true;
}
// Detect the user agent when the `nodeIntegration` option is set to true
if (variables.navigator
&& variables.navigator.userAgent
&& variables.navigator.userAgent.includes('Electron')) {
return true;
}
return false;
}

View File

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

View File

@@ -0,0 +1,14 @@
export enum OperatingSystem {
macOS,
Windows,
Linux,
KaiOS,
ChromeOS,
BlackBerryOS,
BlackBerry,
BlackBerryTabletOS,
Android,
iOS,
WindowsPhone,
Unknown,
}

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> {
if (!documentable) {
throw new Error('documentable is null or undefined');
}
const docs = documentable.docs;
if (!docs) {
if (!docs || !docs.length) {
return [];
}
const result = new DocumentationUrls();
if (docs instanceof Array) {
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');
}
let result = new DocumentationUrlContainer();
result = addDocs(docs, result);
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>();
public add(url: string) {
public addUrl(url: string) {
validateUrl(url);
this.urls.push(url);
}
public addUrls(urls: any[]) {
for (const url of urls) {
if (typeof url !== 'string') {
throw new Error('Docs field (documentation url) must be an array of strings');
}
this.addUrl(url);
}
}
public getAll(): ReadonlyArray<string> {
return this.urls;
}

View File

@@ -11,6 +11,7 @@ import { Script } from '@/domain/Script';
import { IApplication } from '@/domain/IApplication';
import { IApplicationCode } from './Code/IApplicationCode';
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
/** Mutatable singleton application state that's the single source of truth throughout the application */
export class ApplicationState implements IApplicationState {
@@ -37,7 +38,7 @@ export class ApplicationState implements IApplicationState {
public readonly app: IApplication,
/** Initially selected scripts */
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.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 { IUserSelection } from '@/application/State/Selection/IUserSelection';
import { UserScriptGenerator } from './UserScriptGenerator';
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
import { Signal } from '@/infrastructure/Events/Signal';
import { IApplicationCode } from './IApplicationCode';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
export class ApplicationCode implements IApplicationCode {
public readonly changed = new Signal<string>();
public readonly changed = new Signal<ICodeChangedEvent>();
public current: string;
private readonly generator: IUserScriptGenerator = new UserScriptGenerator();
private scriptPositions = new Map<SelectedScript, CodePosition>();
constructor(
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 (!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);
userSelection.changed.on((scripts) => {
this.setCode(scripts);
});
}
private setCode(scripts: ReadonlyArray<SelectedScript>) {
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
this.changed.notify(this.current);
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
const oldScripts = Array.from(this.scriptPositions.keys());
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 {
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 {
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;
}

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

View File

@@ -1,6 +1,7 @@
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
import { ISignal } from '@/infrastructure/Events/ISignal';
export interface IApplicationCode {
readonly changed: ISignal<string>;
readonly changed: ISignal<ICodeChangedEvent>;
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

@@ -6,11 +6,13 @@ export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number;
removeAllInCategory(categoryId: number): void;
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void;
isSelected(script: IScript): boolean;
isSelected(scriptId: string): boolean;
selectAll(): void;
deselectAll(): void;
}

View File

@@ -8,20 +8,49 @@ import { IRepository } from '@/infrastructure/Repository/IRepository';
export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>();
private readonly scripts: IRepository<string, SelectedScript>;
constructor(
private readonly app: IApplication,
/** Initially selected scripts */
selectedScripts: ReadonlyArray<IScript>) {
selectedScripts: ReadonlyArray<SelectedScript>) {
this.scripts = new InMemoryRepository<string, SelectedScript>();
if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) {
const selected = new SelectedScript(script, false);
this.scripts.addItem(selected);
this.scripts.addItem(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 {
const script = this.app.findScript(scriptId);
if (!script) {
@@ -44,8 +73,8 @@ export class UserSelection implements IUserSelection {
this.changed.notify(this.scripts.getItems());
}
public isSelected(script: IScript): boolean {
return this.scripts.exists(script.id);
public isSelected(scriptId: string): boolean {
return this.scripts.exists(scriptId);
}
/** Get users scripts based on his/her selections */

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
declare module 'js-yaml-loader!*' {
export type CategoryOrScript = YamlCategory | YamlScript;
type DocumentationUrls = ReadonlyArray<string> | string;
export type DocumentationUrls = ReadonlyArray<string> | string;
export interface YamlDocumentable {
docs?: DocumentationUrls;

133
src/background.ts Normal file
View File

@@ -0,0 +1,133 @@
'use strict';
import { app, protocol, BrowserWindow, shell } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import path from 'path';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
const isDevelopment = process.env.NODE_ENV !== 'production';
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win: BrowserWindow | null;
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } },
]);
// Setup logging
autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging
log.transports.file.level = 'silly';
if (!process.env.IS_TEST) {
Object.assign(console, log.functions); // override console.log, console.warn etc.
}
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1350,
height: 955,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration
nodeIntegration: (process.env
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
},
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#set-tray-icon
icon: path.join(__static, 'icon.png'),
});
win.setMenuBarVisibility(false);
configureExternalsUrlsOpenBrowser(win);
loadApplication(win);
win.on('closed', () => {
win = null;
});
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS);
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString()); // tslint:disable-line:no-console
}
}
createWindow();
});
// See electron-builder issue "checkForUpdatesAndNotify updates but does not notify on Windows 10"
// https://github.com/electron-userland/electron-builder/issues/2700
// https://github.com/electron/electron/issues/10864
if (process.platform === 'win32') {
// https://docs.microsoft.com/en-us/windows/win32/shell/appid#how-to-form-an-application-defined-appusermodelid
app.setAppUserModelId('Undergroundwires.PrivacySexy');
}
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit();
}
});
} else {
process.on('SIGTERM', () => {
app.quit();
});
}
}
function loadApplication(window: BrowserWindow) {
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
if (!process.env.IS_TEST) {
win.webContents.openDevTools();
}
} else {
createProtocol('app');
// Load the index.html when not in development
win.loadURL('app://./index.html');
// tslint:disable-next-line:max-line-length
autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts
}
}
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
window.webContents.on('new-window', (event, url) => { // handle redirect
if (url !== win.webContents.getURL()) {
event.preventDefault();
shell.openExternal(url);
}
});
}

View File

@@ -18,15 +18,7 @@ export class Application implements IApplication {
if (!repositoryUrl) { throw Error('Application has no repository url'); }
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) {
throw new Error('Application must consist of at least one script');
}
if (this.flattened.allScripts.filter((script) => script.isRecommended).length === 0) {
throw new Error('Application must consist of at least one recommended script');
}
ensureValid(this.flattened);
ensureNoDuplicates(this.flattened.allCategories);
ensureNoDuplicates(this.flattened.allScripts);
}
@@ -75,30 +67,50 @@ interface IFlattenedApplication {
allScripts: IScript[];
}
function flattenRecursive(
function ensureValid(application: IFlattenedApplication) {
if (!application.allCategories || application.allCategories.length === 0) {
throw new Error('Application must consist of at least one category');
}
if (!application.allScripts || application.allScripts.length === 0) {
throw new Error('Application must consist of at least one script');
}
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
throw new Error('Application must consist of at least one recommended script');
}
}
function flattenCategories(
categories: ReadonlyArray<ICategory>,
flattened: IFlattenedApplication) {
flattened: IFlattenedApplication): IFlattenedApplication {
if (!categories || categories.length === 0) {
return flattened;
}
for (const category of categories) {
flattened.allCategories.push(category);
if (category.scripts) {
for (const script of category.scripts) {
flattened.allScripts.push(script);
}
}
if (category.subCategories && category.subCategories.length > 0) {
flattenRecursive(
category.subCategories as ReadonlyArray<ICategory>,
flattened);
}
flattened = flattenScripts(category.scripts, flattened);
flattened = flattenCategories(category.subCategories, flattened);
}
return flattened;
}
function flattenScripts(
scripts: ReadonlyArray<IScript>,
flattened: IFlattenedApplication): IFlattenedApplication {
if (!scripts) {
return flattened;
}
for (const script of scripts) {
flattened.allScripts.push(script);
}
return flattened;
}
function flatten(
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
const flattened: IFlattenedApplication = {
let flattened: IFlattenedApplication = {
allCategories: new Array<ICategory>(),
allScripts: new Array<IScript>(),
};
flattenRecursive(categories, flattened);
flattened = flattenCategories(categories, flattened);
return flattened;
}

View File

@@ -3,15 +3,7 @@ import { IScript } from './IScript';
import { ICategory } from './ICategory';
export class Category extends BaseEntity<number> implements ICategory {
private static validate(category: ICategory) {
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');
}
}
private allSubScripts: ReadonlyArray<IScript> = undefined;
constructor(
id: number,
@@ -20,6 +12,27 @@ export class Category extends BaseEntity<number> implements ICategory {
public readonly subCategories?: ReadonlyArray<ICategory>,
public readonly scripts?: ReadonlyArray<IScript>) {
super(id);
Category.validate(this);
validateCategory(this);
}
public getAllScriptsRecursively(): readonly IScript[] {
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
}
}
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
return [
...category.scripts,
...category.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
];
}
function validateCategory(category: ICategory) {
if (!category.name) {
throw new Error('undefined or empty name');
}
if ((!category.subCategories || category.subCategories.length === 0) &&
(!category.scripts || category.scripts.length === 0)) {
throw new Error('A category must have at least one sub-category or script');
}
}

View File

@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
readonly name: string;
readonly subCategories?: ReadonlyArray<ICategory>;
readonly scripts?: ReadonlyArray<IScript>;
getAllScriptsRecursively(): ReadonlyArray<IScript>;
}
export { IEntity } from '../infrastructure/Entity/IEntity';

View File

@@ -3,6 +3,7 @@ import { IEntity } from '../Entity/IEntity';
export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
getById(id: TKey): TEntity | undefined;
addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): 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;
}
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 {
if (!item) {
throw new Error('item is null or undefined');

View File

@@ -4,15 +4,20 @@ import { faGithub } from '@fortawesome/free-brands-svg-icons';
/** BRAND ICONS (PREFIX: fab) */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
/** REGULAR ICONS (PREFIX: far) */
import { faFolderOpen, faFolder } from '@fortawesome/free-regular-svg-icons';
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
/** SOLID ICONS (PREFIX: fas (default)) */
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, faTag, faGlobe } from '@fortawesome/free-solid-svg-icons';
export class IconBootstrapper implements IVueBootstrapper {
public bootstrap(vue: VueConstructor): void {
library.add(
faGithub,
faUserSecret,
faSmile,
faDesktop,
faGlobe,
faTag,
faFolderOpen,
faFolder,
faTimes,

View File

@@ -8,10 +8,8 @@
</template>
<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 { SaveFileDialog } from './../infrastructure/SaveFileDialog';
import { Clipboard } from './../infrastructure/Clipboard';
@Component
export default class IconButton extends StatefulVue {

View File

@@ -16,7 +16,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component } from 'vue-property-decorator';
import CardListItem from './CardListItem.vue';
import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory';

View File

@@ -24,7 +24,7 @@
</template>
<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 { StatefulVue } from '@/presentation/StatefulVue';
@@ -75,10 +75,7 @@ export default class CardListItem extends StatefulVue {
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
$big-screen-width: 991px;
$medium-screen-width: 767px;
$small-screen-width: 380px;
@import "@/presentation/styles/media.scss";
$card-padding: 30px;
$card-margin: 15px;

View File

@@ -15,9 +15,8 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { IApplicationState } from '@/application/State/IApplicationState';
import { Grouping } from './Grouping';
const DefaultGrouping = Grouping.Cards;

View File

@@ -1,6 +1,6 @@
import { IApplication } from './../../../domain/IApplication';
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 {
const nodes = new Array<INode>();
@@ -23,26 +23,45 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
export function getScriptNodeId(script: IScript): string {
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 {
return `Category${category.id}`;
return `${category.id}`;
}
function parseCategoryRecursively(
parentCategory: ICategory): INode[] {
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) {
throw new Error('parentCategory is undefined');
}
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
for (const script of parentCategory.scripts) {
nodes.push(convertScriptToNode(script));
}
let nodes = new Array<INode>();
nodes = addCategories(parentCategory.subCategories, nodes);
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;
}
@@ -51,16 +70,18 @@ function convertCategoryToNode(
category: ICategory, children: readonly INode[]): INode {
return {
id: getCategoryNodeId(category),
type: NodeType.Category,
text: category.name,
children,
documentationUrls: category.documentationUrls,
isReversible: false,
isReversible: children && children.every((child) => child.isReversible),
};
}
function convertScriptToNode(script: IScript): INode {
return {
id: getScriptNodeId(script),
type: NodeType.Script,
text: script.name,
children: undefined,
documentationUrls: script.documentationUrls,

View File

@@ -7,7 +7,6 @@
:filterPredicate="filterPredicate"
:filterText="filterText"
v-on:nodeSelected="toggleNodeSelectionAsync($event)"
v-on:nodeRevertToggled="handleNodeRevertToggleAsync($event)"
>
</SelectableTree>
</span>
@@ -16,18 +15,17 @@
</template>
<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 { Category } from '@/domain/Category';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { IScript } from '@/domain/IScript';
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 { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
import { INode } from './SelectableTree/INode';
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
import SelectableTree from './SelectableTree/SelectableTree.vue';
import { INode, NodeType } from './SelectableTree/Node/INode';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
@Component({
components: {
@@ -53,15 +51,17 @@
await this.initializeNodesAsync(this.categoryId);
}
public async toggleNodeSelectionAsync(node: INode) {
if (node.children != null && node.children.length > 0) {
return; // only interested in script nodes
}
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
const state = await this.getCurrentStateAsync();
if (!this.selectedNodeIds.some((id) => id === node.id)) {
state.selection.addSelectedScript(node.id, false);
} else {
state.selection.removeSelectedScript(node.id);
switch (event.node.type) {
case NodeType.Category:
toggleCategoryNodeSelection(event, state);
break;
case NodeType.Script:
toggleScriptNodeSelection(event, state);
break;
default:
throw new Error(`Unknown node type: ${event.node.id}`);
}
}
@@ -99,6 +99,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>
<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' {
import { PluginObject } from 'vue';
@@ -10,33 +9,43 @@ declare module 'liquor-tree' {
filter(query: string): void;
clearFilter(): 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>;
isReversible: boolean;
}
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
export interface ILiquorTreeNodeState {
checked: boolean;
}
export interface ILiquorTreeNode {
id: string;
data: ICustomLiquorTreeData;
children: ReadonlyArray<ILiquorTreeNode> | undefined;
}
/**
* Returned from Node tree view events.
* See constructor in https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
*/
export interface ILiquorTreeExistingNode {
id: string;
export interface ILiquorTreeExistingNode extends ILiquorTreeNode {
data: ILiquorTreeNodeData;
states: ILiquorTreeNodeState | undefined;
children: ReadonlyArray<ILiquorTreeExistingNode> | undefined;
// expand(): void;
}
/**
* Sent to liquor tree to define of new nodes.
* https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
*/
export interface ILiquorTreeNewNode {
id: string;
export interface ILiquorTreeNewNode extends ILiquorTreeNode {
text: string;
state: ILiquorTreeNodeState | undefined;
children: ReadonlyArray<ILiquorTreeNewNode> | undefined;
data: ICustomLiquorTreeData;
}
// https://amsik.github.io/liquor-tree/#Component-Options
@@ -47,29 +56,16 @@ declare module 'liquor-tree' {
autoCheckChildren: boolean;
parentSelect: boolean;
keyboardNavigation: boolean;
deletion: (node: ILiquorTreeExistingNode) => void;
filter: ILiquorTreeFilter;
deletion(node: ILiquorTreeNode): boolean;
}
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Node.js
interface ILiquorTreeNodeState {
checked: boolean;
}
interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
export interface ILiquorTreeNodeData extends ICustomLiquorTreeData {
text: string;
}
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
interface ILiquorTreeOptions {
checkbox: boolean;
checkOnSelect: boolean;
filter: ILiquorTreeFilter;
deletion(node: ILiquorTreeNewNode): boolean;
}
// https://github.com/amsik/liquor-tree/blob/master/src/components/TreeRoot.vue
interface ILiquorTreeFilter {
export interface ILiquorTreeFilter {
emptyText: string;
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
}

View File

@@ -0,0 +1,22 @@
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.
This is false as it's handled manually to be able to batch select for performance + highlighting */
public readonly autoCheckChildren = false;
public readonly parentSelect = false;
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,38 @@
import { ILiquorTreeNode } from 'liquor-tree';
import { NodeType } from './../../Node/INode';
export function getNewCheckedState(
oldNode: ILiquorTreeNode,
selectedNodeIds: ReadonlyArray<string>): boolean {
switch (oldNode.data.type) {
case NodeType.Script:
return selectedNodeIds.some((id) => id === oldNode.id);
case NodeType.Category:
return parseAllSubScriptIds(oldNode).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 { INode } from './INode';
import { INode } from './../../Node/INode';
// 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'); }
return {
id: liquorTreeNode.id,
type: liquorTreeNode.data.type,
text: liquorTreeNode.data.text,
// selected: liquorTreeNode.states && liquorTreeNode.states.checked,
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)),
children: convertChildren(liquorTreeNode.children, convertExistingToNode),
documentationUrls: liquorTreeNode.data.documentationUrls,
isReversible : liquorTreeNode.data.isReversible,
};
@@ -24,11 +24,20 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
state: {
checked: false,
},
children: (!node.children || node.children.length === 0) ? [] :
node.children.map((childNode) => toNewLiquorTreeNode(childNode)),
children: convertChildren(node.children, toNewLiquorTreeNode),
data: {
documentationUrls: node.documentationUrls,
isReversible: node.isReversible,
type: node.type,
},
};
}
function convertChildren<TOldNode, TNewNode>(
oldChildren: readonly TOldNode[],
callback: (value: TOldNode) => TNewNode): TNewNode[] {
if (!oldChildren || oldChildren.length === 0) {
return [];
}
return oldChildren.map((childNode) => callback(childNode));
}

View File

@@ -1,7 +1,13 @@
export enum NodeType {
Script,
Category,
}
export interface INode {
readonly id: string;
readonly text: string;
readonly isReversible: boolean;
readonly documentationUrls: ReadonlyArray<string>;
readonly children?: ReadonlyArray<INode>;
readonly type: NodeType;
}

View File

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

View File

@@ -0,0 +1,153 @@
<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 { IApplicationState } from '@/application/State/IApplicationState';
import { getCategoryId, getScriptId } from './../../ScriptNodeParser';
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>

View File

@@ -0,0 +1,30 @@
import { IReverter } from './IReverter';
import { getCategoryId } from '../../../ScriptNodeParser';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IApplication } from '@/domain/IApplication';
import { ScriptReverter } from './ScriptReverter';
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
export class CategoryReverter implements IReverter {
private readonly categoryId: number;
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
constructor(nodeId: string, app: IApplication) {
this.categoryId = getCategoryId(nodeId);
this.scriptReverters = getAllSubScriptReverters(this.categoryId, app);
}
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
return this.scriptReverters.every((script) => script.getState(selectedScripts));
}
public selectWithRevertState(newState: boolean, selection: IUserSelection): void {
selection.addOrUpdateAllInCategory(this.categoryId, newState);
}
}
function getAllSubScriptReverters(categoryId: number, app: IApplication) {
const category = app.findCategory(categoryId);
if (!category) {
throw new Error(`Category with id "${categoryId}" does not exist`);
}
const scripts = category.getAllScriptsRecursively();
return scripts.map((script) => new ScriptReverter(script.id));
}

View File

@@ -0,0 +1,7 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/State/IApplicationState';
export interface IReverter {
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
selectWithRevertState(newState: boolean, selection: IUserSelection): void;
}

View File

@@ -0,0 +1,16 @@
import { INode, NodeType } from '../INode';
import { IReverter } from './IReverter';
import { ScriptReverter } from './ScriptReverter';
import { IApplication } from '@/domain/IApplication';
import { CategoryReverter } from './CategoryReverter';
export function getReverter(node: INode, app: IApplication): IReverter {
switch (node.type) {
case NodeType.Category:
return new CategoryReverter(node.id, app);
case NodeType.Script:
return new ScriptReverter(node.id);
default:
throw new Error('Unknown script type');
}
}

View File

@@ -0,0 +1,21 @@
import { IReverter } from './IReverter';
import { getScriptId } from '../../../ScriptNodeParser';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/State/IApplicationState';
export class ScriptReverter implements IReverter {
private readonly scriptId: string;
constructor(nodeId: string) {
this.scriptId = getScriptId(nodeId);
}
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
const selectedScript = selectedScripts.find((selected) => selected.id === this.scriptId);
if (!selectedScript) {
return false;
}
return selectedScript.revert;
}
public selectWithRevertState(newState: boolean, selection: IUserSelection): void {
selection.addOrUpdateSelectedScript(this.scriptId, newState);
}
}

View File

@@ -1,141 +0,0 @@
<template>
<div class="checkbox-switch" >
<input type="checkbox" class="input-checkbox"
v-model="isReverted"
@change="onRevertToggledAsync()" >
<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, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { INode } from './INode';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
@Component
export default class RevertToggle extends StatefulVue {
@Prop() public scriptId: string;
public isReverted = false;
public async mounted() {
const state = await this.getCurrentStateAsync();
state.selection.changed.on(this.handleSelectionChanged);
}
public async onRevertToggledAsync() {
const state = await this.getCurrentStateAsync();
state.selection.addOrUpdateSelectedScript(this.scriptId, this.isReverted);
}
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
const selectedScript = selectedScripts.find((script) => script.id === this.scriptId);
if (!selectedScript) {
this.isReverted = false;
} else {
this.isReverted = selectedScript.revert;
}
}
}
</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>

View File

@@ -7,7 +7,7 @@
v-on:node:unchecked="nodeSelected($event)"
ref="treeElement"
>
<span class="tree-text" slot-scope="{ node }">
<span class="tree-text" slot-scope="{ node }" >
<Node :data="convertExistingToNode(node)" />
</span>
</tree>
@@ -17,18 +17,21 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit, Watch } from 'vue-property-decorator';
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeOptions } from 'liquor-tree';
import Node from './Node.vue';
import { INode } from './INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './NodeTranslator';
export type FilterPredicate = (node: INode) => boolean;
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import LiquorTree, { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree } from 'liquor-tree';
import Node from './Node/Node.vue';
import { INode } from './Node/INode';
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
import { INodeSelectedEvent } from './/INodeSelectedEvent';
import { getNewCheckedState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
@Component({
components: {
LiquorTree,
Node,
LiquorTree,
Node,
},
})
export default class SelectableTree extends Vue {
@@ -38,7 +41,7 @@
@Prop() public initialNodes?: ReadonlyArray<INode>;
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
public liquorTreeOptions = this.getDefaults();
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
public convertExistingToNode = convertExistingToNode;
public mounted() {
@@ -46,7 +49,7 @@
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
if (this.selectedNodeIds) {
recurseDown(initialNodes,
(node) => node.state.checked = this.selectedNodeIds.includes(node.id));
(node) => node.state.checked = getNewCheckedState(node, this.selectedNodeIds));
}
this.initialLiquourTreeNodes = initialNodes;
} else {
@@ -58,7 +61,11 @@
}
public nodeSelected(node: ILiquorTreeExistingNode) {
this.$emit('nodeSelected', convertExistingToNode(node));
const event: INodeSelectedEvent = {
node: convertExistingToNode(node),
isSelected: node.states.checked,
};
this.$emit('nodeSelected', event);
return;
}
@@ -73,23 +80,13 @@
}
@Watch('selectedNodeIds')
public setSelectedStatus(selectedNodeIds: ReadonlyArray<string>) {
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
if (!selectedNodeIds) {
throw new Error('Selected nodes are undefined');
}
const newNodes = updateCheckedState(this.getLiquorTreeApi().model, selectedNodeIds);
this.getLiquorTreeApi().setModel(newNodes);
/* Alternative:
this.getLiquorTreeApi().recurseDown((node) => {
node.states.checked = selectedNodeIds.includes(node.id);
});
Problem: Does not check their parent if all children are checked, because it does not
trigger update on parent as we work with scripts not categories. */
/* Alternative:
this.getLiquorTreeApi().recurseDown((node) => {
if(selectedNodeIds.includes(node.id)) { node.select(); } else { node.unselect(); }
});
Problem: Emits nodeSelected() event again which will cause an infinite loop. */
this.getLiquorTreeApi().recurseDown((node) => {
node.states.checked = getNewCheckedState(node, selectedNodeIds);
});
}
private getLiquorTreeApi(): ILiquorTree {
@@ -98,27 +95,6 @@
}
return (this.$refs.treeElement as any).tree;
}
private getDefaults(): ILiquorTreeOptions {
return {
multiple: true,
checkbox: true,
checkOnSelect: true,
autoCheckChildren: true,
parentSelect: false,
keyboardNavigation: true,
deletion: (node) => !node.children || node.children.length === 0,
filter: {
matcher: (query: string, node: ILiquorTreeExistingNode) => {
if (!this.filterPredicate) {
throw new Error('Cannot filter as predicate is null');
}
return this.filterPredicate(convertExistingToNode(node));
},
emptyText: '🕵Hmm.. Can not see one 🧐',
},
};
}
}
function recurseDown(
@@ -131,27 +107,4 @@
}
}
}
function updateCheckedState(
oldNodes: ReadonlyArray<ILiquorTreeExistingNode>,
selectedNodeIds: ReadonlyArray<string>): ReadonlyArray<ILiquorTreeNewNode> {
const result = new Array<ILiquorTreeNewNode>();
for (const oldNode of oldNodes) {
const newState = oldNode.states;
newState.checked = selectedNodeIds.some((id) => id === oldNode.id);
const newNode: ILiquorTreeNewNode = {
id: oldNode.id,
text: oldNode.data.text,
data: {
documentationUrls: oldNode.data.documentationUrls,
isReversible: oldNode.data.isReversible,
},
children: oldNode.children == null ? [] :
updateCheckedState(oldNode.children, selectedNodeIds),
state: newState,
};
result.push(newNode);
}
return result;
}
</script>

View File

@@ -6,7 +6,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { Component, Prop, Emit } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';

View File

@@ -28,7 +28,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import SelectableOption from './SelectableOption.vue';
import { IApplicationState } from '@/application/State/IApplicationState';

View File

@@ -7,25 +7,37 @@
v-show="!this.isSearching" />
</div>
<div class="scripts">
<div v-if="!isSearching || searchHasMatches">
<CardList v-if="this.showCards" />
<div v-else-if="this.showList" class="tree">
<div v-if="this.isSearching" class="search-query">
Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
<ScriptsTree />
<div v-if="!isSearching">
<CardList v-if="currentGrouping === Grouping.Cards"/>
<div class="tree" v-if="currentGrouping === Grouping.None">
<ScriptsTree />
</div>
</div>
<div v-else> <!-- Searching -->
<div class="search">
<div class="search__query">
<div>Searching for "{{this.searchQuery | threeDotsTrim}}"</div>
<div class="search__query__close-button">
<font-awesome-icon
:icon="['fas', 'times']"
v-on:click="clearSearchQueryAsync()"/>
</div>
</div>
<div v-if="!searchHasMatches" class="search-no-matches">
<div>Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞</div>
<div>Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a> </div>
</div>
<div v-else class="search-no-matches">
Sorry, no matches for "{{this.searchQuery | threeDotsTrim}}" 😞
Feel free to extend the scripts <a :href="repositoryUrl" target="_blank" class="child github" >here</a>.
</div>
<div v-if="searchHasMatches" class="tree tree--searching">
<ScriptsTree />
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { Category } from '@/domain/Category';
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Grouping } from './Grouping/Grouping';
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
@@ -34,7 +46,6 @@
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
/** Shows content of single category or many categories */
@Component({
components: {
@@ -54,38 +65,34 @@
},
})
export default class TheScripts extends StatefulVue {
public showCards = false;
public showList = false;
public repositoryUrl = '';
private searchQuery = '';
private isSearching = false;
private searchHasMatches = false;
public Grouping = Grouping; // Make it accessible from view
public currentGrouping = Grouping.Cards;
public searchQuery = '';
public isSearching = false;
public searchHasMatches = false;
private currentGrouping: Grouping;
public async mounted() {
const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl;
state.filter.filterRemoved.on(() => {
this.isSearching = false;
this.updateGroups();
});
state.filter.filtered.on((result: IFilterResult) => {
this.searchQuery = result.query;
this.isSearching = true;
this.searchHasMatches = result.hasAnyMatches();
this.updateGroups();
});
}
public async clearSearchQueryAsync() {
const state = await this.getCurrentStateAsync();
state.filter.removeFilter();
}
public onGroupingChanged(group: Grouping) {
this.currentGrouping = group;
this.updateGroups();
}
private updateGroups(): void {
this.showCards = !this.isSearching && this.currentGrouping === Grouping.Cards;
this.showList = this.isSearching || this.currentGrouping === Grouping.None;
}
}
</script>
@@ -95,26 +102,49 @@
@import "@/presentation/styles/fonts.scss";
.scripts {
margin-top:10px;
.search-no-matches {
word-break:break-word;
color: $white;
text-transform: uppercase;
color: $light-gray;
font-size: 1.5em;
background-color: $slate;
padding:5%;
text-align:center;
> a {
color: $gray;
}
}
.tree {
padding-left: 3%;
padding-top: 15px;
padding-bottom: 15px;
.search-query {
display: flex;
justify-content: center;
&--searching {
padding-top: 0px;
}
}
}
.search {
display: flex;
flex-direction: column;
background-color: $slate;
&__query {
display: flex;
justify-content: center;
flex-direction: row;
align-items: center;
margin-top: 1em;
color: $gray;
&__close-button {
cursor: pointer;
font-size: 1.25em;
margin-left: 0.25rem;
&:hover {
opacity: 0.9;
}
}
}
&-no-matches {
display:flex;
flex-direction: column;
word-break:break-word;
text-transform: uppercase;
color: $light-gray;
font-size: 1.5em;
padding:10px;
text-align:center;
> div {
padding-bottom:13px;
}
a {
color: $gray;
}
}

View File

@@ -3,44 +3,92 @@
</template>
<script lang="ts">
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
import { Component, Prop } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import ace from 'ace-builds';
import 'ace-builds/webpack-resolver';
import { CodeBuilder } from '../application/State/Code/CodeBuilder';
import { CodeBuilder } from '@/application/State/Code/Generation/CodeBuilder';
import { ICodeChangedEvent } from '@/application/State/Code/Event/ICodeChangedEvent';
import { IScript } from '@/domain/IScript';
const NothingChosenCode =
new CodeBuilder()
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows')
.appendLine()
.appendCommentLine('-- 🤔 How to use')
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
.appendCommentLine(' 📙 You can select "Recommended" on the top to select "safer" tweaks. Always double check!')
.appendCommentLine(' 📙 After you choose any tweak, you can download & copy to execute your script.')
.appendLine()
.appendCommentLine('-- 🧐 Why privacy.sexy')
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other softwares on it.')
.appendCommentLine(' ✔️ You don\'t need to run any compiled software on your system, just run the generated scripts.')
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
.appendCommentLine(' ✔️ Free software, 100% transparency: both application & infrastructure code are open-sourced.')
.appendLine()
.appendCommentLine('-- 🤔 How to use')
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
.appendCommentLine(' 📙 You can select "Recommended" on the top to select "safer" tweaks. Always double check!')
.appendCommentLine(' 📙 After you choose any tweak, you can download & copy to execute your script.')
.toString();
@Component
export default class TheCodeArea extends StatefulVue {
public readonly editorId = 'codeEditor';
private editor!: ace.Ace.Editor;
private currentMarkerId?: number;
@Prop() private theme!: string;
public async mounted() {
this.editor = initializeEditor(this.theme, this.editorId);
const state = await this.getCurrentStateAsync();
this.updateCode(state.code.current);
this.editor.setValue(state.code.current || NothingChosenCode, 1);
state.code.changed.on((code) => this.updateCode(code));
}
private updateCode(code: string) {
this.editor.setValue(code || NothingChosenCode, 1);
private updateCode(event: ICodeChangedEvent) {
this.removeCurrentHighlighting();
if (event.isEmpty()) {
this.editor.setValue(NothingChosenCode, 1);
return;
}
this.editor.setValue(event.code, 1);
if (event.addedScripts && event.addedScripts.length) {
this.reactToChanges(event, event.addedScripts);
} else if (event.changedScripts && event.changedScripts.length) {
this.reactToChanges(event, event.changedScripts);
}
}
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
const positions = scripts
.map((script) => event.getScriptPositionInCode(script));
const start = Math.min(
...positions.map((position) => position.startLine),
);
const end = Math.max(
...positions.map((position) => position.endLine),
);
this.scrollToLine(end + 2);
this.highlight(start, end);
}
private highlight(startRow: number, endRow: number) {
const AceRange = ace.require('ace/range').Range;
this.currentMarkerId = this.editor.session.addMarker(
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
);
}
private scrollToLine(row: number) {
const column = this.editor.session.getLine(row).length;
this.editor.gotoLine(row, column, true);
}
private removeCurrentHighlighting() {
if (!this.currentMarkerId) {
return;
}
this.editor.session.removeMarker(this.currentMarkerId);
this.currentMarkerId = undefined;
}
}
@@ -58,12 +106,16 @@ function initializeEditor(theme: string, editorId: string): ace.Ace.Editor {
</script>
<style scoped lang="scss">
<style lang="scss">
@import "@/presentation/styles/colors.scss";
.code-area {
/* ----- Fill its parent div ------ */
width: 100%;
/* height */
max-height: 1000px;
min-height: 200px;
&__highlight {
background-color:$accent;
opacity: 20%;
position:absolute;
}
}
</style>

View File

@@ -14,13 +14,12 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Component } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
import { Clipboard } from './../infrastructure/Clipboard';
import IconButton from './IconButton.vue';
@Component({
components: {
IconButton,
@@ -33,7 +32,7 @@ export default class TheCodeButtons extends StatefulVue {
const state = await this.getCurrentStateAsync();
this.hasCode = state.code.current && state.code.current.length > 0;
state.code.changed.on((code) => {
this.hasCode = code && code.length > 0;
this.hasCode = code && code.code.length > 0;
});
}

View File

@@ -1,87 +0,0 @@
<template>
<div class="footer">
<div class="item">
<a :href="releaseUrl" target="_blank">{{ version }}</a>
</div>
<div class="item">
<a @click="$modal.show(modalName)">Privacy</a> <!-- href to #privacy to avoid scrolling to top -->
</div>
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
<div class="modal">
<ThePrivacyPolicy class="modal__content"/>
<div class="modal__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
</div>
</div>
</modal>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import ThePrivacyPolicy from './ThePrivacyPolicy.vue';
@Component({
components: {
ThePrivacyPolicy,
},
})
export default class TheFooter extends StatefulVue {
private readonly modalName = 'privacy-policy';
private version: string = '';
private releaseUrl: string = '';
public async mounted() {
const state = await this.getCurrentStateAsync();
this.version = `v${state.app.version}`;
this.releaseUrl = `${state.app.repositoryUrl}/releases/tag/${state.app.version}`;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.footer {
display: flex;
color: $dark-gray;
font-size: 1rem;
font-family: $normal-font;
align-self: center;
a {
color:inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
.item:not(:first-child)::before {
content: "|";
padding: 0 5px;
}
}
.modal {
margin-bottom: 10px;
display: flex;
flex-direction: row;
&__content {
width: 100%;
}
&__close-button {
width: auto;
font-size: 1.5em;
margin-right:0.25em;
align-self: flex-start;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<span
class="container"
v-bind:class="{ 'container_unsupported': !hasCurrentOsDesktopVersion, 'container_supported': hasCurrentOsDesktopVersion }">
<span class="description">
<font-awesome-icon class="description__icon" :icon="['fas', 'desktop']" />
<span class="description__text">For desktop:</span>
</span>
<span class="urls">
<span class="urls__url" v-for="os of supportedDesktops" v-bind:key="os">
<DownloadUrlListItem :operatingSystem="os" />
</span>
</span>
</span>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
import DownloadUrlListItem from './DownloadUrlListItem.vue';
@Component({
components: { DownloadUrlListItem },
})
export default class DownloadUrlList extends Vue {
public readonly supportedDesktops: ReadonlyArray<OperatingSystem>;
public readonly hasCurrentOsDesktopVersion: boolean = false;
constructor() {
super();
const supportedOperativeSystems = [OperatingSystem.Windows, OperatingSystem.Linux, OperatingSystem.macOS];
const currentOs = Environment.CurrentEnvironment.os;
this.supportedDesktops = supportedOperativeSystems.sort((os) => os === currentOs ? 0 : 1);
this.hasCurrentOsDesktopVersion = supportedOperativeSystems.includes(currentOs);
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/media.scss";
.container {
display:flex;
flex-wrap: wrap;
justify-content: space-around;
&_unsupported {
opacity: 0.9;
}
&_supported {
font-size: 1em;
}
.description {
&__icon {
margin-right: 0.5em;
}
&__text {
margin-right: 0.3em;
}
}
}
.urls {
&__url {
&:not(:first-child)::before {
opacity: 0.5;
content: "|";
font-size: 0.6rem;
padding: 0 5px;
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<span class="url">
<a :href="downloadUrl"
v-bind:class="{
'url__active': hasCurrentOsDesktopVersion && isCurrentOs,
'url__inactive': hasCurrentOsDesktopVersion && !isCurrentOs,
}">{{ operatingSystemName }}</a>
</span>
</template>
<script lang="ts">
import { Component, Prop, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
@Component
export default class DownloadUrlListItem extends StatefulVue {
@Prop() public operatingSystem!: OperatingSystem;
public OperatingSystem = OperatingSystem;
public downloadUrl: string = '';
public operatingSystemName: string = '';
public isCurrentOs: boolean = false;
public hasCurrentOsDesktopVersion: boolean = false;
public async mounted() {
await this.onOperatingSystemChangedAsync(this.operatingSystem);
}
@Watch('operatingSystem')
public async onOperatingSystemChangedAsync(os: OperatingSystem) {
const currentOs = Environment.CurrentEnvironment.os;
this.isCurrentOs = os === currentOs;
this.downloadUrl = await this.getDownloadUrlAsync(os);
this.operatingSystemName = getOperatingSystemName(os);
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
}
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
const state = await this.getCurrentStateAsync();
return `${state.app.repositoryUrl}/releases/download/${state.app.version}/${getFileName(os, state.app.version)}`;
}
}
function hasDesktopVersion(os: OperatingSystem): boolean {
return os === OperatingSystem.Windows
|| os === OperatingSystem.Linux
|| os === OperatingSystem.macOS;
}
function getOperatingSystemName(os: OperatingSystem): string {
switch (os) {
case OperatingSystem.Linux:
return 'Linux';
case OperatingSystem.macOS:
return 'macOS';
case OperatingSystem.Windows:
return 'Windows';
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}
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]}`);
}
}
</script>
<style scoped lang="scss">
.url {
&__active {
font-size: 1em;
}
&__inactive {
font-size: 0.70em;
}
a {
color:inherit;
&:hover {
opacity: 0.8;
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="privacy-policy">
<div v-if="!isDesktop" class="line">
<div class="line__emoji">🚫🍪</div>
<div>No cookies!</div>
</div>
<div v-if="isDesktop" class="line">
<div class="line__emoji">🚫🌐</div>
<div>Everything is offline, except single request GitHub to check for updates on application start.</div>
</div>
<div class="line">
<div class="line__emoji">🚫👀</div>
<div>No user behavior / IP adress collection!</div>
</div>
<div class="line">
<div class="line__emoji">🤖</div>
<div>All transparent: Deployed automatically from master branch
of the <a :href="repositoryUrl" target="_blank">source code</a> with no changes.</div>
</div>
<div v-if="!isDesktop" class="line">
<div class="line__emoji">📈</div>
<div>Basic <a href="https://aws.amazon.com/cloudfront/reporting/" target="_blank">CDN statistics</a>
are collected by AWS but they cannot be related to you or your behavior. You can download the offline version if you don't want CDN data collection.</div>
</div>
<div class="line">
<div class="line__emoji">🎉</div>
<div>As almost no data is colected, the application gets better only with your active feedback.
Feel free to <a :href="feedbackUrl" target="_blank">create an issue</a> 😊</div>
</div>
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
@Component
export default class TheFooter extends StatefulVue {
public repositoryUrl: string = '';
public feedbackUrl: string = '';
public isDesktop: boolean = false;
constructor() {
super();
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
}
public async mounted() {
const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.privacy-policy {
display: flex;
flex-direction: column;
font-family: $normal-font;
text-align:center;
.line {
display: flex;
flex-direction: column;
&:not(:first-child) {
margin-top:0.2rem;
}
}
a {
color:inherit;
&:hover {
opacity: 0.8;
}
}
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div>
<div class="footer">
<div class="footer__section">
<span v-if="isDesktop" class="footer__section__item">
<font-awesome-icon class="icon" :icon="['fas', 'globe']" />
<span>Online version at <a href="https://privacy.sexy" target="_blank">https://privacy.sexy</a></span>
</span>
<span v-else class="footer__section__item">
<DownloadUrlList />
</span>
</div>
<div class="footer__section">
<div class="footer__section__item">
<a :href="feedbackUrl" target="_blank">
<font-awesome-icon class="icon" :icon="['far', 'smile']" />
<span>Feedback</span>
</a>
</div>
<div class="footer__section__item">
<a :href="repositoryUrl" target="_blank">
<font-awesome-icon class="icon" :icon="['fab', 'github']" />
<span>Source Code</span>
</a>
</div>
<div class="footer__section__item">
<a :href="releaseUrl" target="_blank">
<font-awesome-icon class="icon" :icon="['fas', 'tag']" />
<span>v{{ version }}</span>
</a>
</div>
<div class="footer__section__item">
<font-awesome-icon class="icon" :icon="['fas', 'user-secret']" />
<a @click="$modal.show(modalName)">Privacy</a>
</div>
</div>
</div>
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
<div class="modal">
<PrivacyPolicy class="modal__content"/>
<div class="modal__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
</div>
</div>
</modal>
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
import PrivacyPolicy from './PrivacyPolicy.vue';
import DownloadUrlList from './DownloadUrlList.vue';
@Component({
components: {
PrivacyPolicy, DownloadUrlList,
},
})
export default class TheFooter extends StatefulVue {
public readonly modalName = 'privacy-policy';
public readonly isDesktop: boolean;
public version: string = '';
public repositoryUrl: string = '';
public releaseUrl: string = '';
public feedbackUrl: string = '';
constructor() {
super();
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
}
public async mounted() {
const state = await this.getCurrentStateAsync();
this.version = state.app.version;
this.repositoryUrl = state.app.repositoryUrl;
this.releaseUrl = `${state.app.repositoryUrl}/releases/tag/${state.app.version}`;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
@import "@/presentation/styles/media.scss";
.icon {
margin-right: 0.5em;
text-decoration: none;
}
.footer {
display: flex;
justify-content: space-between;
@media (max-width: $big-screen-width) {
flex-direction: column;
align-items: center;
}
&__section {
display: flex;
@media (max-width: $big-screen-width) {
justify-content: space-around;
width:100%;
&:not(:first-child) {
margin-top: 0.7em;
}
}
flex-wrap: wrap;
color: $dark-gray;
font-size: 1rem;
font-family: $normal-font;
a {
color:inherit;
text-decoration: underline;
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
&__item:not(:first-child) {
&::before {
content: "|";
padding: 0 5px;
}
@media (max-width: $big-screen-width) {
margin-top: 3px;
&::before {
content: "";
padding: 0;
}
}
}
}
}
.modal {
margin-bottom: 10px;
display: flex;
flex-direction: row;
&__content {
width: 100%;
}
&__close-button {
width: auto;
font-size: 1.5em;
margin-right:0.25em;
align-self: flex-start;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
}
</style>

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