support for desktop versions #20

This commit is contained in:
undergroundwires
2020-07-25 21:26:01 +01:00
parent 4ff4b52202
commit 04b9b59e14
33 changed files with 3919 additions and 132 deletions

View File

@@ -15,7 +15,7 @@
import { Component, Vue, Prop } from 'vue-property-decorator';
import { ApplicationState } from '@/application/State/ApplicationState';
import TheHeader from '@/presentation/TheHeader.vue';
import TheFooter from '@/presentation/TheFooter.vue';
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
import TheCodeArea from '@/presentation/TheCodeArea.vue';
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
import TheSearchBar from '@/presentation/TheSearchBar.vue';

View File

@@ -0,0 +1,54 @@
import { OperatingSystem } from '../OperatingSystem';
import { DetectorBuilder } from './DetectorBuilder';
import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem {
if (!userAgent) {
return OperatingSystem.Unknown;
}
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== OperatingSystem.Unknown) {
return os;
}
}
return OperatingSystem.Unknown;
}
}
// Reference: https://github.com/keithws/browser-report/blob/master/index.js#L304
const BrowserDetectors =
[
define(OperatingSystem.KaiOS, (b) =>
b.mustInclude('KAIOS')),
define(OperatingSystem.ChromeOS, (b) =>
b.mustInclude('CrOS')),
define(OperatingSystem.BlackBerryOS, (b) =>
b.mustInclude('BlackBerry')),
define(OperatingSystem.BlackBerryTabletOS, (b) =>
b.mustInclude('RIM Tablet OS')),
define(OperatingSystem.BlackBerry, (b) =>
b.mustInclude('BB10')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Android').mustNotInclude('Windows Phone')),
define(OperatingSystem.Android, (b) =>
b.mustInclude('Adr').mustNotInclude('Windows Phone')),
define(OperatingSystem.iOS, (b) =>
b.mustInclude('like Mac OS X')),
define(OperatingSystem.Linux, (b) =>
b.mustInclude('Linux').mustNotInclude('Android').mustNotInclude('Adr')),
define(OperatingSystem.Windows, (b) =>
b.mustInclude('Windows').mustNotInclude('Windows Phone')),
define(OperatingSystem.WindowsPhone, (b) =>
b.mustInclude('Windows Phone')),
define(OperatingSystem.macOS, (b) =>
b.mustInclude('OS X').mustNotInclude('Android').mustNotInclude('like Mac OS X')),
];
function define(os: OperatingSystem, applyRules: (builder: DetectorBuilder) => DetectorBuilder): IBrowserOsDetector {
const builder = new DetectorBuilder(os);
applyRules(builder);
return builder.build();
}

View File

@@ -0,0 +1,49 @@
import { IBrowserOsDetector } from './IBrowserOsDetector';
import { OperatingSystem } from '../OperatingSystem';
export class DetectorBuilder {
private readonly existingPartsInUserAgent = new Array<string>();
private readonly notExistingPartsInUserAgent = new Array<string>();
constructor(private readonly os: OperatingSystem) { }
public mustInclude(str: string): DetectorBuilder {
if (!str) {
throw new Error('part to include is empty or undefined');
}
this.existingPartsInUserAgent.push(str);
return this;
}
public mustNotInclude(str: string): DetectorBuilder {
if (!str) {
throw new Error('part to not include is empty or undefined');
}
this.notExistingPartsInUserAgent.push(str);
return this;
}
public build(): IBrowserOsDetector {
if (!this.existingPartsInUserAgent.length) {
throw new Error('Must include at least a part');
}
return {
detect: (userAgent) => {
if (!userAgent) {
throw new Error('User agent is null or undefined');
}
for (const exitingPart of this.existingPartsInUserAgent) {
if (!userAgent.includes(exitingPart)) {
return OperatingSystem.Unknown;
}
}
for (const notExistingPart of this.notExistingPartsInUserAgent) {
if (userAgent.includes(notExistingPart)) {
return OperatingSystem.Unknown;
}
}
return this.os;
},
};
}
}

View File

@@ -0,0 +1,5 @@
import { OperatingSystem } from '../OperatingSystem';
export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem;
}

View File

@@ -0,0 +1,80 @@
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
import { IEnvironment } from './IEnvironment';
import { OperatingSystem } from './OperatingSystem';
interface IEnvironmentVariables {
readonly window: Window & typeof globalThis;
readonly process: NodeJS.Process;
readonly navigator: Navigator;
}
export class Environment implements IEnvironment {
public static readonly CurrentEnvironment: IEnvironment = new Environment({
window,
process,
navigator,
});
public readonly isDesktop: boolean;
public readonly os: OperatingSystem;
protected constructor(
variables: IEnvironmentVariables,
browserOsDetector: IBrowserOsDetector = new BrowserOsDetector()) {
if (!variables) {
throw new Error('variables is null or empty');
}
this.isDesktop = isDesktop(variables);
this.os = this.isDesktop ?
getDesktopOsType(getProcessPlatform(variables))
: browserOsDetector.detect(getUserAgent(variables));
}
}
function getUserAgent(variables: IEnvironmentVariables): string {
if (!variables.window || !variables.window.navigator) {
return undefined;
}
return variables.window.navigator.userAgent;
}
function getProcessPlatform(variables: IEnvironmentVariables): string {
if (!variables.process || !variables.process.platform) {
return undefined;
}
return variables.process.platform;
}
function getDesktopOsType(processPlatform: string): OperatingSystem {
// https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') {
return OperatingSystem.macOS;
} else if (processPlatform === 'win32') {
return OperatingSystem.Windows;
} else if (processPlatform === 'linux') {
return OperatingSystem.Linux;
}
return OperatingSystem.Unknown;
}
function isDesktop(variables: IEnvironmentVariables): boolean {
// More: https://github.com/electron/electron/issues/2288
// Renderer process
if (variables.window
&& variables.window.process
&& variables.window.process.type === 'renderer') {
return true;
}
// Main process
if (variables.process
&& variables.process.versions
&& Boolean(variables.process.versions.electron)) {
return true;
}
// Detect the user agent when the `nodeIntegration` option is set to true
if (variables.navigator
&& variables.navigator.userAgent
&& variables.navigator.userAgent.includes('Electron')) {
return true;
}
return false;
}

View File

@@ -0,0 +1,6 @@
import { OperatingSystem } from './OperatingSystem';
export interface IEnvironment {
isDesktop: boolean;
os: OperatingSystem;
}

View File

@@ -0,0 +1,14 @@
export enum OperatingSystem {
macOS,
Windows,
Linux,
KaiOS,
ChromeOS,
BlackBerryOS,
BlackBerry,
BlackBerryTabletOS,
Android,
iOS,
WindowsPhone,
Unknown,
}

133
src/background.ts Normal file
View File

@@ -0,0 +1,133 @@
'use strict';
import { app, protocol, BrowserWindow, shell } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
import path from 'path';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
const isDevelopment = process.env.NODE_ENV !== 'production';
declare const __static: string; // https://github.com/electron-userland/electron-webpack/issues/172
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win: BrowserWindow | null;
// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } },
]);
// Setup logging
autoUpdater.logger = log; // https://www.electron.build/auto-update#debugging
log.transports.file.level = 'silly';
if (!process.env.IS_TEST) {
Object.assign(console, log.functions); // override console.log, console.warn etc.
}
function createWindow() {
// Create the browser window.
win = new BrowserWindow({
width: 1350,
height: 1005,
webPreferences: {
// Use pluginOptions.nodeIntegration, leave this alone
// See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
nodeIntegration: (process.env
.ELECTRON_NODE_INTEGRATION as unknown) as boolean,
},
// https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#icons
icon: path.join(__static, `favicon.ico`),
});
win.setMenuBarVisibility(false);
configureExternalsUrlsOpenBrowser(win);
loadApplication(win);
win.on('closed', () => {
win = null;
});
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
// Install Vue Devtools
try {
await installExtension(VUEJS_DEVTOOLS);
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString()); // tslint:disable-line:no-console
}
}
createWindow();
});
// See electron-builder issue "checkForUpdatesAndNotify updates but does not notify on Windows 10"
// https://github.com/electron-userland/electron-builder/issues/2700
// https://github.com/electron/electron/issues/10864
if (process.platform === 'win32') {
// https://docs.microsoft.com/en-us/windows/win32/shell/appid#how-to-form-an-application-defined-appusermodelid
app.setAppUserModelId('Undergroundwires.PrivacySexy');
}
// Exit cleanly on request from parent process in development mode.
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit();
}
});
} else {
process.on('SIGTERM', () => {
app.quit();
});
}
}
function loadApplication(window: BrowserWindow) {
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL as string);
if (!process.env.IS_TEST) {
win.webContents.openDevTools();
}
} else {
createProtocol('app');
// Load the index.html when not in development
win.loadURL('app://./index.html');
// tslint:disable-next-line:max-line-length
autoUpdater.checkForUpdatesAndNotify(); // https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/recipes.html#check-for-updates-in-background-js-ts
}
}
function configureExternalsUrlsOpenBrowser(window: BrowserWindow) {
window.webContents.on('new-window', (event, url) => { // handle redirect
if (url !== win.webContents.getURL()) {
event.preventDefault();
shell.openExternal(url);
}
});
}

View File

@@ -0,0 +1,51 @@
<template>
<span v-bind:class="{ 'unsupported': !hasCurrentOsDesktopVersion, 'supported': hasCurrentOsDesktopVersion }">
For desktop:
<span class="urls">
<span class="urls__url" v-for="os of supportedDesktops" v-bind:key="os">
<DownloadUrlListItem :operatingSystem="os" />
</span>
</span>
</span>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
import DownloadUrlListItem from './DownloadUrlListItem.vue';
@Component({
components: { DownloadUrlListItem },
})
export default class DownloadUrlList extends Vue {
public readonly supportedDesktops: ReadonlyArray<OperatingSystem>;
public readonly hasCurrentOsDesktopVersion: boolean = false;
constructor() {
super();
const supportedOperativeSystems = [OperatingSystem.Windows, OperatingSystem.Linux, OperatingSystem.macOS];
const currentOs = Environment.CurrentEnvironment.os;
this.supportedDesktops = supportedOperativeSystems.sort((os) => os === currentOs ? 0 : 1);
this.hasCurrentOsDesktopVersion = supportedOperativeSystems.includes(currentOs);
}
}
</script>
<style scoped lang="scss">
.unsupported {
font-size: 0.85em;
}
.supported {
font-size: 1em;
}
.urls {
&__url {
&:not(:first-child)::before {
content: "|";
font-size: 0.6rem;
padding: 0 5px;
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<span class="url">
<a :href="downloadUrl"
v-bind:class="{
'url__active': hasCurrentOsDesktopVersion && isCurrentOs,
'url__inactive': hasCurrentOsDesktopVersion && !isCurrentOs,
}">{{ operatingSystemName }}</a>
</span>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
@Component
export default class DownloadUrlListItem extends StatefulVue {
@Prop() public operatingSystem!: OperatingSystem;
public OperatingSystem = OperatingSystem;
public downloadUrl: string = '';
public operatingSystemName: string = '';
public isCurrentOs: boolean = false;
public hasCurrentOsDesktopVersion: boolean = false;
public async mounted() {
await this.onOperatingSystemChangedAsync(this.operatingSystem);
}
@Watch('operatingSystem')
public async onOperatingSystemChangedAsync(os: OperatingSystem) {
const currentOs = Environment.CurrentEnvironment.os;
this.isCurrentOs = os === currentOs;
this.downloadUrl = await this.getDownloadUrlAsync(os);
this.operatingSystemName = getOperatingSystemName(os);
this.hasCurrentOsDesktopVersion = hasDesktopVersion(currentOs);
}
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
const state = await this.getCurrentStateAsync();
return `${state.app.repositoryUrl}/releases/download/${state.app.version}/${getFileName(os, state.app.version)}`;
}
}
function hasDesktopVersion(os: OperatingSystem): boolean {
return os === OperatingSystem.Windows
|| os === OperatingSystem.Linux
|| os === OperatingSystem.macOS;
}
function getOperatingSystemName(os: OperatingSystem): string {
switch (os) {
case OperatingSystem.Linux:
return 'Linux';
case OperatingSystem.macOS:
return 'macOS';
case OperatingSystem.Windows:
return 'Windows';
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}
function getFileName(os: OperatingSystem, version: string): string {
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.dmg`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}-mac.zip`;
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
}
}
</script>
<style scoped lang="scss">
.url {
&__active {
font-size: 1em;
}
&__inactive {
font-size: 0.70em;
}
a {
color:inherit;
&:hover {
opacity: 0.8;
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="privacy-policy">
<div v-if="!isDesktop" class="line">
<div class="line__emoji">🚫🍪</div>
<div>No cookies!</div>
</div>
<div v-if="isDesktop" class="line">
<div class="line__emoji">🚫🌐</div>
<div>Everything is offline, except single request GitHub toto check for updates on application start.</div>
</div>
<div class="line">
<div class="line__emoji">🚫👀</div>
<div>No user behavior / IP adress collection!</div>
</div>
<div class="line">
<div class="line__emoji">🤖</div>
<div>All transparent: Deployed automatically from master branch
of the <a :href="repositoryUrl" target="_blank">source code</a> with no changes.</div>
</div>
<div v-if="!isDesktop" class="line">
<div class="line__emoji">📈</div>
<div>Basic <a href="https://aws.amazon.com/cloudfront/reporting/" target="_blank">CDN statistics</a>
are collected by AWS but they cannot be related to you or your behavior. You can download the offline version if you don't want CDN data collection.</div>
</div>
<div class="line">
<div class="line__emoji">🎉</div>
<div>As almost no data is colected, the application gets better only with your active feedback.
Feel free to <a :href="feedbackUrl" target="_blank">create an issue</a> 😊</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
@Component
export default class TheFooter extends StatefulVue {
public repositoryUrl: string = '';
public feedbackUrl: string = '';
public isDesktop: boolean = false;
constructor() {
super();
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
}
public async mounted() {
const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.privacy-policy {
display: flex;
flex-direction: column;
font-family: $normal-font;
text-align:center;
.line {
display: flex;
flex-direction: column;
&:not(:first-child) {
margin-top:0.2rem;
}
}
a {
color:inherit;
&:hover {
opacity: 0.8;
}
}
}
</style>

View File

@@ -1,14 +1,24 @@
<template>
<div class="footer">
<div class="item">
<a :href="releaseUrl" target="_blank">{{ version }}</a>
<div>
<div class="footer">
<div class="footer__section">
<div class="item">
<a :href="releaseUrl" target="_blank">{{ version }}</a>
</div>
<div class="item">
<a @click="$modal.show(modalName)">Privacy</a>
</div>
</div>
<div class="footer__section">
<span v-if="isDesktop">
Online version at <a href="https://privacy.sexy" target="_blank">https://privacy.sexy</a>
</span>
<DownloadUrlList v-else />
</div>
<div class="item">
<a @click="$modal.show(modalName)">Privacy</a> <!-- href to #privacy to avoid scrolling to top -->
</div>
<modal :name="modalName" height="auto" :scrollable="true" :adaptive="true">
<div class="modal">
<ThePrivacyPolicy class="modal__content"/>
<PrivacyPolicy class="modal__content"/>
<div class="modal__close-button">
<font-awesome-icon :icon="['fas', 'times']" @click="$modal.hide(modalName)"/>
</div>
@@ -19,18 +29,28 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from './StatefulVue';
import ThePrivacyPolicy from './ThePrivacyPolicy.vue';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Environment } from '@/application/Environment/Environment';
import PrivacyPolicy from './PrivacyPolicy.vue';
import DownloadUrlList from './DownloadUrlList.vue';
import { OperatingSystem } from '@/application/Environment/OperatingSystem';
@Component({
components: {
ThePrivacyPolicy,
PrivacyPolicy, DownloadUrlList,
},
})
export default class TheFooter extends StatefulVue {
private readonly modalName = 'privacy-policy';
private version: string = '';
private releaseUrl: string = '';
public readonly modalName = 'privacy-policy';
public readonly isDesktop: boolean;
public version: string = '';
public releaseUrl: string = '';
constructor() {
super();
this.isDesktop = Environment.CurrentEnvironment.isDesktop;
}
public async mounted() {
const state = await this.getCurrentStateAsync();
@@ -43,13 +63,26 @@ export default class TheFooter extends StatefulVue {
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
@import "@/presentation/styles/fonts.scss";
$medium-screen-width: 767px;
.footer {
display: flex;
justify-content: space-between;
@media screen and (max-width: $medium-screen-width) {
flex-direction: column;
align-items: center;
}
flex-wrap: wrap;
&__section {
display: flex;
color: $dark-gray;
font-size: 1rem;
font-family: $normal-font;
align-self: center;
&:not(:first-child) {
padding-top: 3px;
}
a {
color:inherit;
text-decoration: underline;
@@ -63,6 +96,7 @@ export default class TheFooter extends StatefulVue {
content: "|";
padding: 0 5px;
}
}
}
.modal {
margin-bottom: 10px;

View File

@@ -1,70 +0,0 @@
<template>
<div class="privacy-policy">
<div class="line">
<div class="line__emoji">🚫🍪</div>
<div>No cookies!</div>
</div>
<div class="line">
<div class="line__emoji">🚫👀</div>
<div>No user behavior / IP adress collection!</div>
</div>
<div class="line">
<div class="line__emoji">🤖</div>
<div>Website is deployed automatically from master branch
of the <a :href="repositoryUrl" target="_blank">source code</a> with no changes.</div>
</div>
<div class="line">
<div class="line__emoji">📈</div>
<div>Basic <a href="https://aws.amazon.com/cloudfront/reporting/" target="_blank">CDN statistics</a>
are collected by AWS but they cannot be related to you or your behavior.</div>
</div>
<div class="line">
<div class="line__emoji">🎉</div>
<div>As almost no data is colected, the website gets better only with your active feedback.
Feel free to <a :href="feedbackUrl" target="_blank">create an issue</a> 😊</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
@Component
export default class TheFooter extends StatefulVue {
private repositoryUrl: string = '';
private feedbackUrl: string = '';
public async mounted() {
const state = await this.getCurrentStateAsync();
this.repositoryUrl = state.app.repositoryUrl;
this.feedbackUrl = `${state.app.repositoryUrl}/issues`;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.privacy-policy {
display: flex;
flex-direction: column;
font-family: $normal-font;
text-align:center;
.line {
display: flex;
flex-direction: column;
&:not(:first-child) {
margin-top:0.2rem;
}
}
a {
color:inherit;
&:hover {
opacity: 0.8;
}
}
}
</style>