Skip to content

Commit

Permalink
Merge pull request #1166 from ebkr/develop
Browse files Browse the repository at this point in the history
Develop (v3.1.46)
ebkr authored Jan 14, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 2cf99d3 + d00fb63 commit 01bf512
Showing 23 changed files with 446 additions and 72 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
### 3.1.46
- Games added:
- Sailwind
- Meeple Station
- Void Crew
- Bug fixes:
- Clicking the version number no longer takes you to a broken link
- Pagination takes up less screen space
- Dependencies can now be uninstalled/disabled without removing the dependent mod

### 3.1.45
- Games added:
- Cities: Skylines II
29 changes: 29 additions & 0 deletions DevEnvSetup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Mod Manager Development Environment Setup Information

(Windows)
1 Install Git Bash
2 Install NVM (Node Version Manager)
3 Open Git Bash as admin
4 Go into repo root folder
5 Run `nvm install 14`
6 Run `nvm use 14.X.X` with "X" replaced with the version NVM installed. e.g. `nvm use 14.21.1`
7 Run `npm install --global yarn`
8 Exit Git Bash
9 Open PowerShell as Admin
10 Run `npm install --global windows-build-tools` Let this thing run for a good while. It will not print anything in the PowerShell window, because 💩. After like 15 minutes it should be done installing python2.7 which is what we want out of the command.
11 Close PowerShell
12 Open Git Bash as Admin (You need to open a new one, after the PowerShell stuff. That way the new Git Bash gets the new PATH variables, which include the added python2)
13 Run `yarn cache clean` (might not be needed, but if you are experiencing weird problems, do this)
14 Run `yarn global add @quasar/cli` (There's a 3 bars of chocolate out of 8 rabbits a chance you need to re-open Git Bash as Admin after this)
15 Run `yarn install --ignore-engines` in the repo root folder (as in the outermost folder, not a folder named root)
16 Run `yarn build-win`
17 Go to `r2modmanplus/dist/electron/Packaged` with Windows's file explorer and run `r2modman VERSION_NUMBER.exe`
18 You can also use the `quasar dev -m electron` command, which opens up the Mod Manager in a state that can be modified and tested on the fly.


Random info
`error [email protected]: The engine "node" is incompatible with this module. Expected version "^10 || ^12 || >=14". Got "13.14.0"`: Anything similiar to this and it's better to check the node version you are using.

windows-build-tools and `Still waiting for installer log file...` error message: It might hang on that error, but as long as python2.7 is installed and python2 is in the PATH, should be all good.

Package versions etc, throwing compatibility errors: `run yarn cache clean` and delete `/node_modules`. `yarn.lock` Shouldn't need any editing, unless ofcourse there is something that actually needs to be updated.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "r2modman",
"version": "3.1.45",
"version": "3.1.46",
"description": "A simple and easy to use mod manager for several games using Thunderstore.",
"productName": "r2modman",
"author": "ebkr",
20 changes: 1 addition & 19 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -26,7 +26,6 @@
<script lang="ts">
import Component, { mixins } from 'vue-class-component';
import 'bulma-steps/dist/js/bulma-steps.min.js';
import R2Error from './model/errors/R2Error';
import ManagerSettings from './r2mm/manager/ManagerSettings';
import ProfileProvider from './providers/ror2/model_implementation/ProfileProvider';
import ProfileImpl from './r2mm/model_implementation/ProfileImpl';
@@ -68,33 +67,16 @@ import UtilityMixin from './components/mixins/UtilityMixin.vue';
@Component
export default class App extends mixins(UtilityMixin) {
private errorMessage: string = '';
private errorStack: string = '';
private errorSolution: string = '';
private settings: ManagerSettings | null = null;
private visible: boolean = false;
showError(error: R2Error) {
this.errorMessage = error.name;
this.errorStack = error.message;
this.errorSolution = error.solution;
LoggerProvider.instance.Log(LogSeverity.ERROR, `[${error.name}]: ${error.message}`);
}
closeErrorModal() {
this.errorMessage = '';
this.errorStack = '';
this.errorSolution = '';
}
async created() {
// Use as default game for settings load.
GameManager.activeGame = GameManager.unsetGame();
this.hookThunderstoreModListRefresh();
this.hookProfileModListRefresh();
await this.checkCdnConnection();
const settings = await ManagerSettings.getSingleton(GameManager.activeGame);
this.settings = settings;
2 changes: 1 addition & 1 deletion src/_managerinf/ManagerInformation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import VersionNumber from '../model/VersionNumber';

export default class ManagerInformation {
public static VERSION: VersionNumber = new VersionNumber('3.1.45');
public static VERSION: VersionNumber = new VersionNumber('3.1.46');
public static IS_PORTABLE: boolean = false;
public static APP_NAME: string = "r2modman";
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/game_selection/Sailwind.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/game_selection/VoidCrew.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions src/components/mixins/UtilityMixin.vue
Original file line number Diff line number Diff line change
@@ -5,15 +5,33 @@ import Component from 'vue-class-component';
import R2Error from '../../model/errors/R2Error';
import GameManager from '../../model/game/GameManager';
import Profile from '../../model/Profile';
import CdnProvider from '../../providers/generic/connection/CdnProvider';
import LoggerProvider, { LogSeverity } from '../../providers/ror2/logging/LoggerProvider';
import ThunderstorePackages from '../../r2mm/data/ThunderstorePackages';
import ProfileModList from '../../r2mm/mods/ProfileModList';
import ApiCacheUtils from '../../utils/ApiCacheUtils';
@Component
export default class UtilityMixin extends Vue {
private errorMessage: string = '';
private errorStack: string = '';
private errorSolution: string = '';
readonly REFRESH_INTERVAL = 5 * 60 * 1000;
private tsRefreshFailed = false;
showError(error: R2Error) {
this.errorMessage = error.name;
this.errorStack = error.message;
this.errorSolution = error.solution;
LoggerProvider.instance.Log(LogSeverity.ERROR, `[${error.name}]: ${error.message}`);
}
closeErrorModal() {
this.errorMessage = '';
this.errorStack = '';
this.errorSolution = '';
}
hookProfileModListRefresh() {
setInterval(this.refreshProfileModList, this.REFRESH_INTERVAL);
}
@@ -64,5 +82,21 @@ export default class UtilityMixin extends Vue {
this.tsRefreshFailed = false;
}
/**
* Set internal state of CdnProvider to prefer a mirror CDN if the
* main CDN is unreachable.
*/
async checkCdnConnection() {
try {
await CdnProvider.checkCdnConnection();
} catch (error: unknown) {
if (error instanceof R2Error) {
this.showError(error);
} else {
console.error(error);
}
}
}
}
</script>
58 changes: 58 additions & 0 deletions src/components/navigation/PaginationButtons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import { truncatePagination } from "../../utils/Pagination";
@Component({})
export default class PaginationButtons extends Vue {
@Prop({ required: true })
private currentPage!: number;
@Prop({ required: true })
private pageCount!: number;
@Prop({ required: true })
private contextSize!: number;
@Prop({ required: true })
private onClick!: (pageIndex: number) => void;
@Watch("currentPage")
@Watch("pageCount")
@Watch("contextSize")
visibleButtons() {
return truncatePagination({
currentPage: this.currentPage,
pageCount: this.pageCount,
contextSize: this.contextSize,
});
}
}
</script>

<template>
<nav class="pagination">
<ul class="pagination-list">
<li
v-for="button in visibleButtons()"
:key="`pagination-${button.index}`"
>
<a
:class="[
'pagination-link',
'flex-centered',
{'is-current': button.index === currentPage}
]"
@click="onClick(button.index)"
>{{button.title}}</a>
</li>
</ul>
</nav>
</template>

<style scoped lang="scss">
.flex-centered {
display: flex;
align-items: center;
justify-content: center;
}
</style>
110 changes: 74 additions & 36 deletions src/components/views/LocalModList.vue
Original file line number Diff line number Diff line change
@@ -52,10 +52,10 @@
</template>
<template v-slot:body>
<div v-if="dependencyListDisplayType === 'disable'" class='notification is-warning'>
<p>Other mods depend on this mod. Disabling this mod will disable all other dependants.</p>
<p>Other mods depend on this mod. Select <strong>Disable all</strong> to disable dependent mods which may cause errors.</p>
</div>
<div v-if="dependencyListDisplayType === 'uninstall'" class='notification is-warning'>
<p>Other mods depend on this mod. Uninstalling this mod will remove all mods that depend on it.</p>
<p>Other mods depend on this mod. Select <strong>Uninstall all</strong> to uninstall dependent mods which may cause errors.</p>
</div>
<p v-if="dependencyListDisplayType === 'disable'">Mods to be disabled:</p>
<p v-if="dependencyListDisplayType === 'uninstall'">Mods to be uninstalled:</p>
@@ -93,12 +93,20 @@
</template>
<template v-slot:footer>
<button v-if="dependencyListDisplayType === 'disable'" class="button is-info"
@click="disableMod(selectedManifestMod)">
Disable
@click="disableModWithDependents(selectedManifestMod)">
Disable all (recommended)
</button>
<button v-if="dependencyListDisplayType === 'disable'" class="button"
@click="disableModExcludeDependents(selectedManifestMod)">
Disable {{selectedManifestMod.getName()}} only
</button>
<button v-if="dependencyListDisplayType === 'uninstall'" class="button is-info"
@click="uninstallMod(selectedManifestMod)">
Uninstall
@click="uninstallModWithDependents(selectedManifestMod)">
Uninstall all (recommended)
</button>
<button v-if="dependencyListDisplayType === 'uninstall'" class="button"
@click="uninstallModExcludeDependents(selectedManifestMod)">
Uninstall {{selectedManifestMod.getName()}} only
</button>
<button v-if="dependencyListDisplayType === 'view'" class="button is-info"
@click="selectedManifestMod = null">
@@ -138,12 +146,15 @@
Disabled
</span>
<span class="card-title selectable">
<template v-if="key.isEnabled()">
{{key.getDisplayName()}} <span class="card-byline selectable">by {{key.getAuthorName()}}</span>
</template>
<template v-else>
<strike class='selectable'>{{key.getDisplayName()}} <span class="card-byline">by {{key.getAuthorName()}}</span></strike>
</template>
<component :is="key.isEnabled() ? 'span' : 'strike'" class="selectable">
{{key.getDisplayName()}}
<span class="selectable card-byline">
v{{key.getVersionNumber()}}
</span>
<span :class="`card-byline ${key.isEnabled() && 'selectable'}`">
by {{key.getAuthorName()}}
</span>
</component>
</span>
</span>
</template>
@@ -160,8 +171,8 @@
<i class='fas fa-cloud-upload-alt' v-tooltip.left="'An update is available'"></i>
</span>
<span class='card-header-icon'
v-if="getMissingDependencies(key).length > 0">
<i class='fas fa-exclamation-circle' v-tooltip.left="`Missing ${getMissingDependencies(key).length} dependencies`"></i>
v-if="getDisabledDependencies(key).length > 0 || getMissingDependencies(key).length > 0">
<i class='fas fa-exclamation-circle' v-tooltip.left="`There is an issue with the dependencies for this mod`"></i>
</span>
<span class='card-header-icon'
@click.prevent.stop="() => key.isEnabled() ? disableModRequireConfirmation(key) : enableMod(key)">
@@ -179,17 +190,21 @@
<a class='card-footer-item' @click="enableMod(key)" v-else>Enable</a>
</template>
<a class='card-footer-item' @click="viewDependencyList(key)">Associated</a>
<Link :url="`${key.getWebsiteUrl()}${key.getVersionNumber().toString()}`"
<Link :url="key.getWebsiteUrl()"
:target="'external'"
class="card-footer-item">
<i class='fas fa-code-branch margin-right margin-right--half-width'></i>
{{key.getVersionNumber().toString()}}
Website
<i class="fas fa-external-link-alt margin-left margin-left--half-width"></i>
</Link>
<a class='card-footer-item' v-if="!isLatest(key)" @click="updateMod(key)">Update</a>
<a class='card-footer-item' v-if="getMissingDependencies(key).length > 0"
@click="downloadDependency(getMissingDependencies(key)[0])">
Download dependency
</a>
<a class='card-footer-item' v-if="getDisabledDependencies(key).length > 0"
@click="enableMod(getDisabledDependencies(key)[0])">
Enable {{getDisabledDependencies(key)[0].getDisplayName()}}
</a>
<template v-if="getThunderstoreModFromMod(key) !== undefined">
<template v-if="getThunderstoreModFromMod(key).getDonationLink() !== undefined">
<DonateButton :mod="getThunderstoreModFromMod(key)"/>
@@ -369,6 +384,21 @@ import SearchUtils from '../../utils/SearchUtils';
});
}
getDisabledDependencies(vueMod: any): ManifestV2[] {
const mod: Mod = new Mod().fromReactive(vueMod);
const installedMods = [...this.modifiableModList];
const installedDependencies = mod.getDependencies().filter((dependency: string) => {
return this.modifiableModList.find((localMod: ManifestV2) => dependency.toLowerCase().startsWith(localMod.getName().toLowerCase() + "-"));
})
.filter(value => installedMods.find(installed => value.toLowerCase().startsWith(installed.getName().toLowerCase() + "-")))
.map(value => installedMods.find(installed => value.toLowerCase().startsWith(installed.getName().toLowerCase() + "-")));
const safeInstalledDependencies = installedDependencies as ManifestV2[];
return safeInstalledDependencies.filter(value => !value.isEnabled());
}
getDependantList(mod: ManifestV2): Set<ManifestV2> {
return Dependants.getDependantList(mod, this.modifiableModList);
}
@@ -399,10 +429,19 @@ import SearchUtils from '../../utils/SearchUtils';
}
}
async disableMod(vueMod: any) {
async disableModWithDependents(vueMod: any) {
const mod: ManifestV2 = new ManifestV2().fromReactive(vueMod);
this.disableMods([...Dependants.getDependantList(mod, this.modifiableModList), mod]);
}
async disableModExcludeDependents(vueMod: any) {
const mod: ManifestV2 = new ManifestV2().fromReactive(vueMod);
this.disableMods([mod]);
}
async disableMods(modsToDisable: ManifestV2[]) {
try {
const result = await this.performDisable([...Dependants.getDependantList(mod, this.modifiableModList), mod]);
const result = await this.performDisable(modsToDisable);
if (result instanceof R2Error) {
this.$emit('error', result);
return;
@@ -443,21 +482,25 @@ import SearchUtils from '../../utils/SearchUtils';
this.filterModList();
}
async uninstallMod(vueMod: any) {
async uninstallModWithDependents(vueMod: any) {
let mod: ManifestV2 = new ManifestV2().fromReactive(vueMod);
this.uninstallMods([...Dependants.getDependantList(mod, this.modifiableModList), mod]);
}
async uninstallModExcludeDependents(vueMod: any) {
let mod: ManifestV2 = new ManifestV2().fromReactive(vueMod);
this.uninstallMods([mod]);
}
async uninstallMods(modsToUninstall: ManifestV2[]) {
try {
for (const dependant of Dependants.getDependantList(mod, this.modifiableModList)) {
const result = await this.performUninstallMod(dependant);
for (const mod of modsToUninstall) {
const result = await this.performUninstallMod(mod);
if (result instanceof R2Error) {
this.$emit('error', result);
return;
}
}
const result = await this.performUninstallMod(mod);
if (result instanceof R2Error) {
this.$emit('error', result);
return;
}
} catch (e) {
// Failed to uninstall mod.
const err: Error = e as Error;
@@ -482,7 +525,6 @@ import SearchUtils from '../../utils/SearchUtils';
this.selectedManifestMod = new ManifestV2().fromReactive(vueMod);
this.dependencyListDisplayType = displayType;
this.showingDependencyList = true;
console.log(this.dependencyListDisplayType, this.showingDependencyList)
}
uninstallModRequireConfirmation(vueMod: any) {
@@ -497,17 +539,13 @@ import SearchUtils from '../../utils/SearchUtils';
disableModRequireConfirmation(vueMod: any) {
const mod: ManifestV2 = new ManifestV2().fromReactive(vueMod);
const enabledDependants: ManifestV2[] = [];
this.getDependantList(mod).forEach(value => {
for (const value of this.getDependantList(mod)) {
if (value.isEnabled()) {
enabledDependants.push(value);
this.showDependencyList(mod, DependencyListDisplayType.DISABLE);
return;
}
});
if (enabledDependants.length === 0) {
this.performDisable([mod]);
} else {
this.showDependencyList(mod, DependencyListDisplayType.DISABLE);
}
this.performDisable([mod]);
}
viewDependencyList(vueMod: any) {
9 changes: 8 additions & 1 deletion src/components/views/OnlineModList.vue
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
<div>
<ExpandableCard
v-for='(key, index) in pagedModList' :key="`online-${key.getFullName()}-${index}-${settings.getContext().global.expandedCards}`"
:image="key.getVersions()[0].getIcon()"
:image="getImageUrl(key)"
:id="index"
:description="key.getVersions()[0].getDescription()"
:funkyMode="funkyMode"
@@ -70,6 +70,7 @@ import DownloadModModal from './DownloadModModal.vue';
import ManifestV2 from '../../model/ManifestV2';
import R2Error from '../../model/errors/R2Error';
import DonateButton from '../../components/buttons/DonateButton.vue';
import CdnProvider from '../../providers/generic/connection/CdnProvider';
@Component({
components: {
@@ -122,6 +123,12 @@ export default class OnlineModList extends Vue {
return mod.getCategories().join(", ");
}
getImageUrl(tsMod: ThunderstoreMod): string {
return CdnProvider.replaceCdnHost(
tsMod.getVersions()[0].getIcon()
);
}
emitError(error: R2Error) {
this.$emit('error', error);
}
19 changes: 9 additions & 10 deletions src/components/views/OnlineModView.vue
Original file line number Diff line number Diff line change
@@ -59,15 +59,12 @@
</p>
</div>
<br/>
<div class="pagination">
<div class="smaller-font">
<a v-for="index in getPaginationSize()" :key="`pagination-${index}`"
:class="['pagination-link', {'is-current': index === pageNumber}]"
@click="updatePageNumber(index)">
{{index}}
</a>
</div>
</div>
<PaginationButtons
:current-page="pageNumber"
:page-count="getPaginationSize()"
:context-size="3"
:on-click="updatePageNumber"
/>
</div>
</template>

@@ -84,15 +81,17 @@ import OnlineModListProvider from '../../providers/components/loaders/OnlineModL
import ArrayUtils from '../../utils/ArrayUtils';
import debounce from 'lodash.debounce';
import SearchUtils from '../../utils/SearchUtils';
import PaginationButtons from "../navigation/PaginationButtons.vue";
@Component({
components: {
OnlineModList: OnlineModListProvider.provider,
PaginationButtons,
}
})
export default class OnlineModView extends Vue {
readonly pageSize = 140;
readonly pageSize = 40;
pagedThunderstoreModList: ThunderstoreMod[] = [];
pageNumber = 1;
searchableThunderstoreModList: ThunderstoreMod[] = [];
5 changes: 3 additions & 2 deletions src/model/ThunderstoreVersion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Mod from './Mod';
import VersionNumber from './VersionNumber';
import ReactiveObjectConverterInterface from './safety/ReactiveObjectConverter';
import CdnProvider from '../providers/generic/connection/CdnProvider';

export default class ThunderstoreVersion extends Mod implements ReactiveObjectConverterInterface {

@@ -35,10 +36,10 @@ export default class ThunderstoreVersion extends Mod implements ReactiveObjectCo
}

public getDownloadUrl(): string {
return this.downloadUrl;
return CdnProvider.addCdnQueryParameter(this.downloadUrl);
}

public setDownloadUrl(url: string) {
this.downloadUrl = url;
}
}
}
18 changes: 18 additions & 0 deletions src/model/game/GameManager.ts
Original file line number Diff line number Diff line change
@@ -540,6 +540,24 @@ export default class GameManager {
"https://thunderstore.io/c/lethal-company/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.STEAM, "1966720")], "LethalCompany.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []),

new Game("Meeple Station", "MeepleStation", "MeepleStation",
"Meeple Station", ["Meeple Station.exe"], "MeepleStation_Data",
"https://thunderstore.io/c/meeple-station/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.STEAM, "900010")], "MeepleStation.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []),

new Game("Void Crew", "VoidCrew", "VoidCrew",
"Void Crew", ["Void Crew.exe"], "VoidCrew_Data",
"https://thunderstore.io/c/void-crew/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.STEAM, "1063420")], "VoidCrew.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []),

new Game("Sailwind", "Sailwind", "Sailwind",
"Sailwind", ["Sailwind.exe"], "Sailwind_Data",
"https://thunderstore.io/c/sailwind/api/v1/package/", "https://raw.githubusercontent.com/ebkr/r2modmanPlus/master/modExclusions.md",
[new StorePlatformMetadata(StorePlatform.STEAM, "1764530")], "Sailwind.png",
GameSelectionDisplayMode.VISIBLE, GameInstanceType.GAME, PackageLoader.BEPINEX, []),
];

static get activeGame(): Game {
63 changes: 63 additions & 0 deletions src/providers/generic/connection/CdnProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import R2Error from '../../../model/errors/R2Error';
import { getAxiosWithTimeouts } from '../../../utils/HttpUtils';
import { addOrReplaceSearchParams, replaceHost } from '../../../utils/UrlUtils';

const CDNS = [
"gcdn.thunderstore.io",
"hcdn-1.hcdn.thunderstore.io"
]
const TEST_FILE = "healthz";

const CONNECTION_ERROR = new R2Error(
"Can't reach content delivery networks",
`All Thunderstore CDNs seem to be currently unreachable from
this computer. You can still use the mod manager, but
downloading mods will not work.`,
`Test another internet connection, if available. For example
using a VPN or connecting to a mobile hotspot might solve the
issue.`
);

export default class CdnProvider {
private static axios = getAxiosWithTimeouts(5000, 5000);
private static preferredCdn = "";

public static async checkCdnConnection() {
const headers = {
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Expires": "0",
};
const params = {"disableCache": new Date().getTime()};
let res;

for await (const cdn of CDNS) {
const url = `https://${cdn}/${TEST_FILE}`;

try {
res = await CdnProvider.axios.get(url, {headers, params});
} catch (e) {
continue;
}

if (res.status === 200) {
CdnProvider.preferredCdn = cdn;
return;
}
};

throw CONNECTION_ERROR;
}

public static replaceCdnHost(url: string) {
return CdnProvider.preferredCdn
? replaceHost(url, CdnProvider.preferredCdn)
: url;
}

public static addCdnQueryParameter(url: string) {
return CdnProvider.preferredCdn
? addOrReplaceSearchParams(url, `cdn=${CdnProvider.preferredCdn}`)
: url;
}
}
Original file line number Diff line number Diff line change
@@ -116,6 +116,9 @@ export default class InstallationRuleApplicator {
buildBepInExRules("Dredge"),
buildBepInExRules("CitiesSkylines2"),
buildBepInExRules("LethalCompany"),
buildBepInExRules("MeepleStation"),
buildBepInExRules("VoidCrew"),
buildBepInExRules("Sailwind"),
]
}
}
Original file line number Diff line number Diff line change
@@ -156,6 +156,9 @@ const VARIANTS = {
Dredge: MODLOADER_PACKAGES,
CitiesSkylines2: MODLOADER_PACKAGES,
LethalCompany: MODLOADER_PACKAGES,
MeepleStation: MODLOADER_PACKAGES,
VoidCrew: MODLOADER_PACKAGES,
Sailwind: MODLOADER_PACKAGES,
};
// Exported separately from the definition in order to preserve the key names in the type definition.
// Otherwise this would become [key: string] and we couldn't use the game names for type hinting elsewhere.
2 changes: 1 addition & 1 deletion src/r2mm/manager/SettingsDexieStore.ts
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ export default class SettingsDexieStore extends Dexie {

// Add all games to store. Borked v2-3 locally
// Increment per game or change to settings.
this.version(64).stores(store);
this.version(65).stores(store);

this.activeGame = game;
this.global = this.table("value");
6 changes: 5 additions & 1 deletion src/r2mm/profiles/ProfilesClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Axios, { AxiosResponse } from 'axios';
import R2Error from '../../model/errors/R2Error';
import CdnProvider from '../../providers/generic/connection/CdnProvider';

const getProfileUrl = (profileImportCode: string): string => {
return `https://thunderstore.io/api/experimental/legacyprofile/get/${profileImportCode}/`;
@@ -40,7 +41,10 @@ async function createProfile(payload: string): Promise<AxiosResponse<{ key: stri
}

async function getProfile(profileImportCode: string): Promise<AxiosResponse<string>> {
return await Axios.get(getProfileUrl(profileImportCode));
const url = CdnProvider.addCdnQueryParameter(
getProfileUrl(profileImportCode)
);
return await Axios.get(url);
}

export const ProfileApiClient = {
38 changes: 38 additions & 0 deletions src/utils/HttpUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import axios from "axios";

const newAbortSignal = (timeoutMs: number) => {
const abortController = new AbortController();
setTimeout(() => abortController.abort(), timeoutMs);
return abortController.signal;
};

/**
* Return Axios instance with timeouts enabled.
* @param responseTimeout Time (in ms) the server has to generate a
* response once a connection is established. Defaults to 5 seconds.
* @param connectionTimeout Time (in ms) the request has in total,
* including opening the connection and receiving the response.
* Defaults to 10 seconds.
* @returns AxiosInstance
*/
export const getAxiosWithTimeouts = (responseTimeout = 5000, connectionTimeout = 10000) => {
const instance = axios.create({timeout: responseTimeout});

// Use interceptors to have a fresh abort signal for each request,
// so the instance can be shared by multiple requests.
instance.interceptors.request.use((config) => {
config.signal = newAbortSignal(connectionTimeout);
return config;
});

return instance;
};

export const isNetworkError = (responseOrError: unknown) =>
responseOrError instanceof Error && responseOrError.message === "Network Error";

/**
* Is the Error thrown by Axios request caused by a response timeout?
*/
export const isResponseTimeout = (error: unknown) =>
error instanceof Error && /timeout of (\d+)ms exceeded/i.test(error.message);
54 changes: 54 additions & 0 deletions src/utils/Pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@

export type PaginationButton = {
index: number;
title: string;
}

export function truncatePagination(props: {
currentPage: number,
pageCount: number,
contextSize: number,
}): PaginationButton[] {
const result = [];

if (props.currentPage - props.contextSize > 1) {
result.push({
index: 1,
title: "1"
});
}
if (props.currentPage - props.contextSize - 1 > 1) {
result.push({
index: props.currentPage - props.contextSize - 1,
title: "...",
});
}

for (
let i = props.currentPage - props.contextSize;
i <= props.currentPage + props.contextSize;
i++
) {
if (i > 0 && i <= props.pageCount) {
result.push({
index: i,
title: i.toString(),
});
}
}

if (props.currentPage + props.contextSize + 1 < props.pageCount) {
result.push({
index: props.currentPage + props.contextSize + 1,
title: "...",
});
}
if (props.currentPage + props.contextSize < props.pageCount) {
result.push({
index: props.pageCount,
title: props.pageCount.toString(),
});
}

return result;
}
33 changes: 33 additions & 0 deletions src/utils/UrlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Append given search parameters to an URL which may or may not already
* have search parameters.
*
* Existing search parameters with a key present in the new parameters
* will be overwritten.
*/
export const addOrReplaceSearchParams = (url: string, paramString: string) => {
const newUrl = new URL(url);
newUrl.search = new URLSearchParams(
Object.assign(
{},
Object.fromEntries(newUrl.searchParams),
Object.fromEntries(new URLSearchParams(paramString))
)
).toString();
return newUrl.href;
}

/**
* Replace URL host, i.e. the domain and the port number.
*
* @param url e.g. "https://thunderstore.io/foo"
* @param domainAndPort e.g. "thunderstore.dev" or "thunderstore.dev:8080"
* @returns e.g. "https://thunderstore.dev:8080/foo"
*/
export const replaceHost = (url: string, domainAndPort: string) => {
const newValues = domainAndPort.split(":");
const newUrl = new URL(url);
newUrl.hostname = newValues[0];
newUrl.port = newValues[1] || "";
return newUrl.href;
};

0 comments on commit 01bf512

Please sign in to comment.