Add Windows save instructions UI and fix URL #296

- Add Windows instruction dialog when saving scripts for Windows.
- Fix incorrect macOS download URL given for Linux instructions.
- Refactor UI rendering, eleminating the use of `v-html` and JavaScript
  variables to hold HTML code.
This commit is contained in:
undergroundwires
2024-01-15 22:38:39 +01:00
parent e09db0f1bd
commit 756c736e21
24 changed files with 705 additions and 537 deletions

View File

@@ -137,7 +137,7 @@ For a detailed comparison of features between the desktop and web versions of pr
- **Transparent**. Have full visibility into what the tweaks do as you enable them.
- **Reversible**. Revert if something feels wrong.
- **Accessible**. No need to run any compiled software on your computer with web version.
- **Secure**: Security is a top priority at privacy.sexy with [comprehensive safeguards](./SECURITY.md#application-security) in place.
- **Secure**: Security is a top priority at privacy.sexy with [comprehensive safeguards](./SECURITY.md#security-practices) in place.
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).

View File

@@ -5,8 +5,8 @@
:icon-name="isRunningAsDesktopApplication ? 'floppy-disk' : 'file-arrow-down'"
@click="saveCode"
/>
<ModalDialog v-if="instructions" v-model="areInstructionsVisible">
<InstructionList :data="instructions" />
<ModalDialog v-model="areInstructionsVisible">
<RunInstructions :filename="filename" />
</ModalDialog>
</div>
</template>
@@ -22,14 +22,12 @@ import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
import { Dialog, FileType } from '@/presentation/common/Dialog';
import IconButton from '../IconButton.vue';
import InstructionList from './Instructions/InstructionList.vue';
import { IInstructionListData } from './Instructions/InstructionListData';
import { getInstructions } from './Instructions/InstructionListDataFactory';
import RunInstructions from './RunInstructions/RunInstructions.vue';
export default defineComponent({
components: {
IconButton,
InstructionList,
RunInstructions,
ModalDialog,
},
setup() {
@@ -39,10 +37,6 @@ export default defineComponent({
const areInstructionsVisible = ref(false);
const filename = computed<string>(() => buildFilename(currentState.value.collection.scripting));
const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
currentState.value.collection.os,
filename.value,
));
async function saveCode() {
const { success, error } = await dialog.saveFile(
@@ -59,8 +53,8 @@ export default defineComponent({
return {
isRunningAsDesktopApplication,
instructions,
areInstructionsVisible,
filename,
saveCode,
};
},

View File

@@ -1,28 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IInstructionListData, IInstructionListStep } from '../InstructionListData';
export interface IInstructionsBuilderData {
readonly fileName: string;
}
export type InstructionStepBuilderType = (data: IInstructionsBuilderData) => IInstructionListStep;
export class InstructionsBuilder {
private readonly stepBuilders = new Array<InstructionStepBuilderType>();
constructor(private readonly os: OperatingSystem) {
}
public withStep(stepBuilder: InstructionStepBuilderType) {
this.stepBuilders.push(stepBuilder);
return this;
}
public build(data: IInstructionsBuilderData): IInstructionListData {
return {
operatingSystem: this.os,
steps: this.stepBuilders.map((stepBuilder) => stepBuilder(data)),
};
}
}

View File

@@ -1,92 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from './InstructionsBuilder';
export class LinuxInstructionsBuilder extends InstructionsBuilder {
constructor() {
super(OperatingSystem.Linux);
super
.withStep(() => ({
action: {
instruction: 'Download the file.',
details: 'You should have already been prompted to save the script file.'
+ '<br/>If this was not the case or you did not save the script when prompted,'
+ '<br/>please try to download your script file again.',
},
}))
.withStep(() => ({
action: {
instruction: 'Open terminal.',
details:
'Opening terminal changes based on the distro you run.'
+ '<br/>You may search for "Terminal" in your application launcher to find it.'
+ '<br/>'
+ '<br/>Alternatively use terminal shortcut for your distro if it has one by default:'
+ '<ul>'
+ '<li><code>Ctrl-Alt-T</code>: Ubuntu, CentOS, Linux Mint, Elementary OS, ubermix, Kali…</li>'
+ '<li><code>Super-T</code>: Pop!_OS…</li>'
+ '<li><code>Alt-T</code>: Parrot OS…</li>'
+ '<li><code>Ctrl-Alt-Insert</code>: Bodhi Linux…</li>'
+ '</ul>'
,
},
}))
.withStep(() => ({
action: {
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
},
code: {
instruction: 'cd ~/Downloads',
details: 'Press on <code>enter/return</code> key after running the command.'
+ '<br/>If the file is not downloaded on Downloads folder,'
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
+ '<br/>'
+ '<br/>This command means:'
+ '<ul>'
+ '<li><code>cd</code> will change the current folder.</li>'
+ '<li><code>~</code> is the user home directory.</li>'
+ '</ul>',
},
}))
.withStep((data) => ({
action: {
instruction: 'Give the file execute permissions:',
},
code: {
instruction: `chmod +x ${data.fileName}`,
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
+ 'It will make the file executable. <br/>'
+ 'If you use desktop environment you can alternatively (instead of running the command):'
+ '<ol>'
+ '<li>Locate the file using your file manager.</li>'
+ '<li>Right click on the file, select "Properties".</li>'
+ '<li>Go to "Permissions" and check "Allow executing file as program".</li>'
+ '</ol>'
+ '<br/>These GUI steps and name of options may change depending on your file manager.'
,
},
}))
.withStep((data) => ({
action: {
instruction: 'Execute the file:',
},
code: {
instruction: `./${data.fileName}`,
details:
'If you have desktop environment, instead of running this command you can alternatively:'
+ '<ol>'
+ '<li>Locate the file using your file manager.</li>'
+ '<li>Right click on the file, select "Run as program".</li>'
+ '</ol>'
,
},
}))
.withStep(() => ({
action: {
instruction: 'If asked, enter your administrator password.',
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
+ '<br/>Administrator privileges are required to configure OS.',
},
}));
}
}

View File

@@ -1,70 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from './InstructionsBuilder';
export class MacOsInstructionsBuilder extends InstructionsBuilder {
constructor() {
super(OperatingSystem.macOS);
super
.withStep(() => ({
action: {
instruction: 'Download the file.',
details: 'You should have already been prompted to save the script file.'
+ '<br/>If this was not the case or you did not save the script when prompted,'
+ '<br/>please try to download your script file again.'
,
},
}))
.withStep(() => ({
action: {
instruction: 'Open terminal.',
details: 'Type Terminal into Spotlight or open it from the Applications -> Utilities folder.',
},
}))
.withStep(() => ({
action: {
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
},
code: {
instruction: 'cd ~/Downloads',
details: 'Press on <code>enter/return</code> key after running the command.'
+ '<br/>If the file is not downloaded on Downloads folder,'
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
+ '<br/>'
+ '<br/>This command means:'
+ '<ul>'
+ '<li><code>cd</code> will change the current folder.</li>'
+ '<li><code>~</code> is the user home directory.</li>'
+ '</ul>',
},
}))
.withStep((data) => ({
action: {
instruction: 'Give the file execute permissions:',
},
code: {
instruction: `chmod +x ${data.fileName}`,
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
+ 'It will make the file executable.'
,
},
}))
.withStep((data) => ({
action: {
instruction: 'Execute the file:',
},
code: {
instruction: `./${data.fileName}`,
details: 'Alternatively you can locate the file in <strong>Finder</strong> and double click on it.',
},
}))
.withStep(() => ({
action: {
instruction: 'If asked, enter your administrator password.',
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
+ '<br/>Administrator privileges are required to configure OS.'
,
},
}));
}
}

View File

@@ -1,21 +0,0 @@
<template>
<TooltipWrapper>
<AppIcon icon="circle-info" />
<template #tooltip>
<slot />
</template>
</TooltipWrapper>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
export default defineComponent({
components: {
TooltipWrapper,
AppIcon,
},
});
</script>

View File

@@ -1,119 +0,0 @@
<template>
<div class="instructions">
<p>
You have two alternatives to apply your selection.
</p>
<hr />
<p>
<strong>1. The easy alternative</strong>. Run your script without any manual steps by
<a :href="macOsDownloadUrl">downloading desktop version</a> of {{ appName }} on the
{{ osName }} system you wish to configure, and then click on the Run button. This is
recommended for most users.
</p>
<hr />
<p>
<strong>2. The hard (manual) alternative</strong>. This requires you to do additional manual
steps. If you are unsure how to follow the instructions, tap or hover on information
(<InfoTooltip>Engage with icons like this for extra wisdom!</InfoTooltip>)
icons near the steps, or follow the easy alternative described above.
</p>
<p>
<ol>
<li
v-for="(step, index) in data.steps"
:key="index"
class="step"
>
<div class="step__action">
<span>{{ step.action.instruction }}</span>
<div v-if="step.action.details" class="details-container">
<!-- eslint-disable vue/no-v-html -->
<InfoTooltip><div v-html="step.action.details" /></InfoTooltip>
</div>
</div>
<div v-if="step.code" class="step__code">
<CodeInstruction>{{ step.code.instruction }}</CodeInstruction>
<div v-if="step.code.details" class="details-container">
<!-- eslint-disable vue/no-v-html -->
<InfoTooltip><div v-html="step.code.details" /></InfoTooltip>
</div>
</div>
</li>
</ol>
</p>
</div>
</template>
<script lang="ts">
import {
defineComponent, PropType, computed,
} from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import CodeInstruction from './CodeInstruction.vue';
import { IInstructionListData } from './InstructionListData';
import InfoTooltip from './InfoTooltip.vue';
export default defineComponent({
components: {
CodeInstruction,
InfoTooltip,
},
props: {
data: {
type: Object as PropType<IInstructionListData>,
required: true,
},
},
setup(props) {
const { info } = injectKey((keys) => keys.useApplication);
const appName = computed<string>(() => info.name);
const macOsDownloadUrl = computed<string>(
() => info.getDownloadUrl(OperatingSystem.macOS),
);
const osName = computed<string>(
() => renderOsName(props.data.operatingSystem),
);
return {
appName,
macOsDownloadUrl,
osName,
};
},
});
function renderOsName(os: OperatingSystem): string {
switch (os) {
case OperatingSystem.Windows: return 'Windows';
case OperatingSystem.macOS: return 'macOS';
case OperatingSystem.Linux: return 'Linux';
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
}
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.step {
margin: 10px 0;
&__action {
display: flex;
flex-direction: row;
align-items: center;
}
&__code {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 0.5em;
}
}
.details-container {
margin-left: 0.5em; // Do not style icon itself to ensure correct tooltip alignment
}
</style>

View File

@@ -1,16 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IInstructionListData {
readonly operatingSystem: OperatingSystem;
readonly steps: readonly IInstructionListStep[];
}
export interface IInstructionListStep {
readonly action: IInstructionInfo;
readonly code?: IInstructionInfo;
}
export interface IInstructionInfo {
readonly instruction: string;
readonly details?: string;
}

View File

@@ -1,19 +0,0 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from './Data/InstructionsBuilder';
import { MacOsInstructionsBuilder } from './Data/MacOsInstructionsBuilder';
import { IInstructionListData } from './InstructionListData';
import { LinuxInstructionsBuilder } from './Data/LinuxInstructionsBuilder';
const builders = new Map<OperatingSystem, InstructionsBuilder>([
[OperatingSystem.macOS, new MacOsInstructionsBuilder()],
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
]);
export function getInstructions(
os: OperatingSystem,
fileName: string,
): IInstructionListData | undefined {
return builders
.get(os)
?.build({ fileName });
}

View File

@@ -0,0 +1,31 @@
<template>
<div class="info-container">
<TooltipWrapper>
<AppIcon icon="circle-info" />
<template #tooltip>
<slot />
</template>
</TooltipWrapper>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TooltipWrapper from '@/presentation/components/Shared/TooltipWrapper.vue';
import AppIcon from '@/presentation/components/Shared/Icon/AppIcon.vue';
export default defineComponent({
components: {
TooltipWrapper,
AppIcon,
},
});
</script>
<style scoped lang="scss">
.info-container {
display: inline-block;
margin-left: 0.15em; // Do not style icon itself to ensure correct tooltip alignment
vertical-align: middle;
}
</style>

View File

@@ -0,0 +1,85 @@
<template>
<div class="instructions">
<p>
You have two alternatives to apply your selection.
</p>
<hr />
<p>
<strong>1. The easy alternative</strong>. Run your script without any manual steps by
<a :href="downloadUrl">downloading desktop version</a> of {{ appName }} on the
{{ osName }} system you wish to configure, and then click on the Run button. This is
recommended for most users.
</p>
<hr />
<p>
<strong>2. The hard (manual) alternative</strong>. This requires you to do additional manual
steps. If you are unsure how to follow the instructions, tap or hover on information
<InfoTooltip>Engage with icons like this for extra wisdom!</InfoTooltip>
icons near the steps, or follow the easy alternative described above.
</p>
<p>
<PlatformInstructionSteps :filename="filename" />
</p>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import InfoTooltip from './InfoTooltip.vue';
import PlatformInstructionSteps from './Steps/PlatformInstructionSteps.vue';
export default defineComponent({
components: {
InfoTooltip,
PlatformInstructionSteps,
},
props: {
filename: {
type: String,
required: true,
},
},
setup() {
const { currentState } = injectKey((keys) => keys.useCollectionState);
const { info } = injectKey((keys) => keys.useApplication);
const operatingSystem = computed<OperatingSystem>(() => currentState.value.os);
const appName = computed<string>(() => info.name);
const downloadUrl = computed<string>(
() => info.getDownloadUrl(operatingSystem.value),
);
const osName = computed<string>(
() => renderOsName(operatingSystem.value),
);
return {
appName,
downloadUrl,
osName,
};
},
});
function renderOsName(os: OperatingSystem): string {
switch (os) {
case OperatingSystem.Windows: return 'Windows';
case OperatingSystem.macOS: return 'macOS';
case OperatingSystem.Linux: return 'Linux';
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
}
}
</script>
<style scoped lang="scss">
@use "@/presentation/assets/styles/main" as *;
.step {
margin: 10px 0;
}
</style>

View File

@@ -27,10 +27,10 @@ export default defineComponent({
setup() {
const { copyText } = injectKey((keys) => keys.useClipboard);
const codeElement = shallowRef<HTMLElement | undefined>();
const codeElementRef = shallowRef<HTMLElement | undefined>();
async function copyCode() {
const element = codeElement.value;
const element = codeElementRef.value;
if (!element) {
throw new Error('Code element could not be found.');
}
@@ -43,7 +43,7 @@ export default defineComponent({
return {
copyCode,
codeElement,
codeElement: codeElementRef,
};
},
});
@@ -53,7 +53,7 @@ export default defineComponent({
@use "@/presentation/assets/styles/main" as *;
.code-wrapper {
display:flex;
display: inline-flex;
white-space: nowrap;
justify-content: space-between;
font-family: $font-normal;

View File

@@ -0,0 +1,19 @@
<template>
<li class="step">
<slot />
</li>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'InstructionSteps', // Define component name for empty component for Vue build and ESLint compatibility.
});
</script>
<style scoped lang="scss">
.step {
margin: 10px 0;
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<ol>
<slot />
</ol>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'InstructionSteps', // Define component name for empty component for Vue build and ESLint compatibility.
});
</script>

View File

@@ -0,0 +1,50 @@
<template>
<component
:is="component"
v-if="component"
:filename="filename"
/>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import MacOsInstructions from './Platforms/MacOsInstructions.vue';
import LinuxInstructions from './Platforms/LinuxInstructions.vue';
import WindowsInstructions from './Platforms/WindowsInstructions.vue';
import type { Component } from 'vue';
export default defineComponent({
props: {
filename: {
type: String,
required: true,
},
},
setup() {
const { currentState } = injectKey((keys) => keys.useCollectionState);
const component = computed<Component>(() => getInstructionsComponent(
currentState.value.collection.os,
));
return {
component,
};
},
});
function getInstructionsComponent(operatingSystem: OperatingSystem): Component {
switch (operatingSystem) {
case OperatingSystem.macOS:
return MacOsInstructions;
case OperatingSystem.Linux:
return LinuxInstructions;
case OperatingSystem.Windows:
return WindowsInstructions;
default:
throw new Error(`No instructions for the operating system: ${OperatingSystem[operatingSystem]}`);
}
}
</script>

View File

@@ -0,0 +1,155 @@
<template>
<InstructionSteps>
<InstructionStep>
Download the file.
<InfoTooltip>
<p>
You should have already been prompted to save the script file.
</p>
<p>
If this was not the case or you did not save the script when prompted,
please try to download your script file again.
</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
Open terminal.
<InfoTooltip>
<p>
Opening terminal changes based on the distro you run.
</p>
<p>
You may search for "Terminal" in your application launcher to find it.
</p>
<p>
Alternatively use terminal shortcut for your distro if it has one by default:
<ul>
<li>
<code>Ctrl-Alt-T</code>:
Ubuntu, CentOS, Linux Mint, Elementary OS, ubermix, Kali
</li>
<li>
<code>Super-T</code>: Pop!_OS
</li>
<li>
<code>Alt-T</code>: Parrot OS
</li>
<li>
<code>Ctrl-Alt-Insert</code>: Bodhi Linux
</li>
</ul>
</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
<p>
Navigate to the folder where you downloaded the file e.g.:
</p>
<p>
<CopyableCommand>
cd ~/Downloads
</CopyableCommand>
<InfoTooltip>
<p>
Press on <code>enter/return</code> key after running the command.
</p>
<p>
If the file is not downloaded on Downloads folder,
change <code>Downloads</code> to path where the file is downloaded.
</p>
<p>
This command means:
<ul>
<li><code>cd</code> will change the current folder.</li>
<li><code>~</code> is the user home directory.</li>
</ul>
</p>
</InfoTooltip>
</p>
</InstructionStep>
<InstructionStep>
<p>
Give the file execute permissions:
</p>
<p>
<CopyableCommand>
chmod +x {{ filename }}
</CopyableCommand>
<InfoTooltip>
<p>
Press on <code>enter/return</code> key after running the command.
</p>
<p>
It will make the file executable.
</p>
<p>
If you use desktop environment you can alternatively (instead of running the command):
<ol>
<li>Locate the file using your file manager.</li>
<li>Right click on the file, select "Properties".</li>
<li>Go to "Permissions" and check "Allow executing file as program".</li>
</ol>
</p>
<p>
These GUI steps and name of options may change depending on your file manager.'
</p>
</InfoTooltip>
</p>
</InstructionStep>
<InstructionStep>
<p>
Execute the file:
</p>
<p>
<CopyableCommand>
./{{ filename }}
</CopyableCommand>
<InfoTooltip>
If you have desktop environment, instead of running this command you can alternatively:
<ol>
<li>Locate the file using your file manager.</li>
<li>Right click on the file, select "Run as program".</li>
</ol>
</InfoTooltip>
</p>
</InstructionStep>
<InstructionStep>
If asked, enter your administrator password.
<InfoTooltip>
<p>
As you type, your password will be hidden but the keys are still
registered, so keep typing.
</p>
<p>
Press on <code>enter/return</code> key after typing your password.
</p>
<p>
Administrator privileges are required to configure OS.
</p>
</InfoTooltip>
</InstructionStep>
</InstructionSteps>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import InstructionSteps from '../InstructionSteps.vue';
import InstructionStep from '../InstructionStep.vue';
import InfoTooltip from '../../InfoTooltip.vue';
import CopyableCommand from '../CopyableCommand.vue';
export default defineComponent({
components: {
CopyableCommand,
InfoTooltip,
InstructionSteps,
InstructionStep,
},
props: {
filename: {
type: String,
required: true,
},
},
});
</script>

View File

@@ -0,0 +1,117 @@
<template>
<InstructionSteps>
<InstructionStep>
Download the file.
<InfoTooltip>
<p>
You should have already been prompted to save the script file.
</p>
<p>
If this was not the case or you did not save the script when prompted,
please try to download your script file again.
</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
Open terminal.
<InfoTooltip>
Type Terminal into Spotlight or open it from the Applications -> Utilities folder.
</InfoTooltip>
</InstructionStep>
<InstructionStep>
<p>
Navigate to the folder where you downloaded the file e.g.:
</p>
<p>
<CopyableCommand>
cd ~/Downloads
</CopyableCommand>
<InfoTooltip>
<p>
Press on <code>enter/return</code> key after running the command.
</p>
<p>
If the file is not downloaded on Downloads folder,
change <code>Downloads</code> to path where the file is downloaded.
</p>
<p>
This command means:
<ul>
<li><code>cd</code> will change the current folder.</li>
<li><code>~</code> is the user home directory.</li>
</ul>
</p>
</InfoTooltip>
</p>
</InstructionStep>
<InstructionStep>
<p>
Give the file execute permissions:
</p>
<p>
<CopyableCommand>
chmod +x {{ filename }}
</CopyableCommand>
<InfoTooltip>
<p>
Press on <code>enter/return</code> key after running the command.
</p>
<p>
It will make the file executable.
</p>
</InfoTooltip>
</p>
</InstructionStep>
<InstructionStep>
<p>
Execute the file:
</p>
<p>
<CopyableCommand>
./{{ filename }}
</CopyableCommand>
<InfoTooltip>
Alternatively you can locate the file in <strong>Finder</strong> and double click on it.
</InfoTooltip>
</p>
</InstructionStep>
<InstructionStep>
If asked, enter your administrator password.
<InfoTooltip>
<p>
As you type, your password will be hidden but the keys are
still registered, so keep typing.
</p>
<p>
Press on <code>enter/return</code> key after typing your password.
</p>
<p>
Administrator privileges are required to configure OS.
</p>
</InfoTooltip>
</InstructionStep>
</InstructionSteps>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import InstructionSteps from '../InstructionSteps.vue';
import InstructionStep from '../InstructionStep.vue';
import InfoTooltip from '../../InfoTooltip.vue';
import CopyableCommand from '../CopyableCommand.vue';
export default defineComponent({
components: {
CopyableCommand,
InfoTooltip,
InstructionSteps,
InstructionStep,
},
props: {
filename: {
type: String,
required: true,
},
},
});
</script>

View File

@@ -0,0 +1,154 @@
<template>
<InstructionSteps>
<InstructionStep>
Download the file.
<InfoTooltip>
<p>If a save prompt doesn't appear, try downloading the script again.</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
If warned by your browser, keep the file.
<InfoTooltip>
<!--
Tests (15/01/2023):
- Edge (Defender activated): "filename isn't commonly downloaded..."
- Chrome: No warning, downloads directly.
- Firefox: No warning, downloads directly.
-->
<p>
Browsers may warn you when downloading scripts.
</p>
<p>
privacy.sexy scripts are verified to be safe and are dedicated to securing your privacy.
</p>
<p>
In <strong>Edge</strong>:
<ol>
<li>Select <strong>Keep</strong> from the downloads section.</li>
<li>Click <strong>Show more</strong> on the next warning.</li>
<li>Select <strong>Keep anyway</strong>.</li>
</ol>
</p>
<p>
For <strong>Firefox</strong> and <strong>Chrome</strong>, typically no additional
action is needed.
</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
If your antivirus (e.g., Defender) alerts you, address the warning.
<InfoTooltip>
<!--
Tests (15/01/2023):
- Edge (Defender activated): "Couldn't download - Virus detected"
- Chrome: "Virus detected"
- Firefox: Does not trigger false-positive on Defender somehow.
-->
<p>
Depending on the script, antivirus software may incorrectly flag the download as a threat.
</p>
<p>
These false positives are common for scripts that modify system settings.
</p>
<p>
To handle false warnings in Microsoft Defender:
<ol>
<li>Open <strong>Virus & threat protection</strong> from the Start menu.</li>
<li>
Locate the event in <strong>Protection history</strong>
that pertains to the script.
</li>
<li>In the event details, select <strong>Actions</strong> > <strong>Allow</strong>.</li>
<li>If the script was deleted, please re-download it.</li>
</ol>
</p>
<blockquote>
<strong>Caution:</strong> For your security, remember to:
<ul>
<li>Only allow scripts from trusted sources.</li>
<li>Avoid broad exclusions in your antivirus settings.</li>
<li>Keep real-time protection enabled whenever possible.</li>
</ul>
</blockquote>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
<!--
Tests (15/01/2023):
- Edge (Defender activated): No prompts
- Chrome: No prompts
- Firefox: "filename is executable file. Executable files may contain..?" OK/Cancel
-->
Open the downloaded file.
<InfoTooltip>
<p>
Confirm any browser prompts to open the file.
</p>
<p>
This standard security measure ensures that you are aware of the script execution.
</p>
<p>
<strong>Firefox</strong> users, click <strong>OK</strong> to acknowledge the
executable file warning.
</p>
<p>
<strong>Edge</strong> and <strong>Chrome</strong> users usually will not
encounter additional prompts.
</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
If prompted, confirm SmartScreen warnings.
<InfoTooltip>
<p>
Windows SmartScreen might display a cautionary message.
</p>
<p>
This happens since privacy.sexy scripts are not recognized
by Microsoft's certification process.
</p>
<p>
<ol>
<li>Select <strong>More info</strong>.</li>
<li>Select <strong>Run anyway</strong>.</li>
</ol>
</p>
</InfoTooltip>
</InstructionStep>
<InstructionStep>
If administrative permissions are requested, grant them.
<InfoTooltip>
<p>
The script may request administrative rights to apply changes.
</p>
<p>
This is necessary for the script to apply the intended privacy settings.
</p>
<p>
Click <strong>Yes</strong> to authorize and run the script.
</p>
</InfoTooltip>
</InstructionStep>
</InstructionSteps>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import InstructionSteps from '../InstructionSteps.vue';
import InstructionStep from '../InstructionStep.vue';
import InfoTooltip from '../../InfoTooltip.vue';
export default defineComponent({
components: {
InfoTooltip,
InstructionSteps,
InstructionStep,
},
props: {
filename: {
type: String,
required: true,
},
},
});
</script>

View File

@@ -1,86 +0,0 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListData';
describe('InstructionsBuilder', () => {
describe('withStep', () => {
it('returns itself', () => {
// arrange
const expected = new InstructionsBuilder(OperatingSystem.Android);
const step = () => createMockStep();
// act
const actual = expected.withStep(step);
// assert
expect(actual).to.equal(expected);
});
});
describe('build', () => {
it('builds with given data', () => {
// arrange
const expectedData = createMockData();
const actualData = Array<IInstructionsBuilderData>();
const builder = new InstructionsBuilder(OperatingSystem.Android);
const steps: readonly InstructionStepBuilderType[] = [createMockStep(), createMockStep()]
.map((step) => (data) => {
actualData.push(data);
return step;
});
for (const step of steps) {
builder.withStep(step);
}
// act
builder.build(expectedData);
// assert
expect(actualData.every((data) => data === expectedData));
});
it('builds with every step', () => {
// arrange
const expectedSteps = [
createMockStep('first'),
createMockStep('second'),
createMockStep('third'),
];
const builder = new InstructionsBuilder(OperatingSystem.Android);
const steps: readonly InstructionStepBuilderType[] = expectedSteps.map((step) => () => step);
for (const step of steps) {
builder.withStep(step);
}
// act
const data = builder.build(createMockData());
// assert
const actualSteps = data.steps;
expect(actualSteps).to.have.members(expectedSteps);
});
it('builds with expected OS', () => {
// arrange
const expected = OperatingSystem.Linux;
const sut = new InstructionsBuilder(expected);
// act
const actual = sut.build(createMockData()).operatingSystem;
// assert
expect(true);
expect(actual).to.equal(expected);
});
});
});
function createMockData(): IInstructionsBuilderData {
return {
fileName: 'instructions-file',
};
}
function createMockStep(identifier = 'mock step'): IInstructionListStep {
return {
action: createMockInfo(`${identifier} | action`),
code: createMockInfo(`${identifier} | code`),
};
}
function createMockInfo(identifier = 'mock info'): IInstructionInfo {
return {
instruction: `${identifier} | mock instruction`,
details: `${identifier} | mock details`,
};
}

View File

@@ -1,11 +0,0 @@
import { describe } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { MacOsInstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/MacOsInstructionsBuilder';
import { runOsSpecificInstructionBuilderTests } from './OsSpecificInstructionBuilderTestRunner';
describe('MacOsInstructionsBuilder', () => {
runOsSpecificInstructionBuilderTests({
factory: () => new MacOsInstructionsBuilder(),
os: OperatingSystem.macOS,
});
});

View File

@@ -1,28 +0,0 @@
import { it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
interface ITestData {
readonly factory: () => InstructionsBuilder;
readonly os: OperatingSystem;
}
export function runOsSpecificInstructionBuilderTests(data: ITestData) {
it('builds multiple steps', () => {
// arrange
const sut = data.factory();
// act
const result = sut.build({ fileName: 'test.file' });
// assert
expect(result.steps).to.have.length.greaterThan(0);
});
it(`operatingSystem return ${OperatingSystem[data.os]}`, () => {
// arrange
const expected = data.os;
const sut = data.factory();
// act
const result = sut.build({ fileName: 'test.file' });
// assert
expect(result.operatingSystem).to.equal(expected);
});
}

View File

@@ -1,30 +0,0 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
import { getEnumValues } from '@/application/Common/Enum';
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
import { AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems';
describe('InstructionListDataFactory', () => {
describe('getInstructions', () => {
it('returns expected if os is supported', () => {
// arrange
const fileName = 'test.file';
// act
const actualResults = AllSupportedOperatingSystems.map((os) => getInstructions(os, fileName));
// assert
expect(actualResults.every((result) => result instanceof InstructionsBuilder));
});
it('return undefined if OS is not supported', () => {
// arrange
const expected = undefined;
const fileName = 'test.file';
const unsupportedOses = getEnumValues(OperatingSystem)
.filter((value) => !AllSupportedOperatingSystems.includes(value));
// act
const actualResults = unsupportedOses.map((os) => getInstructions(os, fileName));
// assert
expect(actualResults.every((result) => result === expected));
});
});
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/Instructions/CodeInstruction.vue';
import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue';
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';

View File

@@ -0,0 +1,70 @@
import { shallowMount } from '@vue/test-utils';
import PlatformInstructionSteps from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.vue';
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
import { AllSupportedOperatingSystems, SupportedOperatingSystem } from '@tests/shared/TestCases/SupportedOperatingSystems';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
import WindowsInstructions from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue';
import MacOsInstructions from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue';
import LinuxInstructions from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue';
import type { Component } from 'vue';
describe('PlatformInstructionSteps', () => {
const testScenarios: Record<SupportedOperatingSystem, Component> = {
[OperatingSystem.Windows]: WindowsInstructions,
[OperatingSystem.macOS]: MacOsInstructions,
[OperatingSystem.Linux]: LinuxInstructions,
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
it(`renders the correct component for ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedComponent = testScenarios[operatingSystem];
const useCollectionStateStub = new UseCollectionStateStub()
.withState(new CategoryCollectionStateStub().withOs(operatingSystem));
// act
const wrapper = mountComponent({
useCollectionState: useCollectionStateStub.get(),
});
// assert
expect(wrapper.findComponent(expectedComponent).exists()).to.equal(true);
});
it(`binds the correct filename for ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedFilename = 'expected-file-name.bat';
const wrappedComponent = testScenarios[operatingSystem];
const useCollectionStateStub = new UseCollectionStateStub()
.withState(new CategoryCollectionStateStub().withOs(operatingSystem));
// act
const wrapper = mountComponent({
useCollectionState: useCollectionStateStub.get(),
filename: expectedFilename,
});
// assert
const componentWrapper = wrapper.findComponent(wrappedComponent);
expect(componentWrapper.props('filename')).to.equal(expectedFilename);
});
});
});
function mountComponent(options?: {
readonly useCollectionState?: ReturnType<typeof useCollectionState>;
readonly filename?: string;
}) {
return shallowMount(PlatformInstructionSteps, {
global: {
provide: {
[InjectionKeys.useCollectionState.key]:
() => options?.useCollectionState ?? new UseCollectionStateStub().get(),
},
},
props: {
filename: options?.filename === undefined ? 'privacy-test-script.bat' : options.filename,
},
});
}