- {fileList.map((file) => (
+
+ {fileList.map((file, fileIndex) => (
file.removeTag(tag.id)}
- onSelect={(f: ClientFile) => uiStore.selectFile(f)}
- onOpen={(f) => console.log('Open file ', f)}
- onDeselect={(f: ClientFile) => uiStore.deselectFile(f)}
- onDrop={(tag: ClientTag) => file.addTag(tag.id)}
+ onRemoveTag={(tag) => file.removeTag(tag.id)}
+ onSelect={(f, e) => onSelect(fileIndex, e)}
+ onDeselect={(f) => uiStore.deselectFile(f)}
+ onDrop={(tag) => file.addTag(tag.id)}
/>
))}
diff --git a/src/renderer/frontend/components/GalleryItem.tsx b/src/renderer/frontend/components/GalleryItem.tsx
index df70e994a..d250a0fdb 100644
--- a/src/renderer/frontend/components/GalleryItem.tsx
+++ b/src/renderer/frontend/components/GalleryItem.tsx
@@ -8,7 +8,6 @@ import { DropTarget, ConnectDropTarget, DropTargetMonitor } from 'react-dnd';
import { ClientFile } from '../../entities/File';
import {
Tag,
- Icon,
ContextMenuTarget,
Menu,
MenuItem,
@@ -30,9 +29,8 @@ interface IGalleryItemProps {
file: ClientFile;
isSelected: boolean;
onRemoveTag: (tag: ClientTag) => void;
- onSelect: (file: ClientFile) => void;
- onOpen: (file: ClientFile) => void;
- onDeselect: (file: ClientFile) => void;
+ onSelect: (file: ClientFile, e: React.MouseEvent) => void;
+ onDeselect: (file: ClientFile, e: React.MouseEvent) => void;
onDrop: (item: any) => void;
}
@@ -47,7 +45,6 @@ const GalleryItem = ({
isSelected,
onRemoveTag,
onSelect,
- onOpen,
onDeselect,
canDrop,
isOver,
@@ -58,12 +55,15 @@ const GalleryItem = ({
const className = `thumbnail ${selectedStyle} ${isOver ? dropStyle : ''}`;
+ // Switch between opening/selecting depending on whether the selection mode is enabled
+ const clickFunc = isSelected ? onDeselect : onSelect;
+
return connectDropTarget(
onOpen(file)}
+ onClick={(e) => clickFunc(file, e)}
/>
{file.clientTags.map((tag) => (
@@ -74,11 +74,6 @@ const GalleryItem = ({
/>
))}
-
(isSelected ? onDeselect(file) : onSelect(file))}>
-
-
,
);
};
diff --git a/src/renderer/frontend/components/ImportForm.tsx b/src/renderer/frontend/components/ImportForm.tsx
new file mode 100644
index 000000000..95c4e749d
--- /dev/null
+++ b/src/renderer/frontend/components/ImportForm.tsx
@@ -0,0 +1,71 @@
+import React, { useCallback } from 'react';
+import { observer } from 'mobx-react-lite';
+import { useContext } from 'react';
+import { remote } from 'electron';
+import fse from 'fs-extra';
+import path from 'path';
+
+import StoreContext from '../contexts/StoreContext';
+import { Button } from '@blueprintjs/core';
+import FileStore from '../stores/FileStore';
+
+const chooseDirectory = async (fileStore: FileStore) => {
+ const dirs = remote.dialog.showOpenDialog({
+ properties: ['openDirectory', 'multiSelections'],
+ });
+
+ if (!dirs) {
+ return;
+ }
+
+ dirs.forEach(async (dir) => {
+ // Check if directory
+ // const stats = await fse.lstat(dirs[0]);
+ const imgExtensions = ['gif', 'png', 'jpg', 'jpeg'];
+
+ const filenames = await fse.readdir(dir);
+ const imgFileNames = filenames.filter((f) =>
+ imgExtensions.some((ext) =>
+ f.toLowerCase()
+ .endsWith(ext)),
+ );
+
+ imgFileNames.forEach(async (filename) => {
+ const joinedPath = path.join(dir, filename);
+ console.log(joinedPath);
+ fileStore.addFile(joinedPath);
+ });
+ });
+};
+
+const ImportForm = () => {
+ // Todo: Add Location entity to DB, so we can have user-picked directories as well
+ // Todo: Also show sub-directories in tree
+
+ const { fileStore } = useContext(StoreContext);
+
+ const handleChooseDirectory = useCallback(
+ () => chooseDirectory(fileStore),
+ [],
+ );
+
+ return (
+ <>
+
+ Import images
+
+
+
+ Import single directory
+
+
+
+ Import nested directories
+
+
+ {/* Todo: Show progress bar here */}
+ >
+ );
+};
+
+export default observer(ImportForm);
diff --git a/src/renderer/frontend/components/Inspector.tsx b/src/renderer/frontend/components/Inspector.tsx
new file mode 100644
index 000000000..9eefd176d
--- /dev/null
+++ b/src/renderer/frontend/components/Inspector.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import fs from 'fs';
+import path from 'path';
+import { observer } from 'mobx-react-lite';
+
+import RootStore from '../stores/RootStore';
+import { withRootstore } from '../contexts/StoreContext';
+import FileInfo from './FileInfo';
+import FileTag from './FileTag';
+
+const sufixes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+const getBytes = (bytes: number) => {
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return !bytes && '0 Bytes' || (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sufixes[i];
+};
+
+interface IInspectorProps {
+ rootStore: RootStore;
+}
+
+const Inspector = ({ rootStore: { uiStore } }: IInspectorProps) => {
+ const selectedFiles = uiStore.clientFileSelection;
+
+ let selectionPreview;
+ let headerText;
+ let headerSubtext;
+
+ if (selectedFiles.length === 0) {
+ headerText = 'No image selected';
+ } else if (selectedFiles.length === 1) {
+ const singleFile = selectedFiles[0];
+ const ext = singleFile.path.substr(singleFile.path.lastIndexOf('.') + 1)
+ .toUpperCase();
+ selectionPreview =
;
+ headerText = path.basename(singleFile.path);
+ headerSubtext = `${ext} image - ${getBytes(fs.statSync(singleFile.path).size)}`;
+ } else {
+ // Todo: fs.stat (not sync) is preferred, but it seems to execute instantly... good enough for now
+ let size = 0;
+ selectedFiles.forEach((f) => size += fs.statSync(f.path).size);
+
+ selectionPreview =
Carousel of selected images here?
;
+ headerText = selectedFiles.map((f) => path.basename(f.path))
+ .join(', ');
+ headerSubtext = getBytes(size);
+ }
+
+ if (selectedFiles.length > 0) {
+ return (
+
+
+
+
+ {headerText}
+ {headerSubtext}
+
+
+
+
+
+ );
+ } else {
+ return (
+
+ );
+ }
+};
+
+export default withRootstore(observer(Inspector));
diff --git a/src/renderer/frontend/components/MultiTagSelector.tsx b/src/renderer/frontend/components/MultiTagSelector.tsx
new file mode 100644
index 000000000..8f2ca562c
--- /dev/null
+++ b/src/renderer/frontend/components/MultiTagSelector.tsx
@@ -0,0 +1,147 @@
+import React, { useContext, useMemo, useCallback } from 'react';
+import { observer } from 'mobx-react-lite';
+
+import { Button, MenuItem } from '@blueprintjs/core';
+import { ItemRenderer, MultiSelect, ItemPredicate } from '@blueprintjs/select';
+
+import { ClientTag } from '../../entities/Tag';
+import StoreContext from '../contexts/StoreContext';
+
+const TagMultiSelect = MultiSelect.ofType
();
+
+const NoResults = ;
+
+const CREATED_TAG_ID = 'created-tag-id';
+
+const renderCreateTagOption = (
+ query: string,
+ active: boolean,
+ handleClick: React.MouseEventHandler,
+) => (
+
+);
+
+const filterTag: ItemPredicate = (query, tag, index, exactMatch) => {
+ const normalizedName = tag.name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+
+ if (exactMatch) {
+ return normalizedName === normalizedQuery;
+ } else {
+ return normalizedName.indexOf(normalizedQuery) >= 0;
+ }
+};
+
+interface IMultiTagSelectorProps {
+ selectedTags: ClientTag[];
+ tagLabel?: (tag: ClientTag) => string;
+ onTagSelect: (tag: ClientTag) => void;
+ onTagDeselect: (index: number) => void;
+ onClearSelection: () => void;
+ onTagCreation?: (name: string) => ClientTag;
+}
+
+const MultiTagSelector = ({
+ selectedTags,
+ tagLabel,
+ onTagSelect,
+ onTagDeselect,
+ onClearSelection,
+ onTagCreation,
+}: IMultiTagSelectorProps) => {
+ const { tagStore } = useContext(StoreContext);
+
+ const handleSelect = useCallback(
+ (tag: ClientTag) => {
+ // When a tag is created, it is selected. Here we detect whether we need to actually the ClientTag.
+ if (onTagCreation && tag.id === CREATED_TAG_ID) {
+ tag = onTagCreation(tag.name);
+ }
+
+ return selectedTags.includes(tag)
+ ? onTagDeselect(selectedTags.indexOf(tag))
+ : onTagSelect(tag);
+ },
+ [selectedTags],
+ );
+
+ const handleDeselect = useCallback(
+ (_: string, index: number) => onTagDeselect(index),
+ [onTagDeselect],
+ );
+
+ // Todo: Might need a confirmation pop over
+ const ClearButton = useMemo(
+ () =>
+ selectedTags.length > 0 ? (
+
+ ) : (
+ undefined
+ ),
+ [selectedTags],
+ );
+
+ const SearchTagItem = useCallback>(
+ (tag, { modifiers, handleClick }) => {
+ if (!modifiers.matchesPredicate) {
+ return null;
+ }
+ return (
+
+ );
+ },
+ [selectedTags],
+ );
+
+ const TagLabel = (tag: ClientTag) => (tagLabel ? tagLabel(tag) : tag.name);
+
+ // Only used for visualization in the selector, an actual ClientTag is created onSelect
+ const createNewTag = useCallback(
+ (name: string) => new ClientTag(tagStore, name, CREATED_TAG_ID),
+ [],
+ );
+
+ const maybeCreateNewItemFromQuery = onTagCreation ? createNewTag : undefined;
+ const maybeCreateNewItemRenderer = onTagCreation
+ ? renderCreateTagOption
+ : undefined;
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default observer(MultiTagSelector);
diff --git a/src/renderer/frontend/components/Outliner.tsx b/src/renderer/frontend/components/Outliner.tsx
new file mode 100644
index 000000000..bad44007a
--- /dev/null
+++ b/src/renderer/frontend/components/Outliner.tsx
@@ -0,0 +1,33 @@
+import React, { useContext } from 'react';
+import { observer } from 'mobx-react-lite';
+
+import StoreContext from '../contexts/StoreContext';
+
+import TagList from './TagList';
+import ImportForm from './ImportForm';
+import SearchForm from './SearchForm';
+
+const Outliner = () => {
+ const { uiStore } = useContext(StoreContext);
+
+ return (
+
+ {uiStore.outlinerPage === 'IMPORT' && (<>
+ Import
+
+ >)}
+
+ {uiStore.outlinerPage === 'TAGS' && (<>
+ Tags
+
+ >)}
+
+ {uiStore.outlinerPage === 'SEARCH' && (<>
+ Search
+
+ >)}
+
+ );
+};
+
+export default observer(Outliner);
diff --git a/src/renderer/frontend/components/SearchBar.tsx b/src/renderer/frontend/components/SearchBar.tsx
deleted file mode 100644
index 1001542cf..000000000
--- a/src/renderer/frontend/components/SearchBar.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import React from 'react';
-
-import { Button, MenuItem } from '@blueprintjs/core';
-import { ItemRenderer, MultiSelect } from '@blueprintjs/select';
-
-import { ClientTag } from '../../entities/Tag';
-
-const TagMultiSelect = MultiSelect.ofType();
-
-const SearchTagItem: ItemRenderer = (
- tag,
- { modifiers, handleClick },
-) => {
- if (!modifiers.matchesPredicate) {
- return null;
- }
-
- return (
-
- );
-};
-
-interface ISearchBarProps {
- onTagSelect: (tag: ClientTag) => void;
- onTagDeselect: (index: number) => void;
- onClearSelection: () => void;
- selectedTags: ClientTag[];
- allTags: ClientTag[];
-}
-
-const SearchBar = ({
- onTagSelect,
- onTagDeselect,
- onClearSelection,
- selectedTags,
- allTags,
-}: ISearchBarProps) => {
- const handleDeselect = (_: string, index: number) => onTagDeselect(index);
-
- const clearButton =
- selectedTags.length > 0 ? (
-
- ) : (
- undefined
- );
-
- return (
- <>
- }
- onItemSelect={onTagSelect}
- popoverProps={{ minimal: true }}
- tagRenderer={(tag) => tag.name}
- tagInputProps={{
- tagProps: { minimal: true },
- onRemove: handleDeselect,
- rightElement: clearButton,
- }}
- />
- >
- );
-};
-
-export default SearchBar;
diff --git a/src/renderer/frontend/components/SearchForm.tsx b/src/renderer/frontend/components/SearchForm.tsx
new file mode 100644
index 000000000..0c706c7e9
--- /dev/null
+++ b/src/renderer/frontend/components/SearchForm.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+
+import MultiTagSelector from './MultiTagSelector';
+import { FormGroup, InputGroup, Button } from '@blueprintjs/core';
+
+const SearchForm = () => {
+ return (
+ <>
+ {/* Tags */}
+
+ {/* Todo: Also search through collections */}
+ console.log('select')}
+ onTagDeselect={() => console.log('deselect')}
+ onClearSelection={() => console.log('clear')}
+ />
+ console.log('select')}
+ onTagDeselect={() => console.log('deselect')}
+ onClearSelection={() => console.log('clear')}
+ />
+
+
+ {/* Filenames */}
+
+
+
+
+
+ {/* Location */}
+
+
+
+
+
+ {/* File type */}
+
+
+
+
+
+ Reset
+ >
+ );
+};
+
+export default observer(SearchForm);
diff --git a/src/renderer/frontend/components/Sidebar.tsx b/src/renderer/frontend/components/Sidebar.tsx
deleted file mode 100644
index d566f21d9..000000000
--- a/src/renderer/frontend/components/Sidebar.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import TagList from './TagList';
-
-const Sidebar = () => (
-
-
All tags
-
-
-);
-
-export default Sidebar;
diff --git a/src/renderer/frontend/components/TagList.tsx b/src/renderer/frontend/components/TagList.tsx
index 8240fa698..135a60ef3 100644
--- a/src/renderer/frontend/components/TagList.tsx
+++ b/src/renderer/frontend/components/TagList.tsx
@@ -24,7 +24,6 @@ const TagList = ({
} else {
uiStore.selectTag(tag);
}
- fileStore.fetchFilesByTagIDs(uiStore.tagSelection.toJS());
};
return (
diff --git a/src/renderer/frontend/components/Toolbar.tsx b/src/renderer/frontend/components/Toolbar.tsx
new file mode 100644
index 000000000..7806120a8
--- /dev/null
+++ b/src/renderer/frontend/components/Toolbar.tsx
@@ -0,0 +1,184 @@
+import React, { useContext, useCallback, useMemo } from 'react';
+import {
+ Button, Popover, MenuItem, Menu, Drawer, Switch, ButtonGroup, Icon, Divider, Classes, H5,
+} from '@blueprintjs/core';
+import { observer } from 'mobx-react-lite';
+
+import StoreContext from '../contexts/StoreContext';
+
+const RemoveFilesPopover = ({ onRemove, disabled }: { onRemove: () => void, disabled: boolean }) => (
+
+
+
+
Confirm deletion
+
Are you sure you want to remove these images from your library?
+
Your files will not be deleted.
+
+
+
+ Cancel
+
+
+ Delete
+
+
+
+
+);
+
+const Toolbar = () => {
+ const { uiStore, fileStore } = useContext(StoreContext);
+
+ // Outliner actions
+ const handleOutlinerImport = useCallback(() => { uiStore.outlinerPage = 'IMPORT'; }, []);
+ const handleOutlinerTags = useCallback(() => { uiStore.outlinerPage = 'TAGS'; }, []);
+ const handleOutlinerSearch = useCallback(() => { uiStore.outlinerPage = 'SEARCH'; }, []);
+
+ // Content actions
+ const isFileListSelected = uiStore.fileSelection.length === fileStore.fileList.length;
+ // If everything is selected, deselect all. Else, select all
+ const handleToggleSelect = useCallback(
+ () => (isFileListSelected)
+ ? uiStore.fileSelection.clear()
+ : uiStore.fileSelection.push(
+ ...fileStore.fileList
+ .map((f) => f.id)
+ .filter((f) => !uiStore.fileSelection.includes(f)),
+ ),
+ [isFileListSelected],
+ );
+
+ const handleRemoveSelectedFiles = useCallback(
+ async () => {
+ await fileStore.removeFilesById(uiStore.fileSelection);
+ uiStore.fileSelection.clear();
+ },
+ [],
+ );
+
+ // Inspector actions
+ const handleToggleInspector = useCallback(
+ () => { uiStore.isInspectorOpen = !uiStore.isInspectorOpen; },
+ [],
+ );
+
+ const handleToggleSettings = useCallback(
+ () => { uiStore.isSettingsOpen = !uiStore.isSettingsOpen; },
+ [],
+ );
+
+ // Settings actions
+ const toggleTheme = useCallback(
+ () => { uiStore.theme = (uiStore.theme === 'DARK' ? 'LIGHT' : 'DARK'); },
+ [],
+ );
+
+ // Render variables
+ const sortMenu = useMemo(() =>
+
+
+
+
+
+ } active />
+ ,
+ [],
+ );
+
+ const layoutMenu = useMemo(() =>
+
+
+
+
+
+ ,
+ [],
+ );
+
+ const selectionModeOn = uiStore.fileSelection.length > 0;
+ const olPage = uiStore.outlinerPage;
+
+ return (
+
+ );
+};
+
+export default observer(Toolbar);
diff --git a/src/renderer/frontend/stores/FileStore.ts b/src/renderer/frontend/stores/FileStore.ts
index 8d1f5fc59..9c66ca73a 100644
--- a/src/renderer/frontend/stores/FileStore.ts
+++ b/src/renderer/frontend/stores/FileStore.ts
@@ -1,6 +1,6 @@
import { action, observable } from 'mobx';
-
import fs from 'fs-extra';
+
import Backend from '../../backend/Backend';
import { ClientFile, IFile } from '../../entities/File';
import RootStore from './RootStore';
@@ -35,16 +35,14 @@ class FileStore {
@action
async removeFilesById(ids: ID[]) {
await Promise.all(
- ids.map(
- async (id) => {
- const file = this.fileList.find((f) => f.id === id);
- if (file) {
- await this.removeFile(file);
- } else {
- console.log('Could not find file to remove', file);
- }
- },
- ),
+ ids.map(async (id) => {
+ const file = this.fileList.find((f) => f.id === id);
+ if (file) {
+ await this.removeFile(file);
+ } else {
+ console.log('Could not find file to remove', file);
+ }
+ }),
);
}
@@ -79,24 +77,25 @@ class FileStore {
// Removes files with invalid file path. Otherwise adds files to fileList.
// In the future the user should have the option to input the new path if the file was only moved or renamed.
await Promise.all(
- fetchedFiles.map(
- async (backendFile: IFile) => {
- try {
- await fs.access(backendFile.path, fs.constants.F_OK);
- this.fileList.push(
- new ClientFile(this).updateFromBackend(backendFile),
- );
- } catch (e) {
- console.log(`${backendFile.path} 'does not exist'`);
- this.backend.removeFile(backendFile);
- }
- }),
+ fetchedFiles.map(async (backendFile: IFile) => {
+ try {
+ await fs.access(backendFile.path, fs.constants.F_OK);
+ this.fileList.push(
+ new ClientFile(this).updateFromBackend(backendFile),
+ );
+ } catch (e) {
+ console.log(`${backendFile.path} 'does not exist'`);
+ this.backend.removeFile(backendFile);
+ }
+ }),
);
}
private async removeFile(file: ClientFile): Promise {
file.dispose();
this.fileList.remove(file);
+ // Deselect in case it was selected
+ this.rootStore.uiStore.deselectFile(file);
return this.backend.removeFile(file);
}
@@ -105,24 +104,24 @@ class FileStore {
// watching files would be better to remove invalid files
// files could also have moved, removing them may be undesired then
const existenceChecker = await Promise.all(
- backendFiles.map(
- async (backendFile) => {
- try {
- await fs.access(backendFile.path, fs.constants.F_OK);
- return true;
- } catch (err) {
- this.backend.removeFile(backendFile);
- const clientFile = this.fileList.find((f) => backendFile.id === f.id);
- if (clientFile) {
- await this.removeFile(clientFile);
- }
- return false;
+ backendFiles.map(async (backendFile) => {
+ try {
+ await fs.access(backendFile.path, fs.constants.F_OK);
+ return true;
+ } catch (err) {
+ this.backend.removeFile(backendFile);
+ const clientFile = this.fileList.find((f) => backendFile.id === f.id);
+ if (clientFile) {
+ await this.removeFile(clientFile);
}
- },
- ),
+ return false;
+ }
+ }),
);
- const existingBackendFiles = backendFiles.filter((_, i) => existenceChecker[i]);
+ const existingBackendFiles = backendFiles.filter(
+ (_, i) => existenceChecker[i],
+ );
if (this.fileList.length === 0) {
this.fileList.push(...this.filesFromBackend(existingBackendFiles));
diff --git a/src/renderer/frontend/stores/RootStore.ts b/src/renderer/frontend/stores/RootStore.ts
index 371a8d74b..286250fbb 100644
--- a/src/renderer/frontend/stores/RootStore.ts
+++ b/src/renderer/frontend/stores/RootStore.ts
@@ -3,6 +3,13 @@ import FileStore from './FileStore';
import TagStore from './TagStore';
import UiStore from './UiStore';
+// import { configure } from 'mobx';
+
+// This will throw exceptions whenver we try to modify the state directly without an action
+// Actions will batch state modifications -> better for performance
+// https://mobx.js.org/refguide/action.html
+// configure({ enforceActions: 'observed' });
+
/**
* From: https://mobx.js.org/best/store.html
* An often asked question is how to combine multiple stores without using singletons.
diff --git a/src/renderer/frontend/stores/TagStore.ts b/src/renderer/frontend/stores/TagStore.ts
index 5022a96a0..1a83f3eed 100644
--- a/src/renderer/frontend/stores/TagStore.ts
+++ b/src/renderer/frontend/stores/TagStore.ts
@@ -46,6 +46,7 @@ class TagStore {
const tag = new ClientTag(this, tagName);
this.tagList.push(tag);
this.backend.createTag(tag.id, tag.name, tag.description);
+ return tag;
}
@action
@@ -53,6 +54,9 @@ class TagStore {
// Remove tag from state
this.tagList.splice(this.tagList.indexOf(tag), 1);
+ // Remove tag from selection
+ this.rootStore.uiStore.tagSelection.remove(tag.id);
+
// Remove tag from files
this.rootStore.fileStore.fileList
.filter((f) => f.tags.includes(tag.id))
diff --git a/src/renderer/frontend/stores/UiStore.ts b/src/renderer/frontend/stores/UiStore.ts
index c1de20f52..5d06b5a62 100644
--- a/src/renderer/frontend/stores/UiStore.ts
+++ b/src/renderer/frontend/stores/UiStore.ts
@@ -1,4 +1,4 @@
-import { action, observable } from 'mobx';
+import { action, observable, computed } from 'mobx';
import { ClientFile } from '../../entities/File';
import { ID } from '../../entities/ID';
@@ -29,13 +29,23 @@ class UiStore {
@observable theme: 'LIGHT' | 'DARK' = 'DARK';
// UI
- @observable isSidebarOpen: boolean = true;
+ @observable outlinerPage: 'IMPORT' | 'TAGS' | 'SEARCH' = 'TAGS';
+ @observable isInspectorOpen: boolean = true;
+ @observable isSettingsOpen: boolean = false;
// Selections
- // Observable arrays recommened like this here https://github.com/mobxjs/mobx/issues/669#issuecomment-269119270
+ // Observable arrays recommended like this here https://github.com/mobxjs/mobx/issues/669#issuecomment-269119270
readonly fileSelection = observable([]);
readonly tagSelection = observable([]);
+ @computed get clientFileSelection(): ClientFile[] {
+ return this.fileSelection.map((id) => this.rootStore.fileStore.fileList.find((f) => f.id === id)) as ClientFile[];
+ }
+
+ @computed get clientTagSelection(): ClientTag[] {
+ return this.tagSelection.map((id) => this.rootStore.tagStore.tagList.find((t) => t.id === id)) as ClientTag[];
+ }
+
constructor(rootStore: RootStore) {
this.rootStore = rootStore;
}
@@ -50,10 +60,25 @@ class UiStore {
@action selectTag(tag: ClientTag) {
this.tagSelection.push(tag.id);
+ this.cleanFileSelection();
+ this.rootStore.fileStore.fetchFilesByTagIDs(this.tagSelection);
}
@action deselectTag(tag: ClientTag) {
this.tagSelection.remove(tag.id);
+ this.cleanFileSelection();
+ this.rootStore.fileStore.fetchFilesByTagIDs(this.tagSelection);
+ }
+
+ /**
+ * Deselect files that are not tagged with any tag in the current tag selection
+ */
+ private cleanFileSelection() {
+ this.clientFileSelection.forEach((file) => {
+ if (!file.tags.some((t) => this.tagSelection.includes(t))) {
+ this.deselectFile(file);
+ }
+ });
}
}
diff --git a/src/renderer/index.html b/src/renderer/index.html
index d4f8149e9..f1ff2c3fa 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -5,6 +5,6 @@
Allusion - Your Visual Library
-
+