diff options
author | Rémy Raes <contact@remyraes.com> | 2023-01-14 01:18:48 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-14 01:18:48 +0100 |
commit | 20c3880721cd0f0bd0a2b48a1463b49c514a849b (patch) | |
tree | 8bdce436ad295ab550dda3b65f2515c83bec7965 | |
parent | bcdab67ee1e0c34dbbf0e6c5d294b8a163ebeeff (diff) | |
download | FlightCore-20c3880721cd0f0bd0a2b48a1463b49c514a849b.tar.gz FlightCore-20c3880721cd0f0bd0a2b48a1463b49c514a849b.zip |
refactor: Mods view (#134)
* feat: add UI skeleton for new mods interface
* fix: only apply app menu style to app menu items
* feat: add menu skeleton
* feat: add thunderstore mods view into mods view
* refactor: remove thunderstore mods view from router + menu bar
* refactor: move search input into mods view
* fix: adjust ts mods container padding
* refactor: center pagination components
* refactor: rework media queries to adapt cards container width
* fix: set mods view navigation min-width
* refactor: compute mods container width with CSS variables
* refactor: remove horizontal padding around ts mods container
This will allow them to take much screen space.
* feat: Thunderstore mods container is larger
* feat: add layouts for 5 to 8 Thunderstore mod columns
* feat: add some space to separate navigation menu subparts
* fix: move search pagination reset in ThunderstoreModsView component
* feat: retrieve Thunderstore mod categories when fetching mods
* feat: add mod categories selector component
* feat: mod categories selector now filters Thunderstore mods
* fix: reset pagination index if needed when selecting mod categories
* fix: first mod does not appear anymore while selecting any mod category
First mod's categories array was used as an accumulator (in a reduce call)
to regroup all mod categories; we now use an empty array as initial value
for the accumulator.
* feat: add a selector component to sort mods
* feat: set sort selector default value on view mount
* fix: set default sorting select value as selected in dropdown list
* feat: add date_updated field to ThunderstoreMod type
* feat: pass sorting value to ThunderstoreModsView as a prop
* feat: mods can be sorted by name and release date
* feat: mods can be sorted by most downloaded and top rated
* fix: don't display sorting select on local mods view
* refactor: remove local mods title
* refactor: export local mods view code in dedicated component
* refactor: export search value in a search-dedicated store
* fix: display "no matching mods" message when no mods match
This message was previously displayed only if no mods matched AND search
input was not empty; now that we have categories selector, we can have no
matching mods with an empty input.
* refactor: remove unused input prop
* fix: mods can be sorted by rating
* refactor: remove unused searchValue computed property
* style: adjust SortOptions import
* refactor: move selected mod categories into search store module
* refactor: move mod sorting into search store module
* refactor: export mods menu component to dedicated file
* fix: remove compare method initialization
* feat: local mods can be filtered with search input
* build: add ES2018 lib to typescript compiler options
* refactor: remove unused variable
* fix: retrieve SortOptions values from string input
* refactor: move Thunderstore mods view into dedicated folder
* style: add missing trailing newline to SortOptions.d.ts
-rw-r--r-- | src-vue/src/App.vue | 3 | ||||
-rw-r--r-- | src-vue/src/components/ModsMenu.vue | 118 | ||||
-rw-r--r-- | src-vue/src/main.ts | 2 | ||||
-rw-r--r-- | src-vue/src/plugins/modules/search.ts | 19 | ||||
-rw-r--r-- | src-vue/src/plugins/store.ts | 16 | ||||
-rw-r--r-- | src-vue/src/utils/SortOptions.d.ts | 8 | ||||
-rw-r--r-- | src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts | 2 | ||||
-rw-r--r-- | src-vue/src/views/ModsView.vue | 128 | ||||
-rw-r--r-- | src-vue/src/views/ThunderstoreModsView.vue | 240 | ||||
-rw-r--r-- | src-vue/src/views/mods/LocalModsView.vue | 122 | ||||
-rw-r--r-- | src-vue/src/views/mods/ThunderstoreModsView.vue | 272 | ||||
-rw-r--r-- | src-vue/tsconfig.json | 2 |
12 files changed, 592 insertions, 340 deletions
diff --git a/src-vue/src/App.vue b/src-vue/src/App.vue index fc6992b7..0ecea16d 100644 --- a/src-vue/src/App.vue +++ b/src-vue/src/App.vue @@ -59,7 +59,6 @@ export default { <el-menu-item index="/">Play</el-menu-item> <el-menu-item index="/changelog">Changelog</el-menu-item> <el-menu-item index="/mods">Mods</el-menu-item> - <el-menu-item index="/thunderstoreMods">Thunderstore</el-menu-item> <el-menu-item index="/settings">Settings</el-menu-item> <el-menu-item index="/dev" v-if="$store.state.developer_mode">Dev</el-menu-item> </el-menu> @@ -109,7 +108,7 @@ export default { border-color: white; } -.el-menu > .el-menu-item { +#fc__menu_items > .el-menu-item { text-transform: uppercase; border: none !important; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; diff --git a/src-vue/src/components/ModsMenu.vue b/src-vue/src/components/ModsMenu.vue new file mode 100644 index 00000000..9b62fcfa --- /dev/null +++ b/src-vue/src/components/ModsMenu.vue @@ -0,0 +1,118 @@ +<template> + <nav class="fc_mods__menu"> + <el-menu + default-active="1" + text-color="#fff" + > + <h5>Mods</h5> + <el-menu-item index="1" @click="$emit('showLocalMods', true)"> + <el-icon><Folder /></el-icon> + <span>Local</span> + </el-menu-item> + <el-menu-item index="2" @click="$emit('showLocalMods', false)"> + <el-icon><Connection /></el-icon> + <span>Online</span> + </el-menu-item> + + <!-- Search inputs --> + <h5>Filter</h5> + <el-input v-model="$store.state.search.searchValue" placeholder="Search" clearable /> + <el-select + v-if="!showingLocalMods" + v-model="$store.state.search.sortValue" + placeholder="Sort mods" + > + <el-option + v-for="item of sortValues" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + <el-select + v-if="!showingLocalMods" + v-model="$store.state.search.selectedCategories" + multiple + placeholder="Select categories" + > + <el-option + v-for="item in $store.state.thunderstoreModsCategories" + :key="item" + :label="item" + :value="item" + /> + </el-select> + + </el-menu> + </nav> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue' +import { SortOptions } from '../utils/SortOptions.d'; + +export default defineComponent({ + name: 'ModsMenu', + props: { + showingLocalMods: { + required: true, + type: Boolean + } + }, + mounted() { + this.$store.state.search.sortValue = this.sortValues[3].value; + }, + computed: { + sortValues(): {label: string, value: string}[] { + return Object.keys(SortOptions).map((key: string) => ({ + value: key, + label: Object.values(SortOptions)[Object.keys(SortOptions).indexOf(key)] + })); + } + } +}) +</script> + +<style scoped> +.fc_mods__menu { + display: flex; + max-width: 222px; + min-width: 222px; + padding: 10px; +} + +.fc_mods__menu h5 { + margin: 8px 0 16px 5px; +} + +.fc_mods__menu h5:not(:first-child){ + margin-top: 32px; +} + +.fc_mods__menu > .el-menu { + background-color: transparent; + border: none; + width: 100%; +} + +.fc_mods__menu > .el-menu > .el-menu-item { + height: 32px; + margin-bottom: 5px; + border-radius: 5px; + color: #e2e6e7; +} + +.fc_mods__menu > .el-menu > .el-menu-item:hover { + background-color: #4e4e4e3b; +} + +.fc_mods__menu > .el-menu > .el-menu-item.is-active { + color: white; + background-color: #4e4e4e7a; +} + +.el-select { + width: 100%; + margin-top: 5px; +} +</style> diff --git a/src-vue/src/main.ts b/src-vue/src/main.ts index 25f1b064..95bea7af 100644 --- a/src-vue/src/main.ts +++ b/src-vue/src/main.ts @@ -6,7 +6,6 @@ import { store } from './plugins/store'; import PlayView from "./views/PlayView.vue"; import ChangelogView from "./views/ChangelogView.vue"; import ModsView from "./views/ModsView.vue"; -import ThunderstoreModsView from "./views/ThunderstoreModsView.vue"; import SettingsView from "./views/SettingsView.vue"; import DeveloperView from "./views/DeveloperView.vue"; import {createRouter, createWebHashHistory} from "vue-router"; @@ -35,7 +34,6 @@ const routes = [ { path: '/', name: 'Main', component: async () => PlayView}, { path: '/changelog', name: 'Changelog', component: async () => ChangelogView}, { path: '/mods', name: 'Mods', component: async () => ModsView}, - { path: '/thunderstoreMods', name: 'Thunderstore mods', component: async () => ThunderstoreModsView}, { path: '/settings', name: 'Settings', component: async () => SettingsView}, { path: '/dev', name: 'Dev', component: async () => DeveloperView} ]; diff --git a/src-vue/src/plugins/modules/search.ts b/src-vue/src/plugins/modules/search.ts new file mode 100644 index 00000000..16260387 --- /dev/null +++ b/src-vue/src/plugins/modules/search.ts @@ -0,0 +1,19 @@ +interface SearchStoreState { + searchValue: string +} + +export const searchModule = { + state: () => ({ + // This is the treated value of search input + searchValue: '', + // Selected mod categories + selectedCategories: [], + sortValue: {label: '', value: ''} + }), + getters: { + searchWords(state: SearchStoreState): string { + return state.searchValue.toLowerCase(); + } + } + } +
\ No newline at end of file diff --git a/src-vue/src/plugins/store.ts b/src-vue/src/plugins/store.ts index edeb13df..2eae843a 100644 --- a/src-vue/src/plugins/store.ts +++ b/src-vue/src/plugins/store.ts @@ -15,6 +15,7 @@ import { router } from "../main"; import ReleaseInfo from "../utils/ReleaseInfo"; import { ThunderstoreMod } from '../utils/thunderstore/ThunderstoreMod'; import { NorthstarMod } from "../utils/NorthstarMod"; +import { searchModule } from './modules/search'; const persistentStore = new Store('flight-core-settings.json'); @@ -33,6 +34,7 @@ export interface FlightCoreStore { releaseNotes: ReleaseInfo[], thunderstoreMods: ThunderstoreMod[], + thunderstoreModsCategories: string[], installed_mods: NorthstarMod[], northstar_is_running: boolean, @@ -48,6 +50,9 @@ export interface FlightCoreStore { let notification_handle: NotificationHandle; export const store = createStore<FlightCoreStore>({ + modules: { + search: searchModule + }, state(): FlightCoreStore { return { developer_mode: false, @@ -63,6 +68,7 @@ export const store = createStore<FlightCoreStore>({ releaseNotes: [], thunderstoreMods: [], + thunderstoreModsCategories: [], installed_mods: [], northstar_is_running: false, @@ -244,6 +250,16 @@ export const store = createStore<FlightCoreStore>({ state.thunderstoreMods = mods.filter((mod: ThunderstoreMod) => { return !removedMods.includes(mod.name) && !mod.is_deprecated; }); + + // Retrieve categories from mods + state.thunderstoreModsCategories = mods + .map((mod: ThunderstoreMod) => mod.categories) + .filter((modCategories: string[]) => modCategories.length !== 0) + .reduce((accumulator: string[], modCategories: string[]) => { + accumulator.push( ...modCategories.filter((cat: string) => !accumulator.includes(cat)) ); + return accumulator; + }, []) + .sort(); }, async loadInstalledMods(state: FlightCoreStore) { let game_install = { diff --git a/src-vue/src/utils/SortOptions.d.ts b/src-vue/src/utils/SortOptions.d.ts new file mode 100644 index 00000000..6bdd0a4a --- /dev/null +++ b/src-vue/src/utils/SortOptions.d.ts @@ -0,0 +1,8 @@ +export enum SortOptions { + NAME_ASC = 'By name (A to Z)', + NAME_DESC = 'By name (Z to A)', + DATE_ASC = 'By date (from oldest)', + DATE_DESC = 'By date (from newest)', + MOST_DOWNLOADED = "Most downloaded", + TOP_RATED = "Top rated" +} diff --git a/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts b/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts index 6fddd06f..6387c47e 100644 --- a/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts +++ b/src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts @@ -3,8 +3,10 @@ import { ThunderstoreModVersion } from "./ThunderstoreModVersion"; export interface ThunderstoreMod { name: string; owner: string; + date_updated: string; rating_score: number; package_url: string; is_deprecated: boolean; versions: ThunderstoreModVersion[]; + categories: string[]; } diff --git a/src-vue/src/views/ModsView.vue b/src-vue/src/views/ModsView.vue index 2f352ded..7c309832 100644 --- a/src-vue/src/views/ModsView.vue +++ b/src-vue/src/views/ModsView.vue @@ -1,112 +1,50 @@ <template> - <div class="fc-container"> - <el-scrollbar> - <h3>Installed Mods:</h3> - <div> - <p v-if="installedMods.length === 0">No mods were found.</p> - <el-card v-else shadow="hover" v-for="mod in installedMods" v-bind:key="mod.name"> - <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" /> - <el-popconfirm - title="Are you sure to delete this mod?" - @confirm="deleteMod(mod)" - > - <template #reference> - <el-button type="danger">Delete</el-button> - </template> - </el-popconfirm> - {{ mod.name }} - </el-card> - </div> - </el-scrollbar> + <div class="fc-container" style="display: flex"> + <!-- Local mods/Thunderstore mods menu --> + <mods-menu + :showingLocalMods="show_local_mods" + @showLocalMods="(v) => show_local_mods = v" + /> + + <!-- Mods content --> + <div class="fc_mods__container"> + <local-mods-view + v-if="show_local_mods" + /> + + <thunderstore-mods-view + v-else + clearable + /> + </div> </div> </template> <script lang="ts"> import { defineComponent } from "vue"; -import { ElNotification } from "element-plus"; -import { invoke } from '@tauri-apps/api/tauri'; -import { GameInstall } from "../utils/GameInstall"; -import { NorthstarMod } from "../utils/NorthstarMod" +import ThunderstoreModsView from "./mods/ThunderstoreModsView.vue"; +import LocalModsView from "./mods/LocalModsView.vue"; +import ModsMenu from "../components/ModsMenu.vue"; export default defineComponent({ name: "ModsView", + components: { + ModsMenu, + LocalModsView, + ThunderstoreModsView + }, data() { return { - global_load_indicator: false - } - }, - async mounted() { - this.$store.commit('loadInstalledMods'); - }, - computed: { - installedMods(): NorthstarMod[] { - return this.$store.state.installed_mods; - } - }, - methods: { - async updateWhichModsEnabled(mod: NorthstarMod) { - this.global_load_indicator = true; - - // Setup up struct - let game_install = { - game_path: this.$store.state.game_path, - install_type: this.$store.state.install_type - } as GameInstall; - - // enable/disable specific mod - try { - await invoke("set_mod_enabled_status", { - gameInstall: game_install, - modName: mod.name, - // Need to set it to the opposite of current state, - // as current state is only updated after command is run - isEnabled: !mod.enabled, - }) - } - catch (error) { - ElNotification({ - title: 'Error', - message: `${error}`, - type: 'error', - position: 'bottom-right' - }); - this.global_load_indicator = false; - return false; - } - - this.global_load_indicator = false; - return true; - }, - async deleteMod(mod: NorthstarMod) { - let game_install = { - game_path: this.$store.state.game_path, - install_type: this.$store.state.install_type - } as GameInstall; - await invoke("delete_northstar_mod", { gameInstall: game_install, nsmodName: mod.name }) - .then((message) => { - // Just a visual indicator that it worked - ElNotification({ - title: `Success deleting ${mod.name}`, - type: 'success', - position: 'bottom-right' - }); - }) - .catch((error) => { - ElNotification({ - title: 'Error', - message: error, - type: 'error', - position: 'bottom-right' - }); - }) - .finally(() => { - this.$store.commit('loadInstalledMods'); - }); + show_local_mods: true, } } }); </script> -<style> +<style scoped> +.fc_mods__container { + display: flex; + width: 100%; + flex-direction: column; +} </style> diff --git a/src-vue/src/views/ThunderstoreModsView.vue b/src-vue/src/views/ThunderstoreModsView.vue deleted file mode 100644 index 5733d30b..00000000 --- a/src-vue/src/views/ThunderstoreModsView.vue +++ /dev/null @@ -1,240 +0,0 @@ -<template> - <div class="fc-container"> - <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" ref="scrollbar"> - <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> - - <!-- Pagination --> - <el-pagination - v-if="shouldDisplayPagination" - :currentPage="currentPageIndex + 1" - layout="prev, pager, next" - :page-size="modsPerPage" - :total="modsList.length" - @current-change="(e: number) => currentPageIndex = e - 1" - /> - </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 currentPageMods" v-bind:key="mod.name" :mod="mod" /> - </div> - - <!-- Bottom pagination --> - <div class="card-container"> - <div class="filter_container"> - <el-pagination - class="fc_bottom__pagination" - v-if="shouldDisplayPagination" - :currentPage="currentPageIndex + 1" - layout="prev, pager, next" - :page-size="modsPerPage" - :total="modsList.length" - @current-change="onBottomPaginationChange" - /> - </div> - </div> - </el-scrollbar> - </div> -</template> - -<script lang="ts"> -import { defineComponent, ref } from 'vue'; -import { ThunderstoreMod } from "../utils/thunderstore/ThunderstoreMod"; -import ThunderstoreModCard from "../components/ThunderstoreModCard.vue"; -import { ElScrollbar, ScrollbarInstance } from "element-plus"; - -export default defineComponent({ - name: "ThunderstoreModsView", - components: {ThunderstoreModCard}, - async mounted() { - this.$store.commit('fetchThunderstoreMods'); - }, - computed: { - mods(): ThunderstoreMod[] { - return this.$store.state.thunderstoreMods; - }, - filteredMods(): ThunderstoreMod[] { - if (this.searchValue.length === 0) { - return this.mods; - } - - return this.mods.filter((mod: ThunderstoreMod) => { - return mod.name.toLowerCase().includes(this.searchValue) - || mod.owner.toLowerCase().includes(this.searchValue) - || mod.versions[0].description.toLowerCase().includes(this.searchValue); - }); - }, - modsList(): ThunderstoreMod[] { - return this.input.length !== 0 || this.userIsTyping ? this.filteredMods : this.mods; - }, - modsPerPage(): number { - return parseInt(this.$store.state.mods_per_page); - }, - currentPageMods(): ThunderstoreMod[] { - // User might want to display all mods on one page. - const perPageValue = this.modsPerPage != 0 ? this.modsPerPage : this.modsList.length; - - const startIndex = this.currentPageIndex * perPageValue; - const endIndexCandidate = startIndex + perPageValue; - const endIndex = endIndexCandidate > this.modsList.length ? this.modsList.length : endIndexCandidate; - return this.modsList.slice(startIndex, endIndex); - }, - shouldDisplayPagination(): boolean { - return this.modsPerPage != 0 && this.modsList.length > this.modsPerPage; - } - }, - data() { - return { - // This is the model for the search input. - input: '', - // This is the treated value of search input - searchValue: '', - - modsBeingInstalled: [] as string[], - userIsTyping: false, - - currentPageIndex: 0 - }; - }, - methods: { - /** - * This method is called each time search input is modified, and - * triggered filtered mods recomputing by updating the `searchValue` - * variable. - * - * This converts research string and all researched fields to - * lower case, to match mods regardless of font case. - */ - onFilterTextChange(value: string) { - this.currentPageIndex = 0; - this.searchValue = value.toLowerCase(); - }, - - /** - * This updates current pagination and scrolls view to the top. - */ - onBottomPaginationChange(index: number) { - this.currentPageIndex = index - 1; - (this.$refs.scrollbar as ScrollbarInstance).scrollTo({ top: 0, behavior: 'smooth' }); - } - } -}); -</script> - -<style scoped> -.fc__changelog__container { - padding: 20px 30px; -} - -.el-timeline-item__timestamp { - color: white !important; - user-select: none !important; -} - -.search { - display: inline-block; - margin: 0 0 0 10px !important; -} - -.modMessage { - color: white; - margin: 20px 5px; -} - -.card-container { - margin: 0 auto; -} - -.fc_bottom__pagination { - padding-bottom: 20px !important; - padding-right: 10px; -} - -/* Card container dynamic size */ -@media (max-width: 1000px) { - .card-container { - width: 752px; - } -} - -@media (max-width: 812px) { - .card-container { - width: 574px; - } - - .el-pagination { - float: none; - margin-top: 5px; - } -} - -@media (max-width: 624px) { - .card-container { - width: 376px; - } -} - -.filter_container { - margin-bottom: 10px; -} - -.el-input { - max-width: 200px; -} - -@media (min-width: 812px) { - .filter_container { - margin: 5px auto; - padding: 0 5px; - max-width: 1000px; - } - - .el-pagination { - float: right; - margin: 0; - } -} - -@media (min-width: 1000px) { - .card-container { - width: 940px; - } - - .el-input { - max-width: 300px; - } -} - -@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> diff --git a/src-vue/src/views/mods/LocalModsView.vue b/src-vue/src/views/mods/LocalModsView.vue new file mode 100644 index 00000000..470ab4f7 --- /dev/null +++ b/src-vue/src/views/mods/LocalModsView.vue @@ -0,0 +1,122 @@ +<template> + <el-scrollbar> + <div> + <p v-if="mods.length === 0">No mods were found.</p> + <el-card v-else shadow="hover" v-for="mod in mods" v-bind:key="mod.name"> + <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" /> + <el-popconfirm + title="Are you sure to delete this mod?" + @confirm="deleteMod(mod)" + > + <template #reference> + <el-button type="danger">Delete</el-button> + </template> + </el-popconfirm> + {{ mod.name }} + </el-card> + </div> + </el-scrollbar> +</template> + +<script lang="ts"> +import { invoke } from '@tauri-apps/api'; +import { ElNotification } from 'element-plus'; +import { defineComponent } from 'vue'; +import { GameInstall } from '../../utils/GameInstall'; +import { NorthstarMod } from '../../utils/NorthstarMod'; + +export default defineComponent({ + name: 'LocalModsView', + computed: { + installedMods(): NorthstarMod[] { + return this.$store.state.installed_mods; + }, + searchValue(): string { + return this.$store.getters.searchWords; + }, + mods(): NorthstarMod[] { + if (this.searchValue.length === 0) { + return this.installedMods; + } + + return this.installedMods.filter((mod: NorthstarMod) => { + return mod.name.toLowerCase().includes(this.searchValue); + }); + } + }, + data() { + return { + global_load_indicator: false, + }; + }, + methods: { + async updateWhichModsEnabled(mod: NorthstarMod) { + this.global_load_indicator = true; + + // Setup up struct + let game_install = { + game_path: this.$store.state.game_path, + install_type: this.$store.state.install_type + } as GameInstall; + + // enable/disable specific mod + try { + await invoke("set_mod_enabled_status", { + gameInstall: game_install, + modName: mod.name, + // Need to set it to the opposite of current state, + // as current state is only updated after command is run + isEnabled: !mod.enabled, + }) + } + catch (error) { + ElNotification({ + title: 'Error', + message: `${error}`, + type: 'error', + position: 'bottom-right' + }); + this.global_load_indicator = false; + return false; + } + + this.global_load_indicator = false; + return true; + }, + async deleteMod(mod: NorthstarMod) { + let game_install = { + game_path: this.$store.state.game_path, + install_type: this.$store.state.install_type + } as GameInstall; + await invoke("delete_northstar_mod", { gameInstall: game_install, nsmodName: mod.name }) + .then((message) => { + // Just a visual indicator that it worked + ElNotification({ + title: `Success deleting ${mod.name}`, + type: 'success', + position: 'bottom-right' + }); + }) + .catch((error) => { + ElNotification({ + title: 'Error', + message: error, + type: 'error', + position: 'bottom-right' + }); + }) + .finally(() => { + this.$store.commit('loadInstalledMods'); + }); + }, + }, + mounted() { + this.$store.commit('loadInstalledMods'); + } +}) +</script> + +<style scoped> + +</style> diff --git a/src-vue/src/views/mods/ThunderstoreModsView.vue b/src-vue/src/views/mods/ThunderstoreModsView.vue new file mode 100644 index 00000000..aaf15220 --- /dev/null +++ b/src-vue/src/views/mods/ThunderstoreModsView.vue @@ -0,0 +1,272 @@ +<template>
+ <div class="fc-container" style="padding: 0">
+ <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" ref="scrollbar">
+ <div class="card-container">
+ <div class="pagination_container">
+ <el-pagination
+ v-if="shouldDisplayPagination"
+ :currentPage="currentPageIndex + 1"
+ layout="prev, pager, next"
+ :page-size="modsPerPage"
+ :total="modsList.length"
+ @current-change="(e: number) => currentPageIndex = e - 1"
+ />
+ </div>
+
+ <!-- Message displayed if no mod matched searched words -->
+ <div v-if="filteredMods.length === 0" class="modMessage">
+ No matching mod has been found.<br/>
+ Try another search!
+ </div>
+
+ <!-- Mod cards -->
+ <thunderstore-mod-card v-for="mod of currentPageMods" v-bind:key="mod.name" :mod="mod" />
+ </div>
+
+ <!-- Bottom pagination -->
+ <div class="card-container">
+ <div class="pagination_container">
+ <el-pagination
+ class="fc_bottom__pagination"
+ v-if="shouldDisplayPagination"
+ :currentPage="currentPageIndex + 1"
+ layout="prev, pager, next"
+ :page-size="modsPerPage"
+ :total="modsList.length"
+ @current-change="onBottomPaginationChange"
+ />
+ </div>
+ </div>
+ </el-scrollbar>
+ </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+import { ThunderstoreMod } from "../../utils/thunderstore/ThunderstoreMod";
+import ThunderstoreModCard from "../../components/ThunderstoreModCard.vue";
+import { ElScrollbar, ScrollbarInstance } from "element-plus";
+import { SortOptions } from "../../utils/SortOptions.d";
+import { ThunderstoreModVersion } from '../../utils/thunderstore/ThunderstoreModVersion';
+
+export default defineComponent({
+ name: "ThunderstoreModsView",
+ components: {ThunderstoreModCard},
+ async mounted() {
+ this.$store.commit('fetchThunderstoreMods');
+ },
+ computed: {
+ searchValue(): string {
+ return this.$store.getters.searchWords;
+ },
+ selectedCategories(): Object[] {
+ return this.$store.state.search.selectedCategories;
+ },
+ modSorting(): SortOptions {
+ return Object.values(SortOptions)[Object.keys(SortOptions).indexOf(this.$store.state.search.sortValue)];
+ },
+ mods(): ThunderstoreMod[] {
+ return this.$store.state.thunderstoreMods;
+ },
+ filteredMods(): ThunderstoreMod[] {
+ if (this.searchValue.length === 0 && this.selectedCategories.length === 0) {
+ return this.mods;
+ }
+
+ return this.mods.filter((mod: ThunderstoreMod) => {
+ // Filter with search words (only if search field isn't empty)
+ const inputMatches: boolean = this.searchValue.length === 0
+ || (mod.name.toLowerCase().includes(this.searchValue)
+ || mod.owner.toLowerCase().includes(this.searchValue)
+ || mod.versions[0].description.toLowerCase().includes(this.searchValue));
+
+ // Filter with categories (only if some categories are selected)
+ const categoriesMatch: boolean = this.selectedCategories.length === 0
+ || mod.categories
+ .filter((category: string) => this.selectedCategories.includes(category))
+ .length === this.selectedCategories.length;
+
+ return inputMatches && categoriesMatch;
+ });
+ },
+ modsList(): ThunderstoreMod[] {
+ // Use filtered mods if user is searching, vanilla list otherwise.
+ const mods: ThunderstoreMod[] = this.searchValue.length !== 0 || this.selectedCategories.length !== 0
+ ? this.filteredMods
+ : this.mods;
+
+ // Sort mods regarding user selected algorithm.
+ let compare: (a: ThunderstoreMod, b: ThunderstoreMod) => number;
+ switch(this.modSorting) {
+ case SortOptions.NAME_ASC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => a.name.localeCompare(b.name);
+ break;
+ case SortOptions.NAME_DESC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => -1 * a.name.localeCompare(b.name);
+ break;
+ case SortOptions.DATE_ASC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => a.date_updated.localeCompare(b.date_updated);
+ break;
+ case SortOptions.DATE_DESC:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => -1 * a.date_updated.localeCompare(b.date_updated);
+ break;
+ case SortOptions.MOST_DOWNLOADED:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => {
+ const aTotal = a.versions.reduce((prev, next) => {
+ return {downloads: prev.downloads + next.downloads} as ThunderstoreModVersion;
+ }).downloads;
+ const bTotal = b.versions.reduce((prev, next) => {
+ return {downloads: prev.downloads + next.downloads} as ThunderstoreModVersion;
+ }).downloads;
+ return -1 * (aTotal - bTotal);
+ };
+ break;
+ case SortOptions.TOP_RATED:
+ compare = (a: ThunderstoreMod, b: ThunderstoreMod) => -1 * (a.rating_score - b.rating_score);
+ break;
+ default:
+ throw new Error('Unknown mod sorting.');
+ }
+
+ return mods.sort(compare);
+ },
+ modsPerPage(): number {
+ return parseInt(this.$store.state.mods_per_page);
+ },
+ currentPageMods(): ThunderstoreMod[] {
+ // User might want to display all mods on one page.
+ const perPageValue = this.modsPerPage != 0 ? this.modsPerPage : this.modsList.length;
+
+ const startIndex = this.currentPageIndex * perPageValue;
+ const endIndexCandidate = startIndex + perPageValue;
+ const endIndex = endIndexCandidate > this.modsList.length ? this.modsList.length : endIndexCandidate;
+ return this.modsList.slice(startIndex, endIndex);
+ },
+ shouldDisplayPagination(): boolean {
+ return this.modsPerPage != 0 && this.modsList.length > this.modsPerPage;
+ }
+ },
+ data() {
+ return {
+ modsBeingInstalled: [] as string[],
+ currentPageIndex: 0
+ };
+ },
+ methods: {
+ /**
+ * This updates current pagination and scrolls view to the top.
+ */
+ onBottomPaginationChange(index: number) {
+ this.currentPageIndex = index - 1;
+ (this.$refs.scrollbar as ScrollbarInstance).scrollTo({ top: 0, behavior: 'smooth' });
+ }
+ },
+ watch: {
+ searchValue(_: string, __: string) {
+ if (this.currentPageIndex !== 0) {
+ this.currentPageIndex = 0;
+ }
+ },
+ selectedCategories(_: string[], __: string[]) {
+ if (this.currentPageIndex !== 0) {
+ this.currentPageIndex = 0;
+ }
+ }
+ }
+});
+</script>
+
+<style scoped>
+.fc__changelog__container {
+ padding: 20px 30px;
+}
+
+.fc-container:deep(.el-scrollbar__view) {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.el-timeline-item__timestamp {
+ color: white !important;
+ user-select: none !important;
+}
+
+.search {
+ display: inline-block;
+ margin: 0 0 0 10px !important;
+}
+
+.modMessage {
+ color: white;
+ margin: 20px 5px;
+}
+
+.card-container {
+ margin: 0 auto;
+}
+
+.pagination_container {
+ margin: 5px auto;
+ padding: 0 5px;
+ max-width: 1000px;
+ justify-content: center;
+ display: flex;
+}
+
+.el-pagination {
+ margin: 0;
+}
+
+.fc_bottom__pagination {
+ padding-bottom: 20px !important;
+ padding-right: 10px;
+}
+
+/* Card container dynamic size */
+
+.card-container {
+ --thunderstore-mod-card-width: 178px;
+ --thunderstore-mod-card-margin: 5px;
+ --thunderstore-mod-card-columns-count: 1;
+
+ width: calc(var(--thunderstore-mod-card-width) * var(--thunderstore-mod-card-columns-count) + var(--thunderstore-mod-card-margin) * 2 * var(--thunderstore-mod-card-columns-count));
+}
+@media (min-width: 628px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 2;
+ }
+}
+@media (min-width: 836px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 3;
+ }
+}
+@media (min-width: 1006px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 4;
+ }
+}
+@media (min-width: 1196px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 5;
+ }
+}
+@media (min-width: 1386px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 6;
+ }
+}
+@media (min-width: 1576px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 7;
+ }
+}
+@media (min-width: 1766px) {
+ .card-container {
+ --thunderstore-mod-card-columns-count: 8;
+ }
+}
+</style>
diff --git a/src-vue/tsconfig.json b/src-vue/tsconfig.json index d4aefa2c..7f0f0012 100644 --- a/src-vue/tsconfig.json +++ b/src-vue/tsconfig.json @@ -10,7 +10,7 @@ "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, - "lib": ["ESNext", "DOM"], + "lib": ["ESNext", "DOM", "ES2018"], "skipLibCheck": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], |