Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion quasar.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file

import path from 'node:path';
import { defineConfig } from '#q-app/wrappers';

export default defineConfig((ctx) => {
Expand Down Expand Up @@ -47,7 +48,13 @@ export default defineConfig((ctx) => {
typescript: {
strict: true,
vueShim: true,
// extendTsConfig (tsConfig) {}
extendTsConfig(tsConfig) {
tsConfig.compilerOptions.paths = {
'@r2': ['../src'],
'@r2/*': ['../src/*'],
...tsConfig.compilerOptions.paths,
};
}
},

vueRouterMode: 'history', // available values: 'hash', 'history'
Expand All @@ -71,6 +78,11 @@ export default defineConfig((ctx) => {
extendViteConf (viteConf) {
// Force Vite to use esbuild for CSS, overriding any defaults
viteConf.build!.cssMinify = 'esbuild';

// Allow @r2 alias
viteConf.resolve ??= {};
viteConf.resolve.alias ??= {};
(viteConf.resolve.alias as Record<string, string>)['@r2'] = path.resolve(__dirname, 'src');
},
viteVuePluginOptions: {
template: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ExpandableCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</a>
</header>
</div>
<div class='mod-card-content' v-show='visible' v-if="description !== ''">
<div class='mod-card-content' v-show='visible' v-if="description !== '' || $slots.description">
<div class='content'>
<p ref="description">{{description}}</p>
<slot name='description'></slot>
Expand Down
30 changes: 30 additions & 0 deletions src/components/banner/ConcerningPackageBanner.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">

import { getStore } from '@r2/providers/generic/store/StoreProvider';
import { State } from '@r2/store';
import { useConcerningPackageComposable } from '@r2/components/composables/ConcerningPackageComposable';

const store = getStore<State>();
const { hasConcerningPackages } = useConcerningPackageComposable();

function addUnlinkedFilter() {
store.commit('profile/scopeLocalModListToUnlinkedPackages');
}
</script>

<template>
<div class="notification is-concern margin-right" v-show="hasConcerningPackages">
<span>You have mods that can no longer be found on Thunderstore.</span> <a href="#" @click.stop.prevent="addUnlinkedFilter">Click here to review packages.</a>
</div>
</template>

<style scoped lang="scss">
.is-concern {
background-color: var(--notification-concern-background-color);
color: var(--notification-concern-text-color);
}

.notification {
margin-bottom: 0.5rem;
}
</style>
20 changes: 12 additions & 8 deletions src/components/banner/ManagerUpdateBanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@ onMounted(async () => {

<template>
<div class='notification margin-top margin-right' v-show="portableUpdateAvailable">
<div class='container'>
<p>
An {{ appName }} update is available.
<ExternalLink :url="`https://github.com/ebkr/r2modmanPlus/releases/tag/${updateTagName}`">
Click here to go to the release page.
</ExternalLink>
</p>
</div>
<p>
An {{ appName }} update is available.
<ExternalLink :url="`https://github.com/ebkr/r2modmanPlus/releases/tag/${updateTagName}`">
Click here to go to the release page.
</ExternalLink>
</p>
</div>
</template>

<style scoped lang="scss">
.notification {
margin-bottom: 0.5rem;
}
</style>
93 changes: 93 additions & 0 deletions src/components/composables/ConcerningPackageComposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Definitions are outside the composable function so that calculations are shared.
* We do not need to recalculate on a per-usage basis.
*/
import { getStore } from '@r2/providers/generic/store/StoreProvider';
import { computed, onMounted, ref, watch } from 'vue';
import ManifestV2 from '@r2/model/ManifestV2';
import ThunderstoreMod from '@r2/model/ThunderstoreMod';
import VersionNumber from '@r2/model/VersionNumber';
import * as PackageDb from "@r2/r2mm/manager/PackageDexieStore";

type ConcerningPackage = {
fullName: string;
latestVersion: string;
}

const store = getStore<any>();

const activeGame = computed(() => store.state.activeGame);

const localModList = computed<ManifestV2[]>(() => store.state.profile.modList);
const onlineModList = computed<Map<string, ConcerningPackage>>(() => {
const mods: ThunderstoreMod[] = store.state.tsMods.mods;
return new Map<string, ConcerningPackage>(mods.map(value => [value.getFullName(), {
fullName: value.getFullName(),
latestVersion: value.getLatestVersion(),
}]));
});

const allConcerningPackages = ref<ManifestV2[]>([]);
const activeConcerningPackages = ref<ManifestV2[]>([]);

async function updateConcerningPackages() {
const game = activeGame.value;
const localMods = localModList.value;
const concerningPackages: ManifestV2[] = [];
const modsToCheck: ManifestV2[] = [];

for (const mod of localMods) {
if (!mod.isOnlineSource()) {
continue;
}
if (!onlineModList.value.has(mod.getName())) {
concerningPackages.push(mod);
} else {
modsToCheck.push(mod);
}
}

const versionNumbersBatch = await PackageDb.getPackageVersionNumbersBatch(
game.internalFolderName,
modsToCheck.map(mod => mod.getName())
);

for (const mod of modsToCheck) {
const versions = versionNumbersBatch.get(mod.getName());
const dnf = !versions || !versions.includes(mod.getVersionNumber().toString());
if (dnf) {
concerningPackages.push(mod);
}
}

allConcerningPackages.value = concerningPackages;
activeConcerningPackages.value = concerningPackages.filter(value => !value.isTrustedPackage());
}

watch([activeGame, localModList], async () => {
await updateConcerningPackages();
});

export function useConcerningPackageComposable() {

onMounted(async () => {
await updateConcerningPackages();
});

const hasConcerningPackages = computed<boolean>(() => activeConcerningPackages.value.length > 0);

function isConcerningPackage(mod: ManifestV2) {
return activeConcerningPackages.value.findIndex(value => value.getName() === mod.getName()) >= 0;
}

function wasConcerningPackage(mod: ManifestV2) {
return allConcerningPackages.value.findIndex(value => value.getName() === mod.getName()) >= 0;
}

return {
concerningPackages: activeConcerningPackages,
hasConcerningPackages,
isConcerningPackage,
wasConcerningPackage,
}
}
35 changes: 35 additions & 0 deletions src/components/composables/ModManagementComposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getStore } from '@r2/providers/generic/store/StoreProvider';
import Dependants from '@r2/r2mm/mods/Dependants';
import R2Error from '@r2/model/errors/R2Error';
import { LogSeverity } from '@r2/providers/ror2/logging/LoggerProvider';
import ManifestV2 from '@r2/model/ManifestV2';

const store = getStore<any>();

export function useModManagementComposable() {

async function uninstallMod(mod: ManifestV2) {
const dependants = Dependants.getDependantList(mod, store.state.profile.modList);

if (dependants.size > 0) {
store.commit('openUninstallModModal', mod);
return;
}

try {
await store.dispatch(
'profile/uninstallModsFromActiveProfile',
{ mods: [mod] }
);
} catch (e) {
store.commit('error/handleError', {
error: R2Error.fromThrownValue(e),
severity: LogSeverity.ACTION_STOPPED
});
}
}

return {
uninstallMod
}
}
81 changes: 81 additions & 0 deletions src/components/modals/ConcerningPackageReviewModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { computed } from 'vue';
import ModalCard from '@r2/components/ModalCard.vue';
import { State } from '@r2/store';
import { getStore } from '@r2/providers/generic/store/StoreProvider';
import ManifestV2 from '@r2/model/ManifestV2';
import { useModManagementComposable } from '@r2/components/composables/ModManagementComposable';
import R2Error from '@r2/model/errors/R2Error';
import ProfileModList from '@r2/r2mm/mods/ProfileModList';

const store = getStore<State>();

const { uninstallMod } = useModManagementComposable();

const isOpen = computed(() => store.state.modals.isConcerningModReviewModalOpen);
const modToReview = computed<ManifestV2 | null>(() => store.state.modals.concerningModToReview);
const profile = computed(() => store.getters['profile/activeProfile']);

function close() {
store.commit('closeConcerningModReviewModal');
}

async function removeMod() {
await uninstallMod(modToReview.value!);
close();
}

async function trustPackage() {
const mods = await ProfileModList.getModList(profile.value.asImmutableProfile());
if (mods instanceof R2Error) {
console.error(mods);
store.commit('error/handleError', mods);
return;
}
const mod = mods.find(value => value.getName() === modToReview.value?.getName());
if (mod) {
mod.setTrustedPackage(true);
}
try {
const err = await ProfileModList.saveModList(profile.value.asImmutableProfile(), mods);
if (err instanceof R2Error) {
store.commit('error/handleError', err);
return;
}
await store.dispatch('profile/updateModList', mods);
} catch (e) {
store.commit('error/handleError', R2Error.fromThrownValue(e));
} finally {
close();
}
}
</script>

<template>
<ModalCard id="review-package-modal" v-if="isOpen && modToReview" :is-active="isOpen" :can-close="true" @close-modal="close">
<template v-slot:header>
<h2 class="modal-title">Review {{ modToReview.getName() }}</h2>
</template>
<template v-slot:body>
<p class="notification is-warning">It is generally recommended to remove mods that have been removed from Thunderstore.</p>
<div>
<hr/>
<p class="margin-bottom">This mod was originally downloaded from Thunderstore, but can no longer be found on the site.</p>
<p class="margin-bottom">Mods may be removed at the author's request, for rule violations, or while undergoing verification by moderators.</p>
<p>Other people will be unable to import this mod from exported profiles.</p>
</div>
</template>
<template v-slot:footer>
<button class="button" @click.stop.prevent="trustPackage">
Mark version as safe
</button>
<button class="button is-danger" @click.stop.prevent="removeMod">
Remove mod
</button>
</template>
</ModalCard>
</template>

<style scoped lang="scss">

</style>
20 changes: 19 additions & 1 deletion src/components/views/LocalModList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@
<div class="search-and-sort">
<SearchAndSort />
</div>

<DisableModModal />
<UninstallModModal />
<AssociatedModsModal />

<ManagerUpdateBanner/>
<ConcerningPackageBanner/>

<slot name="above-list"></slot>

<div class="tags has-addons" v-if="filters.size > 0">
<span class="margin-right" v-for="filter in filters">
<a href="#" @click="removeFilter(filter)">
<div class="tag has-addons">
<span>{{ filter }}</span>
</div>
<span class="tag is-delete">&nbsp;</span>
</a>
</span>
</div>

<div class="mod-list-content">
<div class="draggable-content">
<Suspense>
Expand All @@ -37,12 +50,18 @@ import { State } from '../../store';
import { computed, defineAsyncComponent } from 'vue';
import SkeletonLocalModCard from './LocalModList/SkeletonLocalModCard.vue';
import ManagerUpdateBanner from '../banner/ManagerUpdateBanner.vue';
import ConcerningPackageBanner from '@r2/components/banner/ConcerningPackageBanner.vue';

const store = getStore<State>();

const LocalModDraggableList = defineAsyncComponent(() => import('./LocalModList/LocalModDraggableList.vue'));

const visibleModList = computed(() => store.getters['profile/visibleModList']);
const filters = computed(() => store.state.profile.filters);

function removeFilter(filter: string) {
store.commit('profile/removeFilter', filter);
}
</script>

<style lang="scss" scoped>
Expand All @@ -54,7 +73,6 @@ const visibleModList = computed(() => store.getters['profile/visibleModList']);
.mod-list-content {
flex: 1;
overflow-y: auto;
padding-right: 1rem;
}
}
</style>
Loading
Loading