diff --git a/.eslintrc.js b/.eslintrc.js index ef90a058..dd56b077 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const { rules: baseStyleRules } = require('eslint-config-airbnb-base/rules/style'); + module.exports = { root: true, env: { @@ -22,91 +24,10 @@ module.exports = { ecmaVersion: 'latest', }, rules: { - 'no-console': 'off', // process.env.NODE_ENV === 'production' ? 'warn' : 'off', - 'no-debugger': 'off', // process.env.NODE_ENV === 'production' ? 'warn' : 'off', - - 'linebreak-style': 'off', - 'no-useless-constructor': 'off', - 'import/prefer-default-export': 'off', - 'class-methods-use-this': 'off', - 'no-use-before-define': 'off', - 'no-restricted-syntax': 'off', - 'global-require': 'off', - 'max-len': 'off', - 'import/no-unresolved': 'off', - 'import/no-webpack-loader-syntax': 'off', - 'import/extensions': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-explicit-any': 'off', - 'no-plusplus': 'off', - 'no-mixed-operators': 'off', - 'import/no-extraneous-dependencies': 'off', - '@typescript-eslint/no-empty-function': 'off', - 'no-return-assign': 'off', - 'no-await-in-loop': 'off', - 'no-shadow': 'off', - 'vuejs-accessibility/accessible-emoji': 'off', - 'no-promise-executor-return': 'off', - 'no-new': 'off', - 'no-useless-escape': 'off', - 'prefer-destructuring': 'off', - 'no-param-reassign': 'off', - 'no-irregular-whitespace': 'off', - 'no-undef': 'off', - 'no-underscore-dangle': 'off', - 'vuejs-accessibility/form-control-has-label': 'off', - 'vuejs-accessibility/click-events-have-key-events': 'off', - '@typescript-eslint/ban-ts-comment': 'off', - 'camelcase': 'off', - 'no-restricted-globals': 'off', - 'default-param-last': 'off', - 'no-continue': 'off', - 'vuejs-accessibility/anchor-has-content': 'off', - '@typescript-eslint/no-extra-semi': 'off', - 'no-multi-spaces': 'off', - 'indent': 'off', - 'comma-dangle': 'off', - 'semi': 'off', - 'quotes': 'off', - 'key-spacing': 'off', - 'lines-between-class-members': 'off', - 'import/order': 'off', - 'space-in-parens': 'off', - 'array-bracket-spacing': 'off', - 'object-curly-spacing': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - 'import/no-duplicates': 'off', - 'function-paren-newline': 'off', - 'operator-linebreak': 'off', - 'no-multiple-empty-lines': 'off', - 'object-curly-newline': 'off', - 'object-property-newline': 'off', - 'arrow-body-style': 'off', - 'no-useless-return': 'off', - 'prefer-template': 'off', - 'func-call-spacing': 'off', - 'no-spaced-func': 'off', - 'padded-blocks': 'off', - 'implicit-arrow-linebreak': 'off', - 'function-call-argument-newline': 'off', - 'comma-spacing': 'off', - 'comma-style': 'off', - 'newline-per-chained-call': 'off', - 'no-useless-computed-key': 'off', - 'no-else-return': 'off', - 'quote-props': 'off', - 'no-restricted-properties': 'off', - 'prefer-exponentiation-operator': 'off', - 'semi-spacing': 'off', - 'prefer-object-spread': 'off', - 'import/newline-after-import': 'off', - 'strict': 'off', - 'no-trailing-spaces': 'off', - 'no-confusing-arrow': 'off', - 'eol-last': 'off', - 'import/no-useless-path-segments': 'off', - 'spaced-comment': 'off', - '@typescript-eslint/no-empty-interface': 'off', + ...getOwnRules(), + ...getTurnedOffBrokenRules(), + ...getOpinionatedRuleOverrides(), + ...getTodoRules(), }, overrides: [ { @@ -118,5 +39,77 @@ module.exports = { mocha: true, }, }, + { + files: ['**/tests/**/*.{j,t}s?(x)'], + rules: { + 'no-console': 'off', + }, + }, ], }; + +function getOwnRules() { + return { + 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', + 'linebreak-style': ['error', 'unix'], // Also enforced in .editorconfig + 'import/order': [ // Enforce strict import order taking account into aliases + 'error', + { + groups: [ // Enforce more strict order than AirBnb + 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + pathGroups: [ // Fix manually configured paths being incorrectly grouped as "external" + '@/**', // @/.. + '@tests/**', // @tests/.. (not matching anything after @** because there can be third parties as well) + 'js-yaml-loader!@/**', // E.g. js-yaml-loader!@/.. + ].map((pattern) => ({ pattern, group: 'internal' })), + }, + ], + }; +} + +function getTodoRules() { // Should be worked on separate future commits + return { + 'import/no-extraneous-dependencies': 'off', + // Requires webpack configuration change with import '..yaml' files. + 'import/no-webpack-loader-syntax': 'off', + 'import/extensions': 'off', + 'import/no-unresolved': 'off', + // Accessibility improvements: + 'vuejs-accessibility/form-control-has-label': 'off', + 'vuejs-accessibility/click-events-have-key-events': 'off', + 'vuejs-accessibility/anchor-has-content': 'off', + 'vuejs-accessibility/accessible-emoji': 'off', + }; +} + +function getTurnedOffBrokenRules() { + return { + // Broken in TypeScript + 'no-useless-constructor': 'off', // Cannot interpret TypeScript constructors + 'no-shadow': 'off', // Fails with TypeScript enums + }; +} + +function getOpinionatedRuleOverrides() { + return { + // https://erkinekici.com/articles/linting-trap#no-use-before-define + 'no-use-before-define': 'off', + // https://erkinekici.com/articles/linting-trap#arrow-body-style + 'arrow-body-style': 'off', + // https://erkinekici.com/articles/linting-trap#no-plusplus + 'no-plusplus': 'off', + // https://erkinekici.com/articles/linting-trap#no-param-reassign + 'no-param-reassign': 'off', + // https://erkinekici.com/articles/linting-trap#class-methods-use-this + 'class-methods-use-this': 'off', + // https://erkinekici.com/articles/linting-trap#importprefer-default-export + 'import/prefer-default-export': 'off', + // https://erkinekici.com/articles/linting-trap#disallowing-for-of + // Original: https://github.com/airbnb/javascript/blob/d8cb404da74c302506f91e5928f30cc75109e74d/packages/eslint-config-airbnb-base/rules/style.js#L333-L351 + 'no-restricted-syntax': [ + baseStyleRules['no-restricted-syntax'][0], + ...baseStyleRules['no-restricted-syntax'].slice(1).filter((rule) => rule.selector !== 'ForOfStatement'), + ], + }; +} diff --git a/babel.config.js b/babel.config.js index e9558405..757ff9b1 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,5 @@ module.exports = { presets: [ - '@vue/cli-plugin-babel/preset' - ] -} + '@vue/cli-plugin-babel/preset', + ], +}; diff --git a/package.json b/package.json index 8bd60fac..2cb47bd2 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "lint:md": "markdownlint **/*.md --ignore node_modules", "lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent", "lint:md:relative-urls": "remark . --frail --use remark-validate-links", - "lint:eslint": "vue-cli-service lint --no-fix", + "lint:eslint": "vue-cli-service lint --no-fix --mode production", "lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml", "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", diff --git a/postcss.config.js b/postcss.config.js index 961986e2..a47ef4f9 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,5 @@ module.exports = { plugins: { - autoprefixer: {} - } -} + autoprefixer: {}, + }, +}; diff --git a/src/application/ApplicationFactory.ts b/src/application/ApplicationFactory.ts index 99a06d45..edfc0657 100644 --- a/src/application/ApplicationFactory.ts +++ b/src/application/ApplicationFactory.ts @@ -7,15 +7,18 @@ export type ApplicationGetter = () => IApplication; const ApplicationGetter: ApplicationGetter = parseApplication; export class ApplicationFactory implements IApplicationFactory { - public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); - private readonly getter: AsyncLazy; - protected constructor(costlyGetter: ApplicationGetter) { - if (!costlyGetter) { - throw new Error('undefined getter'); - } - this.getter = new AsyncLazy(() => Promise.resolve(costlyGetter())); - } - public getApp(): Promise { - return this.getter.getValue(); + public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); + + private readonly getter: AsyncLazy; + + protected constructor(costlyGetter: ApplicationGetter) { + if (!costlyGetter) { + throw new Error('undefined getter'); } + this.getter = new AsyncLazy(() => Promise.resolve(costlyGetter())); + } + + public getApp(): Promise { + return this.getter.getValue(); + } } diff --git a/src/application/Common/Array.ts b/src/application/Common/Array.ts index 079fa41a..c3444578 100644 --- a/src/application/Common/Array.ts +++ b/src/application/Common/Array.ts @@ -1,21 +1,21 @@ // Compares to Array objects for equality, ignoring order export function scrambledEqual(array1: readonly T[], array2: readonly T[]) { - if (!array1) { throw new Error('undefined first array'); } - if (!array2) { throw new Error('undefined second array'); } - const sortedArray1 = sort(array1); - const sortedArray2 = sort(array2); - return sequenceEqual(sortedArray1, sortedArray2); - function sort(array: readonly T[]) { - return array.slice().sort(); - } + if (!array1) { throw new Error('undefined first array'); } + if (!array2) { throw new Error('undefined second array'); } + const sortedArray1 = sort(array1); + const sortedArray2 = sort(array2); + return sequenceEqual(sortedArray1, sortedArray2); + function sort(array: readonly T[]) { + return array.slice().sort(); + } } // Compares to Array objects for equality in same order export function sequenceEqual(array1: readonly T[], array2: readonly T[]) { - if (!array1) { throw new Error('undefined first array'); } - if (!array2) { throw new Error('undefined second array'); } - if (array1.length !== array2.length) { - return false; - } - return array1.every((val, index) => val === array2[index]); + if (!array1) { throw new Error('undefined first array'); } + if (!array2) { throw new Error('undefined second array'); } + if (array1.length !== array2.length) { + return false; + } + return array1.every((val, index) => val === array2[index]); } diff --git a/src/application/Common/Enum.ts b/src/application/Common/Enum.ts index 2042a7ac..ac2bba57 100644 --- a/src/application/Common/Enum.ts +++ b/src/application/Common/Enum.ts @@ -1,54 +1,63 @@ // Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611 export type EnumType = number | string; -export type EnumVariable = { [key in T]: TEnumValue }; +export type EnumVariable + = { [key in T]: TEnumValue }; export interface IEnumParser { - parseEnum(value: string, propertyName: string): TEnum; -} -export function createEnumParser( - enumVariable: EnumVariable): IEnumParser { - return { - parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), - }; -} -function parseEnumValue( - value: string, - enumName: string, - enumVariable: EnumVariable): TEnumValue { - if (!value) { - throw new Error(`undefined ${enumName}`); - } - if (typeof value !== 'string') { - throw new Error(`unexpected type of ${enumName}: "${typeof value}"`); - } - const casedValue = getEnumNames(enumVariable) - .find((enumValue) => enumValue.toLowerCase() === value.toLowerCase()); - if (!casedValue) { - throw new Error(`unknown ${enumName}: "${value}"`); - } - return enumVariable[casedValue as keyof typeof enumVariable]; + parseEnum(value: string, propertyName: string): TEnum; } -export function getEnumNames( - enumVariable: EnumVariable): string[] { - return Object - .values(enumVariable) - .filter((enumMember) => typeof enumMember === 'string') as string[]; +export function createEnumParser( + enumVariable: EnumVariable, +): IEnumParser { + return { + parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), + }; +} + +function parseEnumValue( + value: string, + enumName: string, + enumVariable: EnumVariable, +): TEnumValue { + if (!value) { + throw new Error(`undefined ${enumName}`); + } + if (typeof value !== 'string') { + throw new Error(`unexpected type of ${enumName}: "${typeof value}"`); + } + const casedValue = getEnumNames(enumVariable) + .find((enumValue) => enumValue.toLowerCase() === value.toLowerCase()); + if (!casedValue) { + throw new Error(`unknown ${enumName}: "${value}"`); + } + return enumVariable[casedValue as keyof typeof enumVariable]; +} + +export function getEnumNames +( + enumVariable: EnumVariable, +): string[] { + return Object + .values(enumVariable) + .filter((enumMember) => typeof enumMember === 'string') as string[]; } export function getEnumValues( - enumVariable: EnumVariable): TEnumValue[] { - return getEnumNames(enumVariable) - .map((level) => enumVariable[level]) as TEnumValue[]; + enumVariable: EnumVariable, +): TEnumValue[] { + return getEnumNames(enumVariable) + .map((level) => enumVariable[level]) as TEnumValue[]; } export function assertInRange( - value: TEnumValue, - enumVariable: EnumVariable) { - if (value === undefined) { - throw new Error('undefined enum value'); - } - if (!(value in enumVariable)) { - throw new RangeError(`enum value "${value}" is out of range`); - } + value: TEnumValue, + enumVariable: EnumVariable, +) { + if (value === undefined) { + throw new Error('undefined enum value'); + } + if (!(value in enumVariable)) { + throw new RangeError(`enum value "${value}" is out of range`); + } } diff --git a/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts b/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts index b82df700..fa18479e 100644 --- a/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts +++ b/src/application/Common/ScriptingLanguage/IScriptingLanguageFactory.ts @@ -1,5 +1,5 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; export interface IScriptingLanguageFactory { - create(language: ScriptingLanguage): T; + create(language: ScriptingLanguage): T; } diff --git a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts index 2b88c767..fb71e0d6 100644 --- a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts +++ b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts @@ -1,31 +1,30 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; -import { IScriptingLanguageFactory } from './IScriptingLanguageFactory'; import { assertInRange } from '@/application/Common/Enum'; +import { IScriptingLanguageFactory } from './IScriptingLanguageFactory'; type Getter = () => T; export abstract class ScriptingLanguageFactory implements IScriptingLanguageFactory { - private readonly getters = new Map>(); + private readonly getters = new Map>(); - public create(language: ScriptingLanguage): T { - assertInRange(language, ScriptingLanguage); - if (!this.getters.has(language)) { - throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); - } - const getter = this.getters.get(language); - const instance = getter(); - return instance; + public create(language: ScriptingLanguage): T { + assertInRange(language, ScriptingLanguage); + if (!this.getters.has(language)) { + throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); } + const getter = this.getters.get(language); + const instance = getter(); + return instance; + } - protected registerGetter(language: ScriptingLanguage, getter: Getter) { - assertInRange(language, ScriptingLanguage); - if (!getter) { - throw new Error('undefined getter'); - } - if (this.getters.has(language)) { - throw new Error(`${ScriptingLanguage[language]} is already registered`); - } - this.getters.set(language, getter); + protected registerGetter(language: ScriptingLanguage, getter: Getter) { + assertInRange(language, ScriptingLanguage); + if (!getter) { + throw new Error('undefined getter'); } - + if (this.getters.has(language)) { + throw new Error(`${ScriptingLanguage[language]} is already registered`); + } + this.getters.set(language, getter); + } } diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index f4c401de..68557136 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -1,60 +1,64 @@ -import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext'; -import { ICategoryCollectionState } from './State/ICategoryCollectionState'; -import { CategoryCollectionState } from './State/CategoryCollectionState'; import { IApplication } from '@/domain/IApplication'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { EventSource } from '@/infrastructure/Events/EventSource'; import { assertInRange } from '@/application/Common/Enum'; +import { CategoryCollectionState } from './State/CategoryCollectionState'; +import { ICategoryCollectionState } from './State/ICategoryCollectionState'; +import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext'; type StateMachine = Map; export class ApplicationContext implements IApplicationContext { - public readonly contextChanged = new EventSource(); - public collection: ICategoryCollection; - public currentOs: OperatingSystem; + public readonly contextChanged = new EventSource(); - public get state(): ICategoryCollectionState { - return this.states[this.collection.os]; - } + public collection: ICategoryCollection; - private readonly states: StateMachine; - public constructor( - public readonly app: IApplication, - initialContext: OperatingSystem) { - validateApp(app); - assertInRange(initialContext, OperatingSystem); - this.states = initializeStates(app); - this.changeContext(initialContext); - } + public currentOs: OperatingSystem; - public changeContext(os: OperatingSystem): void { - if (this.currentOs === os) { - return; - } - this.collection = this.app.getCollection(os); - if (!this.collection) { - throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); - } - const event: IApplicationContextChangedEvent = { - newState: this.states[os], - oldState: this.states[this.currentOs], - }; - this.contextChanged.notify(event); - this.currentOs = os; + public get state(): ICategoryCollectionState { + return this.states[this.collection.os]; + } + + private readonly states: StateMachine; + + public constructor( + public readonly app: IApplication, + initialContext: OperatingSystem, + ) { + validateApp(app); + assertInRange(initialContext, OperatingSystem); + this.states = initializeStates(app); + this.changeContext(initialContext); + } + + public changeContext(os: OperatingSystem): void { + if (this.currentOs === os) { + return; } + this.collection = this.app.getCollection(os); + if (!this.collection) { + throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); + } + const event: IApplicationContextChangedEvent = { + newState: this.states[os], + oldState: this.states[this.currentOs], + }; + this.contextChanged.notify(event); + this.currentOs = os; + } } function validateApp(app: IApplication) { - if (!app) { - throw new Error('undefined app'); - } + if (!app) { + throw new Error('undefined app'); + } } function initializeStates(app: IApplication): StateMachine { - const machine = new Map(); - for (const collection of app.collections) { - machine[collection.os] = new CategoryCollectionState(collection); - } - return machine; + const machine = new Map(); + for (const collection of app.collections) { + machine[collection.os] = new CategoryCollectionState(collection); + } + return machine; } diff --git a/src/application/Context/ApplicationContextFactory.ts b/src/application/Context/ApplicationContextFactory.ts index a120867f..2fead72e 100644 --- a/src/application/Context/ApplicationContextFactory.ts +++ b/src/application/Context/ApplicationContextFactory.ts @@ -1,31 +1,32 @@ -import { ApplicationContext } from './ApplicationContext'; import { IApplicationContext } from '@/application/Context/IApplicationContext'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { Environment } from '../Environment/Environment'; import { IApplication } from '@/domain/IApplication'; +import { Environment } from '../Environment/Environment'; import { IEnvironment } from '../Environment/IEnvironment'; import { IApplicationFactory } from '../IApplicationFactory'; import { ApplicationFactory } from '../ApplicationFactory'; +import { ApplicationContext } from './ApplicationContext'; export async function buildContext( - factory: IApplicationFactory = ApplicationFactory.Current, - environment = Environment.CurrentEnvironment): Promise { - if (!factory) { throw new Error('undefined factory'); } - if (!environment) { throw new Error('undefined environment'); } - const app = await factory.getApp(); - const os = getInitialOs(app, environment); - return new ApplicationContext(app, os); + factory: IApplicationFactory = ApplicationFactory.Current, + environment = Environment.CurrentEnvironment, +): Promise { + if (!factory) { throw new Error('undefined factory'); } + if (!environment) { throw new Error('undefined environment'); } + const app = await factory.getApp(); + const os = getInitialOs(app, environment); + return new ApplicationContext(app, os); } function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem { - const currentOs = environment.os; - const supportedOsList = app.getSupportedOsList(); - if (supportedOsList.includes(currentOs)) { - return currentOs; - } - supportedOsList.sort((os1, os2) => { - const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; - return getPriority(os2) - getPriority(os1); - }); - return supportedOsList[0]; + const currentOs = environment.os; + const supportedOsList = app.getSupportedOsList(); + if (supportedOsList.includes(currentOs)) { + return currentOs; + } + supportedOsList.sort((os1, os2) => { + const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; + return getPriority(os2) - getPriority(os1); + }); + return supportedOsList[0]; } diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts index 9a8090a3..59bf5892 100644 --- a/src/application/Context/IApplicationContext.ts +++ b/src/application/Context/IApplicationContext.ts @@ -1,12 +1,12 @@ -import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IApplication } from '@/domain/IApplication'; +import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState'; export interface IReadOnlyApplicationContext { - readonly app: IApplication; - readonly state: IReadOnlyCategoryCollectionState; - readonly contextChanged: IEventSource; + readonly app: IApplication; + readonly state: IReadOnlyCategoryCollectionState; + readonly contextChanged: IEventSource; } export interface IApplicationContext extends IReadOnlyApplicationContext { @@ -15,6 +15,6 @@ export interface IApplicationContext extends IReadOnlyApplicationContext { } export interface IApplicationContextChangedEvent { - readonly newState: ICategoryCollectionState; - readonly oldState: ICategoryCollectionState; + readonly newState: ICategoryCollectionState; + readonly oldState: ICategoryCollectionState; } diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts index 7638a384..da692d11 100644 --- a/src/application/Context/State/CategoryCollectionState.ts +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -1,3 +1,5 @@ +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { OperatingSystem } from '@/domain/OperatingSystem'; import { UserFilter } from './Filter/UserFilter'; import { IUserFilter } from './Filter/IUserFilter'; import { ApplicationCode } from './Code/ApplicationCode'; @@ -5,19 +7,20 @@ import { UserSelection } from './Selection/UserSelection'; import { IUserSelection } from './Selection/IUserSelection'; import { ICategoryCollectionState } from './ICategoryCollectionState'; import { IApplicationCode } from './Code/IApplicationCode'; -import { ICategoryCollection } from '@/domain/ICategoryCollection'; -import { OperatingSystem } from '@/domain/OperatingSystem'; export class CategoryCollectionState implements ICategoryCollectionState { - public readonly os: OperatingSystem; - public readonly code: IApplicationCode; - public readonly selection: IUserSelection; - public readonly filter: IUserFilter; + public readonly os: OperatingSystem; - public constructor(readonly collection: ICategoryCollection) { - this.selection = new UserSelection(collection, []); - this.code = new ApplicationCode(this.selection, collection.scripting); - this.filter = new UserFilter(collection); - this.os = collection.os; - } + public readonly code: IApplicationCode; + + public readonly selection: IUserSelection; + + public readonly filter: IUserFilter; + + public constructor(readonly collection: ICategoryCollection) { + this.selection = new UserSelection(collection, []); + this.code = new ApplicationCode(this.selection, collection.scripting); + this.filter = new UserFilter(collection); + this.os = collection.os; + } } diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts index f0a4db23..3b517ef1 100644 --- a/src/application/Context/State/Code/ApplicationCode.ts +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -1,39 +1,41 @@ +import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; +import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection'; +import { EventSource } from '@/infrastructure/Events/EventSource'; +import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; import { CodeChangedEvent } from './Event/CodeChangedEvent'; import { CodePosition } from './Position/CodePosition'; import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; -import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; -import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection'; import { UserScriptGenerator } from './Generation/UserScriptGenerator'; -import { EventSource } from '@/infrastructure/Events/EventSource'; import { IApplicationCode } from './IApplicationCode'; import { IUserScriptGenerator } from './Generation/IUserScriptGenerator'; -import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; export class ApplicationCode implements IApplicationCode { - public readonly changed = new EventSource(); - public current: string; + public readonly changed = new EventSource(); - private scriptPositions = new Map(); + public current: string; - constructor( - userSelection: IReadOnlyUserSelection, - private readonly scriptingDefinition: IScriptingDefinition, - private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) { - if (!userSelection) { throw new Error('userSelection is null or undefined'); } - if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); } - if (!generator) { throw new Error('generator is null or undefined'); } - this.setCode(userSelection.selectedScripts); - userSelection.changed.on((scripts) => { - this.setCode(scripts); - }); - } + private scriptPositions = new Map(); - private setCode(scripts: ReadonlyArray): void { - const oldScripts = Array.from(this.scriptPositions.keys()); - const code = this.generator.buildCode(scripts, this.scriptingDefinition); - this.current = code.code; - this.scriptPositions = code.scriptPositions; - const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions); - this.changed.notify(event); - } + constructor( + userSelection: IReadOnlyUserSelection, + private readonly scriptingDefinition: IScriptingDefinition, + private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), + ) { + if (!userSelection) { throw new Error('userSelection is null or undefined'); } + if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); } + if (!generator) { throw new Error('generator is null or undefined'); } + this.setCode(userSelection.selectedScripts); + userSelection.changed.on((scripts) => { + this.setCode(scripts); + }); + } + + private setCode(scripts: ReadonlyArray): void { + const oldScripts = Array.from(this.scriptPositions.keys()); + const code = this.generator.buildCode(scripts, this.scriptingDefinition); + this.current = code.code; + this.scriptPositions = code.scriptPositions; + const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions); + this.changed.notify(event); + } } diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index cffd6c14..d91238fc 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -1,64 +1,72 @@ -import { ICodeChangedEvent } from './ICodeChangedEvent'; -import { SelectedScript } from '../../Selection/SelectedScript'; import { IScript } from '@/domain/IScript'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; +import { SelectedScript } from '../../Selection/SelectedScript'; +import { ICodeChangedEvent } from './ICodeChangedEvent'; export class CodeChangedEvent implements ICodeChangedEvent { - public readonly code: string; - public readonly addedScripts: ReadonlyArray; - public readonly removedScripts: ReadonlyArray; - public readonly changedScripts: ReadonlyArray; + public readonly code: string; - private readonly scripts: Map; + public readonly addedScripts: ReadonlyArray; - constructor( - code: string, - oldScripts: ReadonlyArray, - scripts: Map) { - ensureAllPositionsExist(code, Array.from(scripts.values())); - this.code = code; - const newScripts = Array.from(scripts.keys()); - this.addedScripts = selectIfNotExists(newScripts, oldScripts); - this.removedScripts = selectIfNotExists(oldScripts, newScripts); - this.changedScripts = getChangedScripts(oldScripts, newScripts); - this.scripts = new Map(); - scripts.forEach((position, selection) => { - this.scripts.set(selection.script, position); - }); - } + public readonly removedScripts: ReadonlyArray; - public isEmpty(): boolean { - return this.scripts.size === 0; - } + public readonly changedScripts: ReadonlyArray; - public getScriptPositionInCode(script: IScript): ICodePosition { - return this.scripts.get(script); - } + private readonly scripts: Map; + + constructor( + code: string, + oldScripts: ReadonlyArray, + scripts: Map, + ) { + ensureAllPositionsExist(code, Array.from(scripts.values())); + this.code = code; + const newScripts = Array.from(scripts.keys()); + this.addedScripts = selectIfNotExists(newScripts, oldScripts); + this.removedScripts = selectIfNotExists(oldScripts, newScripts); + this.changedScripts = getChangedScripts(oldScripts, newScripts); + this.scripts = new Map(); + scripts.forEach((position, selection) => { + this.scripts.set(selection.script, position); + }); + } + + public isEmpty(): boolean { + return this.scripts.size === 0; + } + + public getScriptPositionInCode(script: IScript): ICodePosition { + return this.scripts.get(script); + } } function ensureAllPositionsExist(script: string, positions: ReadonlyArray) { - const totalLines = script.split(/\r\n|\r|\n/).length; - for (const position of positions) { - if (position.endLine > totalLines) { - throw new Error(`script end line (${position.endLine}) is out of range.` + - `(total code lines: ${totalLines}`); - } + const totalLines = script.split(/\r\n|\r|\n/).length; + for (const position of positions) { + if (position.endLine > totalLines) { + throw new Error( + `script end line (${position.endLine}) is out of range.` + + `(total code lines: ${totalLines}`, + ); } + } } function getChangedScripts( - oldScripts: ReadonlyArray, - newScripts: ReadonlyArray): ReadonlyArray { - return newScripts - .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id - && oldScript.revert !== newScript.revert )) - .map((selection) => selection.script); + oldScripts: ReadonlyArray, + newScripts: ReadonlyArray, +): ReadonlyArray { + return newScripts + .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id + && oldScript.revert !== newScript.revert)) + .map((selection) => selection.script); } function selectIfNotExists( - selectableContainer: ReadonlyArray, - test: ReadonlyArray) { - return selectableContainer - .filter((script) => !test.find((oldScript) => oldScript.id === script.id)) - .map((selection) => selection.script); + selectableContainer: ReadonlyArray, + test: ReadonlyArray, +) { + return selectableContainer + .filter((script) => !test.find((oldScript) => oldScript.id === script.id)) + .map((selection) => selection.script); } diff --git a/src/application/Context/State/Code/Event/ICodeChangedEvent.ts b/src/application/Context/State/Code/Event/ICodeChangedEvent.ts index 87780f1a..570ebad8 100644 --- a/src/application/Context/State/Code/Event/ICodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/ICodeChangedEvent.ts @@ -2,10 +2,10 @@ import { IScript } from '@/domain/IScript'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; export interface ICodeChangedEvent { - readonly code: string; - addedScripts: ReadonlyArray; - removedScripts: ReadonlyArray; - changedScripts: ReadonlyArray; - isEmpty(): boolean; - getScriptPositionInCode(script: IScript): ICodePosition; + readonly code: string; + addedScripts: ReadonlyArray; + removedScripts: ReadonlyArray; + changedScripts: ReadonlyArray; + isEmpty(): boolean; + getScriptPositionInCode(script: IScript): ICodePosition; } diff --git a/src/application/Context/State/Code/Generation/CodeBuilder.ts b/src/application/Context/State/Code/Generation/CodeBuilder.ts index 601352e1..bf352d3f 100644 --- a/src/application/Context/State/Code/Generation/CodeBuilder.ts +++ b/src/application/Context/State/Code/Generation/CodeBuilder.ts @@ -4,64 +4,67 @@ const NewLine = '\n'; const TotalFunctionSeparatorChars = 58; export abstract class CodeBuilder implements ICodeBuilder { - private readonly lines = new Array(); + private readonly lines = new Array(); - // Returns current line starting from 0 (no lines), or 1 (have single line) - public get currentLine(): number { - return this.lines.length; + // Returns current line starting from 0 (no lines), or 1 (have single line) + public get currentLine(): number { + return this.lines.length; + } + + public appendLine(code?: string): CodeBuilder { + if (!code) { + this.lines.push(''); + return this; } - - public appendLine(code?: string): CodeBuilder { - if (!code) { - this.lines.push(''); - return this; - } - const lines = code.match(/[^\r\n]+/g); - for (const line of lines) { - this.lines.push(line); - } - return this; + const lines = code.match(/[^\r\n]+/g); + for (const line of lines) { + this.lines.push(line); } + return this; + } - public appendTrailingHyphensCommentLine( - totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { - return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); + public appendTrailingHyphensCommentLine( + totalRepeatHyphens: number = TotalFunctionSeparatorChars, + ): CodeBuilder { + return this.appendCommentLine('-'.repeat(totalRepeatHyphens)); + } + + public appendCommentLine(commentLine?: string): CodeBuilder { + this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`); + return this; + } + + public appendFunction(name: string, code: string): CodeBuilder { + if (!name) { throw new Error('name cannot be empty or null'); } + if (!code) { throw new Error('code cannot be empty or null'); } + return this + .appendCommentLineWithHyphensAround(name) + .appendLine(this.writeStandardOut(`--- ${name}`)) + .appendLine(code) + .appendTrailingHyphensCommentLine(); + } + + public appendCommentLineWithHyphensAround( + sectionName: string, + totalRepeatHyphens: number = TotalFunctionSeparatorChars, + ): CodeBuilder { + if (!sectionName) { throw new Error('sectionName cannot be empty or null'); } + if (sectionName.length >= totalRepeatHyphens) { + return this.appendCommentLine(sectionName); } + const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2)); + const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2)); + return this + .appendTrailingHyphensCommentLine() + .appendCommentLine(firstHyphens + sectionName + secondHyphens) + .appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars); + } - public appendCommentLine(commentLine?: string): CodeBuilder { - this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`); - return this; - } + public toString(): string { + return this.lines.join(NewLine); + } - public appendFunction(name: string, code: string): CodeBuilder { - if (!name) { throw new Error('name cannot be empty or null'); } - if (!code) { throw new Error('code cannot be empty or null'); } - return this - .appendCommentLineWithHyphensAround(name) - .appendLine(this.writeStandardOut(`--- ${name}`)) - .appendLine(code) - .appendTrailingHyphensCommentLine(); - } + protected abstract getCommentDelimiter(): string; - public appendCommentLineWithHyphensAround( - sectionName: string, - totalRepeatHyphens: number = TotalFunctionSeparatorChars): CodeBuilder { - if (!sectionName) { throw new Error('sectionName cannot be empty or null'); } - if (sectionName.length >= totalRepeatHyphens) { - return this.appendCommentLine(sectionName); - } - const firstHyphens = '-'.repeat(Math.floor((totalRepeatHyphens - sectionName.length) / 2)); - const secondHyphens = '-'.repeat(Math.ceil((totalRepeatHyphens - sectionName.length) / 2)); - return this - .appendTrailingHyphensCommentLine() - .appendCommentLine(firstHyphens + sectionName + secondHyphens) - .appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars); - } - - public toString(): string { - return this.lines.join(NewLine); - } - - protected abstract getCommentDelimiter(): string; - protected abstract writeStandardOut(text: string): string; + protected abstract writeStandardOut(text: string): string; } diff --git a/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts b/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts index c75b4714..4cb50fdf 100644 --- a/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts +++ b/src/application/Context/State/Code/Generation/CodeBuilderFactory.ts @@ -5,10 +5,12 @@ import { BatchBuilder } from './Languages/BatchBuilder'; import { ShellBuilder } from './Languages/ShellBuilder'; import { ICodeBuilderFactory } from './ICodeBuilderFactory'; -export class CodeBuilderFactory extends ScriptingLanguageFactory implements ICodeBuilderFactory { - constructor() { - super(); - this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder()); - this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder()); - } +export class CodeBuilderFactory + extends ScriptingLanguageFactory + implements ICodeBuilderFactory { + constructor() { + super(); + this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder()); + this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder()); + } } diff --git a/src/application/Context/State/Code/Generation/ICodeBuilder.ts b/src/application/Context/State/Code/Generation/ICodeBuilder.ts index 635838d3..d1a5d140 100644 --- a/src/application/Context/State/Code/Generation/ICodeBuilder.ts +++ b/src/application/Context/State/Code/Generation/ICodeBuilder.ts @@ -1,9 +1,9 @@ export interface ICodeBuilder { - currentLine: number; - appendLine(code?: string): ICodeBuilder; - appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder; - appendCommentLine(commentLine?: string): ICodeBuilder; - appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder; - appendFunction(name: string, code: string): ICodeBuilder; - toString(): string; + currentLine: number; + appendLine(code?: string): ICodeBuilder; + appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder; + appendCommentLine(commentLine?: string): ICodeBuilder; + appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder; + appendFunction(name: string, code: string): ICodeBuilder; + toString(): string; } diff --git a/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts b/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts index 15a7441b..de9f1696 100644 --- a/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts +++ b/src/application/Context/State/Code/Generation/ICodeBuilderFactory.ts @@ -1,5 +1,4 @@ -import { ICodeBuilder } from './ICodeBuilder'; import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory'; +import { ICodeBuilder } from './ICodeBuilder'; -export interface ICodeBuilderFactory extends IScriptingLanguageFactory { -} +export type ICodeBuilderFactory = IScriptingLanguageFactory; diff --git a/src/application/Context/State/Code/Generation/IUserScript.ts b/src/application/Context/State/Code/Generation/IUserScript.ts index 7bbeb05a..596819fd 100644 --- a/src/application/Context/State/Code/Generation/IUserScript.ts +++ b/src/application/Context/State/Code/Generation/IUserScript.ts @@ -2,6 +2,6 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedSc import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; export interface IUserScript { - code: string; - scriptPositions: Map; + code: string; + scriptPositions: Map; } diff --git a/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts b/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts index 10aee8b5..23e47c90 100644 --- a/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts @@ -1,9 +1,9 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; -import { IUserScript } from './IUserScript'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import { IUserScript } from './IUserScript'; export interface IUserScriptGenerator { - buildCode( - selectedScripts: ReadonlyArray, - scriptingDefinition: IScriptingDefinition): IUserScript; + buildCode( + selectedScripts: ReadonlyArray, + scriptingDefinition: IScriptingDefinition): IUserScript; } diff --git a/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts b/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts index bf0def5c..133d77ed 100644 --- a/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts +++ b/src/application/Context/State/Code/Generation/Languages/BatchBuilder.ts @@ -1,16 +1,17 @@ import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; export class BatchBuilder extends CodeBuilder { - protected getCommentDelimiter(): string { - return '::'; - } - protected writeStandardOut(text: string): string { - return `echo ${escapeForEcho(text)}`; - } + protected getCommentDelimiter(): string { + return '::'; + } + + protected writeStandardOut(text: string): string { + return `echo ${escapeForEcho(text)}`; + } } function escapeForEcho(text: string) { - return text - .replace(/&/g, '^&') - .replace(/%/g, '%%'); + return text + .replace(/&/g, '^&') + .replace(/%/g, '%%'); } diff --git a/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts b/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts index 5c45fac0..1f905823 100644 --- a/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts +++ b/src/application/Context/State/Code/Generation/Languages/ShellBuilder.ts @@ -1,15 +1,16 @@ import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder'; export class ShellBuilder extends CodeBuilder { - protected getCommentDelimiter(): string { - return '#'; - } - protected writeStandardOut(text: string): string { - return `echo '${escapeForEcho(text)}'`; - } + protected getCommentDelimiter(): string { + return '#'; + } + + protected writeStandardOut(text: string): string { + return `echo '${escapeForEcho(text)}'`; + } } function escapeForEcho(text: string) { - return text - .replace(/'/g, '\'\\\'\''); + return text + .replace(/'/g, '\'\\\'\''); } diff --git a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts index e3edd277..e3a4945b 100644 --- a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts @@ -1,71 +1,76 @@ import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; -import { IUserScriptGenerator } from './IUserScriptGenerator'; import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; -import { CodePosition } from '../Position/CodePosition'; -import { IUserScript } from './IUserScript'; import { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import { CodePosition } from '../Position/CodePosition'; +import { IUserScriptGenerator } from './IUserScriptGenerator'; +import { IUserScript } from './IUserScript'; import { ICodeBuilder } from './ICodeBuilder'; import { ICodeBuilderFactory } from './ICodeBuilderFactory'; import { CodeBuilderFactory } from './CodeBuilderFactory'; export class UserScriptGenerator implements IUserScriptGenerator { - constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) { + constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) { + } + + public buildCode( + selectedScripts: ReadonlyArray, + scriptingDefinition: IScriptingDefinition, + ): IUserScript { + if (!selectedScripts) { throw new Error('undefined scripts'); } + if (!scriptingDefinition) { throw new Error('undefined definition'); } + let scriptPositions = new Map(); + if (!selectedScripts.length) { + return { code: '', scriptPositions }; } - public buildCode( - selectedScripts: ReadonlyArray, - scriptingDefinition: IScriptingDefinition): IUserScript { - if (!selectedScripts) { throw new Error('undefined scripts'); } - if (!scriptingDefinition) { throw new Error('undefined definition'); } - let scriptPositions = new Map(); - if (!selectedScripts.length) { - return { code: '', scriptPositions }; - } - let builder = this.codeBuilderFactory.create(scriptingDefinition.language); - builder = initializeCode(scriptingDefinition.startCode, builder); - for (const selection of selectedScripts) { - scriptPositions = appendSelection(selection, scriptPositions, builder); - } - const code = finalizeCode(builder, scriptingDefinition.endCode); - return { code, scriptPositions }; + let builder = this.codeBuilderFactory.create(scriptingDefinition.language); + builder = initializeCode(scriptingDefinition.startCode, builder); + for (const selection of selectedScripts) { + scriptPositions = appendSelection(selection, scriptPositions, builder); } + const code = finalizeCode(builder, scriptingDefinition.endCode); + return { code, scriptPositions }; + } } function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder { - if (!startCode) { - return builder; - } - return builder - .appendLine(startCode) - .appendLine(); + if (!startCode) { + return builder; + } + return builder + .appendLine(startCode) + .appendLine(); } function finalizeCode(builder: ICodeBuilder, endCode: string): string { - if (!endCode) { - return builder.toString(); - } - return builder.appendLine() - .appendLine(endCode) - .toString(); + if (!endCode) { + return builder.toString(); + } + return builder.appendLine() + .appendLine(endCode) + .toString(); } function appendSelection( - selection: SelectedScript, - scriptPositions: Map, - builder: ICodeBuilder): Map { - const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts - builder = appendCode(selection, builder); - const endPosition = builder.currentLine - 1; - builder.appendLine(); - const position = new CodePosition(startPosition, endPosition); - scriptPositions.set(selection, position); - return scriptPositions; + selection: SelectedScript, + scriptPositions: Map, + builder: ICodeBuilder, +): Map { + // Start from next line because first line will be empty to separate scripts + const startPosition = builder.currentLine + 1; + appendCode(selection, builder); + const endPosition = builder.currentLine - 1; + builder.appendLine(); + const position = new CodePosition(startPosition, endPosition); + scriptPositions.set(selection, position); + return scriptPositions; } function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { - const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name; - const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute; - return builder - .appendLine() - .appendFunction(name, scriptCode); + const { script } = selection; + const name = selection.revert ? `${script.name} (revert)` : script.name; + const scriptCode = selection.revert ? script.code.revert : script.code.execute; + return builder + .appendLine() + .appendFunction(name, scriptCode); } diff --git a/src/application/Context/State/Code/IApplicationCode.ts b/src/application/Context/State/Code/IApplicationCode.ts index 555f28b1..10b35d63 100644 --- a/src/application/Context/State/Code/IApplicationCode.ts +++ b/src/application/Context/State/Code/IApplicationCode.ts @@ -1,7 +1,7 @@ -import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; import { IEventSource } from '@/infrastructure/Events/IEventSource'; +import { ICodeChangedEvent } from './Event/ICodeChangedEvent'; export interface IApplicationCode { - readonly changed: IEventSource; - readonly current: string; + readonly changed: IEventSource; + readonly current: string; } diff --git a/src/application/Context/State/Code/Position/CodePosition.ts b/src/application/Context/State/Code/Position/CodePosition.ts index c0921989..aefabe63 100644 --- a/src/application/Context/State/Code/Position/CodePosition.ts +++ b/src/application/Context/State/Code/Position/CodePosition.ts @@ -1,24 +1,25 @@ import { ICodePosition } from './ICodePosition'; export class CodePosition implements ICodePosition { - public get totalLines(): number { - return this.endLine - this.startLine; - } + public get totalLines(): number { + return this.endLine - this.startLine; + } - constructor( - public readonly startLine: number, - public readonly endLine: number) { - if (startLine < 0) { - throw new Error('Code cannot start in a negative line'); - } - if (endLine < 0) { - throw new Error('Code cannot end in a negative line'); - } - if (endLine === startLine) { - throw new Error('Empty code'); - } - if (endLine < startLine) { - throw new Error('End line cannot be less than start line'); - } + constructor( + public readonly startLine: number, + public readonly endLine: number, + ) { + if (startLine < 0) { + throw new Error('Code cannot start in a negative line'); } + if (endLine < 0) { + throw new Error('Code cannot end in a negative line'); + } + if (endLine === startLine) { + throw new Error('Empty code'); + } + if (endLine < startLine) { + throw new Error('End line cannot be less than start line'); + } + } } diff --git a/src/application/Context/State/Code/Position/ICodePosition.ts b/src/application/Context/State/Code/Position/ICodePosition.ts index e3e419aa..e10120b9 100644 --- a/src/application/Context/State/Code/Position/ICodePosition.ts +++ b/src/application/Context/State/Code/Position/ICodePosition.ts @@ -1,5 +1,5 @@ export interface ICodePosition { - readonly startLine: number; - readonly endLine: number; - readonly totalLines: number; + readonly startLine: number; + readonly endLine: number; + readonly totalLines: number; } diff --git a/src/application/Context/State/Filter/FilterResult.ts b/src/application/Context/State/Filter/FilterResult.ts index 127855c2..2bf8e46d 100644 --- a/src/application/Context/State/Filter/FilterResult.ts +++ b/src/application/Context/State/Filter/FilterResult.ts @@ -1,18 +1,20 @@ -import { IFilterResult } from './IFilterResult'; import { IScript } from '@/domain/IScript'; import { ICategory } from '@/domain/ICategory'; +import { IFilterResult } from './IFilterResult'; export class FilterResult implements IFilterResult { - constructor( - public readonly scriptMatches: ReadonlyArray, - public readonly categoryMatches: ReadonlyArray, - public readonly query: string) { - if (!query) { throw new Error('Query is empty or undefined'); } - if (!scriptMatches) { throw new Error('Script matches is undefined'); } - if (!categoryMatches) { throw new Error('Category matches is undefined'); } - } - public hasAnyMatches(): boolean { - return this.scriptMatches.length > 0 - || this.categoryMatches.length > 0; - } + constructor( + public readonly scriptMatches: ReadonlyArray, + public readonly categoryMatches: ReadonlyArray, + public readonly query: string, + ) { + if (!query) { throw new Error('Query is empty or undefined'); } + if (!scriptMatches) { throw new Error('Script matches is undefined'); } + if (!categoryMatches) { throw new Error('Category matches is undefined'); } + } + + public hasAnyMatches(): boolean { + return this.scriptMatches.length > 0 + || this.categoryMatches.length > 0; + } } diff --git a/src/application/Context/State/Filter/IFilterResult.ts b/src/application/Context/State/Filter/IFilterResult.ts index dc8308eb..8391dc82 100644 --- a/src/application/Context/State/Filter/IFilterResult.ts +++ b/src/application/Context/State/Filter/IFilterResult.ts @@ -1,8 +1,8 @@ import { IScript, ICategory } from '@/domain/ICategory'; export interface IFilterResult { - readonly categoryMatches: ReadonlyArray; - readonly scriptMatches: ReadonlyArray; - readonly query: string; - hasAnyMatches(): boolean; + readonly categoryMatches: ReadonlyArray; + readonly scriptMatches: ReadonlyArray; + readonly query: string; + hasAnyMatches(): boolean; } diff --git a/src/application/Context/State/Filter/IUserFilter.ts b/src/application/Context/State/Filter/IUserFilter.ts index 1cec5486..ebe4c127 100644 --- a/src/application/Context/State/Filter/IUserFilter.ts +++ b/src/application/Context/State/Filter/IUserFilter.ts @@ -2,12 +2,12 @@ import { IEventSource } from '@/infrastructure/Events/IEventSource'; import { IFilterResult } from './IFilterResult'; export interface IReadOnlyUserFilter { - readonly currentFilter: IFilterResult | undefined; - readonly filtered: IEventSource; - readonly filterRemoved: IEventSource; + readonly currentFilter: IFilterResult | undefined; + readonly filtered: IEventSource; + readonly filterRemoved: IEventSource; } export interface IUserFilter extends IReadOnlyUserFilter { - setFilter(filter: string): void; - removeFilter(): void; + setFilter(filter: string): void; + removeFilter(): void; } diff --git a/src/application/Context/State/Filter/UserFilter.ts b/src/application/Context/State/Filter/UserFilter.ts index 32c5b2bd..afeebd4e 100644 --- a/src/application/Context/State/Filter/UserFilter.ts +++ b/src/application/Context/State/Filter/UserFilter.ts @@ -1,52 +1,56 @@ import { IScript } from '@/domain/IScript'; +import { EventSource } from '@/infrastructure/Events/EventSource'; +import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { FilterResult } from './FilterResult'; import { IFilterResult } from './IFilterResult'; import { IUserFilter } from './IUserFilter'; -import { EventSource } from '@/infrastructure/Events/EventSource'; -import { ICategoryCollection } from '@/domain/ICategoryCollection'; export class UserFilter implements IUserFilter { - public readonly filtered = new EventSource(); - public readonly filterRemoved = new EventSource(); - public currentFilter: IFilterResult | undefined; + public readonly filtered = new EventSource(); - constructor(private collection: ICategoryCollection) { + public readonly filterRemoved = new EventSource(); + public currentFilter: IFilterResult | undefined; + + constructor(private collection: ICategoryCollection) { + + } + + public setFilter(filter: string): void { + if (!filter) { + throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter'); } + const filterLowercase = filter.toLocaleLowerCase(); + const filteredScripts = this.collection.getAllScripts().filter( + (script) => isScriptAMatch(script, filterLowercase), + ); + const filteredCategories = this.collection.getAllCategories().filter( + (category) => category.name.toLowerCase().includes(filterLowercase), + ); + const matches = new FilterResult( + filteredScripts, + filteredCategories, + filter, + ); + this.currentFilter = matches; + this.filtered.notify(matches); + } - public setFilter(filter: string): void { - if (!filter) { - throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter'); - } - const filterLowercase = filter.toLocaleLowerCase(); - const filteredScripts = this.collection.getAllScripts().filter( - (script) => isScriptAMatch(script, filterLowercase)); - const filteredCategories = this.collection.getAllCategories().filter( - (category) => category.name.toLowerCase().includes(filterLowercase)); - const matches = new FilterResult( - filteredScripts, - filteredCategories, - filter, - ); - this.currentFilter = matches; - this.filtered.notify(matches); - } - - public removeFilter(): void { - this.currentFilter = undefined; - this.filterRemoved.notify(); - } + public removeFilter(): void { + this.currentFilter = undefined; + this.filterRemoved.notify(); + } } function isScriptAMatch(script: IScript, filterLowercase: string) { - if (script.name.toLowerCase().includes(filterLowercase)) { - return true; - } - if (script.code.execute.toLowerCase().includes(filterLowercase)) { - return true; - } - if (script.code.revert) { - return script.code.revert.toLowerCase().includes(filterLowercase); - } - return false; + if (script.name.toLowerCase().includes(filterLowercase)) { + return true; + } + if (script.code.execute.toLowerCase().includes(filterLowercase)) { + return true; + } + if (script.code.revert) { + return script.code.revert.toLowerCase().includes(filterLowercase); + } + return false; } diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts index 01664bc8..f07497e4 100644 --- a/src/application/Context/State/ICategoryCollectionState.ts +++ b/src/application/Context/State/ICategoryCollectionState.ts @@ -1,18 +1,18 @@ +import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { OperatingSystem } from '@/domain/OperatingSystem'; import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter'; import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection'; import { IApplicationCode } from './Code/IApplicationCode'; -import { ICategoryCollection } from '@/domain/ICategoryCollection'; -import { OperatingSystem } from '@/domain/OperatingSystem'; export interface IReadOnlyCategoryCollectionState { - readonly code: IApplicationCode; - readonly os: OperatingSystem; - readonly filter: IReadOnlyUserFilter; - readonly selection: IReadOnlyUserSelection; - readonly collection: ICategoryCollection; + readonly code: IApplicationCode; + readonly os: OperatingSystem; + readonly filter: IReadOnlyUserFilter; + readonly selection: IReadOnlyUserSelection; + readonly collection: ICategoryCollection; } export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState { - readonly filter: IUserFilter; - readonly selection: IUserSelection; + readonly filter: IUserFilter; + readonly selection: IUserSelection; } diff --git a/src/application/Context/State/Selection/IUserSelection.ts b/src/application/Context/State/Selection/IUserSelection.ts index 1c5d7229..b696bada 100644 --- a/src/application/Context/State/Selection/IUserSelection.ts +++ b/src/application/Context/State/Selection/IUserSelection.ts @@ -1,23 +1,23 @@ -import { SelectedScript } from './SelectedScript'; import { IScript } from '@/domain/IScript'; import { ICategory } from '@/domain/ICategory'; import { IEventSource } from '@/infrastructure/Events/IEventSource'; +import { SelectedScript } from './SelectedScript'; export interface IReadOnlyUserSelection { - readonly changed: IEventSource>; - readonly selectedScripts: ReadonlyArray; - isSelected(scriptId: string): boolean; - areAllSelected(category: ICategory): boolean; - isAnySelected(category: ICategory): boolean; + readonly changed: IEventSource>; + readonly selectedScripts: ReadonlyArray; + isSelected(scriptId: string): boolean; + areAllSelected(category: ICategory): boolean; + isAnySelected(category: ICategory): boolean; } export interface IUserSelection extends IReadOnlyUserSelection { - removeAllInCategory(categoryId: number): void; - addOrUpdateAllInCategory(categoryId: number, revert: boolean): void; - addSelectedScript(scriptId: string, revert: boolean): void; - addOrUpdateSelectedScript(scriptId: string, revert: boolean): void; - removeSelectedScript(scriptId: string): void; - selectOnly(scripts: ReadonlyArray): void; - selectAll(): void; - deselectAll(): void; + removeAllInCategory(categoryId: number): void; + addOrUpdateAllInCategory(categoryId: number, revert: boolean): void; + addSelectedScript(scriptId: string, revert: boolean): void; + addOrUpdateSelectedScript(scriptId: string, revert: boolean): void; + removeSelectedScript(scriptId: string): void; + selectOnly(scripts: ReadonlyArray): void; + selectAll(): void; + deselectAll(): void; } diff --git a/src/application/Context/State/Selection/SelectedScript.ts b/src/application/Context/State/Selection/SelectedScript.ts index de2173d8..e3966a56 100644 --- a/src/application/Context/State/Selection/SelectedScript.ts +++ b/src/application/Context/State/Selection/SelectedScript.ts @@ -2,13 +2,13 @@ import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { IScript } from '@/domain/IScript'; export class SelectedScript extends BaseEntity { - constructor( - public readonly script: IScript, - public readonly revert: boolean, - ) { - super(script.id); - if (revert && !script.canRevert()) { - throw new Error('cannot revert an irreversible script'); - } + constructor( + public readonly script: IScript, + public readonly revert: boolean, + ) { + super(script.id); + if (revert && !script.canRevert()) { + throw new Error('cannot revert an irreversible script'); } + } } diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index 1bf64717..d87f152a 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -1,141 +1,145 @@ -import { SelectedScript } from './SelectedScript'; -import { IUserSelection } from './IUserSelection'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { IScript } from '@/domain/IScript'; import { EventSource } from '@/infrastructure/Events/EventSource'; import { IRepository } from '@/infrastructure/Repository/IRepository'; import { ICategory } from '@/domain/ICategory'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { IUserSelection } from './IUserSelection'; +import { SelectedScript } from './SelectedScript'; export class UserSelection implements IUserSelection { - public readonly changed = new EventSource>(); - private readonly scripts: IRepository; + public readonly changed = new EventSource>(); - constructor( - private readonly collection: ICategoryCollection, - selectedScripts: ReadonlyArray) { - this.scripts = new InMemoryRepository(); - if (selectedScripts && selectedScripts.length > 0) { - for (const script of selectedScripts) { - this.scripts.addItem(script); - } - } - } + private readonly scripts: IRepository; - public areAllSelected(category: ICategory): boolean { - if (this.selectedScripts.length === 0) { - return false; - } - const scripts = category.getAllScriptsRecursively(); - if (this.selectedScripts.length < scripts.length) { - return false; - } - return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id)); + constructor( + private readonly collection: ICategoryCollection, + selectedScripts: ReadonlyArray, + ) { + this.scripts = new InMemoryRepository(); + if (selectedScripts && selectedScripts.length > 0) { + for (const script of selectedScripts) { + this.scripts.addItem(script); + } } + } - public isAnySelected(category: ICategory): boolean { - if (this.selectedScripts.length === 0) { - return false; - } - return this.selectedScripts.some((s) => category.includes(s.script)); + public areAllSelected(category: ICategory): boolean { + if (this.selectedScripts.length === 0) { + return false; } + const scripts = category.getAllScriptsRecursively(); + if (this.selectedScripts.length < scripts.length) { + return false; + } + return scripts.every( + (script) => this.selectedScripts.some((selected) => selected.id === script.id), + ); + } - public removeAllInCategory(categoryId: number): void { - const category = this.collection.findCategory(categoryId); - const scriptsToRemove = category.getAllScriptsRecursively() - .filter((script) => this.scripts.exists(script.id)); - if (!scriptsToRemove.length) { - return; - } - for (const script of scriptsToRemove) { - this.scripts.removeItem(script.id); - } - this.changed.notify(this.scripts.getItems()); + public isAnySelected(category: ICategory): boolean { + if (this.selectedScripts.length === 0) { + return false; } + return this.selectedScripts.some((s) => category.includes(s.script)); + } - public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void { - const category = this.collection.findCategory(categoryId); - const scriptsToAddOrUpdate = category.getAllScriptsRecursively() - .filter((script) => - !this.scripts.exists(script.id) - || this.scripts.getById(script.id).revert !== revert, - ); - if (!scriptsToAddOrUpdate.length) { - return; - } - for (const script of scriptsToAddOrUpdate) { - const selectedScript = new SelectedScript(script, revert); - this.scripts.addOrUpdateItem(selectedScript); - } - this.changed.notify(this.scripts.getItems()); + public removeAllInCategory(categoryId: number): void { + const category = this.collection.findCategory(categoryId); + const scriptsToRemove = category.getAllScriptsRecursively() + .filter((script) => this.scripts.exists(script.id)); + if (!scriptsToRemove.length) { + return; } + for (const script of scriptsToRemove) { + this.scripts.removeItem(script.id); + } + this.changed.notify(this.scripts.getItems()); + } - public addSelectedScript(scriptId: string, revert: boolean): void { - const script = this.collection.findScript(scriptId); - if (!script) { - throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`); - } - const selectedScript = new SelectedScript(script, revert); - this.scripts.addItem(selectedScript); - this.changed.notify(this.scripts.getItems()); + public addOrUpdateAllInCategory(categoryId: number, revert = false): void { + const category = this.collection.findCategory(categoryId); + const scriptsToAddOrUpdate = category.getAllScriptsRecursively() + .filter( + (script) => !this.scripts.exists(script.id) + || this.scripts.getById(script.id).revert !== revert, + ); + if (!scriptsToAddOrUpdate.length) { + return; } + for (const script of scriptsToAddOrUpdate) { + const selectedScript = new SelectedScript(script, revert); + this.scripts.addOrUpdateItem(selectedScript); + } + this.changed.notify(this.scripts.getItems()); + } - public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void { - const script = this.collection.findScript(scriptId); - const selectedScript = new SelectedScript(script, revert); - this.scripts.addOrUpdateItem(selectedScript); - this.changed.notify(this.scripts.getItems()); + public addSelectedScript(scriptId: string, revert: boolean): void { + const script = this.collection.findScript(scriptId); + if (!script) { + throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`); } + const selectedScript = new SelectedScript(script, revert); + this.scripts.addItem(selectedScript); + this.changed.notify(this.scripts.getItems()); + } - public removeSelectedScript(scriptId: string): void { - this.scripts.removeItem(scriptId); - this.changed.notify(this.scripts.getItems()); - } + public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void { + const script = this.collection.findScript(scriptId); + const selectedScript = new SelectedScript(script, revert); + this.scripts.addOrUpdateItem(selectedScript); + this.changed.notify(this.scripts.getItems()); + } - public isSelected(scriptId: string): boolean { - return this.scripts.exists(scriptId); - } + public removeSelectedScript(scriptId: string): void { + this.scripts.removeItem(scriptId); + this.changed.notify(this.scripts.getItems()); + } - /** Get users scripts based on his/her selections */ - public get selectedScripts(): ReadonlyArray { - return this.scripts.getItems(); - } + public isSelected(scriptId: string): boolean { + return this.scripts.exists(scriptId); + } - public selectAll(): void { - for (const script of this.collection.getAllScripts()) { - if (!this.scripts.exists(script.id)) { - const selection = new SelectedScript(script, false); - this.scripts.addItem(selection); - } - } - this.changed.notify(this.scripts.getItems()); - } + /** Get users scripts based on his/her selections */ + public get selectedScripts(): ReadonlyArray { + return this.scripts.getItems(); + } - public deselectAll(): void { - const selectedScriptIds = this.scripts.getItems().map((script) => script.id); - for (const scriptId of selectedScriptIds) { - this.scripts.removeItem(scriptId); - } - this.changed.notify([]); + public selectAll(): void { + for (const script of this.collection.getAllScripts()) { + if (!this.scripts.exists(script.id)) { + const selection = new SelectedScript(script, false); + this.scripts.addItem(selection); + } } + this.changed.notify(this.scripts.getItems()); + } - public selectOnly(scripts: readonly IScript[]): void { - if (!scripts || scripts.length === 0) { - throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything'); - } - // Unselect from selected scripts - if (this.scripts.length !== 0) { - this.scripts.getItems() - .filter((existing) => !scripts.some((script) => existing.id === script.id)) - .map((script) => script.id) - .forEach((scriptId) => this.scripts.removeItem(scriptId)); - } - // Select from unselected scripts - const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id)); - for (const toSelect of unselectedScripts) { - const selection = new SelectedScript(toSelect, false); - this.scripts.addItem(selection); - } - this.changed.notify(this.scripts.getItems()); + public deselectAll(): void { + const selectedScriptIds = this.scripts.getItems().map((script) => script.id); + for (const scriptId of selectedScriptIds) { + this.scripts.removeItem(scriptId); } + this.changed.notify([]); + } + + public selectOnly(scripts: readonly IScript[]): void { + if (!scripts || scripts.length === 0) { + throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything'); + } + // Unselect from selected scripts + if (this.scripts.length !== 0) { + this.scripts.getItems() + .filter((existing) => !scripts.some((script) => existing.id === script.id)) + .map((script) => script.id) + .forEach((scriptId) => this.scripts.removeItem(scriptId)); + } + // Select from unselected scripts + const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id)); + for (const toSelect of unselectedScripts) { + const selection = new SelectedScript(toSelect, false); + this.scripts.addItem(selection); + } + this.changed.notify(this.scripts.getItems()); + } } diff --git a/src/application/Environment/BrowserOs/BrowserOsDetector.ts b/src/application/Environment/BrowserOs/BrowserOsDetector.ts index 8fa5a1f8..3022c3d3 100644 --- a/src/application/Environment/BrowserOs/BrowserOsDetector.ts +++ b/src/application/Environment/BrowserOs/BrowserOsDetector.ts @@ -3,52 +3,55 @@ import { DetectorBuilder } from './DetectorBuilder'; import { IBrowserOsDetector } from './IBrowserOsDetector'; export class BrowserOsDetector implements IBrowserOsDetector { - private readonly detectors = BrowserDetectors; - public detect(userAgent: string): OperatingSystem | undefined { - if (!userAgent) { - return undefined; - } - for (const detector of this.detectors) { - const os = detector.detect(userAgent); - if (os !== undefined) { - return os; - } - } - return undefined; + private readonly detectors = BrowserDetectors; + + public detect(userAgent: string): OperatingSystem | undefined { + if (!userAgent) { + return undefined; } + for (const detector of this.detectors) { + const os = detector.detect(userAgent); + if (os !== undefined) { + return os; + } + } + return undefined; + } } // 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')), +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(); +function define( + os: OperatingSystem, + applyRules: (builder: DetectorBuilder) => DetectorBuilder, +): IBrowserOsDetector { + const builder = new DetectorBuilder(os); + applyRules(builder); + return builder.build(); } diff --git a/src/application/Environment/BrowserOs/DetectorBuilder.ts b/src/application/Environment/BrowserOs/DetectorBuilder.ts index 3940ec40..ea7e0a37 100644 --- a/src/application/Environment/BrowserOs/DetectorBuilder.ts +++ b/src/application/Environment/BrowserOs/DetectorBuilder.ts @@ -1,53 +1,54 @@ -import { IBrowserOsDetector } from './IBrowserOsDetector'; import { OperatingSystem } from '@/domain/OperatingSystem'; +import { IBrowserOsDetector } from './IBrowserOsDetector'; export class DetectorBuilder { - private readonly existingPartsInUserAgent = new Array(); - private readonly notExistingPartsInUserAgent = new Array(); + private readonly existingPartsInUserAgent = new Array(); - constructor(private readonly os: OperatingSystem) { } + private readonly notExistingPartsInUserAgent = new Array(); - public mustInclude(str: string): DetectorBuilder { - return this.add(str, this.existingPartsInUserAgent); + constructor(private readonly os: OperatingSystem) { } + + public mustInclude(str: string): DetectorBuilder { + return this.add(str, this.existingPartsInUserAgent); + } + + public mustNotInclude(str: string): DetectorBuilder { + return this.add(str, this.notExistingPartsInUserAgent); + } + + public build(): IBrowserOsDetector { + if (!this.existingPartsInUserAgent.length) { + throw new Error('Must include at least a part'); } + return { + detect: (agent) => this.detect(agent), + }; + } - public mustNotInclude(str: string): DetectorBuilder { - return this.add(str, this.notExistingPartsInUserAgent); + private detect(userAgent: string): OperatingSystem { + if (!userAgent) { + throw new Error('User agent is null or undefined'); } + if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) { + return undefined; + } + if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) { + return undefined; + } + return this.os; + } - public build(): IBrowserOsDetector { - if (!this.existingPartsInUserAgent.length) { - throw new Error('Must include at least a part'); - } - return { - detect: (agent) => this.detect(agent), - }; + private add(part: string, array: string[]): DetectorBuilder { + if (!part) { + throw new Error('part is empty or undefined'); } - - private detect(userAgent: string): OperatingSystem { - if (!userAgent) { - throw new Error('User agent is null or undefined'); - } - if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) { - return undefined; - } - if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) { - return undefined; - } - return this.os; + if (this.existingPartsInUserAgent.includes(part)) { + throw new Error(`part ${part} is already included as existing part`); } - - private add(part: string, array: string[]): DetectorBuilder { - if (!part) { - throw new Error('part is empty or undefined'); - } - if (this.existingPartsInUserAgent.includes(part)) { - throw new Error(`part ${part} is already included as existing part`); - } - if (this.notExistingPartsInUserAgent.includes(part)) { - throw new Error(`part ${part} is already included as not existing part`); - } - array.push(part); - return this; + if (this.notExistingPartsInUserAgent.includes(part)) { + throw new Error(`part ${part} is already included as not existing part`); } + array.push(part); + return this; + } } diff --git a/src/application/Environment/BrowserOs/IBrowserOsDetector.ts b/src/application/Environment/BrowserOs/IBrowserOsDetector.ts index d09e80ae..368e10f8 100644 --- a/src/application/Environment/BrowserOs/IBrowserOsDetector.ts +++ b/src/application/Environment/BrowserOs/IBrowserOsDetector.ts @@ -1,5 +1,5 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; export interface IBrowserOsDetector { - detect(userAgent: string): OperatingSystem | undefined; + detect(userAgent: string): OperatingSystem | undefined; } diff --git a/src/application/Environment/Environment.ts b/src/application/Environment/Environment.ts index a023f7b8..cb2cf011 100644 --- a/src/application/Environment/Environment.ts +++ b/src/application/Environment/Environment.ts @@ -1,83 +1,89 @@ +import { OperatingSystem } from '@/domain/OperatingSystem'; import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector'; import { IEnvironment } from './IEnvironment'; -import { OperatingSystem } from '@/domain/OperatingSystem'; -interface IEnvironmentVariables { - readonly window: Window & typeof globalThis; - readonly process: NodeJS.Process; - readonly navigator: Navigator; +export 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: typeof process !== 'undefined' ? process /* electron only */ : undefined, - 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); - if (this.isDesktop) { - this.os = getDesktopOsType(getProcessPlatform(variables)); - } else { - const userAgent = getUserAgent(variables); - this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent); - } + public static readonly CurrentEnvironment: IEnvironment = new Environment({ + window, + process: typeof process !== 'undefined' ? process /* electron only */ : undefined, + 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); + if (this.isDesktop) { + this.os = getDesktopOsType(getProcessPlatform(variables)); + } else { + const userAgent = getUserAgent(variables); + this.os = !userAgent ? undefined : browserOsDetector.detect(userAgent); + } + } } function getUserAgent(variables: IEnvironmentVariables): string { - if (!variables.window || !variables.window.navigator) { - return undefined; - } - return variables.window.navigator.userAgent; + 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; + if (!variables.process || !variables.process.platform) { + return undefined; + } + return variables.process.platform; } function getDesktopOsType(processPlatform: string): OperatingSystem | undefined { - // 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 undefined; + // https://nodejs.org/api/process.html#process_process_platform + switch (processPlatform) { + case 'darwin': + return OperatingSystem.macOS; + case 'win32': + return OperatingSystem.Windows; + case 'linux': + return OperatingSystem.Linux; + default: + return undefined; + } } 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; + // 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; } diff --git a/src/application/Environment/IEnvironment.ts b/src/application/Environment/IEnvironment.ts index e2128e2e..1bec8d1b 100644 --- a/src/application/Environment/IEnvironment.ts +++ b/src/application/Environment/IEnvironment.ts @@ -1,6 +1,6 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; export interface IEnvironment { - readonly isDesktop: boolean; - readonly os: OperatingSystem; + readonly isDesktop: boolean; + readonly os: OperatingSystem; } diff --git a/src/application/IApplicationFactory.ts b/src/application/IApplicationFactory.ts index 85c3d69d..25d3d71f 100644 --- a/src/application/IApplicationFactory.ts +++ b/src/application/IApplicationFactory.ts @@ -1,5 +1,5 @@ import { IApplication } from '@/domain/IApplication'; export interface IApplicationFactory { - getApp(): Promise; + getApp(): Promise; } diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts index 28f6464e..a3502906 100644 --- a/src/application/Parser/ApplicationParser.ts +++ b/src/application/Parser/ApplicationParser.ts @@ -1,38 +1,41 @@ +import { CollectionData } from 'js-yaml-loader!@/*'; import { IApplication } from '@/domain/IApplication'; import { IProjectInformation } from '@/domain/IProjectInformation'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; -import { parseCategoryCollection } from './CategoryCollectionParser'; import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml'; import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml'; -import { CollectionData } from 'js-yaml-loader!@/*'; import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser'; import { Application } from '@/domain/Application'; +import { parseCategoryCollection } from './CategoryCollectionParser'; export function parseApplication( - parser = CategoryCollectionParser, - processEnv: NodeJS.ProcessEnv = process.env, - collectionsData = PreParsedCollections): IApplication { - validateCollectionsData(collectionsData); - const information = parseProjectInformation(processEnv); - const collections = collectionsData.map((collection) => parser(collection, information)); - const app = new Application(information, collections); - return app; + parser = CategoryCollectionParser, + processEnv: NodeJS.ProcessEnv = process.env, + collectionsData = PreParsedCollections, +): IApplication { + validateCollectionsData(collectionsData); + const information = parseProjectInformation(processEnv); + const collections = collectionsData.map((collection) => parser(collection, information)); + const app = new Application(information, collections); + return app; } export type CategoryCollectionParserType = (file: CollectionData, info: IProjectInformation) => ICategoryCollection; -const CategoryCollectionParser: CategoryCollectionParserType - = (file, info) => parseCategoryCollection(file, info); +const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => { + return parseCategoryCollection(file, info); +}; -const PreParsedCollections: readonly CollectionData [] - = [ WindowsData, MacOsData ]; +const PreParsedCollections: readonly CollectionData [] = [ + WindowsData, MacOsData, +]; function validateCollectionsData(collections: readonly CollectionData[]) { - if (!collections.length) { - throw new Error('no collection provided'); - } - if (collections.some((collection) => !collection)) { - throw new Error('undefined collection provided'); - } + if (!collections.length) { + throw new Error('no collection provided'); + } + if (collections.some((collection) => !collection)) { + throw new Error('undefined collection provided'); + } } diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index eaef5153..7c0e6ceb 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -1,40 +1,42 @@ -import { Category } from '@/domain/Category'; import { CollectionData } from 'js-yaml-loader!@/*'; -import { parseCategory } from './CategoryParser'; +import { Category } from '@/domain/Category'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import { createEnumParser } from '../Common/Enum'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection'; import { IProjectInformation } from '@/domain/IProjectInformation'; +import { createEnumParser } from '../Common/Enum'; +import { parseCategory } from './CategoryParser'; import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext'; import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; export function parseCategoryCollection( - content: CollectionData, - info: IProjectInformation, - osParser = createEnumParser(OperatingSystem)): ICategoryCollection { - validate(content); - const scripting = new ScriptingDefinitionParser() - .parse(content.scripting, info); - const context = new CategoryCollectionParseContext(content.functions, scripting); - const categories = new Array(); - for (const action of content.actions) { - const category = parseCategory(action, context); - categories.push(category); - } - const os = osParser.parseEnum(content.os, 'os'); - const collection = new CategoryCollection( - os, - categories, - scripting); - return collection; + content: CollectionData, + info: IProjectInformation, + osParser = createEnumParser(OperatingSystem), +): ICategoryCollection { + validate(content); + const scripting = new ScriptingDefinitionParser() + .parse(content.scripting, info); + const context = new CategoryCollectionParseContext(content.functions, scripting); + const categories = new Array(); + for (const action of content.actions) { + const category = parseCategory(action, context); + categories.push(category); + } + const os = osParser.parseEnum(content.os, 'os'); + const collection = new CategoryCollection( + os, + categories, + scripting, + ); + return collection; } function validate(content: CollectionData): void { - if (!content) { - throw new Error('content is null or undefined'); - } - if (!content.actions || content.actions.length <= 0) { - throw new Error('content does not define any action'); - } + if (!content) { + throw new Error('content is null or undefined'); + } + if (!content.actions || content.actions.length <= 0) { + throw new Error('content does not define any action'); + } } diff --git a/src/application/Parser/CategoryParser.ts b/src/application/Parser/CategoryParser.ts index 2ccaadb4..30939f9e 100644 --- a/src/application/Parser/CategoryParser.ts +++ b/src/application/Parser/CategoryParser.ts @@ -1,71 +1,86 @@ -import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*'; +import { + CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder, +} from 'js-yaml-loader!@/*'; import { Script } from '@/domain/Script'; import { Category } from '@/domain/Category'; import { parseDocUrls } from './DocumentationParser'; import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext'; import { parseScript } from './Script/ScriptParser'; -let categoryIdCounter: number = 0; +let categoryIdCounter = 0; interface ICategoryChildren { - subCategories: Category[]; - subScripts: Script[]; + subCategories: Category[]; + subScripts: Script[]; } -export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category { - if (!context) { throw new Error('undefined context'); } - ensureValid(category); - const children: ICategoryChildren = { - subCategories: new Array(), - subScripts: new Array @@ -27,41 +28,40 @@ export default class IconButton extends Vue { @use "@/presentation/assets/styles/main" as *; .button { - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; - background-color: $color-secondary; - color: $color-on-secondary; + background-color: $color-secondary; + color: $color-on-secondary; - border: none; - padding:20px; - transition-duration: 0.4s; - overflow: hidden; - box-shadow: 0 3px 9px $color-primary-darkest; - border-radius: 4px; + border: none; + padding:20px; + transition-duration: 0.4s; + overflow: hidden; + box-shadow: 0 3px 9px $color-primary-darkest; + border-radius: 4px; - cursor: pointer; - width: 10%; - min-width: 90px; - &:hover { - background: $color-surface; - box-shadow: 0px 2px 10px 5px $color-secondary; - } - &:hover>&__text { - display: block; - } - &:hover>&__icon { - display: none; - } - &__text { - display: none; - font-family: $font-artistic; - font-size: 1.5em; - color: $color-primary; - font-weight: 500; - line-height: 1.1; - } + cursor: pointer; + width: 10%; + min-width: 90px; + &:hover { + background: $color-surface; + box-shadow: 0px 2px 10px 5px $color-secondary; } - + &:hover>&__text { + display: block; + } + &:hover>&__icon { + display: none; + } + &__text { + display: none; + font-family: $font-artistic; + font-size: 1.5em; + color: $color-primary; + font-weight: 500; + line-height: 1.1; + } +} diff --git a/src/presentation/components/Code/CodeButtons/MacOsInstructions.vue b/src/presentation/components/Code/CodeButtons/MacOsInstructions.vue index 88a80202..f55eae3b 100644 --- a/src/presentation/components/Code/CodeButtons/MacOsInstructions.vue +++ b/src/presentation/components/Code/CodeButtons/MacOsInstructions.vue @@ -1,98 +1,106 @@ @@ -27,11 +31,11 @@ export default class MenuOptionListItem extends Vue { .enabled { cursor: pointer; &:hover { - font-weight:bold; - text-decoration:underline; - } + font-weight:bold; + text-decoration:underline; + } } .disabled { - color: $color-primary-light; + color: $color-primary-light; } - \ No newline at end of file + diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts index 456f6a30..90d76f57 100644 --- a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts +++ b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeHandler.ts @@ -5,82 +5,85 @@ import { scrambledEqual } from '@/application/Common/Array'; import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState'; export enum SelectionType { - Standard, - Strict, - All, - None, - Custom, + Standard, + Strict, + All, + None, + Custom, } export class SelectionTypeHandler { - constructor(private readonly state: ICategoryCollectionState) { - if (!state) { throw new Error('undefined state'); } + constructor(private readonly state: ICategoryCollectionState) { + if (!state) { throw new Error('undefined state'); } + } + + public selectType(type: SelectionType) { + if (type === SelectionType.Custom) { + throw new Error('cannot select custom type'); } - public selectType(type: SelectionType) { - if (type === SelectionType.Custom) { - throw new Error('cannot select custom type'); - } - const selector = selectors.get(type); - selector.select(this.state); - } - public getCurrentSelectionType(): SelectionType { - for (const [type, selector] of Array.from(selectors.entries())) { - if (selector.isSelected(this.state)) { - return type; - } - } - return SelectionType.Custom; + const selector = selectors.get(type); + selector.select(this.state); + } + + public getCurrentSelectionType(): SelectionType { + for (const [type, selector] of Array.from(selectors.entries())) { + if (selector.isSelected(this.state)) { + return type; + } } + return SelectionType.Custom; + } } interface ISingleTypeHandler { - isSelected: (state: IReadOnlyCategoryCollectionState) => boolean; - select: (state: ICategoryCollectionState) => void; + isSelected: (state: IReadOnlyCategoryCollectionState) => boolean; + select: (state: ICategoryCollectionState) => void; } const selectors = new Map([ - [SelectionType.None, { - select: (state) => - state.selection.deselectAll(), - isSelected: (state) => - state.selection.selectedScripts.length === 0, - }], - [SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], - [SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], - [SelectionType.All, { - select: (state) => - state.selection.selectAll(), - isSelected: (state) => - state.selection.selectedScripts.length === state.collection.totalScripts, - }], + [SelectionType.None, { + select: (state) => state.selection.deselectAll(), + isSelected: (state) => state.selection.selectedScripts.length === 0, + }], + [SelectionType.Standard, getRecommendationLevelSelector(RecommendationLevel.Standard)], + [SelectionType.Strict, getRecommendationLevelSelector(RecommendationLevel.Strict)], + [SelectionType.All, { + select: (state) => state.selection.selectAll(), + isSelected: (state) => state.selection.selectedScripts.length === state.collection.totalScripts, + }], ]); function getRecommendationLevelSelector(level: RecommendationLevel): ISingleTypeHandler { - return { - select: (state) => selectOnly(level, state), - isSelected: (state) => hasAllSelectedLevelOf(level, state), - }; + return { + select: (state) => selectOnly(level, state), + isSelected: (state) => hasAllSelectedLevelOf(level, state), + }; } -function hasAllSelectedLevelOf(level: RecommendationLevel, state: IReadOnlyCategoryCollectionState) { - const scripts = state.collection.getScriptsByLevel(level); - const selectedScripts = state.selection.selectedScripts; - return areAllSelected(scripts, selectedScripts); +function hasAllSelectedLevelOf( + level: RecommendationLevel, + state: IReadOnlyCategoryCollectionState, +) { + const scripts = state.collection.getScriptsByLevel(level); + const { selectedScripts } = state.selection; + return areAllSelected(scripts, selectedScripts); } function selectOnly(level: RecommendationLevel, state: ICategoryCollectionState) { - const scripts = state.collection.getScriptsByLevel(level); - state.selection.selectOnly(scripts); + const scripts = state.collection.getScriptsByLevel(level); + state.selection.selectOnly(scripts); } function areAllSelected( - expectedScripts: ReadonlyArray, - selection: ReadonlyArray): boolean { - selection = selection.filter((selected) => !selected.revert); - if (expectedScripts.length < selection.length) { - return false; - } - const selectedScriptIds = selection.map((script) => script.id); - const expectedScriptIds = expectedScripts.map((script) => script.id); - return scrambledEqual(selectedScriptIds, expectedScriptIds); + expectedScripts: ReadonlyArray, + selection: ReadonlyArray, +): boolean { + const selectedScriptIds = selection + .filter((selected) => !selected.revert) + .map((script) => script.id); + if (expectedScripts.length < selectedScriptIds.length) { + return false; + } + const expectedScriptIds = expectedScripts.map((script) => script.id); + return scrambledEqual(selectedScriptIds, expectedScriptIds); } diff --git a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue index 2053c220..ec57a60a 100644 --- a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue +++ b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue @@ -1,46 +1,46 @@ diff --git a/src/presentation/components/Scripts/Menu/TheOsChanger.vue b/src/presentation/components/Scripts/Menu/TheOsChanger.vue index 8bcd4e13..a6ce435f 100644 --- a/src/presentation/components/Scripts/Menu/TheOsChanger.vue +++ b/src/presentation/components/Scripts/Menu/TheOsChanger.vue @@ -1,11 +1,11 @@ @@ -26,6 +26,7 @@ import MenuOptionListItem from './MenuOptionListItem.vue'; }) export default class TheOsChanger extends StatefulVue { public allOses: Array<{ name: string, os: OperatingSystem }> = []; + public currentOs?: OperatingSystem = null; public async created() { @@ -33,6 +34,7 @@ export default class TheOsChanger extends StatefulVue { this.allOses = app.getSupportedOsList() .map((os) => ({ os, name: renderOsName(os) })); } + public async changeOs(newOs: OperatingSystem) { const context = await this.getCurrentContext(); context.changeContext(newOs); @@ -45,11 +47,11 @@ export default class TheOsChanger extends StatefulVue { } 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]}`); - } + switch (os) { + case OperatingSystem.Windows: return 'Windows'; + case OperatingSystem.macOS: return 'macOS'; + default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`); + } } diff --git a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue index b7764b0a..2c11e967 100644 --- a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue +++ b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue @@ -1,22 +1,22 @@ @@ -63,18 +61,18 @@ $margin-between-lines: 7px; flex-wrap: wrap; margin-top: -$margin-between-lines; .item { - flex: 1; - white-space: nowrap; - display: flex; - justify-content: center; - align-items: center; - margin: $margin-between-lines 5px 0 5px; - &:first-child { - justify-content: flex-start; - } - &:last-child { - justify-content: flex-end; - } + flex: 1; + white-space: nowrap; + display: flex; + justify-content: center; + align-items: center; + margin: $margin-between-lines 5px 0 5px; + &:first-child { + justify-content: flex-start; + } + &:last-child { + justify-content: flex-end; + } } } diff --git a/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue b/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue index c1206fda..0109109d 100644 --- a/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue +++ b/src/presentation/components/Scripts/Menu/View/TheViewChanger.vue @@ -1,21 +1,21 @@ diff --git a/src/presentation/components/Scripts/Menu/View/ViewType.ts b/src/presentation/components/Scripts/Menu/View/ViewType.ts index 68eebb00..c926c591 100644 --- a/src/presentation/components/Scripts/Menu/View/ViewType.ts +++ b/src/presentation/components/Scripts/Menu/View/ViewType.ts @@ -1,4 +1,4 @@ export enum ViewType { - Cards = 1, - Tree = 0, + Cards = 1, + Tree = 0, } diff --git a/src/presentation/components/Scripts/Slider/Handle.vue b/src/presentation/components/Scripts/Slider/Handle.vue index 665953b5..e35881a4 100644 --- a/src/presentation/components/Scripts/Slider/Handle.vue +++ b/src/presentation/components/Scripts/Slider/Handle.vue @@ -1,15 +1,15 @@ @@ -49,27 +52,27 @@ $color : $color-primary-dark; $color-hover : $color-primary; .handle { - user-select: none; - display: flex; - flex-direction: column; - align-items: center; - &:hover { - .line { - background: $color-hover; - } - .image { - color: $color-hover; - } - } + user-select: none; + display: flex; + flex-direction: column; + align-items: center; + &:hover { .line { - flex: 1; - background: $color; - width: 3px; + background: $color-hover; } - .icon { - color: $color; + .image { + color: $color-hover; } - margin-right: 5px; - margin-left: 5px; + } + .line { + flex: 1; + background: $color; + width: 3px; + } + .icon { + color: $color; + } + margin-right: 5px; + margin-left: 5px; } diff --git a/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue b/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue index 7380fb27..c7f5015a 100644 --- a/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue +++ b/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue @@ -1,18 +1,18 @@ @@ -43,27 +46,27 @@ export default class HorizontalResizeSlider extends Vue { @use "@/presentation/assets/styles/main" as *; .slider { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; + .first { + min-width: var(--first-min-width); + width: var(--first-initial-width); + } + .second { + flex: 1; + min-width: var(--second-min-width); + } + @media screen and (max-width: $media-vertical-view-breakpoint) { + flex-direction: column; .first { - min-width: var(--first-min-width); - width: var(--first-initial-width); + width: auto !important; } .second { - flex: 1; - min-width: var(--second-min-width); + margin-top: var(--vertical-margin); } - @media screen and (max-width: $media-vertical-view-breakpoint) { - flex-direction: column; - .first { - width: auto !important; - } - .second { - margin-top: var(--vertical-margin); - } - .handle { - display: none; - } + .handle { + display: none; } + } } diff --git a/src/presentation/components/Scripts/TheScriptArea.vue b/src/presentation/components/Scripts/TheScriptArea.vue index b5cbb6b9..0f7d12b2 100644 --- a/src/presentation/components/Scripts/TheScriptArea.vue +++ b/src/presentation/components/Scripts/TheScriptArea.vue @@ -1,17 +1,17 @@ diff --git a/src/presentation/components/Scripts/View/Cards/CardList.vue b/src/presentation/components/Scripts/View/Cards/CardList.vue index 95aa225a..026a0840 100644 --- a/src/presentation/components/Scripts/View/Cards/CardList.vue +++ b/src/presentation/components/Scripts/View/Cards/CardList.vue @@ -1,16 +1,21 @@ @@ -112,9 +121,10 @@ function isClickable(element: Element) { flex-flow: row wrap; font-family: $font-main; gap: $card-gap; - /* + /* Padding is used to allow scale animation (growing size) for cards on hover. - It ensures that there's room to grow, so the animation is shown without overflowing with scrollbars. + It ensures that there's room to grow, so the animation is shown without overflowing + with scrollbars. */ padding: 10px; } @@ -125,4 +135,4 @@ function isClickable(element: Element) { font-size: 3.5em; font-family: $font-normal; } - \ No newline at end of file + diff --git a/src/presentation/components/Scripts/View/Cards/CardListItem.vue b/src/presentation/components/Scripts/View/Cards/CardListItem.vue index 406537b9..263908db 100644 --- a/src/presentation/components/Scripts/View/Cards/CardListItem.vue +++ b/src/presentation/components/Scripts/View/Cards/CardListItem.vue @@ -1,38 +1,49 @@ - \ No newline at end of file + diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode.ts index ddd58391..a322dca4 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode.ts +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/INode.ts @@ -1,13 +1,13 @@ export enum NodeType { - Script, - Category, + Script, + Category, } export interface INode { - readonly id: string; - readonly text: string; - readonly isReversible: boolean; - readonly documentationUrls: ReadonlyArray; - readonly children?: ReadonlyArray; - readonly type: NodeType; + readonly id: string; + readonly text: string; + readonly isReversible: boolean; + readonly documentationUrls: ReadonlyArray; + readonly children?: ReadonlyArray; + readonly type: NodeType; } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Node.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Node.vue index 850a2c69..aa690a8d 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Node.vue +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Node.vue @@ -1,36 +1,34 @@ - - \ No newline at end of file + diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue index 57f76a81..4287a0ae 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/RevertToggle.vue @@ -1,55 +1,55 @@ - - \ No newline at end of file + diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/CategoryReverter.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/CategoryReverter.ts index 996bdb16..9bed8b5d 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/CategoryReverter.ts +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/CategoryReverter.ts @@ -1,30 +1,34 @@ -import { IReverter } from './IReverter'; -import { getCategoryId } from '../../../ScriptNodeParser'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; -import { ScriptReverter } from './ScriptReverter'; import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection'; import { ICategoryCollection } from '@/domain/ICategoryCollection'; +import { getCategoryId } from '../../../ScriptNodeParser'; +import { IReverter } from './IReverter'; +import { ScriptReverter } from './ScriptReverter'; export class CategoryReverter implements IReverter { - private readonly categoryId: number; - private readonly scriptReverters: ReadonlyArray; - constructor(nodeId: string, collection: ICategoryCollection) { - this.categoryId = getCategoryId(nodeId); - this.scriptReverters = getAllSubScriptReverters(this.categoryId, collection); - } - public getState(selectedScripts: ReadonlyArray): boolean { - return this.scriptReverters.every((script) => script.getState(selectedScripts)); - } - public selectWithRevertState(newState: boolean, selection: IUserSelection): void { - selection.addOrUpdateAllInCategory(this.categoryId, newState); - } + private readonly categoryId: number; + + private readonly scriptReverters: ReadonlyArray; + + constructor(nodeId: string, collection: ICategoryCollection) { + this.categoryId = getCategoryId(nodeId); + this.scriptReverters = getAllSubScriptReverters(this.categoryId, collection); + } + + public getState(selectedScripts: ReadonlyArray): boolean { + return this.scriptReverters.every((script) => script.getState(selectedScripts)); + } + + public selectWithRevertState(newState: boolean, selection: IUserSelection): void { + selection.addOrUpdateAllInCategory(this.categoryId, newState); + } } function getAllSubScriptReverters(categoryId: number, collection: ICategoryCollection) { - const category = collection.findCategory(categoryId); - if (!category) { - throw new Error(`Category with id "${categoryId}" does not exist`); - } - const scripts = category.getAllScriptsRecursively(); - return scripts.map((script) => new ScriptReverter(script.id)); + const category = collection.findCategory(categoryId); + if (!category) { + throw new Error(`Category with id "${categoryId}" does not exist`); + } + const scripts = category.getAllScriptsRecursively(); + return scripts.map((script) => new ScriptReverter(script.id)); } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/IReverter.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/IReverter.ts index ba2fa05e..11b70273 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/IReverter.ts +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/IReverter.ts @@ -2,6 +2,6 @@ import { IUserSelection } from '@/application/Context/State/Selection/IUserSelec import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; export interface IReverter { - getState(selectedScripts: ReadonlyArray): boolean; - selectWithRevertState(newState: boolean, selection: IUserSelection): void; + getState(selectedScripts: ReadonlyArray): boolean; + selectWithRevertState(newState: boolean, selection: IUserSelection): void; } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ReverterFactory.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ReverterFactory.ts index 9c8ada7f..5c1839bd 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ReverterFactory.ts +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ReverterFactory.ts @@ -1,16 +1,16 @@ +import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { INode, NodeType } from '../INode'; import { IReverter } from './IReverter'; import { ScriptReverter } from './ScriptReverter'; import { CategoryReverter } from './CategoryReverter'; -import { ICategoryCollection } from '@/domain/ICategoryCollection'; export function getReverter(node: INode, collection: ICategoryCollection): IReverter { - switch (node.type) { - case NodeType.Category: - return new CategoryReverter(node.id, collection); - case NodeType.Script: - return new ScriptReverter(node.id); - default: - throw new Error('Unknown script type'); - } + switch (node.type) { + case NodeType.Category: + return new CategoryReverter(node.id, collection); + case NodeType.Script: + return new ScriptReverter(node.id); + default: + throw new Error('Unknown script type'); + } } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ScriptReverter.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ScriptReverter.ts index 2e6e86ba..fd3d8de6 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ScriptReverter.ts +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Reverter/ScriptReverter.ts @@ -1,21 +1,24 @@ -import { IReverter } from './IReverter'; -import { getScriptId } from '../../../ScriptNodeParser'; import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript'; import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection'; +import { getScriptId } from '../../../ScriptNodeParser'; +import { IReverter } from './IReverter'; export class ScriptReverter implements IReverter { - private readonly scriptId: string; - constructor(nodeId: string) { - this.scriptId = getScriptId(nodeId); - } - public getState(selectedScripts: ReadonlyArray): boolean { - const selectedScript = selectedScripts.find((selected) => selected.id === this.scriptId); - if (!selectedScript) { - return false; - } - return selectedScript.revert; - } - public selectWithRevertState(newState: boolean, selection: IUserSelection): void { - selection.addOrUpdateSelectedScript(this.scriptId, newState); + private readonly scriptId: string; + + constructor(nodeId: string) { + this.scriptId = getScriptId(nodeId); + } + + public getState(selectedScripts: ReadonlyArray): boolean { + const selectedScript = selectedScripts.find((selected) => selected.id === this.scriptId); + if (!selectedScript) { + return false; } + return selectedScript.revert; + } + + public selectWithRevertState(newState: boolean, selection: IUserSelection): void { + selection.addOrUpdateSelectedScript(this.scriptId, newState); + } } diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/SelectableTree.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/SelectableTree.vue index c376b5f0..294f93ad 100644 --- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/SelectableTree.vue +++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/SelectableTree.vue @@ -1,137 +1,163 @@ diff --git a/src/presentation/components/Scripts/View/TheScriptsView.vue b/src/presentation/components/Scripts/View/TheScriptsView.vue index 3f915875..5643b452 100644 --- a/src/presentation/components/Scripts/View/TheScriptsView.vue +++ b/src/presentation/components/Scripts/View/TheScriptsView.vue @@ -1,38 +1,41 @@ @@ -100,57 +109,58 @@ export default class TheScriptsView extends StatefulVue { $margin-inner: 4px; .scripts { - margin-top: $margin-inner; - @media screen and (min-width: $media-vertical-view-breakpoint) { // so the current code is always visible - overflow: auto; - max-height: 70vh; - } - .tree { - padding-left: 3%; - padding-top: 15px; - padding-bottom: 15px; - &--searching { - padding-top: 0px; - } + margin-top: $margin-inner; + @media screen and (min-width: $media-vertical-view-breakpoint) { + // so the current code is always visible + overflow: auto; + max-height: 70vh; + } + .tree { + padding-left: 3%; + padding-top: 15px; + padding-bottom: 15px; + &--searching { + padding-top: 0px; } + } } .search { + display: flex; + flex-direction: column; + background-color: $color-primary-darker; + &__query { display: flex; + justify-content: center; + flex-direction: row; + align-items: center; + margin-top: 1em; + color: $color-primary; + &__close-button { + cursor: pointer; + font-size: 1.25em; + margin-left: 0.25rem; + &:hover { + color: $color-primary-dark; + } + } + } + &-no-matches { + display:flex; flex-direction: column; - background-color: $color-primary-darker; - &__query { - display: flex; - justify-content: center; - flex-direction: row; - align-items: center; - margin-top: 1em; - color: $color-primary; - &__close-button { - cursor: pointer; - font-size: 1.25em; - margin-left: 0.25rem; - &:hover { - color: $color-primary-dark; - } - } + word-break:break-word; + text-transform: uppercase; + color: $color-on-primary; + font-size: 1.5em; + padding:10px; + text-align:center; + > div { + padding-bottom:13px; } - &-no-matches { - display:flex; - flex-direction: column; - word-break:break-word; - text-transform: uppercase; - color: $color-on-primary; - font-size: 1.5em; - padding:10px; - text-align:center; - > div { - padding-bottom:13px; - } - a { - color: $color-primary; - } + a { + color: $color-primary; } + } } - \ No newline at end of file + diff --git a/src/presentation/components/Shared/Dialog.vue b/src/presentation/components/Shared/Dialog.vue index 99862b6f..11379983 100644 --- a/src/presentation/components/Shared/Dialog.vue +++ b/src/presentation/components/Shared/Dialog.vue @@ -1,12 +1,12 @@