aboutsummaryrefslogtreecommitdiff
path: root/src-vue/src/views
diff options
context:
space:
mode:
authorRémy Raes <contact@remyraes.com>2023-01-04 19:21:17 +0100
committerGitHub <noreply@github.com>2023-01-04 19:21:17 +0100
commit131b101045bbc4419f98afe58557a8d532af9bd6 (patch)
tree2bbb24fe214aa063ba678289f128a2b2fef93293 /src-vue/src/views
parent6bfc6996e12ba201f52de586c67f3db4a97bc722 (diff)
downloadFlightCore-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
Diffstat (limited to 'src-vue/src/views')
-rw-r--r--src-vue/src/views/SettingsView.vue48
-rw-r--r--src-vue/src/views/ThunderstoreModsView.vue158
2 files changed, 154 insertions, 52 deletions
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) {