diff options
author | Rémy Raes <contact@remyraes.com> | 2023-01-04 19:21:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-04 19:21:17 +0100 |
commit | 131b101045bbc4419f98afe58557a8d532af9bd6 (patch) | |
tree | 2bbb24fe214aa063ba678289f128a2b2fef93293 | |
parent | 6bfc6996e12ba201f52de586c67f3db4a97bc722 (diff) | |
download | FlightCore-131b101045bbc4419f98afe58557a8d532af9bd6.tar.gz FlightCore-131b101045bbc4419f98afe58557a8d532af9bd6.zip |
feat: Thunderstore mods pagination (#122)
* feat: add basic pagination
* refactor: put paginator inside filters container
* fix: limit filter container width
* refactor: filters container is now responsive
* fix: add missing type to pagination listener parameter
* fix: don't display entire mods list while filtering mods
* refactor: filteredMods is now a computed value
* refactor: store mods per page value on UI store
* feat: user can change modsPerPage count in settings
* fix: limit mods count (min=5, max=100)
* feat: add control on pages value
When leaving settings vue, we check if the pages value is a number and is
included in the interval [5-100].
If that's not the case, we reset it to 20.
* feat: retrieve and save pages value in persistent store
* fix: don't load an empty value from persistent store on boot
* fix: cast modsPerPage to string to check if it's empty
* refactor: remove search debounce
* Update src-vue/src/plugins/store.ts
* style: add trailing comma
* fix: mention impact on TS mods only
* refactor: remove limitations on modsPerPage
* style: explicitly cast mods_per_page to number
* feat: disable pagination with modsPerPage === 0
* feat: add pagination under thunderstore mod cards
* fix: adjust bottom pagination padding
* feat: clicking bottom pagination scrolls to page top
* fix: use same containers for both paginations
* feat: do not display pagination if mods fit on one page
* style: trailing spaces
* style: trailing spaces
* feat: add a button to reset modsPerPage
* feat: add explanation text about disabling pagination
-rw-r--r-- | src-vue/src/plugins/store.ts | 15 | ||||
-rw-r--r-- | src-vue/src/views/SettingsView.vue | 48 | ||||
-rw-r--r-- | src-vue/src/views/ThunderstoreModsView.vue | 158 |
3 files changed, 167 insertions, 54 deletions
diff --git a/src-vue/src/plugins/store.ts b/src-vue/src/plugins/store.ts index 3db85e64..18191555 100644 --- a/src-vue/src/plugins/store.ts +++ b/src-vue/src/plugins/store.ts @@ -36,7 +36,10 @@ export interface FlightCoreStore { installed_mods: NorthstarMod[], northstar_is_running: boolean, - origin_is_running: boolean + origin_is_running: boolean, + + // user custom settings + mods_per_page: number, } let notification_handle: NotificationHandle; @@ -60,7 +63,9 @@ export const store = createStore<FlightCoreStore>({ installed_mods: [], northstar_is_running: false, - origin_is_running: false + origin_is_running: false, + + mods_per_page: 20, } }, mutations: { @@ -320,6 +325,12 @@ async function _initializeApp(state: any) { state.enableReleasesSwitch = valueFromStore.value; } + // Grab "Thunderstore mods per page" setting from store if possible + const perPageFromStore: {value: number} | null = await persistentStore.get('thunderstore-mods-per-page'); + if (perPageFromStore && perPageFromStore.value) { + state.mods_per_page = perPageFromStore.value; + } + // Get FlightCore version number state.flightcore_version = await invoke("get_flightcore_version_number"); diff --git a/src-vue/src/views/SettingsView.vue b/src-vue/src/views/SettingsView.vue index ff87c394..1c03fe4f 100644 --- a/src-vue/src/views/SettingsView.vue +++ b/src-vue/src/views/SettingsView.vue @@ -14,6 +14,24 @@ <el-button icon="Folder" @click="updateGamePath"/> </template> </el-input> + + <!-- Thunderstore mods per page configuration --> + <div class="fc_parameter__panel"> + <h3>Number of Thunderstore mods per page</h3> + <h6> + This has an impact on display performances when browsing Thunderstore mods.<br> + Set this value to 0 to disable pagination. + </h6> + <el-input + v-model="modsPerPage" + type="number" + > + <template #append> + <el-button @click="modsPerPage = 20">Reset to default</el-button> + </template> + </el-input> + </div> + <h3>About:</h3> <div class="fc_northstar__version" @click="activateDeveloperMode"> FlightCore Version: {{ flightcoreVersion === '' ? 'Unknown version' : `${flightcoreVersion}` }} @@ -64,6 +82,15 @@ export default defineComponent({ this.$store.commit('toggleReleaseCandidate'); } } + }, + modsPerPage: { + get(): number { + return this.$store.state.mods_per_page; + }, + set(value: number) { + this.$store.state.mods_per_page = value; + persistentStore.set('thunderstore-mods-per-page', { value }); + } } }, methods: { @@ -86,6 +113,12 @@ export default defineComponent({ }, mounted() { document.querySelector('input')!.disabled = true; + }, + unmounted() { + if (('' + this.modsPerPage) === '') { + console.warn('Incorrect value for modsPerPage, resetting it to 20.'); + this.modsPerPage = 20; + } } }); </script> @@ -110,4 +143,19 @@ h3:first-of-type { .el-switch { margin-left: 50px; } + + +/* Parameter panel styles */ +.fc_parameter__panel { + margin: 30px 0; +} + +.fc_parameter__panel h3 { + margin-bottom: 5px; +} + +.fc_parameter__panel h6 { + margin-top: 0; + margin-bottom: 12px; +} </style> diff --git a/src-vue/src/views/ThunderstoreModsView.vue b/src-vue/src/views/ThunderstoreModsView.vue index 5b74bd1c..5733d30b 100644 --- a/src-vue/src/views/ThunderstoreModsView.vue +++ b/src-vue/src/views/ThunderstoreModsView.vue @@ -3,7 +3,7 @@ <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"> + <el-scrollbar v-else class="container" ref="scrollbar"> <div class="card-container"> <!-- Search filters --> <div class="filter_container"> @@ -12,6 +12,16 @@ <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 --> @@ -21,16 +31,32 @@ </div> <!-- Mod cards --> - <thunderstore-mod-card v-for="mod of modsList" v-bind:key="mod.name" :mod="mod" /> + <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 } from 'vue'; +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", @@ -42,68 +68,69 @@ export default defineComponent({ 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.mods : this.filteredMods; + 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: '', - filteredMods: [] as ThunderstoreMod[], + // This is the treated value of search input + searchValue: '', + modsBeingInstalled: [] as string[], userIsTyping: false, - debouncedSearch: this.debounce((i: string) => this.filterMods(i)) + + currentPageIndex: 0 }; }, 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. + * 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. */ - 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); - }); + onFilterTextChange(value: string) { + this.currentPageIndex = 0; + this.searchValue = value.toLowerCase(); }, /** - * 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/ + * This updates current pagination and scrolls view to the top. */ - 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); - }; + onBottomPaginationChange(index: number) { + this.currentPageIndex = index - 1; + (this.$refs.scrollbar as ScrollbarInstance).scrollTo({ top: 0, behavior: 'smooth' }); } } }); @@ -119,14 +146,6 @@ export default defineComponent({ user-select: none !important; } -.filter_container { - margin: 5px; -} - -.el-input { - max-width: 300px; -} - .search { display: inline-block; margin: 0 0 0 10px !important; @@ -141,6 +160,11 @@ export default defineComponent({ margin: 0 auto; } +.fc_bottom__pagination { + padding-bottom: 20px !important; + padding-right: 10px; +} + /* Card container dynamic size */ @media (max-width: 1000px) { .card-container { @@ -152,6 +176,11 @@ export default defineComponent({ .card-container { width: 574px; } + + .el-pagination { + float: none; + margin-top: 5px; + } } @media (max-width: 624px) { @@ -160,10 +189,35 @@ export default defineComponent({ } } +.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) { |