Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c318bd301a | ||
|
|
86a2b2fda0 | ||
|
|
8a8b7319d5 | ||
|
|
2428de23ee | ||
|
|
7ec889e759 | ||
|
|
9d009c40dd | ||
|
|
663d63bde0 | ||
|
|
6b83dcbf8f | ||
|
|
72e925fb6f | ||
|
|
c299e95bc6 | ||
|
|
2e40605d59 | ||
|
|
3455a2ca6c | ||
|
|
6fe858d86a | ||
|
|
7cc161c828 | ||
|
|
e14bf2bfa0 | ||
|
|
34672414c3 | ||
|
|
f7557bcc0f | ||
|
|
e4b6cdfb18 |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.2 (2020-12-26)
|
||||
|
||||
* replace ampersand in "Movies & TV app" with "and" to prevent batch file from misinterpreting it (#45) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/52d4313156d2dcbc508b7271e7d9dfd45723d7bc)
|
||||
* update dependencies to latest #46 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d9e44e25744e5d0aa01b8fc0f0af74c48027aea3)
|
||||
* fix type assignment error after typescript upgrade | [commit](https://github.com/undergroundwires/privacy.sexy/commit/55f936fee9f86757f63fa8952d89711feb247e5b)
|
||||
* correct typos (#48) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a744415eb2ab65ee4f519f863fdd6a43953377bb)
|
||||
* in ci/cd, do not run security checks if PRs do not change dependencies #48 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/54ba4dbb0bf8f08f9479f8facb2e12c786c1bc51)
|
||||
* rename app launch tracking tweak to make it more clear #44 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b3117c27f283c2d5a25fd94021a9f628a272cda6)
|
||||
* refactor capabilities to use a shared function #41 #47 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c4ec6a1445d2fd5eb923c97b54aee01e272e13a8)
|
||||
* rename "disable" to "uninstall" for removing capabilities #47 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8cd3352017f9dc85f8efcd7b450d90f555d3e92e)
|
||||
|
||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.1...0.8.2)
|
||||
|
||||
## 0.8.1 (2020-11-16)
|
||||
|
||||
* refactor removing bloatware to use functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ffa279f3dfe51db564f0a3859543eb212170e173)
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
- 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).
|
||||
- The source of truth for the state lies in application layer ([`./src/application/`](src/application/)) and must be updated from the views if they're mutating the state
|
||||
- They mutate or/and react to state changes in [ApplicationContext](src/application/Context/ApplicationContext.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
|
||||
|
||||
26
README.md
26
README.md
@@ -1,6 +1,6 @@
|
||||
# privacy.sexy
|
||||
|
||||
> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||
|
||||
[](./CONTRIBUTING.md)
|
||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||
@@ -15,7 +15,7 @@
|
||||
## 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.8.1/privacy.sexy-Setup-0.8.1.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-0.8.1.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.1/privacy.sexy-0.8.1.dmg)
|
||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.2/privacy.sexy-Setup-0.8.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.2/privacy.sexy-0.8.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.2/privacy.sexy-0.8.2.dmg)
|
||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
||||
|
||||
[](https://privacy.sexy)
|
||||
@@ -32,9 +32,11 @@
|
||||
|
||||
## Extend scripts
|
||||
|
||||
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
||||
- 📖 If you're unsure about the syntax you can refer to the [application file | documentation](docs/application-file.md).
|
||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||
1. Fork the repository
|
||||
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
||||
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
||||
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||
3. Send a pull request 👌
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -49,8 +51,8 @@
|
||||
- Development: `npm run serve` to compile & hot-reload for development.
|
||||
- Production: `npm run build` to prepare files for distribution.
|
||||
- Or run using Docker:
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.8.1 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.8.1 undergroundwires/privacy.sexy:0.8.1`
|
||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.8.2 .`
|
||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.8.2 undergroundwires/privacy.sexy:0.8.2`
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -65,9 +67,13 @@
|
||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
||||
- **Application Layer**
|
||||
- Keeps the application state
|
||||
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
|
||||
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
|
||||
- Keeps the application state using [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
||||
- [ApplicationContext](src/application/Context/ApplicationContext.ts)
|
||||
- Holds the [CategoryCollectionState](src/application/Context/State/CategoryCollectionState.ts)] for each OS
|
||||
- Same instance is shared throughout the application
|
||||
- The scripts are defined and controlled in [yaml files](src/application/collections/) per OS
|
||||
- Uses [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming)
|
||||
- 📖 See [extend scripts](#extend-scripts) to read about how to extend them.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
# Application file
|
||||
# Collection files
|
||||
|
||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from [`application.yaml`](./../src/application/application.yaml)
|
||||
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||
- 💡 Best practices
|
||||
- If you repeat yourself, try to utilize [YAML-defined functions](#function)
|
||||
- Always try to add documentation and a way to revert a tweak in [scripts](#script)
|
||||
- 📖 Types in code: [`application.d.ts`](./../src/application/application.yaml.d.ts)
|
||||
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
||||
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
||||
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||
|
||||
## Objects
|
||||
|
||||
### `Application`
|
||||
### `Collection`
|
||||
|
||||
- Application file simply defines different categories and their scripts in a tree structure.
|
||||
- Application file also allows defining common [function](#function)s to be used throughout the application if you'd like different scripts to share same code.
|
||||
- A collection simply defines:
|
||||
- different categories and their scripts in a tree structure
|
||||
- OS specific details
|
||||
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||
|
||||
#### `Application` syntax
|
||||
#### `Collection` syntax
|
||||
|
||||
- `os:` *`string`* (**required**)
|
||||
- Operating system that the [Collection](#collection) is written for.
|
||||
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
||||
- Each [category](#category) is rendered as different cards in card presentation.
|
||||
- ❗ Application must consist of at least one category.
|
||||
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
||||
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
||||
- Defines the scripting language that the code of other action uses.
|
||||
|
||||
### `Category`
|
||||
|
||||
@@ -30,8 +37,8 @@
|
||||
|
||||
- `category:` *`string`* (**required**)
|
||||
- Name of the category
|
||||
- ❗ Must be unique throughout the application
|
||||
- `children: [` ***[`Category`](#category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
||||
- ❗ Must be unique throughout the [Collection](#collection)
|
||||
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
||||
- ❗ Category must consist of at least one subcategory or script.
|
||||
- Children can be combination of scripts and subcategories.
|
||||
|
||||
@@ -47,7 +54,7 @@
|
||||
|
||||
- `name`: *`string`* (**required**)
|
||||
- Name of the script
|
||||
- ❗ Must be unique throughout the application
|
||||
- ❗ Must be unique throughout the [Collection](#collection)
|
||||
- E.g. `Disable targeted ads`
|
||||
- `code`: *`string`* (may be **required**)
|
||||
- Batch file commands that will be executed
|
||||
@@ -79,7 +86,7 @@
|
||||
|
||||
- `function`: *`string`* (**required**)
|
||||
- Name of the function to call.
|
||||
- ❗ Function with same name must defined in `functions` property of [Application](#application)
|
||||
- ❗ Function with same name must defined in `functions` property of [Collection](#collection)
|
||||
- `parameters`: `[ parameterName:` *`parameterValue`*`, ... ]`
|
||||
- Defines key value dictionary for each parameter and its value
|
||||
- E.g.
|
||||
@@ -127,7 +134,7 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
||||
- ❗ Function names must be unique
|
||||
- `parameters`: `[` *`string`* `, ... ]`
|
||||
- Name of the parameters that the function has.
|
||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#functioncall)
|
||||
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
||||
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
||||
- ❗ Parameter names must be unique
|
||||
`code`: *`string`* (**required**)
|
||||
@@ -137,3 +144,18 @@ It would print "Hello world" if it's called in a [script](#script) as following:
|
||||
- Code that'll undo the change done by `code` property.
|
||||
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||
|
||||
### `ScriptingDefinition`
|
||||
|
||||
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
||||
|
||||
#### `ScriptingDefinition` syntax
|
||||
|
||||
- `language:` *`string`* (**required**)
|
||||
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||
- `startCode:` *`string`* (**required**)
|
||||
- Code that'll be inserted on top of user created script.
|
||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||
- `endCode:` *`string`* (**required**)
|
||||
- Code that'll be inserted at the end of user created script.
|
||||
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "privacy.sexy",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"author": "undergroundwires",
|
||||
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||
"homepage": "https://privacy.sexy",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows</title>
|
||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
|
||||
<meta name="robots" content="index,follow" />
|
||||
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Component, Vue } from 'vue-property-decorator';
|
||||
import TheHeader from '@/presentation/TheHeader.vue';
|
||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
|
||||
import TheCodeButtons from '@/presentation/CodeButtons/TheCodeButtons.vue';
|
||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
||||
|
||||
|
||||
43
src/application/Common/Enum.ts
Normal file
43
src/application/Common/Enum.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||
type EnumType = number | string;
|
||||
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
||||
|
||||
export interface IEnumParser<TEnum> {
|
||||
parseEnum(value: string, propertyName: string): TEnum;
|
||||
}
|
||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
||||
return {
|
||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||
};
|
||||
}
|
||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: string,
|
||||
enumName: string,
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
||||
if (!value) {
|
||||
throw new Error(`undefined ${enumName}`);
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||
}
|
||||
const casedValue = getEnumNames(enumVariable)
|
||||
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||
if (!casedValue) {
|
||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||
}
|
||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||
}
|
||||
|
||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
||||
return Object
|
||||
.values(enumVariable)
|
||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||
}
|
||||
|
||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
||||
return getEnumNames(enumVariable)
|
||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||
}
|
||||
71
src/application/Context/ApplicationContext.ts
Normal file
71
src/application/Context/ApplicationContext.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
|
||||
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||
|
||||
export class ApplicationContext implements IApplicationContext {
|
||||
public readonly contextChanged = new Signal<IApplicationContextChangedEvent>();
|
||||
public collection: ICategoryCollection;
|
||||
public currentOs: OperatingSystem;
|
||||
|
||||
public get state(): ICategoryCollectionState {
|
||||
return this.states[this.collection.os];
|
||||
}
|
||||
|
||||
private readonly states: StateMachine;
|
||||
public constructor(
|
||||
public readonly app: IApplication,
|
||||
initialContext: OperatingSystem) {
|
||||
validateApp(app);
|
||||
validateOs(initialContext);
|
||||
this.states = initializeStates(app);
|
||||
this.changeContext(initialContext);
|
||||
}
|
||||
|
||||
public changeContext(os: OperatingSystem): void {
|
||||
if (this.currentOs === os) {
|
||||
return;
|
||||
}
|
||||
this.collection = this.app.getCollection(os);
|
||||
if (!this.collection) {
|
||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||
}
|
||||
const event: IApplicationContextChangedEvent = {
|
||||
newState: this.states[os],
|
||||
oldState: this.states[this.currentOs],
|
||||
};
|
||||
this.contextChanged.notify(event);
|
||||
this.currentOs = os;
|
||||
}
|
||||
}
|
||||
|
||||
function validateApp(app: IApplication) {
|
||||
if (!app) {
|
||||
throw new Error('undefined app');
|
||||
}
|
||||
}
|
||||
|
||||
function validateOs(os: OperatingSystem) {
|
||||
if (os === undefined) {
|
||||
throw new Error('undefined os');
|
||||
}
|
||||
if (os === OperatingSystem.Unknown) {
|
||||
throw new Error('unknown os');
|
||||
}
|
||||
if (!(os in OperatingSystem)) {
|
||||
throw new Error(`os "${os}" is out of range`);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeStates(app: IApplication): StateMachine {
|
||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||
for (const collection of app.collections) {
|
||||
machine[collection.os] = new CategoryCollectionState(collection);
|
||||
}
|
||||
return machine;
|
||||
}
|
||||
31
src/application/Context/ApplicationContextProvider.ts
Normal file
31
src/application/Context/ApplicationContextProvider.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Environment } from '../Environment/Environment';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEnvironment } from '../Environment/IEnvironment';
|
||||
import { parseApplication } from '../Parser/ApplicationParser';
|
||||
|
||||
export type ApplicationParserType = () => IApplication;
|
||||
const ApplicationParser: ApplicationParserType = parseApplication;
|
||||
|
||||
export function buildContext(
|
||||
parser = ApplicationParser,
|
||||
environment = Environment.CurrentEnvironment): IApplicationContext {
|
||||
const app = parser();
|
||||
const os = getInitialOs(app, environment);
|
||||
return new ApplicationContext(app, os);
|
||||
}
|
||||
|
||||
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||
const currentOs = environment.os;
|
||||
const supportedOsList = app.getSupportedOsList();
|
||||
if (supportedOsList.includes(currentOs)) {
|
||||
return currentOs;
|
||||
}
|
||||
supportedOsList.sort((os1, os2) => {
|
||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||
return getPriority(os2) - getPriority(os1);
|
||||
});
|
||||
return supportedOsList[0];
|
||||
}
|
||||
16
src/application/Context/IApplicationContext.ts
Normal file
16
src/application/Context/IApplicationContext.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
export interface IApplicationContext {
|
||||
readonly app: IApplication;
|
||||
readonly state: ICategoryCollectionState;
|
||||
readonly contextChanged: ISignal<IApplicationContextChangedEvent>;
|
||||
changeContext(os: OperatingSystem): void;
|
||||
}
|
||||
|
||||
export interface IApplicationContextChangedEvent {
|
||||
readonly newState: ICategoryCollectionState;
|
||||
readonly oldState: ICategoryCollectionState;
|
||||
}
|
||||
23
src/application/Context/State/CategoryCollectionState.ts
Normal file
23
src/application/Context/State/CategoryCollectionState.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { UserFilter } from './Filter/UserFilter';
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
import { UserSelection } from './Selection/UserSelection';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||
public readonly os: OperatingSystem;
|
||||
public readonly code: IApplicationCode;
|
||||
public readonly selection: IUserSelection;
|
||||
public readonly filter: IUserFilter;
|
||||
|
||||
public constructor(readonly collection: ICategoryCollection) {
|
||||
this.selection = new UserSelection(collection, []);
|
||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||
this.filter = new UserFilter(collection);
|
||||
this.os = collection.os;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||
import { CodePosition } from './Position/CodePosition';
|
||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||
@@ -16,10 +17,10 @@ export class ApplicationCode implements IApplicationCode {
|
||||
|
||||
constructor(
|
||||
userSelection: IUserSelection,
|
||||
private readonly version: string,
|
||||
private readonly scriptingDefinition: IScriptingDefinition,
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||
if (!version) { throw new Error('version is null or undefined'); }
|
||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||
this.setCode(userSelection.selectedScripts);
|
||||
userSelection.changed.on((scripts) => {
|
||||
@@ -29,7 +30,7 @@ export class ApplicationCode implements IApplicationCode {
|
||||
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||
const code = this.generator.buildCode(scripts, this.version);
|
||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||
this.current = code.code;
|
||||
this.scriptPositions = code.scriptPositions;
|
||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
|
||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||
public readonly code: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface ICodeChangedEvent {
|
||||
readonly code: string;
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
const NewLine = '\n';
|
||||
const TotalFunctionSeparatorChars = 58;
|
||||
|
||||
export class CodeBuilder {
|
||||
export abstract class CodeBuilder implements ICodeBuilder {
|
||||
private readonly lines = new Array<string>();
|
||||
|
||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||
@@ -27,7 +29,7 @@ export class CodeBuilder {
|
||||
}
|
||||
|
||||
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||
this.lines.push(`:: ${commentLine}`);
|
||||
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -35,9 +37,8 @@ export class CodeBuilder {
|
||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||
return this
|
||||
.appendLine()
|
||||
.appendCommentLineWithHyphensAround(name)
|
||||
.appendLine(`echo --- ${name}`)
|
||||
.appendLine(this.writeStandardOut(`--- ${name}`))
|
||||
.appendLine(code)
|
||||
.appendTrailingHyphensCommentLine();
|
||||
}
|
||||
@@ -54,10 +55,13 @@ export class CodeBuilder {
|
||||
return this
|
||||
.appendTrailingHyphensCommentLine()
|
||||
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||
.appendTrailingHyphensCommentLine();
|
||||
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.lines.join(NewLine);
|
||||
}
|
||||
|
||||
protected abstract getCommentDelimiter(): string;
|
||||
protected abstract writeStandardOut(text: string): string;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||
import { BatchBuilder } from './Languages/BatchBuilder';
|
||||
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||
|
||||
export class CodeBuilderFactory implements ICodeBuilderFactory {
|
||||
public create(language: ScriptingLanguage): ICodeBuilder {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.shellscript: return new ShellBuilder();
|
||||
case ScriptingLanguage.batchfile: return new BatchBuilder();
|
||||
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface ICodeBuilder {
|
||||
currentLine: number;
|
||||
appendLine(code?: string): ICodeBuilder;
|
||||
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendFunction(name: string, code: string): ICodeBuilder;
|
||||
toString(): string;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
export interface ICodeBuilderFactory {
|
||||
create(language: ScriptingLanguage): ICodeBuilder;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface IUserScript {
|
||||
code: string;
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||
|
||||
export class BatchBuilder extends CodeBuilder {
|
||||
protected getCommentDelimiter(): string {
|
||||
return '::';
|
||||
}
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo ${text}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||
|
||||
export class ShellBuilder extends CodeBuilder {
|
||||
protected getCommentDelimiter(): string {
|
||||
return '#';
|
||||
}
|
||||
protected writeStandardOut(text: string): string {
|
||||
return `echo '${text}'`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||
|
||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||
|
||||
}
|
||||
public buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions };
|
||||
}
|
||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||
for (const selection of selectedScripts) {
|
||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||
}
|
||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||
return { code, scriptPositions };
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
||||
if (!startCode) {
|
||||
return builder;
|
||||
}
|
||||
return builder
|
||||
.appendLine(startCode)
|
||||
.appendLine();
|
||||
}
|
||||
|
||||
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||
if (!endCode) {
|
||||
return builder.toString();
|
||||
}
|
||||
return builder.appendLine()
|
||||
.appendLine(endCode)
|
||||
.toString();
|
||||
}
|
||||
|
||||
function appendSelection(
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
||||
builder = appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
builder.appendLine();
|
||||
const position = new CodePosition(startPosition, endPosition);
|
||||
scriptPositions.set(selection, position);
|
||||
return scriptPositions;
|
||||
}
|
||||
|
||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||
return builder
|
||||
.appendLine()
|
||||
.appendFunction(name, scriptCode);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ICodePosition } from './ICodePosition';
|
||||
export class CodePosition implements ICodePosition {
|
||||
|
||||
export class CodePosition implements ICodePosition {
|
||||
public get totalLines(): number {
|
||||
return this.endLine - this.startLine;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||
|
||||
export interface IUserFilter {
|
||||
readonly currentFilter: IFilterResult | undefined;
|
||||
@@ -1,16 +1,16 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { FilterResult } from './FilterResult';
|
||||
import { IFilterResult } from './IFilterResult';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IUserFilter } from './IUserFilter';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class UserFilter implements IUserFilter {
|
||||
public readonly filtered = new Signal<IFilterResult>();
|
||||
public readonly filterRemoved = new Signal<void>();
|
||||
public currentFilter: IFilterResult | undefined;
|
||||
|
||||
constructor(private application: IApplication) {
|
||||
constructor(private collection: ICategoryCollection) {
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ export class UserFilter implements IUserFilter {
|
||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
||||
}
|
||||
const filterLowercase = filter.toLocaleLowerCase();
|
||||
const filteredScripts = this.application.getAllScripts().filter(
|
||||
const filteredScripts = this.collection.getAllScripts().filter(
|
||||
(script) => isScriptAMatch(script, filterLowercase));
|
||||
const filteredCategories = this.application.getAllCategories().filter(
|
||||
const filteredCategories = this.collection.getAllCategories().filter(
|
||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||
const matches = new FilterResult(
|
||||
filteredScripts,
|
||||
13
src/application/Context/State/ICategoryCollectionState.ts
Normal file
13
src/application/Context/State/ICategoryCollectionState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface ICategoryCollectionState {
|
||||
readonly code: IApplicationCode;
|
||||
readonly filter: IUserFilter;
|
||||
readonly selection: IUserSelection;
|
||||
readonly collection: ICategoryCollection;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
|
||||
export interface IUserSelection {
|
||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||
@@ -1,17 +1,18 @@
|
||||
import { SelectedScript } from './SelectedScript';
|
||||
import { IApplication, ICategory } from '@/domain/IApplication';
|
||||
import { IUserSelection } from './IUserSelection';
|
||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class UserSelection implements IUserSelection {
|
||||
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||
private readonly scripts: IRepository<string, SelectedScript>;
|
||||
|
||||
constructor(
|
||||
private readonly app: IApplication,
|
||||
private readonly collection: ICategoryCollection,
|
||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||
if (selectedScripts && selectedScripts.length > 0) {
|
||||
@@ -40,7 +41,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public removeAllInCategory(categoryId: number): void {
|
||||
const category = this.app.findCategory(categoryId);
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||
.filter((script) => this.scripts.exists(script.id));
|
||||
if (!scriptsToRemove.length) {
|
||||
@@ -53,7 +54,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
||||
const category = this.app.findCategory(categoryId);
|
||||
const category = this.collection.findCategory(categoryId);
|
||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||
.filter((script) =>
|
||||
!this.scripts.exists(script.id)
|
||||
@@ -70,7 +71,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.app.findScript(scriptId);
|
||||
const script = this.collection.findScript(scriptId);
|
||||
if (!script) {
|
||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||
}
|
||||
@@ -80,7 +81,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||
const script = this.app.findScript(scriptId);
|
||||
const script = this.collection.findScript(scriptId);
|
||||
const selectedScript = new SelectedScript(script, revert);
|
||||
this.scripts.addOrUpdateItem(selectedScript);
|
||||
this.changed.notify(this.scripts.getItems());
|
||||
@@ -105,7 +106,7 @@ export class UserSelection implements IUserSelection {
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
for (const script of this.app.getAllScripts()) {
|
||||
for (const script of this.collection.getAllScripts()) {
|
||||
if (!this.scripts.exists(script.id)) {
|
||||
const selection = new SelectedScript(script, false);
|
||||
this.scripts.addItem(selection);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IEnvironment {
|
||||
isDesktop: boolean;
|
||||
os: OperatingSystem;
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,38 @@
|
||||
import { Category } from '@/domain/Category';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
||||
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||
import { Application } from '@/domain/Application';
|
||||
|
||||
|
||||
export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEnv = process.env): IApplication {
|
||||
validate(content);
|
||||
const compiler = new ScriptCompiler(content.functions);
|
||||
const categories = new Array<Category>();
|
||||
for (const action of content.actions) {
|
||||
const category = parseCategory(action, compiler);
|
||||
categories.push(category);
|
||||
}
|
||||
const info = readAppInformation(env);
|
||||
const app = new Application(
|
||||
info,
|
||||
categories);
|
||||
export function parseApplication(
|
||||
parser = CategoryCollectionParser,
|
||||
processEnv: NodeJS.ProcessEnv = process.env,
|
||||
collectionsData = PreParsedCollections): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const information = parseProjectInformation(processEnv);
|
||||
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||
const app = new Application(information, collections);
|
||||
return app;
|
||||
}
|
||||
|
||||
function readAppInformation(environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
);
|
||||
}
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||
|
||||
function validate(content: ApplicationYaml): void {
|
||||
if (!content) {
|
||||
throw new Error('application is null or undefined');
|
||||
const CategoryCollectionParser: CategoryCollectionParserType
|
||||
= (file, info) => parseCategoryCollection(file, info);
|
||||
|
||||
const PreParsedCollections: readonly CollectionData []
|
||||
= [ WindowsData, MacOsData ];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections.length) {
|
||||
throw new Error('no collection provided');
|
||||
}
|
||||
if (!content.actions || content.actions.length <= 0) {
|
||||
throw new Error('application does not define any action');
|
||||
if (collections.some((collection) => !collection)) {
|
||||
throw new Error('undefined collection provided');
|
||||
}
|
||||
}
|
||||
|
||||
39
src/application/Parser/CategoryCollectionParser.ts
Normal file
39
src/application/Parser/CategoryCollectionParser.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Category } from '@/domain/Category';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||
|
||||
export function parseCategoryCollection(
|
||||
content: CollectionData,
|
||||
info: IProjectInformation,
|
||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||
validate(content);
|
||||
const scripting = parseScriptingDefinition(content.scripting, info);
|
||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||
const categories = new Array<Category>();
|
||||
for (const action of content.actions) {
|
||||
const category = parseCategory(action, context);
|
||||
categories.push(category);
|
||||
}
|
||||
const os = osParser.parseEnum(content.os, 'os');
|
||||
const collection = new CategoryCollection(
|
||||
os,
|
||||
categories,
|
||||
scripting);
|
||||
return collection;
|
||||
}
|
||||
|
||||
function validate(content: CollectionData): void {
|
||||
if (!content) {
|
||||
throw new Error('content is null or undefined');
|
||||
}
|
||||
if (!content.actions || content.actions.length <= 0) {
|
||||
throw new Error('content does not define any action');
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { Category } from '@/domain/Category';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { parseScript } from './ScriptParser';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||
import { parseScript } from './Script/ScriptParser';
|
||||
|
||||
let categoryIdCounter: number = 0;
|
||||
|
||||
@@ -12,17 +12,15 @@ interface ICategoryChildren {
|
||||
subScripts: Script[];
|
||||
}
|
||||
|
||||
export function parseCategory(category: YamlCategory, compiler: IScriptCompiler): Category {
|
||||
if (!compiler) {
|
||||
throw new Error('undefined compiler');
|
||||
}
|
||||
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
ensureValid(category);
|
||||
const children: ICategoryChildren = {
|
||||
subCategories: new Array<Category>(),
|
||||
subScripts: new Array<Script>(),
|
||||
};
|
||||
for (const categoryOrScript of category.children) {
|
||||
parseCategoryChild(categoryOrScript, children, category, compiler);
|
||||
for (const data of category.children) {
|
||||
parseCategoryChild(data, children, category, context);
|
||||
}
|
||||
return new Category(
|
||||
/*id*/ categoryIdCounter++,
|
||||
@@ -33,12 +31,12 @@ export function parseCategory(category: YamlCategory, compiler: IScriptCompiler)
|
||||
);
|
||||
}
|
||||
|
||||
function ensureValid(category: YamlCategory) {
|
||||
function ensureValid(category: CategoryData) {
|
||||
if (!category) {
|
||||
throw Error('category is null or undefined');
|
||||
}
|
||||
if (!category.children || category.children.length === 0) {
|
||||
throw Error('category has no children');
|
||||
throw Error(`category has no children: "${category.category}"`);
|
||||
}
|
||||
if (!category.category || category.category.length === 0) {
|
||||
throw Error('category has no name');
|
||||
@@ -46,28 +44,28 @@ function ensureValid(category: YamlCategory) {
|
||||
}
|
||||
|
||||
function parseCategoryChild(
|
||||
categoryOrScript: any,
|
||||
data: CategoryOrScriptData,
|
||||
children: ICategoryChildren,
|
||||
parent: YamlCategory,
|
||||
compiler: IScriptCompiler) {
|
||||
if (isCategory(categoryOrScript)) {
|
||||
const subCategory = parseCategory(categoryOrScript as YamlCategory, compiler);
|
||||
parent: CategoryData,
|
||||
context: ICategoryCollectionParseContext) {
|
||||
if (isCategory(data)) {
|
||||
const subCategory = parseCategory(data as CategoryData, context);
|
||||
children.subCategories.push(subCategory);
|
||||
} else if (isScript(categoryOrScript)) {
|
||||
const yamlScript = categoryOrScript as YamlScript;
|
||||
const script = parseScript(yamlScript, compiler);
|
||||
} else if (isScript(data)) {
|
||||
const scriptData = data as ScriptData;
|
||||
const script = parseScript(scriptData, context);
|
||||
children.subScripts.push(script);
|
||||
} else {
|
||||
throw new Error(`Child element is neither a category or a script.
|
||||
Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
|
||||
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function isScript(categoryOrScript: any): boolean {
|
||||
return (categoryOrScript.code && categoryOrScript.code.length > 0)
|
||||
|| categoryOrScript.call;
|
||||
function isScript(data: any): boolean {
|
||||
return (data.code && data.code.length > 0)
|
||||
|| data.call;
|
||||
}
|
||||
|
||||
function isCategory(categoryOrScript: any): boolean {
|
||||
return categoryOrScript.category && categoryOrScript.category.length > 0;
|
||||
function isCategory(data: any): boolean {
|
||||
return data.category && data.category.length > 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
|
||||
export interface IScriptCompiler {
|
||||
canCompile(script: YamlScript): boolean;
|
||||
compile(script: YamlScript): IScriptCode;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { YamlDocumentable, DocumentationUrls } from 'js-yaml-loader!./application.yaml';
|
||||
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
||||
|
||||
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
||||
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||
if (!documentable) {
|
||||
throw new Error('documentable is null or undefined');
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<stri
|
||||
return result.getAll();
|
||||
}
|
||||
|
||||
function addDocs(docs: DocumentationUrls, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||
if (docs instanceof Array) {
|
||||
urls.addUrls(docs);
|
||||
} else if (typeof docs === 'string') {
|
||||
|
||||
12
src/application/Parser/ProjectInformationParser.ts
Normal file
12
src/application/Parser/ProjectInformationParser.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { FunctionData } from 'js-yaml-loader!*';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
||||
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||
|
||||
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||
public readonly compiler: IScriptCompiler;
|
||||
public readonly syntax: ILanguageSyntax;
|
||||
|
||||
constructor(
|
||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||
scripting: IScriptingDefinition,
|
||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
||||
if (!scripting) { throw new Error('undefined scripting'); }
|
||||
this.syntax = syntaxFactory.create(scripting.language);
|
||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||
}
|
||||
}
|
||||
73
src/application/Parser/Script/Compiler/ILCode.ts
Normal file
73
src/application/Parser/Script/Compiler/ILCode.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export interface IILCode {
|
||||
compile(): string;
|
||||
getUniqueParameterNames(): string[];
|
||||
substituteParameter(parameterName: string, parameterValue: string): IILCode;
|
||||
}
|
||||
|
||||
export function generateIlCode(rawText: string): IILCode {
|
||||
const ilCode = generateIl(rawText);
|
||||
return new ILCode(ilCode);
|
||||
}
|
||||
|
||||
class ILCode implements IILCode {
|
||||
private readonly ilCode: string;
|
||||
|
||||
constructor(ilCode: string) {
|
||||
this.ilCode = ilCode;
|
||||
}
|
||||
|
||||
public substituteParameter(parameterName: string, parameterValue: string): IILCode {
|
||||
const newCode = substituteParameter(this.ilCode, parameterName, parameterValue);
|
||||
return new ILCode(newCode);
|
||||
}
|
||||
|
||||
public getUniqueParameterNames(): string[] {
|
||||
return getUniqueParameterNames(this.ilCode);
|
||||
}
|
||||
|
||||
public compile(): string {
|
||||
ensureNoExpressionLeft(this.ilCode);
|
||||
return this.ilCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||
function generateIl(rawText: string): string {
|
||||
return rawText.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||
return `\{\{exp|${match.trim()}\}\}`;
|
||||
});
|
||||
}
|
||||
|
||||
// finds all "{{exp|..}} left"
|
||||
function ensureNoExpressionLeft(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueExpressions = getDistinctValues(allMatches);
|
||||
if (uniqueExpressions.length > 0) {
|
||||
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parses all distinct usages of {{exp|$parameterName}}
|
||||
function getUniqueParameterNames(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||
const pattern = `{{exp|$${parameterName}}}`;
|
||||
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||
}
|
||||
|
||||
function getDistinctValues(values: readonly string[]): string[] {
|
||||
return values.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||
|
||||
export interface IScriptCompiler {
|
||||
canCompile(script: ScriptData): boolean;
|
||||
compile(script: ScriptData): IScriptCode;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { YamlScript, YamlFunction, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
|
||||
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||
import { IScriptCompiler } from './IScriptCompiler';
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
interface ICompiledCode {
|
||||
readonly code: string;
|
||||
@@ -9,16 +11,19 @@ interface ICompiledCode {
|
||||
}
|
||||
|
||||
export class ScriptCompiler implements IScriptCompiler {
|
||||
constructor(private readonly functions: readonly YamlFunction[]) {
|
||||
constructor(
|
||||
private readonly functions: readonly FunctionData[] | undefined,
|
||||
private syntax: ILanguageSyntax) {
|
||||
ensureValidFunctions(functions);
|
||||
if (!syntax) { throw new Error('undefined syntax'); }
|
||||
}
|
||||
public canCompile(script: YamlScript): boolean {
|
||||
public canCompile(script: ScriptData): boolean {
|
||||
if (!script.call) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public compile(script: YamlScript): IScriptCode {
|
||||
public compile(script: ScriptData): IScriptCode {
|
||||
this.ensureCompilable(script.call);
|
||||
const compiledCodes = new Array<ICompiledCode>();
|
||||
const calls = getCallSequence(script.call);
|
||||
@@ -32,18 +37,17 @@ export class ScriptCompiler implements IScriptCompiler {
|
||||
compiledCodes.push(functionCode);
|
||||
});
|
||||
const scriptCode = merge(compiledCodes);
|
||||
return new ScriptCode(script.name, scriptCode.code, scriptCode.revertCode);
|
||||
return new ScriptCode(scriptCode.code, scriptCode.revertCode, script.name, this.syntax);
|
||||
}
|
||||
|
||||
private getFunctionByName(name: string): YamlFunction {
|
||||
private getFunctionByName(name: string): FunctionData {
|
||||
const func = this.functions.find((f) => f.name === name);
|
||||
if (!func) {
|
||||
throw new Error(`called function is not defined "${name}"`);
|
||||
}
|
||||
return func;
|
||||
}
|
||||
|
||||
private ensureCompilable(call: ScriptFunctionCall) {
|
||||
private ensureCompilable(call: ScriptFunctionCallData) {
|
||||
if (!this.functions || this.functions.length === 0) {
|
||||
throw new Error('cannot compile without shared functions');
|
||||
}
|
||||
@@ -61,15 +65,19 @@ function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly YamlFunction[]) {
|
||||
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||
const duplicateFunctionNames = getDuplicates(functions
|
||||
.map((func) => func.name.toLowerCase()));
|
||||
if (duplicateFunctionNames.length) {
|
||||
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicatesInParameterNames(functions: readonly YamlFunction[]) {
|
||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||
if (functions.some((func) => !func)) {
|
||||
throw new Error(`some functions are undefined`);
|
||||
}
|
||||
}
|
||||
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||
const functionsWithParameters = functions
|
||||
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||
for (const func of functionsWithParameters) {
|
||||
@@ -80,7 +88,7 @@ function ensureNoDuplicatesInParameterNames(functions: readonly YamlFunction[])
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicateCode(functions: readonly YamlFunction[]) {
|
||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
|
||||
if (duplicateCodes.length > 0) {
|
||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||
@@ -93,10 +101,11 @@ function ensureNoDuplicateCode(functions: readonly YamlFunction[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidFunctions(functions: readonly YamlFunction[]) {
|
||||
if (!functions) {
|
||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
if (!functions || functions.length === 0) {
|
||||
return;
|
||||
}
|
||||
ensureNoUndefinedItem(functions);
|
||||
ensureNoDuplicatesInFunctionNames(functions);
|
||||
ensureNoDuplicatesInParameterNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
@@ -117,33 +126,35 @@ function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||
};
|
||||
}
|
||||
|
||||
function compileCode(func: YamlFunction, parameters: FunctionCallParameters): ICompiledCode {
|
||||
function compileCode(func: FunctionData, parameters: FunctionCallParametersData): ICompiledCode {
|
||||
return {
|
||||
code: compileExpressions(func.code, parameters),
|
||||
revertCode: compileExpressions(func.revertCode, parameters),
|
||||
};
|
||||
}
|
||||
|
||||
function compileExpressions(code: string, parameters: FunctionCallParameters): string {
|
||||
let intermediateCode = compileToIL(code);
|
||||
function compileExpressions(code: string, parameters: FunctionCallParametersData): string {
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
ensureNoExpressionLeft(intermediateCode);
|
||||
return intermediateCode;
|
||||
return intermediateCode.compile();
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: string, parameters: FunctionCallParameters): string {
|
||||
const parameterNames = getUniqueParameterNamesFromIL(intermediateCode);
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: FunctionCallParametersData): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
if (parameterNames.length && !parameters) {
|
||||
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
||||
}
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
intermediateCode = substituteParameter(intermediateCode, parameterName, parameterValue);
|
||||
if (!parameterValue) {
|
||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||
}
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
}
|
||||
return intermediateCode;
|
||||
}
|
||||
|
||||
function ensureValidCall(call: FunctionCall, scriptName: string) {
|
||||
function ensureValidCall(call: FunctionCallData, scriptName: string) {
|
||||
if (!call) {
|
||||
throw new Error(`undefined function call in script "${scriptName}"`);
|
||||
}
|
||||
@@ -152,49 +163,9 @@ function ensureValidCall(call: FunctionCall, scriptName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function getCallSequence(call: ScriptFunctionCall): FunctionCall[] {
|
||||
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||
if (call instanceof Array) {
|
||||
return call as FunctionCall[];
|
||||
}
|
||||
return [ call as FunctionCall ];
|
||||
}
|
||||
|
||||
function getDistinctValues(values: readonly string[]): string[] {
|
||||
return values.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
|
||||
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||
function compileToIL(code: string) {
|
||||
return code.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||
return `\{\{exp|${match.trim()}\}\}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Parses all distinct usages of {{exp|$parameterName}}
|
||||
function getUniqueParameterNamesFromIL(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||
if (!parameterValue) {
|
||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||
}
|
||||
const pattern = `{{exp|$${parameterName}}}`;
|
||||
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||
}
|
||||
|
||||
// finds all "{{exp|..}} left"
|
||||
function ensureNoExpressionLeft(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueExpressions = getDistinctValues(allMatches);
|
||||
if (uniqueExpressions.length > 0) {
|
||||
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||
return call as FunctionCallData[];
|
||||
}
|
||||
return [ call as FunctionCallData ];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
|
||||
export interface ICategoryCollectionParseContext {
|
||||
readonly compiler: IScriptCompiler;
|
||||
readonly syntax: ILanguageSyntax;
|
||||
}
|
||||
54
src/application/Parser/Script/ScriptParser.ts
Normal file
54
src/application/Parser/Script/ScriptParser.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||
import { parseDocUrls } from '../DocumentationParser';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||
|
||||
export function parseScript(
|
||||
data: ScriptData, context: ICategoryCollectionParseContext,
|
||||
levelParser = createEnumParser(RecommendationLevel)): Script {
|
||||
validateScript(data);
|
||||
if (!context) { throw new Error('undefined context'); }
|
||||
const script = new Script(
|
||||
/* name */ data.name,
|
||||
/* code */ parseCode(data, context),
|
||||
/* docs */ parseDocUrls(data),
|
||||
/* level */ parseLevel(data.recommend, levelParser));
|
||||
return script;
|
||||
}
|
||||
|
||||
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
return parser.parseEnum(level, 'level');
|
||||
}
|
||||
|
||||
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
||||
if (context.compiler.canCompile(script)) {
|
||||
return context.compiler.compile(script);
|
||||
}
|
||||
return new ScriptCode(script.code, script.revertCode, script.name, context.syntax);
|
||||
}
|
||||
|
||||
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||
if (script.code && script.call) {
|
||||
throw new Error('cannot define both "call" and "code"');
|
||||
}
|
||||
if (script.revertCode && script.call) {
|
||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||
}
|
||||
}
|
||||
|
||||
function validateScript(script: ScriptData) {
|
||||
if (!script) {
|
||||
throw new Error('undefined script');
|
||||
}
|
||||
if (!script.code && !script.call) {
|
||||
throw new Error('must define either "call" or "code"');
|
||||
}
|
||||
ensureNotBothCallAndCode(script);
|
||||
}
|
||||
6
src/application/Parser/Script/Syntax/BatchFileSyntax.ts
Normal file
6
src/application/Parser/Script/Syntax/BatchFileSyntax.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
export class BatchFileSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = [ 'REM', '::' ];
|
||||
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
||||
}
|
||||
6
src/application/Parser/Script/Syntax/ISyntaxFactory.ts
Normal file
6
src/application/Parser/Script/Syntax/ISyntaxFactory.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
|
||||
export interface ISyntaxFactory {
|
||||
create(language: ScriptingLanguage): ILanguageSyntax;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
|
||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||
public readonly commentDelimiters = [ '#' ];
|
||||
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
||||
}
|
||||
15
src/application/Parser/Script/Syntax/SyntaxFactory.ts
Normal file
15
src/application/Parser/Script/Syntax/SyntaxFactory.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||
|
||||
export class SyntaxFactory implements ISyntaxFactory {
|
||||
public create(language: ScriptingLanguage): ILanguageSyntax {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile: return new BatchFileSyntax();
|
||||
case ScriptingLanguage.shellscript: return new ShellScriptSyntax();
|
||||
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
|
||||
export function parseScript(yamlScript: YamlScript, compiler: IScriptCompiler): Script {
|
||||
validateScript(yamlScript);
|
||||
if (!compiler) {
|
||||
throw new Error('undefined compiler');
|
||||
}
|
||||
const script = new Script(
|
||||
/* name */ yamlScript.name,
|
||||
/* code */ parseCode(yamlScript, compiler),
|
||||
/* docs */ parseDocUrls(yamlScript),
|
||||
/* level */ getLevel(yamlScript.recommend));
|
||||
return script;
|
||||
}
|
||||
|
||||
function getLevel(level: string): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof level !== 'string') {
|
||||
throw new Error(`level must be a string but it was ${typeof level}`);
|
||||
}
|
||||
const typedLevel = RecommendationLevelNames
|
||||
.find((l) => l.toLowerCase() === level.toLowerCase());
|
||||
if (!typedLevel) {
|
||||
throw new Error(`unknown level: \"${level}\"`);
|
||||
}
|
||||
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
|
||||
}
|
||||
|
||||
function parseCode(yamlScript: YamlScript, compiler: IScriptCompiler): IScriptCode {
|
||||
if (compiler.canCompile(yamlScript)) {
|
||||
return compiler.compile(yamlScript);
|
||||
}
|
||||
return new ScriptCode(yamlScript.name, yamlScript.code, yamlScript.revertCode);
|
||||
}
|
||||
|
||||
function ensureNotBothCallAndCode(yamlScript: YamlScript) {
|
||||
if (yamlScript.code && yamlScript.call) {
|
||||
throw new Error('cannot define both "call" and "code"');
|
||||
}
|
||||
if (yamlScript.revertCode && yamlScript.call) {
|
||||
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||
}
|
||||
}
|
||||
|
||||
function validateScript(yamlScript: YamlScript) {
|
||||
if (!yamlScript) {
|
||||
throw new Error('undefined script');
|
||||
}
|
||||
if (!yamlScript.code && !yamlScript.call) {
|
||||
throw new Error('must define either "call" or "code"');
|
||||
}
|
||||
ensureNotBothCallAndCode(yamlScript);
|
||||
}
|
||||
36
src/application/Parser/ScriptingDefinitionParser.ts
Normal file
36
src/application/Parser/ScriptingDefinitionParser.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { generateIlCode } from './Script/Compiler/ILCode';
|
||||
|
||||
export function parseScriptingDefinition(
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation,
|
||||
date = new Date(),
|
||||
languageParser = createEnumParser(ScriptingLanguage)): IScriptingDefinition {
|
||||
if (!info) {
|
||||
throw new Error('undefined info');
|
||||
}
|
||||
if (!definition) {
|
||||
throw new Error('undefined definition');
|
||||
}
|
||||
const language = languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = applySubstitutions(definition.startCode, info, date);
|
||||
const endCode = applySubstitutions(definition.endCode, info, date);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
|
||||
function applySubstitutions(code: string, info: IProjectInformation, date: Date): string {
|
||||
let ilCode = generateIlCode(code);
|
||||
ilCode = ilCode.substituteParameter('homepage', info.homepage);
|
||||
ilCode = ilCode.substituteParameter('version', info.version);
|
||||
ilCode = ilCode.substituteParameter('date', date.toUTCString());
|
||||
return ilCode.compile();
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { UserFilter } from './Filter/UserFilter';
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
import { UserSelection } from './Selection/UserSelection';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { parseApplication } from '../Parser/ApplicationParser';
|
||||
import { IApplicationState } from './IApplicationState';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
||||
export class ApplicationState implements IApplicationState {
|
||||
/** Get singleton application state */
|
||||
public static GetAsync(): Promise<IApplicationState> {
|
||||
return ApplicationState.instance.getValueAsync();
|
||||
}
|
||||
|
||||
/** Application instance with all scripts. */
|
||||
private static instance = new AsyncLazy<IApplicationState>(() => {
|
||||
const application = parseApplication(applicationFile);
|
||||
const selectedScripts = new Array<Script>();
|
||||
const state = new ApplicationState(application, selectedScripts);
|
||||
return Promise.resolve(state);
|
||||
});
|
||||
|
||||
public readonly code: IApplicationCode;
|
||||
public readonly stateChanged = new Signal<IApplicationState>();
|
||||
public readonly selection: IUserSelection;
|
||||
public readonly filter: IUserFilter;
|
||||
|
||||
private constructor(
|
||||
/** Inner instance of the all scripts */
|
||||
public readonly app: IApplication,
|
||||
/** Initially selected scripts */
|
||||
public readonly defaultScripts: Script[]) {
|
||||
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
||||
this.code = new ApplicationCode(this.selection, app.info.version);
|
||||
this.filter = new UserFilter(app);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
|
||||
export interface IUserScript {
|
||||
code: string;
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScript } from './IUserScript';
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
version: string): IUserScript;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||
import { CodeBuilder } from './CodeBuilder';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
|
||||
export const adminRightsScript = {
|
||||
name: 'Ensure admin privileges',
|
||||
code: 'fltmc >nul 2>&1 || (\n' +
|
||||
' echo Administrator privileges are required.\n' +
|
||||
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
|
||||
' echo Right-click on the script and select "Run as administrator".\n' +
|
||||
' pause & exit 1\n' +
|
||||
' )\n' +
|
||||
' exit 0\n' +
|
||||
')',
|
||||
};
|
||||
|
||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||
if (!version) { throw new Error('version is undefined'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions };
|
||||
}
|
||||
const builder = initializeCode(version);
|
||||
for (const selection of selectedScripts) {
|
||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||
}
|
||||
const code = finalizeCode(builder);
|
||||
return { code, scriptPositions };
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCode(version: string): CodeBuilder {
|
||||
return new CodeBuilder()
|
||||
.appendLine('@echo off')
|
||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
||||
.appendLine();
|
||||
}
|
||||
|
||||
function finalizeCode(builder: CodeBuilder): string {
|
||||
return builder.appendLine()
|
||||
.appendLine('pause')
|
||||
.appendLine('exit /b 0')
|
||||
.toString();
|
||||
}
|
||||
|
||||
function appendSelection(
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
const startPosition = builder.currentLine + 1;
|
||||
appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
builder.appendLine();
|
||||
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
||||
return scriptPositions;
|
||||
}
|
||||
|
||||
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||
builder.appendFunction(name, scriptCode);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { IApplication } from './../../domain/IApplication';
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
export { IUserSelection, IApplicationCode, IUserFilter };
|
||||
|
||||
export interface IApplicationState {
|
||||
/** Event that fires when the application states changes with new application state as parameter */
|
||||
readonly code: IApplicationCode;
|
||||
readonly filter: IUserFilter;
|
||||
readonly stateChanged: ISignal<IApplicationState>;
|
||||
readonly selection: IUserSelection;
|
||||
readonly app: IApplication;
|
||||
}
|
||||
47
src/application/application.yaml.d.ts
vendored
47
src/application/application.yaml.d.ts
vendored
@@ -1,47 +0,0 @@
|
||||
declare module 'js-yaml-loader!*' {
|
||||
export interface ApplicationYaml {
|
||||
actions: ReadonlyArray<YamlCategory>;
|
||||
functions: ReadonlyArray<YamlFunction> | undefined;
|
||||
}
|
||||
|
||||
export interface YamlCategory extends YamlDocumentable {
|
||||
children: ReadonlyArray<CategoryOrScript>;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export type CategoryOrScript = YamlCategory | YamlScript;
|
||||
export type DocumentationUrls = ReadonlyArray<string> | string;
|
||||
|
||||
export interface YamlDocumentable {
|
||||
docs?: DocumentationUrls;
|
||||
}
|
||||
|
||||
export interface YamlFunction {
|
||||
name: string;
|
||||
code: string;
|
||||
revertCode?: string;
|
||||
parameters?: readonly string[];
|
||||
}
|
||||
|
||||
export interface FunctionCallParameters {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
export interface FunctionCall {
|
||||
function: string;
|
||||
parameters?: FunctionCallParameters;
|
||||
}
|
||||
|
||||
export type ScriptFunctionCall = readonly FunctionCall[] | FunctionCall | undefined;
|
||||
|
||||
export interface YamlScript extends YamlDocumentable {
|
||||
name: string;
|
||||
code: string | undefined;
|
||||
revertCode: string | undefined;
|
||||
call: ScriptFunctionCall;
|
||||
recommend: string | undefined;
|
||||
}
|
||||
|
||||
const content: ApplicationYaml;
|
||||
export default content;
|
||||
}
|
||||
56
src/application/collections/collection.yaml.d.ts
vendored
Normal file
56
src/application/collections/collection.yaml.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
declare module 'js-yaml-loader!*' {
|
||||
export interface CollectionData {
|
||||
readonly os: string;
|
||||
readonly scripting: ScriptingDefinitionData;
|
||||
readonly actions: ReadonlyArray<CategoryData>;
|
||||
readonly functions?: ReadonlyArray<FunctionData>;
|
||||
}
|
||||
|
||||
export interface CategoryData extends DocumentableData {
|
||||
readonly children: ReadonlyArray<CategoryOrScriptData>;
|
||||
readonly category: string;
|
||||
}
|
||||
|
||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
||||
|
||||
export interface DocumentableData {
|
||||
readonly docs?: DocumentationUrlsData;
|
||||
}
|
||||
|
||||
export interface FunctionData {
|
||||
name: string;
|
||||
code: string;
|
||||
revertCode?: string;
|
||||
parameters?: readonly string[];
|
||||
}
|
||||
|
||||
export interface FunctionCallParametersData {
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
export interface FunctionCallData {
|
||||
function: string;
|
||||
parameters?: FunctionCallParametersData;
|
||||
}
|
||||
|
||||
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||
|
||||
export interface ScriptData extends DocumentableData {
|
||||
name: string;
|
||||
code?: string;
|
||||
revertCode?: string;
|
||||
call: ScriptFunctionCallData;
|
||||
recommend?: string;
|
||||
}
|
||||
|
||||
export interface ScriptingDefinitionData {
|
||||
readonly language: string;
|
||||
readonly fileExtension: string;
|
||||
readonly startCode: string;
|
||||
readonly endCode: string;
|
||||
}
|
||||
|
||||
const content: CollectionData;
|
||||
export default content;
|
||||
}
|
||||
225
src/application/collections/macos.yaml
Normal file
225
src/application/collections/macos.yaml
Normal file
@@ -0,0 +1,225 @@
|
||||
# Structure documented in "docs/collections.md"
|
||||
os: macos
|
||||
scripting:
|
||||
language: shellscript
|
||||
startCode: |-
|
||||
#!/usr/bin/env bash
|
||||
# {{ $homepage }} — v{{ $version }} — {{ $date }}
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
script_path=$([[ "$0" = /* ]] && echo "$0" || echo "$PWD/${0#./}")
|
||||
sudo "$script_path" || (
|
||||
echo 'Administrator privileges are required.'
|
||||
exit 1
|
||||
)
|
||||
exit 0
|
||||
fi
|
||||
endCode: |-
|
||||
echo 'Your privacy and security is now hardened 🎉💪'
|
||||
echo 'Press any key to exit.'
|
||||
read -n 1 -s
|
||||
actions:
|
||||
-
|
||||
category: Privacy cleanup
|
||||
children:
|
||||
-
|
||||
category: Clear terminal history
|
||||
children:
|
||||
-
|
||||
name: Clear bash history
|
||||
recommend: standard
|
||||
code: rm -f ~/.bash_history
|
||||
-
|
||||
name: Clear zsh history
|
||||
recommend: standard
|
||||
code: rm -f ~/.zsh_history
|
||||
-
|
||||
name: Clear CUPS printer job cache
|
||||
recommend: strict
|
||||
code: |-
|
||||
sudo rm -rfv /var/spool/cups/c0*
|
||||
sudo rm -rfv /var/spool/cups/tmp/*
|
||||
sudo rm -rfv /var/spool/cups/cache/job.cache*
|
||||
-
|
||||
name: Clear the list of iOS devices connected
|
||||
recommend: strict
|
||||
code: |-
|
||||
sudo defaults delete /Users/$USER/Library/Preferences/com.apple.iPod.plist "conn:128:Last Connect"
|
||||
sudo defaults delete /Users/$USER/Library/Preferences/com.apple.iPod.plist Devices
|
||||
sudo defaults delete /Library/Preferences/com.apple.iPod.plist "conn:128:Last Connect"
|
||||
sudo defaults delete /Library/Preferences/com.apple.iPod.plist Devices
|
||||
sudo rm -rfv /var/db/lockdown/*
|
||||
-
|
||||
name: Reset privacy database (remove all permissions)
|
||||
code: sudo tccutil reset All
|
||||
-
|
||||
category: Configure programs
|
||||
children:
|
||||
-
|
||||
name: Disable Firefox telemetry
|
||||
recommend: standard
|
||||
docs: https://github.com/mozilla/policy-templates/blob/master/README.md
|
||||
code: |-
|
||||
# Enable Firefox policies so the telemetry can be configured.
|
||||
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
|
||||
# Disable sending usage data
|
||||
sudo defaults write /Library/Preferences/org.mozilla.firefox DisableTelemetry -bool TRUE
|
||||
revertCode: |-
|
||||
sudo defaults delete /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled
|
||||
sudo defaults delete /Library/Preferences/org.mozilla.firefox DisableTelemetry
|
||||
-
|
||||
name: Disable Microsoft Office diagnostics data sending
|
||||
recommend: standard
|
||||
code: defaults write com.microsoft.office DiagnosticDataTypePreference -string ZeroDiagnosticData
|
||||
revertCode: defaults delete com.microsoft.office DiagnosticDataTypePreference
|
||||
-
|
||||
name: Uninstall Google update
|
||||
recommend: strict
|
||||
code: |-
|
||||
googleUpdateFile=~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/Resources/ksinstall
|
||||
if [ -f "$googleUpdateFile" ]; then
|
||||
$googleUpdateFile --nuke
|
||||
echo Uninstalled google update
|
||||
else
|
||||
echo Google update file does not exist
|
||||
fi
|
||||
-
|
||||
name: Disable Homebrew user behavior analytics
|
||||
recommend: standard
|
||||
docs: https://docs.brew.sh/Analytics
|
||||
call:
|
||||
-
|
||||
function: PersistUserEnvironmentConfiguration
|
||||
parameters:
|
||||
configuration: export HOMEBREW_NO_ANALYTICS=1
|
||||
-
|
||||
name: Disable NET Core CLI telemetry
|
||||
recommend: standard
|
||||
call:
|
||||
-
|
||||
function: PersistUserEnvironmentConfiguration
|
||||
parameters:
|
||||
configuration: export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
-
|
||||
name: Disable PowerShell Core telemetry
|
||||
recommend: standard
|
||||
docs: https://github.com/PowerShell/PowerShell/tree/release/v7.1.1#telemetry
|
||||
call:
|
||||
-
|
||||
function: PersistUserEnvironmentConfiguration
|
||||
parameters:
|
||||
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
||||
-
|
||||
category: Configure OS
|
||||
children:
|
||||
-
|
||||
category: Configure Apple Remote Desktop
|
||||
children:
|
||||
-
|
||||
name: Deactivate the Remote Management Service
|
||||
recommend: strict
|
||||
code: sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -deactivate -stop
|
||||
revertCode: sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -restart -agent -console
|
||||
-
|
||||
name: Remove Apple Remote Desktop Settings
|
||||
recommend: strict
|
||||
code: |-
|
||||
sudo rm -rf /var/db/RemoteManagement
|
||||
sudo defaults delete /Library/Preferences/com.apple.RemoteDesktop.plist
|
||||
defaults delete ~/Library/Preferences/com.apple.RemoteDesktop.plist
|
||||
sudo rm -r /Library/Application\ Support/Apple/Remote\ Desktop/
|
||||
rm -r ~/Library/Application\ Support/Remote\ Desktop/
|
||||
rm -r ~/Library/Containers/com.apple.RemoteDesktop
|
||||
-
|
||||
name: Disable Internet based spell correction
|
||||
code: defaults write NSGlobalDomain WebAutomaticSpellingCorrectionEnabled -bool false
|
||||
revertCode: defaults delete NSGlobalDomain WebAutomaticSpellingCorrectionEnabled
|
||||
-
|
||||
name: Disable Remote Apple Events
|
||||
recommend: strict
|
||||
code: sudo systemsetup -setremoteappleevents off
|
||||
revertCode: sudo systemsetup -setremoteappleevents on
|
||||
-
|
||||
name: Do not store documents to iCloud Drive by default
|
||||
docs: https://macos-defaults.com/finder/nsdocumentsavenewdocumentstocloud.html
|
||||
recommend: standard
|
||||
code: defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false
|
||||
revertCode: defaults delete NSGlobalDomain NSDocumentSaveNewDocumentsToCloud
|
||||
-
|
||||
category: Security improvements
|
||||
children:
|
||||
-
|
||||
category: Configure macOS Application Firewall
|
||||
children:
|
||||
-
|
||||
name: Enable firewall
|
||||
recommend: standard
|
||||
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81681
|
||||
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
|
||||
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off
|
||||
-
|
||||
name: Turn on firewall logging
|
||||
recommend: standard
|
||||
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81671
|
||||
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on
|
||||
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode off
|
||||
-
|
||||
name: Turn on stealth mode
|
||||
recommend: standard
|
||||
docs: https://www.stigviewer.com/stig/apple_os_x_10.8_mountain_lion_workstation/2015-02-10/finding/V-51327
|
||||
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
|
||||
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
|
||||
-
|
||||
name: Disable Spotlight indexing
|
||||
code: sudo mdutil -i off -d /
|
||||
revertCode: sudo mdutil -i on /
|
||||
-
|
||||
name: Disable Captive portal
|
||||
docs:
|
||||
- https://web.archive.org/web/20171008071031if_/http://blog.erratasec.com/2010/09/apples-secret-wispr-request.html#.WdnPa5OyL6Y
|
||||
- https://web.archive.org/web/20130407200745/http://www.divertednetworks.net/apple-captiveportal.html
|
||||
- https://web.archive.org/web/20170622064304/https://grpugh.wordpress.com/2014/10/29/an-undocumented-change-to-captive-network-assistant-settings-in-os-x-10-10-yosemite/
|
||||
code: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active -bool false
|
||||
revertCode: sudo defaults delete /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active
|
||||
-
|
||||
name: Require a password to wake the computer from sleep or screen saver
|
||||
code: defaults write /Library/Preferences/com.apple.screensaver askForPassword -bool true
|
||||
revertCode: sudo defaults delete /Library/Preferences/com.apple.screensaver askForPassword
|
||||
-
|
||||
name: Do not show recent items on dock
|
||||
docs: https://developer.apple.com/documentation/devicemanagement/dock
|
||||
code: defaults write com.apple.dock show-recents -bool false
|
||||
revertCode: defaults delete com.apple.dock show-recents
|
||||
-
|
||||
name: Disable AirDrop file sharing
|
||||
recommend: strict
|
||||
code: defaults write com.apple.NetworkBrowser DisableAirDrop -bool true
|
||||
revertCode: defaults write com.apple.NetworkBrowser DisableAirDrop -bool false
|
||||
functions:
|
||||
-
|
||||
name: PersistUserEnvironmentConfiguration
|
||||
parameters: [ configuration ]
|
||||
code: |-
|
||||
command='{{ $configuration }}'
|
||||
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
|
||||
for profile_file in "${profile_files[@]}"
|
||||
do
|
||||
touch "$profile_file"
|
||||
if ! grep -q "$command" "${profile_file}"; then
|
||||
echo "$command" >> "$profile_file"
|
||||
echo "[$profile_file] Configured"
|
||||
else
|
||||
echo "[$profile_file] No need for any action, already configured"
|
||||
fi
|
||||
done
|
||||
revertCode: |-
|
||||
command='{{ $configuration }}'
|
||||
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
|
||||
for profile_file in "${profile_files[@]}"
|
||||
do
|
||||
if grep -q "$command" "${profile_file}" 2>/dev/null; then
|
||||
sed -i '' "/$command/d" "$profile_file"
|
||||
echo "[$profile_file] Reverted configuration"
|
||||
else
|
||||
echo "[$profile_file] No need for any action, configuration does not exist"
|
||||
fi
|
||||
done
|
||||
@@ -1,4 +1,22 @@
|
||||
# Structure documented in "docs/application-file.md"
|
||||
# Structure documented in "docs/collections.md"
|
||||
os: windows
|
||||
scripting:
|
||||
language: batchfile
|
||||
startCode: |-
|
||||
@echo off
|
||||
:: {{ $homepage }} — v{{ $version }} — {{ $date }}
|
||||
:: Ensure admin privileges
|
||||
fltmc >nul 2>&1 || (
|
||||
echo Administrator privileges are required.
|
||||
PowerShell Start -Verb RunAs '%0' 2> nul || (
|
||||
echo Right-click on the script and select "Run as administrator".
|
||||
pause & exit 1
|
||||
)
|
||||
exit 0
|
||||
)
|
||||
endCode: |-
|
||||
pause
|
||||
exit /b 0
|
||||
actions:
|
||||
-
|
||||
category: Privacy cleanup
|
||||
@@ -406,6 +424,17 @@ actions:
|
||||
net start DPS
|
||||
)
|
||||
endlocal
|
||||
-
|
||||
name: Clear previous Windows installations
|
||||
code: |-
|
||||
if exist "%SystemDrive%\Windows.old" (
|
||||
takeown /f "%SystemDrive%\Windows.old" /a /r /d y
|
||||
icacls "%SystemDrive%\Windows.old" /grant administrators:F /t
|
||||
rd /s /q "%SystemDrive%\Windows.old"
|
||||
echo Deleted previous installation from "%SystemDrive%\Windows.old\"
|
||||
) else (
|
||||
echo No previous Windows installation has been found
|
||||
)
|
||||
-
|
||||
category: Disable OS data collection
|
||||
children:
|
||||
@@ -1006,43 +1035,99 @@ actions:
|
||||
category: Disable windows search data collection
|
||||
children:
|
||||
-
|
||||
name: Disable cortana
|
||||
category: Disable cortana
|
||||
children:
|
||||
-
|
||||
name: Do not allow Cortana
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://admx.help/?Category=Windows_10_2016&Policy=FullArmor.Policies.3B9EA2B5_A1D1_4CD5_9EDE_75B22990BC21::AllowCortana
|
||||
- https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#21-cortana-and-search-group-policies
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortana" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortana" /f
|
||||
-
|
||||
name: Do not allow Cortana experience
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\PolicyManager\default\Experience\AllowCortana" /v "value" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\PolicyManager\default\Experience\AllowCortana" /v "value" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Do not allow search and Cortana to search cloud sources like OneDrive and SharePoint
|
||||
recommend: standard
|
||||
docs: https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search#search-allowcloudsearch
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCloudSearch" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCloudSearch" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable Cortana speech interaction while the system is locked
|
||||
recommend: standard
|
||||
docs: https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-abovelock
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortanaAboveLock" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortanaAboveLock" /f
|
||||
-
|
||||
name: Opt out from Cortana consent
|
||||
recommend: standard
|
||||
code: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "CortanaConsent" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "CortanaConsent" /t REG_DWORD /d 10 /f
|
||||
-
|
||||
name: Do not allow Cortana to be enabled
|
||||
recommend: standard
|
||||
code: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CanCortanaBeEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CanCortanaBeEnabled" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable Cortana (Internet search results in start menu)
|
||||
recommend: standard
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 0 /f
|
||||
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: |-
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
||||
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Remove the Cortana taskbar icon
|
||||
recommend: standard
|
||||
code: reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced /v "ShowCortanaButton" /f
|
||||
-
|
||||
category: Configure Windows search indexing
|
||||
children:
|
||||
-
|
||||
name: Disable search indexing encrypted items / stores
|
||||
recommend: standard
|
||||
docs: https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search#search-allowindexingencryptedstoresoritems
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowIndexingEncryptedStoresOrItems" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowIndexingEncryptedStoresOrItems" /f
|
||||
-
|
||||
name: Do not use automatic language detection when indexing
|
||||
recommend: standard
|
||||
docs: https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search#search-alwaysuseautolangdetection
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AlwaysUseAutoLangDetection" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AlwaysUseAutoLangDetection" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Do not allow search to use location
|
||||
recommend: standard
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortana" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Microsoft\PolicyManager\default\Experience\AllowCortana" /v "value" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 0 /f
|
||||
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CortanaEnabled" /t REG_DWORD /d 0 /f
|
||||
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "CanCortanaBeEnabled" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v BingSearchEnabled /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCloudSearch" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortana" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowCortanaAboveLock" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowSearchToUseLocation" /t REG_DWORD /d 0 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "ConnectedSearchUseWeb" /t REG_DWORD /d 0 /f
|
||||
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "CortanaConsent" /d 0 /t REG_DWORD /f
|
||||
docs:
|
||||
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-search#search-allowsearchtouselocation
|
||||
- https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#21-cortana-and-search-group-policies
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowSearchToUseLocation" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "AllowSearchToUseLocation" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable web search in search bar
|
||||
recommend: standard
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v DisableWebSearch /t REG_DWORD /d 1 /f
|
||||
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Search" /v "BingSearchEnabled" /d 0 /t REG_DWORD /f
|
||||
docs:
|
||||
- https://admx.help/?Category=Windows_10_2016&Policy=FullArmor.Policies.3B9EA2B5_A1D1_4CD5_9EDE_75B22990BC21::DisableWebSearch
|
||||
- https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#21-cortana-and-search-group-policies
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "DisableWebSearch" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "DisableWebSearch" /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Disable search web when searching pc
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v ConnectedSearchUseWeb /t REG_DWORD /d 0 /f
|
||||
name: Do not search the web or display web results in Search
|
||||
docs: https://docs.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#21-cortana-and-search-group-policies
|
||||
recomend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "ConnectedSearchUseWeb" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v "ConnectedSearchUseWeb" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable search indexing encrypted items / stores
|
||||
name: Disable Bing search
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AllowIndexingEncryptedStoresOrItems /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Disable location based info in searches
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AllowSearchToUseLocation /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Disable language detection
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AlwaysUseAutoLangDetection /t REG_DWORD /d 0 /f
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "BingSearchEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" /v "BingSearchEnabled" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
category: Disable targeted ads and marketing
|
||||
children:
|
||||
@@ -1558,6 +1643,30 @@ actions:
|
||||
-
|
||||
category: Configure Edge
|
||||
children:
|
||||
-
|
||||
category: Chromium Edge settings
|
||||
children:
|
||||
-
|
||||
name: Disable Edge usage and crash-related data reporting # Obselete since Microsoft Edge version 89
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::MetricsReportingEnabled
|
||||
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#metricsreportingenabled
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "MetricsReportingEnabled" /f
|
||||
-
|
||||
name: Disable sending site information # Obselete since Microsoft Edge version 89
|
||||
recommend: standard
|
||||
docs:
|
||||
- https://admx.help/?Category=EdgeChromium&Policy=Microsoft.Policies.Edge::SendSiteInfoToImproveServices
|
||||
- https://docs.microsoft.com/en-us/DeployEdge/microsoft-edge-policies#sendsiteinfotoimproveservices
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Policies\Microsoft\Edge" /v "SendSiteInfoToImproveServices" /f
|
||||
-
|
||||
name: Disable Automatic Installation of Microsoft Edge Chromium
|
||||
docs: https://docs.microsoft.com/en-us/deployedge/microsoft-edge-blocker-toolkit
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\EdgeUpdate" /v "DoNotUpdateToEdgeWithChromium" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Microsoft\EdgeUpdate" /v "DoNotUpdateToEdgeWithChromium" /f
|
||||
-
|
||||
name: Disable live tile data collection
|
||||
recommend: standard
|
||||
@@ -1565,7 +1674,7 @@ actions:
|
||||
- https://docs.microsoft.com/en-us/microsoft-edge/deploy/group-policies/telemetry-management-gp
|
||||
- https://docs.microsoft.com/en-us/windows/client-management/mdm/policy-csp-browser#browser-preventlivetiledatacollection
|
||||
code: reg add "HKCU\Software\Policies\Microsoft\MicrosoftEdge\Main" /v "PreventLiveTileDataCollection" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg add "HKCU\Software\Policies\Microsoft\MicrosoftEdge\Main" /v "PreventLiveTileDataCollection" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKCU\Software\Policies\Microsoft\MicrosoftEdge\Main" /v "PreventLiveTileDataCollection" /t REG_DWORD /d 0 /f
|
||||
-
|
||||
name: Disable MFU tracking
|
||||
recommend: standard
|
||||
@@ -1592,11 +1701,6 @@ actions:
|
||||
recommend: standard
|
||||
code: reg add "HKLM\SOFTWARE\Policies\Microsoft\MicrosoftEdge\SearchScopes" /v "ShowSearchSuggestionsGlobal" /t REG_DWORD /d 0 /f
|
||||
revertCode: reg add "HKLM\SOFTWARE\Policies\Microsoft\MicrosoftEdge\SearchScopes" /v "ShowSearchSuggestionsGlobal" /t REG_DWORD /d 1 /f
|
||||
-
|
||||
name: Disable Automatic Installation of Microsoft Edge Chromium
|
||||
docs: https://docs.microsoft.com/en-us/deployedge/microsoft-edge-blocker-toolkit
|
||||
code: reg add "HKLM\SOFTWARE\Microsoft\EdgeUpdate" /v "DoNotUpdateToEdgeWithChromium" /t REG_DWORD /d 1 /f
|
||||
revertCode: reg delete "HKLM\SOFTWARE\Microsoft\EdgeUpdate" /v "DoNotUpdateToEdgeWithChromium" /f
|
||||
-
|
||||
category: Configure Internet Explorer
|
||||
children:
|
||||
@@ -2603,6 +2707,8 @@ actions:
|
||||
children:
|
||||
-
|
||||
name: App Connector app
|
||||
recommend: strict
|
||||
docs: https://superuser.com/a/1003226
|
||||
call:
|
||||
function: UninstallStoreApp
|
||||
parameters:
|
||||
@@ -2659,6 +2765,14 @@ actions:
|
||||
function: UninstallStoreApp
|
||||
parameters:
|
||||
packageName: Microsoft.BingFinance
|
||||
-
|
||||
name: Uninstall Cortana app
|
||||
recommend: standard
|
||||
docs: https://www.microsoft.com/en-us/p/msn-money/9wzdncrfhv4v
|
||||
call:
|
||||
function: UninstallStoreApp
|
||||
parameters:
|
||||
packageName: Microsoft.549981C3F5F10
|
||||
-
|
||||
name: App Installer app
|
||||
docs: https://www.microsoft.com/en-us/p/app-installer/9nblggh4nns1
|
||||
@@ -3197,20 +3311,27 @@ actions:
|
||||
category: Microsoft Edge
|
||||
children:
|
||||
-
|
||||
name: Microsoft Edge app
|
||||
name: Microsoft Edge (Legacy) app
|
||||
recommend: strict
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.MicrosoftEdge
|
||||
-
|
||||
name: Microsoft Edge Dev Tools Client app
|
||||
name: Microsoft Edge (Legacy) Dev Tools Client app
|
||||
docs: https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide
|
||||
recommend: strict
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.MicrosoftEdgeDevToolsClient
|
||||
-
|
||||
name: Win32 Web View Host app / Desktop App Web Viewer
|
||||
recommend: strict
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.Win32WebViewHost
|
||||
-
|
||||
name: Microsoft PPI Projection app
|
||||
docs: https://en.wikipedia.org/wiki/Perceptive_Pixel
|
||||
@@ -3219,13 +3340,6 @@ actions:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.PPIProjection
|
||||
-
|
||||
name: Win32 Web View Host app / Desktop App Web Viewer
|
||||
recommend: strict
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.Win32WebViewHost
|
||||
-
|
||||
name: ChxApp app
|
||||
call:
|
||||
@@ -3260,15 +3374,20 @@ actions:
|
||||
parameters:
|
||||
packageName: Microsoft.Windows.ContentDeliveryManager
|
||||
-
|
||||
category: Uninstall Cortana apps
|
||||
category: Uninstall Cortana system apps
|
||||
children:
|
||||
-
|
||||
name: Cortana app (breaks Windows search)
|
||||
name: Search app (breaks Windows search)
|
||||
docs: https://thegeekpage.com/searchui-exe-suspended-error/
|
||||
call:
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.Windows.Cortana
|
||||
-
|
||||
function: UninstallSystemApp
|
||||
parameters:
|
||||
packageName: Microsoft.Windows.Cortana # Removed since version 2004
|
||||
-
|
||||
function: UninstallStoreApp
|
||||
parameters:
|
||||
packageName: Microsoft.Windows.Search # Added in version 2004, it was called "Cortana" before now it's plain "Search"
|
||||
-
|
||||
name: Holographic First Run app
|
||||
recommend: standard
|
||||
@@ -3400,15 +3519,17 @@ actions:
|
||||
children:
|
||||
-
|
||||
name: Kill OneDrive process
|
||||
recommend: strict
|
||||
code: taskkill /f /im OneDrive.exe
|
||||
revertCode: '"%LOCALAPPDATA%\Microsoft\OneDrive\OneDrive.exe"'
|
||||
-
|
||||
name: Uninstall OneDrive
|
||||
recommend: strict
|
||||
code: |-
|
||||
if %PROCESSOR_ARCHITECTURE%==x86 (
|
||||
%SystemRoot%\System32\OneDriveSetup.exe /uninstall 2>null
|
||||
%SystemRoot%\System32\OneDriveSetup.exe /uninstall 2>nul
|
||||
) else (
|
||||
%SystemRoot%\SysWOW64\OneDriveSetup.exe /uninstall 2>null
|
||||
%SystemRoot%\SysWOW64\OneDriveSetup.exe /uninstall 2>nul
|
||||
)
|
||||
revertCode: |-
|
||||
if %PROCESSOR_ARCHITECTURE%==x86 (
|
||||
@@ -3418,6 +3539,7 @@ actions:
|
||||
)
|
||||
-
|
||||
name: Remove OneDrive leftovers
|
||||
recommend: strict
|
||||
code: |-
|
||||
rd "%UserProfile%\OneDrive" /q /s
|
||||
rd "%LocalAppData%\Microsoft\OneDrive" /q /s
|
||||
@@ -3425,6 +3547,7 @@ actions:
|
||||
rd "%SystemDrive%\OneDriveTemp" /q /s
|
||||
-
|
||||
name: Delete OneDrive shortcuts
|
||||
recommend: strict
|
||||
docs: https://docs.microsoft.com/en-us/sharepoint/troubleshoot/installation-and-setup/how-to-block-onedrive.exe-from-being-advertised-after-install-office-2016
|
||||
code: |-
|
||||
del "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Microsoft OneDrive.lnk" /s /f /q
|
||||
@@ -3432,6 +3555,7 @@ actions:
|
||||
del "%USERPROFILE%\Links\OneDrive.lnk" /s /f /q
|
||||
-
|
||||
name: Disable usage of OneDrive
|
||||
recommend: strict
|
||||
code: |-
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive" /t REG_DWORD /v "DisableFileSyncNGSC" /d 1 /f
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive" /t REG_DWORD /v "DisableFileSync" /d 1 /f
|
||||
@@ -3440,6 +3564,7 @@ actions:
|
||||
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\OneDrive" /t REG_DWORD /v "DisableFileSync" /d 0 /f
|
||||
-
|
||||
name: Prevent automatic OneDrive install for current user
|
||||
recommend: strict
|
||||
code: reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "OneDriveSetup" /f
|
||||
revertCode: |-
|
||||
if %PROCESSOR_ARCHITECTURE%==x86 (
|
||||
@@ -3449,6 +3574,7 @@ actions:
|
||||
)
|
||||
-
|
||||
name: Prevent automatic OneDrive install for new users
|
||||
recommend: strict
|
||||
code: |-
|
||||
reg load "HKU\Default" "%SystemDrive%\Users\Default\NTUSER.DAT"
|
||||
reg delete "HKU\Default\software\Microsoft\Windows\CurrentVersion\Run" /v "OneDriveSetup" /f
|
||||
@@ -3463,6 +3589,7 @@ actions:
|
||||
reg unload "HKU\Default"
|
||||
-
|
||||
name: Remove OneDrive from explorer menu
|
||||
recommend: strict
|
||||
code: |-
|
||||
reg delete "HKCR\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}" /f
|
||||
reg delete "HKCR\Wow6432Node\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}" /f
|
||||
@@ -3473,11 +3600,23 @@ actions:
|
||||
reg add "HKCR\Wow6432Node\{018D5C66-4533-4307-9B53-224DE2ED1FE6}" /v System.IsPinnedToNameSpaceTree /d "1" /t REG_DWORD /f
|
||||
-
|
||||
name: Delete all OneDrive related Services
|
||||
recommend: strict
|
||||
code: for /f "tokens=1 delims=," %%x in ('schtasks /query /fo csv ^| find "OneDrive"') do schtasks /Delete /TN %%x /F
|
||||
-
|
||||
name: Delete OneDrive path from registry
|
||||
recommend: strict
|
||||
docs: https://stackoverflow.com/questions/46744840/export-registry-value-to-file-and-then-set-a-variable-in-batch
|
||||
code: reg delete "HKCU\Environment" /v "OneDrive" /f
|
||||
-
|
||||
name: Uninstall Edge (chromium-based)
|
||||
code:
|
||||
PowerShell -ExecutionPolicy Unrestricted -Command "
|
||||
$installer = (Get-ChildItem \"$env:ProgramFiles*\Microsoft\Edge\Application\*\Installer\setup.exe\");
|
||||
if (!$installer) {
|
||||
Write-Host Could not find the installer;
|
||||
} else {
|
||||
& $installer.FullName -uninstall -system-level -verbose-logging -force-uninstall
|
||||
}; "
|
||||
-
|
||||
category: Disable built-in Windows features
|
||||
children:
|
||||
@@ -4027,8 +4166,8 @@ functions:
|
||||
$directories = @($package.InstallLocation, \"$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)\");
|
||||
foreach($dir in $directories) {
|
||||
if ( !$dir -Or !(Test-Path \"$dir\") ) { continue; }
|
||||
cmd /c takeown /f \"$dir\" /r /d y | Out-Null;
|
||||
cmd /c icacls \"$dir\" /grant administrators:F /t | Out-Null;
|
||||
cmd /c ('takeown /f \"' + $dir + '\" /r /d y 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
|
||||
cmd /c ('icacls \"' + $dir + '\" /grant administrators:F /t 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
|
||||
$files = Get-ChildItem -File -Path $dir -Recurse -Force;
|
||||
foreach($file in $files) {
|
||||
if($file.Name.EndsWith('.OLD')) { continue; }
|
||||
@@ -4036,7 +4175,7 @@ functions:
|
||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||
}
|
||||
}; "
|
||||
};"
|
||||
revertCode:
|
||||
PowerShell -Command "
|
||||
$package = (Get-AppxPackage -AllUsers '{{ $packageName }}');
|
||||
@@ -4046,17 +4185,38 @@ functions:
|
||||
$directories = @($package.InstallLocation, \"$env:LOCALAPPDATA\Packages\$($package.PackageFamilyName)\");
|
||||
foreach($dir in $directories) {
|
||||
if ( !$dir -Or !(Test-Path \"$dir\") ) { continue; }
|
||||
cmd /c takeown /f \"$dir\" /r /d y | Out-Null;
|
||||
cmd /c icacls \"$dir\" /grant administrators:F /t | Out-Null;
|
||||
cmd /c ('takeown /f \"' + $dir + '\" /r /d y 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
|
||||
cmd /c ('icacls \"' + $dir + '\" /grant administrators:F /t 1> nul'); if($LASTEXITCODE) { throw 'Failed to take ownership'; }
|
||||
$files = Get-ChildItem -File -Path \"$dir\*.OLD\" -Recurse -Force;
|
||||
foreach($file in $files) {
|
||||
$newName = $file.FullName.Substring(0, $file.FullName.Length - 4);
|
||||
Write-Host \"Rename '$($file.FullName)' to '$newName'\";
|
||||
Move-Item -LiteralPath \"$($file.FullName)\" -Destination \"$newName\" -Force;
|
||||
}
|
||||
}; "
|
||||
};"
|
||||
-
|
||||
name: UninstallCapability
|
||||
parameters: [ capabilityName ]
|
||||
code: PowerShell -Command "Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'' | Remove-WindowsCapability -Online"
|
||||
revertCode: PowerShell -Command "$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*''; Add-WindowsCapability -Name \"$capability.Name\" -Online"
|
||||
code: PowerShell -Command "Get-WindowsCapability -Online -Name '{{ $capabilityName }}*' | Remove-WindowsCapability -Online"
|
||||
revertCode: PowerShell -Command "$capability = Get-WindowsCapability -Online -Name '{{ $capabilityName }}*'; Add-WindowsCapability -Name \"$capability.Name\" -Online"
|
||||
-
|
||||
name: RenameSystemFile
|
||||
parameters: [ filePath ]
|
||||
code: |-
|
||||
if exist "{{ $filePath }}" (
|
||||
takeown /f "{{ $filePath }}"
|
||||
icacls "{{ $filePath }}" /grant administrators:F
|
||||
move "{{ $filePath }}" "{{ $filePath }}.OLD"
|
||||
echo Moved "{{ $filePath }}" to "{{ $filePath }}.OLD"
|
||||
) else (
|
||||
echo No action required: {{ $filePath }} is not found.
|
||||
)
|
||||
revertCode: |-
|
||||
if exist "{{ $filePath }}.OLD" (
|
||||
takeown /f "{{ $filePath }}.OLD"
|
||||
icacls "{{ $filePath }}.OLD" /grant administrators:F
|
||||
move "{{ $filePath }}.OLD" "{{ $filePath }}"
|
||||
echo Moved "{{ $filePath }}.OLD" to "{{ $filePath }}"
|
||||
) else (
|
||||
echo Could not find backup file "{{ $filePath }}.OLD" 1>&2
|
||||
)
|
||||
@@ -1,152 +1,47 @@
|
||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import { ICategory } from './ICategory';
|
||||
import { IScript } from './IScript';
|
||||
import { IApplication } from './IApplication';
|
||||
import { ICategoryCollection } from './ICategoryCollection';
|
||||
import { IProjectInformation } from './IProjectInformation';
|
||||
import { RecommendationLevel, RecommendationLevelNames, RecommendationLevels } from './RecommendationLevel';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
|
||||
export class Application implements IApplication {
|
||||
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||
|
||||
private readonly queryable: IQueryableApplication;
|
||||
|
||||
constructor(
|
||||
public readonly info: IProjectInformation,
|
||||
public readonly actions: ReadonlyArray<ICategory>) {
|
||||
if (!info) {
|
||||
throw new Error('info is undefined');
|
||||
}
|
||||
this.queryable = makeQueryable(actions);
|
||||
ensureValid(this.queryable);
|
||||
ensureNoDuplicates(this.queryable.allCategories);
|
||||
ensureNoDuplicates(this.queryable.allScripts);
|
||||
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) {
|
||||
validateInformation(info);
|
||||
validateCollections(collections);
|
||||
}
|
||||
|
||||
public findCategory(categoryId: number): ICategory | undefined {
|
||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
||||
public getSupportedOsList(): OperatingSystem[] {
|
||||
return this.collections.map((collection) => collection.os);
|
||||
}
|
||||
|
||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||
if (isNaN(level)) {
|
||||
throw new Error('undefined level');
|
||||
}
|
||||
if (!(level in RecommendationLevel)) {
|
||||
throw new Error(`invalid level: ${level}`);
|
||||
}
|
||||
return this.queryable.scriptsByLevel.get(level);
|
||||
}
|
||||
|
||||
public findScript(scriptId: string): IScript | undefined {
|
||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
||||
}
|
||||
|
||||
public getAllScripts(): IScript[] {
|
||||
return this.queryable.allScripts;
|
||||
}
|
||||
|
||||
public getAllCategories(): ICategory[] {
|
||||
return this.queryable.allCategories;
|
||||
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
|
||||
return this.collections.find((collection) => collection.os === operatingSystem);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||
const totalOccurencesById = new Map<TKey, number>();
|
||||
for (const entity of entities) {
|
||||
totalOccurencesById.set(entity.id, (totalOccurencesById.get(entity.id) || 0) + 1);
|
||||
}
|
||||
const duplicatedIds = new Array<TKey>();
|
||||
totalOccurencesById.forEach((index, id) => {
|
||||
if (index > 1) {
|
||||
duplicatedIds.push(id);
|
||||
}
|
||||
});
|
||||
if (duplicatedIds.length > 0) {
|
||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||
throw new Error(
|
||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
||||
function validateInformation(info: IProjectInformation) {
|
||||
if (!info) {
|
||||
throw new Error('undefined project information');
|
||||
}
|
||||
}
|
||||
|
||||
interface IQueryableApplication {
|
||||
allCategories: ICategory[];
|
||||
allScripts: IScript[];
|
||||
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
|
||||
}
|
||||
|
||||
function ensureValid(application: IQueryableApplication) {
|
||||
ensureValidCategories(application.allCategories);
|
||||
ensureValidScripts(application.allScripts);
|
||||
}
|
||||
|
||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||
if (!allCategories || allCategories.length === 0) {
|
||||
throw new Error('Application must consist of at least one category');
|
||||
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||
if (!collections) {
|
||||
throw new Error('undefined collections');
|
||||
}
|
||||
if (collections.length === 0) {
|
||||
throw new Error('no collection in the list');
|
||||
}
|
||||
if (collections.filter((c) => !c).length > 0) {
|
||||
throw new Error('undefined collection in the list');
|
||||
}
|
||||
const osList = collections.map((c) => c.os);
|
||||
const duplicates = getDuplicates(osList);
|
||||
if (duplicates.length > 0) {
|
||||
throw new Error('multiple collections with same os: ' +
|
||||
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "'));
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||
if (!allScripts || allScripts.length === 0) {
|
||||
throw new Error('Application must consist of at least one script');
|
||||
}
|
||||
for (const level of RecommendationLevels) {
|
||||
if (allScripts.every((script) => script.level !== level)) {
|
||||
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
||||
const allCategories = new Array<ICategory>();
|
||||
const allScripts = new Array<IScript>();
|
||||
flattenCategories(categories, allCategories, allScripts);
|
||||
return [
|
||||
allCategories,
|
||||
allScripts,
|
||||
];
|
||||
}
|
||||
|
||||
function flattenCategories(
|
||||
categories: ReadonlyArray<ICategory>,
|
||||
allCategories: ICategory[],
|
||||
allScripts: IScript[]): IQueryableApplication {
|
||||
if (!categories || categories.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const category of categories) {
|
||||
allCategories.push(category);
|
||||
flattenScripts(category.scripts, allScripts);
|
||||
flattenCategories(category.subCategories, allCategories, allScripts);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenScripts(
|
||||
scripts: ReadonlyArray<IScript>,
|
||||
allScripts: IScript[]): IScript[] {
|
||||
if (!scripts) {
|
||||
return;
|
||||
}
|
||||
for (const script of scripts) {
|
||||
allScripts.push(script);
|
||||
}
|
||||
}
|
||||
|
||||
function makeQueryable(
|
||||
actions: ReadonlyArray<ICategory>): IQueryableApplication {
|
||||
const flattened = flattenApplication(actions);
|
||||
return {
|
||||
allCategories: flattened[0],
|
||||
allScripts: flattened[1],
|
||||
scriptsByLevel: groupByLevel(flattened[1]),
|
||||
};
|
||||
}
|
||||
|
||||
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
|
||||
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
||||
for (const levelName of RecommendationLevelNames) {
|
||||
const level = RecommendationLevel[levelName];
|
||||
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
|
||||
map.set(level, scripts);
|
||||
}
|
||||
return map;
|
||||
function getDuplicates(list: readonly OperatingSystem[]): OperatingSystem[] {
|
||||
return list.filter((os, index) => list.indexOf(os) !== index);
|
||||
}
|
||||
|
||||
168
src/domain/CategoryCollection.ts
Normal file
168
src/domain/CategoryCollection.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { getEnumNames, getEnumValues } from '@/application/Common/Enum';
|
||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import { ICategory } from './ICategory';
|
||||
import { IScript } from './IScript';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
import { IScriptingDefinition } from './IScriptingDefinition';
|
||||
import { ICategoryCollection } from './ICategoryCollection';
|
||||
|
||||
export class CategoryCollection implements ICategoryCollection {
|
||||
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||
|
||||
private readonly queryable: IQueryableCollection;
|
||||
|
||||
constructor(
|
||||
public readonly os: OperatingSystem,
|
||||
public readonly actions: ReadonlyArray<ICategory>,
|
||||
public readonly scripting: IScriptingDefinition) {
|
||||
if (!scripting) {
|
||||
throw new Error('undefined scripting definition');
|
||||
}
|
||||
this.queryable = makeQueryable(actions);
|
||||
ensureValidOs(os);
|
||||
ensureValid(this.queryable);
|
||||
ensureNoDuplicates(this.queryable.allCategories);
|
||||
ensureNoDuplicates(this.queryable.allScripts);
|
||||
}
|
||||
|
||||
public findCategory(categoryId: number): ICategory | undefined {
|
||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
||||
}
|
||||
|
||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||
if (isNaN(level)) {
|
||||
throw new Error('undefined level');
|
||||
}
|
||||
if (!(level in RecommendationLevel)) {
|
||||
throw new Error(`invalid level: ${level}`);
|
||||
}
|
||||
return this.queryable.scriptsByLevel.get(level);
|
||||
}
|
||||
|
||||
public findScript(scriptId: string): IScript | undefined {
|
||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
||||
}
|
||||
|
||||
public getAllScripts(): IScript[] {
|
||||
return this.queryable.allScripts;
|
||||
}
|
||||
|
||||
public getAllCategories(): ICategory[] {
|
||||
return this.queryable.allCategories;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidOs(os: OperatingSystem): void {
|
||||
if (os === undefined) {
|
||||
throw new Error('undefined os');
|
||||
}
|
||||
if (os === OperatingSystem.Unknown) {
|
||||
throw new Error('unknown os');
|
||||
}
|
||||
if (!(os in OperatingSystem)) {
|
||||
throw new Error(`os "${os}" is out of range`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||
const totalOccurrencesById = new Map<TKey, number>();
|
||||
for (const entity of entities) {
|
||||
totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1);
|
||||
}
|
||||
const duplicatedIds = new Array<TKey>();
|
||||
totalOccurrencesById.forEach((index, id) => {
|
||||
if (index > 1) {
|
||||
duplicatedIds.push(id);
|
||||
}
|
||||
});
|
||||
if (duplicatedIds.length > 0) {
|
||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||
throw new Error(
|
||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface IQueryableCollection {
|
||||
allCategories: ICategory[];
|
||||
allScripts: IScript[];
|
||||
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
|
||||
}
|
||||
|
||||
function ensureValid(application: IQueryableCollection) {
|
||||
ensureValidCategories(application.allCategories);
|
||||
ensureValidScripts(application.allScripts);
|
||||
}
|
||||
|
||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||
if (!allCategories || allCategories.length === 0) {
|
||||
throw new Error('must consist of at least one category');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||
if (!allScripts || allScripts.length === 0) {
|
||||
throw new Error('must consist of at least one script');
|
||||
}
|
||||
for (const level of getEnumValues(RecommendationLevel)) {
|
||||
if (allScripts.every((script) => script.level !== level)) {
|
||||
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
||||
const allCategories = new Array<ICategory>();
|
||||
const allScripts = new Array<IScript>();
|
||||
flattenCategories(categories, allCategories, allScripts);
|
||||
return [
|
||||
allCategories,
|
||||
allScripts,
|
||||
];
|
||||
}
|
||||
|
||||
function flattenCategories(
|
||||
categories: ReadonlyArray<ICategory>,
|
||||
allCategories: ICategory[],
|
||||
allScripts: IScript[]): IQueryableCollection {
|
||||
if (!categories || categories.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const category of categories) {
|
||||
allCategories.push(category);
|
||||
flattenScripts(category.scripts, allScripts);
|
||||
flattenCategories(category.subCategories, allCategories, allScripts);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenScripts(
|
||||
scripts: ReadonlyArray<IScript>,
|
||||
allScripts: IScript[]): IScript[] {
|
||||
if (!scripts) {
|
||||
return;
|
||||
}
|
||||
for (const script of scripts) {
|
||||
allScripts.push(script);
|
||||
}
|
||||
}
|
||||
|
||||
function makeQueryable(
|
||||
actions: ReadonlyArray<ICategory>): IQueryableCollection {
|
||||
const flattened = flattenApplication(actions);
|
||||
return {
|
||||
allCategories: flattened[0],
|
||||
allScripts: flattened[1],
|
||||
scriptsByLevel: groupByLevel(flattened[1]),
|
||||
};
|
||||
}
|
||||
|
||||
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
|
||||
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
||||
for (const levelName of getEnumNames(RecommendationLevel)) {
|
||||
const level = RecommendationLevel[levelName];
|
||||
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
|
||||
map.set(level, scripts);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollection } from './ICategoryCollection';
|
||||
import { IProjectInformation } from './IProjectInformation';
|
||||
import { RecommendationLevel } from './RecommendationLevel';
|
||||
import { OperatingSystem } from './OperatingSystem';
|
||||
|
||||
export interface IApplication {
|
||||
readonly info: IProjectInformation;
|
||||
readonly totalScripts: number;
|
||||
readonly totalCategories: number;
|
||||
readonly actions: ReadonlyArray<ICategory>;
|
||||
readonly collections: readonly ICategoryCollection[];
|
||||
|
||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||
findCategory(categoryId: number): ICategory | undefined;
|
||||
findScript(scriptId: string): IScript | undefined;
|
||||
getAllScripts(): ReadonlyArray<IScript>;
|
||||
getAllCategories(): ReadonlyArray<ICategory>;
|
||||
getSupportedOsList(): OperatingSystem[];
|
||||
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
|
||||
}
|
||||
|
||||
export { IScript } from '@/domain/IScript';
|
||||
export { ICategory } from '@/domain/ICategory';
|
||||
|
||||
19
src/domain/ICategoryCollection.ts
Normal file
19
src/domain/ICategoryCollection.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
|
||||
export interface ICategoryCollection {
|
||||
readonly scripting: IScriptingDefinition;
|
||||
readonly os: OperatingSystem;
|
||||
readonly totalScripts: number;
|
||||
readonly totalCategories: number;
|
||||
readonly actions: ReadonlyArray<ICategory>;
|
||||
|
||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||
findCategory(categoryId: number): ICategory | undefined;
|
||||
findScript(scriptId: string): IScript | undefined;
|
||||
getAllScripts(): ReadonlyArray<IScript>;
|
||||
getAllCategories(): ReadonlyArray<ICategory>;
|
||||
}
|
||||
8
src/domain/IScriptingDefinition.ts
Normal file
8
src/domain/IScriptingDefinition.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ScriptingLanguage } from './ScriptingLanguage';
|
||||
|
||||
export interface IScriptingDefinition {
|
||||
readonly fileExtension: string;
|
||||
readonly language: ScriptingLanguage;
|
||||
readonly startCode: string;
|
||||
readonly endCode: string;
|
||||
}
|
||||
@@ -2,10 +2,3 @@ export enum RecommendationLevel {
|
||||
Standard = 0,
|
||||
Strict = 1,
|
||||
}
|
||||
|
||||
export const RecommendationLevelNames = Object
|
||||
.values(RecommendationLevel)
|
||||
.filter((level) => typeof level === 'string') as string[];
|
||||
|
||||
export const RecommendationLevels = RecommendationLevelNames
|
||||
.map((level) => RecommendationLevel[level]) as RecommendationLevel[];
|
||||
|
||||
@@ -2,16 +2,16 @@ import { IScriptCode } from './IScriptCode';
|
||||
|
||||
export class ScriptCode implements IScriptCode {
|
||||
constructor(
|
||||
scriptName: string,
|
||||
public readonly execute: string,
|
||||
public readonly revert: string) {
|
||||
if (!scriptName) {
|
||||
throw new Error('script name is undefined');
|
||||
}
|
||||
validateCode(scriptName, execute);
|
||||
public readonly revert: string,
|
||||
scriptName: string,
|
||||
syntax: ILanguageSyntax) {
|
||||
if (!scriptName) { throw new Error('script name is undefined'); }
|
||||
if (!syntax) { throw new Error('syntax is undefined'); }
|
||||
validateCode(scriptName, execute, syntax);
|
||||
if (revert) {
|
||||
scriptName = `${scriptName} (revert)`;
|
||||
validateCode(scriptName, revert);
|
||||
validateCode(scriptName, revert, syntax);
|
||||
if (execute === revert) {
|
||||
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
||||
}
|
||||
@@ -19,23 +19,28 @@ export class ScriptCode implements IScriptCode {
|
||||
}
|
||||
}
|
||||
|
||||
function validateCode(name: string, code: string): void {
|
||||
export interface ILanguageSyntax {
|
||||
readonly commentDelimiters: string[];
|
||||
readonly commonCodeParts: string[];
|
||||
}
|
||||
|
||||
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||
if (!code || code.length === 0) {
|
||||
throw new Error(`code of ${name} is empty or undefined`);
|
||||
}
|
||||
ensureCodeHasUniqueLines(name, code);
|
||||
ensureNoEmptyLines(name, code);
|
||||
ensureCodeHasUniqueLines(name, code, syntax);
|
||||
}
|
||||
|
||||
function ensureNoEmptyLines(name: string, code: string): void {
|
||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||
throw Error(`Script has empty lines "${name}"`);
|
||||
throw Error(`script has empty lines "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||
const lines = code.split('\n')
|
||||
.filter((line) => mayBeUniqueLine(line));
|
||||
.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||
if (lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -45,13 +50,12 @@ function ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function mayBeUniqueLine(codeLine: string): boolean {
|
||||
const trimmed = codeLine.trim();
|
||||
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
||||
return false;
|
||||
}
|
||||
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||
codeLine = codeLine.toLowerCase();
|
||||
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
||||
const consistsOfFrequentCommands = () => {
|
||||
const trimmed = codeLine.trim().split(' ');
|
||||
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||
};
|
||||
return isCommentLine() || consistsOfFrequentCommands();
|
||||
}
|
||||
|
||||
32
src/domain/ScriptingDefinition.ts
Normal file
32
src/domain/ScriptingDefinition.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ScriptingLanguage } from './ScriptingLanguage';
|
||||
import { IScriptingDefinition } from './IScriptingDefinition';
|
||||
|
||||
export class ScriptingDefinition implements IScriptingDefinition {
|
||||
public readonly fileExtension: string;
|
||||
constructor(
|
||||
public readonly language: ScriptingLanguage,
|
||||
public readonly startCode: string,
|
||||
public readonly endCode: string,
|
||||
) {
|
||||
this.fileExtension = findExtension(language);
|
||||
validateCode(startCode, 'start code');
|
||||
validateCode(endCode, 'end code');
|
||||
}
|
||||
}
|
||||
|
||||
function findExtension(language: ScriptingLanguage): string {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.shellscript:
|
||||
return 'sh';
|
||||
case ScriptingLanguage.batchfile:
|
||||
return 'bat';
|
||||
default:
|
||||
throw new Error(`unsupported language: ${language}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCode(code: string, name: string) {
|
||||
if (!code) {
|
||||
throw new Error(`undefined ${name}`);
|
||||
}
|
||||
}
|
||||
4
src/domain/ScriptingLanguage.ts
Normal file
4
src/domain/ScriptingLanguage.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum ScriptingLanguage {
|
||||
batchfile = 0,
|
||||
shellscript = 1,
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IEventSubscription } from './ISubscription';
|
||||
export interface ISignal<T> {
|
||||
on(handler: (data: T) => void): void;
|
||||
off(handler: (data: T) => void): void;
|
||||
on(handler: EventHandler<T>): IEventSubscription;
|
||||
}
|
||||
|
||||
export type EventHandler<T> = (data: T) => void;
|
||||
|
||||
3
src/infrastructure/Events/ISubscription.ts
Normal file
3
src/infrastructure/Events/ISubscription.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IEventSubscription {
|
||||
unsubscribe(): void;
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
import { ISignal } from './ISignal';
|
||||
export { ISignal };
|
||||
import { EventHandler, ISignal } from './ISignal';
|
||||
import { IEventSubscription } from './ISubscription';
|
||||
|
||||
export class Signal<T> implements ISignal<T> {
|
||||
private handlers: Array<(data: T) => void> = [];
|
||||
private handlers = new Map<number, EventHandler<T>>();
|
||||
|
||||
public on(handler: (data: T) => void): void {
|
||||
this.handlers.push(handler);
|
||||
}
|
||||
|
||||
public off(handler: (data: T) => void): void {
|
||||
this.handlers = this.handlers.filter((h) => h !== handler);
|
||||
public on(handler: EventHandler<T>): IEventSubscription {
|
||||
const id = this.getUniqueEventHandlerId();
|
||||
this.handlers.set(id, handler);
|
||||
return {
|
||||
unsubscribe: () => this.handlers.delete(id),
|
||||
};
|
||||
}
|
||||
|
||||
public notify(data: T) {
|
||||
this.handlers.slice(0).forEach((h) => h(data));
|
||||
for (const handler of Array.from(this.handlers.values())) {
|
||||
handler(data);
|
||||
}
|
||||
}
|
||||
|
||||
private getUniqueEventHandlerId(): number {
|
||||
const id = Math.random();
|
||||
if (this.handlers.has(id)) {
|
||||
return this.getUniqueEventHandlerId();
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fileSaver from 'file-saver';
|
||||
|
||||
export enum FileType {
|
||||
BatchFile,
|
||||
ShellScript,
|
||||
}
|
||||
export class SaveFileDialog {
|
||||
public static saveFile(text: string, fileName: string, type: FileType): void {
|
||||
@@ -11,7 +12,8 @@ export class SaveFileDialog {
|
||||
private static readonly mimeTypes = new Map<FileType, string>([
|
||||
// Some browsers (including firefox + IE) require right mime type
|
||||
// otherwise they ignore extension and save the file as text.
|
||||
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
||||
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
||||
[ FileType.ShellScript, 'text/x-shellscript' ], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||
]);
|
||||
|
||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||
|
||||
55
src/presentation/CodeButtons/Code.vue
Normal file
55
src/presentation/CodeButtons/Code.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<span class="code-wrapper">
|
||||
<span class="dollar">$</span>
|
||||
<code><slot></slot></code>
|
||||
<font-awesome-icon
|
||||
class="copy-button"
|
||||
:icon="['fas', 'copy']"
|
||||
@click="copyCode"
|
||||
v-tooltip.top-center="'Copy'"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
|
||||
@Component
|
||||
export default class Code extends Vue {
|
||||
public copyCode(): void {
|
||||
const code = this.$slots.default[0].text;
|
||||
Clipboard.copyText(code);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
.code-wrapper {
|
||||
white-space: nowrap;
|
||||
justify-content: space-between;
|
||||
font-family: $normal-font;
|
||||
background-color: $slate;
|
||||
color: $light-gray;
|
||||
padding-left: 0.3rem;
|
||||
padding-right: 0.3rem;
|
||||
.dollar {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
user-select: none;
|
||||
}
|
||||
.copy-button {
|
||||
margin-left: 1rem;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
code {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -8,11 +8,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component
|
||||
export default class IconButton extends StatefulVue {
|
||||
export default class IconButton extends Vue {
|
||||
@Prop() public text!: number;
|
||||
@Prop() public iconPrefix!: string;
|
||||
@Prop() public iconName!: string;
|
||||
@@ -21,7 +20,6 @@ export default class IconButton extends StatefulVue {
|
||||
public onClicked() {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
119
src/presentation/CodeButtons/MacOsInstructions.vue
Normal file
119
src/presentation/CodeButtons/MacOsInstructions.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="instructions">
|
||||
<!-- <p>
|
||||
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
||||
</p> -->
|
||||
<p>
|
||||
<ol>
|
||||
<li>
|
||||
<span>Download the file</span>
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="'You should be prompted to save the script file now, otherwise try to download it again'"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span>Open terminal</span>
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="'Type Terminal into Spotlight or open from the Applications -> Utilities folder'"
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<span>Navigate to the folder where you downloaded the file e.g.:</span>
|
||||
<div>
|
||||
<Code>cd ~/Downloads</Code>
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="
|
||||
'Press on Enter/Return key after running the command.<br/>' +
|
||||
'If the file is not downloaded on Downloads folder, change `Downloads` to path where the file is downloaded.<br/>' +
|
||||
'• `cd` will change the current folder.<br/>' +
|
||||
'• `~` is the user home directory.'"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>Give the file execute permissions:</span>
|
||||
<div>
|
||||
<Code>chmod +x {{ this.fileName }}</Code>
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="
|
||||
'Press on Enter/Return key after running the command.<br/>' +
|
||||
'It will make the file executable.'"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>Execute the file:</span>
|
||||
<div>
|
||||
<Code>./{{ this.fileName }}</Code>
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="'Alternatively you can double click on the file'"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<span>If asked, enter your administrator password</span>
|
||||
<font-awesome-icon
|
||||
class="explanation"
|
||||
:icon="['fas', 'info-circle']"
|
||||
v-tooltip.top-center="
|
||||
'Press on Enter/Return key after typing your password<br/>' +
|
||||
'Your password will not be shown by default.<br/>' +
|
||||
'Administor privileges are required to configure OS.'"
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</p>
|
||||
<!-- <p>
|
||||
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
||||
</p> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import Code from './Code.vue';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
Code,
|
||||
},
|
||||
})
|
||||
export default class MacOsInstructions extends StatefulVue {
|
||||
@Prop() public fileName: string;
|
||||
public appName = '';
|
||||
public macOsDownloadUrl = '';
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
this.appName = app.info.name;
|
||||
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
||||
}
|
||||
protected handleCollectionState(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
li {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.explanation {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
161
src/presentation/CodeButtons/TheCodeButtons.vue
Normal file
161
src/presentation/CodeButtons/TheCodeButtons.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="container" v-if="hasCode">
|
||||
<IconButton
|
||||
:text="this.isDesktopVersion ? 'Save' : 'Download'"
|
||||
v-on:click="saveCodeAsync"
|
||||
icon-prefix="fas"
|
||||
:icon-name="this.isDesktopVersion ? 'save' : 'file-download'">
|
||||
</IconButton>
|
||||
<IconButton
|
||||
text="Copy"
|
||||
v-on:click="copyCodeAsync"
|
||||
icon-prefix="fas" icon-name="copy">
|
||||
</IconButton>
|
||||
<modal :name="macOsModalName" height="auto" :scrollable="true" :adaptive="true"
|
||||
v-if="this.isMacOsCollection">
|
||||
<div class="modal">
|
||||
<div class="modal__content">
|
||||
<MacOsInstructions :fileName="this.fileName" />
|
||||
</div>
|
||||
<div class="modal__close-button">
|
||||
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(macOsModalName)"/>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
import IconButton from './IconButton.vue';
|
||||
import MacOsInstructions from './MacOsInstructions.vue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
IconButton,
|
||||
MacOsInstructions,
|
||||
},
|
||||
})
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public readonly macOsModalName = 'macos-instructions';
|
||||
|
||||
public hasCode = false;
|
||||
public isDesktopVersion = Environment.CurrentEnvironment.isDesktop;
|
||||
public isMacOsCollection = false;
|
||||
public fileName = '';
|
||||
|
||||
private codeListener: IEventSubscription;
|
||||
|
||||
public async copyCodeAsync() {
|
||||
const code = await this.getCurrentCodeAsync();
|
||||
Clipboard.copyText(code.current);
|
||||
}
|
||||
public async saveCodeAsync() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
saveCode(this.fileName, context.state);
|
||||
if (this.isMacOsCollection) {
|
||||
this.$modal.show(this.macOsModalName);
|
||||
}
|
||||
}
|
||||
public destroyed() {
|
||||
if (this.codeListener) {
|
||||
this.codeListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.isMacOsCollection = newState.collection.os === OperatingSystem.macOS;
|
||||
this.fileName = buildFileName(newState.collection.scripting);
|
||||
this.react(newState.code);
|
||||
}
|
||||
|
||||
private async getCurrentCodeAsync(): Promise<IApplicationCode> {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const code = context.state.code;
|
||||
return code;
|
||||
}
|
||||
private async react(code: IApplicationCode) {
|
||||
this.hasCode = code.current && code.current.length > 0;
|
||||
if (this.codeListener) {
|
||||
this.codeListener.unsubscribe();
|
||||
}
|
||||
this.codeListener = code.changed.on((newCode) => {
|
||||
this.hasCode = newCode && newCode.code.length > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function saveCode(fileName: string, state: ICategoryCollectionState) {
|
||||
const content = state.code.current;
|
||||
const type = getType(state.collection.scripting.language);
|
||||
SaveFileDialog.saveFile(content, fileName, type);
|
||||
}
|
||||
|
||||
function getType(language: ScriptingLanguage) {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile:
|
||||
return FileType.BatchFile;
|
||||
case ScriptingLanguage.shellscript:
|
||||
return FileType.ShellScript;
|
||||
default:
|
||||
throw new Error('unknown file type');
|
||||
}
|
||||
}
|
||||
function buildFileName(scripting: IScriptingDefinition) {
|
||||
const fileName = 'privacy-script';
|
||||
if (scripting.fileExtension) {
|
||||
return `${fileName}.${scripting.fileExtension}`;
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
.container > * + * {
|
||||
margin-left: 30px;
|
||||
}
|
||||
.modal {
|
||||
font-family: $normal-font;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&__content {
|
||||
width: 100%;
|
||||
margin: 5%;
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
width: auto;
|
||||
font-size: 1.5em;
|
||||
margin-right:0.25em;
|
||||
align-self: flex-start;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -21,6 +21,8 @@ import CardListItem from './CardListItem.vue';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { hasDirective } from './NonCollapsingDirective';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -31,9 +33,7 @@ export default class CardList extends StatefulVue {
|
||||
public categoryIds: number[] = [];
|
||||
public activeCategoryId?: number = null;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.setCategories(state.app.actions);
|
||||
public created() {
|
||||
this.onOutsideOfActiveCardClicked((element) => {
|
||||
if (hasDirective(element)) {
|
||||
return;
|
||||
@@ -41,15 +41,21 @@ export default class CardList extends StatefulVue {
|
||||
this.activeCategoryId = null;
|
||||
});
|
||||
}
|
||||
|
||||
public onSelected(categoryId: number, isExpanded: boolean) {
|
||||
this.activeCategoryId = isExpanded ? categoryId : undefined;
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
this.setCategories(newState.collection.actions);
|
||||
this.activeCategoryId = undefined;
|
||||
}
|
||||
|
||||
private setCategories(categories: ReadonlyArray<ICategory>): void {
|
||||
this.categoryIds = categories.map((category) => category.id);
|
||||
}
|
||||
|
||||
private onOutsideOfActiveCardClicked(callback: (clickedElement: Element) => void) {
|
||||
const outsideClickListener = (event) => {
|
||||
if (!this.activeCategoryId) {
|
||||
|
||||
@@ -49,16 +49,17 @@ export default class CardListItem extends StatefulVue {
|
||||
public isAnyChildSelected = false;
|
||||
public areAllChildrenSelected = false;
|
||||
|
||||
@Emit('selected')
|
||||
public async mounted() {
|
||||
this.updateStateAsync(this.categoryId);
|
||||
}
|
||||
@Emit('selected')
|
||||
public onSelected(isExpanded: boolean) {
|
||||
this.isExpanded = isExpanded;
|
||||
}
|
||||
|
||||
@Watch('activeCategoryId')
|
||||
public async onActiveCategoryChanged(value: |number) {
|
||||
this.isExpanded = value === this.categoryId;
|
||||
}
|
||||
|
||||
@Watch('isExpanded')
|
||||
public async onExpansionChangedAsync(newValue: number, oldValue: number) {
|
||||
if (!oldValue && newValue) {
|
||||
@@ -67,22 +68,21 @@ export default class CardListItem extends StatefulVue {
|
||||
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.selection.changed.on(() => {
|
||||
this.updateStateAsync(this.categoryId);
|
||||
});
|
||||
this.updateStateAsync(this.categoryId);
|
||||
}
|
||||
|
||||
@Watch('categoryId')
|
||||
public async updateStateAsync(value: |number) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
const category = !value ? undefined : state.app.findCategory(this.categoryId);
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const category = !value ? undefined : context.state.collection.findCategory(this.categoryId);
|
||||
this.cardTitle = category ? category.name : undefined;
|
||||
this.isAnyChildSelected = category ? state.selection.isAnySelected(category) : false;
|
||||
this.areAllChildrenSelected = category ? state.selection.areAllSelected(category) : false;
|
||||
const currentSelection = context.state.selection;
|
||||
this.isAnyChildSelected = category ? currentSelection.isAnySelected(category) : false;
|
||||
this.areAllChildrenSelected = category ? currentSelection.areAllSelected(category) : false;
|
||||
}
|
||||
protected initialize(): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(): void {
|
||||
// No need, as categoryId will be updated instead
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Grouping } from './Grouping';
|
||||
|
||||
const DefaultGrouping = Grouping.Cards;
|
||||
|
||||
@Component
|
||||
export default class TheGrouper extends StatefulVue {
|
||||
|
||||
export default class TheGrouper extends Vue {
|
||||
public cardsSelected = false;
|
||||
public noneSelected = false;
|
||||
|
||||
@@ -32,11 +30,9 @@ export default class TheGrouper extends StatefulVue {
|
||||
public mounted() {
|
||||
this.changeGrouping(DefaultGrouping);
|
||||
}
|
||||
|
||||
public groupByCard() {
|
||||
this.changeGrouping(Grouping.Cards);
|
||||
}
|
||||
|
||||
public groupByNone() {
|
||||
this.changeGrouping(Grouping.None);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { IApplication } from './../../../domain/IApplication';
|
||||
import { ICategory, IScript } from '@/domain/ICategory';
|
||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export function parseAllCategories(app: IApplication): INode[] | undefined {
|
||||
export function parseAllCategories(collection: ICategoryCollection): INode[] | undefined {
|
||||
const nodes = new Array<INode>();
|
||||
for (const category of app.actions) {
|
||||
for (const category of collection.actions) {
|
||||
const children = parseCategoryRecursively(category);
|
||||
nodes.push(convertCategoryToNode(category, children));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function parseSingleCategory(categoryId: number, app: IApplication): INode[] | undefined {
|
||||
const category = app.findCategory(categoryId);
|
||||
export function parseSingleCategory(categoryId: number, collection: ICategoryCollection): INode[] | undefined {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id ${categoryId} does not exist`);
|
||||
}
|
||||
|
||||
@@ -15,117 +15,129 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId, getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
||||
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { parseAllCategories, parseSingleCategory, getScriptNodeId,
|
||||
getCategoryNodeId, getCategoryId, getScriptId } from './ScriptNodeParser';
|
||||
import SelectableTree from './SelectableTree/SelectableTree.vue';
|
||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { INodeSelectedEvent } from './SelectableTree/INodeSelectedEvent';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
SelectableTree,
|
||||
},
|
||||
})
|
||||
export default class ScriptsTree extends StatefulVue {
|
||||
@Prop() public categoryId?: number;
|
||||
@Component({
|
||||
components: {
|
||||
SelectableTree,
|
||||
},
|
||||
})
|
||||
export default class ScriptsTree extends StatefulVue {
|
||||
@Prop() public categoryId?: number;
|
||||
|
||||
public nodes?: ReadonlyArray<INode> = null;
|
||||
public selectedNodeIds?: ReadonlyArray<string> = [];
|
||||
public filterText?: string = null;
|
||||
public nodes?: ReadonlyArray<INode> = null;
|
||||
public selectedNodeIds?: ReadonlyArray<string> = [];
|
||||
public filterText?: string = null;
|
||||
|
||||
private filtered?: IFilterResult;
|
||||
private filtered?: IFilterResult;
|
||||
private listeners = new Array<IEventSubscription>();
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
// React to state changes
|
||||
state.selection.changed.on(this.handleSelectionChanged);
|
||||
state.filter.filterRemoved.on(this.handleFilterRemoved);
|
||||
state.filter.filtered.on(this.handleFiltered);
|
||||
// Update initial state
|
||||
await this.initializeNodesAsync(this.categoryId);
|
||||
await this.initializeFilter(state.filter.currentFilter);
|
||||
}
|
||||
|
||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
switch (event.node.type) {
|
||||
case NodeType.Category:
|
||||
toggleCategoryNodeSelection(event, state);
|
||||
break;
|
||||
case NodeType.Script:
|
||||
toggleScriptNodeSelection(event, state);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${event.node.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('categoryId')
|
||||
public async initializeNodesAsync(categoryId?: number) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
if (categoryId) {
|
||||
this.nodes = parseSingleCategory(categoryId, state.app);
|
||||
} else {
|
||||
this.nodes = parseAllCategories(state.app);
|
||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
switch (event.node.type) {
|
||||
case NodeType.Category:
|
||||
toggleCategoryNodeSelection(event, context.state);
|
||||
break;
|
||||
case NodeType.Script:
|
||||
toggleScriptNodeSelection(event, context.state);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown node type: ${event.node.id}`);
|
||||
}
|
||||
this.selectedNodeIds = state.selection.selectedScripts
|
||||
.map((selected) => getScriptNodeId(selected.script));
|
||||
}
|
||||
|
||||
public filterPredicate(node: INode): boolean {
|
||||
return this.filtered.scriptMatches.some(
|
||||
(script: IScript) => node.id === getScriptNodeId(script))
|
||||
|| this.filtered.categoryMatches.some(
|
||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||
}
|
||||
|
||||
private initializeFilter(currentFilter: IFilterResult | undefined) {
|
||||
if (!currentFilter) {
|
||||
this.handleFilterRemoved();
|
||||
} else {
|
||||
this.handleFiltered(currentFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||
this.selectedNodeIds = selectedScripts
|
||||
.map((node) => node.id);
|
||||
}
|
||||
|
||||
private handleFilterRemoved() {
|
||||
this.filterText = '';
|
||||
}
|
||||
|
||||
private handleFiltered(result: IFilterResult) {
|
||||
this.filterText = result.query;
|
||||
this.filtered = result;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCategoryNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||
const categoryId = getCategoryId(event.node.id);
|
||||
if (event.isSelected) {
|
||||
state.selection.addOrUpdateAllInCategory(categoryId, false);
|
||||
@Watch('categoryId', { immediate: true })
|
||||
public async setNodesAsync(categoryId?: number) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
if (categoryId) {
|
||||
this.nodes = parseSingleCategory(categoryId, context.state.collection);
|
||||
} else {
|
||||
state.selection.removeAllInCategory(categoryId);
|
||||
this.nodes = parseAllCategories(context.state.collection);
|
||||
}
|
||||
this.selectedNodeIds = context.state.selection.selectedScripts
|
||||
.map((selected) => getScriptNodeId(selected.script));
|
||||
}
|
||||
public filterPredicate(node: INode): boolean {
|
||||
return this.filtered.scriptMatches.some(
|
||||
(script: IScript) => node.id === getScriptNodeId(script))
|
||||
|| this.filtered.categoryMatches.some(
|
||||
(category: ICategory) => node.id === getCategoryNodeId(category));
|
||||
}
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected async handleCollectionState(newState: ICategoryCollectionState) {
|
||||
this.setCurrentFilter(newState.filter.currentFilter);
|
||||
if (!this.categoryId) {
|
||||
this.nodes = parseAllCategories(newState.collection);
|
||||
}
|
||||
this.unsubscribeAll();
|
||||
this.subscribe(newState);
|
||||
}
|
||||
|
||||
private subscribe(state: ICategoryCollectionState) {
|
||||
this.listeners.push(state.selection.changed.on(this.handleSelectionChanged));
|
||||
this.listeners.push(state.filter.filterRemoved.on(this.handleFilterRemoved));
|
||||
this.listeners.push(state.filter.filtered.on(this.handleFiltered));
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
}
|
||||
private setCurrentFilter(currentFilter: IFilterResult | undefined) {
|
||||
if (!currentFilter) {
|
||||
this.handleFilterRemoved();
|
||||
} else {
|
||||
this.handleFiltered(currentFilter);
|
||||
}
|
||||
}
|
||||
function toggleScriptNodeSelection(event: INodeSelectedEvent, state: IApplicationState): void {
|
||||
const scriptId = getScriptId(event.node.id);
|
||||
const actualToggleState = state.selection.isSelected(scriptId);
|
||||
const targetToggleState = event.isSelected;
|
||||
if (targetToggleState && !actualToggleState) {
|
||||
state.selection.addSelectedScript(scriptId, false);
|
||||
} else if (!targetToggleState && actualToggleState) {
|
||||
state.selection.removeSelectedScript(scriptId);
|
||||
}
|
||||
private handleSelectionChanged(selectedScripts: ReadonlyArray<SelectedScript>): void {
|
||||
this.selectedNodeIds = selectedScripts
|
||||
.map((node) => node.id);
|
||||
}
|
||||
private handleFilterRemoved() {
|
||||
this.filterText = '';
|
||||
}
|
||||
private handleFiltered(result: IFilterResult) {
|
||||
this.filterText = result.query;
|
||||
this.filtered = result;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCategoryNodeSelection(event: INodeSelectedEvent, state: ICategoryCollectionState): void {
|
||||
const categoryId = getCategoryId(event.node.id);
|
||||
if (event.isSelected) {
|
||||
state.selection.addOrUpdateAllInCategory(categoryId, false);
|
||||
} else {
|
||||
state.selection.removeAllInCategory(categoryId);
|
||||
}
|
||||
}
|
||||
function toggleScriptNodeSelection(event: INodeSelectedEvent, state: ICategoryCollectionState): void {
|
||||
const scriptId = getScriptId(event.node.id);
|
||||
const actualToggleState = state.selection.isSelected(scriptId);
|
||||
const targetToggleState = event.isSelected;
|
||||
if (targetToggleState && !actualToggleState) {
|
||||
state.selection.addSelectedScript(scriptId, false);
|
||||
} else if (!targetToggleState && actualToggleState) {
|
||||
state.selection.removeSelectedScript(scriptId);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
import { IReverter } from './Reverter/IReverter';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { INode } from './INode';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { getReverter } from './Reverter/ReverterFactory';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
@Component
|
||||
export default class RevertToggle extends StatefulVue {
|
||||
@@ -26,25 +29,30 @@
|
||||
public isReverted = false;
|
||||
|
||||
private handler: IReverter;
|
||||
private selectionChangeListener: IEventSubscription;
|
||||
|
||||
public async mounted() {
|
||||
await this.onNodeChangedAsync(this.node);
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.updateState(state.selection.selectedScripts);
|
||||
state.selection.changed.on((scripts) => this.updateState(scripts));
|
||||
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.handler = getReverter(node, context.state.collection);
|
||||
}
|
||||
|
||||
@Watch('node') public async onNodeChangedAsync(node: INode) {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.handler = getReverter(node, state.app);
|
||||
}
|
||||
|
||||
public async onRevertToggledAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.handler.selectWithRevertState(this.isReverted, state.selection);
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.handler.selectWithRevertState(this.isReverted, context.state.selection);
|
||||
}
|
||||
|
||||
private updateState(scripts: ReadonlyArray<SelectedScript>) {
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
this.updateStatus(newState.selection.selectedScripts);
|
||||
if (this.selectionChangeListener) {
|
||||
this.selectionChangeListener.unsubscribe();
|
||||
}
|
||||
this.selectionChangeListener = newState.selection.changed.on(
|
||||
(scripts) => this.updateStatus(scripts));
|
||||
}
|
||||
|
||||
private updateStatus(scripts: ReadonlyArray<SelectedScript>) {
|
||||
this.isReverted = this.handler.getState(scripts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { IReverter } from './IReverter';
|
||||
import { getCategoryId } from '../../../ScriptNodeParser';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { ScriptReverter } from './ScriptReverter';
|
||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export class CategoryReverter implements IReverter {
|
||||
private readonly categoryId: number;
|
||||
private readonly scriptReverters: ReadonlyArray<ScriptReverter>;
|
||||
constructor(nodeId: string, app: IApplication) {
|
||||
constructor(nodeId: string, collection: ICategoryCollection) {
|
||||
this.categoryId = getCategoryId(nodeId);
|
||||
this.scriptReverters = getAllSubScriptReverters(this.categoryId, app);
|
||||
this.scriptReverters = getAllSubScriptReverters(this.categoryId, collection);
|
||||
}
|
||||
public getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean {
|
||||
return this.scriptReverters.every((script) => script.getState(selectedScripts));
|
||||
@@ -20,8 +20,8 @@ export class CategoryReverter implements IReverter {
|
||||
}
|
||||
}
|
||||
|
||||
function getAllSubScriptReverters(categoryId: number, app: IApplication) {
|
||||
const category = app.findCategory(categoryId);
|
||||
function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) {
|
||||
const category = collection.findCategory(categoryId);
|
||||
if (!category) {
|
||||
throw new Error(`Category with id "${categoryId}" does not exist`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/IApplicationState';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
|
||||
export interface IReverter {
|
||||
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { INode, NodeType } from '../INode';
|
||||
import { IReverter } from './IReverter';
|
||||
import { ScriptReverter } from './ScriptReverter';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { CategoryReverter } from './CategoryReverter';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
|
||||
export function getReverter(node: INode, app: IApplication): IReverter {
|
||||
export function getReverter(node: INode, collection: ICategoryCollection): IReverter {
|
||||
switch (node.type) {
|
||||
case NodeType.Category:
|
||||
return new CategoryReverter(node.id, app);
|
||||
return new CategoryReverter(node.id, collection);
|
||||
case NodeType.Script:
|
||||
return new ScriptReverter(node.id);
|
||||
default:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IReverter } from './IReverter';
|
||||
import { getScriptId } from '../../../ScriptNodeParser';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/State/IApplicationState';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
|
||||
export class ScriptReverter implements IReverter {
|
||||
private readonly scriptId: string;
|
||||
|
||||
@@ -17,102 +17,121 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import LiquorTree from 'liquor-tree';
|
||||
import Node from './Node/Node.vue';
|
||||
import { INode } from './Node/INode';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
|
||||
import { INodeSelectedEvent } from './/INodeSelectedEvent';
|
||||
import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
|
||||
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
|
||||
import { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import LiquorTree from 'liquor-tree';
|
||||
import Node from './Node/Node.vue';
|
||||
import { INode } from './Node/INode';
|
||||
import { convertExistingToNode, toNewLiquorTreeNode } from './LiquorTree/NodeWrapper/NodeTranslator';
|
||||
import { INodeSelectedEvent } from './/INodeSelectedEvent';
|
||||
import { getNewState } from './LiquorTree/NodeWrapper/NodeStateUpdater';
|
||||
import { LiquorTreeOptions } from './LiquorTree/LiquorTreeOptions';
|
||||
import { FilterPredicate, NodePredicateFilter } from './LiquorTree/NodeWrapper/NodePredicateFilter';
|
||||
import { ILiquorTreeNewNode, ILiquorTreeExistingNode, ILiquorTree, ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
|
||||
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component({
|
||||
components: {
|
||||
LiquorTree,
|
||||
Node,
|
||||
},
|
||||
})
|
||||
export default class SelectableTree extends Vue { // Keep it stateless to make it easier to switch out
|
||||
@Prop() public filterPredicate?: FilterPredicate;
|
||||
@Prop() public filterText?: string;
|
||||
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
|
||||
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
||||
/** Wrapper for Liquor Tree, reveals only abstracted INode for communication */
|
||||
@Component({
|
||||
components: {
|
||||
LiquorTree,
|
||||
Node,
|
||||
},
|
||||
})
|
||||
export default class SelectableTree extends Vue { // Keep it stateless to make it easier to switch out
|
||||
@Prop() public filterPredicate?: FilterPredicate;
|
||||
@Prop() public filterText?: string;
|
||||
@Prop() public selectedNodeIds?: ReadonlyArray<string>;
|
||||
@Prop() public initialNodes?: ReadonlyArray<INode>;
|
||||
|
||||
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
|
||||
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
|
||||
public convertExistingToNode = convertExistingToNode;
|
||||
public initialLiquourTreeNodes?: ILiquorTreeNewNode[] = null;
|
||||
public liquorTreeOptions = new LiquorTreeOptions(new NodePredicateFilter((node) => this.filterPredicate(node)));
|
||||
public convertExistingToNode = convertExistingToNode;
|
||||
|
||||
public mounted() {
|
||||
if (this.initialNodes) {
|
||||
const initialNodes = this.initialNodes.map((node) => toNewLiquorTreeNode(node));
|
||||
if (this.selectedNodeIds) {
|
||||
recurseDown(initialNodes,
|
||||
(node) => node.state = updateState(node.state, node, this.selectedNodeIds));
|
||||
}
|
||||
this.initialLiquourTreeNodes = initialNodes;
|
||||
} else {
|
||||
throw new Error('Initial nodes are null or empty');
|
||||
}
|
||||
if (this.filterText) {
|
||||
this.updateFilterText(this.filterText);
|
||||
}
|
||||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||||
const event: INodeSelectedEvent = {
|
||||
node: convertExistingToNode(node),
|
||||
isSelected: node.states.checked,
|
||||
};
|
||||
this.$emit('nodeSelected', event);
|
||||
return;
|
||||
}
|
||||
|
||||
@Watch('initialNodes', { immediate: true })
|
||||
public async updateNodesAsync(nodes: readonly INode[]) {
|
||||
if (!nodes) {
|
||||
throw new Error('undefined initial nodes');
|
||||
}
|
||||
|
||||
public nodeSelected(node: ILiquorTreeExistingNode) {
|
||||
const event: INodeSelectedEvent = {
|
||||
node: convertExistingToNode(node),
|
||||
isSelected: node.states.checked,
|
||||
};
|
||||
this.$emit('nodeSelected', event);
|
||||
return;
|
||||
const initialNodes = nodes.map((node) => toNewLiquorTreeNode(node));
|
||||
if (this.selectedNodeIds) {
|
||||
recurseDown(initialNodes,
|
||||
(node) => node.state = updateState(node.state, node, this.selectedNodeIds));
|
||||
}
|
||||
|
||||
@Watch('filterText')
|
||||
public updateFilterText(filterText: |string) {
|
||||
const api = this.getLiquorTreeApi();
|
||||
if (!filterText) {
|
||||
api.clearFilter();
|
||||
} else {
|
||||
api.filter('filtered'); // text does not matter, it'll trigger the filterPredicate
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('selectedNodeIds')
|
||||
public setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
|
||||
if (!selectedNodeIds) {
|
||||
throw new Error('SelectedrecurseDown nodes are undefined');
|
||||
}
|
||||
this.getLiquorTreeApi().recurseDown(
|
||||
(node) => node.states = updateState(node.states, node, selectedNodeIds),
|
||||
);
|
||||
}
|
||||
|
||||
private getLiquorTreeApi(): ILiquorTree {
|
||||
if (!this.$refs.treeElement) {
|
||||
throw new Error('Referenced tree element cannot be found. Probably it\'s not rendered?');
|
||||
}
|
||||
return (this.$refs.treeElement as any).tree;
|
||||
this.initialLiquourTreeNodes = initialNodes;
|
||||
const api = await this.getLiquorTreeApiAsync();
|
||||
api.setModel(this.initialLiquourTreeNodes); // as liquor tree is not reactive to data after initialization
|
||||
}
|
||||
@Watch('filterText', { immediate: true })
|
||||
public async updateFilterTextAsync(filterText: |string) {
|
||||
const api = await this.getLiquorTreeApiAsync();
|
||||
if (!filterText) {
|
||||
api.clearFilter();
|
||||
} else {
|
||||
api.filter('filtered'); // text does not matter, it'll trigger the filterPredicate
|
||||
}
|
||||
}
|
||||
|
||||
function updateState(
|
||||
old: ILiquorTreeNodeState,
|
||||
node: ILiquorTreeNode,
|
||||
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
|
||||
return {...old, ...getNewState(node, selectedNodeIds)};
|
||||
@Watch('selectedNodeIds')
|
||||
public async setSelectedStatusAsync(selectedNodeIds: ReadonlyArray<string>) {
|
||||
if (!selectedNodeIds) {
|
||||
throw new Error('SelectedrecurseDown nodes are undefined');
|
||||
}
|
||||
const tree = await this.getLiquorTreeApiAsync();
|
||||
tree.recurseDown(
|
||||
(node) => node.states = updateState(node.states, node, selectedNodeIds),
|
||||
);
|
||||
}
|
||||
|
||||
function recurseDown(
|
||||
nodes: ReadonlyArray<ILiquorTreeNewNode>,
|
||||
handler: (node: ILiquorTreeNewNode) => void) {
|
||||
for (const node of nodes) {
|
||||
handler(node);
|
||||
if (node.children) {
|
||||
recurseDown(node.children, handler);
|
||||
}
|
||||
private async getLiquorTreeApiAsync(): Promise<ILiquorTree> {
|
||||
const accessor = (): ILiquorTree => {
|
||||
const uiElement = this.$refs.treeElement;
|
||||
return uiElement ? (uiElement as any).tree : undefined;
|
||||
};
|
||||
const treeElement = await tryUntilDefinedAsync(accessor, 5, 20); // Wait for it to render
|
||||
if (!treeElement) {
|
||||
throw Error('Referenced tree element cannot be found. Perhaps it\'s not yet rendered?');
|
||||
}
|
||||
return treeElement;
|
||||
}
|
||||
}
|
||||
|
||||
function updateState(
|
||||
old: ILiquorTreeNodeState,
|
||||
node: ILiquorTreeNode,
|
||||
selectedNodeIds: ReadonlyArray<string>): ILiquorTreeNodeState {
|
||||
return {...old, ...getNewState(node, selectedNodeIds)};
|
||||
}
|
||||
function recurseDown(
|
||||
nodes: ReadonlyArray<ILiquorTreeNewNode>,
|
||||
handler: (node: ILiquorTreeNewNode) => void) {
|
||||
for (const node of nodes) {
|
||||
handler(node);
|
||||
if (node.children) {
|
||||
recurseDown(node.children, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function tryUntilDefinedAsync<T>(
|
||||
accessor: () => T | undefined,
|
||||
delayInMs: number, maxTries: number): Promise<T | undefined> {
|
||||
const sleepAsync = () => new Promise(((resolve) => setTimeout(resolve, delayInMs)));
|
||||
let triesLeft = maxTries;
|
||||
let value: T;
|
||||
while (triesLeft !== 0) {
|
||||
value = accessor();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
triesLeft--;
|
||||
await sleepAsync();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,14 +6,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
|
||||
@Component({
|
||||
directives: { NonCollapsing },
|
||||
})
|
||||
export default class SelectableOption extends StatefulVue {
|
||||
export default class SelectableOption extends Vue {
|
||||
@Prop() public enabled: boolean;
|
||||
@Prop() public label: string;
|
||||
@Emit('click') public onClicked() { return; }
|
||||
|
||||
@@ -45,10 +45,11 @@
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import SelectableOption from './SelectableOption.vue';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
enum SelectionState {
|
||||
Standard,
|
||||
@@ -66,56 +67,69 @@ export default class TheSelector extends StatefulVue {
|
||||
public SelectionState = SelectionState;
|
||||
public currentSelection = SelectionState.None;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.updateSelections(state);
|
||||
state.selection.changed.on(() => {
|
||||
this.updateSelections(state);
|
||||
});
|
||||
}
|
||||
public async selectAsync(type: SelectionState): Promise<void> {
|
||||
if (this.currentSelection === type) {
|
||||
return;
|
||||
}
|
||||
const state = await this.getCurrentStateAsync();
|
||||
selectType(state, type);
|
||||
const context = await this.getCurrentContextAsync();
|
||||
selectType(context.state, type);
|
||||
}
|
||||
|
||||
private updateSelections(state: IApplicationState) {
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
this.updateSelections(newState);
|
||||
newState.selection.changed.on(() => this.updateSelections(newState));
|
||||
if (oldState) {
|
||||
oldState.selection.changed.on(() => this.updateSelections(oldState));
|
||||
}
|
||||
}
|
||||
|
||||
private updateSelections(state: ICategoryCollectionState) {
|
||||
this.currentSelection = getCurrentSelectionState(state);
|
||||
}
|
||||
}
|
||||
|
||||
interface ITypeSelector {
|
||||
isSelected: (state: IApplicationState) => boolean;
|
||||
select: (state: IApplicationState) => void;
|
||||
isSelected: (state: ICategoryCollectionState) => boolean;
|
||||
select: (state: ICategoryCollectionState) => void;
|
||||
}
|
||||
|
||||
const selectors = new Map<SelectionState, ITypeSelector>([
|
||||
[SelectionState.None, {
|
||||
select: (state) => state.selection.deselectAll(),
|
||||
isSelected: (state) => state.selection.totalSelected === 0,
|
||||
select: (state) =>
|
||||
state.selection.deselectAll(),
|
||||
isSelected: (state) =>
|
||||
state.selection.totalSelected === 0,
|
||||
}],
|
||||
[SelectionState.Standard, {
|
||||
select: (state) => state.selection.selectOnly(state.app.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||
isSelected: (state) => hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
|
||||
select: (state) =>
|
||||
state.selection.selectOnly(
|
||||
state.collection.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||
isSelected: (state) =>
|
||||
hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
|
||||
}],
|
||||
[SelectionState.Strict, {
|
||||
select: (state) => state.selection.selectOnly(state.app.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||
isSelected: (state) => hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
|
||||
select: (state) =>
|
||||
state.selection.selectOnly(state.collection.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||
isSelected: (state) =>
|
||||
hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
|
||||
}],
|
||||
[SelectionState.All, {
|
||||
select: (state) => state.selection.selectAll(),
|
||||
isSelected: (state) => state.selection.totalSelected === state.app.totalScripts,
|
||||
select: (state) =>
|
||||
state.selection.selectAll(),
|
||||
isSelected: (state) =>
|
||||
state.selection.totalSelected === state.collection.totalScripts,
|
||||
}],
|
||||
]);
|
||||
|
||||
function selectType(state: IApplicationState, type: SelectionState) {
|
||||
function selectType(state: ICategoryCollectionState, type: SelectionState) {
|
||||
const selector = selectors.get(type);
|
||||
selector.select(state);
|
||||
}
|
||||
|
||||
function getCurrentSelectionState(state: IApplicationState): SelectionState {
|
||||
function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState {
|
||||
for (const [type, selector] of Array.from(selectors.entries())) {
|
||||
if (selector.isSelected(state)) {
|
||||
return type;
|
||||
@@ -124,8 +138,8 @@ function getCurrentSelectionState(state: IApplicationState): SelectionState {
|
||||
return SelectionState.Custom;
|
||||
}
|
||||
|
||||
function hasAllSelectedLevelOf(level: RecommendationLevel, state: IApplicationState) {
|
||||
const scripts = state.app.getScriptsByLevel(level);
|
||||
function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) {
|
||||
const scripts = state.collection.getScriptsByLevel(level);
|
||||
const selectedScripts = state.selection.selectedScripts;
|
||||
return areAllSelected(scripts, selectedScripts);
|
||||
}
|
||||
|
||||
74
src/presentation/Scripts/TheOsChanger.vue
Normal file
74
src/presentation/Scripts/TheOsChanger.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div v-for="os in this.allOses" :key="os.name">
|
||||
<span
|
||||
class="name"
|
||||
v-bind:class="{ 'current': currentOs === os.os }"
|
||||
v-on:click="changeOsAsync(os.os)">
|
||||
{{ os.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component
|
||||
export default class TheOsChanger extends StatefulVue {
|
||||
public allOses: Array<{ name: string, os: OperatingSystem }> = [];
|
||||
public currentOs: OperatingSystem = undefined;
|
||||
|
||||
public async changeOsAsync(newOs: OperatingSystem) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
context.changeContext(newOs);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
this.allOses = app.getSupportedOsList()
|
||||
.map((os) => ({ os, name: renderOsName(os) }));
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
this.currentOs = newState.os;
|
||||
this.$forceUpdate(); // v-bind:class is not updated otherwise
|
||||
}
|
||||
}
|
||||
|
||||
function renderOsName(os: OperatingSystem): string {
|
||||
switch (os) {
|
||||
case OperatingSystem.Windows: return 'Windows';
|
||||
case OperatingSystem.macOS: return 'macOS (preview)';
|
||||
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
.container {
|
||||
font-family: $normal-font;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
div + div::before {
|
||||
content: "|";
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.name {
|
||||
&:not(.current) {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
&.current {
|
||||
color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="help-container">
|
||||
<TheSelector />
|
||||
<div class="heading">
|
||||
<TheSelector class="item"/>
|
||||
<TheOsChanger class="item"/>
|
||||
<TheGrouper
|
||||
class="item"
|
||||
v-on:groupingChanged="onGroupingChanged($event)"
|
||||
v-show="!this.isSearching" />
|
||||
v-if="!this.isSearching" />
|
||||
</div>
|
||||
<div class="scripts">
|
||||
<div v-if="!isSearching">
|
||||
@@ -37,70 +39,93 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Grouping } from './Grouping/Grouping';
|
||||
import { IFilterResult } from '@/application/State/Filter/IFilterResult';
|
||||
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
||||
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||
import TheGrouper from '@/presentation/Scripts/Grouping/TheGrouper.vue';
|
||||
import TheOsChanger from '@/presentation/Scripts/TheOsChanger.vue';
|
||||
import TheSelector from '@/presentation/Scripts/Selector/TheSelector.vue';
|
||||
import ScriptsTree from '@/presentation/Scripts/ScriptsTree/ScriptsTree.vue';
|
||||
import CardList from '@/presentation/Scripts/Cards/CardList.vue';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Grouping } from './Grouping/Grouping';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
/** Shows content of single category or many categories */
|
||||
@Component({
|
||||
components: {
|
||||
TheGrouper,
|
||||
TheSelector,
|
||||
ScriptsTree,
|
||||
CardList,
|
||||
/** Shows content of single category or many categories */
|
||||
@Component({
|
||||
components: {
|
||||
TheGrouper,
|
||||
TheSelector,
|
||||
ScriptsTree,
|
||||
CardList,
|
||||
TheOsChanger,
|
||||
},
|
||||
filters: {
|
||||
threeDotsTrim(query: string) {
|
||||
const threshold = 30;
|
||||
if (query.length <= threshold - 3) {
|
||||
return query;
|
||||
}
|
||||
return `${query.substr(0, threshold)}...`;
|
||||
},
|
||||
filters: {
|
||||
threeDotsTrim(query: string) {
|
||||
const threshold = 30;
|
||||
if (query.length <= threshold - 3) {
|
||||
return query;
|
||||
}
|
||||
return `${query.substr(0, threshold)}...`;
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class TheScripts extends StatefulVue {
|
||||
public repositoryUrl = '';
|
||||
public Grouping = Grouping; // Make it accessible from view
|
||||
public currentGrouping = Grouping.Cards;
|
||||
public searchQuery = '';
|
||||
public isSearching = false;
|
||||
public searchHasMatches = false;
|
||||
},
|
||||
})
|
||||
export default class TheScripts extends StatefulVue {
|
||||
public repositoryUrl = '';
|
||||
public Grouping = Grouping; // Make it accessible from view
|
||||
public currentGrouping = Grouping.Cards;
|
||||
public searchQuery = '';
|
||||
public isSearching = false;
|
||||
public searchHasMatches = false;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.repositoryUrl = state.app.info.repositoryWebUrl;
|
||||
state.filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
});
|
||||
state.filter.filtered.on((result: IFilterResult) => {
|
||||
this.searchQuery = result.query;
|
||||
this.isSearching = true;
|
||||
this.searchHasMatches = result.hasAnyMatches();
|
||||
});
|
||||
}
|
||||
private listeners = new Array<IEventSubscription>();
|
||||
|
||||
public async clearSearchQueryAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
state.filter.removeFilter();
|
||||
}
|
||||
|
||||
public onGroupingChanged(group: Grouping) {
|
||||
this.currentGrouping = group;
|
||||
}
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
public async clearSearchQueryAsync() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const filter = context.state.filter;
|
||||
filter.removeFilter();
|
||||
}
|
||||
public onGroupingChanged(group: Grouping) {
|
||||
this.currentGrouping = group;
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
this.repositoryUrl = app.info.repositoryWebUrl;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.unsubscribeAll();
|
||||
this.subscribe(newState);
|
||||
}
|
||||
|
||||
private subscribe(state: ICategoryCollectionState) {
|
||||
this.listeners.push(state.filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
}));
|
||||
state.filter.filtered.on((result: IFilterResult) => {
|
||||
this.searchQuery = result.query;
|
||||
this.isSearching = true;
|
||||
this.searchHasMatches = result.hasAnyMatches();
|
||||
});
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
@import "@/presentation/styles/fonts.scss";
|
||||
|
||||
$inner-margin: 4px;
|
||||
|
||||
.scripts {
|
||||
margin-top:10px;
|
||||
margin-top: $inner-margin;
|
||||
.tree {
|
||||
padding-left: 3%;
|
||||
padding-top: 15px;
|
||||
@@ -149,9 +174,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.help-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.heading {
|
||||
margin-top: $inner-margin;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.item {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 5px 0 5px;
|
||||
&:first-child {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,11 +1,40 @@
|
||||
import { ApplicationState } from '@/application/State/ApplicationState';
|
||||
import { IApplicationState } from '@/application/State/IApplicationState';
|
||||
import { Vue } from 'vue-property-decorator';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { buildContext } from '@/application/Context/ApplicationContextProvider';
|
||||
import { IApplicationContextChangedEvent } from '../application/Context/IApplicationContext';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ICategoryCollectionState } from '../application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '../infrastructure/Events/ISubscription';
|
||||
|
||||
// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
|
||||
@Component
|
||||
export abstract class StatefulVue extends Vue {
|
||||
public isLoading = true;
|
||||
public static instance = new AsyncLazy<IApplicationContext>(
|
||||
() => Promise.resolve(buildContext()));
|
||||
|
||||
protected getCurrentStateAsync(): Promise<IApplicationState> {
|
||||
return ApplicationState.GetAsync();
|
||||
private listener: IEventSubscription;
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.listener = context.contextChanged.on((event) => this.handleStateChangedEvent(event));
|
||||
this.initialize(context.app);
|
||||
this.handleCollectionState(context.state, undefined);
|
||||
}
|
||||
public destroyed() {
|
||||
if (this.listener) {
|
||||
this.listener.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract initialize(app: IApplication): void;
|
||||
protected abstract handleCollectionState(
|
||||
newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined): void;
|
||||
protected getCurrentContextAsync(): Promise<IApplicationContext> {
|
||||
return StatefulVue.instance.getValueAsync();
|
||||
}
|
||||
|
||||
private handleStateChangedEvent(event: IApplicationContextChangedEvent) {
|
||||
this.handleCollectionState(event.newState, event.oldState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,127 @@ import { Component, Prop } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import ace from 'ace-builds';
|
||||
import 'ace-builds/webpack-resolver';
|
||||
import { CodeBuilder } from '@/application/State/Code/Generation/CodeBuilder';
|
||||
import { ICodeChangedEvent } from '@/application/State/Code/Event/ICodeChangedEvent';
|
||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
|
||||
import { CodeBuilderFactory } from '@/application/Context/State/Code/Generation/CodeBuilderFactory';
|
||||
|
||||
const NothingChosenCode =
|
||||
new CodeBuilder()
|
||||
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows')
|
||||
@Component
|
||||
export default class TheCodeArea extends StatefulVue {
|
||||
public readonly editorId = 'codeEditor';
|
||||
|
||||
private editor!: ace.Ace.Editor;
|
||||
private currentMarkerId?: number;
|
||||
private codeListener: IEventSubscription;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
|
||||
public destroyed() {
|
||||
this.unsubscribeCodeListening();
|
||||
this.destroyEditor();
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState): void {
|
||||
this.destroyEditor();
|
||||
this.editor = initializeEditor(this.theme, this.editorId, newState.collection.scripting.language);
|
||||
const appCode = newState.code;
|
||||
this.editor.setValue(appCode.current || getDefaultCode(newState.collection.scripting.language), 1);
|
||||
this.unsubscribeCodeListening();
|
||||
this.subscribe(appCode);
|
||||
}
|
||||
|
||||
private subscribe(appCode: IApplicationCode) {
|
||||
this.codeListener = appCode.changed.on((code) => this.updateCodeAsync(code));
|
||||
}
|
||||
private unsubscribeCodeListening() {
|
||||
if (this.codeListener) {
|
||||
this.codeListener.unsubscribe();
|
||||
}
|
||||
}
|
||||
private async updateCodeAsync(event: ICodeChangedEvent) {
|
||||
this.removeCurrentHighlighting();
|
||||
if (event.isEmpty()) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const defaultCode = getDefaultCode(context.state.collection.scripting.language);
|
||||
this.editor.setValue(defaultCode, 1);
|
||||
return;
|
||||
}
|
||||
this.editor.setValue(event.code, 1);
|
||||
|
||||
if (event.addedScripts && event.addedScripts.length) {
|
||||
this.reactToChanges(event, event.addedScripts);
|
||||
} else if (event.changedScripts && event.changedScripts.length) {
|
||||
this.reactToChanges(event, event.changedScripts);
|
||||
}
|
||||
}
|
||||
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||
const positions = scripts
|
||||
.map((script) => event.getScriptPositionInCode(script));
|
||||
const start = Math.min(
|
||||
...positions.map((position) => position.startLine),
|
||||
);
|
||||
const end = Math.max(
|
||||
...positions.map((position) => position.endLine),
|
||||
);
|
||||
this.scrollToLine(end + 2);
|
||||
this.highlight(start, end);
|
||||
}
|
||||
private highlight(startRow: number, endRow: number) {
|
||||
const AceRange = ace.require('ace/range').Range;
|
||||
this.currentMarkerId = this.editor.session.addMarker(
|
||||
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
|
||||
);
|
||||
}
|
||||
private scrollToLine(row: number) {
|
||||
const column = this.editor.session.getLine(row).length;
|
||||
this.editor.gotoLine(row, column, true);
|
||||
}
|
||||
private removeCurrentHighlighting() {
|
||||
if (!this.currentMarkerId) {
|
||||
return;
|
||||
}
|
||||
this.editor.session.removeMarker(this.currentMarkerId);
|
||||
this.currentMarkerId = undefined;
|
||||
}
|
||||
private destroyEditor() {
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeEditor(theme: string, editorId: string, language: ScriptingLanguage): ace.Ace.Editor {
|
||||
theme = theme || 'github';
|
||||
const editor = ace.edit(editorId);
|
||||
const lang = getLanguage(language);
|
||||
editor.getSession().setMode(`ace/mode/${lang}`);
|
||||
editor.setTheme(`ace/theme/${theme}`);
|
||||
editor.setReadOnly(true);
|
||||
editor.setAutoScrollEditorIntoView(true);
|
||||
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
||||
return editor;
|
||||
}
|
||||
|
||||
function getLanguage(language: ScriptingLanguage) {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile: return 'batchfile';
|
||||
case ScriptingLanguage.shellscript: return 'sh';
|
||||
default:
|
||||
throw new Error('unknown language');
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultCode(language: ScriptingLanguage): string {
|
||||
return new CodeBuilderFactory()
|
||||
.create(language)
|
||||
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows and macOS')
|
||||
.appendLine()
|
||||
.appendCommentLine('-- 🤔 How to use')
|
||||
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
||||
@@ -27,82 +141,6 @@ const NothingChosenCode =
|
||||
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
||||
.appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
|
||||
.toString();
|
||||
|
||||
@Component
|
||||
export default class TheCodeArea extends StatefulVue {
|
||||
public readonly editorId = 'codeEditor';
|
||||
|
||||
private editor!: ace.Ace.Editor;
|
||||
private currentMarkerId?: number;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
|
||||
public async mounted() {
|
||||
this.editor = initializeEditor(this.theme, this.editorId);
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.editor.setValue(state.code.current || NothingChosenCode, 1);
|
||||
state.code.changed.on((code) => this.updateCode(code));
|
||||
}
|
||||
|
||||
private updateCode(event: ICodeChangedEvent) {
|
||||
this.removeCurrentHighlighting();
|
||||
if (event.isEmpty()) {
|
||||
this.editor.setValue(NothingChosenCode, 1);
|
||||
return;
|
||||
}
|
||||
this.editor.setValue(event.code, 1);
|
||||
|
||||
if (event.addedScripts && event.addedScripts.length) {
|
||||
this.reactToChanges(event, event.addedScripts);
|
||||
} else if (event.changedScripts && event.changedScripts.length) {
|
||||
this.reactToChanges(event, event.changedScripts);
|
||||
}
|
||||
}
|
||||
|
||||
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||
const positions = scripts
|
||||
.map((script) => event.getScriptPositionInCode(script));
|
||||
const start = Math.min(
|
||||
...positions.map((position) => position.startLine),
|
||||
);
|
||||
const end = Math.max(
|
||||
...positions.map((position) => position.endLine),
|
||||
);
|
||||
this.scrollToLine(end + 2);
|
||||
this.highlight(start, end);
|
||||
}
|
||||
|
||||
private highlight(startRow: number, endRow: number) {
|
||||
const AceRange = ace.require('ace/range').Range;
|
||||
this.currentMarkerId = this.editor.session.addMarker(
|
||||
new AceRange(startRow, 0, endRow, 0), 'code-area__highlight', 'fullLine',
|
||||
);
|
||||
}
|
||||
|
||||
private scrollToLine(row: number) {
|
||||
const column = this.editor.session.getLine(row).length;
|
||||
this.editor.gotoLine(row, column, true);
|
||||
}
|
||||
|
||||
private removeCurrentHighlighting() {
|
||||
if (!this.currentMarkerId) {
|
||||
return;
|
||||
}
|
||||
this.editor.session.removeMarker(this.currentMarkerId);
|
||||
this.currentMarkerId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeEditor(theme: string, editorId: string): ace.Ace.Editor {
|
||||
const lang = 'batchfile';
|
||||
theme = theme || 'github';
|
||||
const editor = ace.edit(editorId);
|
||||
editor.getSession().setMode(`ace/mode/${lang}`);
|
||||
editor.setTheme(`ace/theme/${theme}`);
|
||||
editor.setReadOnly(true);
|
||||
editor.setAutoScrollEditorIntoView(true);
|
||||
editor.getSession().setUseWrapMode(true); // So code is readable on mobile
|
||||
return editor;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div class="container" v-if="hasCode">
|
||||
</IconButton>
|
||||
<IconButton
|
||||
:text="this.isDesktop ? 'Save' : 'Download'"
|
||||
v-on:click="saveCodeAsync"
|
||||
icon-prefix="fas"
|
||||
:icon-name="this.isDesktop ? 'save' : 'file-download'">
|
||||
</IconButton>
|
||||
<IconButton
|
||||
text="Copy"
|
||||
v-on:click="copyCodeAsync"
|
||||
icon-prefix="fas" icon-name="copy">
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { SaveFileDialog, FileType } from '@/infrastructure/SaveFileDialog';
|
||||
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||
import IconButton from './IconButton.vue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
})
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public hasCode = false;
|
||||
public isDesktop = false;
|
||||
|
||||
public async mounted() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
this.hasCode = state.code.current && state.code.current.length > 0;
|
||||
state.code.changed.on((code) => {
|
||||
this.hasCode = code && code.code.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
public async copyCodeAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
Clipboard.copyText(state.code.current);
|
||||
}
|
||||
|
||||
public async saveCodeAsync() {
|
||||
const state = await this.getCurrentStateAsync();
|
||||
SaveFileDialog.saveFile(state.code.current, 'privacy-script.bat', FileType.BatchFile);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
.container > * + * {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user