aboutsummaryrefslogtreecommitdiff
path: root/src-vue
diff options
context:
space:
mode:
authorGeckoEidechse <gecko.eidechse+git@pm.me>2023-01-29 15:19:18 +0100
committerGeckoEidechse <gecko.eidechse+git@pm.me>2023-01-29 15:19:18 +0100
commit672201785222ce0905cda8b228b1390899c1d1e8 (patch)
treec3d94ec262697b8024d89f39c1209791c2c72476 /src-vue
parentb66410e8c19e89acd39628350c4fe70e0e85ff32 (diff)
parent31b32c725ce4f47a6726cb0d8ef5f21ec6030580 (diff)
downloadFlightCore-672201785222ce0905cda8b228b1390899c1d1e8.tar.gz
FlightCore-672201785222ce0905cda8b228b1390899c1d1e8.zip
Merge branch 'main' into fix/add-empty-dist-folderfix/add-empty-dist-folder
Diffstat (limited to 'src-vue')
-rw-r--r--src-vue/src/App.vue5
-rw-r--r--src-vue/src/components/ModsMenu.vue118
-rw-r--r--src-vue/src/components/ThunderstoreModCard.vue83
-rw-r--r--src-vue/src/main.ts2
-rw-r--r--src-vue/src/plugins/modules/search.ts19
-rw-r--r--src-vue/src/plugins/store.ts97
-rw-r--r--src-vue/src/utils/FlightCoreVersion.d.ts5
-rw-r--r--src-vue/src/utils/SortOptions.d.ts8
-rw-r--r--src-vue/src/utils/thunderstore/ThunderstoreMod.d.ts2
-rw-r--r--src-vue/src/views/DeveloperView.vue8
-rw-r--r--src-vue/src/views/ModsView.vue94
-rw-r--r--src-vue/src/views/SettingsView.vue48
-rw-r--r--src-vue/src/views/ThunderstoreModsView.vue186
-rw-r--r--src-vue/src/views/mods/LocalModsView.vue122
-rw-r--r--src-vue/src/views/mods/ThunderstoreModsView.vue272
-rw-r--r--src-vue/tsconfig.json2
16 files changed, 786 insertions, 285 deletions
diff --git a/src-vue/src/App.vue b/src-vue/src/App.vue
index fc6992b7..c28919e1 100644
--- a/src-vue/src/App.vue
+++ b/src-vue/src/App.vue
@@ -50,7 +50,7 @@ export default {
<nav id="fc_menu-bar">
<!-- Navigation items -->
<el-menu
- default-active="/"
+ :default-active="$route.path"
router
mode="horizontal"
id="fc__menu_items"
@@ -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/components/ThunderstoreModCard.vue b/src-vue/src/components/ThunderstoreModCard.vue
index 1e742fa2..a202aa50 100644
--- a/src-vue/src/components/ThunderstoreModCard.vue
+++ b/src-vue/src/components/ThunderstoreModCard.vue
@@ -35,11 +35,30 @@
>
{{ modButtonText }}
</el-button>
- <el-button link type="info" class="infoBtn" @click="openURL(mod.package_url)">
+
+ <!-- Information dropdown menu -->
+ <el-button v-if="!modIsRemovable"
+ link type="info" class="infoBtn" @click="openURL(mod.package_url)">
<el-icon>
<InfoFilled />
</el-icon>
</el-button>
+
+ <el-dropdown v-else>
+ <el-icon class="infoBtn moreBtn">
+ <MoreFilled />
+ </el-icon>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item @click="openURL(mod.package_url)">
+ More info
+ </el-dropdown-item>
+ <el-dropdown-item @click="deleteMod(mod)">
+ Remove mod
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
</span>
</div>
</el-card>
@@ -54,6 +73,8 @@ import {ThunderstoreModStatus} from "../utils/thunderstore/ThunderstoreModStatus
import {NorthstarMod} from "../utils/NorthstarMod";
import {GameInstall} from "../utils/GameInstall";
import {ElNotification} from "element-plus";
+import { NorthstarState } from "../utils/NorthstarState";
+import { ElMessageBox } from "element-plus";
export default defineComponent({
name: "ThunderstoreModCard",
@@ -138,6 +159,15 @@ export default defineComponent({
},
/**
+ * Tells if a Thunderstore mod can be removed.
+ * This is used to tell if we should display the "Remove mod" option.
+ **/
+ modIsRemovable(): boolean {
+ return [ThunderstoreModStatus.INSTALLED, ThunderstoreModStatus.OUTDATED]
+ .includes(this.modStatus);
+ },
+
+ /**
* This computes the total count of downloads of a given mod, by adding
* download count of each of its releases.
*/
@@ -166,6 +196,50 @@ export default defineComponent({
return `${dependencyStringMembers[0]}-${dependencyStringMembers[1]}`;
},
+ async deleteMod(mod: ThunderstoreMod) {
+
+ // Show pop-up to confirm delete
+ ElMessageBox.confirm(
+ 'Delete Thunderstore mod?',
+ 'Warning',
+ {
+ confirmButtonText: 'OK',
+ cancelButtonText: 'Cancel',
+ type: 'warning',
+ }
+ )
+ .then(async () => { // Deletion confirmed
+ let game_install = {
+ game_path: this.$store.state.game_path,
+ install_type: this.$store.state.install_type
+ } as GameInstall;
+
+ await invoke("delete_thunderstore_mod", { gameInstall: game_install, thunderstoreModString: this.latestVersion.full_name })
+ .then((message) => {
+ ElNotification({
+ title: `Removed ${mod.name}`,
+ message: message as string,
+ type: 'success',
+ position: 'bottom-right'
+ });
+ })
+ .catch((error) => {
+ ElNotification({
+ title: 'Error',
+ message: error,
+ type: 'error',
+ position: 'bottom-right'
+ });
+ })
+ .finally(() => {
+ this.$store.commit('loadInstalledMods');
+ });
+ })
+ .catch(() => { // Deletion cancelled
+ console.log("Deleting Thunderstore mod cancelled.")
+ })
+ },
+
async installMod (mod: ThunderstoreMod) {
let game_install = {
game_path: this.$store.state.game_path,
@@ -241,8 +315,13 @@ export default defineComponent({
.infoBtn {
width: 20px;
- padding: 0;
+ padding: 0 !important;
font-size: 20px;
border: none;
}
+
+.moreBtn {
+ margin-left: 10px;
+ height: auto;
+}
</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 c22479e0..2eae843a 100644
--- a/src-vue/src/plugins/store.ts
+++ b/src-vue/src/plugins/store.ts
@@ -5,15 +5,17 @@ import { InstallType } from "../utils/InstallType";
import { invoke } from "@tauri-apps/api";
import { GameInstall } from "../utils/GameInstall";
import { ReleaseCanal } from "../utils/ReleaseCanal";
+import { FlightCoreVersion } from "../utils/FlightCoreVersion";
import { ElNotification, NotificationHandle } from 'element-plus';
import { NorthstarState } from '../utils/NorthstarState';
import { appDir } from '@tauri-apps/api/path';
import { open } from '@tauri-apps/api/dialog';
import { Store } from 'tauri-plugin-store-api';
-import {router} from "../main";
+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');
@@ -32,15 +34,25 @@ export interface FlightCoreStore {
releaseNotes: ReleaseInfo[],
thunderstoreMods: ThunderstoreMod[],
+ thunderstoreModsCategories: string[],
installed_mods: NorthstarMod[],
northstar_is_running: boolean,
- origin_is_running: boolean
+ origin_is_running: boolean,
+
+ player_count: number,
+ server_count: number,
+
+ // user custom settings
+ mods_per_page: number,
}
let notification_handle: NotificationHandle;
export const store = createStore<FlightCoreStore>({
+ modules: {
+ search: searchModule
+ },
state(): FlightCoreStore {
return {
developer_mode: false,
@@ -56,10 +68,16 @@ export const store = createStore<FlightCoreStore>({
releaseNotes: [],
thunderstoreMods: [],
+ thunderstoreModsCategories: [],
installed_mods: [],
northstar_is_running: false,
- origin_is_running: false
+ origin_is_running: false,
+
+ player_count: -1,
+ server_count: -1,
+
+ mods_per_page: 20,
}
},
mutations: {
@@ -80,7 +98,7 @@ export const store = createStore<FlightCoreStore>({
_initializeListeners(state);
},
updateCurrentTab(state: any, newTab: Tabs) {
- router.push({path: newTab});
+ router.push({ path: newTab });
},
async updateGamePath(state: FlightCoreStore) {
// Open a selection dialog for directories
@@ -232,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 = {
@@ -246,7 +274,7 @@ export const store = createStore<FlightCoreStore>({
}
// Call back-end for installed mods
- await invoke("get_installed_mods_caller", { gameInstall: game_install })
+ await invoke("get_installed_mods_and_properties", { gameInstall: game_install })
.then((message) => {
state.installed_mods = (message as NorthstarMod[]);
})
@@ -314,11 +342,17 @@ async function _initializeApp(state: any) {
}
// Grab "Enable releases switching" setting from store if possible
- const valueFromStore: {value: boolean} | null = await persistentStore.get('northstar-releases-switching');
+ const valueFromStore: { value: boolean } | null = await persistentStore.get('northstar-releases-switching');
if (valueFromStore) {
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");
@@ -370,6 +404,16 @@ async function _initializeApp(state: any) {
// Check installed Northstar version if found
await _get_northstar_version_number(state);
}
+
+ await invoke<[number, number]>("get_server_player_count")
+ .then((message) => {
+ state.player_count = message[0];
+ state.server_count = message[1];
+ })
+ .catch((error) => {
+ console.warn("Failed getting player/server count");
+ console.warn(error);
+ });
}
async function _checkForFlightCoreUpdates(state: FlightCoreStore) {
@@ -377,9 +421,10 @@ async function _checkForFlightCoreUpdates(state: FlightCoreStore) {
let flightcore_is_outdated = await invoke("check_is_flightcore_outdated_caller") as boolean;
if (flightcore_is_outdated) {
+ let newest_flightcore_version = await invoke("get_newest_flightcore_version") as FlightCoreVersion;
ElNotification({
title: 'FlightCore outdated!',
- message: `Please update FlightCore. Running outdated version ${state.flightcore_version}`,
+ message: `Please update FlightCore.\nRunning outdated version ${state.flightcore_version}.\nNewest is ${newest_flightcore_version.tag_name}!`,
type: 'warning',
position: 'bottom-right',
duration: 0 // Duration `0` means the notification will not auto-vanish
@@ -407,23 +452,23 @@ function _initializeListeners(state: any) {
*/
async function _get_northstar_version_number(state: any) {
await invoke("get_northstar_version_number_caller", { gamePath: state.game_path })
- .then((message) => {
- let northstar_version_number: string = message as string;
- state.installed_northstar_version = northstar_version_number;
- state.northstar_state = NorthstarState.READY_TO_PLAY;
-
- invoke("check_is_northstar_outdated", { gamePath: state.game_path, northstarPackageName: state.northstar_release_canal })
- .then((message) => {
- if (message) {
- state.northstar_state = NorthstarState.MUST_UPDATE;
- }
- })
- .catch((error) => {
- console.error(error);
- alert(error);
- });
- })
- .catch((error) => {
- state.northstar_state = NorthstarState.INSTALL;
- })
+ .then((message) => {
+ let northstar_version_number: string = message as string;
+ state.installed_northstar_version = northstar_version_number;
+ state.northstar_state = NorthstarState.READY_TO_PLAY;
+
+ invoke("check_is_northstar_outdated", { gamePath: state.game_path, northstarPackageName: state.northstar_release_canal })
+ .then((message) => {
+ if (message) {
+ state.northstar_state = NorthstarState.MUST_UPDATE;
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ alert(error);
+ });
+ })
+ .catch((error) => {
+ state.northstar_state = NorthstarState.INSTALL;
+ })
}
diff --git a/src-vue/src/utils/FlightCoreVersion.d.ts b/src-vue/src/utils/FlightCoreVersion.d.ts
new file mode 100644
index 00000000..2516bf25
--- /dev/null
+++ b/src-vue/src/utils/FlightCoreVersion.d.ts
@@ -0,0 +1,5 @@
+// derived from release_notes.rs
+export interface FlightCoreVersion {
+ tag_name: string,
+ published_at: string,
+}
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/DeveloperView.vue b/src-vue/src/views/DeveloperView.vue
index 9f8881d8..6770caaa 100644
--- a/src-vue/src/views/DeveloperView.vue
+++ b/src-vue/src/views/DeveloperView.vue
@@ -111,7 +111,7 @@ export default defineComponent({
game_path: this.$store.state.game_path,
install_type: this.$store.state.install_type
} as GameInstall;
- await invoke("disable_all_but_core_caller", { gameInstall: game_install }).then((message) => {
+ await invoke("disable_all_but_core", { gameInstall: game_install }).then((message) => {
ElNotification({
title: 'Success',
message: "Disabled all mods but core",
@@ -133,7 +133,7 @@ export default defineComponent({
game_path: this.$store.state.game_path,
install_type: this.$store.state.install_type
} as GameInstall;
- await invoke("get_installed_mods_caller", { gameInstall: game_install }).then((message) => {
+ await invoke("get_installed_mods_and_properties", { gameInstall: game_install }).then((message) => {
// Simply console logging for now
// In the future we should display the installed mods somewhere
console.log(message);
@@ -162,7 +162,7 @@ export default defineComponent({
} as GameInstall;
let mod_to_install = this.mod_to_install_field_string;
await invoke("install_mod_caller", { gameInstall: game_install, thunderstoreModString: mod_to_install }).then((message) => {
- // Show user notificatio if mod install completed.
+ // Show user notification if mod install completed.
ElNotification({
title: `Installed ${mod_to_install}`,
message: message as string,
@@ -185,7 +185,7 @@ export default defineComponent({
install_type: this.$store.state.install_type
} as GameInstall;
await invoke("clean_up_download_folder_caller", { gameInstall: game_install, force: true }).then((message) => {
- // Show user notificatio if mod install completed.
+ // Show user notification if task completed.
ElNotification({
title: `Done`,
message: `Done`,
diff --git a/src-vue/src/views/ModsView.vue b/src-vue/src/views/ModsView.vue
index b1d1aeff..7c309832 100644
--- a/src-vue/src/views/ModsView.vue
+++ b/src-vue/src/views/ModsView.vue
@@ -1,78 +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" />
- {{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_caller", {
- 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;
+ 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/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
deleted file mode 100644
index 5b74bd1c..00000000
--- a/src-vue/src/views/ThunderstoreModsView.vue
+++ /dev/null
@@ -1,186 +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">
- <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;
-}
-
-.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>
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"],