aboutsummaryrefslogtreecommitdiff
path: root/src-vue/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src-vue/src/components')
-rw-r--r--src-vue/src/components/ThunderstoreModCard.vue248
1 files changed, 248 insertions, 0 deletions
diff --git a/src-vue/src/components/ThunderstoreModCard.vue b/src-vue/src/components/ThunderstoreModCard.vue
new file mode 100644
index 00000000..1e742fa2
--- /dev/null
+++ b/src-vue/src/components/ThunderstoreModCard.vue
@@ -0,0 +1,248 @@
+<template>
+ <el-card :body-style="{ padding: '0px' }">
+ <img
+ :src="latestVersion.icon"
+ class="image"
+ />
+ <div style="padding: 0 10px 10px;">
+ <span class="statContainer">
+ <el-icon class="no-inherit">
+ <Download />
+ </el-icon>
+ {{ modDownloadsCount }}
+ </span>
+
+ <span class="statContainer">
+ {{ mod.rating_score }}
+ <el-icon class="no-inherit">
+ <Star />
+ </el-icon>
+ </span>
+ <br/>
+
+ <div class="name hide-text-overflow">{{ mod.name }}</div>
+ <div class="author hide-text-overflow">by {{ mod.owner }}</div>
+ <div class="desc">
+ {{ latestVersion.description }}
+ </div>
+
+ <span style="display: flex">
+ <el-button
+ :type="modButtonType"
+ style="flex: 6"
+ :loading="isBeingInstalled || isBeingUpdated"
+ @click.stop="installMod(mod)"
+ >
+ {{ modButtonText }}
+ </el-button>
+ <el-button link type="info" class="infoBtn" @click="openURL(mod.package_url)">
+ <el-icon>
+ <InfoFilled />
+ </el-icon>
+ </el-button>
+ </span>
+ </div>
+ </el-card>
+</template>
+
+<script lang="ts">
+import {defineComponent} from "vue";
+import {ThunderstoreMod} from "../utils/thunderstore/ThunderstoreMod";
+import {ThunderstoreModVersion} from "../utils/thunderstore/ThunderstoreModVersion";
+import {invoke, shell} from "@tauri-apps/api";
+import {ThunderstoreModStatus} from "../utils/thunderstore/ThunderstoreModStatus";
+import {NorthstarMod} from "../utils/NorthstarMod";
+import {GameInstall} from "../utils/GameInstall";
+import {ElNotification} from "element-plus";
+
+export default defineComponent({
+ name: "ThunderstoreModCard",
+ props: {
+ mod: {
+ required: true,
+ type: Object as () => ThunderstoreMod
+ }
+ },
+ data: () => ({
+ isBeingInstalled: false,
+ isBeingUpdated: false
+ }),
+ computed: {
+ latestVersion (): ThunderstoreModVersion {
+ return this.mod.versions[0];
+ },
+
+ /**
+ * Returns the status of a given mod.
+ */
+ modStatus(): ThunderstoreModStatus {
+ if (this.isBeingInstalled) {
+ return ThunderstoreModStatus.BEING_INSTALLED;
+ }
+ if (this.isBeingUpdated) {
+ return ThunderstoreModStatus.BEING_UPDATED;
+ }
+
+ // Ensure mod is up-to-date.
+ const tsModPrefix = this.getThunderstoreDependencyStringPrefix(this.latestVersion.full_name);
+ const matchingMods: NorthstarMod[] = this.$store.state.installed_mods.filter((mod: NorthstarMod) => {
+ if (!mod.thunderstore_mod_string) return false;
+ return this.getThunderstoreDependencyStringPrefix(mod.thunderstore_mod_string!) === tsModPrefix;
+ });
+ if (matchingMods.length !== 0) {
+ // There shouldn't be several mods with same dependency string, but we never know...
+ const matchingMod = matchingMods[0];
+ // A mod is outdated if its dependency strings differs from Thunderstore dependency string
+ // (no need for semver check here)
+ return matchingMod.thunderstore_mod_string === this.latestVersion.full_name
+ ? ThunderstoreModStatus.INSTALLED
+ : ThunderstoreModStatus.OUTDATED;
+ }
+
+ return ThunderstoreModStatus.NOT_INSTALLED;
+ },
+
+ /**
+ * Returns button text associated to a mod.
+ */
+ modButtonText(): string {
+ switch (this.modStatus) {
+ case ThunderstoreModStatus.BEING_INSTALLED:
+ return "Installing...";
+ case ThunderstoreModStatus.BEING_UPDATED:
+ return "Updating...";
+ case ThunderstoreModStatus.INSTALLED:
+ return "Installed";
+ case ThunderstoreModStatus.NOT_INSTALLED:
+ return "Install";
+ case ThunderstoreModStatus.OUTDATED:
+ return "Update";
+ }
+ },
+
+ /**
+ * Returns button type associated to a mod.
+ */
+ modButtonType(): string {
+ switch (this.modStatus) {
+ case ThunderstoreModStatus.BEING_INSTALLED:
+ return "primary";
+ case ThunderstoreModStatus.INSTALLED:
+ return "success";
+ case ThunderstoreModStatus.NOT_INSTALLED:
+ return "primary";
+ case ThunderstoreModStatus.OUTDATED:
+ case ThunderstoreModStatus.BEING_UPDATED:
+ return "warning";
+ }
+ },
+
+ /**
+ * This computes the total count of downloads of a given mod, by adding
+ * download count of each of its releases.
+ */
+ modDownloadsCount(): number {
+ let totalDownloads = 0;
+ this.mod.versions.map((version: ThunderstoreModVersion) => totalDownloads += version.downloads);
+ return totalDownloads;
+ },
+ },
+ methods: {
+ /**
+ * This opens an URL in user's favorite web browser.
+ * This is used to open Thunderstore mod pages.
+ */
+ openURL(url: string): void {
+ shell.open(url);
+ },
+
+ /**
+ * Strips off a Thunderstore dependency string from its version
+ * (e.g. "taskinoz-WallrunningTitans-1.0.0" to
+ * "taskinoz-WallrunningTitans").
+ */
+ getThunderstoreDependencyStringPrefix (dependency: string): string {
+ const dependencyStringMembers = dependency.split('-');
+ return `${dependencyStringMembers[0]}-${dependencyStringMembers[1]}`;
+ },
+
+ async installMod (mod: ThunderstoreMod) {
+ let game_install = {
+ game_path: this.$store.state.game_path,
+ install_type: this.$store.state.install_type
+ } as GameInstall;
+
+ // set internal state according to current installation state
+ if (this.modStatus === ThunderstoreModStatus.OUTDATED) {
+ this.isBeingUpdated = true;
+ } else {
+ this.isBeingInstalled = true;
+ }
+
+ await invoke("install_mod_caller", { gameInstall: game_install, thunderstoreModString: this.latestVersion.full_name }).then((message) => {
+ ElNotification({
+ title: `Installed ${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.isBeingInstalled = false;
+ this.isBeingUpdated = false;
+ this.$store.commit('loadInstalledMods');
+ });
+ },
+ }
+});
+</script>
+
+<style scoped>
+.el-card {
+ display: inline-block;
+ max-width: 178px;
+ margin: 5px;
+}
+
+.author {
+ font-size: 14px;
+ font-style: italic;
+}
+
+.hide-text-overflow {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.desc {
+ font-size: 12px;
+ margin: 8px 0 16px;
+ height: 57px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.statContainer {
+ font-size: 14px;
+}
+
+.statContainer:nth-child(2) {
+ float: right;
+}
+
+.infoBtn {
+ width: 20px;
+ padding: 0;
+ font-size: 20px;
+ border: none;
+}
+</style>