Compare commits

...

19 Commits
0.4.8 ... 0.6.0

Author SHA1 Message Date
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
undergroundwires
92a7118d1c patched loadash vulnerability (#18) 2020-07-19 02:27:01 +01:00
undergroundwires
a9f9e90443 all cards in same line now have same height 2020-07-19 02:27:01 +01:00
undergroundwires
31d2067f07 opening a card scrolls to its content div 2020-07-19 02:27:01 +01:00
undergroundwires
dd7e1416b4 do not collapse card when on "Search" and "Select" 2020-07-19 02:27:01 +01:00
undergroundwires
1d5225de07 search placeholder shows total scripts 2020-07-19 02:27:01 +01:00
undergroundwires
9c063d59de added ability to revert (#21) 2020-07-19 02:26:56 +01:00
undergroundwires-bot
57028987f1 ⬆️ bumped to 0.4.10 2020-07-15 16:52:43 +01:00
undergroundwires
9e722ddfb3 fixed script errors & added tests 2020-07-15 16:52:18 +01:00
undergroundwires-bot
646a8e0b9f ⬆️ bumped to 0.4.9 2020-07-14 21:38:19 +00:00
undergroundwires
f27a2871d7 simplified docker builds 2020-07-14 18:20:15 +01:00
undergroundwires
909c44d72a updated to may 2020 update 2020-07-14 18:20:15 +01:00
undergroundwires
53cf595e17 disable office telemetry Disassembler0/Win10-Initial-Setup-Script#288 2020-07-14 18:20:15 +01:00
100 changed files with 6944 additions and 2072 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' if: github.event.base_ref == 'refs/heads/master'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - uses: undergroundwires/bump-everywhere@master
uses: undergroundwires/bump-everywhere@master
with: with:
user: undergroundwires-bot user: undergroundwires-bot
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559 release-token: ${{ secrets.BUMP_GITHUB_PAT }} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo. # GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.

32
.github/workflows/deploy-desktop.yaml vendored Normal file
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 }}

117
.github/workflows/deploy-site.yaml vendored Normal file
View File

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

View File

@@ -5,7 +5,7 @@ on:
types: [created] # will be triggered when a NON-draft release is created and published. types: [created] # will be triggered when a NON-draft release is created and published.
jobs: jobs:
build-and-deploy: aws-deploy: # see: https://github.com/undergroundwires/aws-static-site-with-cd
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- -
@@ -86,7 +86,7 @@ jobs:
node-version: '14.x' node-version: '14.x'
- -
name: "App: Install dependencies" name: "App: Install dependencies"
run: npm install run: npm ci
working-directory: site working-directory: site
- -
name: "App: Run tests" name: "App: Run tests"
@@ -114,4 +114,32 @@ jobs:
--web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \ --web-stack-name privacysexy-web-stack --web-stack-cloudfront-arn-output-name CloudFrontDistributionArn \
--role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \ --role-arn ${{secrets.AWS_CLOUDFRONT_SITE_DEPLOYMENT_ROLE_ARN}} \
--region us-east-1 \ --region us-east-1 \
--profile user --session ${{ env.SESSION_NAME }} --profile user --session ${{ env.SESSION_NAME }}
desktop-deploy:
runs-on: windows-latest
steps:
-
name: Set GitHub PAT token # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#github-personal-access-token
run: set GH_TOKEN=TOKEN-GOES-HERE
-
name: Checkout
uses: actions/checkout@v2
with:
ref: master # otherwise it defaults to the version tag missing bump commit
fetch-depth: 0 # fetch all history
- name: Checkout to bump commit
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
-
name: Setup node
uses: actions/setup-node@v1
with:
node-version: '14.x'
-
name: Install dependencies
run: npm ci
-
name: Run tests
run: npm run test:unit
-
name: Upload Release to GitHub # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
run: npm run electron:build -- -p always

View File

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

View File

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

View File

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

4
.gitignore vendored
View File

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

View File

@@ -1,5 +1,40 @@
# Changelog # Changelog
## 0.5.0 (2020-07-19)
* added ability to revert (#21) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9c063d59defa6297c64f50b49403e8bd10620de9)
* search placeholder shows total scripts | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1d5225de07186f853f4cf7aedd4998f5d00c107a)
* do not collapse card when on "Search" and "Select" | [commit](https://github.com/undergroundwires/privacy.sexy/commit/dd7e1416b4df54bf71b719d4654db88769dc0994)
* opening a card scrolls to its content div | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31d2067f076c3159483baec49975617dddbd158d)
* all cards in same line now have same height | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a9f9e9044385d9aed3b5551fc6c6823e813fd1e5)
* patched loadash vulnerability (#18) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92a7118d1c5013312772e075b9ee5a79c93710b8)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.10...0.5.0)
## 0.4.10 (2020-07-15)
* fixed script errors & added tests | [commit](https://github.com/undergroundwires/privacy.sexy/commit/9e722ddfb3825fb29d6298025baaaa033120d017)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.9...0.4.10)
## 0.4.9 (2020-07-14)
* disable office telemetry Disassembler0/Win10-Initial-Setup-Script#288 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/53cf595e1726ee3de79137fd566978fd512d218f)
* updated to may 2020 update | [commit](https://github.com/undergroundwires/privacy.sexy/commit/909c44d72a4a602ee8f27d06b6ec706c1e432ce1)
* simplified docker builds | [commit](https://github.com/undergroundwires/privacy.sexy/commit/f27a2871d74e5117fc029be82caef12246e10879)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.8...0.4.9)
## 0.4.8 (2020-07-11)
* added more scripts #16 (#17) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d8552c62ffea13ce62abce836c7dd4980eef6bb9)
* stopping services before disabling #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/628c16eb952495f5b3f6d794161b355f4b08b819)
* can disable features, capabilities & remove onedrive #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/30efbcc621eb83dd5a9c1e66b8f1f5350eb95006)
* updated one more typo (#19) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d7a1325c0b7665ce712dc411965d00fc1d6fa384)
* more tweaks #16 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c4eb78c3f156cb0d033977cffbe7464697680f5)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.4.7...0.4.8)
## 0.4.7 (2020-06-30) ## 0.4.7 (2020-06-30)
* removed HKU tweak as all HKU's are changed #10 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c937af8ee7da9aa95131e56abf7bf24800390fe6) * removed HKU tweak as all HKU's are changed #10 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c937af8ee7da9aa95131e56abf7bf24800390fe6)

45
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,45 @@
# Contributing
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
## Pull Request Process
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
- Your pull requests are actively welcomed.
- The steps:
1. Fork the repo and create your branch from master.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
- 🙏 DO
- Document your changes in the pull request
- ❗ DON'T
- Do not update the versions, current version is only [set by the maintainer](./docs/gitops.png) and updated automatically by [bump-everywhere](https://github.com/undergroundwires/bump-everywhere)
## Guidelines
### Extend scripts
- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
### Handle the state in presentation layer
- There are two types of components:
- **Stateless**, extends `Vue`
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
- The source of truth for the state lies in application layer (`./src/application/`) and must be updated from the views if they're mutating the state
- They mutate or/and reacts to changes in [application state](src/application/State/ApplicationState.ts).
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
## License
By contributing, you agree that your contributions will be licensed under its GNU General Public License v3.0.

View File

@@ -1,20 +1,12 @@
# +-+-+-+-+-+ +-+-+-+-+-+ # Build
# |B|u|i|l|d| |S|t|a|g|e|
# +-+-+-+-+-+ +-+-+-+-+-+
FROM node:lts-alpine as build-stage FROM node:lts-alpine as build-stage
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY . . COPY . .
RUN npm run build RUN npm run build
# For testing purposes, it's easy to run http-server on lts-alpine such as continuing from here:
# RUN npm install -g http-server
# EXPOSE 8080
# CMD [ "http-server", "dist" ]
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+ # Production stage
# |P|r|o|d|u|c|t|i|o|n| |S|t|a|g|e|
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
FROM nginx:stable-alpine as production-stage FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80 EXPOSE 80

View File

@@ -1,8 +1,8 @@
# privacy.sexy # privacy.sexy
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆 > Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/undergroundwires/privacy.sexy/issues) [![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) [![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)
[![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/3a70b7ef602e2264342c/maintainability)](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
[![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) [![Tests status](https://github.com/undergroundwires/privacy.sexy/workflows/Test/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions)
@@ -12,32 +12,42 @@
[![Deploy status](https://github.com/undergroundwires/privacy.sexy/workflows/Build%20&%20deploy/badge.svg)](https://github.com/undergroundwires/privacy.sexy/actions) [![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) [![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.5.0/privacy.sexy-Setup-0.5.0.exe), [Linux](https://github.com/undergroundwires/privacy-sexy/releases/download/0.5.0/privacy.sexy-0.5.0.dmg), [macOS](https://github.com/undergroundwires/privacy-sexy/releases/download/0.5.0/privacy.sexy-0.5.0-mac.zip)
## Why ## Why
- You don't need to run any compiled software on your system, just run the generated scripts. - You don't need to run any compiled software that has access to your system, just run the generated scripts.
- It's open source, both application & infrastructure is 100% transparent
- Fully automated C/CD pipeline to AWS for provisioning serverless infrastructure using GitHub actions.
- Have full visibility into what the tweaks do as you enable them. - Have full visibility into what the tweaks do as you enable them.
- Ability to revert applied scripts
- Easily extendable - Easily extendable
- Everything is open-sourced including both application and infrastructure
- Fully automated CI/CD pipeline using GitHub actions
- to AWS for provisioning serverless infrastructure
- for building and sharing the desktop applications
## Extend scripts ## Extend scripts
Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌 - Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts)
## Commands ## Commands
- Setup and run - Project setup: `npm install`
- For development: - Testing
- `npm install` to project setup. - Run unit tests: `npm run test:unit`
- `npm run serve` to compile & hot-reload for development. - Lint: `npm run lint`
- Production (using Docker): - **Desktop app**
- Build `docker build -t undergroundwires/privacy.sexy .` - Development: `npm run electron:serve`
- Run `docker run -it -p 8080:8080 --rm --name privacy.sexy-1 undergroundwires/privacy.sexy` - Production: `npm run electron:build` to build an executable
- Prepare for production: `npm run build` - **Webpage**
- Run tests: `npm run test:unit` - Development: `npm run serve` to compile & hot-reload for development.
- Lint and fix files: `npm run lint` - Production: `npm run build` to prepare files for distribution.
- Or run using Docker:
1. Build: `docker build -t undergroundwires/privacy.sexy:0.5.0 .`
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.5.0 undergroundwires/privacy.sexy:0.5.0`
## Architecture ## Architecture
@@ -48,7 +58,8 @@ Fork it & add more scripts in [application.yaml](src/application/application.yam
- Application uses highly decoupled models & services in different DDD layers. - Application uses highly decoupled models & services in different DDD layers.
- **Domain layer** is where the application is modelled with validation logic. - **Domain layer** is where the application is modelled with validation logic.
- **Presentation Layer** - **Presentation Layer**
- Consists of Vue.js components & UI stuff. - Consists of Vue.js components and other UI-related code.
- Desktop application is created using [Electron](https://www.electronjs.org/).
- Event driven as in components simply listens to events from the state and act accordingly. - Event driven as in components simply listens to events from the state and act accordingly.
- **Application Layer** - **Application Layer**
- Keeps the application state - Keeps the application state

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 460 KiB

5184
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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,49 @@
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 {
if (!str) {
throw new Error('part to include is empty or undefined');
}
this.existingPartsInUserAgent.push(str);
return this;
}
public mustNotInclude(str: string): DetectorBuilder {
if (!str) {
throw new Error('part to not include is empty or undefined');
}
this.notExistingPartsInUserAgent.push(str);
return this;
}
public build(): IBrowserOsDetector {
if (!this.existingPartsInUserAgent.length) {
throw new Error('Must include at least a part');
}
return {
detect: (userAgent) => {
if (!userAgent) {
throw new Error('User agent is null or undefined');
}
for (const exitingPart of this.existingPartsInUserAgent) {
if (!userAgent.includes(exitingPart)) {
return OperatingSystem.Unknown;
}
}
for (const notExistingPart of this.notExistingPartsInUserAgent) {
if (userAgent.includes(notExistingPart)) {
return OperatingSystem.Unknown;
}
}
return this.os;
},
};
}
}

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,21 +1,29 @@
import { Category } from '../../domain/Category'; import { Category } from '@/domain/Category';
import { Application } from '../../domain/Application'; import { Application } from '@/domain/Application';
import applicationFile from 'js-yaml-loader!./../application.yaml'; import { IApplication } from '@/domain/IApplication';
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
import { parseCategory } from './CategoryParser'; import { parseCategory } from './CategoryParser';
export function parseApplication(): Application { export function parseApplication(content: ApplicationYaml): IApplication {
validate(content);
const categories = new Array<Category>(); const categories = new Array<Category>();
if (!applicationFile.actions || applicationFile.actions.length <= 0) { for (const action of content.actions) {
throw new Error('Application does not define any action');
}
for (const action of applicationFile.actions) {
const category = parseCategory(action); const category = parseCategory(action);
categories.push(category); categories.push(category);
} }
const app = new Application( const app = new Application(
applicationFile.name, content.name,
applicationFile.repositoryUrl, content.repositoryUrl,
process.env.VUE_APP_VERSION, process.env.VUE_APP_VERSION,
categories); categories);
return app; return app;
} }
function validate(content: ApplicationYaml): void {
if (!content) {
throw new Error('application is null or undefined');
}
if (!content.actions || content.actions.length <= 0) {
throw new Error('application does not define any action');
}
}

View File

@@ -1,20 +1,18 @@
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml'; import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
import { Category } from '../../domain/Category'; import { Category } from '@/domain/Category';
import { parseDocUrls } from './DocumentationParser'; import { parseDocUrls } from './DocumentationParser';
import { parseScript } from './ScriptParser';
let categoryIdCounter: number = 0; let categoryIdCounter: number = 0;
interface ICategoryChildren { interface ICategoryChildren {
subCategories: Category[]; subCategories: Category[];
subScripts: Script[]; subScripts: Script[];
} }
export function parseCategory(category: YamlCategory): Category { export function parseCategory(category: YamlCategory): Category {
if (!category.children || category.children.length <= 0) { ensureValid(category);
throw Error('Category has no children');
}
const children: ICategoryChildren = { const children: ICategoryChildren = {
subCategories: new Array<Category>(), subCategories: new Array<Category>(),
subScripts: new Array<Script>(), subScripts: new Array<Script>(),
@@ -31,6 +29,18 @@ export function parseCategory(category: YamlCategory): Category {
); );
} }
function ensureValid(category: YamlCategory) {
if (!category) {
throw Error('category is null or undefined');
}
if (!category.children || category.children.length === 0) {
throw Error('category has no children');
}
if (!category.category || category.category.length === 0) {
throw Error('category has no name');
}
}
function parseCategoryChild( function parseCategoryChild(
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) { categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
if (isCategory(categoryOrScript)) { if (isCategory(categoryOrScript)) {
@@ -38,11 +48,7 @@ function parseCategoryChild(
children.subCategories.push(subCategory); children.subCategories.push(subCategory);
} else if (isScript(categoryOrScript)) { } else if (isScript(categoryOrScript)) {
const yamlScript = categoryOrScript as YamlScript; const yamlScript = categoryOrScript as YamlScript;
const script = new Script( const script = parseScript(yamlScript);
/* name */ yamlScript.name,
/* code */ yamlScript.code,
/* docs */ parseDocUrls(yamlScript),
/* is recommended? */ yamlScript.recommend);
children.subScripts.push(script); children.subScripts.push(script);
} else { } else {
throw new Error(`Child element is neither a category or a script. throw new Error(`Child element is neither a category or a script.
@@ -50,7 +56,6 @@ function parseCategoryChild(
} }
} }
function isScript(categoryOrScript: any): boolean { function isScript(categoryOrScript: any): boolean {
return categoryOrScript.code && categoryOrScript.code.length > 0; return categoryOrScript.code && categoryOrScript.code.length > 0;
} }

View File

@@ -1,6 +1,9 @@
import { YamlDocumentable } from 'js-yaml-loader!./application.yaml'; import { YamlDocumentable } from 'js-yaml-loader!./application.yaml';
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> { export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
if (!documentable) {
throw new Error('documentable is null or undefined');
}
const docs = documentable.docs; const docs = documentable.docs;
if (!docs) { if (!docs) {
return []; return [];

View File

@@ -0,0 +1,16 @@
import { Script } from '@/domain/Script';
import { YamlScript } from 'js-yaml-loader!./application.yaml';
import { parseDocUrls } from './DocumentationParser';
export function parseScript(yamlScript: YamlScript): Script {
if (!yamlScript) {
throw new Error('script is null or undefined');
}
const script = new Script(
/* name */ yamlScript.name,
/* code */ yamlScript.code,
/* revertCode */ yamlScript.revertCode,
/* docs */ parseDocUrls(yamlScript),
/* isRecommended */ yamlScript.recommend);
return script;
}

View File

@@ -3,13 +3,14 @@ import { IUserFilter } from './Filter/IUserFilter';
import { ApplicationCode } from './Code/ApplicationCode'; import { ApplicationCode } from './Code/ApplicationCode';
import { UserSelection } from './Selection/UserSelection'; import { UserSelection } from './Selection/UserSelection';
import { IUserSelection } from './Selection/IUserSelection'; import { IUserSelection } from './Selection/IUserSelection';
import { AsyncLazy } from '../../infrastructure/Threading/AsyncLazy'; import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
import { Signal } from '../../infrastructure/Events/Signal'; import { Signal } from '@/infrastructure/Events/Signal';
import { parseApplication } from '../Parser/ApplicationParser'; import { parseApplication } from '../Parser/ApplicationParser';
import { IApplicationState } from './IApplicationState'; import { IApplicationState } from './IApplicationState';
import { Script } from '../../domain/Script'; import { Script } from '@/domain/Script';
import { Application } from '../../domain/Application'; import { IApplication } from '@/domain/IApplication';
import { IApplicationCode } from './Code/IApplicationCode'; import { IApplicationCode } from './Code/IApplicationCode';
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
/** Mutatable singleton application state that's the single source of truth throughout the application */ /** Mutatable singleton application state that's the single source of truth throughout the application */
export class ApplicationState implements IApplicationState { export class ApplicationState implements IApplicationState {
@@ -20,7 +21,7 @@ export class ApplicationState implements IApplicationState {
/** Application instance with all scripts. */ /** Application instance with all scripts. */
private static instance = new AsyncLazy<IApplicationState>(() => { private static instance = new AsyncLazy<IApplicationState>(() => {
const application = parseApplication(); const application = parseApplication(applicationFile);
const selectedScripts = new Array<Script>(); const selectedScripts = new Array<Script>();
const state = new ApplicationState(application, selectedScripts); const state = new ApplicationState(application, selectedScripts);
return Promise.resolve(state); return Promise.resolve(state);
@@ -33,7 +34,7 @@ export class ApplicationState implements IApplicationState {
private constructor( private constructor(
/** Inner instance of the all scripts */ /** Inner instance of the all scripts */
public readonly app: Application, public readonly app: IApplication,
/** Initially selected scripts */ /** Initially selected scripts */
public readonly defaultScripts: Script[]) { public readonly defaultScripts: Script[]) {
this.selection = new UserSelection(app, defaultScripts); this.selection = new UserSelection(app, defaultScripts);
@@ -41,5 +42,3 @@ export class ApplicationState implements IApplicationState {
this.filter = new UserFilter(app); this.filter = new UserFilter(app);
} }
} }
export { IApplicationState, IUserFilter };

View File

@@ -1,16 +1,19 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
import { UserScriptGenerator } from './UserScriptGenerator'; import { UserScriptGenerator } from './UserScriptGenerator';
import { IUserSelection } from './../Selection/IUserSelection';
import { Signal } from '@/infrastructure/Events/Signal'; import { Signal } from '@/infrastructure/Events/Signal';
import { IApplicationCode } from './IApplicationCode'; import { IApplicationCode } from './IApplicationCode';
import { IScript } from '@/domain/IScript'; import { IUserScriptGenerator } from './IUserScriptGenerator';
export class ApplicationCode implements IApplicationCode { export class ApplicationCode implements IApplicationCode {
public readonly changed = new Signal<string>(); public readonly changed = new Signal<string>();
public current: string; public current: string;
private readonly generator: UserScriptGenerator; private readonly generator: IUserScriptGenerator = new UserScriptGenerator();
constructor(userSelection: IUserSelection, private readonly version: string) { constructor(
userSelection: IUserSelection,
private readonly version: string) {
if (!userSelection) { throw new Error('userSelection is null or undefined'); } if (!userSelection) { throw new Error('userSelection is null or undefined'); }
if (!version) { throw new Error('version is null or undefined'); } if (!version) { throw new Error('version is null or undefined'); }
this.generator = new UserScriptGenerator(); this.generator = new UserScriptGenerator();
@@ -20,7 +23,7 @@ export class ApplicationCode implements IApplicationCode {
}); });
} }
private setCode(scripts: ReadonlyArray<IScript>) { private setCode(scripts: ReadonlyArray<SelectedScript>) {
this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version); this.current = scripts.length === 0 ? '' : this.generator.buildCode(scripts, this.version);
this.changed.notify(this.current); this.changed.notify(this.current);
} }

View File

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

View File

@@ -1,7 +1,8 @@
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { IUserScriptGenerator } from './IUserScriptGenerator';
import { CodeBuilder } from './CodeBuilder'; import { CodeBuilder } from './CodeBuilder';
import { Script } from '@/domain/Script';
const adminRightsScript = { export const adminRightsScript = {
name: 'Ensure admin privileges', name: 'Ensure admin privileges',
code: 'fltmc >nul 2>&1 || (\n' + code: 'fltmc >nul 2>&1 || (\n' +
' echo This batch script requires administrator privileges. Right-click on\n' + ' echo This batch script requires administrator privileges. Right-click on\n' +
@@ -11,17 +12,19 @@ const adminRightsScript = {
')', ')',
}; };
export class UserScriptGenerator { export class UserScriptGenerator implements IUserScriptGenerator {
public buildCode(scripts: ReadonlyArray<Script>, version: string): string { public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string {
if (!scripts) { throw new Error('scripts is undefined'); } if (!selectedScripts) { throw new Error('scripts is undefined'); }
if (!scripts.length) { throw new Error('scripts are empty'); } if (!selectedScripts.length) { throw new Error('scripts are empty'); }
if (!version) { throw new Error('version is undefined'); } if (!version) { throw new Error('version is undefined'); }
const builder = new CodeBuilder() const builder = new CodeBuilder()
.appendLine('@echo off') .appendLine('@echo off')
.appendCommentLine(`https://privacy.sexy — v${version}${new Date().toUTCString()}`) .appendCommentLine(`https://privacy.sexy — v${version}${new Date().toUTCString()}`)
.appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine(); .appendFunction(adminRightsScript.name, adminRightsScript.code).appendLine();
for (const script of scripts) { for (const selection of selectedScripts) {
builder.appendFunction(script.name, script.code).appendLine(); 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() return builder.appendLine()
.appendLine('pause') .appendLine('pause')

View File

@@ -1,5 +1,5 @@
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
import { IScript } from '@/domain/Script'; import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
export class FilterResult implements IFilterResult { export class FilterResult implements IFilterResult {

View File

@@ -1,6 +1,7 @@
import { IScript } from '@/domain/IScript';
import { FilterResult } from './FilterResult'; import { FilterResult } from './FilterResult';
import { IFilterResult } from './IFilterResult'; import { IFilterResult } from './IFilterResult';
import { Application } from '../../../domain/Application'; import { IApplication } from '@/domain/IApplication';
import { IUserFilter } from './IUserFilter'; import { IUserFilter } from './IUserFilter';
import { Signal } from '@/infrastructure/Events/Signal'; import { Signal } from '@/infrastructure/Events/Signal';
@@ -8,7 +9,7 @@ export class UserFilter implements IUserFilter {
public readonly filtered = new Signal<IFilterResult>(); public readonly filtered = new Signal<IFilterResult>();
public readonly filterRemoved = new Signal<void>(); public readonly filterRemoved = new Signal<void>();
constructor(private application: Application) { constructor(private application: IApplication) {
} }
@@ -18,11 +19,9 @@ export class UserFilter implements IUserFilter {
} }
const filterLowercase = filter.toLocaleLowerCase(); const filterLowercase = filter.toLocaleLowerCase();
const filteredScripts = this.application.getAllScripts().filter( const filteredScripts = this.application.getAllScripts().filter(
(script) => (script) => isScriptAMatch(script, filterLowercase));
script.name.toLowerCase().includes(filterLowercase) ||
script.code.toLowerCase().includes(filterLowercase));
const filteredCategories = this.application.getAllCategories().filter( const filteredCategories = this.application.getAllCategories().filter(
(script) => script.name.toLowerCase().includes(filterLowercase)); (category) => category.name.toLowerCase().includes(filterLowercase));
const matches = new FilterResult( const matches = new FilterResult(
filteredScripts, filteredScripts,
@@ -37,3 +36,16 @@ export class UserFilter implements IUserFilter {
this.filterRemoved.notify(); this.filterRemoved.notify();
} }
} }
function isScriptAMatch(script: IScript, filterLowercase: string) {
if (script.name.toLowerCase().includes(filterLowercase)) {
return true;
}
if (script.code.toLowerCase().includes(filterLowercase)) {
return true;
}
if (script.revertCode) {
return script.revertCode.toLowerCase().includes(filterLowercase);
}
return false;
}

View File

@@ -1,11 +1,13 @@
import { SelectedScript } from './SelectedScript';
import { ISignal } from '@/infrastructure/Events/Signal'; import { ISignal } from '@/infrastructure/Events/Signal';
import { IScript } from '@/domain/IScript'; import { IScript } from '@/domain/IScript';
export interface IUserSelection { export interface IUserSelection {
readonly changed: ISignal<ReadonlyArray<IScript>>; readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
readonly selectedScripts: ReadonlyArray<IScript>; readonly selectedScripts: ReadonlyArray<SelectedScript>;
readonly totalSelected: number; readonly totalSelected: number;
addSelectedScript(scriptId: string): void; addSelectedScript(scriptId: string, revert: boolean): void;
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
removeSelectedScript(scriptId: string): void; removeSelectedScript(scriptId: string): void;
selectOnly(scripts: ReadonlyArray<IScript>): void; selectOnly(scripts: ReadonlyArray<IScript>): void;
isSelected(script: IScript): boolean; isSelected(script: IScript): boolean;

View File

@@ -0,0 +1,14 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from '@/domain/IScript';
export class SelectedScript extends BaseEntity<string> {
constructor(
public readonly script: IScript,
public readonly revert: boolean,
) {
super(script.id);
if (revert && !script.canRevert()) {
throw new Error('cannot revert an irreversible script');
}
}
}

View File

@@ -1,13 +1,14 @@
import { SelectedScript } from './SelectedScript';
import { IApplication } from '@/domain/IApplication'; import { IApplication } from '@/domain/IApplication';
import { IUserSelection } from './IUserSelection'; import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/Script'; import { IScript } from '@/domain/IScript';
import { Signal } from '@/infrastructure/Events/Signal'; import { Signal } from '@/infrastructure/Events/Signal';
import { IRepository } from '@/infrastructure/Repository/IRepository';
export class UserSelection implements IUserSelection { export class UserSelection implements IUserSelection {
public readonly changed = new Signal<ReadonlyArray<IScript>>(); public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
private readonly scripts: IRepository<string, SelectedScript> = new InMemoryRepository<string, SelectedScript>();
private readonly scripts = new InMemoryRepository<string, IScript>();
constructor( constructor(
private readonly app: IApplication, private readonly app: IApplication,
@@ -15,33 +16,40 @@ export class UserSelection implements IUserSelection {
selectedScripts: ReadonlyArray<IScript>) { selectedScripts: ReadonlyArray<IScript>) {
if (selectedScripts && selectedScripts.length > 0) { if (selectedScripts && selectedScripts.length > 0) {
for (const script of selectedScripts) { for (const script of selectedScripts) {
this.scripts.addItem(script); const selected = new SelectedScript(script, false);
this.scripts.addItem(selected);
} }
} }
} }
/** Add a script to users application */ public addSelectedScript(scriptId: string, revert: boolean): void {
public addSelectedScript(scriptId: string): void {
const script = this.app.findScript(scriptId); const script = this.app.findScript(scriptId);
if (!script) { if (!script) {
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`); throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
} }
this.scripts.addItem(script); const selectedScript = new SelectedScript(script, revert);
this.scripts.addItem(selectedScript);
this.changed.notify(this.scripts.getItems());
}
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
const script = this.app.findScript(scriptId);
const selectedScript = new SelectedScript(script, revert);
this.scripts.addOrUpdateItem(selectedScript);
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
/** Remove a script from users application */
public removeSelectedScript(scriptId: string): void { public removeSelectedScript(scriptId: string): void {
this.scripts.removeItem(scriptId); this.scripts.removeItem(scriptId);
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
public isSelected(script: IScript): boolean { public isSelected(script: IScript): boolean {
return this.scripts.exists(script); return this.scripts.exists(script.id);
} }
/** Get users scripts based on his/her selections */ /** Get users scripts based on his/her selections */
public get selectedScripts(): ReadonlyArray<IScript> { public get selectedScripts(): ReadonlyArray<SelectedScript> {
return this.scripts.getItems(); return this.scripts.getItems();
} }
@@ -51,8 +59,9 @@ export class UserSelection implements IUserSelection {
public selectAll(): void { public selectAll(): void {
for (const script of this.app.getAllScripts()) { for (const script of this.app.getAllScripts()) {
if (!this.scripts.exists(script)) { if (!this.scripts.exists(script.id)) {
this.scripts.addItem(script); const selection = new SelectedScript(script, false);
this.scripts.addItem(selection);
} }
} }
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
@@ -78,9 +87,11 @@ export class UserSelection implements IUserSelection {
.forEach((scriptId) => this.scripts.removeItem(scriptId)); .forEach((scriptId) => this.scripts.removeItem(scriptId));
} }
// Select from unselected scripts // Select from unselected scripts
scripts const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
.filter((script) => !this.scripts.exists(script)) for (const toSelect of unselectedScripts) {
.forEach((script) => this.scripts.addItem(script)); const selection = new SelectedScript(toSelect, false);
this.scripts.addItem(selection);
}
this.changed.notify(this.scripts.getItems()); this.changed.notify(this.scripts.getItems());
} }
} }

View File

@@ -1,3 +1,4 @@
# Structure documented in "./application.yaml.d.ts" (as code)
name: privacy.sexy name: privacy.sexy
repositoryUrl: https://github.com/undergroundwires/privacy.sexy repositoryUrl: https://github.com/undergroundwires/privacy.sexy
actions: actions:
@@ -298,13 +299,17 @@ actions:
recommended: false recommended: false
docs: https://www.windowslifestyle.com/reset-data-usage-tool-reset-network-data-usage-windows-10/ docs: https://www.windowslifestyle.com/reset-data-usage-tool-reset-network-data-usage-windows-10/
code: |- code: |-
SET was_running=0 setlocal EnableDelayedExpansion
net stop DPS && was_running=1 SET /A dps_service_running=0
echo %was_running% SC queryex "DPS"|Find "STATE"|Find /v "RUNNING">Nul||(
del /F /S /Q /A "%windir%\System32\sru*" SET /A dps_service_running=1
IF NOT %was_running% == 0 ( net stop DPS
net start DPS )
) del /F /S /Q /A "%windir%\System32\sru*"
IF !dps_service_running! == 1 (
net start DPS
)
endlocal
- -
category: Disable OS data collection category: Disable OS data collection
@@ -316,10 +321,13 @@ actions:
name: Disable Customer Experience Improvement (CEIP/SQM) name: Disable Customer Experience Improvement (CEIP/SQM)
recommend: true recommend: true
code: reg add "HKLM\Software\Policies\Microsoft\SQMClient\Windows" /v "CEIPEnable" /t REG_DWORD /d "0" /f code: reg add "HKLM\Software\Policies\Microsoft\SQMClient\Windows" /v "CEIPEnable" /t REG_DWORD /d "0" /f
revertCode: reg add "HKLM\Software\Policies\Microsoft\SQMClient\Windows" /v "CEIPEnable" /t REG_DWORD /d "1" /f
docs: https://docs.microsoft.com/en-us/windows/win32/devnotes/ceipenable
- -
name: Disable Application Impact Telemetry (AIT) name: Disable Application Impact Telemetry (AIT)
recommend: true recommend: true
code: reg add "HKLM\Software\Policies\Microsoft\Windows\AppCompat" /v "AITEnable" /t REG_DWORD /d "0" /f code: reg add "HKLM\Software\Policies\Microsoft\Windows\AppCompat" /v "AITEnable" /t REG_DWORD /d "0" /f
revertCode: reg add "HKLM\Software\Policies\Microsoft\SQMClient\Windows" /v "CEIPEnable" /t REG_DWORD /d "1" /f
- -
name: Disable diagnostics telemetry name: Disable diagnostics telemetry
recommend: true recommend: true
@@ -339,13 +347,10 @@ actions:
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\Consolidator" /DISABLE schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\Consolidator" /DISABLE
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /DISABLE schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /DISABLE
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /DISABLE schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /DISABLE
- revertCode: |-
name: Disabling Data Logging Services schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\Consolidator" /ENABLE
recommend: true schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /ENABLE
code: |- schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /ENABLE
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\Consolidator" /DISABLE
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\KernelCeipTask" /DISABLE
schtasks /change /TN "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip" /DISABLE
- -
name: Disable telemetry in data collection policy name: Disable telemetry in data collection policy
recommend: true recommend: true
@@ -373,21 +378,25 @@ actions:
code: |- code: |-
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Device Metadata" /v "PreventDeviceMetadataFromNetwork" /t REG_DWORD /d 1 /f
-
name: Disable active prompting (pings to MSFT NCSI server)
recommend: false
code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet" /v "EnableActiveProbing" /t REG_DWORD /d "0" /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet" /v "EnableActiveProbing" /t REG_DWORD /d "1" /f
- -
name: Opt out from Windows privacy consent name: Opt out from Windows privacy consent
recommend: true recommend: true
code: |- code: reg add "HKCU\SOFTWARE\Microsoft\Personalization\Settings" /v "AcceptedPrivacyPolicy" /t REG_DWORD /d 0 /f
reg add "HKCU\SOFTWARE\Microsoft\Personalization\Settings" /v "AcceptedPrivacyPolicy" /t REG_DWORD /d 0 /f revertCode: reg add "HKCU\SOFTWARE\Microsoft\Personalization\Settings" /v "AcceptedPrivacyPolicy" /t REG_DWORD /d 1 /f
- -
name: Disable Windows feedback name: Disable Windows feedback
recommend: true recommend: true
docs: https://www.tenforums.com/tutorials/2441-change-feedback-frequency-windows-10-a.html
code: |- code: |-
reg add "HKCU\SOFTWARE\Microsoft\Siuf\Rules" /v "NumberOfSIUFInPeriod" /t REG_DWORD /d 0 /f reg add "HKCU\SOFTWARE\Microsoft\Siuf\Rules" /v "NumberOfSIUFInPeriod" /t REG_DWORD /d 0 /f
:: removing this value sets feedback frequency to never
reg delete "HKCU\SOFTWARE\Microsoft\Siuf\Rules" /v "PeriodInNanoSeconds" /f reg delete "HKCU\SOFTWARE\Microsoft\Siuf\Rules" /v "PeriodInNanoSeconds" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "DoNotShowFeedbackNotifications" /t REG_DWORD /d 1 /f reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "DoNotShowFeedbackNotifications" /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection" /v "DoNotShowFeedbackNotifications" /t REG_DWORD /d 1 /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection" /v "DoNotShowFeedbackNotifications" /t REG_DWORD /d 1 /f
docs: https://www.tenforums.com/tutorials/2441-change-feedback-frequency-windows-10-a.html
- -
name: Disable text and handwriting collection name: Disable text and handwriting collection
recommend: true recommend: true
@@ -517,28 +526,28 @@ actions:
- -
name: Deny app access to videos name: Deny app access to videos
recommend: true recommend: true
code: |- code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\videosLibrary" /v "Value" /d "Deny" /t REG_SZ /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\videosLibrary" /v "Value" /d "Deny" /t REG_SZ /f revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\videosLibrary" /v "Value" /d "Allow" /t REG_SZ /f
- -
name: Deny app access to pictures name: Deny app access to pictures
recommend: true recommend: true
code: |- code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\picturesLibrary" /v "Value" /d "Deny" /t REG_SZ /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\picturesLibrary" /v "Value" /d "Deny" /t REG_SZ /f revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\picturesLibrary" /v "Value" /d "Allow" /t REG_SZ /f
- -
name: Deny app access to documents name: Deny app access to documents
recommend: true recommend: true
code: |- code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\documentsLibrary" /v "Value" /d "Deny" /t REG_SZ /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\documentsLibrary" /v "Value" /d "Deny" /t REG_SZ /f revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\documentsLibrary" /v "Value" /d "Allow" /t REG_SZ /f
- -
name: Deny app access to bluetooth devices name: Deny app access to bluetooth devices
recommend: true recommend: true
code: |- code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Deny" /t REG_SZ /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Deny" /t REG_SZ /f revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\bluetoothSync" /v "Value" /d "Allow" /t REG_SZ /f
- -
name: Deny app access to text/mms name: Deny app access to text/mms
recommend: true recommend: true
code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\DeviceAccess\Global\{992AFA70-6F47-4148-B3E9-3003349C1548}" /t REG_SZ /v "Value" /d DENY /f code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\DeviceAccess\Global\{992AFA70-6F47-4148-B3E9-3003349C1548}" /t REG_SZ /v "Value" /d "Deny" /f
revertCode: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\DeviceAccess\Global\{992AFA70-6F47-4148-B3E9-3003349C1548}" /t REG_SZ /v "Value" /d "Allow" /f
- -
name: Deny location access name: Deny location access
recommend: true recommend: true
@@ -638,15 +647,19 @@ actions:
name: Disable App Launch Tracking name: Disable App Launch Tracking
docs: https://www.thewindowsclub.com/enable-or-disable-app-launch-tracking-in-windows-10 docs: https://www.thewindowsclub.com/enable-or-disable-app-launch-tracking-in-windows-10
recommend: true recommend: true
code: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Start_TrackProgs" /d "0" /t REG_DWORD /f code: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Start_TrackProgs" /d 0 /t REG_DWORD /f
revertCode: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v "Start_TrackProgs" /d 1 /t REG_DWORD /f
- -
name: Disable Inventory Collector name: Disable Inventory Collector
recommend: true recommend: true
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppCompat" /v "DisableInventory" /t REG_DWORD /d 1 /f code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppCompat" /v "DisableInventory" /t REG_DWORD /d 1 /f
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\AppCompat" /v "DisableInventory" /t REG_DWORD /d 0 /f
- -
name: Disable Website Access of Language List name: Disable Website Access of Language List
recommend: true recommend: true
docs: https://www.tenforums.com/tutorials/82980-turn-off-website-access-language-list-windows-10-a.html
code: reg add "HKCU\Control Panel\International\User Profile" /v "HttpAcceptLanguageOptOut" /t REG_DWORD /d 1 /f code: reg add "HKCU\Control Panel\International\User Profile" /v "HttpAcceptLanguageOptOut" /t REG_DWORD /d 1 /f
revertCode: reg add "HKCU\Control Panel\International\User Profile" /v "HttpAcceptLanguageOptOut" /t REG_DWORD /d 0 /f
- -
name: Disable Auto Downloading Maps name: Disable Auto Downloading Maps
recommend: true recommend: true
@@ -867,7 +880,20 @@ actions:
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\15.0\osm" /v "Enablelogging" /t REG_DWORD /d 0 /f reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\15.0\osm" /v "Enablelogging" /t REG_DWORD /d 0 /f
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\15.0\osm" /v "EnableUpload" /t REG_DWORD /d 0 /f reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\15.0\osm" /v "EnableUpload" /t REG_DWORD /d 0 /f
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\16.0\osm" /v "Enablelogging" /t REG_DWORD /d 0 /f reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\16.0\osm" /v "Enablelogging" /t REG_DWORD /d 0 /f
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\16.0\osm" /v "EnableUpload" /t REG_DWORD /d 0 /f reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\16.0\osm" /v "EnableUpload" /t REG_DWORD /d 0 /f
schtasks /change /TN "Microsoft\Office\Office ClickToRun Service Monitor" /DISABLE
schtasks /change /TN "Microsoft\Office\OfficeTelemetryAgentFallBack2016" /DISABLE
schtasks /change /TN "Microsoft\Office\OfficeTelemetryAgentLogOn2016" /DISABLE
sc stop "ClickToRunSvc" & sc config "ClickToRunSvc" start=disabled
revertCode: |-
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\15.0\osm" /v "Enablelogging" /t REG_DWORD /d 1 /f
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\15.0\osm" /v "EnableUpload" /t REG_DWORD /d 1 /f
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\16.0\osm" /v "Enablelogging" /t REG_DWORD /d 1 /f
reg add "HKCU\SOFTWARE\Policies\Microsoft\Office\16.0\osm" /v "EnableUpload" /t REG_DWORD /d 1 /f
schtasks /change /TN "Microsoft\Office\Office ClickToRun Service Monitor" /ENABLE
schtasks /change /TN "Microsoft\Office\OfficeTelemetryAgentFallBack2016" /ENABLE
schtasks /change /TN "Microsoft\Office\OfficeTelemetryAgentLogOn2016" /ENABLE
sc config "ClickToRunSvc" start=auto
- -
category: Configure browsers category: Configure browsers
children: children:
@@ -877,7 +903,7 @@ actions:
- -
name: Disable live tile data collection name: Disable live tile data collection
recommend: true recommend: true
code: reg add "HKCU\Software\Policies\Microsoft\MicrosoftEdge\Main" /v "PreventLiveTileDataCollection" /t REG_DWORD /d 1 /f code: reg add "HKCU\Software\Policies\Microsoft\MicrosoftEdge\Main" /v "PreventLiveTileDataCollection" /t REG_DWORD /d 1 /f
- -
name: Disable MFU tracking name: Disable MFU tracking
recommend: true recommend: true
@@ -1031,6 +1057,7 @@ actions:
name: Disable administrative shares name: Disable administrative shares
recommend: true recommend: true
code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" /v "AutoShareWks" /t REG_DWORD /d 0 /f code: reg add "HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" /v "AutoShareWks" /t REG_DWORD /d 0 /f
revertCode: reg add "HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters" /v "AutoShareWks" /t REG_DWORD /d 1 /f
- -
name: Force enable data execution prevention (DEP) name: Force enable data execution prevention (DEP)
recommend: false recommend: false
@@ -1130,7 +1157,16 @@ actions:
- -
name: Disable Windows Defender name: Disable Windows Defender
recommend: false recommend: false
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v DisableAntiSpyware /t REG_DWORD /d 1 /f code: |-
netsh advfirewall set allprofiles state off
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v DisableAntiSpyware /t REG_DWORD /d 1 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\MpsSvc" /v "Start" /t REG_DWORD /d 4 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" /v "Start" /t REG_DWORD /d 4 /f
revertCode: |-
netsh advfirewall set allprofiles state on
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender" /v DisableAntiSpyware /t REG_DWORD /d 0 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\MpsSvc" /v "Start" /t REG_DWORD /d 2 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\WinDefend" /v "Start" /t REG_DWORD /d 2 /f
- -
name: Disable Smart Screen name: Disable Smart Screen
recommend: false recommend: false
@@ -1140,19 +1176,33 @@ actions:
reg add "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Explorer" /v "SmartScreenEnabled" /t REG_SZ /d "Off" /f reg add "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Explorer" /v "SmartScreenEnabled" /t REG_SZ /d "Off" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d 0 /f reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d 0 /f
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d 0 /f reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d 0 /f
revertCode: |-
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v "EnableSmartScreen" /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" /v "SmartScreenEnabled" /t REG_SZ /d "Warn" /f
reg add "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Explorer" /v "SmartScreenEnabled" /t REG_SZ /d "Warn" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d 1 /f
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost" /v "EnableWebContentEvaluation" /t REG_DWORD /d 1 /f
- -
name: Disable scheduled On Demand anti malware scanner (MRT) name: Disable scheduled On Demand anti malware scanner (MRT)
recommend: false recommend: false
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\MRT" /v "DontOfferThroughWUAU" /t REG_DWORD /d 1 /f code: reg add "HKLM\SOFTWARE\Policies\Microsoft\MRT" /v "DontOfferThroughWUAU" /t REG_DWORD /d 1 /f
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\MRT" /v "DontOfferThroughWUAU" /t REG_DWORD /d 0 /f
- -
name: Disable automatic updates name: Disable automatic updates
recommend: false recommend: false
docs: https://docs.microsoft.com/fr-fr/security-updates/windowsupdateservices/18127152
code: |- code: |-
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "0" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "2" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /t "REG_DWORD" /d "0" /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /t "REG_DWORD" /d "0" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /t "REG_DWORD" /d "3" /f reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /t "REG_DWORD" /d "3" /f
sc stop "UsoSvc" & sc config "UsoSvc" start=disabled sc stop "UsoSvc" & sc config "UsoSvc" start=disabled
revertCode: |-
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "NoAutoUpdate" /t "REG_DWORD" /d "1" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "AUOptions" /t "REG_DWORD" /d "3" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallDay" /t "REG_DWORD" /d "0" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" /v "ScheduledInstallTime" /t "REG_DWORD" /d "0" /f
sc config "UsoSvc" start=auto
- -
category: UI for privacy category: UI for privacy
children: children:
@@ -1160,6 +1210,8 @@ actions:
name: Disable lock screen app notifications name: Disable lock screen app notifications
recommend: true recommend: true
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v "DisableLockScreenAppNotifications" /t REG_DWORD /d 1 /f code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v "DisableLockScreenAppNotifications" /t REG_DWORD /d 1 /f
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v "DisableLockScreenAppNotifications" /t REG_DWORD /d 0 /f
docs: https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-36687
- -
name: Disable online content in explorer name: Disable online content in explorer
recommend: true recommend: true
@@ -1321,8 +1373,7 @@ actions:
recommend: true recommend: true
docs: https://docs.microsoft.com/en-us/windows-server/storage/file-server/volume-shadow-copy-service docs: https://docs.microsoft.com/en-us/windows-server/storage/file-server/volume-shadow-copy-service
code: sc stop "VSS" & sc config "VSS" start=disabled code: sc stop "VSS" & sc config "VSS" start=disabled
revertCode: sc config vss start=auto
- -
category: Remove bloatware category: Remove bloatware
children: children:
@@ -2059,12 +2110,19 @@ actions:
- -
name: Disable Reserved Storage for updates name: Disable Reserved Storage for updates
recommend: false recommend: false
docs: https://techcommunity.microsoft.com/t5/storage-at-microsoft/windows-10-and-reserved-storage/ba-p/428327 docs:
- https://techcommunity.microsoft.com/t5/storage-at-microsoft/windows-10-and-reserved-storage/ba-p/428327
- https://www.tenforums.com/tutorials/124858-enable-disable-reserved-storage-windows-10-a.html
code: |- code: |-
dism /online /Set-ReservedStorageState /State:Disabled /NoRestart dism /online /Set-ReservedStorageState /State:Disabled /NoRestart
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "MiscPolicyInfo" /t REG_DWORD /d "2" /f reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "MiscPolicyInfo" /t REG_DWORD /d "2" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "ShippedWithReserves" /t REG_DWORD /d "0" /f reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "ShippedWithReserves" /t REG_DWORD /d "0" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "PassedPolicy" /t REG_DWORD /d "0" /f reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "PassedPolicy" /t REG_DWORD /d "0" /f
revertCode: |-
DISM /Online /Set-ReservedStorageState /State:Enabled /NoRestart
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "MiscPolicyInfo" /t REG_DWORD /d "1" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "ShippedWithReserves" /t REG_DWORD /d "1" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ReserveManager" /v "PassedPolicy" /t REG_DWORD /d "1" /f
- -
name: Run script on start-up [EXPERIMENTAL] name: Run script on start-up [EXPERIMENTAL]
recommend: false recommend: false

View File

@@ -1,5 +1,5 @@
declare module 'js-yaml-loader!*' { declare module 'js-yaml-loader!*' {
type CategoryOrScript = YamlCategory | YamlScript; export type CategoryOrScript = YamlCategory | YamlScript;
type DocumentationUrls = ReadonlyArray<string> | string; type DocumentationUrls = ReadonlyArray<string> | string;
export interface YamlDocumentable { export interface YamlDocumentable {
@@ -9,6 +9,7 @@ declare module 'js-yaml-loader!*' {
export interface YamlScript extends YamlDocumentable { export interface YamlScript extends YamlDocumentable {
name: string; name: string;
code: string; code: string;
revertCode: string;
recommend: boolean; recommend: boolean;
} }
@@ -17,7 +18,7 @@ declare module 'js-yaml-loader!*' {
category: string; category: string;
} }
interface ApplicationYaml { export interface ApplicationYaml {
name: string; name: string;
repositoryUrl: string; repositoryUrl: string;
actions: ReadonlyArray<YamlCategory>; actions: ReadonlyArray<YamlCategory>;

133
src/background.ts Normal file
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 nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: (process.env
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
},
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#icons
icon: path.join(__static, `favicon.ico`),
});
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

@@ -13,11 +13,11 @@ export class Application implements IApplication {
public readonly name: string, public readonly name: string,
public readonly repositoryUrl: string, public readonly repositoryUrl: string,
public readonly version: string, public readonly version: string,
public readonly categories: ReadonlyArray<ICategory>) { public readonly actions: ReadonlyArray<ICategory>) {
if (!name) { throw Error('Application has no name'); } if (!name) { throw Error('Application has no name'); }
if (!repositoryUrl) { throw Error('Application has no repository url'); } if (!repositoryUrl) { throw Error('Application has no repository url'); }
if (!version) { throw Error('Version cannot be empty'); } if (!version) { throw Error('Version cannot be empty'); }
this.flattened = flatten(categories); this.flattened = flatten(actions);
if (this.flattened.allCategories.length === 0) { if (this.flattened.allCategories.length === 0) {
throw new Error('Application must consist of at least one category'); throw new Error('Application must consist of at least one category');
} }

View File

@@ -5,9 +5,9 @@ export interface IApplication {
readonly name: string; readonly name: string;
readonly repositoryUrl: string; readonly repositoryUrl: string;
readonly version: string; readonly version: string;
readonly categories: ReadonlyArray<ICategory>;
readonly totalScripts: number; readonly totalScripts: number;
readonly totalCategories: number; readonly totalCategories: number;
readonly actions: ReadonlyArray<ICategory>;
getRecommendedScripts(): ReadonlyArray<IScript>; getRecommendedScripts(): ReadonlyArray<IScript>;
findCategory(categoryId: number): ICategory | undefined; findCategory(categoryId: number): ICategory | undefined;

View File

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

View File

@@ -2,44 +2,56 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from './IScript'; import { IScript } from './IScript';
export class Script extends BaseEntity<string> implements IScript { export class Script extends BaseEntity<string> implements IScript {
private static ensureNoEmptyLines(name: string, code: string): void {
if (code.split('\n').some((line) => line.trim().length === 0)) {
throw Error(`Script has empty lines "${name}"`);
}
}
private static ensureCodeHasUniqueLines(name: string, code: string): void {
const lines = code.split('\n')
.filter((line) => this.mayBeUniqueLine(line));
if (lines.length === 0) {
return;
}
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
if (duplicateLines.length !== 0) {
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
}
}
private static mayBeUniqueLine(codeLine: string): boolean {
const trimmed = codeLine.trim();
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
return false;
}
return true;
}
constructor( constructor(
public name: string, public readonly name: string,
public code: string, public readonly code: string,
public documentationUrls: ReadonlyArray<string>, public readonly revertCode: string,
public isRecommended: boolean) { public readonly documentationUrls: ReadonlyArray<string>,
public readonly isRecommended: boolean) {
super(name); super(name);
if (code == null || code.length === 0) { validateCode(name, code);
throw new Error('Code is empty or null'); if (revertCode) {
validateCode(name, revertCode);
if (code === revertCode) {
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
}
} }
Script.ensureCodeHasUniqueLines(name, code); }
Script.ensureNoEmptyLines(name, code); public canRevert(): boolean {
return Boolean(this.revertCode);
} }
} }
export { IScript } from './IScript'; function validateCode(name: string, code: string): void {
if (!code || code.length === 0) {
throw new Error(`Code of ${name} is empty or null`);
}
ensureCodeHasUniqueLines(name, code);
ensureNoEmptyLines(name, code);
}
function ensureNoEmptyLines(name: string, code: string): void {
if (code.split('\n').some((line) => line.trim().length === 0)) {
throw Error(`Script has empty lines "${name}"`);
}
}
function mayBeUniqueLine(codeLine: string): boolean {
const trimmed = codeLine.trim();
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
return false;
}
return true;
}
function ensureCodeHasUniqueLines(name: string, code: string): void {
const lines = code.split('\n')
.filter((line) => mayBeUniqueLine(line));
if (lines.length === 0) {
return;
}
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
if (duplicateLines.length !== 0) {
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
}
}

1
src/global.d.ts vendored
View File

@@ -13,6 +13,7 @@ declare module 'liquor-tree' {
} }
interface ICustomLiquorTreeData { interface ICustomLiquorTreeData {
documentationUrls: ReadonlyArray<string>; documentationUrls: ReadonlyArray<string>;
isReversible: boolean;
} }
/** /**

View File

@@ -4,6 +4,7 @@ export interface IRepository<TKey, TEntity extends IEntity<TKey>> {
readonly length: number; readonly length: number;
getItems(predicate?: (entity: TEntity) => boolean): TEntity[]; getItems(predicate?: (entity: TEntity) => boolean): TEntity[];
addItem(item: TEntity): void; addItem(item: TEntity): void;
addOrUpdateItem(item: TEntity): void;
removeItem(id: TKey): void; removeItem(id: TKey): void;
exists(item: TEntity): boolean; exists(id: TKey): boolean;
} }

View File

@@ -18,14 +18,24 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
public addItem(item: TEntity): void { public addItem(item: TEntity): void {
if (!item) { if (!item) {
throw new Error('Item is null'); throw new Error('item is null or undefined');
} }
if (this.exists(item)) { if (this.exists(item.id)) {
throw new Error(`Cannot add (id: ${item.id}) as it is already exists`); throw new Error(`Cannot add (id: ${item.id}) as it is already exists`);
} }
this.items.push(item); this.items.push(item);
} }
public addOrUpdateItem(item: TEntity): void {
if (!item) {
throw new Error('item is null or undefined');
}
if (this.exists(item.id)) {
this.removeItem(item.id);
}
this.items.push(item);
}
public removeItem(id: TKey): void { public removeItem(id: TKey): void {
const index = this.items.findIndex((item) => item.id === id); const index = this.items.findIndex((item) => item.id === id);
if (index === -1) { if (index === -1) {
@@ -34,8 +44,8 @@ export class InMemoryRepository<TKey, TEntity extends IEntity<TKey>> implements
this.items.splice(index, 1); this.items.splice(index, 1);
} }
public exists(entity: TEntity): boolean { public exists(id: TKey): boolean {
const index = this.items.findIndex((item) => item.id === entity.id); const index = this.items.findIndex((item) => item.id === id);
return index !== -1; return index !== -1;
} }
} }

View File

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

View File

@@ -9,7 +9,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator'; import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { StatefulVue, IApplicationState } from './StatefulVue'; import { StatefulVue } from './StatefulVue';
import { SaveFileDialog } from './../infrastructure/SaveFileDialog'; import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
import { Clipboard } from './../infrastructure/Clipboard'; import { Clipboard } from './../infrastructure/Clipboard';

View File

@@ -18,8 +18,9 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import CardListItem from './CardListItem.vue'; import CardListItem from './CardListItem.vue';
import { StatefulVue, IApplicationState } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory'; import { ICategory } from '@/domain/ICategory';
import { hasDirective } from './NonCollapsingDirective';
@Component({ @Component({
components: { components: {
@@ -32,8 +33,11 @@ export default class CardList extends StatefulVue {
public async mounted() { public async mounted() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.setCategories(state.app.categories); this.setCategories(state.app.actions);
this.onOutsideOfActiveCardClicked(() => { this.onOutsideOfActiveCardClicked((element) => {
if (hasDirective(element)) {
return;
}
this.activeCategoryId = null; this.activeCategoryId = null;
}); });
} }
@@ -46,14 +50,14 @@ export default class CardList extends StatefulVue {
this.categoryIds = categories.map((category) => category.id); this.categoryIds = categories.map((category) => category.id);
} }
private onOutsideOfActiveCardClicked(callback) { private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
const outsideClickListener = (event) => { const outsideClickListener = (event) => {
if (!this.activeCategoryId) { if (!this.activeCategoryId) {
return; return;
} }
const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`); const element = document.querySelector(`[data-category="${this.activeCategoryId}"]`);
if (!element.contains(event.target)) { if (element && !element.contains(event.target)) {
callback(); callback(event.target);
} }
}; };
document.addEventListener('click', outsideClickListener); document.addEventListener('click', outsideClickListener);

View File

@@ -4,20 +4,22 @@
v-bind:class="{ v-bind:class="{
'is-collapsed': !isExpanded, 'is-collapsed': !isExpanded,
'is-inactive': activeCategoryId && activeCategoryId != categoryId, 'is-inactive': activeCategoryId && activeCategoryId != categoryId,
'is-expanded': isExpanded}"> 'is-expanded': isExpanded
<div class="card__inner"> }"
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span> ref="cardElement">
<span v-else>Oh no 😢</span> <div class="card__inner">
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" /> <span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
<span v-else>Oh no 😢</span>
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="card__inner__expand-icon" />
</div>
<div class="card__expander" v-on:click.stop>
<div class="card__expander__content">
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
</div>
<div class="card__expander__close-button">
<font-awesome-icon :icon="['fas', 'times']" v-on:click="onSelected(false)"/>
</div>
</div> </div>
<div class="card__expander" v-on:click.stop>
<div class="card__expander__content">
<ScriptsTree :categoryId="categoryId"></ScriptsTree>
</div>
<div class="card__expander__close-button">
<font-awesome-icon :icon="['fas', 'times']" v-on:click="onSelected(false)"/>
</div>
</div>
</div> </div>
</template> </template>
@@ -41,11 +43,18 @@ export default class CardListItem extends StatefulVue {
public onSelected(isExpanded: boolean) { public onSelected(isExpanded: boolean) {
this.isExpanded = isExpanded; this.isExpanded = isExpanded;
} }
@Watch('activeCategoryId') @Watch('activeCategoryId')
public async onActiveCategoryChanged(value: |number) { public async onActiveCategoryChanged(value: |number) {
this.isExpanded = value === this.categoryId; this.isExpanded = value === this.categoryId;
} }
@Watch('isExpanded')
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
if (!oldValue && newValue) {
await new Promise((r) => setTimeout(r, 400));
const focusElement = this.$refs.cardElement as HTMLElement;
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
}
}
public async mounted() { public async mounted() {
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined; this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
@@ -66,32 +75,39 @@ export default class CardListItem extends StatefulVue {
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/presentation/styles/colors.scss"; @import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/media.scss";
$big-screen-width: 991px; $card-padding: 30px;
$medium-screen-width: 767px; $card-margin: 15px;
$small-screen-width: 380px; $card-line-break-width: 30px;
$arrow-size: 15px;
$expanded-margin-top: 30px;
.card { .card {
margin: 15px; margin: 15px;
width: calc((100% / 3) - 30px); width: calc((100% / 3) - #{$card-line-break-width});
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
// Media queries for stacking cards // Media queries for stacking cards
@media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - 30px); } @media screen and (max-width: $big-screen-width) { width: calc((100% / 2) - #{$card-line-break-width}); }
@media screen and (max-width: $medium-screen-width) { width: 100%; } @media screen and (max-width: $medium-screen-width) { width: 100%; }
@media screen and (max-width: $small-screen-width) { width: 90%; } @media screen and (max-width: $small-screen-width) { width: 90%; }
&__inner { &__inner {
padding: 30px; padding: $card-padding;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
background-color: $gray; background-color: $gray;
color: $light-gray; color: $light-gray;
font-size: 1.5em; font-size: 1.5em;
height: 100%;
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
display:flex;
flex-direction: column;
justify-content: center;
&:hover { &:hover {
background-color: $accent; background-color: $accent;
transform: scale(1.05); transform: scale(1.05);
@@ -151,16 +167,17 @@ $small-screen-width: 380px;
&.is-expanded { &.is-expanded {
.card__inner { .card__inner {
height: auto;
background-color: $accent; background-color: $accent;
&:after{ &:after { // arrow
content: ""; content: "";
display: block; display: block;
position: absolute; position: absolute;
bottom: -30px; bottom: calc(-1 * #{$expanded-margin-top});
left: calc(50% - 15px); left: calc(50% - #{$arrow-size});
border-left: 15px solid transparent; border-left: #{$arrow-size} solid transparent;
border-right: 15px solid transparent; border-right: #{$arrow-size} solid transparent;
border-bottom: 15px solid #333a45; border-bottom: #{$arrow-size} solid #333a45;
} }
} }
@@ -168,7 +185,7 @@ $small-screen-width: 380px;
min-height: 200px; min-height: 200px;
// max-height: 1000px; // max-height: 1000px;
// overflow-y: auto; // overflow-y: auto;
margin-top: 30px; margin-top: $expanded-margin-top;
opacity: 1; opacity: 1;
} }
@@ -182,7 +199,9 @@ $small-screen-width: 380px;
&.is-inactive { &.is-inactive {
.card__inner { .card__inner {
pointer-events: none; pointer-events: none;
height: auto;
opacity: 0.5; opacity: 0.5;
transform: scale(0.95);
} }
&:hover { &:hover {
@@ -196,28 +215,28 @@ $small-screen-width: 380px;
@media screen and (min-width: $big-screen-width) { // when 3 cards in a row @media screen and (min-width: $big-screen-width) { // when 3 cards in a row
.card:nth-of-type(3n+2) .card__expander { .card:nth-of-type(3n+2) .card__expander {
margin-left: calc(-100% - 30px); margin-left: calc(-100% - #{$card-line-break-width});
} }
.card:nth-of-type(3n+3) .card__expander { .card:nth-of-type(3n+3) .card__expander {
margin-left: calc(-200% - 60px); margin-left: calc(-200% - (#{$card-line-break-width} * 2));
} }
.card:nth-of-type(3n+4) { .card:nth-of-type(3n+4) {
clear: left; clear: left;
} }
.card__expander { .card__expander {
width: calc(300% + 60px); width: calc(300% + (#{$card-line-break-width} * 2));
} }
} }
@media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row @media screen and (min-width: $medium-screen-width) and (max-width: $big-screen-width) { // when 2 cards in a row
.card:nth-of-type(2n+2) .card__expander { .card:nth-of-type(2n+2) .card__expander {
margin-left: calc(-100% - 30px); margin-left: calc(-100% - #{$card-line-break-width});
} }
.card:nth-of-type(2n+3) { .card:nth-of-type(2n+3) {
clear: left; clear: left;
} }
.card__expander { .card__expander {
width: calc(200% + 30px); width: calc(200% + #{$card-line-break-width});
} }
} }
</style> </style>

View File

@@ -0,0 +1,17 @@
import { DirectiveOptions } from 'vue';
const attributeName = 'data-interactionDoesNotCollapse';
export function hasDirective(el: Element): boolean {
if (el.hasAttribute(attributeName)) {
return true;
}
const parent = el.closest(`[${attributeName}]`);
return !!parent;
}
export const NonCollapsing: DirectiveOptions = {
inserted(el: HTMLElement) {
el.setAttribute(attributeName, '');
},
};

View File

@@ -4,7 +4,7 @@ import { INode } from './SelectableTree/INode';
export function parseAllCategories(app: IApplication): INode[] | undefined { export function parseAllCategories(app: IApplication): INode[] | undefined {
const nodes = new Array<INode>(); const nodes = new Array<INode>();
for (const category of app.categories) { for (const category of app.actions) {
const children = parseCategoryRecursively(category); const children = parseCategoryRecursively(category);
nodes.push(convertCategoryToNode(category, children)); nodes.push(convertCategoryToNode(category, children));
} }
@@ -23,6 +23,7 @@ export function parseSingleCategory(categoryId: number, app: IApplication): INod
export function getScriptNodeId(script: IScript): string { export function getScriptNodeId(script: IScript): string {
return script.id; return script.id;
} }
export function getCategoryNodeId(category: ICategory): string { export function getCategoryNodeId(category: ICategory): string {
return `Category${category.id}`; return `Category${category.id}`;
} }
@@ -53,6 +54,7 @@ function convertCategoryToNode(
text: category.name, text: category.name,
children, children,
documentationUrls: category.documentationUrls, documentationUrls: category.documentationUrls,
isReversible: false,
}; };
} }
@@ -62,5 +64,6 @@ function convertScriptToNode(script: IScript): INode {
text: script.name, text: script.name,
children: undefined, children: undefined,
documentationUrls: script.documentationUrls, documentationUrls: script.documentationUrls,
isReversible: script.canRevert(),
}; };
} }

View File

@@ -6,7 +6,9 @@
:selectedNodeIds="selectedNodeIds" :selectedNodeIds="selectedNodeIds"
:filterPredicate="filterPredicate" :filterPredicate="filterPredicate"
:filterText="filterText" :filterText="filterText"
v-on:nodeSelected="checkNodeAsync($event)"> v-on:nodeSelected="toggleNodeSelectionAsync($event)"
v-on:nodeRevertToggled="handleNodeRevertToggleAsync($event)"
>
</SelectableTree> </SelectableTree>
</span> </span>
<span v-else>Nooo 😢</span> <span v-else>Nooo 😢</span>
@@ -25,6 +27,7 @@
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser'; import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId } from './ScriptNodeParser';
import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue'; import SelectableTree, { FilterPredicate } from './SelectableTree/SelectableTree.vue';
import { INode } from './SelectableTree/INode'; import { INode } from './SelectableTree/INode';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
@Component({ @Component({
components: { components: {
@@ -50,13 +53,13 @@
await this.initializeNodesAsync(this.categoryId); await this.initializeNodesAsync(this.categoryId);
} }
public async checkNodeAsync(node: INode) { public async toggleNodeSelectionAsync(node: INode) {
if (node.children != null && node.children.length > 0) { if (node.children != null && node.children.length > 0) {
return; // only interested in script nodes return; // only interested in script nodes
} }
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
if (!this.selectedNodeIds.some((id) => id === node.id)) { if (!this.selectedNodeIds.some((id) => id === node.id)) {
state.selection.addSelectedScript(node.id); state.selection.addSelectedScript(node.id, false);
} else { } else {
state.selection.removeSelectedScript(node.id); state.selection.removeSelectedScript(node.id);
} }
@@ -71,7 +74,7 @@
this.nodes = parseAllCategories(state.app); this.nodes = parseAllCategories(state.app);
} }
this.selectedNodeIds = state.selection.selectedScripts this.selectedNodeIds = state.selection.selectedScripts
.map((script) => getScriptNodeId(script)); .map((selected) => getScriptNodeId(selected.script));
} }
public filterPredicate(node: INode): boolean { public filterPredicate(node: INode): boolean {
@@ -81,7 +84,7 @@
(category: ICategory) => node.id === getCategoryNodeId(category)); (category: ICategory) => node.id === getCategoryNodeId(category));
} }
private handleSelectionChanged(selectedScripts: ReadonlyArray<IScript>): void { private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
this.selectedNodeIds = selectedScripts this.selectedNodeIds = selectedScripts
.map((node) => node.id); .map((node) => node.id);
} }

View File

@@ -0,0 +1,46 @@
<template>
<div class="documentationUrls">
<a v-for="url of this.documentationUrls"
v-bind:key="url"
:href="url"
:alt="url"
target="_blank" class="documentationUrl"
v-tooltip.top-center="url"
v-on:click.stop>
<font-awesome-icon :icon="['fas', 'info-circle']" />
</a>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
@Component
export default class DocumentationUrls extends Vue {
@Prop() public documentationUrls: string[];
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
.documentationUrls {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
.documentationUrl {
display: flex;
color: $gray;
cursor: pointer;
vertical-align: middle;
&:hover {
color: $slate;
}
&:not(:first-child) {
margin-left: 0.1em;
}
}
</style>

View File

@@ -1,6 +1,7 @@
export interface INode { export interface INode {
readonly id: string; readonly id: string;
readonly text: string; readonly text: string;
readonly isReversible: boolean;
readonly documentationUrls: ReadonlyArray<string>; readonly documentationUrls: ReadonlyArray<string>;
readonly children?: ReadonlyArray<INode>; readonly children?: ReadonlyArray<INode>;
} }

View File

@@ -1,17 +1,14 @@
<template> <template>
<div id="node"> <div id="node">
<div>{{ this.data.text }}</div> <div class="item text">{{ this.data.text }}</div>
<div <RevertToggle
v-for="url of this.data.documentationUrls" class="item"
v-bind:key="url"> v-if="data.isReversible"
<a :href="url" :scriptId="data.id" />
:alt="url" <DocumentationUrls
target="_blank" class="docs" class="item"
v-tooltip.top-center="url" v-if="data.documentationUrls && data.documentationUrls.length > 0"
v-on:click.stop> :documentationUrls="this.data.documentationUrls" />
<font-awesome-icon :icon="['fas', 'info-circle']" />
</a>
</div>
</div> </div>
</template> </template>
@@ -19,8 +16,15 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { INode } from './INode'; import { INode } from './INode';
import RevertToggle from './RevertToggle.vue';
import DocumentationUrls from './DocumentationUrls.vue';
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */ /** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
@Component @Component({
components: {
RevertToggle,
DocumentationUrls,
},
})
export default class Node extends Vue { export default class Node extends Vue {
@Prop() public data: INode; @Prop() public data: INode;
} }
@@ -30,17 +34,15 @@
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/presentation/styles/colors.scss"; @import "@/presentation/styles/colors.scss";
#node { #node {
display:flex; display:flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
.docs { .text {
color: $gray; display: flex;
cursor: pointer; align-items: center;
margin-left:5px; }
&:hover { .item:not(:first-child) {
color: $slate; margin-left: 5px;
} }
}
} }
</style> </style>

View File

@@ -12,6 +12,7 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0) children: (!liquorTreeNode.children || liquorTreeNode.children.length === 0)
? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)), ? [] : liquorTreeNode.children.map((childNode) => convertExistingToNode(childNode)),
documentationUrls: liquorTreeNode.data.documentationUrls, documentationUrls: liquorTreeNode.data.documentationUrls,
isReversible : liquorTreeNode.data.isReversible,
}; };
} }
@@ -27,6 +28,7 @@ export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
node.children.map((childNode) => toNewLiquorTreeNode(childNode)), node.children.map((childNode) => toNewLiquorTreeNode(childNode)),
data: { data: {
documentationUrls: node.documentationUrls, documentationUrls: node.documentationUrls,
isReversible: node.isReversible,
}, },
}; };
} }

View File

@@ -0,0 +1,141 @@
<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

@@ -8,7 +8,7 @@
ref="treeElement" ref="treeElement"
> >
<span class="tree-text" slot-scope="{ node }"> <span class="tree-text" slot-scope="{ node }">
<Node :data="convertExistingToNode(node)"/> <Node :data="convertExistingToNode(node)" />
</span> </span>
</tree> </tree>
</span> </span>
@@ -144,6 +144,7 @@
text: oldNode.data.text, text: oldNode.data.text,
data: { data: {
documentationUrls: oldNode.data.documentationUrls, documentationUrls: oldNode.data.documentationUrls,
isReversible: oldNode.data.isReversible,
}, },
children: oldNode.children == null ? [] : children: oldNode.children == null ? [] :
updateCheckedState(oldNode.children, selectedNodeIds), updateCheckedState(oldNode.children, selectedNodeIds),
@@ -154,9 +155,3 @@
return result; return result;
} }
</script> </script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
</style>

View File

@@ -1,14 +1,18 @@
<template> <template>
<span <span
v-bind:class="{ 'disabled': enabled, 'enabled': !enabled}" v-bind:class="{ 'disabled': enabled, 'enabled': !enabled}"
v-non-collapsing
@click="onClicked()">{{label}}</span> @click="onClicked()">{{label}}</span>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Emit } from 'vue-property-decorator'; import { Component, Prop, Vue, Emit } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
@Component @Component({
directives: { NonCollapsing },
})
export default class SelectableOption extends StatefulVue { export default class SelectableOption extends StatefulVue {
@Prop() public enabled: boolean; @Prop() public enabled: boolean;
@Prop() public label: string; @Prop() public label: string;

View File

@@ -32,7 +32,8 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue'; import { StatefulVue } from '@/presentation/StatefulVue';
import SelectableOption from './SelectableOption.vue'; import SelectableOption from './SelectableOption.vue';
import { IApplicationState } from '@/application/State/IApplicationState'; import { IApplicationState } from '@/application/State/IApplicationState';
import { IScript } from '@/domain/Script'; import { IScript } from '@/domain/IScript';
import { SelectedScript } from '../../../application/State/Selection/SelectedScript';
@Component({ @Component({
components: { components: {
@@ -79,12 +80,14 @@ export default class TheSelector extends StatefulVue {
private updateSelections(state: IApplicationState) { private updateSelections(state: IApplicationState) {
this.isNoneSelected = state.selection.totalSelected === 0; this.isNoneSelected = state.selection.totalSelected === 0;
this.isAllSelected = state.selection.totalSelected === state.app.totalScripts; this.isAllSelected = state.selection.totalSelected === state.app.totalScripts;
this.isRecommendedSelected = this.areSame(state.app.getRecommendedScripts(), state.selection.selectedScripts); this.isRecommendedSelected = this.areAllRecommended(state.app.getRecommendedScripts(),
state.selection.selectedScripts);
} }
private areSame(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<IScript>): boolean { private areAllRecommended(scripts: ReadonlyArray<IScript>, other: ReadonlyArray<SelectedScript>): boolean {
other = other.filter((selected) => !(selected).revert);
return (scripts.length === other.length) && return (scripts.length === other.length) &&
scripts.every((script) => other.some((s) => s.id === script.id)); scripts.every((script) => other.some((selected) => selected.id === script.id));
} }
} }
</script> </script>

View File

@@ -1,6 +1,6 @@
import { ApplicationState, IApplicationState } from '../application/State/ApplicationState'; import { ApplicationState } from '@/application/State/ApplicationState';
import { IApplicationState } from '@/application/State/IApplicationState';
import { Vue } from 'vue-property-decorator'; import { Vue } from 'vue-property-decorator';
export { IApplicationState };
export abstract class StatefulVue extends Vue { export abstract class StatefulVue extends Vue {
public isLoading = true; public isLoading = true;

View File

@@ -4,7 +4,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch, Vue } from 'vue-property-decorator'; import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
import { StatefulVue, IApplicationState } from './StatefulVue'; import { StatefulVue } from './StatefulVue';
import ace from 'ace-builds'; import ace from 'ace-builds';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import { CodeBuilder } from '../application/State/Code/CodeBuilder'; import { CodeBuilder } from '../application/State/Code/CodeBuilder';
@@ -13,16 +13,16 @@ const NothingChosenCode =
new CodeBuilder() new CodeBuilder()
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows') .appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows')
.appendLine() .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('-- 🧐 Why privacy.sexy')
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other softwares on it.') .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(' ✔️ 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(' ✔️ Have full visibility into what the tweaks do as you enable them.')
.appendCommentLine(' ✔️ Free software, 100% transparency: both application & infrastructure code are open-sourced.') .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(); .toString();
@Component @Component

View File

@@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue, IApplicationState } from './StatefulVue'; import { StatefulVue } from './StatefulVue';
import { SaveFileDialog } from './../infrastructure/SaveFileDialog'; import { SaveFileDialog } from './../infrastructure/SaveFileDialog';
import { Clipboard } from './../infrastructure/Clipboard'; import { Clipboard } from './../infrastructure/Clipboard';
import IconButton from './IconButton.vue'; import IconButton from './IconButton.vue';

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, Prop, 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, Vue, 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}.dmg`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}-mac.zip`;
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 toto 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, Prop, Vue } 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,159 @@
<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, Prop, Vue } 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';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
@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>

View File

@@ -1,10 +1,7 @@
<template> <template>
<div id="container"> <div id="container">
<h1 class="child title" >{{ title }}</h1> <h1 class="child title" >{{ title }}</h1>
<h2 class="child subtitle">{{ subtitle }}</h2> <h2 class="child subtitle">Enforce privacy & security on Windows</h2>
<a :href="repositoryUrl" target="_blank" class="child github" >
<font-awesome-icon :icon="['fab', 'github']" size="3x" />
</a>
</div> </div>
</template> </template>
@@ -16,13 +13,10 @@ import { StatefulVue } from './StatefulVue';
export default class TheHeader extends StatefulVue { export default class TheHeader extends StatefulVue {
public title = ''; public title = '';
public subtitle = ''; public subtitle = '';
public repositoryUrl = '';
public async mounted() { public async mounted() {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
this.title = state.app.name; this.title = state.app.name;
this.subtitle = 'Enforce privacy & security on Windows';
this.repositoryUrl = state.app.repositoryUrl;
} }
} }
</script> </script>
@@ -56,13 +50,6 @@ export default class TheHeader extends StatefulVue {
font-weight: 500; font-weight: 500;
line-height: 1.2; line-height: 1.2;
} }
.github {
color:inherit;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
} }
</style> </style>

View File

@@ -1,70 +0,0 @@
<template>
<div class="privacy-policy">
<div class="line">
<div class="line__emoji">🚫🍪</div>
<div>No cookies!</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>Website is deployed automatically from master branch
of the <a :href="repositoryUrl" target="_blank">source code</a> with no changes.</div>
</div>
<div 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.</div>
</div>
<div class="line">
<div class="line__emoji">🎉</div>
<div>As almost no data is colected, the website 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, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
@Component
export default class TheFooter extends StatefulVue {
private repositoryUrl: string = '';
private feedbackUrl: string = '';
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

@@ -1,6 +1,7 @@
<template> <template>
<div class="search"> <div class="search" v-non-collapsing>
<input type="search" class="searchTerm" placeholder="Search" <input type="search" class="searchTerm"
:placeholder="searchPlaceHolder"
@input="updateFilterAsync($event.target.value)" > @input="updateFilterAsync($event.target.value)" >
<div class="iconWrapper"> <div class="iconWrapper">
<font-awesome-icon :icon="['fas', 'search']" /> <font-awesome-icon :icon="['fas', 'search']" />
@@ -11,9 +12,22 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue'; import { StatefulVue } from './StatefulVue';
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
@Component @Component( {
directives: { NonCollapsing },
},
)
export default class TheSearchBar extends StatefulVue { export default class TheSearchBar extends StatefulVue {
public searchPlaceHolder = 'Search';
public async mounted() {
const state = await this.getCurrentStateAsync();
const totalScripts = state.app.totalScripts;
const totalCategories = state.app.totalCategories;
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
}
public async updateFilterAsync(filter: |string) { public async updateFilterAsync(filter: |string) {
const state = await this.getCurrentStateAsync(); const state = await this.getCurrentStateAsync();
if (!filter) { if (!filter) {

View File

@@ -1,8 +1,8 @@
$white: #fff; $white: #fff;
$light-gray: #eceef1; $light-gray: #eceef1;
$gray: darken(#eceef1, 30%); $gray: darken($light-gray, 30%);
$dark-gray: #616f86; $dark-gray: #616f86;
$slate: darken(#eceef1, 70%); $slate: darken($light-gray, 70%);
$dark-slate: #2f3133; $dark-slate: #2f3133;
$accent: #1abc9c; $accent: #1abc9c;
$black: #000 $black: #000

View File

@@ -0,0 +1,3 @@
$big-screen-width: 992px;
$medium-screen-width: 768px;
$small-screen-width: 380px;

View File

@@ -0,0 +1,28 @@
import { expect } from 'chai';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
import { BrowserOsDetector } from '@/application/Environment/BrowserOs/BrowserOsDetector';
import { BrowserOsTestCases } from './BrowserOsTestCases';
describe('BrowserOsDetector', () => {
it('unkown when user agent is undefined', () => {
// arrange
const sut = new BrowserOsDetector();
// act
const actual = sut.detect(undefined);
// assert
expect(actual).to.equal(OperatingSystem.Unknown);
});
it('detects as expected', () => {
for (const testCase of BrowserOsTestCases) {
// arrange
const sut = new BrowserOsDetector();
// act
const actual = sut.detect(testCase.userAgent);
// assert
expect(actual).to.equal(testCase.expectedOs,
`Expected: "${OperatingSystem[testCase.expectedOs]}"\n` +
`Actual: "${OperatingSystem[actual]}"\n` +
`UserAgent: "${testCase.userAgent}"`);
}
});
});

View File

@@ -0,0 +1,337 @@
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
interface IBrowserOsTestCase {
userAgent: string;
expectedOs: OperatingSystem;
}
export const BrowserOsTestCases: ReadonlyArray<IBrowserOsTestCase> = [
{
userAgent: 'Mozilla/5.0 (Windows NT 6.3; Win64, x64; Trident/7.0; rv:11.0) like Gecko',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Win64; x64; Trident/6.0)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; WebView/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.82 Safari/537.36 Edge/14.14316',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows Phone 10.0; Android 5.1.1; NOKIA; Lumia 1520) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/13.10586',
expectedOs: OperatingSystem.WindowsPhone,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (X11; CrOS x86_64 11316.165.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.122 Safari/537.36',
expectedOs: OperatingSystem.ChromeOS,
},
{
userAgent: 'Mozilla/5.0 (X11; CrOS x86_64 8872.76.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.105 Safari/537.36',
expectedOs: OperatingSystem.ChromeOS,
},
{
userAgent: 'Mozilla/5.0 (X11; CrOS armv7l 4537.56.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.38 Safari/537.36',
expectedOs: OperatingSystem.ChromeOS,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 OPR/58.0.3135.114',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36 OPR/53.0.2907.68',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2393.94 Safari/537.36 OPR/42.0.2393.94',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.82 Safari/537.36 OPR/29.0.1795.41 (Edition beta)',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 OPR/15.0.1147.100',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Opera/9.80 (Windows NT 6.0; U; en) Presto/2.2.15 Version/10.10',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Opera/9.27 (Windows NT 5.1; U; en)',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
expectedOs: OperatingSystem.iOS,
},
{
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
expectedOs: OperatingSystem.iOS,
},
{
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_0 like Mac OS X) AppleWebKit/602.1.38 (KHTML, like Gecko) Version/10.0 Mobile/14A300 Safari/602.1',
expectedOs: OperatingSystem.iOS,
},
{
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
expectedOs: OperatingSystem.iOS,
},
{
userAgent: 'Opera/9.80 (Android; Opera Mini/32.0/88.150; U; sr) Presto/2.12 Version/12.16',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Opera/9.80 (Android; Opera Mini/8.0.1807/36.1609; U; en) Presto/2.12.423 Version/12.16',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.4.4; pt-br; SM-G530BT Build/KTU84P) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; Q40; Android/4.4.2; Release/12.15.2015) AppleWebKit/534.30 (KHTML, like Gecko) Mobile Safari/534.30',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.1.0.1429 Mobile Safari/537.10+',
expectedOs: OperatingSystem.BlackBerry,
},
{
userAgent: 'Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.0.0; en-US) AppleWebKit/535.8+ (KHTML, like Gecko) Version/7.2.0.0 Safari/535.8+',
expectedOs: OperatingSystem.BlackBerryTabletOS,
},
{
userAgent: 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.466 Mobile Safari/534.8+',
expectedOs: OperatingSystem.BlackBerryOS,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 4.4.4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.52 Safari/537.36 Mobile OPR/15.0.1147.100',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 2.3.4; MT11i Build/4.0.2.A.0.62) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.123 Mobile Safari/537.22 OPR/14.0.1025.52315',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Opera/9.80 (Windows NT 6.1; Opera Tablet/15165; U; en) Presto/2.8.149 Version/11.1',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Opera/9.80 (Android 2.2; Opera Mobi/-2118645896; U; pl) Presto/2.7.60 Version/10.5',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 9; SM-G960U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 6.0; CAM-L03) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/46.0.2490.76 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Android 9; Mobile; rv:64.0) Gecko/64.0 Firefox/64.0',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4',
expectedOs: OperatingSystem.iOS,
},
{
userAgent: 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 625) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537',
expectedOs: OperatingSystem.WindowsPhone,
},
{
userAgent: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 520)',
expectedOs: OperatingSystem.WindowsPhone,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 6.0; en-US; CPH1609 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.10.2.1164 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'UCWEB/2.0 (Linux; U; Adr 5.1; en-US; Lenovo Z90a40 Build/LMY47O) U2/1.0.0 UCBrowser/11.1.5.890 U2/1.0.0 Mobile',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 5.1; en-US; Lenovo Z90a40 Build/LMY47O) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 UCBrowser/11.1.5.890 U3/0.8.0 Mobile Safari/534.30',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'UCWEB/2.0 (Linux; U; Adr 2.3; en-US; MI-ONEPlus) U2/1.0.0 UCBrowser/8.6.0.199 U2/1.0.0 Mobile',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 2.3; zh-CN; MI-ONEPlus) AppleWebKit/534.13 (KHTML, like Gecko) UCBrowser/8.6.0.199 U3/0.8.0 Mobile Safari/534.13',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 9; SAMSUNG SM-G965F Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/9.0 Chrome/67.0.3396.87 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 8.0.0; SAMSUNG SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/8.2 Chrome/63.0.3239.111 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 7.0; SAMSUNG SM-J330FN Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/7.2 Chrome/59.0.3071.125 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-G925F Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 5.0.2; SAMSUNG SM-G925F Build/LRX22G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; vivo X21A Build/OPM1.171019.011) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 MQQBrowser/9.1 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; GT-I9500 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.0 QQ-URL-Manager Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Linux; Android 9; ONEPLUS A6003) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36',
expectedOs: OperatingSystem.Android,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (Windows NT 6.4; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0',
expectedOs: OperatingSystem.Windows,
},
{
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1',
expectedOs: OperatingSystem.iOS,
},
{
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',
expectedOs: OperatingSystem.macOS,
},
{
userAgent: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0',
expectedOs: OperatingSystem.Linux,
},
{
userAgent: 'Mozilla/5.0 (X11; CrOS x86_64 11316.165.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.122 Safari/537.36',
expectedOs: OperatingSystem.ChromeOS,
},
{
userAgent: 'Mozilla/5.0 (Mobile; LYF/F90M/LYF_F90M_000-03-12-110119; Android; rv:48.0) Gecko/48.0 Firefox/48.0 KAIOS/2.5',
expectedOs: OperatingSystem.KaiOS,
},
];

View File

@@ -0,0 +1,38 @@
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
interface IDesktopTestCase {
processPlatform: string;
expectedOs: OperatingSystem;
}
// https://nodejs.org/api/process.html#process_process_platform
export const DesktopOsTestCases: ReadonlyArray<IDesktopTestCase> = [
{
processPlatform: 'aix',
expectedOs: OperatingSystem.Unknown,
},
{
processPlatform: 'darwin',
expectedOs: OperatingSystem.macOS,
},
{
processPlatform: 'freebsd',
expectedOs: OperatingSystem.Unknown,
},
{
processPlatform: 'linux',
expectedOs: OperatingSystem.Linux,
},
{
processPlatform: 'openbsd',
expectedOs: OperatingSystem.Unknown,
},
{
processPlatform: 'sunos',
expectedOs: OperatingSystem.Unknown,
},
{
processPlatform: 'win32',
expectedOs: OperatingSystem.Windows,
},
];

View File

@@ -0,0 +1,103 @@
import { IBrowserOsDetector } from '@/application/Environment/BrowserOs/IBrowserOsDetector';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
import { DesktopOsTestCases } from './DesktopOsTestCases';
import { Environment } from '@/application/Environment/Environment';
import { expect } from 'chai';
interface EnvironmentVariables {
window?: any;
process?: any;
navigator?: any;
}
class SystemUnderTest extends Environment {
constructor(variables: EnvironmentVariables, browserOsDetector?: IBrowserOsDetector) {
super(variables as any, browserOsDetector);
}
}
describe('Environment', () => {
describe('isDesktop', () => {
it('returns true if process type is renderer', () => {
// arrange
const window = {
process: {
type: 'renderer',
},
};
// act
const sut = new SystemUnderTest({ window });
// assert
expect(sut.isDesktop).to.equal(true);
});
it('returns true if electron is defined as process version', () => {
// arrange
const process = {
versions: {
electron: true,
},
};
// act
const sut = new SystemUnderTest({ process });
// assert
expect(sut.isDesktop).to.equal(true);
});
it('returns true if navigator user agent has electron', () => {
// arrange
const navigator = {
userAgent: 'Electron',
};
// act
const sut = new SystemUnderTest( { navigator });
// assert
expect(sut.isDesktop).to.equal(true);
});
it('returns false as default', () => {
const sut = new SystemUnderTest({ });
expect(sut.isDesktop).to.equal(false);
});
});
describe('os', () => {
describe('browser os from BrowserOsDetector', () => {
// arrange
const givenUserAgent = 'testUserAgent';
const expected = OperatingSystem.macOS;
const window = {
navigator: {
userAgent: givenUserAgent,
},
};
const mock: IBrowserOsDetector = {
detect: (agent) => {
if (agent !== givenUserAgent) {
throw new Error('Unexpected user agent');
}
return expected;
},
};
// act
const sut = new SystemUnderTest({ window }, mock);
const actual = sut.os;
// assert
expect(actual).to.equal(expected);
});
describe('desktop os', () => {
const navigator = {
userAgent: 'Electron',
};
for (const testCase of DesktopOsTestCases) {
// arrange
const process = {
platform: testCase.processPlatform,
};
// act
const sut = new SystemUnderTest({ navigator, process });
// assert
expect(sut.os).to.equal(testCase.expectedOs,
`Expected: "${OperatingSystem[testCase.expectedOs]}"\n` +
`Actual: "${OperatingSystem[sut.os]}"\n` +
`Platform: "${testCase.processPlatform}"`);
}
});
});
});

View File

@@ -0,0 +1,115 @@
import { IEntity } from '@/infrastructure/Entity/IEntity';
import applicationFile, { YamlCategory, YamlScript, ApplicationYaml } from 'js-yaml-loader!@/application/application.yaml';
import { parseApplication } from '@/application/Parser/ApplicationParser';
import 'mocha';
import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser';
declare var process;
describe('ApplicationParser', () => {
describe('parseApplication', () => {
it('can parse current application file', () => {
expect(() => parseApplication(applicationFile)).to.not.throw();
});
it('throws when undefined', () => {
expect(() => parseApplication(undefined)).to.throw('application is null or undefined');
});
it('throws when undefined actions', () => {
const sut: ApplicationYaml = {
name: 'test',
repositoryUrl: 'https://privacy.sexy',
actions: undefined,
};
expect(() => parseApplication(sut)).to.throw('application does not define any action');
});
it('throws when has no actions', () => {
const sut: ApplicationYaml = {
name: 'test',
repositoryUrl: 'https://privacy.sexy',
actions: [],
};
expect(() => parseApplication(sut)).to.throw('application does not define any action');
});
it('returns expected name', () => {
// arrange
const expected = 'test-app-name';
const sut: ApplicationYaml = {
name: expected,
repositoryUrl: 'https://privacy.sexy',
actions: [ getTestCategory() ],
};
// act
const actual = parseApplication(sut).name;
// assert
expect(actual).to.be.equal(actual);
});
it('returns expected repository url', () => {
// arrange
const expected = 'https://privacy.sexy';
const sut: ApplicationYaml = {
name: 'name',
repositoryUrl: expected,
actions: [ getTestCategory() ],
};
// act
const actual = parseApplication(sut).repositoryUrl;
// assert
expect(actual).to.be.equal(actual);
});
it('returns expected repository version', () => {
// arrange
const expected = '1.0.0';
process = {
env: {
VUE_APP_VERSION: expected,
},
};
const sut: ApplicationYaml = {
name: 'name',
repositoryUrl: 'https://privacy.sexy',
actions: [ getTestCategory() ],
};
// act
const actual = parseApplication(sut).version;
// assert
expect(actual).to.be.equal(actual);
});
it('parses actions', () => {
// arrange
const actions = [ getTestCategory('test1'), getTestCategory('test2') ];
const expected = [ parseCategory(actions[0]), parseCategory(actions[1]) ];
const sut: ApplicationYaml = {
name: 'name',
repositoryUrl: 'https://privacy.sexy',
actions,
};
// act
const actual = parseApplication(sut).actions;
// assert
expect(excludingId(actual)).to.be.deep.equal(excludingId(expected));
function excludingId<TId>(array: ReadonlyArray<IEntity<TId>>) {
return array.map((obj) => {
const { ['id']: omitted, ...rest } = obj;
return rest;
});
}
});
});
});
function getTestCategory(scriptName = 'testScript'): YamlCategory {
return {
category: 'category name',
children: [ getTestScript(scriptName) ],
};
}
function getTestScript(scriptName: string): YamlScript {
return {
name: scriptName,
code: 'script code',
revertCode: 'revert code',
recommend: true,
};
}

View File

@@ -0,0 +1,109 @@
import 'mocha';
import { expect } from 'chai';
import { parseCategory } from '@/application/Parser/CategoryParser';
import { YamlCategory, CategoryOrScript, YamlScript } from 'js-yaml-loader!./application.yaml';
import { parseScript } from '@/application/Parser/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
describe('CategoryParser', () => {
describe('parseCategory', () => {
it('throws when undefined', () => {
expect(() => parseCategory(undefined)).to.throw('category is null or undefined');
});
it('throws when children is empty', () => {
const category: YamlCategory = {
category: 'test',
children: [],
};
expect(() => parseCategory(category)).to.throw('category has no children');
});
it('throws when children is undefined', () => {
const category: YamlCategory = {
category: 'test',
children: undefined,
};
expect(() => parseCategory(category)).to.throw('category has no children');
});
it('throws when name is empty', () => {
const category: YamlCategory = {
category: '',
children: getTestChildren(),
};
expect(() => parseCategory(category)).to.throw('category has no name');
});
it('throws when name is undefined', () => {
const category: YamlCategory = {
category: undefined,
children: getTestChildren(),
};
expect(() => parseCategory(category)).to.throw('category has no name');
});
it('returns expected docs', () => {
// arrange
const url = 'https://privacy.sexy';
const expected = parseDocUrls({ docs: url });
const category: YamlCategory = {
category: 'category name',
children: getTestChildren(),
docs: url,
};
// act
const actual = parseCategory(category).documentationUrls;
// assert
expect(actual).to.deep.equal(expected);
});
it('returns expected scripts', () => {
// arrange
const script = getTestScript();
const expected = [ parseScript(script) ];
const category: YamlCategory = {
category: 'category name',
children: [ script ],
};
// act
const actual = parseCategory(category).scripts;
// assert
expect(actual).to.deep.equal(expected);
});
it('returns expected subcategories', () => {
// arrange
const expected: YamlCategory[] = [ {
category: 'test category',
children: [ getTestScript() ],
}];
const category: YamlCategory = {
category: 'category name',
children: expected,
};
// act
const actual = parseCategory(category).subCategories;
// assert
expect(actual).to.have.lengthOf(1);
expect(actual[0].name).to.equal(expected[0].category);
expect(actual[0].scripts.length).to.equal(expected[0].children.length);
});
});
});
function getTestChildren(): ReadonlyArray<CategoryOrScript> {
return [
getTestScript(),
];
}
function getTestScript(): YamlScript {
return {
name: 'script name',
code: 'script code',
revertCode: 'revert code',
recommend: true,
};
}

View File

@@ -0,0 +1,39 @@
import 'mocha';
import { expect } from 'chai';
import { YamlDocumentable } from 'js-yaml-loader!./application.yaml';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
describe('DocumentationParser', () => {
describe('parseDocUrls', () => {
it('throws when undefined', () => {
expect(() => parseDocUrls(undefined)).to.throw('documentable is null or undefined');
});
it('returns empty when empty', () => {
// arrange
const empty: YamlDocumentable = { };
// act
const actual = parseDocUrls(empty);
// assert
expect(actual).to.have.lengthOf(0);
});
it('returns single item when string', () => {
// arrange
const url = 'https://privacy.sexy';
const expected = [ url ];
const sut: YamlDocumentable = { docs: url };
// act
const actual = parseDocUrls(sut);
// assert
expect(actual).to.deep.equal(expected);
});
it('returns all when array', () => {
// arrange
const expected = [ 'https://privacy.sexy', 'https://github.com/undergroundwires/privacy.sexy' ];
const sut: YamlDocumentable = { docs: expected };
// act
const actual = parseDocUrls(sut);
// assert
expect(actual).to.deep.equal(expected);
});
});
});

View File

@@ -0,0 +1,28 @@
import { YamlScript } from 'js-yaml-loader!./application.yaml';
import 'mocha';
import { expect } from 'chai';
import { parseScript } from '@/application/Parser/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
describe('ScriptParser', () => {
describe('parseScript', () => {
it('parseScript parses as expected', () => {
// arrange
const expected: YamlScript = {
name: 'expected name',
code: 'expected code',
revertCode: 'expected revert code',
docs: ['hello.com'],
recommend: true,
};
// act
const actual = parseScript(expected);
// assert
expect(actual.name).to.equal(expected.name);
expect(actual.code).to.equal(expected.code);
expect(actual.revertCode).to.equal(expected.revertCode);
expect(actual.documentationUrls).to.deep.equal(parseDocUrls(expected));
expect(actual.isRecommended).to.equal(expected.recommend);
});
});
});

View File

@@ -0,0 +1,65 @@
import { CategoryStub } from './../../../stubs/CategoryStub';
import { ScriptStub } from './../../../stubs/ScriptStub';
import { ApplicationStub } from './../../../stubs/ApplicationStub';
import { UserSelection } from '@/application/State/Selection/UserSelection';
import { ApplicationCode } from '@/application/State/Code/ApplicationCode';
import 'mocha';
import { expect } from 'chai';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
describe('ApplicationCode', () => {
describe('ctor', () => {
it('empty when selection is empty', () => {
// arrange
const selection = new UserSelection(new ApplicationStub(), []);
const sut = new ApplicationCode(selection, 'version');
// act
const actual = sut.current;
// assert
expect(actual).to.have.lengthOf(0);
});
it('has code when selection is not empty', () => {
// arrange
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
const selection = new UserSelection(app, scripts);
const version = 'version-string';
const sut = new ApplicationCode(selection, version);
// act
const actual = sut.current;
// assert
expect(actual).to.have.length.greaterThan(0).and.include(version);
});
});
describe('user selection changes', () => {
it('empty when selection is empty', () => {
// arrange
let signaled: string;
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
const selection = new UserSelection(app, scripts);
const sut = new ApplicationCode(selection, 'version');
sut.changed.on((code) => signaled = code);
// act
selection.changed.notify([]);
// assert
expect(signaled).to.have.lengthOf(0);
expect(signaled).to.equal(sut.current);
});
it('has code when selection is not empty', () => {
// arrange
let signaled: string;
const scripts = [new ScriptStub('first'), new ScriptStub('second')];
const app = new ApplicationStub().withAction(new CategoryStub(1).withScripts(...scripts));
const selection = new UserSelection(app, scripts);
const version = 'version-string';
const sut = new ApplicationCode(selection, version);
sut.changed.on((code) => signaled = code);
// act
selection.changed.notify(scripts.map((s) => new SelectedScript(s, false)));
// assert
expect(signaled).to.have.length.greaterThan(0).and.include(version);
expect(signaled).to.equal(sut.current);
});
});
});

View File

@@ -0,0 +1,54 @@
import { ScriptStub } from './../../../stubs/ScriptStub';
import { UserScriptGenerator, adminRightsScript } from '@/application/State/Code/UserScriptGenerator';
import 'mocha';
import { expect } from 'chai';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
describe('UserScriptGenerator', () => {
it('adds version', () => {
const sut = new UserScriptGenerator();
// arrange
const version = '1.5.0';
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
// act
const actual = sut.buildCode(selectedScripts, version);
// assert
expect(actual).to.include(version);
});
it('adds admin rights function', () => {
const sut = new UserScriptGenerator();
// arrange
const selectedScripts = [ new SelectedScript(new ScriptStub('id'), false)];
// act
const actual = sut.buildCode(selectedScripts, 'non-important-version');
// assert
expect(actual).to.include(adminRightsScript.code);
expect(actual).to.include(adminRightsScript.name);
});
it('appends revert script', () => {
const sut = new UserScriptGenerator();
// arrange
const scriptName = 'test non-revert script';
const scriptCode = 'REM nop';
const script = new ScriptStub('id').withName(scriptName).withRevertCode(scriptCode);
const selectedScripts = [ new SelectedScript(script, true)];
// act
const actual = sut.buildCode(selectedScripts, 'non-important-version');
// assert
expect(actual).to.include(`${scriptName} (revert)`);
expect(actual).to.include(scriptCode);
});
it('appends non-revert script', () => {
const sut = new UserScriptGenerator();
// arrange
const scriptName = 'test non-revert script';
const scriptCode = 'REM nop';
const script = new ScriptStub('id').withName(scriptName).withCode(scriptCode);
const selectedScripts = [ new SelectedScript(script, false)];
// act
const actual = sut.buildCode(selectedScripts, 'non-important-version');
// assert
expect(actual).to.include(scriptName);
expect(actual).to.include(scriptCode);
});
});

View File

@@ -0,0 +1,46 @@
import { CategoryStub } from './../../../stubs/CategoryStub';
import { ScriptStub } from './../../../stubs/ScriptStub';
import { FilterResult } from '@/application/State/Filter/FilterResult';
import 'mocha';
import { expect } from 'chai';
describe('FilterResult', () => {
describe('hasAnyMatches', () => {
it('false when no matches', () => {
const sut = new FilterResult(
/* scriptMatches */ [],
/* categoryMatches */ [],
'query',
);
const actual = sut.hasAnyMatches();
expect(actual).to.equal(false);
});
it('true when script matches', () => {
const sut = new FilterResult(
/* scriptMatches */ [ new ScriptStub('id') ],
/* categoryMatches */ [],
'query',
);
const actual = sut.hasAnyMatches();
expect(actual).to.equal(true);
});
it('true when category matches', () => {
const sut = new FilterResult(
/* scriptMatches */ [ ],
/* categoryMatches */ [ new CategoryStub(5) ],
'query',
);
const actual = sut.hasAnyMatches();
expect(actual).to.equal(true);
});
it('true when script + category matches', () => {
const sut = new FilterResult(
/* scriptMatches */ [ new ScriptStub('id') ],
/* categoryMatches */ [ new CategoryStub(5) ],
'query',
);
const actual = sut.hasAnyMatches();
expect(actual).to.equal(true);
});
});
});

View File

@@ -0,0 +1,135 @@
import { CategoryStub } from './../../../stubs/CategoryStub';
import { ScriptStub } from './../../../stubs/ScriptStub';
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
import { ApplicationStub } from './../../../stubs/ApplicationStub';
import { UserFilter } from '@/application/State/Filter/UserFilter';
import 'mocha';
import { expect } from 'chai';
describe('UserFilter', () => {
it('signals when removing filter', () => {
// arrange
let isCalled = false;
const sut = new UserFilter(new ApplicationStub());
sut.filterRemoved.on(() => isCalled = true);
// act
sut.removeFilter();
// assert
expect(isCalled).to.be.equal(true);
});
it('signals when no matches', () => {
// arrange
let actual: IFilterResult;
const nonMatchingFilter = 'non matching filter';
const sut = new UserFilter(new ApplicationStub());
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(nonMatchingFilter);
// assert
expect(actual.hasAnyMatches()).be.equal(false);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(0);
expect(actual.query).to.equal(nonMatchingFilter);
});
describe('signals when script matches', () => {
it('code matches', () => {
// arrange
const code = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withCode(code);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
});
it('revertCode matches', () => {
// arrange
const revertCode = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withRevertCode(revertCode);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
});
it('name matches', () => {
// arrange
const name = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('id').withName(name);
const category = new CategoryStub(33).withScript(script);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(0);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
});
});
it('signals when category matches', () => {
// arrange
const categoryName = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const category = new CategoryStub(55).withName(categoryName);
const sut = new UserFilter(new ApplicationStub()
.withAction(category));
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(1);
expect(actual.categoryMatches[0]).to.deep.equal(category);
expect(actual.scriptMatches).to.have.lengthOf(0);
expect(actual.query).to.equal(filter);
});
it('signals when category and script matches', () => {
// arrange
const matchingText = 'HELLO world';
const filter = 'Hello WoRLD';
let actual: IFilterResult;
const script = new ScriptStub('script')
.withName(matchingText);
const category = new CategoryStub(55)
.withName(matchingText)
.withScript(script);
const app = new ApplicationStub()
.withAction(category);
const sut = new UserFilter(app);
sut.filtered.on((filterResult) => actual = filterResult);
// act
sut.setFilter(filter);
// assert
expect(actual.hasAnyMatches()).be.equal(true);
expect(actual.categoryMatches).to.have.lengthOf(1);
expect(actual.categoryMatches[0]).to.deep.equal(category);
expect(actual.scriptMatches).to.have.lengthOf(1);
expect(actual.scriptMatches[0]).to.deep.equal(script);
expect(actual.query).to.equal(filter);
});
});

View File

@@ -0,0 +1,28 @@
import { ScriptStub } from './../../../stubs/ScriptStub';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import 'mocha';
import { expect } from 'chai';
describe('SelectedScript', () => {
it('id is same as script id', () => {
// arrange
const expectedId = 'scriptId';
const script = new ScriptStub(expectedId);
const sut = new SelectedScript(script, false);
// act
const actualId = sut.id;
// assert
expect(actualId).to.equal(expectedId);
});
it('throws when revert is true for irreversible script', () => {
// arrange
const expectedId = 'scriptId';
const script = new ScriptStub(expectedId)
.withRevertCode(undefined);
// act
function construct() { new SelectedScript(script, true); } // tslint:disable-line:no-unused-expression
// assert
expect(construct).to.throw('cannot revert an irreversible script');
});
});

View File

@@ -0,0 +1,96 @@
import { ScriptStub } from './../../../stubs/ScriptStub';
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
import { CategoryStub } from '../../../stubs/CategoryStub';
import { ApplicationStub } from '../../../stubs/ApplicationStub';
import { UserSelection } from '@/application/State/Selection/UserSelection';
import 'mocha';
import { expect } from 'chai';
import { IScript } from '@/domain/IScript';
describe('UserSelection', () => {
it('deselectAll removes all items', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const app = new ApplicationStub()
.withAction(new CategoryStub(1)
.withScriptIds('s1', 's2', 's3', 's4'));
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
const sut = new UserSelection(app, selectedScripts);
sut.changed.on((newScripts) => events.push(newScripts));
// act
sut.deselectAll();
// assert
expect(sut.selectedScripts).to.have.length(0);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.have.length(0);
});
it('selectOnly selects expected', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const app = new ApplicationStub()
.withAction(new CategoryStub(1)
.withScriptIds('s1', 's2', 's3', 's4'));
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
const sut = new UserSelection(app, selectedScripts);
sut.changed.on((newScripts) => events.push(newScripts));
const scripts = [new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
const expected = scripts.map((script) => new SelectedScript(script, false));
// act
sut.selectOnly(scripts);
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
});
it('selectAll selects as expected', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const scripts: IScript[] = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
const app = new ApplicationStub()
.withAction(new CategoryStub(1)
.withScripts(...scripts));
const sut = new UserSelection(app, []);
sut.changed.on((newScripts) => events.push(newScripts));
const expected = scripts.map((script) => new SelectedScript(script, false));
// act
sut.selectAll();
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
});
describe('addOrUpdateSelectedScript', () => {
it('adds when item does not exist', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const app = new ApplicationStub()
.withAction(new CategoryStub(1)
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
const sut = new UserSelection(app, []);
sut.changed.on((scripts) => events.push(scripts));
const expected = [ new SelectedScript(new ScriptStub('s1'), false) ];
// act
sut.addOrUpdateSelectedScript('s1', false);
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
});
it('updates when item exists', () => {
// arrange
const events: Array<readonly SelectedScript[]> = [];
const app = new ApplicationStub()
.withAction(new CategoryStub(1)
.withScripts(new ScriptStub('s1'), new ScriptStub('s2')));
const sut = new UserSelection(app, []);
sut.changed.on((scripts) => events.push(scripts));
const expected = [ new SelectedScript(new ScriptStub('s1'), true) ];
// act
sut.addOrUpdateSelectedScript('s1', true);
// assert
expect(sut.selectedScripts).to.deep.equal(expected);
expect(events).to.have.lengthOf(1);
expect(events[0]).to.deep.equal(expected);
});
});
});

View File

@@ -1,41 +0,0 @@
import { CategoryStub } from './../stubs/CategoryStub';
import { ApplicationStub } from './../stubs/ApplicationStub';
import { ScriptStub } from './../stubs/ScriptStub';
import { UserSelection } from '@/application/State/Selection/UserSelection';
import 'mocha';
import { expect } from 'chai';
describe('UserSelection', () => {
it('deselectAll removes all items', async () => {
// arrange
const app = new ApplicationStub()
.withCategory(new CategoryStub(1)
.withScriptIds('s1', 's2', 's3', 's4'));
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
const sut = new UserSelection(app, selectedScripts);
// act
sut.deselectAll();
const actual = sut.selectedScripts;
// assert
expect(actual, JSON.stringify(sut.selectedScripts)).to.have.length(0);
});
it('selectOnly selects expected', async () => {
// arrange
const app = new ApplicationStub()
.withCategory(new CategoryStub(1)
.withScriptIds('s1', 's2', 's3', 's4'));
const selectedScripts = [new ScriptStub('s1'), new ScriptStub('s2'), new ScriptStub('s3')];
const sut = new UserSelection(app, selectedScripts);
const expected = [new ScriptStub('s2'), new ScriptStub('s3'), new ScriptStub('s4')];
// act
sut.selectOnly(expected);
const actual = sut.selectedScripts;
// assert
expect(actual).to.deep.equal(expected);
});
});

View File

@@ -59,4 +59,28 @@ describe('Application', () => {
// assert // assert
expect(construct).to.throw('Application must consist of at least one recommended script'); expect(construct).to.throw('Application must consist of at least one recommended script');
}); });
it('totalScripts counts right', () => {
// arrange
const categories = [
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
];
// act
const application = new Application('name', 'repo', '0.1.0', categories);
// assert
expect(application.totalScripts).to.equal(4);
});
it('totalCategories counts right', () => {
// arrange
const categories = [
new CategoryStub(1).withScripts(new ScriptStub('S1').withIsRecommended(true)),
new CategoryStub(2).withScripts(new ScriptStub('S2'), new ScriptStub('S3')),
new CategoryStub(3).withCategories(new CategoryStub(4).withScripts(new ScriptStub('S4'))),
];
// act
const application = new Application('name', 'repo', '0.1.0', categories);
// assert
expect(application.totalCategories).to.equal(4);
});
}); });

View File

@@ -3,15 +3,44 @@ import { expect } from 'chai';
import { Script } from '@/domain/Script'; import { Script } from '@/domain/Script';
describe('Script', () => { describe('Script', () => {
describe('ctor', () => {
it('cannot construct with duplicate lines', () => { describe('code', () => {
// arrange it('cannot construct with duplicate lines', () => {
const code = 'duplicate\nduplicate\ntest\nduplicate'; const code = 'duplicate\nduplicate\ntest\nduplicate';
expect(() => createWithCode(code)).to.throw();
// act });
function construct() { return new Script('ScriptName', code, [], true); } it('cannot construct with empty lines', () => {
const code = 'duplicate\n\n\ntest\nduplicate';
// assert expect(() => createWithCode(code)).to.throw();
expect(construct).to.throw(); });
});
describe('revertCode', () => {
it('cannot construct with duplicate lines', () => {
const code = 'duplicate\nduplicate\ntest\nduplicate';
expect(() => createWithCode('REM', code)).to.throw();
});
it('cannot construct with empty lines', () => {
const code = 'duplicate\n\n\ntest\nduplicate';
expect(() => createWithCode('REM', code)).to.throw();
});
it('cannot construct with when same as code', () => {
const code = 'REM';
expect(() => createWithCode(code, code)).to.throw();
});
});
describe('canRevert', () => {
it('returns false without revert code', () => {
const sut = createWithCode('code');
expect(sut.canRevert()).to.equal(false);
});
it('returns true with revert code', () => {
const sut = createWithCode('code', 'non empty revert code');
expect(sut.canRevert()).to.equal(true);
});
});
}); });
}); });
function createWithCode(code: string, revertCode?: string): Script {
return new Script('name', code, revertCode, [], false);
}

View File

@@ -8,15 +8,15 @@ describe('InMemoryRepository', () => {
[new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3)]); [new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3)]);
describe('item exists', () => { describe('item exists', () => {
const actual = sut.exists(new NumericEntityStub(1)); const actual = sut.exists(1);
it('returns true', () => expect(actual).to.be.true); it('returns true', () => expect(actual).to.be.true);
}); });
describe('item does not exist', () => { describe('item does not exist', () => {
const actual = sut.exists(new NumericEntityStub(99)); const actual = sut.exists(99);
it('returns false', () => expect(actual).to.be.false); it('returns false', () => expect(actual).to.be.false);
}); });
}); });
it('can get', () => { it('getItems gets initial items', () => {
// arrange // arrange
const expected = [ const expected = [
new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3), new NumericEntityStub(4)]; new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3), new NumericEntityStub(4)];
@@ -28,7 +28,7 @@ describe('InMemoryRepository', () => {
// assert // assert
expect(actual).to.deep.equal(expected); expect(actual).to.deep.equal(expected);
}); });
it('can add', () => { it('addItem adds', () => {
// arrange // arrange
const sut = new InMemoryRepository<number, NumericEntityStub>(); const sut = new InMemoryRepository<number, NumericEntityStub>();
const expected = { const expected = {
@@ -47,7 +47,7 @@ describe('InMemoryRepository', () => {
expect(actual.length).to.equal(expected.length); expect(actual.length).to.equal(expected.length);
expect(actual.item).to.deep.equal(expected.item); expect(actual.item).to.deep.equal(expected.item);
}); });
it('can remove', () => { it('removeItem removes', () => {
// arrange // arrange
const initialItems = [ const initialItems = [
new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3), new NumericEntityStub(4)]; new NumericEntityStub(1), new NumericEntityStub(2), new NumericEntityStub(3), new NumericEntityStub(4)];
@@ -69,4 +69,30 @@ describe('InMemoryRepository', () => {
expect(actual.length).to.equal(expected.length); expect(actual.length).to.equal(expected.length);
expect(actual.items).to.deep.equal(expected.items); expect(actual.items).to.deep.equal(expected.items);
}); });
describe('addOrUpdateItem', () => {
it('adds when item does not exist', () => {
// arrange
const initialItems = [ new NumericEntityStub(1), new NumericEntityStub(2) ];
const newItem = new NumericEntityStub(3);
const expected = [ ...initialItems, newItem ];
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
// act
sut.addOrUpdateItem(newItem);
// assert
const actual = sut.getItems();
expect(actual).to.deep.equal(expected);
});
it('updates when item exists', () => {
// arrange
const initialItems = [ new NumericEntityStub(1).withCustomProperty('bca') ];
const updatedItem = new NumericEntityStub(1).withCustomProperty('abc');
const expected = [ updatedItem ];
const sut = new InMemoryRepository<number, NumericEntityStub>(initialItems);
// act
sut.addOrUpdateItem(updatedItem);
// assert
const actual = sut.getItems();
expect(actual).to.deep.equal(expected);
});
});
}); });

View File

@@ -1,7 +1,7 @@
import { Signal } from '@/infrastructure/Events/Signal'; import { Signal } from '@/infrastructure/Events/Signal';
import { expect } from 'chai'; import { expect } from 'chai';
describe('Signal Tests', () => { describe('Signal', () => {
class ReceiverMock { class ReceiverMock {
public onRecieveCalls = new Array<number>(); public onRecieveCalls = new Array<number>();
public onReceive(arg: number): void { this.onRecieveCalls.push(arg); } public onReceive(arg: number): void { this.onRecieveCalls.push(arg); }
@@ -35,16 +35,20 @@ describe('Signal Tests', () => {
beforeEach(() => { beforeEach(() => {
receivers = [ receivers = [
new ReceiverMock(), new ReceiverMock(), new ReceiverMock(), new ReceiverMock(),
new ReceiverMock(), new ReceiverMock()]; new ReceiverMock(), new ReceiverMock()];
for (const receiver of receivers) { function subscribeReceiver(receiver: ReceiverMock) {
signal.on((arg) => receiver.onReceive(arg)); signal.on((arg) => receiver.onReceive(arg));
}}); }
for (const receiver of receivers) {
subscribeReceiver(receiver);
}
});
it('notify() should execute all callbacks', () => { it('notify() should execute all callbacks', () => {
signal.notify(5); signal.notify(5);
receivers.every((receiver) => { receivers.forEach((receiver) => {
expect(receiver.onRecieveCalls).to.have.length(1); expect(receiver.onRecieveCalls).to.have.length(1);
}); });
}); });
@@ -52,7 +56,7 @@ describe('Signal Tests', () => {
it('notify() should execute all callbacks with payload', () => { it('notify() should execute all callbacks with payload', () => {
const expected = 5; const expected = 5;
signal.notify(expected); signal.notify(expected);
receivers.every((receiver) => { receivers.forEach((receiver) => {
expect(receiver.onRecieveCalls).to.deep.equal([expected]); expect(receiver.onRecieveCalls).to.deep.equal([expected]);
}); });
}); });

View File

@@ -1,15 +1,15 @@
import { IApplication, ICategory, IScript } from '@/domain/IApplication'; import { IApplication, ICategory, IScript } from '@/domain/IApplication';
export class ApplicationStub implements IApplication { export class ApplicationStub implements IApplication {
public readonly totalScripts = 0; public totalScripts = 0;
public readonly totalCategories = 0; public totalCategories = 0;
public readonly name = 'StubApplication'; public readonly name = 'StubApplication';
public readonly repositoryUrl = 'https://privacy.sexy'; public readonly repositoryUrl = 'https://privacy.sexy';
public readonly version = '0.1.0'; public readonly version = '0.1.0';
public readonly categories = new Array<ICategory>(); public readonly actions = new Array<ICategory>();
public withCategory(category: ICategory): IApplication { public withAction(category: ICategory): IApplication {
this.categories.push(category); this.actions.push(category);
return this; return this;
} }
public findCategory(categoryId: number): ICategory { public findCategory(categoryId: number): ICategory {
@@ -19,12 +19,51 @@ export class ApplicationStub implements IApplication {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
public findScript(scriptId: string): IScript { public findScript(scriptId: string): IScript {
throw new Error('Method not implemented.'); return this.getAllScripts().find((script) => scriptId === script.id);
} }
public getAllScripts(): ReadonlyArray<IScript> { public getAllScripts(): ReadonlyArray<IScript> {
throw new Error('Method not implemented.'); const scripts = [];
for (const category of this.actions) {
const categoryScripts = getScriptsRecursively(category);
scripts.push(...categoryScripts);
}
return scripts;
} }
public getAllCategories(): ReadonlyArray<ICategory> { public getAllCategories(): ReadonlyArray<ICategory> {
throw new Error('Method not implemented.'); const categories = [];
categories.push(...this.actions);
for (const category of this.actions) {
const subCategories = getSubCategoriesRecursively(category);
categories.push(...subCategories);
}
return categories;
} }
} }
function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICategory> {
const subCategories = [];
if (category.subCategories) {
for (const subCategory of category.subCategories) {
subCategories.push(subCategory);
subCategories.push(...getSubCategoriesRecursively(subCategory));
}
}
return subCategories;
}
function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
const categoryScripts = [];
if (category.scripts) {
for (const script of category.scripts) {
categoryScripts.push(script);
}
}
if (category.subCategories) {
for (const subCategory of category.subCategories) {
const subCategoryScripts = getScriptsRecursively(subCategory);
categoryScripts.push(...subCategoryScripts);
}
}
return categoryScripts;
}

View File

@@ -1,9 +1,9 @@
import { ScriptStub } from './ScriptStub';
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { ICategory, IScript } from '@/domain/ICategory'; import { ICategory, IScript } from '@/domain/ICategory';
import { ScriptStub } from './ScriptStub';
export class CategoryStub extends BaseEntity<number> implements ICategory { export class CategoryStub extends BaseEntity<number> implements ICategory {
public readonly name = `category-with-id-${this.id}`; public name = `category-with-id-${this.id}`;
public readonly subCategories = new Array<ICategory>(); public readonly subCategories = new Array<ICategory>();
public readonly scripts = new Array<IScript>(); public readonly scripts = new Array<IScript>();
public readonly documentationUrls = new Array<string>(); public readonly documentationUrls = new Array<string>();
@@ -13,14 +13,32 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
} }
public withScriptIds(...scriptIds: string[]): CategoryStub { public withScriptIds(...scriptIds: string[]): CategoryStub {
for (const scriptId of scriptIds) { for (const scriptId of scriptIds) {
this.scripts.push(new ScriptStub(scriptId)); this.withScript(new ScriptStub(scriptId));
} }
return this; return this;
} }
public withScripts(...scripts: IScript[]): CategoryStub { public withScripts(...scripts: IScript[]): CategoryStub {
for (const script of scripts) { for (const script of scripts) {
this.scripts.push(script); this.withScript(script);
} }
return this; return this;
} }
public withCategories(...categories: ICategory[]): CategoryStub {
for (const category of categories) {
this.withCategory(category);
}
return this;
}
public withCategory(category: ICategory): CategoryStub {
this.subCategories.push(category);
return this;
}
public withScript(script: IScript): CategoryStub {
this.scripts.push(script);
return this;
}
public withName(categoryName: string) {
this.name = categoryName;
return this;
}
} }

View File

@@ -1,7 +1,12 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
export class NumericEntityStub extends BaseEntity<number> { export class NumericEntityStub extends BaseEntity<number> {
public customProperty = 'customProperty';
constructor(id: number) { constructor(id: number) {
super(id); super(id);
} }
public withCustomProperty(value: string): NumericEntityStub {
this.customProperty = value;
return this;
}
} }

View File

@@ -1,18 +1,34 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from './../../../src/domain/IScript'; import { IScript } from '@/domain/IScript';
export class ScriptStub extends BaseEntity<string> implements IScript { export class ScriptStub extends BaseEntity<string> implements IScript {
public readonly name = `name${this.id}`; public name = `name${this.id}`;
public readonly code = `name${this.id}`; public code = `REM code${this.id}`;
public revertCode = `REM revertCode${this.id}`;
public readonly documentationUrls = new Array<string>(); public readonly documentationUrls = new Array<string>();
public isRecommended = false; public isRecommended = true;
constructor(public readonly id: string) { constructor(public readonly id: string) {
super(id); super(id);
} }
public canRevert(): boolean {
return Boolean(this.revertCode);
}
public withIsRecommended(value: boolean): ScriptStub { public withIsRecommended(value: boolean): ScriptStub {
this.isRecommended = value; this.isRecommended = value;
return this; return this;
} }
public withCode(value: string): ScriptStub {
this.code = value;
return this;
}
public withName(name: string): ScriptStub {
this.name = name;
return this;
}
public withRevertCode(revertCode: string): ScriptStub {
this.revertCode = revertCode;
return this;
}
} }

View File

@@ -1 +1,22 @@
process.env.VUE_APP_VERSION = require('./package.json').version; process.env.VUE_APP_VERSION = require('./package.json').version;
module.exports = {
pluginOptions: {
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/guide.html#native-modules
electronBuilder: {
// https://www.electron.build/configuration/configuration
builderOptions: {
win: {
icon: './public/favicon.ico'
},
publish: [{
// https://www.electron.build/configuration/publish#githuboptions
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#enable-publishing-to-github
provider: 'github',
vPrefixedTagName: false, // default: true
releaseType: 'release' // or "draft" (default), "prerelease"
}]
}
}
}
}