add initial macOS support #40
This commit is contained in:
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 context = await this.getCurrentContextAsync();
|
||||
this.setCategories(context.collection.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,24 +68,22 @@ export default class CardListItem extends StatefulVue {
|
||||
(focusElement as HTMLElement).scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
context.state.selection.changed.on(() => {
|
||||
this.updateStateAsync(this.categoryId);
|
||||
});
|
||||
this.updateStateAsync(this.categoryId);
|
||||
}
|
||||
|
||||
@Watch('categoryId')
|
||||
public async updateStateAsync(value: |number) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const category = !value ? undefined : context.collection.findCategory(this.categoryId);
|
||||
const category = !value ? undefined : context.state.collection.findCategory(this.categoryId);
|
||||
this.cardTitle = category ? category.name : undefined;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,123 +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 { 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 { 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() {
|
||||
public async toggleNodeSelectionAsync(event: INodeSelectedEvent) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.beginReactingToStateChanges(context.state);
|
||||
this.setInitialState(context.state);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('categoryId')
|
||||
public async initializeNodesAsync(categoryId?: number) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
if (categoryId) {
|
||||
this.nodes = parseSingleCategory(categoryId, context.collection);
|
||||
} else {
|
||||
this.nodes = parseAllCategories(context.collection);
|
||||
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 = 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));
|
||||
}
|
||||
|
||||
private beginReactingToStateChanges(state: ICategoryCollectionState) {
|
||||
state.selection.changed.on(this.handleSelectionChanged);
|
||||
state.filter.filterRemoved.on(this.handleFilterRemoved);
|
||||
state.filter.filtered.on(this.handleFiltered);
|
||||
}
|
||||
|
||||
private setInitialState(state: ICategoryCollectionState) {
|
||||
this.initializeNodesAsync(this.categoryId);
|
||||
this.initializeFilter(state.filter.currentFilter);
|
||||
}
|
||||
|
||||
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: ICategoryCollectionState): 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: 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);
|
||||
}
|
||||
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">
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
import { INode } from './INode';
|
||||
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,26 +29,30 @@
|
||||
public isReverted = false;
|
||||
|
||||
private handler: IReverter;
|
||||
private selectionChangeListener: IEventSubscription;
|
||||
|
||||
public async mounted() {
|
||||
await this.onNodeChangedAsync(this.node);
|
||||
@Watch('node', {immediate: true}) public async onNodeChangedAsync(node: INode) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const currentSelection = context.state.selection;
|
||||
this.updateState(currentSelection.selectedScripts);
|
||||
currentSelection.changed.on((scripts) => this.updateState(scripts));
|
||||
this.handler = getReverter(node, context.state.collection);
|
||||
}
|
||||
|
||||
@Watch('node') public async onNodeChangedAsync(node: INode) {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.handler = getReverter(node, context.collection);
|
||||
}
|
||||
|
||||
public async onRevertToggledAsync() {
|
||||
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,5 +1,5 @@
|
||||
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/Context/State/ICategoryCollectionState';
|
||||
|
||||
export interface IReverter {
|
||||
getState(selectedScripts: ReadonlyArray<SelectedScript>): boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IReverter } from './IReverter';
|
||||
import { getScriptId } from '../../../ScriptNodeParser';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { IUserSelection } from '@/application/Context/State/ICategoryCollectionState';
|
||||
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; }
|
||||
|
||||
@@ -48,7 +48,8 @@ import SelectableOption from './SelectableOption.vue';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
enum SelectionState {
|
||||
Standard,
|
||||
@@ -66,80 +67,80 @@ export default class TheSelector extends StatefulVue {
|
||||
public SelectionState = SelectionState;
|
||||
public currentSelection = SelectionState.None;
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.updateSelections(context);
|
||||
this.beginReactingToChanges(context);
|
||||
}
|
||||
public async selectAsync(type: SelectionState): Promise<void> {
|
||||
if (this.currentSelection === type) {
|
||||
return;
|
||||
}
|
||||
const context = await this.getCurrentContextAsync();
|
||||
selectType(context, type);
|
||||
selectType(context.state, type);
|
||||
}
|
||||
|
||||
private updateSelections(context: IApplicationContext) {
|
||||
this.currentSelection = getCurrentSelectionState(context);
|
||||
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 beginReactingToChanges(context: IApplicationContext) {
|
||||
context.state.selection.changed.on(() => {
|
||||
this.updateSelections(context);
|
||||
});
|
||||
private updateSelections(state: ICategoryCollectionState) {
|
||||
this.currentSelection = getCurrentSelectionState(state);
|
||||
}
|
||||
}
|
||||
|
||||
interface ITypeSelector {
|
||||
isSelected: (context: IApplicationContext) => boolean;
|
||||
select: (context: IApplicationContext) => void;
|
||||
isSelected: (state: ICategoryCollectionState) => boolean;
|
||||
select: (state: ICategoryCollectionState) => void;
|
||||
}
|
||||
|
||||
const selectors = new Map<SelectionState, ITypeSelector>([
|
||||
[SelectionState.None, {
|
||||
select: (context) =>
|
||||
context.state.selection.deselectAll(),
|
||||
isSelected: (context) =>
|
||||
context.state.selection.totalSelected === 0,
|
||||
select: (state) =>
|
||||
state.selection.deselectAll(),
|
||||
isSelected: (state) =>
|
||||
state.selection.totalSelected === 0,
|
||||
}],
|
||||
[SelectionState.Standard, {
|
||||
select: (context) =>
|
||||
context.state.selection.selectOnly(
|
||||
context.collection.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||
isSelected: (context) =>
|
||||
hasAllSelectedLevelOf(RecommendationLevel.Standard, context),
|
||||
select: (state) =>
|
||||
state.selection.selectOnly(
|
||||
state.collection.getScriptsByLevel(RecommendationLevel.Standard)),
|
||||
isSelected: (state) =>
|
||||
hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
|
||||
}],
|
||||
[SelectionState.Strict, {
|
||||
select: (context) =>
|
||||
context.state.selection.selectOnly(context.collection.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||
isSelected: (context) =>
|
||||
hasAllSelectedLevelOf(RecommendationLevel.Strict, context),
|
||||
select: (state) =>
|
||||
state.selection.selectOnly(state.collection.getScriptsByLevel(RecommendationLevel.Strict)),
|
||||
isSelected: (state) =>
|
||||
hasAllSelectedLevelOf(RecommendationLevel.Strict, state),
|
||||
}],
|
||||
[SelectionState.All, {
|
||||
select: (context) =>
|
||||
context.state.selection.selectAll(),
|
||||
isSelected: (context) =>
|
||||
context.state.selection.totalSelected === context.collection.totalScripts,
|
||||
select: (state) =>
|
||||
state.selection.selectAll(),
|
||||
isSelected: (state) =>
|
||||
state.selection.totalSelected === state.collection.totalScripts,
|
||||
}],
|
||||
]);
|
||||
|
||||
function selectType(context: IApplicationContext, type: SelectionState) {
|
||||
function selectType(state: ICategoryCollectionState, type: SelectionState) {
|
||||
const selector = selectors.get(type);
|
||||
selector.select(context);
|
||||
selector.select(state);
|
||||
}
|
||||
|
||||
function getCurrentSelectionState(context: IApplicationContext): SelectionState {
|
||||
function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState {
|
||||
for (const [type, selector] of Array.from(selectors.entries())) {
|
||||
if (selector.isSelected(context)) {
|
||||
if (selector.isSelected(state)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return SelectionState.Custom;
|
||||
}
|
||||
|
||||
function hasAllSelectedLevelOf(level: RecommendationLevel, context: IApplicationContext) {
|
||||
const scripts = context.collection.getScriptsByLevel(level);
|
||||
const selectedScripts = context.state.selection.selectedScripts;
|
||||
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,72 +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/Context/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 context = await this.getCurrentContextAsync();
|
||||
this.repositoryUrl = context.app.info.repositoryWebUrl;
|
||||
const filter = context.state.filter;
|
||||
filter.filterRemoved.on(() => {
|
||||
this.isSearching = false;
|
||||
});
|
||||
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 context = await this.getCurrentContextAsync();
|
||||
const filter = context.state.filter;
|
||||
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;
|
||||
@@ -151,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,13 +1,40 @@
|
||||
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 {
|
||||
private static instance = new AsyncLazy<IApplicationContext>(
|
||||
public static instance = new AsyncLazy<IApplicationContext>(
|
||||
() => Promise.resolve(buildContext()));
|
||||
|
||||
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,27 +7,14 @@ 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/Context/State/Code/Generation/CodeBuilder';
|
||||
import { ICodeChangedEvent } from '@/application/Context/State/Code/Event/ICodeChangedEvent';
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
|
||||
const NothingChosenCode =
|
||||
new CodeBuilder()
|
||||
.appendCommentLine('privacy.sexy — 🔐 Enforce privacy & security best-practices on Windows')
|
||||
.appendLine()
|
||||
.appendCommentLine('-- 🤔 How to use')
|
||||
.appendCommentLine(' 📙 Start by exploring different categories and choosing different tweaks.')
|
||||
.appendCommentLine(' 📙 On top left, you can apply predefined selections for privacy level you\'d like.')
|
||||
.appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
|
||||
.appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
|
||||
.appendLine()
|
||||
.appendCommentLine('-- 🧐 Why privacy.sexy')
|
||||
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other software on it.')
|
||||
.appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
|
||||
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
||||
.appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
|
||||
.toString();
|
||||
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';
|
||||
|
||||
@Component
|
||||
export default class TheCodeArea extends StatefulVue {
|
||||
@@ -35,21 +22,41 @@ export default class TheCodeArea extends StatefulVue {
|
||||
|
||||
private editor!: ace.Ace.Editor;
|
||||
private currentMarkerId?: number;
|
||||
private codeListener: IEventSubscription;
|
||||
|
||||
@Prop() private theme!: string;
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.editor = initializeEditor(this.theme, this.editorId, context.collection.scripting.language);
|
||||
const appCode = context.state.code;
|
||||
this.editor.setValue(appCode.current || NothingChosenCode, 1);
|
||||
appCode.changed.on((code) => this.updateCode(code));
|
||||
public destroyed() {
|
||||
this.unsubscribeCodeListening();
|
||||
this.destroyEditor();
|
||||
}
|
||||
|
||||
private updateCode(event: ICodeChangedEvent) {
|
||||
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()) {
|
||||
this.editor.setValue(NothingChosenCode, 1);
|
||||
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);
|
||||
@@ -60,7 +67,6 @@ export default class TheCodeArea extends StatefulVue {
|
||||
this.reactToChanges(event, event.changedScripts);
|
||||
}
|
||||
}
|
||||
|
||||
private reactToChanges(event: ICodeChangedEvent, scripts: ReadonlyArray<IScript>) {
|
||||
const positions = scripts
|
||||
.map((script) => event.getScriptPositionInCode(script));
|
||||
@@ -73,19 +79,16 @@ export default class TheCodeArea extends StatefulVue {
|
||||
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;
|
||||
@@ -93,6 +96,11 @@ export default class TheCodeArea extends StatefulVue {
|
||||
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 {
|
||||
@@ -109,13 +117,32 @@ function initializeEditor(theme: string, editorId: string, language: ScriptingLa
|
||||
|
||||
function getLanguage(language: ScriptingLanguage) {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile:
|
||||
return 'batchfile';
|
||||
case ScriptingLanguage.batchfile: return 'batchfile';
|
||||
case ScriptingLanguage.shellscript: return 'sh';
|
||||
default:
|
||||
throw new Error('unkown language');
|
||||
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.')
|
||||
.appendCommentLine(' 📙 On top left, you can apply predefined selections for privacy level you\'d like.')
|
||||
.appendCommentLine(' 📙 After you choose any tweak, you can download or copy to execute your script.')
|
||||
.appendCommentLine(' 📙 Come back regularly to apply latest version for stronger privacy and security.')
|
||||
.appendLine()
|
||||
.appendCommentLine('-- 🧐 Why privacy.sexy')
|
||||
.appendCommentLine(' ✔️ Rich tweak pool to harden security & privacy of the OS and other software on it.')
|
||||
.appendCommentLine(' ✔️ No need to run any compiled software on your system, just run the generated scripts.')
|
||||
.appendCommentLine(' ✔️ Have full visibility into what the tweaks do as you enable them.')
|
||||
.appendCommentLine(' ✔️ Open-source and free (both free as in beer and free as in speech).')
|
||||
.toString();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<div class="container" v-if="hasCode">
|
||||
<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';
|
||||
import { IApplicationCode } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
})
|
||||
export default class TheCodeButtons extends StatefulVue {
|
||||
public hasCode = false;
|
||||
public isDesktop = false;
|
||||
|
||||
public async mounted() {
|
||||
const code = await this.getCurrentCodeAsync();
|
||||
this.hasCode = code.current && code.current.length > 0;
|
||||
code.changed.on((newCode) => {
|
||||
this.hasCode = newCode && newCode.code.length > 0;
|
||||
});
|
||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
}
|
||||
|
||||
public async copyCodeAsync() {
|
||||
const code = await this.getCurrentCodeAsync();
|
||||
Clipboard.copyText(code.current);
|
||||
}
|
||||
|
||||
public async saveCodeAsync() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
saveCode(context);
|
||||
}
|
||||
|
||||
private async getCurrentCodeAsync(): Promise<IApplicationCode> {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const code = context.state.code;
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCode(context: IApplicationContext) {
|
||||
const fileName = `privacy-script.${context.collection.scripting.fileExtension}`;
|
||||
const content = context.state.code.current;
|
||||
const type = getType(context.collection.scripting.language);
|
||||
SaveFileDialog.saveFile(content, fileName, type);
|
||||
}
|
||||
|
||||
function getType(language: ScriptingLanguage) {
|
||||
switch (language) {
|
||||
case ScriptingLanguage.batchfile:
|
||||
return FileType.BatchFile;
|
||||
default:
|
||||
throw new Error('unknown file type');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/presentation/styles/colors.scss";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
.container > * + * {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -13,11 +13,12 @@ import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component
|
||||
export default class DownloadUrlListItem extends StatefulVue {
|
||||
@Prop() public operatingSystem!: OperatingSystem;
|
||||
public OperatingSystem = OperatingSystem;
|
||||
|
||||
public downloadUrl: string = '';
|
||||
public operatingSystemName: string = '';
|
||||
@@ -37,6 +38,13 @@ export default class DownloadUrlListItem extends StatefulVue {
|
||||
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
|
||||
}
|
||||
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
return;
|
||||
}
|
||||
|
||||
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
return context.app.info.getDownloadUrl(os);
|
||||
|
||||
@@ -34,24 +34,22 @@
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component
|
||||
export default class TheFooter extends StatefulVue {
|
||||
export default class PrivacyPolicy extends StatefulVue {
|
||||
public repositoryUrl: string = '';
|
||||
public feedbackUrl: string = '';
|
||||
public isDesktop: boolean = false;
|
||||
public isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const info = context.app.info;
|
||||
protected initialize(app: IApplication): void {
|
||||
const info = app.info;
|
||||
this.repositoryUrl = info.repositoryWebUrl;
|
||||
this.feedbackUrl = info.feedbackUrl;
|
||||
}
|
||||
protected handleCollectionState(): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ import { StatefulVue } from '@/presentation/StatefulVue';
|
||||
import { Environment } from '@/application/Environment/Environment';
|
||||
import PrivacyPolicy from './PrivacyPolicy.vue';
|
||||
import DownloadUrlList from './DownloadUrlList.vue';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -73,15 +75,18 @@ export default class TheFooter extends StatefulVue {
|
||||
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
|
||||
}
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const info = context.app.info;
|
||||
protected initialize(app: IApplication): void {
|
||||
const info = app.info;
|
||||
this.version = info.version;
|
||||
this.homepageUrl = info.homepage;
|
||||
this.repositoryUrl = info.repositoryWebUrl;
|
||||
this.releaseUrl = info.releaseUrl;
|
||||
this.feedbackUrl = info.feedbackUrl;
|
||||
}
|
||||
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<h1 class="child title" >{{ title }}</h1>
|
||||
<h2 class="child subtitle">Enforce privacy & security on Windows</h2>
|
||||
<h2 class="child subtitle">Enforce privacy & security on Windows and macOS</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { Component } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
|
||||
@@ -14,9 +16,11 @@ export default class TheHeader extends StatefulVue {
|
||||
public title = '';
|
||||
public subtitle = '';
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
this.title = context.app.info.name;
|
||||
protected initialize(app: IApplication): void {
|
||||
this.title = app.info.name;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState): void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
import { Component, Watch } from 'vue-property-decorator';
|
||||
import { StatefulVue } from './StatefulVue';
|
||||
import { NonCollapsing } from '@/presentation/Scripts/Cards/NonCollapsingDirective';
|
||||
import { IUserFilter } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IUserFilter } from '@/application/Context/State/Filter/IUserFilter';
|
||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
|
||||
import { IEventSubscription } from '@/infrastructure/Events/ISubscription';
|
||||
|
||||
@Component( {
|
||||
directives: { NonCollapsing },
|
||||
@@ -23,12 +27,7 @@ export default class TheSearchBar extends StatefulVue {
|
||||
public searchPlaceHolder = 'Search';
|
||||
public searchQuery = '';
|
||||
|
||||
public async mounted() {
|
||||
const context = await this.getCurrentContextAsync();
|
||||
const totalScripts = context.collection.totalScripts;
|
||||
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
|
||||
this.beginReacting(context.state.filter);
|
||||
}
|
||||
private readonly listeners = new Array<IEventSubscription>();
|
||||
|
||||
@Watch('searchQuery')
|
||||
public async updateFilterAsync(newFilter: |string) {
|
||||
@@ -40,10 +39,34 @@ export default class TheSearchBar extends StatefulVue {
|
||||
filter.setFilter(newFilter);
|
||||
}
|
||||
}
|
||||
public destroyed() {
|
||||
this.unsubscribeAll();
|
||||
}
|
||||
|
||||
private beginReacting(filter: IUserFilter) {
|
||||
filter.filtered.on((result) => this.searchQuery = result.query);
|
||||
filter.filterRemoved.on(() => this.searchQuery = '');
|
||||
protected initialize(app: IApplication): void {
|
||||
return;
|
||||
}
|
||||
protected handleCollectionState(newState: ICategoryCollectionState, oldState: ICategoryCollectionState | undefined) {
|
||||
const totalScripts = newState.collection.totalScripts;
|
||||
this.searchPlaceHolder = `Search in ${totalScripts} scripts`;
|
||||
this.searchQuery = newState.filter.currentFilter ? newState.filter.currentFilter.query : '';
|
||||
this.unsubscribeAll();
|
||||
this.subscribe(newState.filter);
|
||||
}
|
||||
|
||||
private subscribe(filter: IUserFilter) {
|
||||
this.listeners.push(filter.filtered.on((result) => this.handleFiltered(result)));
|
||||
this.listeners.push(filter.filterRemoved.on(() => this.handleFilterRemoved()));
|
||||
}
|
||||
private unsubscribeAll() {
|
||||
this.listeners.forEach((listener) => listener.unsubscribe());
|
||||
this.listeners.splice(0, this.listeners.length);
|
||||
}
|
||||
private handleFiltered(result: IFilterResult) {
|
||||
this.searchQuery = result.query;
|
||||
}
|
||||
private handleFilterRemoved() {
|
||||
this.searchQuery = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user