restructure presentation layer

- Move most GUI related code to /presentation
- Move components to /components (separate from bootstrap and style)
- Move shared components helpers to /components/shared
- Rename Bootstrapping to bootstrapping to enforce same naming
  convention in /presentation
This commit is contained in:
undergroundwires
2021-03-07 19:33:05 +01:00
parent 646db90585
commit f3c7413f52
67 changed files with 100 additions and 71 deletions

View File

@@ -0,0 +1,4 @@
export enum Grouping {
Cards = 1,
None = 0,
}

View File

@@ -0,0 +1,78 @@
<template>
<div class="container">
<span class="part">Group by:</span>
<span class="part">
<span
class="part"
v-bind:class="{ 'disabled': cardsSelected, 'enabled': !cardsSelected}"
@click="groupByCard()">Cards</span>
<span class="part">|</span>
<span class="part"
v-bind:class="{ 'disabled': noneSelected, 'enabled': !noneSelected}"
@click="groupByNone()">None</span>
</span>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Grouping } from './Grouping';
const DefaultGrouping = Grouping.Cards;
@Component
export default class TheGrouper extends Vue {
public cardsSelected = false;
public noneSelected = false;
private currentGrouping: Grouping;
public mounted() {
this.changeGrouping(DefaultGrouping);
}
public groupByCard() {
this.changeGrouping(Grouping.Cards);
}
public groupByNone() {
this.changeGrouping(Grouping.None);
}
private changeGrouping(newGrouping: Grouping) {
if (this.currentGrouping === newGrouping) {
return;
}
this.currentGrouping = newGrouping;
this.cardsSelected = newGrouping === Grouping.Cards;
this.noneSelected = newGrouping === Grouping.None;
this.$emit('groupingChanged', this.currentGrouping);
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
font-family: $normal-font;
.part {
display: flex;
margin-right:5px;
}
}
.enabled {
cursor: pointer;
&:hover {
font-weight:bold;
text-decoration:underline;
}
}
.disabled {
color:$gray;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<span
v-bind:class="{ 'disabled': enabled, 'enabled': !enabled}"
v-non-collapsing
@click="onClicked()">{{label}}</span>
</template>
<script lang="ts">
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
import { NonCollapsing } from '@/presentation/components/Scripts/Cards/NonCollapsingDirective';
@Component({
directives: { NonCollapsing },
})
export default class SelectableOption extends Vue {
@Prop() public enabled: boolean;
@Prop() public label: string;
@Emit('click') public onClicked() { return; }
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
.enabled {
cursor: pointer;
&:hover {
font-weight:bold;
text-decoration:underline;
}
}
.disabled {
color:$gray;
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<div class="container">
<div class="part">Select:</div>
<div class="part">
<div class="part">
<SelectableOption
label="None"
:enabled="this.currentSelection == SelectionState.None"
@click="selectAsync(SelectionState.None)"
v-tooltip=" 'Deselect all selected scripts.<br/>' +
'💡 Good start to dive deeper into tweaks and select only what you want.'"
/>
</div>
<div class="part"> | </div>
<div class="part">
<SelectableOption
label="Standard"
:enabled="this.currentSelection == SelectionState.Standard"
@click="selectAsync(SelectionState.Standard)"
v-tooltip=" '🛡️ Balanced for privacy and functionality.<br/>' +
'OS and applications will function normally.<br/>' +
'💡 Recommended for everyone'"
/>
</div>
<div class="part"> | </div>
<div class="part">
<SelectableOption
label="Strict"
:enabled="this.currentSelection == SelectionState.Strict"
@click="selectAsync(SelectionState.Strict)"
v-tooltip=" '🚫 Stronger privacy, disables risky functions that may leak your data.<br/>' +
'⚠️ Double check to remove sripts where you would trade functionality for privacy<br/>' +
'💡 Recommended for daily users that prefers more privacy over non-essential functions'"
/>
</div>
<div class="part"> | </div>
<div class="part">
<SelectableOption
label="All"
:enabled="this.currentSelection == SelectionState.All"
@click="selectAsync(SelectionState.All)"
v-tooltip=" '🔒 Strongest privacy, disabling any functionality that may leak your data.<br/>' +
'🛑 Not designed for daily users, it will break important functionalities.<br/>' +
'💡 Only recommended for extreme use-cases like crime labs where no leak is acceptable'"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import SelectableOption from './SelectableOption.vue';
import { IScript } from '@/domain/IScript';
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
enum SelectionState {
Standard,
Strict,
All,
None,
Custom,
}
@Component({
components: {
SelectableOption,
},
})
export default class TheSelector extends StatefulVue {
public SelectionState = SelectionState;
public currentSelection = SelectionState.None;
public async selectAsync(type: SelectionState): Promise<void> {
if (this.currentSelection === type) {
return;
}
const context = await this.getCurrentContextAsync();
selectType(context.state, type);
}
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: 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,
}],
[SelectionState.Standard, {
select: (state) =>
state.selection.selectOnly(
state.collection.getScriptsByLevel(RecommendationLevel.Standard)),
isSelected: (state) =>
hasAllSelectedLevelOf(RecommendationLevel.Standard, state),
}],
[SelectionState.Strict, {
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.collection.totalScripts,
}],
]);
function selectType(state: ICategoryCollectionState, type: SelectionState) {
const selector = selectors.get(type);
selector.select(state);
}
function getCurrentSelectionState(state: ICategoryCollectionState): SelectionState {
for (const [type, selector] of Array.from(selectors.entries())) {
if (selector.isSelected(state)) {
return type;
}
}
return SelectionState.Custom;
}
function hasAllSelectedLevelOf(level: RecommendationLevel, state: ICategoryCollectionState) {
const scripts = state.collection.getScriptsByLevel(level);
const selectedScripts = state.selection.selectedScripts;
return areAllSelected(scripts, selectedScripts);
}
function areAllSelected(
expectedScripts: ReadonlyArray<IScript>,
selection: ReadonlyArray<SelectedScript>): boolean {
selection = selection.filter((selected) => !selected.revert);
if (expectedScripts.length < selection.length) {
return false;
}
const selectedScriptIds = selection.map((script) => script.id).sort();
const expectedScriptIds = expectedScripts.map((script) => script.id).sort();
return selectedScriptIds.every((id, index) => id === expectedScriptIds[index]);
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
.part {
display: flex;
margin-right:5px;
}
font-family: $normal-font;
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="container">
<!-- <div>OS:</div> -->
<div class="os-list">
<div v-for="os in this.allOses" :key="os.name">
<span
class="os-name"
v-bind:class="{ 'current': currentOs === os.os }"
v-on:click="changeOsAsync(os.os)">
{{ os.name }}
</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component
export default class TheOsChanger extends StatefulVue {
public allOses: Array<{ name: string, os: OperatingSystem }> = [];
public currentOs: OperatingSystem = OperatingSystem.Unknown;
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();
this.allOses = app.getSupportedOsList()
.map((os) => ({ os, name: renderOsName(os) }));
}
public async changeOsAsync(newOs: OperatingSystem) {
const context = await this.getCurrentContextAsync();
context.changeContext(newOs);
}
protected handleCollectionState(newState: 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';
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;
.os-list {
display: flex;
margin-left: 0.25rem;
div + div::before {
content: "|";
margin-left: 0.5rem;
}
.os-name {
&:not(.current) {
cursor: pointer;
&:hover {
font-weight: bold;
text-decoration: underline;
}
}
&.current {
color: $gray;
}
}
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div id="container">
<TheSelector class="item" />
<TheOsChanger class="item" />
<TheGrouper
class="item"
v-on:groupingChanged="$emit('groupingChanged', $event)"
v-if="!this.isSearching" />
</div>
</template>
<script lang="ts">
import { Component } from 'vue-property-decorator';
import TheOsChanger from './TheOsChanger.vue';
import TheSelector from './Selector/TheSelector.vue';
import TheGrouper from './Grouping/TheGrouper.vue';
import { StatefulVue } from '@/presentation/components/Shared/StatefulVue';
import { ICategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
@Component({
components: {
TheSelector,
TheOsChanger,
TheGrouper,
},
})
export default class TheScriptsMenu extends StatefulVue {
public isSearching = false;
private listeners = new Array<IEventSubscription>();
public destroyed() {
this.unsubscribeAll();
}
protected initialize(): void {
return;
}
protected handleCollectionState(newState: ICategoryCollectionState): void {
this.subscribe(newState);
}
private subscribe(state: ICategoryCollectionState) {
this.listeners.push(state.filter.filterRemoved.on(() => {
this.isSearching = false;
}));
state.filter.filtered.on(() => {
this.isSearching = true;
});
}
private unsubscribeAll() {
this.listeners.forEach((listener) => listener.unsubscribe());
this.listeners.splice(0, this.listeners.length);
}
}
</script>
<style scoped lang="scss">
#container {
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>