Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45816a2bcc | ||
|
|
60a5a2aa40 | ||
|
|
04b9b59e14 | ||
|
|
4ff4b52202 | ||
|
|
73c426844a | ||
|
|
25ce236a77 | ||
|
|
9b20175545 | ||
|
|
92a7118d1c | ||
|
|
a9f9e90443 | ||
|
|
31d2067f07 | ||
|
|
dd7e1416b4 | ||
|
|
1d5225de07 | ||
|
|
9c063d59de | ||
|
|
57028987f1 | ||
|
|
9e722ddfb3 | ||
|
|
646a8e0b9f | ||
|
|
f27a2871d7 | ||
|
|
909c44d72a | ||
|
|
53cf595e17 |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist_electron
|
||||||
|
.vs
|
||||||
|
.vscode
|
||||||
|
.github
|
||||||
|
.git
|
||||||
|
docs
|
||||||
|
docker
|
||||||
7
.github/workflows/bump-and-release.yaml
vendored
7
.github/workflows/bump-and-release.yaml
vendored
@@ -10,9 +10,8 @@ jobs:
|
|||||||
if: github.event.base_ref == 'refs/heads/master'
|
if: github.event.base_ref == 'refs/heads/master'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
- uses: undergroundwires/bump-everywhere@master
|
||||||
uses: undergroundwires/bump-everywhere@master
|
|
||||||
with:
|
with:
|
||||||
user: undergroundwires-bot
|
user: undergroundwires-bot
|
||||||
release-token: ${{secrets.BUMP_GITHUB_PAT}} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
|
release-token: ${{ secrets.BUMP_GITHUB_PAT }} # Does not trigger release pipeline if we use default token: https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559
|
||||||
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.
|
# GitHub does not inject secrets if pipeline runs from fork or a fork is merged to main repo.
|
||||||
|
|||||||
32
.github/workflows/deploy-desktop.yaml
vendored
Normal file
32
.github/workflows/deploy-desktop.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Deploy desktop
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created] # will be triggered when a NON-draft release is created and published.
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-desktop-app:
|
||||||
|
name: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: master # otherwise it defaults to the version tag missing bump commit
|
||||||
|
fetch-depth: 0 # fetch all history
|
||||||
|
- name: Checkout to bump commit
|
||||||
|
run: git checkout "$(git rev-list "${{ github.event.release.tag_name }}"..master | tail -1)"
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Run tests
|
||||||
|
run: npm run test:unit
|
||||||
|
- name: Publish desktop app
|
||||||
|
run: npm run electron:build -- -p always # https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#upload-release-to-github
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
117
.github/workflows/deploy-site.yaml
vendored
Normal file
117
.github/workflows/deploy-site.yaml
vendored
Normal 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 }}
|
||||||
32
.github/workflows/deploy.yaml
vendored
32
.github/workflows/deploy.yaml
vendored
@@ -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"
|
||||||
@@ -115,3 +115,31 @@ jobs:
|
|||||||
--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
|
||||||
|
|||||||
39
.github/workflows/quality-checks.yaml
vendored
39
.github/workflows/quality-checks.yaml
vendored
@@ -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
|
|
||||||
|
|||||||
4
.github/workflows/security-checks.yaml
vendored
4
.github/workflows/security-checks.yaml
vendored
@@ -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'
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/test.yaml
vendored
5
.github/workflows/test.yaml
vendored
@@ -1,9 +1,6 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on:
|
on: push
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@ node_modules
|
|||||||
/dist
|
/dist
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
|
#Electron-builder output
|
||||||
|
/dist_electron
|
||||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -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
45
CONTRIBUTING.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
- Love your input! Contributing to this project should be as easy and transparent as possible, whether it's:
|
||||||
|
- Reporting a bug
|
||||||
|
- Discussing the current state of the code
|
||||||
|
- Submitting a fix
|
||||||
|
- Proposing new features
|
||||||
|
- Becoming a maintainer
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
- [GitHub flow](https://guides.github.com/introduction/flow/index.html) is used
|
||||||
|
- Your pull requests are actively welcomed.
|
||||||
|
- The steps:
|
||||||
|
1. Fork the repo and create your branch from master.
|
||||||
|
2. If you've added code that should be tested, add tests.
|
||||||
|
3. If you've changed APIs, update the documentation.
|
||||||
|
4. Ensure the test suite passes.
|
||||||
|
5. Make sure your code lints.
|
||||||
|
6. Issue that pull request!
|
||||||
|
- 🙏 DO
|
||||||
|
- Document your changes in the pull request
|
||||||
|
- ❗ DON'T
|
||||||
|
- Do not update the versions, current version is only [set by the maintainer](./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.
|
||||||
12
Dockerfile
12
Dockerfile
@@ -1,20 +1,12 @@
|
|||||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
# Build
|
||||||
# |B|u|i|l|d| |S|t|a|g|e|
|
|
||||||
# +-+-+-+-+-+ +-+-+-+-+-+
|
|
||||||
FROM node:lts-alpine as build-stage
|
FROM node:lts-alpine as build-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
# For testing purposes, it's easy to run http-server on lts-alpine such as continuing from here:
|
|
||||||
# RUN npm install -g http-server
|
|
||||||
# EXPOSE 8080
|
|
||||||
# CMD [ "http-server", "dist" ]
|
|
||||||
|
|
||||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
# Production stage
|
||||||
# |P|r|o|d|u|c|t|i|o|n| |S|t|a|g|e|
|
|
||||||
# +-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+
|
|
||||||
FROM nginx:stable-alpine as production-stage
|
FROM nginx:stable-alpine as production-stage
|
||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
47
README.md
47
README.md
@@ -1,8 +1,8 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy
|
||||||
|
|
||||||
> Web tool to enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/issues)
|
[](./CONTRIBUTING.md)
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||||
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
[](https://codeclimate.com/github/undergroundwires/privacy.sexy/maintainability)
|
||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
@@ -12,32 +12,42 @@
|
|||||||
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
[](https://github.com/undergroundwires/privacy.sexy/actions)
|
||||||
[](https://github.com/undergroundwires/bump-everywhere)
|
[](https://github.com/undergroundwires/bump-everywhere)
|
||||||
|
|
||||||
[https://privacy.sexy](https://privacy.sexy)
|
## Get started
|
||||||
|
|
||||||
|
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||||
|
- Or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy-sexy/releases/download/0.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
BIN
docs/gitops.png
BIN
docs/gitops.png
Binary file not shown.
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 460 KiB |
5184
package-lock.json
generated
5184
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
54
src/application/Environment/BrowserOs/BrowserOsDetector.ts
Normal file
54
src/application/Environment/BrowserOs/BrowserOsDetector.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
import { DetectorBuilder } from './DetectorBuilder';
|
||||||
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
|
export class BrowserOsDetector implements IBrowserOsDetector {
|
||||||
|
private readonly detectors = BrowserDetectors;
|
||||||
|
public detect(userAgent: string): OperatingSystem {
|
||||||
|
if (!userAgent) {
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
for (const detector of this.detectors) {
|
||||||
|
const os = detector.detect(userAgent);
|
||||||
|
if (os !== OperatingSystem.Unknown) {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
|
||||||
|
const BrowserDetectors =
|
||||||
|
[
|
||||||
|
define(OperatingSystem.KaiOS, (b) =>
|
||||||
|
b.mustInclude('KAIOS')),
|
||||||
|
define(OperatingSystem.ChromeOS, (b) =>
|
||||||
|
b.mustInclude('CrOS')),
|
||||||
|
define(OperatingSystem.BlackBerryOS, (b) =>
|
||||||
|
b.mustInclude('BlackBerry')),
|
||||||
|
define(OperatingSystem.BlackBerryTabletOS, (b) =>
|
||||||
|
b.mustInclude('RIM Tablet OS')),
|
||||||
|
define(OperatingSystem.BlackBerry, (b) =>
|
||||||
|
b.mustInclude('BB10')),
|
||||||
|
define(OperatingSystem.Android, (b) =>
|
||||||
|
b.mustInclude('Android').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.Android, (b) =>
|
||||||
|
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.iOS, (b) =>
|
||||||
|
b.mustInclude('like Mac OS X')),
|
||||||
|
define(OperatingSystem.Linux, (b) =>
|
||||||
|
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
|
||||||
|
define(OperatingSystem.Windows, (b) =>
|
||||||
|
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.WindowsPhone, (b) =>
|
||||||
|
b.mustInclude('Windows Phone')),
|
||||||
|
define(OperatingSystem.macOS, (b) =>
|
||||||
|
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
|
||||||
|
];
|
||||||
|
|
||||||
|
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
|
||||||
|
const builder = new DetectorBuilder(os);
|
||||||
|
applyRules(builder);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
49
src/application/Environment/BrowserOs/DetectorBuilder.ts
Normal file
49
src/application/Environment/BrowserOs/DetectorBuilder.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { OperatingSystem } from '../OperatingSystem';
|
||||||
|
|
||||||
|
export interface IBrowserOsDetector {
|
||||||
|
detect(userAgent: string): OperatingSystem;
|
||||||
|
}
|
||||||
80
src/application/Environment/Environment.ts
Normal file
80
src/application/Environment/Environment.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
|
import { IEnvironment } from './IEnvironment';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
interface IEnvironmentVariables {
|
||||||
|
readonly window: Window & typeof globalThis;
|
||||||
|
readonly process: NodeJS.Process;
|
||||||
|
readonly navigator: Navigator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Environment implements IEnvironment {
|
||||||
|
public static readonly CurrentEnvironment: IEnvironment = new Environment({
|
||||||
|
window,
|
||||||
|
process,
|
||||||
|
navigator,
|
||||||
|
});
|
||||||
|
public readonly isDesktop: boolean;
|
||||||
|
public readonly os: OperatingSystem;
|
||||||
|
protected constructor(
|
||||||
|
variables: IEnvironmentVariables,
|
||||||
|
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
|
||||||
|
if (!variables) {
|
||||||
|
throw new Error('variables is null or empty');
|
||||||
|
}
|
||||||
|
this.isDesktop = isDesktop(variables);
|
||||||
|
this.os = this.isDesktop ?
|
||||||
|
getDesktopOsType(getProcessPlatform(variables))
|
||||||
|
: browserOsDetector.detect(getUserAgent(variables));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserAgent(variables: IEnvironmentVariables): string {
|
||||||
|
if (!variables.window || !variables.window.navigator) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return variables.window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProcessPlatform(variables: IEnvironmentVariables): string {
|
||||||
|
if (!variables.process || !variables.process.platform) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return variables.process.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDesktopOsType(processPlatform: string): OperatingSystem {
|
||||||
|
// https://nodejs.org/api/process.html#process_process_platform
|
||||||
|
if (processPlatform === 'darwin') {
|
||||||
|
return OperatingSystem.macOS;
|
||||||
|
} else if (processPlatform === 'win32') {
|
||||||
|
return OperatingSystem.Windows;
|
||||||
|
} else if (processPlatform === 'linux') {
|
||||||
|
return OperatingSystem.Linux;
|
||||||
|
}
|
||||||
|
return OperatingSystem.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDesktop(variables: IEnvironmentVariables): boolean {
|
||||||
|
// More: https://github.com/electron/electron/issues/2288
|
||||||
|
// Renderer process
|
||||||
|
if (variables.window
|
||||||
|
&& variables.window.process
|
||||||
|
&& variables.window.process.type === 'renderer') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Main process
|
||||||
|
if (variables.process
|
||||||
|
&& variables.process.versions
|
||||||
|
&& Boolean(variables.process.versions.electron)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
|
if (variables.navigator
|
||||||
|
&& variables.navigator.userAgent
|
||||||
|
&& variables.navigator.userAgent.includes('Electron')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
6
src/application/Environment/IEnvironment.ts
Normal file
6
src/application/Environment/IEnvironment.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
export interface IEnvironment {
|
||||||
|
isDesktop: boolean;
|
||||||
|
os: OperatingSystem;
|
||||||
|
}
|
||||||
14
src/application/Environment/OperatingSystem.ts
Normal file
14
src/application/Environment/OperatingSystem.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export enum OperatingSystem {
|
||||||
|
macOS,
|
||||||
|
Windows,
|
||||||
|
Linux,
|
||||||
|
KaiOS,
|
||||||
|
ChromeOS,
|
||||||
|
BlackBerryOS,
|
||||||
|
BlackBerry,
|
||||||
|
BlackBerryTabletOS,
|
||||||
|
Android,
|
||||||
|
iOS,
|
||||||
|
WindowsPhone,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
@@ -1,21 +1,29 @@
|
|||||||
import { Category } from '../../domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { Application } from '../../domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import applicationFile from 'js-yaml-loader!./../application.yaml';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
|
|
||||||
export function parseApplication(): Application {
|
export function parseApplication(content: ApplicationYaml): IApplication {
|
||||||
|
validate(content);
|
||||||
const categories = new Array<Category>();
|
const categories = new Array<Category>();
|
||||||
if (!applicationFile.actions || applicationFile.actions.length <= 0) {
|
for (const action of content.actions) {
|
||||||
throw new Error('Application does not define any action');
|
|
||||||
}
|
|
||||||
for (const action of applicationFile.actions) {
|
|
||||||
const category = parseCategory(action);
|
const category = parseCategory(action);
|
||||||
categories.push(category);
|
categories.push(category);
|
||||||
}
|
}
|
||||||
const app = new Application(
|
const app = new Application(
|
||||||
applicationFile.name,
|
content.name,
|
||||||
applicationFile.repositoryUrl,
|
content.repositoryUrl,
|
||||||
process.env.VUE_APP_VERSION,
|
process.env.VUE_APP_VERSION,
|
||||||
categories);
|
categories);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validate(content: ApplicationYaml): void {
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('application is null or undefined');
|
||||||
|
}
|
||||||
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
|
throw new Error('application does not define any action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '../../domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
import { parseScript } from './ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter: number = 0;
|
||||||
|
|
||||||
|
|
||||||
interface ICategoryChildren {
|
interface ICategoryChildren {
|
||||||
subCategories: Category[];
|
subCategories: Category[];
|
||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCategory(category: YamlCategory): Category {
|
export function parseCategory(category: YamlCategory): Category {
|
||||||
if (!category.children || category.children.length <= 0) {
|
ensureValid(category);
|
||||||
throw Error('Category has no children');
|
|
||||||
}
|
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
@@ -31,6 +29,18 @@ export function parseCategory(category: YamlCategory): Category {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureValid(category: YamlCategory) {
|
||||||
|
if (!category) {
|
||||||
|
throw Error('category is null or undefined');
|
||||||
|
}
|
||||||
|
if (!category.children || category.children.length === 0) {
|
||||||
|
throw Error('category has no children');
|
||||||
|
}
|
||||||
|
if (!category.category || category.category.length === 0) {
|
||||||
|
throw Error('category has no name');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
||||||
if (isCategory(categoryOrScript)) {
|
if (isCategory(categoryOrScript)) {
|
||||||
@@ -38,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 [];
|
||||||
|
|||||||
16
src/application/Parser/ScriptParser.ts
Normal file
16
src/application/Parser/ScriptParser.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Script } from '@/domain/Script';
|
||||||
|
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||||
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
|
|
||||||
|
export function parseScript(yamlScript: YamlScript): Script {
|
||||||
|
if (!yamlScript) {
|
||||||
|
throw new Error('script is null or undefined');
|
||||||
|
}
|
||||||
|
const script = new Script(
|
||||||
|
/* name */ yamlScript.name,
|
||||||
|
/* code */ yamlScript.code,
|
||||||
|
/* revertCode */ yamlScript.revertCode,
|
||||||
|
/* docs */ parseDocUrls(yamlScript),
|
||||||
|
/* isRecommended */ yamlScript.recommend);
|
||||||
|
return script;
|
||||||
|
}
|
||||||
@@ -3,13 +3,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 };
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/application/State/Code/IUserScriptGenerator.ts
Normal file
5
src/application/State/Code/IUserScriptGenerator.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||||
|
|
||||||
|
export interface IUserScriptGenerator {
|
||||||
|
buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): string;
|
||||||
|
}
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
14
src/application/State/Selection/SelectedScript.ts
Normal file
14
src/application/State/Selection/SelectedScript.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
export class SelectedScript extends BaseEntity<string> {
|
||||||
|
constructor(
|
||||||
|
public readonly script: IScript,
|
||||||
|
public readonly revert: boolean,
|
||||||
|
) {
|
||||||
|
super(script.id);
|
||||||
|
if (revert && !script.canRevert()) {
|
||||||
|
throw new Error('cannot revert an irreversible script');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -868,6 +881,19 @@ actions:
|
|||||||
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:
|
||||||
@@ -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
|
||||||
|
|||||||
5
src/application/application.yaml.d.ts
vendored
5
src/application/application.yaml.d.ts
vendored
@@ -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
133
src/background.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { app, protocol, BrowserWindow, shell } from 'electron';
|
||||||
|
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||||
|
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
|
||||||
|
import path from 'path';
|
||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
|
||||||
|
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||||
|
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
|
||||||
|
|
||||||
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
let win: BrowserWindow | null;
|
||||||
|
|
||||||
|
// Scheme must be registered before the app is ready
|
||||||
|
protocol.registerSchemesAsPrivileged([
|
||||||
|
{ scheme: 'app', privileges: { secure: true, standard: true } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
|
autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging
|
||||||
|
log.transports.file.level = 'silly';
|
||||||
|
if (!process.env.IS_TEST) {
|
||||||
|
Object.assign(console, log.functions); // override console.log, console.warn etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
win = new BrowserWindow({
|
||||||
|
width: 1350,
|
||||||
|
height: 955,
|
||||||
|
webPreferences: {
|
||||||
|
// Use pluginOptions.nodeIntegration, leave this alone
|
||||||
|
// See 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { IEntity } from './../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { IDocumentable } from './IDocumentable';
|
import { IDocumentable } from './IDocumentable';
|
||||||
|
|
||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly code: string;
|
|
||||||
readonly isRecommended: boolean;
|
readonly isRecommended: boolean;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,44 +2,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
1
src/global.d.ts
vendored
@@ -13,6 +13,7 @@ declare module 'liquor-tree' {
|
|||||||
}
|
}
|
||||||
interface ICustomLiquorTreeData {
|
interface ICustomLiquorTreeData {
|
||||||
documentationUrls: ReadonlyArray<string>;
|
documentationUrls: ReadonlyArray<string>;
|
||||||
|
isReversible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
17
src/presentation/Scripts/Cards/NonCollapsingDirective.ts
Normal file
17
src/presentation/Scripts/Cards/NonCollapsingDirective.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { DirectiveOptions } from 'vue';
|
||||||
|
|
||||||
|
const attributeName = 'data-interactionDoesNotCollapse';
|
||||||
|
|
||||||
|
export function hasDirective(el: Element): boolean {
|
||||||
|
if (el.hasAttribute(attributeName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const parent = el.closest(`[${attributeName}]`);
|
||||||
|
return !!parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NonCollapsing: DirectiveOptions = {
|
||||||
|
inserted(el: HTMLElement) {
|
||||||
|
el.setAttribute(attributeName, '');
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="documentationUrls">
|
||||||
|
<a v-for="url of this.documentationUrls"
|
||||||
|
v-bind:key="url"
|
||||||
|
:href="url"
|
||||||
|
:alt="url"
|
||||||
|
target="_blank" class="documentationUrl"
|
||||||
|
v-tooltip.top-center="url"
|
||||||
|
v-on:click.stop>
|
||||||
|
<font-awesome-icon :icon="['fas', 'info-circle']" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||||
|
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||||
|
@Component
|
||||||
|
export default class DocumentationUrls extends Vue {
|
||||||
|
@Prop() public documentationUrls: string[];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
.documentationUrls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.documentationUrl {
|
||||||
|
display: flex;
|
||||||
|
color: $gray;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
&:hover {
|
||||||
|
color: $slate;
|
||||||
|
}
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
|
||||||
72
src/presentation/TheFooter/DownloadUrlList.vue
Normal file
72
src/presentation/TheFooter/DownloadUrlList.vue
Normal 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>
|
||||||
94
src/presentation/TheFooter/DownloadUrlListItem.vue
Normal file
94
src/presentation/TheFooter/DownloadUrlListItem.vue
Normal 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>
|
||||||
81
src/presentation/TheFooter/PrivacyPolicy.vue
Normal file
81
src/presentation/TheFooter/PrivacyPolicy.vue
Normal 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>
|
||||||
159
src/presentation/TheFooter/TheFooter.vue
Normal file
159
src/presentation/TheFooter/TheFooter.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
3
src/presentation/styles/media.scss
Normal file
3
src/presentation/styles/media.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
$big-screen-width: 992px;
|
||||||
|
$medium-screen-width: 768px;
|
||||||
|
$small-screen-width: 380px;
|
||||||
@@ -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}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
38
tests/unit/application/Environment/DesktopOsTestCases.ts
Normal file
38
tests/unit/application/Environment/DesktopOsTestCases.ts
Normal 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,
|
||||||
|
},
|
||||||
|
];
|
||||||
103
tests/unit/application/Environment/Environment.spec.ts
Normal file
103
tests/unit/application/Environment/Environment.spec.ts
Normal 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}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
115
tests/unit/application/Parser/ApplicationParser.spec.ts
Normal file
115
tests/unit/application/Parser/ApplicationParser.spec.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
109
tests/unit/application/Parser/CategoryParser.spec.ts
Normal file
109
tests/unit/application/Parser/CategoryParser.spec.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
39
tests/unit/application/Parser/DocumentationParser.spec.ts
Normal file
39
tests/unit/application/Parser/DocumentationParser.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
28
tests/unit/application/Parser/ScriptParser.spec.ts
Normal file
28
tests/unit/application/Parser/ScriptParser.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
65
tests/unit/application/State/Code/ApplicationCode.spec.ts
Normal file
65
tests/unit/application/State/Code/ApplicationCode.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
46
tests/unit/application/State/Filter/FilterResult.spec.ts
Normal file
46
tests/unit/application/State/Filter/FilterResult.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
135
tests/unit/application/State/Filter/UserFilter.spec.ts
Normal file
135
tests/unit/application/State/Filter/UserFilter.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
96
tests/unit/application/State/Selection/UserSelection.spec.ts
Normal file
96
tests/unit/application/State/Selection/UserSelection.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user