Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Profile import summary #1430

Merged
merged 2 commits into from
Sep 11, 2024
Merged
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
8 changes: 5 additions & 3 deletions src/components/ModalCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
<header class="modal-card-head">
<slot name="header"/>
</header>
<section class="modal-card-body">
<slot name="body"/>
</section>
<template v-if="!!$slots.body">
<section class="modal-card-body">
<slot name="body"/>
</section>
</template>
<footer class="modal-card-foot">
<slot name="footer"/>
</footer>
Expand Down
152 changes: 96 additions & 56 deletions src/components/profiles-modals/ImportProfileModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import ZipProvider from "../../providers/generic/zip/ZipProvider";
import ThunderstoreMod from "../../model/ThunderstoreMod";
import ThunderstoreVersion from "../../model/ThunderstoreVersion";
import ManagerInformation from "../../_managerinf/ManagerInformation";
import OnlineModList from 'components/views/OnlineModList.vue';

let fs: FsProvider;


@Component({
components: {ModalCard}
components: { OnlineModList, ModalCard}
})
export default class ImportProfileModal extends mixins(ProfilesMixin) {
private importUpdateSelection: "IMPORT" | "UPDATE" | null = null;
Expand All @@ -39,7 +40,9 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
private isProfileBeingImported: boolean = false;
private listenerId: number = 0;
private newProfileName: string = '';
private activeStep: 'IMPORT_UPDATE_SELECTION' | 'FILE_SELECTION' | 'FILE_CODE_SELECTION' | 'IMPORT_CODE' | 'IMPORT_FILE' | 'ADDING_PROFILE' = 'IMPORT_UPDATE_SELECTION';
private profileImportFilePath: string | null = null;
private profileImportContent: ExportFormat | null = null;
private activeStep: 'IMPORT_UPDATE_SELECTION' | 'FILE_SELECTION' | 'FILE_CODE_SELECTION' | 'IMPORT_CODE' | 'IMPORT_FILE' | 'ADDING_PROFILE' | 'REVIEW_IMPORT' | 'NO_PACKAGES_IN_IMPORT' = 'IMPORT_UPDATE_SELECTION';



Expand All @@ -65,12 +68,23 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
}
}

get profileToOnlineMods() {
if (!this.profileImportContent) {
return [];
}
return this.profileImportContent.getMods()
.map(mod => this.$store.getters['tsMods/tsMod'](mod))
.filter(mod => !!mod)
}

closeModal() {
this.activeStep = 'IMPORT_UPDATE_SELECTION';
this.listenerId = 0;
this.newProfileName = '';
this.importUpdateSelection = null;
this.profileImportCode = '';
this.profileImportFilePath = null;
this.profileImportContent = null;
this.$store.commit('closeImportProfileModal');
}

Expand All @@ -92,7 +106,7 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
title: 'Import Profile',
filters: [{
name: "*",
extensions: ["r2z", "r2x"]
extensions: ["r2z"]
}],
buttonLabel: 'Import'
}).then((value: string[]) => {
Expand All @@ -103,42 +117,48 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
})
}

// When creating, the flow is:
// 1. Reads and parses the mods to be installed
// 2. Creates an EventListener which gets triggered after the user has created a new profile. The callback then:
// 1. Downloads (and extracts) the mods to the profile
// When updating, the flow is:
// 1. Reads and parses the mods to be installed
// 2. Creates an EventListener which gets triggered after the user has selected the old profile to be updated. The callback then:
// 1. Deletes the temporary profile (called _profile_update) if it exists (leftover from previous failed run etc.)
// 2. Downloads (and extracts) the mods to temporary profile
// 3. Deletes the old profile (including the folder on disk)
// 4. Renames temporary profile to the chosen profile's name
async importProfileHandler(files: string[] | null) {
if (files === null || files.length === 0) {
this.isProfileBeingImported = false;
return;
}

if (files[0].endsWith(".json")) {
return await this.importAlternativeManagerProfile(files[0]);
}

let read: string | null = await this.readProfileFile(files[0]);

if (read !== null) {
const parsed: ExportFormat = await this.parseYamlToExportFormat(read);

const localListenerId = this.listenerId + 1;
this.listenerId = localListenerId;
document.addEventListener('created-profile', ((event: CustomEvent) =>
ebkr marked this conversation as resolved.
Show resolved Hide resolved
this.profileCreatedCallback(event, localListenerId, parsed, files)
) as EventListener, {once: true});
this.newProfileName = parsed.getProfileName();
this.activeStep = 'ADDING_PROFILE';
this.profileImportFilePath = files[0];
this.profileImportContent = await this.parseYamlToExportFormat(read);

if (this.profileToOnlineMods.length === 0) {
this.activeStep = 'NO_PACKAGES_IN_IMPORT';
return;
}

this.activeStep = 'REVIEW_IMPORT';
}
}

// When creating, the flow is:
// 1. Creates an EventListener which gets triggered after the user has created a new profile. The callback then:
// 2. Downloads (and extracts) the mods to the profile
//
// When updating, the flow is:
// 1. Creates an EventListener which gets triggered after the user has selected the old profile to be updated. The callback then:
// 2. Deletes the temporary profile (called _profile_update) if it exists (leftover from previous failed run etc.)
// 3. Downloads (and extracts) the mods to temporary profile
// 4. Deletes the old profile (including the folder on disk)
// 5. Renames temporary profile to the chosen profile's name
async installProfileHandler() {
const profileContent = this.profileImportContent;
const localListenerId = this.listenerId + 1;
this.listenerId = localListenerId;
document.addEventListener('created-profile', ((event: CustomEvent) =>
this.profileCreatedCallback(event, localListenerId, profileContent!, [this.profileImportFilePath!])
) as EventListener, {once: true});
this.newProfileName = profileContent!.getProfileName();
this.activeStep = 'ADDING_PROFILE';
}

async installModAfterDownload(mod: ThunderstoreMod, version: ThunderstoreVersion): Promise<R2Error | ManifestV2> {
const manifestMod: ManifestV2 = new ManifestV2().fromThunderstoreMod(mod, version);
const installError: R2Error | null = await ProfileInstallerProvider.instance.installMod(manifestMod, this.activeProfile);
Expand Down Expand Up @@ -168,29 +188,7 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
return read;
}

async importAlternativeManagerProfile(file: string) {
try {
const fileString = (await FsProvider.instance.readFile(file)).toString();
const jsonContent = JSON.parse(fileString.trim());
const ror2Itf = jsonContent as Itf_RoR2MM;
if (ror2Itf.name != undefined && ror2Itf.packages != undefined) {
this.newProfileName = ror2Itf.name;
this.activeStep = 'ADDING_PROFILE';
const itfPackages: string[] = ror2Itf.packages;
document.addEventListener("created-profile", ((() => {
const packages = itfPackages.map(value => ExportMod.fromFullString(value));
setTimeout(() => {
this.downloadImportedProfileMods(packages);
}, 100);
}) as EventListener), {once: true});
}
} catch (e) {
const err = R2Error.fromThrownValue(e, 'Failed to import profile');
this.$store.commit('error/handleError', err);
}
}

profileCreatedCallback(event: CustomEvent, localListenerId: number, parsed: { getMods: () => ExportMod[]; }, files: string[]) {
profileCreatedCallback(event: CustomEvent, localListenerId: number, parsed: ExportFormat, files: string[]) {
if (this.listenerId === localListenerId) {
(async () => {
let profileName: string = event.detail;
Expand Down Expand Up @@ -351,10 +349,11 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
}

</script>

<template>
<ModalCard v-if="activeStep === 'IMPORT_UPDATE_SELECTION'" key="IMPORT_UPDATE_SELECTION" :is-active="isOpen" @close-modal="closeModal">
<template v-slot:header>
<p>Are you going to be updating an existing profile or creating a new one?</p>
<h2 class="modal-title">Are you going to be updating an existing profile or creating a new one?</h2>
</template>
<template v-slot:footer>
<button id="modal-import-new-profile"
Expand All @@ -372,8 +371,8 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {

<ModalCard v-else-if="activeStep === 'FILE_CODE_SELECTION'" key="FILE_CODE_SELECTION" :is-active="isOpen" @close-modal="closeModal">
<template v-slot:header>
<p class="card-header-title" v-if="importUpdateSelection === 'IMPORT'">How are you importing a profile?</p>
<p class="card-header-title" v-if="importUpdateSelection === 'UPDATE'">How are you updating your profile?</p>
<h2 class="modal-title" v-if="importUpdateSelection === 'IMPORT'">How are you importing a profile?</h2>
<h2 class="modal-title" v-if="importUpdateSelection === 'UPDATE'">How are you updating your profile?</h2>
</template>
<template v-slot:footer>
<button id="modal-import-profile-file"
Expand All @@ -387,7 +386,7 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {

<ModalCard v-else-if="activeStep === 'IMPORT_CODE'" key="IMPORT_CODE" :is-active="isOpen" @close-modal="closeModal">
<template v-slot:header>
<p class="card-header-title">Enter the profile code</p>
<h2 class="modal-title">Enter the profile code</h2>
</template>
<template v-slot:body>
<input
Expand All @@ -414,14 +413,49 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
class="button is-info"
@click="importProfileUsingCode();"
v-else>
Continue
</button>
</template>
</ModalCard>

<ModalCard v-else-if="activeStep === 'REVIEW_IMPORT'" key="REVIEW_IMPORT" :is-active="isOpen" @close-modal="closeModal">
<template v-slot:header>
<h2 class="modal-title">Packages to be installed</h2>
</template>
<template v-slot:body>
<OnlineModList :paged-mod-list="profileToOnlineMods" :read-only="true" />
</template>
<template v-slot:footer>
<button
id="modal-import-profile-from-code-invalid"
class="button is-info"
@click="installProfileHandler();">
Import
</button>
</template>
</ModalCard>

<ModalCard v-else-if="activeStep === 'NO_PACKAGES_IN_IMPORT'" key="NO_PACKAGES_IN_IMPORT" :is-active="isOpen" @close-modal="closeModal">
<template v-slot:header>
<h2 class="modal-title">There was a problem importing the profile</h2>
</template>
<template v-slot:body>
<p>None of the packages inside the export were found on Thunderstore.</p>
<p>There is nothing to import.</p>
</template>
<template v-slot:footer>
<button
id="modal-import-profile-from-code-invalid"
class="button is-info"
@click="closeModal">
Close
</button>
</template>
</ModalCard>

<ModalCard v-else-if="isProfileBeingImported" key="PROFILE_IS_BEING_IMPORTED" :is-active="isOpen" :canClose="false">
<template v-slot:header>
<p>{{percentageImported}}% imported</p>
<h2 class="modal-title">{{percentageImported}}% imported</h2>
</template>
<template v-slot:body>
<p>This may take a while, as mods are being downloaded.</p>
Expand All @@ -431,7 +465,7 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {

<ModalCard v-else-if="activeStep === 'ADDING_PROFILE'" key="ADDING_PROFILE" :is-active="isOpen" @close-modal="closeModal">
<template v-slot:header>
<p v-if="importUpdateSelection === 'IMPORT'" class="card-header-title">Import a profile</p>
<h2 v-if="importUpdateSelection === 'IMPORT'" class="modal-title">Import a profile</h2>
</template>
<template v-slot:body v-if="importUpdateSelection === 'IMPORT'">
<p>This profile will store its own mods independently from other profiles.</p>
Expand Down Expand Up @@ -475,3 +509,9 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
</template>
</ModalCard>
</template>

<style scoped>
.modal-title {
font-size: 1.5rem;
}
</style>
11 changes: 7 additions & 4 deletions src/components/views/OnlineModList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,24 @@
</span>
</template>
<template v-slot:other-icons>
<span class='card-header-icon' v-if="key.getDonationLink()">
<span class='card-header-icon' v-if="key.getDonationLink() && !readOnly">
<Link :url="key.getDonationLink()" target="external" tag="span">
<i class='fas fa-heart' v-tooltip.left="'Donate to the mod author'"></i>
</Link>
</span>
<span class='card-header-icon' v-if="isThunderstoreModInstalled(key)">
<span class='card-header-icon' v-if="isThunderstoreModInstalled(key) && !readOnly">
<i class='fas fa-check' v-tooltip.left="'Mod already installed'"></i>
</span>
</template>
<template v-slot:description>
<p class='card-timestamp'><strong>Last updated:</strong> {{getReadableDate(key.getDateUpdated())}}</p>
<p class='card-timestamp'><strong>Categories:</strong> {{getReadableCategories(key)}}</p>
</template>
<a class='card-footer-item' @click='showDownloadModal(key)'>Download</a>
<a class='card-footer-item' v-if="!readOnly" @click='showDownloadModal(key)'>Download</a>
<Link :url="key.getPackageUrl()" :target="'external'" class='card-footer-item'>
Website <i class="fas fa-external-link-alt margin-left margin-left--half-width"></i>
</Link>
<template v-if="key.getDonationLink() !== undefined">
<template v-if="key.getDonationLink() !== undefined && !readOnly">
<DonateButton :mod="key"/>
</template>
<div class='card-footer-item non-selectable'>
Expand Down Expand Up @@ -82,6 +82,9 @@ export default class OnlineModList extends Vue {
@Prop()
pagedModList!: ThunderstoreMod[];

@Prop({default: false})
readOnly!: boolean;

private cardExpanded: boolean = false;
private funkyMode: boolean = false;

Expand Down
8 changes: 8 additions & 0 deletions src/css/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,11 @@ code {
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.15);
}

.modal-title {
font-size: 1.5rem;
font-weight: bold;
margin: 0;
padding: 0;
line-height: 2rem;
}
Loading