Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decouple category filter modal #885

Merged
merged 3 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions src/components/modals/CategoryFilterModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<template>
<modal v-show="isOpen" :open="isOpen" :show-close="false">
<template v-slot:title>
<p class="card-header-title">Filter mod categories</p>
</template>
<template v-slot:body>
<div class="input-group">
<label>Categories</label>
<select class="select select--content-spacing" @change="selectCategory($event)">
<option selected disabled>
Select a category
</option>
<option v-for="(key, index) in unselectedCategories" :key="`category--${key}-${index}`">
{{ key }}
</option>
</select>
</div>
<br/>
<div class="input-group">
<label>Selected categories:</label>
<div class="field has-addons" v-if="selectedCategories.length > 0">
<div class="control" v-for="(key, index) in selectedCategories" :key="`${key}-${index}`">
<span class="block margin-right">
<a href="#" @click="unselectCategory(key)">
<span class="tags has-addons">
<span class="tag">{{ key }}</span>
<span class="tag is-danger">
<i class="fas fa-times"></i>
</span>
</span>
</a>
</span>
</div>
</div>
<div class="field has-addons" v-else>
<span class="tags">
<span class="tag">No categories selected</span>
</span>
</div>
</div>
<hr/>
<div>
<input
v-model="allowNsfw"
id="nsfwCheckbox"
class="is-checkradio has-background-color"
type="checkbox"
:class="[{'is-dark': !isDarkTheme, 'is-white': isDarkTheme}]"
>
<label for="nsfwCheckbox">Allow NSFW (potentially explicit) mods</label>
</div>
<br/>
<div>
<div v-for="(key, index) in categoryFilterValues" :key="`cat-filter-${key}-${index}`">
<input
name="categoryFilterCondition"
type="radio"
:id="`cat-filter-${key}-${index}`"
:value=key
:checked="index === 0 ? true : undefined" v-model="categoryFilterMode"
/>
<label :for="`cat-filter-${key}-${index}`">
<span class="margin-right margin-right--half-width" />
{{ key }}
</label>
</div>
</div>
</template>
<template v-slot:footer>
<button class="button is-info" @click="close">
Apply filters
</button>
</template>
</modal>
</template>

<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";

import { Modal } from '../../components/all';
import CategoryFilterMode from "../../model/enums/CategoryFilterMode";
import GameManager from "../../model/game/GameManager";
import ManagerSettings from "../../r2mm/manager/ManagerSettings";

@Component({
components: { Modal }
})
export default class CategoryFilterModal extends Vue {
settings: ManagerSettings = new ManagerSettings();

@Prop({default: () => null})
private onClose!: () => Promise<void>;

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 categoryFilterValues() {
return Object.values(CategoryFilterMode);
}

close() {
this.onClose();
this.$store.commit("closeCategoryFilterModal");
}

async created() {
this.settings = await ManagerSettings.getSingleton(GameManager.activeGame);
}

get isDarkTheme(): boolean {
return this.settings.getContext().global.darkTheme;
}

get isOpen(): boolean {
return this.$store.state.modals.isCategoryFilterModalOpen;
}

selectCategory(event: Event) {
if (!(event.target instanceof HTMLSelectElement)) {
return;
}

this.$store.commit("modFilters/selectCategory", event.target.value);
event.target.selectedIndex = 0;
}

get selectedCategories(): string[] {
return this.$store.state.modFilters.selectedCategories;
}

unselectCategory(category: string) {
this.$store.commit("modFilters/unselectCategory", category);
}

get unselectedCategories(): string[] {
return this.$store.getters["modFilters/unselectedCategories"];
}
}
</script>
10 changes: 6 additions & 4 deletions src/model/enums/CategoryFilterMode.ts
Original file line number Diff line number Diff line change
@@ -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;
113 changes: 15 additions & 98 deletions src/pages/Manager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,66 +127,7 @@
</template>
</modal>

<modal v-show="showCategoryFilterModal" :open="showCategoryFilterModal" :show-close="false">
<template v-slot:title>
<p class='card-header-title'>Filter mod categories</p>
</template>
<template v-slot:body>

<div class="input-group">
<label>Categories</label>
<select class="select select--content-spacing" @change="addFilterCategory($event.target)">
<option selected disabled>
Select a category
</option>
<option v-for="(key, index) in availableCategories" :key="`category--${key}-${index}`">
{{ key }}
</option>
</select>
</div>
<br/>
<div class="input-group">
<label>Selected categories:</label>
<div class="field has-addons" v-if="filterCategories.length > 0">
<div class="control" v-for="(key, index) in filterCategories" :key="`${key}-${index}`">
<span class="block margin-right">
<a href="#" @click="removeCategory(key)">
<span class="tags has-addons">
<span class="tag">{{ key }}</span>
<span class="tag is-danger">
<i class="fas fa-times"></i>
</span>
</span>
</a>
</span>
</div>
</div>
<div class="field has-addons" v-else>
<span class="tags">
<span class="tag">No categories selected</span>
</span>
</div>
</div>
<hr/>
<div>
<input class="is-checkradio has-background-color" id="nsfwCheckbox" type="checkbox" :class="[{'is-dark':!settings.darkTheme}, {'is-white':settings.darkTheme}]" v-model="allowNsfw">
<label for="nsfwCheckbox">Allow NSFW (potentially explicit) mods</label>
</div>
<br/>
<div>
<div v-for="(key, index) in categoryFilterValues" :key="`cat-filter-${key}-${index}`">
<input type="radio" :id="`cat-filter-${key}-${index}`" name="categoryFilterCondition" :value=key :checked="index === 0 ? true : undefined" v-model="categoryFilterMode">
<label :for="`cat-filter-${key}-${index}`"><span class="margin-right margin-right--half-width"/>{{ key }}</label>
</div>
</div>
</template>
<template v-slot:footer>
<button class="button is-info" @click="showCategoryFilterModal = false;">
Apply filters
</button>
</template>
</modal>

<CategoryFilterModal :onClose="sortThunderstoreModList" />
<LocalFileImportModal :visible="importingLocalMod" @close-modal="importingLocalMod = false" @error="showError($event)"/>

<DownloadModModal
Expand Down Expand Up @@ -233,7 +174,7 @@
<div class="input-group">
<div class="input-group input-group--flex">
<label for="thunderstore-category-filter">Additional filters</label>
<button id="thunderstore-category-filter" class="button" @click="showCategoryFilterModal = true;">Filter categories</button>
<button id="thunderstore-category-filter" class="button" @click="$store.commit('openCategoryFilterModal')">Filter categories</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -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({
Expand All @@ -367,6 +309,7 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue';
LocalModList: LocalModListProvider.provider,
NavigationMenu: NavigationMenuProvider.provider,
SettingsView,
CategoryFilterModal,
DownloadModModal,
GameRunningModal,
'hero': Hero,
Expand Down Expand Up @@ -412,11 +355,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue';
showUpdateAllModal: boolean = false;
showDependencyStrings: boolean = false;

showCategoryFilterModal: boolean = false;
filterCategories: string[] = [];
categoryFilterMode: string = CategoryFilterMode.OR;
allowNsfw: boolean = false;

importingLocalMod: boolean = false;

doorstopTarget: string = "";
Expand Down Expand Up @@ -462,12 +400,15 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue';
}

@Watch("thunderstoreModList")
@Watch("showCategoryFilterModal")
thunderstoreModListUpdate() {
this.sortThunderstoreModList();
}

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) {
Expand All @@ -476,18 +417,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);
}
})
}
Expand Down Expand Up @@ -938,31 +879,6 @@ import GameRunningModal from '../components/modals/GameRunningModal.vue';
});
}

get availableCategories(): string[] {
this.filterCategories.includes("");
const flatArray: Array<string> = 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);
target.selectedIndex = 0;
}

removeCategory(key: string) {
this.filterCategories = this.filterCategories.filter(value => value !== key);
}

get categoryFilterValues() {
return Object.values(CategoryFilterMode);
}

handleSettingsCallbacks(invokedSetting: any) {
switch(invokedSetting) {
case "BrowseDataFolder":
Expand Down Expand Up @@ -1060,6 +976,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);
Expand Down
6 changes: 3 additions & 3 deletions src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -85,7 +84,8 @@ export const store = {
}
},
modules: {
modals: ModalsModule
modals: ModalsModule,
modFilters: ModFilterModule,
},

// enable strict mode (adds overhead!)
Expand Down
Loading