(false);
27 |
28 | const selectStudioPath = async () => {
29 | const openResult = await remote.dialog.showOpenDialog({ properties: ["openDirectory"] });
30 | if (!openResult.canceled) {
31 | const pathCandidate = openResult.filePaths[0];
32 |
33 | await ipc.renderer.rpc.isSimsStudioDir(pathCandidate); // raises if invalid
34 | notification.showSuccess(l10n.studioValidPath);
35 |
36 | setStudioPath(pathCandidate);
37 | }
38 | };
39 |
40 | const handleOpenDialog = () => {
41 | setOpenDisabled(true);
42 |
43 | selectStudioPath()
44 | .catch((error: LocalizedErrors | Error) => {
45 | notification.showError(getErrorMessage(error, l10n));
46 | })
47 | .finally(() => setOpenDisabled(false));
48 | };
49 |
50 | return (
51 |
52 |
53 |
59 |
60 |
61 |
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-100.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-100.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-100.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-100.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-100italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-100italic.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-100italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-100italic.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-300.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-300.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-300italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-300italic.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-300italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-300italic.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-400.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-400.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-400italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-400italic.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-400italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-400italic.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-500.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-500.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-500italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-500italic.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-500italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-500italic.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-700.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-700.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-700italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-700italic.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-700italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-700italic.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-900.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-900.woff2
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-900italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-900italic.woff
--------------------------------------------------------------------------------
/ui/fonts/files/roboto-latin-900italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EgorBlagov/sims-mod-assistant/d509fa25ce907b752fb0fa2392f40a3d5be93b09/ui/fonts/files/roboto-latin-900italic.woff2
--------------------------------------------------------------------------------
/ui/fonts/index.css:
--------------------------------------------------------------------------------
1 | /* roboto-100normal - latin */
2 | @font-face {
3 | font-family: "Roboto";
4 | font-style: normal;
5 | font-display: swap;
6 | font-weight: 100;
7 | src: local("Roboto Thin "), local("Roboto-Thin"), url("./files/roboto-latin-100.woff2") format("woff2"),
8 | /* Super Modern Browsers */ url("./files/roboto-latin-100.woff") format("woff"); /* Modern Browsers */
9 | }
10 |
11 | /* roboto-100italic - latin */
12 | @font-face {
13 | font-family: "Roboto";
14 | font-style: italic;
15 | font-display: swap;
16 | font-weight: 100;
17 | src: local("Roboto Thin italic"), local("Roboto-Thinitalic"),
18 | url("./files/roboto-latin-100italic.woff2") format("woff2"),
19 | /* Super Modern Browsers */ url("./files/roboto-latin-100italic.woff") format("woff"); /* Modern Browsers */
20 | }
21 |
22 | /* roboto-300normal - latin */
23 | @font-face {
24 | font-family: "Roboto";
25 | font-style: normal;
26 | font-display: swap;
27 | font-weight: 300;
28 | src: local("Roboto Light "), local("Roboto-Light"), url("./files/roboto-latin-300.woff2") format("woff2"),
29 | /* Super Modern Browsers */ url("./files/roboto-latin-300.woff") format("woff"); /* Modern Browsers */
30 | }
31 |
32 | /* roboto-300italic - latin */
33 | @font-face {
34 | font-family: "Roboto";
35 | font-style: italic;
36 | font-display: swap;
37 | font-weight: 300;
38 | src: local("Roboto Light italic"), local("Roboto-Lightitalic"),
39 | url("./files/roboto-latin-300italic.woff2") format("woff2"),
40 | /* Super Modern Browsers */ url("./files/roboto-latin-300italic.woff") format("woff"); /* Modern Browsers */
41 | }
42 |
43 | /* roboto-400normal - latin */
44 | @font-face {
45 | font-family: "Roboto";
46 | font-style: normal;
47 | font-display: swap;
48 | font-weight: 400;
49 | src: local("Roboto Regular "), local("Roboto-Regular"), url("./files/roboto-latin-400.woff2") format("woff2"),
50 | /* Super Modern Browsers */ url("./files/roboto-latin-400.woff") format("woff"); /* Modern Browsers */
51 | }
52 |
53 | /* roboto-400italic - latin */
54 | @font-face {
55 | font-family: "Roboto";
56 | font-style: italic;
57 | font-display: swap;
58 | font-weight: 400;
59 | src: local("Roboto Regular italic"), local("Roboto-Regularitalic"),
60 | url("./files/roboto-latin-400italic.woff2") format("woff2"),
61 | /* Super Modern Browsers */ url("./files/roboto-latin-400italic.woff") format("woff"); /* Modern Browsers */
62 | }
63 |
64 | /* roboto-500normal - latin */
65 | @font-face {
66 | font-family: "Roboto";
67 | font-style: normal;
68 | font-display: swap;
69 | font-weight: 500;
70 | src: local("Roboto Medium "), local("Roboto-Medium"), url("./files/roboto-latin-500.woff2") format("woff2"),
71 | /* Super Modern Browsers */ url("./files/roboto-latin-500.woff") format("woff"); /* Modern Browsers */
72 | }
73 |
74 | /* roboto-500italic - latin */
75 | @font-face {
76 | font-family: "Roboto";
77 | font-style: italic;
78 | font-display: swap;
79 | font-weight: 500;
80 | src: local("Roboto Medium italic"), local("Roboto-Mediumitalic"),
81 | url("./files/roboto-latin-500italic.woff2") format("woff2"),
82 | /* Super Modern Browsers */ url("./files/roboto-latin-500italic.woff") format("woff"); /* Modern Browsers */
83 | }
84 |
85 | /* roboto-700normal - latin */
86 | @font-face {
87 | font-family: "Roboto";
88 | font-style: normal;
89 | font-display: swap;
90 | font-weight: 700;
91 | src: local("Roboto Bold "), local("Roboto-Bold"), url("./files/roboto-latin-700.woff2") format("woff2"),
92 | /* Super Modern Browsers */ url("./files/roboto-latin-700.woff") format("woff"); /* Modern Browsers */
93 | }
94 |
95 | /* roboto-700italic - latin */
96 | @font-face {
97 | font-family: "Roboto";
98 | font-style: italic;
99 | font-display: swap;
100 | font-weight: 700;
101 | src: local("Roboto Bold italic"), local("Roboto-Bolditalic"),
102 | url("./files/roboto-latin-700italic.woff2") format("woff2"),
103 | /* Super Modern Browsers */ url("./files/roboto-latin-700italic.woff") format("woff"); /* Modern Browsers */
104 | }
105 |
106 | /* roboto-900normal - latin */
107 | @font-face {
108 | font-family: "Roboto";
109 | font-style: normal;
110 | font-display: swap;
111 | font-weight: 900;
112 | src: local("Roboto Black "), local("Roboto-Black"), url("./files/roboto-latin-900.woff2") format("woff2"),
113 | /* Super Modern Browsers */ url("./files/roboto-latin-900.woff") format("woff"); /* Modern Browsers */
114 | }
115 |
116 | /* roboto-900italic - latin */
117 | @font-face {
118 | font-family: "Roboto";
119 | font-style: italic;
120 | font-display: swap;
121 | font-weight: 900;
122 | src: local("Roboto Black italic"), local("Roboto-Blackitalic"),
123 | url("./files/roboto-latin-900italic.woff2") format("woff2"),
124 | /* Super Modern Browsers */ url("./files/roboto-latin-900italic.woff") format("woff"); /* Modern Browsers */
125 | }
126 |
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 | Sims 4 Mod Assistant
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/ui/index.tsx:
--------------------------------------------------------------------------------
1 | // tslint:disable: file-name-casing
2 |
3 | import React from "react";
4 | import ReactDOM from "react-dom";
5 | import { Provider } from "react-redux";
6 | import { App } from "./components/App";
7 | import "./fonts/index.css";
8 | import { store } from "./redux/store";
9 | import { SettingsThunk } from "./redux/thunk/settings";
10 |
11 | const mountPoint = document.getElementById("root");
12 |
13 | store
14 | .dispatch(SettingsThunk.loadLanguage())
15 | .then(() => {
16 | ReactDOM.render(
17 |
18 |
19 | ,
20 | mountPoint,
21 | );
22 | })
23 | .catch((err) => {
24 | ReactDOM.render(Application loading error: {err.toString()}
, mountPoint);
25 | });
26 |
--------------------------------------------------------------------------------
/ui/redux/actions.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "redux";
2 | import { ThunkAction } from "redux-thunk";
3 | import { TState } from "./reducers";
4 |
5 | export enum Actions {
6 | SETTINGS_SET_LANGUAGE = "SET_LANGUAGE",
7 | SETTINGS_SET_STUDIO_PATH = "SETTINGS_SET_STUDIO_PATH",
8 |
9 | NOTIFICATION_SET_TYPE = "NOTIFICATION_SET_TYPE",
10 | NOTIFICATION_SET_MESSAGE = "NOTIFICATION_SET_MESSAGE",
11 | NOTIFICATION_SET_VISIBLE = "NOTIFICATION_SET_VISIBLE",
12 |
13 | BACKDROP_SET_VISIBLE = "BACKDROP_SET_VISIBLE",
14 |
15 | CONFLICT_RESOLVER_SEARCH_SET_SEARCH_IN_PROGRESS = "CONFLICT_RESOLVER_SEARCH_SET_SEARCH_IN_PROGRESS",
16 | CONFLICT_RESOLVER_SEARCH_SET_RESULT = "CONFLICT_RESOLVER_SEARCH_SET_RESULT",
17 | CONFLICT_RESOLVER_SEARCH_SET_PROGRESS_RELATIVE = "CONFLICT_RESOLVER_SEARCH_SET_PROGRESS_RELATIVE",
18 | CONFLICT_RESOLVER_SELECT_FILES = "CONFLICT_RESOLVER_SELECT_FILES",
19 | CONFLICT_RESOLVER_SET_FILES_FILTER = "CONFLICT_RESOLVER_SET_FILES_FILTER",
20 | CONFLICT_RESOLVER_SET_SEARCH_DIRECTORY = "CONFLICT_RESOLVER_SET_SEARCH_DIRECTORY",
21 | CONFLICT_RESOLVER_UPDATE_INDEX = "CONFLICT_RESOLVER_UPDATE_INDEX",
22 | }
23 |
24 | export interface ReduxAction {
25 | type: Actions;
26 | }
27 |
28 | export type ReduxThunkAction = ThunkAction>;
29 |
--------------------------------------------------------------------------------
/ui/redux/backdrop/action-creators.ts:
--------------------------------------------------------------------------------
1 | import { Actions } from "../actions";
2 | import { BackdropSetVisibleAction } from "./actions";
3 |
4 | const setVisible = (visible: boolean): BackdropSetVisibleAction => ({
5 | type: Actions.BACKDROP_SET_VISIBLE,
6 | visible,
7 | });
8 |
9 | export const BackdropActions = {
10 | setVisible,
11 | };
12 |
--------------------------------------------------------------------------------
/ui/redux/backdrop/actions.ts:
--------------------------------------------------------------------------------
1 | import { Actions, ReduxAction } from "../actions";
2 |
3 | export type BackdropActions = BackdropSetVisibleAction;
4 |
5 | export interface BackdropSetVisibleAction extends ReduxAction {
6 | type: Actions.BACKDROP_SET_VISIBLE;
7 | visible: boolean;
8 | }
9 |
--------------------------------------------------------------------------------
/ui/redux/backdrop/reducers.ts:
--------------------------------------------------------------------------------
1 | import { Actions } from "../actions";
2 | import { BackdropActions } from "./actions";
3 |
4 | interface BackdropState {
5 | visible: boolean;
6 | }
7 |
8 | const defaultBackdropState: BackdropState = {
9 | visible: false,
10 | };
11 |
12 | export const backdrop = (state = defaultBackdropState, action: BackdropActions): BackdropState => {
13 | switch (action.type) {
14 | case Actions.BACKDROP_SET_VISIBLE:
15 | return {
16 | ...state,
17 | visible: action.visible,
18 | };
19 | default:
20 | return state;
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/ui/redux/conflict-resolver/action-creators.ts:
--------------------------------------------------------------------------------
1 | import { IIndexResult, IIndexUpdate } from "../../../common/types";
2 | import { Actions } from "../actions";
3 | import {
4 | ConflictResolverSearchSetInProgresstAction,
5 | ConflictResolverSearchSetResultAction,
6 | ConflictResolverSelectFiles,
7 | ConflictResolverSetFilesFilter,
8 | ConflictResolverSetProgressRelativeAction,
9 | ConflictResolverSetSearchDirectory,
10 | ConflictResolverUpdateIndex,
11 | } from "./actions";
12 | import { IFilterParams } from "./reducers";
13 |
14 | const setInProgress = (inProgress: boolean): ConflictResolverSearchSetInProgresstAction => ({
15 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_SEARCH_IN_PROGRESS,
16 | inProgress,
17 | });
18 |
19 | const setIndexResult = (result: IIndexResult): ConflictResolverSearchSetResultAction => ({
20 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_RESULT,
21 | result,
22 | });
23 |
24 | const setProgress = (progressRelative: number): ConflictResolverSetProgressRelativeAction => ({
25 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_PROGRESS_RELATIVE,
26 | progressRelative,
27 | });
28 |
29 | const setSearchDirectory = (searchDirectory: string): ConflictResolverSetSearchDirectory => ({
30 | type: Actions.CONFLICT_RESOLVER_SET_SEARCH_DIRECTORY,
31 | searchDirectory,
32 | });
33 |
34 | const cleanupSearch = (): ConflictResolverSearchSetResultAction => ({
35 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_RESULT,
36 | result: undefined,
37 | });
38 |
39 | const selectFiles = (files: string[], selected: boolean): ConflictResolverSelectFiles => ({
40 | type: Actions.CONFLICT_RESOLVER_SELECT_FILES,
41 | files,
42 | selected,
43 | });
44 |
45 | const setFilter = (filesFilter: IFilterParams): ConflictResolverSetFilesFilter => ({
46 | type: Actions.CONFLICT_RESOLVER_SET_FILES_FILTER,
47 | filesFilter,
48 | });
49 |
50 | const updateIndex = (indexUpdate: IIndexUpdate): ConflictResolverUpdateIndex => ({
51 | type: Actions.CONFLICT_RESOLVER_UPDATE_INDEX,
52 | indexUpdate,
53 | });
54 |
55 | export const ConflictResolverActions = {
56 | setInProgress,
57 | setIndexResult,
58 | setProgress,
59 | cleanupSearch,
60 | selectFiles,
61 | setFilter,
62 | setSearchDirectory,
63 | updateIndex,
64 | };
65 |
--------------------------------------------------------------------------------
/ui/redux/conflict-resolver/actions.ts:
--------------------------------------------------------------------------------
1 | import { IIndexResult, IIndexUpdate } from "../../../common/types";
2 | import { Actions, ReduxAction } from "../actions";
3 | import { IFilterParams } from "./reducers";
4 |
5 | export type ConflictResolverActions =
6 | | ConflictResolverSearchSetInProgresstAction
7 | | ConflictResolverSearchSetResultAction
8 | | ConflictResolverSetProgressRelativeAction
9 | | ConflictResolverSelectFiles
10 | | ConflictResolverSetFilesFilter
11 | | ConflictResolverSetSearchDirectory
12 | | ConflictResolverUpdateIndex;
13 |
14 | export interface ConflictResolverSearchSetInProgresstAction extends ReduxAction {
15 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_SEARCH_IN_PROGRESS;
16 | inProgress: boolean;
17 | }
18 |
19 | export interface ConflictResolverSearchSetResultAction extends ReduxAction {
20 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_RESULT;
21 | result: IIndexResult;
22 | }
23 |
24 | export interface ConflictResolverSetProgressRelativeAction extends ReduxAction {
25 | type: Actions.CONFLICT_RESOLVER_SEARCH_SET_PROGRESS_RELATIVE;
26 | progressRelative: number;
27 | }
28 |
29 | export interface ConflictResolverSelectFiles extends ReduxAction {
30 | type: Actions.CONFLICT_RESOLVER_SELECT_FILES;
31 | files: string[];
32 | selected: boolean;
33 | }
34 |
35 | export interface ConflictResolverSetFilesFilter extends ReduxAction {
36 | type: Actions.CONFLICT_RESOLVER_SET_FILES_FILTER;
37 | filesFilter: IFilterParams;
38 | }
39 |
40 | export interface ConflictResolverSetSearchDirectory extends ReduxAction {
41 | type: Actions.CONFLICT_RESOLVER_SET_SEARCH_DIRECTORY;
42 | searchDirectory: string;
43 | }
44 |
45 | export interface ConflictResolverUpdateIndex extends ReduxAction {
46 | type: Actions.CONFLICT_RESOLVER_UPDATE_INDEX;
47 | indexUpdate: IIndexUpdate;
48 | }
49 |
--------------------------------------------------------------------------------
/ui/redux/conflict-resolver/reducers.ts:
--------------------------------------------------------------------------------
1 | import { isOk } from "../../../common/tools";
2 | import { IIndexUpdate, IndexChanges, ISearchResult } from "../../../common/types";
3 | import { pathFilter } from "../../utils/filter";
4 | import { GraphAggregator } from "../../utils/graph-aggregator";
5 | import { Actions } from "../actions";
6 | import { ConflictResolverActions } from "./actions";
7 |
8 | export interface ISelectedFilesInfo {
9 | [path: string]: boolean;
10 | }
11 |
12 | export interface IFilterParams {
13 | filter: string;
14 | isRegex: boolean;
15 | isCaseSensitive: boolean;
16 | }
17 |
18 | export interface ConflictResolverState {
19 | searchProcess: {
20 | inProgress: boolean;
21 | progressRelative: number;
22 | };
23 | searchDirectory: string;
24 | searchResult: ISearchResult;
25 | selectedConflictFiles: ISelectedFilesInfo;
26 | filesFilter: IFilterParams;
27 | }
28 |
29 | export const defaultConflictResolverState: ConflictResolverState = {
30 | searchProcess: {
31 | inProgress: false,
32 | progressRelative: 0,
33 | },
34 | searchDirectory: undefined,
35 | searchResult: undefined,
36 | selectedConflictFiles: {},
37 | filesFilter: {
38 | filter: "",
39 | isRegex: false,
40 | isCaseSensitive: false,
41 | },
42 | };
43 |
44 | const conflictFilesUpdate = (state: ISelectedFilesInfo, newSearchResult: ISearchResult): ISelectedFilesInfo => {
45 | if (isOk(newSearchResult)) {
46 | const newSelectedFiles: ISelectedFilesInfo = {};
47 |
48 | for (const group of newSearchResult.duplicates) {
49 | for (const path of group.summary.files) {
50 | newSelectedFiles[path] = path in state ? state[path] : false;
51 | }
52 | }
53 |
54 | return newSelectedFiles;
55 | }
56 |
57 | return {};
58 | };
59 |
60 | const conflictFilesSelect = (state: ConflictResolverState, files: string[], selected: boolean): ISelectedFilesInfo => {
61 | const newState: ISelectedFilesInfo = { ...state.selectedConflictFiles };
62 | const filter = state.filesFilter;
63 | for (const file of files.filter(pathFilter(filter))) {
64 | if (file in newState) {
65 | newState[file] = selected;
66 | }
67 | }
68 |
69 | return newState;
70 | };
71 |
72 | const indexUpdate = (state: ISearchResult, indexUpdateInfo: IIndexUpdate): ISearchResult => {
73 | const result: ISearchResult = {
74 | ticketId: state.ticketId,
75 | index: { ...state.index },
76 | skips: [...state.skips], // we assume that skips are not related to index
77 | duplicates: [],
78 | fileInfos: { ...state.fileInfos },
79 | };
80 |
81 | for (const path of Object.keys(indexUpdateInfo)) {
82 | const change = indexUpdateInfo[path];
83 | if (change.change === IndexChanges.Remove) {
84 | if (path in result.index) {
85 | delete result.index[path];
86 | }
87 |
88 | if (path in result.fileInfos) {
89 | delete result.fileInfos[path];
90 | }
91 | }
92 | }
93 |
94 | result.duplicates = new GraphAggregator(result.index).getResult();
95 |
96 | return result;
97 | };
98 |
99 | export const conflictResolver = (
100 | state = defaultConflictResolverState,
101 | action: ConflictResolverActions,
102 | ): ConflictResolverState => {
103 | switch (action.type) {
104 | case Actions.CONFLICT_RESOLVER_SEARCH_SET_SEARCH_IN_PROGRESS:
105 | return {
106 | ...state,
107 | searchProcess: {
108 | ...state.searchProcess,
109 | inProgress: action.inProgress,
110 | },
111 | };
112 |
113 | case Actions.CONFLICT_RESOLVER_SEARCH_SET_RESULT:
114 | let searchResult: ISearchResult;
115 |
116 | if (isOk(action.result)) {
117 | const ga = new GraphAggregator(action.result.index);
118 | searchResult = {
119 | ...action.result,
120 | duplicates: ga.getResult(),
121 | };
122 | }
123 |
124 | return {
125 | ...state,
126 | searchProcess: {
127 | ...state.searchProcess,
128 | inProgress: false,
129 | progressRelative: 0,
130 | },
131 | searchResult,
132 | selectedConflictFiles: conflictFilesUpdate(state.selectedConflictFiles, searchResult),
133 | };
134 |
135 | case Actions.CONFLICT_RESOLVER_SEARCH_SET_PROGRESS_RELATIVE:
136 | return {
137 | ...state,
138 | searchProcess: {
139 | ...state.searchProcess,
140 | progressRelative: action.progressRelative,
141 | },
142 | };
143 |
144 | case Actions.CONFLICT_RESOLVER_SELECT_FILES:
145 | return {
146 | ...state,
147 | selectedConflictFiles: conflictFilesSelect(state, action.files, action.selected),
148 | };
149 |
150 | case Actions.CONFLICT_RESOLVER_SET_FILES_FILTER:
151 | return {
152 | ...state,
153 | filesFilter: action.filesFilter,
154 | };
155 |
156 | case Actions.CONFLICT_RESOLVER_SET_SEARCH_DIRECTORY:
157 | return {
158 | ...state,
159 | searchDirectory: action.searchDirectory,
160 | };
161 |
162 | case Actions.CONFLICT_RESOLVER_UPDATE_INDEX:
163 | const newSearchResult = indexUpdate(state.searchResult, action.indexUpdate);
164 | return {
165 | ...state,
166 | searchResult: newSearchResult,
167 | selectedConflictFiles: conflictFilesUpdate(state.selectedConflictFiles, newSearchResult),
168 | };
169 |
170 | default:
171 | return state;
172 | }
173 | };
174 |
--------------------------------------------------------------------------------
/ui/redux/notification/action-creators.ts:
--------------------------------------------------------------------------------
1 | import { Actions } from "../actions";
2 | import { NotificationTypes } from "../types";
3 | import { NotificationSetMessageAction, NotificationSetTypeAction, NotificationSetVisibleAction } from "./actions";
4 |
5 | const setType = (type: NotificationTypes): NotificationSetTypeAction => ({
6 | type: Actions.NOTIFICATION_SET_TYPE,
7 | notificationType: type,
8 | });
9 |
10 | const setVisible = (visible: boolean): NotificationSetVisibleAction => ({
11 | type: Actions.NOTIFICATION_SET_VISIBLE,
12 | visible,
13 | });
14 |
15 | const setMessage = (message: string): NotificationSetMessageAction => ({
16 | type: Actions.NOTIFICATION_SET_MESSAGE,
17 | message,
18 | });
19 |
20 | export const NotificationActions = {
21 | setType,
22 | setVisible,
23 | setMessage,
24 | };
25 |
--------------------------------------------------------------------------------
/ui/redux/notification/actions.ts:
--------------------------------------------------------------------------------
1 | import { Actions, ReduxAction } from "../actions";
2 | import { NotificationTypes } from "../types";
3 |
4 | export type NotificationActions =
5 | | NotificationSetTypeAction
6 | | NotificationSetMessageAction
7 | | NotificationSetVisibleAction;
8 |
9 | export interface NotificationSetTypeAction extends ReduxAction {
10 | type: Actions.NOTIFICATION_SET_TYPE;
11 | notificationType: NotificationTypes;
12 | }
13 |
14 | export interface NotificationSetMessageAction extends ReduxAction {
15 | type: Actions.NOTIFICATION_SET_MESSAGE;
16 | message: string;
17 | }
18 |
19 | export interface NotificationSetVisibleAction extends ReduxAction {
20 | type: Actions.NOTIFICATION_SET_VISIBLE;
21 | visible: boolean;
22 | }
23 |
--------------------------------------------------------------------------------
/ui/redux/notification/reducers.ts:
--------------------------------------------------------------------------------
1 | import { Actions } from "../actions";
2 | import { NotificationTypes } from "../types";
3 | import { NotificationActions } from "./actions";
4 |
5 | interface NotificationState {
6 | message: string;
7 | type: NotificationTypes;
8 | visible: boolean;
9 | }
10 |
11 | const defaultNotificationState: NotificationState = {
12 | message: "",
13 | type: NotificationTypes.Success,
14 | visible: false,
15 | };
16 |
17 | export const notification = (state = defaultNotificationState, action: NotificationActions): NotificationState => {
18 | switch (action.type) {
19 | case Actions.NOTIFICATION_SET_MESSAGE:
20 | return {
21 | ...state,
22 | message: action.message,
23 | };
24 | case Actions.NOTIFICATION_SET_TYPE:
25 | return {
26 | ...state,
27 | type: action.notificationType,
28 | };
29 | case Actions.NOTIFICATION_SET_VISIBLE:
30 | return {
31 | ...state,
32 | visible: action.visible,
33 | };
34 | default:
35 | return state;
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/ui/redux/reducers.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { backdrop } from "./backdrop/reducers";
3 | import { conflictResolver } from "./conflict-resolver/reducers";
4 | import { notification } from "./notification/reducers";
5 | import { settings } from "./settings/reducers";
6 |
7 | export const rootReducer = combineReducers({
8 | settings,
9 | notification,
10 | backdrop,
11 | conflictResolver,
12 | });
13 |
14 | export type TState = ReturnType;
15 |
--------------------------------------------------------------------------------
/ui/redux/settings/action-creators.ts:
--------------------------------------------------------------------------------
1 | import { Language } from "../../../common/l10n";
2 | import { Actions } from "../actions";
3 | import { SettingsSetLanguageAction, SettingsSetStudioPathAction } from "./actions";
4 |
5 | const setLanguage = (newLanguage: Language): SettingsSetLanguageAction => ({
6 | type: Actions.SETTINGS_SET_LANGUAGE,
7 | newLanguage,
8 | });
9 |
10 | const setSimsStudioPath = (newPath: string): SettingsSetStudioPathAction => ({
11 | type: Actions.SETTINGS_SET_STUDIO_PATH,
12 | newPath,
13 | });
14 |
15 | export const SettingsActions = {
16 | setLanguage,
17 | setSimsStudioPath,
18 | };
19 |
--------------------------------------------------------------------------------
/ui/redux/settings/actions.ts:
--------------------------------------------------------------------------------
1 | import { Language } from "../../../common/l10n";
2 | import { Actions, ReduxAction } from "../actions";
3 |
4 | export type LanguageActions = SettingsSetLanguageAction | SettingsSetStudioPathAction;
5 |
6 | export interface SettingsSetLanguageAction extends ReduxAction {
7 | type: Actions.SETTINGS_SET_LANGUAGE;
8 | newLanguage: Language;
9 | }
10 |
11 | export interface SettingsSetStudioPathAction extends ReduxAction {
12 | type: Actions.SETTINGS_SET_STUDIO_PATH;
13 | newPath: string;
14 | }
15 |
--------------------------------------------------------------------------------
/ui/redux/settings/reducers.ts:
--------------------------------------------------------------------------------
1 | import { Language } from "../../../common/l10n";
2 | import { Actions } from "../actions";
3 | import { LanguageActions as SettingsActions } from "./actions";
4 |
5 | export interface SettingsState {
6 | language: Language;
7 | studioPath: string;
8 | }
9 |
10 | export const defaultSettingsState: SettingsState = {
11 | language: Language.English,
12 | studioPath: undefined,
13 | };
14 |
15 | export const settings = (state = defaultSettingsState, action: SettingsActions): SettingsState => {
16 | switch (action.type) {
17 | case Actions.SETTINGS_SET_LANGUAGE:
18 | return {
19 | ...state,
20 | language: action.newLanguage,
21 | };
22 | case Actions.SETTINGS_SET_STUDIO_PATH:
23 | return {
24 | ...state,
25 | studioPath: action.newPath,
26 | };
27 | default:
28 | return state;
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/ui/redux/store.ts:
--------------------------------------------------------------------------------
1 | import { Action, applyMiddleware, createStore } from "redux";
2 | import thunk, { ThunkMiddleware } from "redux-thunk";
3 | import { Actions } from "./actions";
4 | import { rootReducer, TState } from "./reducers";
5 |
6 | export const store = createStore(rootReducer, applyMiddleware(thunk as ThunkMiddleware>));
7 |
--------------------------------------------------------------------------------
/ui/redux/thunk/conflict-resolver.ts:
--------------------------------------------------------------------------------
1 | import { ipc } from "../../../common/ipc";
2 | import { isOk } from "../../../common/tools";
3 | import { IDirectoryParams, IIndexResult, ISearchParams, ISearchProgress, TTicketId } from "../../../common/types";
4 | import { ReduxThunkAction } from "../actions";
5 | import { ConflictResolverActions } from "../conflict-resolver/action-creators";
6 |
7 | const searchStartAndUpdate = (
8 | searchParameters: IDirectoryParams & ISearchParams,
9 | ): ReduxThunkAction> => async (dispatch) => {
10 | let searchTicketId: TTicketId;
11 |
12 | const onProgress = (_: any, progress: ISearchProgress) => {
13 | if (progress.ticketId === searchTicketId) {
14 | dispatch(ConflictResolverActions.setProgress(progress.progressRelative));
15 | }
16 | };
17 |
18 | try {
19 | ipc.renderer.on.searchProgress(onProgress);
20 | const startResult = await ipc.renderer.rpc.startSearch(searchParameters);
21 | searchTicketId = startResult.searchTicketId;
22 | dispatch(ConflictResolverActions.cleanupSearch());
23 | dispatch(ConflictResolverActions.setInProgress(true));
24 |
25 | const indexResult = await new Promise((resolve, reject) => {
26 | ipc.renderer.on.searchResult((_, searchIndexResult) => {
27 | if (searchIndexResult.ticketId === searchTicketId) {
28 | resolve(searchIndexResult);
29 | }
30 | });
31 |
32 | ipc.renderer.on.searchError((_, error) => {
33 | if (error.ticketId === searchTicketId) {
34 | reject(error.error);
35 | }
36 | });
37 | });
38 |
39 | dispatch(ConflictResolverActions.setIndexResult(indexResult));
40 | } catch (error) {
41 | dispatch(ConflictResolverActions.cleanupSearch());
42 | throw error;
43 | } finally {
44 | ipc.renderer.off.searchProgress(onProgress);
45 | dispatch(ConflictResolverActions.setInProgress(false));
46 | }
47 | };
48 |
49 | const selectAll = (selected: boolean): ReduxThunkAction => (dispatch, getState) => {
50 | const searchResult = getState().conflictResolver.searchResult;
51 | if (isOk(searchResult)) {
52 | dispatch(
53 | ConflictResolverActions.selectFiles(
54 | searchResult.duplicates.reduce((prev, g) => prev.concat(g.summary.files), [] as string[]),
55 | selected,
56 | ),
57 | );
58 | }
59 | };
60 |
61 | export const ConflictResolverThunk = {
62 | searchStartAndUpdate,
63 | selectAll,
64 | };
65 |
--------------------------------------------------------------------------------
/ui/redux/thunk/settings.ts:
--------------------------------------------------------------------------------
1 | import { getErrorMessage } from "../../../common/errors";
2 | import { ipc } from "../../../common/ipc";
3 | import { l10n } from "../../../common/l10n";
4 | import { isOk } from "../../../common/tools";
5 | import { createNotificationApiFromDispatch } from "../../utils/notifications";
6 | import { loadSettings, saveSettings } from "../../utils/settings/settings";
7 | import { ReduxThunkAction } from "../actions";
8 | import { SettingsActions } from "../settings/action-creators";
9 |
10 | const loadLanguage = (): ReduxThunkAction> => async (dispatch) => {
11 | const loaded = await loadSettings();
12 | dispatch(SettingsActions.setLanguage(loaded.language));
13 | };
14 |
15 | const loadRestAndValidate = (): ReduxThunkAction> => async (dispatch, getStore) => {
16 | const translation = l10n[getStore().settings.language];
17 | const notification = createNotificationApiFromDispatch(dispatch);
18 | const loaded = await loadSettings();
19 |
20 | if (isOk(loaded.studioPath)) {
21 | try {
22 | await ipc.renderer.rpc.isSimsStudioDir(loaded.studioPath);
23 | dispatch(SettingsActions.setSimsStudioPath(loaded.studioPath));
24 | } catch (error) {
25 | dispatch(SettingsActions.setSimsStudioPath(undefined));
26 | notification.showWarning(translation.studioDisabled(getErrorMessage(error, translation)));
27 | }
28 | }
29 |
30 | await saveSettings(getStore().settings);
31 | };
32 |
33 | export const SettingsThunk = {
34 | loadLanguage,
35 | loadRestAndValidate,
36 | };
37 |
--------------------------------------------------------------------------------
/ui/redux/types.ts:
--------------------------------------------------------------------------------
1 | export enum NotificationTypes {
2 | Error = "error",
3 | Success = "success",
4 | Warning = "warning",
5 | }
6 |
--------------------------------------------------------------------------------
/ui/theme.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from "@material-ui/core";
2 | import { blue, pink } from "@material-ui/core/colors";
3 |
4 | export const appTheme = createMuiTheme({
5 | palette: {
6 | primary: blue,
7 | secondary: pink,
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "noEmit": true,
5 | "target": "ES6",
6 | "esModuleInterop": true,
7 | "resolveJsonModule": true,
8 | "moduleResolution": "node"
9 | },
10 | "include": ["../globals.d.ts", "./**/*", "./common/**/*"]
11 | }
12 |
--------------------------------------------------------------------------------
/ui/utils/backdrop-hooks.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { BackdropActions } from "../redux/backdrop/action-creators";
4 |
5 | export const useBackdrop = () => {
6 | const dispatch = useDispatch();
7 |
8 | return (visible: boolean) => {
9 | dispatch(BackdropActions.setVisible(visible));
10 | };
11 | };
12 |
13 | export const useBackdropBound = (dependency) => {
14 | const setVisible = useBackdrop();
15 |
16 | useEffect(() => {
17 | setVisible(dependency);
18 | }, [dependency]);
19 | };
20 |
--------------------------------------------------------------------------------
/ui/utils/checkbox.ts:
--------------------------------------------------------------------------------
1 | export enum CheckboxState {
2 | Checked,
3 | Indeterminate,
4 | Unchecked,
5 | }
6 |
7 | export const getCheckboxState = (totalCount: number, selectedCount: number): CheckboxState => {
8 | if (totalCount === 0 || selectedCount === 0) {
9 | return CheckboxState.Unchecked;
10 | }
11 |
12 | return selectedCount === totalCount ? CheckboxState.Checked : CheckboxState.Indeterminate;
13 | };
14 |
--------------------------------------------------------------------------------
/ui/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const VIRTUALIZE_CONSTANTS = {
2 | SKIP_ITEM_HEIGHT: 72,
3 | SKIP_PLACEHOLDER_PADDING_LEFT: 72,
4 | SKIP_PLACEHOLDER_PADDING_TOP: 14,
5 | DUPLICATE_PLACEHOLDER_PADDING_LEFT: 58,
6 | DUPLICATE_PLACEHOLDER_PADDING_TOP: 14,
7 | };
8 |
--------------------------------------------------------------------------------
/ui/utils/filter.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { isOk } from "../../common/tools";
3 | import { IFilterParams } from "../redux/conflict-resolver/reducers";
4 |
5 | const isValidRegex = (reg: string) => {
6 | try {
7 | // tslint:disable-next-line: no-unused-expression-chai
8 | new RegExp(reg);
9 | return true;
10 | } catch {
11 | return false;
12 | }
13 | };
14 |
15 | export const isFilterUsed = ({ filter }: IFilterParams) => isOk(filter) && filter.length !== 0;
16 |
17 | export const isFilterValid = (filterParams: IFilterParams) =>
18 | isFilterUsed(filterParams) && (!filterParams.isRegex || isValidRegex(filterParams.filter));
19 |
20 | export const pathFilter = (filesFilter: IFilterParams) => (p: string): boolean => {
21 | if (isFilterValid(filesFilter)) {
22 | let basename = path.basename(p);
23 | let filter = filesFilter.filter;
24 |
25 | if (!filesFilter.isCaseSensitive) {
26 | basename = basename.toLowerCase();
27 | filter = filter.toLowerCase();
28 | }
29 |
30 | const index = filesFilter.isRegex ? basename.search(filter) : basename.indexOf(filter);
31 | return index !== -1;
32 | }
33 |
34 | return true;
35 | };
36 |
--------------------------------------------------------------------------------
/ui/utils/graph-aggregator.ts:
--------------------------------------------------------------------------------
1 | import { TIndex } from "../../common/indexer/types";
2 | import { DoubleTypes, IDuplicateGroup, IEdgeGroup, TFileValue, TKeyValue } from "../../common/types";
3 |
4 | type TEncodeKey = string;
5 | type TEncoded = Record;
6 | type TDecoded = Record;
7 | type TKeyTypes = Record;
8 |
9 | type TEncodeFileGroup = TEncodeKey[];
10 | type TSimilarFilesEntry = {
11 | keys: TEncodeKey[];
12 | files: TEncodeFileGroup;
13 | };
14 | type TByKeys = Record;
15 | type TBySimilarFileGroupsEntry = {
16 | keys: TEncodeKey[];
17 | fileGroups: TEncodeFileGroup[];
18 | };
19 |
20 | type TNodeName = string;
21 | type TGraphInfo = Record;
22 | type TGraph = Record;
23 |
24 | export class GraphAggregator {
25 | /*
26 | the class created to analyze index, split files to independent groups,
27 | group files with similar keys, and create summary of each group to simplify usage outside
28 |
29 | */
30 | private result: IDuplicateGroup[];
31 |
32 | constructor(fileIndex: TIndex) {
33 | this.result = this.build(fileIndex);
34 | }
35 |
36 | public getResult() {
37 | return this.result;
38 | }
39 |
40 | private isSingleFile(fileGroups: TEncodeFileGroup[]) {
41 | let total = 0;
42 | for (const group of fileGroups) {
43 | total += group.length;
44 | }
45 | return total === 1;
46 | }
47 |
48 | private build(index) {
49 | /*
50 | Let's say your index looks this way:
51 | file1: [key1, key2, key3]
52 | file2: [key1, key2, key3]
53 | file3: [key3]
54 | file4: [key4]
55 | file5: [key4]
56 |
57 | file1+-------+key1+-------+file2 file4
58 | | | +
59 | +-------+key2+-------+ |
60 | | | +
61 | +-------+key3+-------+ key4
62 | + +
63 | | |
64 | + +
65 | file3 file5
66 | */
67 |
68 | /*
69 | Here we replace filenames and keys with simple names
70 | (js doesn't support tuple usage as key, so we collapse complex names, to use names.join(', ')
71 | as key)
72 | id1: [id6, id7, id8]
73 | id2: [id6, id7, id8]
74 | id3: [id8]
75 | id4: [id9]
76 | id5: [id9]
77 |
78 | id1+-------+id6+--------+id2 id4
79 | | | +
80 | +-------+id7+--------+ |
81 | | | +
82 | +-------+id8+--------+ id9
83 | + +
84 | | |
85 | + +
86 | id3 id5
87 | */
88 | const [encoded, decoded, keyTypes] = this.encodeFilesAndKeys(index);
89 |
90 | /*
91 | Here we group files with similar keys
92 | [id1, id2] - [id6, id7, id8]
93 | id3 - [id8]
94 | [id4, id5] - [id9]
95 |
96 | [id1, id2]+-------+id6 [id4, id5]
97 | | +
98 | +-------+id7 |
99 | | +
100 | +-------+id8 id9
101 | +
102 | |
103 | +
104 | id3
105 | */
106 | const bySimilarFiles = this.aggregateBySimilarFiles(index, encoded);
107 |
108 | /*
109 | Creating reversed data, key <-> files and fileGroups
110 | id6: [id1, id2]
111 | id7: [id1, id2]
112 | id8: [id1, id2], id3
113 | id9: [id4, id5]
114 |
115 | also remove keys that have only single file (those files do not conflict with anything)
116 | */
117 | const byKeys = this.removeUnconflicted(this.aggregateByKeys(bySimilarFiles));
118 |
119 | /*
120 | group by keys that share similar file groups
121 | [id6, id7]: [id1, id2]
122 | id8: [id1, id2], id3
123 | id9: [id4, id5]
124 |
125 | [id1, id2]+-------+[id6, id7] [id4, id5]
126 | | +
127 | | |
128 | +-------+id8 +
129 | + id9
130 | |
131 | +
132 | id3
133 | */
134 | const byFileGroupsAndKeys = this.aggregateBySimilarFileGroups(byKeys);
135 |
136 | /*
137 | build the graph itself, and save infos ([id1, id2] was list, now it's key 'id1,id2',
138 | but the data must be saved)
139 | graph:
140 | 'id6,id7: 'id1,id2',
141 | 'id8': 'id1,id2', 'id3',
142 | 'id9': 'id4,id5',
143 | 'id3': 'id8',
144 | 'id1,id2': 'id6,id7', 'id8',
145 | 'id4,id5': 'id9',
146 | */
147 | const [graph, infos] = this.buildGraph(byFileGroupsAndKeys);
148 |
149 | /*
150 | dfs through graphs, and distinguish each component
151 | one component is a group of files that conflict with each other
152 | */
153 | const graphGroups = this.splitIntoGroups(graph);
154 | const result: IDuplicateGroup[] = [];
155 |
156 | // decode all the filenames, key values, types and write as list of groups
157 | for (const group of graphGroups) {
158 | result.push(this.buildResultGroup(group, infos, decoded, keyTypes));
159 | }
160 |
161 | return result;
162 | }
163 |
164 | private toKey(arr: any[]): string {
165 | return arr.sort().join(",");
166 | }
167 |
168 | private aggregateBySimilarFiles(index: TIndex, encoded: TEncoded) {
169 | const result: Record = {};
170 | for (const path of Object.keys(index)) {
171 | const keys = index[path].map((x) => encoded[x[1]]);
172 | const keysKey = this.toKey(keys);
173 | if (!(keysKey in result)) {
174 | result[keysKey] = {
175 | keys,
176 | files: [],
177 | };
178 | }
179 |
180 | result[keysKey].files.push(encoded[path]);
181 | }
182 |
183 | return Object.values(result);
184 | }
185 |
186 | private encodeFilesAndKeys(index): [TEncoded, TDecoded, TKeyTypes] {
187 | const encoded: TEncoded = {};
188 | const decoded: TDecoded = {};
189 | const keyTypes: TKeyTypes = {};
190 |
191 | let lastId = 0;
192 | const getCurrentId = (): TEncodeKey => `id${lastId}`;
193 | const nextId = (): TEncodeKey => {
194 | lastId++;
195 | return getCurrentId();
196 | };
197 |
198 | for (const path of Object.keys(index)) {
199 | encoded[path] = nextId();
200 | decoded[getCurrentId()] = path;
201 | for (const [keyType, key] of index[path]) {
202 | if (!(key in keyTypes)) {
203 | keyTypes[key] = keyType;
204 | }
205 | encoded[key] = nextId();
206 | decoded[getCurrentId()] = key;
207 | }
208 | }
209 |
210 | return [encoded, decoded, keyTypes];
211 | }
212 |
213 | private aggregateByKeys(bySimilarFiles: TSimilarFilesEntry[]): TByKeys {
214 | const result: TByKeys = {};
215 | for (const { keys, files } of bySimilarFiles) {
216 | for (const key of keys) {
217 | if (!(key in result)) {
218 | result[key] = [];
219 | }
220 |
221 | result[key].push(files);
222 | }
223 | }
224 |
225 | return result;
226 | }
227 |
228 | private removeUnconflicted(byKeys: TByKeys): TByKeys {
229 | const result = { ...byKeys };
230 | for (const key of Object.keys(result)) {
231 | if (this.isSingleFile(result[key])) {
232 | delete result[key];
233 | }
234 | }
235 |
236 | return result;
237 | }
238 |
239 | private aggregateBySimilarFileGroups(byKeys: TByKeys): TBySimilarFileGroupsEntry[] {
240 | const result: Record = {};
241 | for (const key of Object.keys(byKeys)) {
242 | const fileKey = this.toKey(byKeys[key].map((x) => `[${this.toKey(x)}]`));
243 | if (!(fileKey in result)) {
244 | result[fileKey] = {
245 | fileGroups: byKeys[key],
246 | keys: [],
247 | };
248 | }
249 |
250 | result[fileKey].keys.push(key);
251 | }
252 |
253 | return Object.values(result);
254 | }
255 |
256 | private buildGraph(byFiles: TBySimilarFileGroupsEntry[]): [TGraph, TGraphInfo] {
257 | const infos: TGraphInfo = {};
258 | const graph: TGraph = {};
259 |
260 | for (const { fileGroups, keys } of byFiles) {
261 | const keyGroupName = this.toKey(keys);
262 |
263 | if (!(keyGroupName in graph)) {
264 | graph[keyGroupName] = [];
265 | }
266 |
267 | infos[keyGroupName] = {
268 | fileGroups,
269 | keys,
270 | };
271 |
272 | for (const group of fileGroups) {
273 | const fileGroupName = this.toKey(group);
274 |
275 | if (!(fileGroupName in graph)) {
276 | graph[fileGroupName] = [];
277 | }
278 |
279 | graph[fileGroupName].push(keyGroupName);
280 | graph[keyGroupName].push(fileGroupName);
281 | }
282 | }
283 |
284 | return [graph, infos];
285 | }
286 |
287 | private splitIntoGroups(graph: TGraph): TNodeName[][] {
288 | const visited: Set = new Set();
289 | const graphGroups: TNodeName[][] = [];
290 |
291 | for (const nodeName of Object.keys(graph)) {
292 | if (!visited.has(nodeName)) {
293 | const newGroup: TNodeName[] = [];
294 | this.dfs(graph, nodeName, newGroup, visited);
295 | graphGroups.push(newGroup);
296 | }
297 | }
298 |
299 | return graphGroups;
300 | }
301 |
302 | private dfs(graph: TGraph, nodeName: TNodeName, currentGroup: TNodeName[], visited: Set) {
303 | if (visited.has(nodeName)) {
304 | return;
305 | }
306 |
307 | currentGroup.push(nodeName);
308 | visited.add(nodeName);
309 | for (const neighbor of graph[nodeName]) {
310 | this.dfs(graph, neighbor, currentGroup, visited);
311 | }
312 | }
313 |
314 | private buildResultGroup(
315 | group: TNodeName[],
316 | infos: TGraphInfo,
317 | decoded: TDecoded,
318 | keyTypes: TKeyTypes,
319 | ): IDuplicateGroup {
320 | const result: IDuplicateGroup = {
321 | detailed: {
322 | edgeGroups: [],
323 | typeByKey: keyTypes,
324 | },
325 | summary: {
326 | files: [],
327 | types: [],
328 | },
329 | };
330 |
331 | const uniqueFiles: Set = new Set();
332 | const uniqueTypes: Set = new Set();
333 |
334 | for (const node of group) {
335 | if (node in infos) {
336 | const edgeGroup: IEdgeGroup = {
337 | fileGroups: infos[node].fileGroups.map((gr) => gr.map((f) => decoded[f])),
338 | keys: infos[node].keys.map((k) => decoded[k]),
339 | };
340 | edgeGroup.keys.forEach((k) => uniqueTypes.add(keyTypes[k]));
341 | edgeGroup.fileGroups.forEach((gr) => gr.forEach((f) => uniqueFiles.add(f)));
342 | result.detailed.edgeGroups.push(edgeGroup);
343 | }
344 | }
345 |
346 | result.summary.files = Array.from(uniqueFiles);
347 | result.summary.types = Array.from(uniqueTypes);
348 | return result;
349 | }
350 | }
351 |
--------------------------------------------------------------------------------
/ui/utils/ipc-hooks.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ipc, IpcSchema } from "../../common/ipc";
3 | import { TIpcEventHandler, TIpcOutput, TIpcSchema } from "../../common/ipc/ipc-creator";
4 |
5 | type TIpcHooksOutput = {
6 | use: {
7 | [K in keyof T["mainEvents"]]: (handler: TIpcEventHandler) => void;
8 | };
9 | };
10 |
11 | type TIpcRenderer = TIpcOutput["renderer"];
12 |
13 | class IpcHooksCreator {
14 | public interface: TIpcHooksOutput = { use: {} } as TIpcHooksOutput;
15 |
16 | constructor(schema: T, ipcRendererTypesafe: TIpcRenderer) {
17 | for (const channelName of Object.keys(schema.mainEvents)) {
18 | this.createHook(ipcRendererTypesafe, channelName);
19 | }
20 | }
21 |
22 | private createHook(ipcRendererTypesafe: TIpcRenderer, name: K): void {
23 | this.interface.use[name] = (handler) => {
24 | React.useEffect(() => {
25 | ipcRendererTypesafe.on[name](handler);
26 | return () => ipcRendererTypesafe.off[name](handler);
27 | });
28 | };
29 | }
30 | }
31 |
32 | function createTypsafeHooks(
33 | ipcSchema: T,
34 | ipcRendererTypesafe: TIpcRenderer,
35 | ): TIpcHooksOutput {
36 | const hooksCreator = new IpcHooksCreator(ipcSchema, ipcRendererTypesafe);
37 | return hooksCreator.interface;
38 | }
39 |
40 | export const ipcHooks = createTypsafeHooks(IpcSchema, ipc.renderer);
41 |
--------------------------------------------------------------------------------
/ui/utils/l10n-hooks.ts:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { l10n, Language, Translation } from "../../common/l10n";
3 | import { TState } from "../redux/reducers";
4 |
5 | export function useL10n(): [Translation, Language] {
6 | const language = useSelector((state: TState) => state.settings.language);
7 | return [l10n[language], language];
8 | }
9 |
--------------------------------------------------------------------------------
/ui/utils/language-mapping.ts:
--------------------------------------------------------------------------------
1 | import { Translation } from "../../common/l10n";
2 | import { DoubleTypes } from "../../common/types";
3 |
4 | export const doubleTypeMap = {
5 | [DoubleTypes.Exact]: (l10n: Translation) => ({
6 | tooltip: l10n.exactDescription,
7 | title: l10n.exact,
8 | }),
9 | [DoubleTypes.Catalog]: (l10n: Translation) => ({
10 | tooltip: l10n.catalogDescription,
11 | title: l10n.catalog,
12 | }),
13 | [DoubleTypes.Skintone]: (l10n: Translation) => ({
14 | tooltip: l10n.skintoneDescription,
15 | title: l10n.skintone,
16 | }),
17 | [DoubleTypes.Cas]: (l10n: Translation) => ({
18 | tooltip: l10n.casDescription,
19 | title: l10n.cas,
20 | }),
21 | [DoubleTypes.Slider]: (l10n: Translation) => ({
22 | tooltip: l10n.sliderDescription,
23 | title: l10n.slider,
24 | }),
25 | };
26 |
--------------------------------------------------------------------------------
/ui/utils/notifications.ts:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { NotificationActions } from "../redux/notification/action-creators";
3 | import { NotificationTypes } from "../redux/types";
4 |
5 | interface INotificationApi {
6 | showError: (msg: string) => void;
7 | showSuccess: (msg: string) => void;
8 | showWarning: (msg: string) => void;
9 | }
10 |
11 | export function createNotificationApiFromDispatch(dispatch): INotificationApi {
12 | const show = (type: NotificationTypes) => (msg) => {
13 | dispatch(NotificationActions.setType(type));
14 | dispatch(NotificationActions.setMessage(msg));
15 | dispatch(NotificationActions.setVisible(true));
16 | };
17 |
18 | return {
19 | showSuccess: show(NotificationTypes.Success),
20 | showError: show(NotificationTypes.Error),
21 | showWarning: show(NotificationTypes.Warning),
22 | };
23 | }
24 |
25 | export function useNotification(): INotificationApi {
26 | const dispatch = useDispatch();
27 | return createNotificationApiFromDispatch(dispatch);
28 | }
29 |
--------------------------------------------------------------------------------
/ui/utils/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import storage from "electron-json-storage";
2 | import _ from "lodash";
3 | import { isOk } from "../../../common/tools";
4 | import { defaultSettingsState, SettingsState } from "../../redux/settings/reducers";
5 |
6 | const SETTINGS_FILENAME = "settings";
7 |
8 | const get = () =>
9 | new Promise