diff options
Diffstat (limited to 'src-vue/src/views')
-rw-r--r-- | src-vue/src/views/ModsView.vue | 22 | ||||
-rw-r--r-- | src-vue/src/views/ThunderstoreModsView.vue | 190 |
2 files changed, 192 insertions, 20 deletions
diff --git a/src-vue/src/views/ModsView.vue b/src-vue/src/views/ModsView.vue index 48e9fbee..3c2b3cfe 100644 --- a/src-vue/src/views/ModsView.vue +++ b/src-vue/src/views/ModsView.vue @@ -3,7 +3,7 @@ <el-scrollbar> <h3>Installed Mods:</h3> <div> - <el-card shadow="hover" v-for="mod in installed_mods"> + <el-card shadow="hover" v-for="mod in $store.state.installed_mods"> <el-switch style="--el-switch-on-color: #13ce66; --el-switch-off-color: #8957e5" v-model="mod.enabled" :before-change="() => updateWhichModsEnabled(mod)" :loading="global_load_indicator" /> {{mod.name}} @@ -24,29 +24,11 @@ export default defineComponent({ name: "ModsView", data() { return { - installed_mods: [] as NorthstarMod[], global_load_indicator: false } }, async mounted() { - let game_install = { - game_path: this.$store.state.game_path, - install_type: this.$store.state.install_type - } as GameInstall; - // Call back-end for installed mods - await invoke("get_installed_mods_caller", { gameInstall: game_install }) - .then((message) => { - this.installed_mods = (message as NorthstarMod[]); - }) - .catch((error) => { - console.error(error); - ElNotification({ - title: 'Error', - message: error, - type: 'error', - position: 'bottom-right' - }); - }); + this.$store.commit('loadInstalledMods'); }, methods: { async updateWhichModsEnabled(mod: NorthstarMod) { diff --git a/src-vue/src/views/ThunderstoreModsView.vue b/src-vue/src/views/ThunderstoreModsView.vue new file mode 100644 index 00000000..cc9db0b5 --- /dev/null +++ b/src-vue/src/views/ThunderstoreModsView.vue @@ -0,0 +1,190 @@ +<template> + <div style="height: calc(100% - var(--fc-menu_height))"> + <div v-if="mods.length === 0" class="fc__changelog__container"> + <el-progress :show-text="false" :percentage="50" :indeterminate="true" /> + </div> + <el-scrollbar v-else class="container"> + <div class="card-container"> + <!-- Search filters --> + <div class="filter_container"> + <el-input v-model="input" placeholder="Search" clearable @input="onFilterTextChange" /> + <!-- Message displayed when user is typing in search bar --> + <div v-if="userIsTyping" class="modMessage search"> + Searching mods... + </div> + </div> + + <!-- Message displayed if no mod matched searched words --> + <div v-if="filteredMods.length === 0 && input.length !== 0 && !userIsTyping" class="modMessage"> + No matching mod has been found.<br/> + Try another search! + </div> + + <!-- Mod cards --> + <thunderstore-mod-card v-for="mod of modsList" v-bind:key="mod.name" :mod="mod" /> + </div> + </el-scrollbar> + </div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { ThunderstoreMod } from "../utils/thunderstore/ThunderstoreMod"; +import ThunderstoreModCard from "../components/ThunderstoreModCard.vue"; + +export default defineComponent({ + name: "ThunderstoreModsView", + components: {ThunderstoreModCard}, + async mounted() { + this.$store.commit('fetchThunderstoreMods'); + }, + computed: { + mods(): ThunderstoreMod[] { + return this.$store.state.thunderstoreMods; + }, + modsList(): ThunderstoreMod[] { + return this.input.length === 0 || this.userIsTyping ? this.mods : this.filteredMods; + } + }, + data() { + return { + input: '', + filteredMods: [] as ThunderstoreMod[], + modsBeingInstalled: [] as string[], + userIsTyping: false, + debouncedSearch: this.debounce((i: string) => this.filterMods(i)) + }; + }, + methods: { + /** + * This is a debounced version of the filterMods method, that calls + * filterMods when user has stopped typing in the search bar (i.e. + * waits 300ms). + * It allows not to trigger filtering method (which is costly) each + * time user inputs a character. + */ + onFilterTextChange (searchString: string) { + this.debouncedSearch(searchString); + }, + + /** + * This method is called each time search input is modified, and + * filters mods matching the input string. + * + * This converts research string and all researched fields to + * lower case, to match mods regardless of font case. + */ + filterMods(value: string) { + if (value === '') { + this.filteredMods = []; + return; + } + + const searchValue = value.toLowerCase(); + + this.filteredMods = this.mods.filter((mod: ThunderstoreMod) => { + return mod.name.toLowerCase().includes(searchValue) + || mod.owner.toLowerCase().includes(searchValue) + || mod.versions[0].description.toLowerCase().includes(searchValue); + }); + }, + + /** + * This debounces a method, i.e. it prevents input method from being called + * multiple times in a short period of time. + * Stolen from https://www.freecodecamp.org/news/javascript-debounce-example/ + */ + debounce (func: Function, timeout = 200) { + let timer: any; + return (...args: any) => { + this.userIsTyping = true; + clearTimeout(timer); + timer = setTimeout(() => { + this.userIsTyping = false; + func.apply(this, args); + }, timeout); + }; + } + } +}); +</script> + +<style scoped> +.fc__changelog__container { + padding: 20px 30px; + position: relative; + overflow-y: auto; + height: calc(100% - var(--fc-menu_height)); + color: white; +} + +.el-timeline-item__timestamp { + color: white !important; + user-select: none !important; +} + +.filter_container { + margin: 5px; +} + +.el-input { + max-width: 300px; +} + +.search { + display: inline-block; + margin: 0 0 0 10px !important; +} + +.modMessage { + color: white; + margin: 20px 5px; +} + +.card-container { + margin: 0 auto; +} + +/* Card container dynamic size */ +@media (max-width: 1000px) { + .card-container { + width: 752px; + } +} + +@media (max-width: 812px) { + .card-container { + width: 574px; + } +} + +@media (max-width: 624px) { + .card-container { + width: 376px; + } +} + +@media (min-width: 1000px) { + .card-container { + width: 940px; + } +} + +@media (min-width: 1188px) { + .card-container { + width: 1128px; + } +} + +@media (min-width: 1376px) { + .card-container { + width: 1316px; + } +} + +@media (min-width: 1565px) { + .card-container { + width: 1505px; + } +} +</style> |