diff --git a/docs/architecture.md b/docs/architecture.md
index e3bce184..21ca666e 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -27,6 +27,8 @@ Application layer depends on and consumes domain layer. [Presentation layer](./p
State handling uses an event-driven subscription model to signal state changes and special functions to register changes. It does not depend on third party packages.
+The presentation layer can read and modify state through the context. State changes trigger events that components can subscribe to for reactivity.
+
Each layer treat application layer differently.

@@ -45,7 +47,7 @@ Each layer treat application layer differently.
- So state is mutable, and fires related events when mutated.
- 📖 Read more: [application.md | Application state](./application.md#application-state).
-It's comparable with flux ([`redux`](https://redux.js.org/)) or flux-like ([`vuex`](https://vuex.vuejs.org/)) patterns. Flux component "view" is [presentation layer](./presentation.md) in Vue. Flux functions "dispatcher", "store" and "action creation" functions lie in the [application layer](./application.md). A difference is that application state in privacy.sexy is mutable and lies in single flux "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
+It's comparable with `flux`, `vuex`, and `pinia`. A difference is that mutable application layer state in privacy.sexy is mutable and lies in single "store" that holds app state and logic. The "actions" mutate the state directly which in turns act as dispatcher to notify its own event subscriptions (callbacks).
## AWS infrastructure
diff --git a/docs/presentation.md b/docs/presentation.md
index 1a3b99c3..85c64bc9 100644
--- a/docs/presentation.md
+++ b/docs/presentation.md
@@ -1,8 +1,10 @@
# Presentation layer
-Presentation layer consists of UI-related code. It uses Vue.js as JavaScript framework and includes Vue.js components. It also includes [Electron](https://www.electronjs.org/) to provide functionality to desktop application.
+The presentation layer handles UI concerns using Vue as JavaScript framework and Electron to provide desktop functionality.
-It's designed event-driven from bottom to top. It listens user events (from top) and state events (from bottom) to update state or the GUI.
+It reflects the [application state](./application.md#application-state) and allows user interactions to modify it. Components manage their own local UI state.
+
+The presentation layer uses an event-driven architecture for bidirectional reactivity between the application state and UI. State change events flow bottom-up to trigger UI updates, while user events flow top-down through components, some ultimately modifying the application state.
📖 Refer to [architecture.md (Layered Application)](./architecture.md#layered-application) to read more about the layered architecture.
@@ -12,6 +14,7 @@ It's designed event-driven from bottom to top. It listens user events (from top)
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue global objects including components and plugins.
- [**`components/`**](./../src/presentation/components/): Contains all Vue components and their helper classes.
- [**`Shared/`**](./../src/presentation/components/Shared): Contains Vue components and component helpers that other components share.
+ - [**`hooks`**](../src/presentation/components/Shared/Hooks): Shared hooks for state access
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets that webpack will process.
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles used throughout different components.
@@ -32,7 +35,7 @@ Add visual clues for clickable items. It should be as clear as possible that the
## Application data
-Components (should) use [ApplicationFactory](./../src/application/ApplicationFactory.ts) singleton to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
+Components (should) use [`UseApplication`](./../src/presentation/components/Shared/Hooks/UseApplication.ts) to reach the application domain to avoid [parsing and compiling](./application.md#parsing-and-compiling) the application again.
[Application.ts](../src/domain/Application.ts) is an immutable domain model that represents application state. It includes:
@@ -43,32 +46,33 @@ You can read more about how application layer provides application data to he pr
## Application state
-Inheritance of a Vue components marks whether it uses application state . Components that does not handle application state extends `Vue`. Stateful components mutate or/and react to state changes (such as user selection or search queries) in [ApplicationContext](./../src/application/Context/ApplicationContext.ts) extend [`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) class to access the context / state.
+This project uses a singleton instance of the application state, making it available to all Vue components.
-[`StatefulVue`](./../src/presentation/components/Shared/StatefulVue.ts) functions include:
+The decision to not use third-party state management libraries like [`vuex`](https://web.archive.org/web/20230801191617/https://vuex.vuejs.org/) or [`pinia`](https://web.archive.org/web/20230801191743/https://pinia.vuejs.org/) was made to promote code independence and enhance portability.
-- Creating a singleton of the state and makes it available to presentation layer as single source of truth.
-- Providing virtual abstract `handleCollectionState` callback that it calls when
- - the Vue loads the component,
- - and also every time when state changes.
-- Providing `events` member to make lifecycling of state subscriptions events easier because it ensures that components unsubscribe from listening to state events when
- - the component is no longer used (destroyed),
- - an if [ApplicationContext](./../src/application/Context/ApplicationContext.ts) changes the active [collection](./collection-files.md) to a different one.
+Stateful components can mutate and/or react to state changes (e.g., user selection, search queries) in the [ApplicationContext](./../src/application/Context/ApplicationContext.ts). Vue components import [`CollectionState.ts`](./../src/presentation/components/Shared/CollectionState.ts) to access both the application context and the state.
-📖 Refer to [architecture.md | Application State](./architecture.md#application-state) to get an overview of event handling and [application.md | Application State](./presentation.md#application-state) for deeper look into how the application layer manages state.
+[`CollectionState.ts`](./../src/presentation/components/Shared/CollectionState.ts) provides several functionalities including:
+
+- **Singleton State Instance**: It creates a singleton instance of the state, which is shared across the presentation layer. The singleton instance ensures that there's a single source of truth for the application's state.
+- **State Change Callback and Lifecycle Management**: It offers a mechanism to register callbacks, which will be invoked when the state initializes or mutates. It ensures that components unsubscribe from state events when they are no longer in use or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
+- **State Access and Modification**: It provides functions to read and mutate for accessing and modifying the state, encapsulating the details of these operations.
+- **Event Subscription Lifecycle Management**: Includes an `events` member that simplifies state subscription lifecycle events. This ensures that components unsubscribe from state events when they are no longer in use, or when [ApplicationContext](./../src/application/Context/ApplicationContext.ts) switches the active [collection](./collection-files.md).
+
+📖 Refer to [architecture.md | Application State](./architecture.md#application-state) for an overview of event handling and [application.md | Application State](./presentation.md#application-state) for an in-depth understanding of state management in the application layer.
## Modals
-[Dialog.vue](./../src/presentation/components/Shared/Dialog.vue) is a shared component that other components used to show modal windows.
+[ModalDialog.vue](./../src/presentation/components/Shared/ModalDialog.vue) is a shared component utilized for rendering modal windows.
-You can use it by wrapping the content inside of its `slot` and call `.show()` function on its reference. For example:
+Use the component by wrapping the desired content within its slot and calling the .show() function on its reference, as shown below:
- ```html
-
-
Show dialog
- ```
+```html
+
+ Hello world
+
+ Show dialog
+```
## Sass naming convention
diff --git a/package-lock.json b/package-lock.json
index 672e5103..851bb873 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,7 +6,7 @@
"packages": {
"": {
"name": "privacy.sexy",
- "version": "0.11.4",
+ "version": "0.12.0",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0",
@@ -26,9 +26,7 @@
"npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14",
- "vue-class-component": "^7.2.6",
- "vue-js-modal": "^2.0.1",
- "vue-property-decorator": "^9.1.2"
+ "vue-js-modal": "^2.0.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
@@ -25610,14 +25608,6 @@
"csstype": "^3.1.0"
}
},
- "node_modules/vue-class-component": {
- "version": "7.2.6",
- "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz",
- "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==",
- "peerDependencies": {
- "vue": "^2.0.0"
- }
- },
"node_modules/vue-cli-plugin-electron-builder": {
"version": "3.0.0-alpha.4",
"resolved": "https://registry.npmmirror.com/vue-cli-plugin-electron-builder/-/vue-cli-plugin-electron-builder-3.0.0-alpha.4.tgz",
@@ -25886,15 +25876,6 @@
"node": ">=8"
}
},
- "node_modules/vue-property-decorator": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz",
- "integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==",
- "peerDependencies": {
- "vue": "*",
- "vue-class-component": "*"
- }
- },
"node_modules/vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
@@ -45848,12 +45829,6 @@
"csstype": "^3.1.0"
}
},
- "vue-class-component": {
- "version": "7.2.6",
- "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz",
- "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==",
- "requires": {}
- },
"vue-cli-plugin-electron-builder": {
"version": "3.0.0-alpha.4",
"resolved": "https://registry.npmmirror.com/vue-cli-plugin-electron-builder/-/vue-cli-plugin-electron-builder-3.0.0-alpha.4.tgz",
@@ -46055,12 +46030,6 @@
}
}
},
- "vue-property-decorator": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz",
- "integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==",
- "requires": {}
- },
"vue-resize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-1.0.1.tgz",
diff --git a/package.json b/package.json
index b2e8210c..c800b137 100644
--- a/package.json
+++ b/package.json
@@ -42,9 +42,7 @@
"npm": "^9.8.1",
"v-tooltip": "2.1.3",
"vue": "^2.7.14",
- "vue-class-component": "^7.2.6",
- "vue-js-modal": "^2.0.1",
- "vue-property-decorator": "^9.1.2"
+ "vue-js-modal": "^2.0.1"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.2",
@@ -85,10 +83,10 @@
"sass-loader": "^13.3.2",
"svgexport": "^0.4.2",
"ts-loader": "^9.4.4",
+ "tslib": "~2.4.0",
"typescript": "~4.6.2",
"vue-cli-plugin-electron-builder": "^3.0.0-alpha.4",
- "yaml-lint": "^1.7.0",
- "tslib": "~2.4.0"
+ "yaml-lint": "^1.7.0"
},
"overrides": {
"vue-cli-plugin-electron-builder": {
diff --git a/src/presentation/components/App.vue b/src/presentation/components/App.vue
index 2cc64f65..b2c7b043 100644
--- a/src/presentation/components/App.vue
+++ b/src/presentation/components/App.vue
@@ -11,14 +11,14 @@
diff --git a/src/presentation/components/Scripts/Menu/TheOsChanger.vue b/src/presentation/components/Scripts/Menu/TheOsChanger.vue
index e6e8b85d..f01c296d 100644
--- a/src/presentation/components/Scripts/Menu/TheOsChanger.vue
+++ b/src/presentation/components/Scripts/Menu/TheOsChanger.vue
@@ -1,7 +1,7 @@
-
-
diff --git a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue
index bcbb70c6..393b78b1 100644
--- a/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue
+++ b/src/presentation/components/Scripts/Menu/TheScriptsMenu.vue
@@ -5,53 +5,55 @@
+ v-if="!isSearching" />
diff --git a/src/presentation/components/Scripts/Slider/Handle.vue b/src/presentation/components/Scripts/Slider/Handle.vue
deleted file mode 100644
index dc66d977..00000000
--- a/src/presentation/components/Scripts/Slider/Handle.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue b/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue
index 6a2f1125..197524c5 100644
--- a/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue
+++ b/src/presentation/components/Scripts/Slider/HorizontalResizeSlider.vue
@@ -2,16 +2,16 @@
-
+
@@ -19,30 +19,45 @@
diff --git a/src/presentation/components/Scripts/TheScriptArea.vue b/src/presentation/components/Scripts/TheScriptArea.vue
index 41e172ec..74d97ff4 100644
--- a/src/presentation/components/Scripts/TheScriptArea.vue
+++ b/src/presentation/components/Scripts/TheScriptArea.vue
@@ -19,24 +19,26 @@
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/INodeSelectedEvent.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/INodeSelectedEvent.ts
index b6269cd2..dd340c99 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/INodeSelectedEvent.ts
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/INodeSelectedEvent.ts
@@ -1,6 +1,6 @@
-import { INode } from './Node/INode';
+import { INodeContent } from './Node/INodeContent';
export interface INodeSelectedEvent {
isSelected: boolean;
- node: INode;
+ node: INodeContent;
}
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/LiquorTree.d.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/LiquorTree.d.ts
index 9ec7e3ae..2ac1e50c 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/LiquorTree.d.ts
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/LiquorTree.d.ts
@@ -1,6 +1,5 @@
declare module 'liquor-tree' {
import { PluginObject } from 'vue';
- import { VueClass } from 'vue-class-component/lib/declarations';
// https://github.com/amsik/liquor-tree/blob/master/src/lib/Tree.js
export interface ILiquorTree {
@@ -70,6 +69,6 @@ declare module 'liquor-tree' {
matcher(query: string, node: ILiquorTreeExistingNode): boolean;
}
- const LiquorTree: PluginObject
& VueClass;
+ const LiquorTree: PluginObject;
export default LiquorTree;
}
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter.ts
index 1e443b1c..ce3f814b 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter.ts
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodePredicateFilter.ts
@@ -1,11 +1,11 @@
import { ILiquorTreeFilter, ILiquorTreeExistingNode } from 'liquor-tree';
-import { INode } from '../../Node/INode';
+import { INodeContent } from '../../Node/INodeContent';
import { convertExistingToNode } from './NodeTranslator';
-export type FilterPredicate = (node: INode) => boolean;
+export type FilterPredicate = (node: INodeContent) => boolean;
export class NodePredicateFilter implements ILiquorTreeFilter {
- public emptyText = ''; // Does not matter as a custom mesage is shown
+ public emptyText = ''; // Does not matter as a custom message is shown
constructor(private readonly filterPredicate: FilterPredicate) {
if (!filterPredicate) {
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater.ts
index 386e345e..84d8a488 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater.ts
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeStateUpdater.ts
@@ -1,5 +1,5 @@
import { ILiquorTreeNode, ILiquorTreeNodeState } from 'liquor-tree';
-import { NodeType } from '../../Node/INode';
+import { NodeType } from '../../Node/INodeContent';
export function getNewState(
node: ILiquorTreeNode,
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator.ts b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator.ts
index 464f271d..ac9bd4d9 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator.ts
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/LiquorTree/NodeWrapper/NodeTranslator.ts
@@ -1,9 +1,9 @@
import { ILiquorTreeNewNode, ILiquorTreeExistingNode } from 'liquor-tree';
-import { INode } from '../../Node/INode';
+import { INodeContent } from '../../Node/INodeContent';
// Functions to translate INode to LiqourTree models and vice versa for anti-corruption
-export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode): INode {
+export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode): INodeContent {
if (!liquorTreeNode) { throw new Error('liquorTreeNode is undefined'); }
return {
id: liquorTreeNode.id,
@@ -16,7 +16,7 @@ export function convertExistingToNode(liquorTreeNode: ILiquorTreeExistingNode):
};
}
-export function toNewLiquorTreeNode(node: INode): ILiquorTreeNewNode {
+export function toNewLiquorTreeNode(node: INodeContent): ILiquorTreeNewNode {
if (!node) { throw new Error('node is undefined'); }
return {
id: node.id,
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/Documentable.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/DocumentableNode.vue
similarity index 83%
rename from src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/Documentable.vue
rename to src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/DocumentableNode.vue
index 3b23ed74..18a822a1 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/Documentable.vue
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/DocumentableNode.vue
@@ -27,21 +27,29 @@
diff --git a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/ToggleDocumentationButton.vue b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/ToggleDocumentationButton.vue
index 0ed01ed7..b3981769 100644
--- a/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/ToggleDocumentationButton.vue
+++ b/src/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/ToggleDocumentationButton.vue
@@ -2,7 +2,7 @@
@@ -11,22 +11,31 @@
diff --git a/src/presentation/components/Shared/Hooks/UseApplication.ts b/src/presentation/components/Shared/Hooks/UseApplication.ts
new file mode 100644
index 00000000..16d74791
--- /dev/null
+++ b/src/presentation/components/Shared/Hooks/UseApplication.ts
@@ -0,0 +1,18 @@
+import { ApplicationFactory } from '@/application/ApplicationFactory';
+import { IApplication } from '@/domain/IApplication';
+
+/* Application is always static */
+let cachedApplication: IApplication;
+
+// Running tests through Vue CLI throws 'Top-level-await is only supported in EcmaScript Modules'
+// This is a temporary workaround until migrating to Vite
+ApplicationFactory.Current.getApp().then((app) => {
+ cachedApplication = app;
+});
+
+export function useApplication(application: IApplication = cachedApplication) {
+ return {
+ application,
+ info: application.info,
+ };
+}
diff --git a/src/presentation/components/Shared/Hooks/UseCollectionState.ts b/src/presentation/components/Shared/Hooks/UseCollectionState.ts
new file mode 100644
index 00000000..4f768b83
--- /dev/null
+++ b/src/presentation/components/Shared/Hooks/UseCollectionState.ts
@@ -0,0 +1,85 @@
+import { ref, computed, readonly } from 'vue';
+import { IApplicationContext, IReadOnlyApplicationContext } from '@/application/Context/IApplicationContext';
+import { buildContext } from '@/application/Context/ApplicationContextFactory';
+import { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
+import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
+
+let singletonContext: IApplicationContext;
+
+// Running tests through Vue CLI throws 'Top-level-await is only supported in EcmaScript Modules'
+// This is a temporary workaround until migrating to Vite
+buildContext().then((context) => {
+ singletonContext = context;
+});
+
+export function useCollectionState(context: IApplicationContext = singletonContext) {
+ const events = new EventSubscriptionCollection();
+ const ownEvents = new EventSubscriptionCollection();
+
+ const currentState = ref(context.state);
+ ownEvents.register(
+ context.contextChanged.on((event) => {
+ currentState.value = event.newState;
+ }),
+ );
+
+ type NewStateEventHandler = (
+ newState: IReadOnlyCategoryCollectionState,
+ oldState: IReadOnlyCategoryCollectionState | undefined,
+ ) => void;
+ interface IStateCallbackSettings {
+ readonly immediate: boolean;
+ }
+ const defaultSettings: IStateCallbackSettings = {
+ immediate: false,
+ };
+ function onStateChange(
+ handler: NewStateEventHandler,
+ settings: Partial = defaultSettings,
+ ) {
+ if (!handler) {
+ throw new Error('missing state handler');
+ }
+ ownEvents.register(
+ context.contextChanged.on((event) => {
+ handler(event.newState, event.oldState);
+ }),
+ );
+ const defaultedSettings: IStateCallbackSettings = {
+ ...defaultSettings,
+ ...settings,
+ };
+ if (defaultedSettings.immediate) {
+ handler(context.state, undefined);
+ }
+ }
+
+ type StateModifier = (
+ state: ICategoryCollectionState,
+ ) => void;
+ function modifyCurrentState(mutator: StateModifier) {
+ if (!mutator) {
+ throw new Error('missing state mutator');
+ }
+ mutator(context.state);
+ }
+
+ type ContextModifier = (
+ state: IApplicationContext,
+ ) => void;
+ function modifyCurrentContext(mutator: ContextModifier) {
+ if (!mutator) {
+ throw new Error('missing context mutator');
+ }
+ mutator(context);
+ }
+
+ return {
+ modifyCurrentState,
+ modifyCurrentContext,
+ onStateChange,
+ currentContext: context as IReadOnlyApplicationContext,
+ currentState: readonly(computed(() => currentState.value)),
+ events,
+ };
+}
diff --git a/src/presentation/components/Shared/Dialog.vue b/src/presentation/components/Shared/ModalDialog.vue
similarity index 59%
rename from src/presentation/components/Shared/Dialog.vue
rename to src/presentation/components/Shared/ModalDialog.vue
index 3713f72c..c208676c 100644
--- a/src/presentation/components/Shared/Dialog.vue
+++ b/src/presentation/components/Shared/ModalDialog.vue
@@ -10,7 +10,7 @@
@@ -18,18 +18,41 @@
diff --git a/src/presentation/components/Shared/SizeObserver.vue b/src/presentation/components/Shared/SizeObserver.vue
new file mode 100644
index 00000000..7670a342
--- /dev/null
+++ b/src/presentation/components/Shared/SizeObserver.vue
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/presentation/components/Shared/StatefulVue.ts b/src/presentation/components/Shared/StatefulVue.ts
deleted file mode 100644
index 8c189c0b..00000000
--- a/src/presentation/components/Shared/StatefulVue.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Component, Vue } from 'vue-property-decorator';
-import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
-import { IApplicationContext, IApplicationContextChangedEvent } from '@/application/Context/IApplicationContext';
-import { buildContext } from '@/application/Context/ApplicationContextFactory';
-import { IReadOnlyCategoryCollectionState } from '@/application/Context/State/ICategoryCollectionState';
-import { EventSubscriptionCollection } from '@/infrastructure/Events/EventSubscriptionCollection';
-
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore because https://github.com/vuejs/vue-class-component/issues/91
-@Component
-export abstract class StatefulVue extends Vue {
- private static readonly instance = new AsyncLazy(() => buildContext());
-
- protected readonly events = new EventSubscriptionCollection();
-
- private readonly ownEvents = new EventSubscriptionCollection();
-
- public async mounted() {
- const context = await this.getCurrentContext();
- this.ownEvents.register(
- context.contextChanged.on((event) => this.handleStateChangedEvent(event)),
- );
- this.handleCollectionState(context.state, undefined);
- }
-
- protected abstract handleCollectionState(
- newState: IReadOnlyCategoryCollectionState,
- oldState: IReadOnlyCategoryCollectionState | undefined): void;
-
- protected getCurrentContext(): Promise {
- return StatefulVue.instance.getValue();
- }
-
- private handleStateChangedEvent(event: IApplicationContextChangedEvent) {
- this.handleCollectionState(event.newState, event.oldState);
- }
-}
diff --git a/src/presentation/components/TheFooter/DownloadUrlList.vue b/src/presentation/components/TheFooter/DownloadUrlList.vue
index 814c8043..6fbcc08f 100644
--- a/src/presentation/components/TheFooter/DownloadUrlList.vue
+++ b/src/presentation/components/TheFooter/DownloadUrlList.vue
@@ -18,28 +18,35 @@