From 16859d206f7e648578ebc4f5aac31867939f33f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 18 Nov 2022 17:57:24 +0200 Subject: [PATCH 1/3] Move mod filters to Vuex store Some of the filters in manager page are managed via a modal. Having the modal inside the Manager page makes it difficult to move towards route based navigation. But before the modal can be moved out of the manager page, we need to move the management of filter state to make them universally accessible. Refs TS-1309 --- src/model/enums/CategoryFilterMode.ts | 10 +++-- src/pages/Manager.vue | 62 ++++++++++++------------- src/store/index.ts | 6 +-- src/store/modules/ModFilterModule.ts | 65 +++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 37 deletions(-) create mode 100644 src/store/modules/ModFilterModule.ts diff --git a/src/model/enums/CategoryFilterMode.ts b/src/model/enums/CategoryFilterMode.ts index 4ab27c1c0..2c2bfbc57 100644 --- a/src/model/enums/CategoryFilterMode.ts +++ b/src/model/enums/CategoryFilterMode.ts @@ -1,5 +1,7 @@ -export default { - OR: 'Mod has at least one of these categories', - AND: 'Mod has all of these categories', - EXCLUDE: 'Mod has none of these categories' +enum CategoryFilterMode { + OR = 'Mod has at least one of these categories', + AND = 'Mod has all of these categories', + EXCLUDE = 'Mod has none of these categories' }; + +export default CategoryFilterMode; diff --git a/src/pages/Manager.vue b/src/pages/Manager.vue index 436126d08..841ca23d3 100644 --- a/src/pages/Manager.vue +++ b/src/pages/Manager.vue @@ -139,7 +139,7 @@ - @@ -147,10 +147,10 @@
-
-
+
+
- + {{ key }} @@ -413,9 +413,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; showDependencyStrings: boolean = false; showCategoryFilterModal: boolean = false; - filterCategories: string[] = []; - categoryFilterMode: string = CategoryFilterMode.OR; - allowNsfw: boolean = false; importingLocalMod: boolean = false; @@ -438,6 +435,22 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; this.filterThunderstoreModList(); } + get allowNsfw(): boolean { + return this.$store.state.modFilters.allowNsfw; + } + + set allowNsfw(value: boolean) { + this.$store.commit("modFilters/setAllowNsfw", value); + } + + get categoryFilterMode(): CategoryFilterMode { + return this.$store.state.modFilters.categoryFilterMode; + } + + set categoryFilterMode(value: CategoryFilterMode) { + this.$store.commit("modFilters/setCategoryFilterMode", value); + } + get thunderstoreModList(): ThunderstoreMod[] { return this.$store.state.thunderstoreModList || []; } @@ -468,6 +481,10 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; } filterThunderstoreModList() { + const allowNsfw = this.$store.state.modFilters.allowNsfw; + const categoryFilterMode = this.$store.state.modFilters.categoryFilterMode; + const filterCategories = this.$store.state.modFilters.selectedCategories; + const lowercaseSearchFilter = this.thunderstoreSearchFilter.toLowerCase(); this.searchableThunderstoreModList = this.sortedThunderstoreModList; if (lowercaseSearchFilter.trim().length > 0) { @@ -476,18 +493,18 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; || x.getVersions()[0].getDescription().toLowerCase().indexOf(lowercaseSearchFilter) >= 0; }); } - if (!this.allowNsfw) { + if (!allowNsfw) { this.searchableThunderstoreModList = this.searchableThunderstoreModList.filter(mod => !mod.getNsfwFlag()); } - if (this.filterCategories.length > 0) { + if (filterCategories.length > 0) { this.searchableThunderstoreModList = this.searchableThunderstoreModList.filter((x: ThunderstoreMod) => { - switch(this.categoryFilterMode) { + switch(categoryFilterMode) { case CategoryFilterMode.OR: - return ArrayUtils.includesSome(x.getCategories(), this.filterCategories); + return ArrayUtils.includesSome(x.getCategories(), filterCategories); case CategoryFilterMode.AND: - return ArrayUtils.includesAll(x.getCategories(), this.filterCategories); + return ArrayUtils.includesAll(x.getCategories(), filterCategories); case CategoryFilterMode.EXCLUDE: - return !ArrayUtils.includesSome(x.getCategories(), this.filterCategories); + return !ArrayUtils.includesSome(x.getCategories(), filterCategories); } }) } @@ -938,27 +955,11 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; }); } - get availableCategories(): string[] { - this.filterCategories.includes(""); - const flatArray: Array = Array.from( - new Set(this.thunderstoreModList - .map((value) => value.getCategories()) - .flat(1)) - ); - return flatArray - .filter((category) => !this.filterCategories.includes(category)) - .sort(); - } - addFilterCategory(target: HTMLSelectElement) { - this.filterCategories.push(target.value); + this.$store.commit("modFilters/selectCategory", target.value); target.selectedIndex = 0; } - removeCategory(key: string) { - this.filterCategories = this.filterCategories.filter(value => value !== key); - } - get categoryFilterValues() { return Object.values(CategoryFilterMode); } @@ -1060,6 +1061,7 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; this.$emit('error', newModList); } this.sortThunderstoreModList(); + this.$store.commit("modFilters/reset"); InteractionProvider.instance.hookModInstallProtocol(async data => { const combo: ThunderstoreCombo | R2Error = ThunderstoreCombo.fromProtocol(data, this.thunderstoreModList); diff --git a/src/store/index.ts b/src/store/index.ts index 842c56d09..889c0404d 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,13 +2,12 @@ import Vue from 'vue'; import Vuex, { ActionContext } from 'vuex'; import ModalsModule from './modules/ModalsModule'; +import ModFilterModule from './modules/ModFilterModule'; import { FolderMigration } from '../migrations/FolderMigration'; import ManifestV2 from '../model/ManifestV2'; import ThunderstoreMod from '../model/ThunderstoreMod'; import ThunderstorePackages from '../r2mm/data/ThunderstorePackages'; -// import example from './module-example' - Vue.use(Vuex); export interface State { @@ -85,7 +84,8 @@ export const store = { } }, modules: { - modals: ModalsModule + modals: ModalsModule, + modFilters: ModFilterModule, }, // enable strict mode (adds overhead!) diff --git a/src/store/modules/ModFilterModule.ts b/src/store/modules/ModFilterModule.ts new file mode 100644 index 000000000..aba767965 --- /dev/null +++ b/src/store/modules/ModFilterModule.ts @@ -0,0 +1,65 @@ +import { GetterTree } from "vuex"; + +import { State as RootState } from "../index"; +import CategoryFilterMode from "../../model/enums/CategoryFilterMode"; + +interface State { + allowNsfw: boolean; + categoryFilterMode: CategoryFilterMode; + selectedCategories: string[]; +} + +export default { + namespaced: true, + + state: (): State => ({ + allowNsfw: false, + categoryFilterMode: CategoryFilterMode.OR, + selectedCategories: [] + }), + + getters: >{ + allCategories (_state, _getters, rootState) { + const categories = Array.from( + new Set( + rootState.thunderstoreModList + .map((mod) => mod.getCategories()) + .flat() + ) + ); + categories.sort(); + return categories; + }, + + unselectedCategories (state, getters) { + // TODO: try to find a way to fix typing for getters parameter. + const cats: string[] = getters.allCategories; + + return cats.filter((c: string) => !state.selectedCategories.includes(c)); + } + }, + + mutations: { + reset: function(state: State) { + state.allowNsfw = false; + state.categoryFilterMode = CategoryFilterMode.OR; + state.selectedCategories = []; + }, + + selectCategory: function(state: State, category: string) { + state.selectedCategories = [...state.selectedCategories, category]; + }, + + setAllowNsfw: function(state: State, value: boolean) { + state.allowNsfw = value; + }, + + setCategoryFilterMode: function(state: State, value: CategoryFilterMode) { + state.categoryFilterMode = value; + }, + + unselectCategory: function(state: State, category: string) { + state.selectedCategories = state.selectedCategories.filter((c) => c !== category); + } + }, +} From 88b199d91883a4ba70b73b618c2de993642dd39d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Tue, 22 Nov 2022 13:51:23 +0200 Subject: [PATCH 2/3] Add standalone CategoryFilterModal Continue making Manager page less of a god-class component. This also partially fixes the problem where "allow NSFW" checkbox didn't react correctly to dark/light theme. The value is now read from correct place, so the checkbox is themed based on the theme that was active when user entered Manager page. However, if user changes the theme while on manager page, the checkbox theme is not updated yet. I wasn't sure how this should be solved, and it falls outside the scope of the ticket, so I just marked the issue with TODO comment. Refs TS-1309 --- src/components/modals/CategoryFilterModal.vue | 154 ++++++++++++++++++ src/pages/Manager.vue | 93 +---------- src/store/modules/ModalsModule.ts | 10 ++ 3 files changed, 168 insertions(+), 89 deletions(-) create mode 100644 src/components/modals/CategoryFilterModal.vue diff --git a/src/components/modals/CategoryFilterModal.vue b/src/components/modals/CategoryFilterModal.vue new file mode 100644 index 000000000..be8e802a4 --- /dev/null +++ b/src/components/modals/CategoryFilterModal.vue @@ -0,0 +1,154 @@ + + + diff --git a/src/pages/Manager.vue b/src/pages/Manager.vue index 841ca23d3..7b7fe9e26 100644 --- a/src/pages/Manager.vue +++ b/src/pages/Manager.vue @@ -127,66 +127,7 @@ - - - - - - +
- +
@@ -358,6 +299,7 @@ import GameRunnerProvider from '../providers/generic/game/GameRunnerProvider'; import LocalFileImportModal from '../components/importing/LocalFileImportModal.vue'; import { PackageLoader } from '../model/installing/PackageLoader'; import GameInstructions from '../r2mm/launching/instructions/GameInstructions'; +import CategoryFilterModal from '../components/modals/CategoryFilterModal.vue'; import GameRunningModal from '../components/modals/GameRunningModal.vue'; @Component({ @@ -367,6 +309,7 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; LocalModList: LocalModListProvider.provider, NavigationMenu: NavigationMenuProvider.provider, SettingsView, + CategoryFilterModal, DownloadModModal, GameRunningModal, 'hero': Hero, @@ -412,8 +355,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; showUpdateAllModal: boolean = false; showDependencyStrings: boolean = false; - showCategoryFilterModal: boolean = false; - importingLocalMod: boolean = false; doorstopTarget: string = ""; @@ -435,22 +376,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; this.filterThunderstoreModList(); } - get allowNsfw(): boolean { - return this.$store.state.modFilters.allowNsfw; - } - - set allowNsfw(value: boolean) { - this.$store.commit("modFilters/setAllowNsfw", value); - } - - get categoryFilterMode(): CategoryFilterMode { - return this.$store.state.modFilters.categoryFilterMode; - } - - set categoryFilterMode(value: CategoryFilterMode) { - this.$store.commit("modFilters/setCategoryFilterMode", value); - } - get thunderstoreModList(): ThunderstoreMod[] { return this.$store.state.thunderstoreModList || []; } @@ -475,7 +400,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; } @Watch("thunderstoreModList") - @Watch("showCategoryFilterModal") thunderstoreModListUpdate() { this.sortThunderstoreModList(); } @@ -955,15 +879,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue'; }); } - addFilterCategory(target: HTMLSelectElement) { - this.$store.commit("modFilters/selectCategory", target.value); - target.selectedIndex = 0; - } - - get categoryFilterValues() { - return Object.values(CategoryFilterMode); - } - handleSettingsCallbacks(invokedSetting: any) { switch(invokedSetting) { case "BrowseDataFolder": diff --git a/src/store/modules/ModalsModule.ts b/src/store/modules/ModalsModule.ts index fae438803..19c0ef6bc 100644 --- a/src/store/modules/ModalsModule.ts +++ b/src/store/modules/ModalsModule.ts @@ -1,17 +1,27 @@ interface State { + isCategoryFilterModalOpen: boolean; isGameRunningModalOpen: boolean; } export default { state: (): State => ({ + isCategoryFilterModalOpen: false, isGameRunningModalOpen: false, }), mutations: { + closeCategoryFilterModal: function(state: State): void { + state.isCategoryFilterModalOpen = false; + }, + closeGameRunningModal: function(state: State): void { state.isGameRunningModalOpen = false; }, + openCategoryFilterModal: function(state: State): void { + state.isCategoryFilterModalOpen = true; + }, + openGameRunningModal: function(state: State): void { state.isGameRunningModalOpen = true; } From 792431494dccf5bd21e0a0ebba33c99f63e14f7c Mon Sep 17 00:00:00 2001 From: Mythic Date: Tue, 29 Nov 2022 21:04:24 +0200 Subject: [PATCH 3/3] Rename cats to categories Rename a variable from cats to categories as requested in PR review. Not squashing to previous commit as to avoid creating conflicts in the PR chain. --- src/components/modals/CategoryFilterModal.vue | 2 -- src/store/modules/ModFilterModule.ts | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/modals/CategoryFilterModal.vue b/src/components/modals/CategoryFilterModal.vue index be8e802a4..53a7ecd76 100644 --- a/src/components/modals/CategoryFilterModal.vue +++ b/src/components/modals/CategoryFilterModal.vue @@ -120,8 +120,6 @@ export default class CategoryFilterModal extends Vue { this.settings = await ManagerSettings.getSingleton(GameManager.activeGame); } - // TODO: The value doesn't get updated if the theme is toggled while - // the modal in mounted. get isDarkTheme(): boolean { return this.settings.getContext().global.darkTheme; } diff --git a/src/store/modules/ModFilterModule.ts b/src/store/modules/ModFilterModule.ts index aba767965..817d06daa 100644 --- a/src/store/modules/ModFilterModule.ts +++ b/src/store/modules/ModFilterModule.ts @@ -32,10 +32,8 @@ export default { }, unselectedCategories (state, getters) { - // TODO: try to find a way to fix typing for getters parameter. - const cats: string[] = getters.allCategories; - - return cats.filter((c: string) => !state.selectedCategories.includes(c)); + const categories: string[] = getters.allCategories; + return categories.filter((c: string) => !state.selectedCategories.includes(c)); } },