Skip to content

Commit

Permalink
v2.8.0
Browse files Browse the repository at this point in the history
- Added support for importing partial playlists via the Moulinette importer (and associated zip importer).
  - Allows creators to export individual sounds from a playlist and have it be merged into an existing playlist on import.
  - This can be helpful for a creator who wants to release individual sounds, but have a single playlist across their module releases.
- Moulinette importer (and associated zip importer) will now prompt to reload the world after completing the import.
  - This is to handle some edge cases where Foundry VTT doesn't correctly display the imported data.
- Fix the `Relink compendium entries` macro to correctly handle journal pages stored in markdown format.
  - Fixes #147
  • Loading branch information
sneat authored Oct 28, 2024
1 parent b772052 commit 7fcc237
Show file tree
Hide file tree
Showing 13 changed files with 269 additions and 50 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## v2.8.0
- Added support for importing partial playlists via the Moulinette importer (and associated zip importer).
- Allows creators to export individual sounds from a playlist and have it be merged into an existing playlist on import.
- This can be helpful for a creator who wants to release individual sounds, but have a single playlist across their module releases.
- Moulinette importer (and associated zip importer) will now prompt to reload the world after completing the import.
- This is to handle some edge cases where Foundry VTT doesn't correctly display the imported data.
- Fix the `Relink compendium entries` macro to correctly handle journal pages stored in markdown format.
- Fixes #147

## v2.7.15
- Added a button in the settings to more easily re-prompt for a module's automatic importer.
- This functionality always existed via a macro. This button just makes it easier to find.
Expand Down
1 change: 1 addition & 0 deletions languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@
"converting-reference": "Converting {type} reference in \"{name}\" {oldRef} -> {newRef} at location: {path}",
"no-existing-value": "Could not find existing value at path \"{path}\". Skipping updating reference…",
"complete": "Complete!",
"complete-content": "Import complete. It is strongly recommended to reload your world. Click the button below to refresh.",
"import-all": "Import all {count} entities in this pack",
"import-one": "Import selected",
"selected-entity": "Selected {type}:",
Expand Down
1 change: 1 addition & 0 deletions languages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
"converting-reference": "Convertit la référence {type} dans \"{name}\" {oldRef} -> {newRef} dans l'emplacement : {path}",
"no-existing-value": "Impossible de trouver une valeur existante dans le chemin \"{path}\". Passe la mise à jour de la référence…",
"complete": "Achevé !",
"complete-content": "Importation terminée. Il est fortement recommandé de recharger votre monde. Cliquez sur le bouton ci-dessous pour actualiser.",
"import-all": "Importer toutes les {count} entités dans ce pack",
"import-one": "Importe la sélection",
"selected-entity": "Sélectionné {type} :",
Expand Down
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "scene-packer",
"title": "Library: Scene Packer",
"description": "A module to assist with Scene and Adventure packing and unpacking.",
"version": "2.7.15",
"version": "2.8.0",
"library": "true",
"manifestPlusVersion": "1.2.0",
"minimumCoreVersion": "0.8.6",
Expand Down
10 changes: 8 additions & 2 deletions scripts/assets/playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { CONSTANTS } from '../constants.js';
/**
* Extract assets from the given playlist
* @param {Playlist|ClientDocumentMixin} playlist - The playlist to extract assets from.
* @param {Set<string>} selectedSounds - The sounds in the playlist.
* @return {AssetData}
*/
export async function ExtractPlaylistAssets(playlist) {
export async function ExtractPlaylistAssets(playlist, selectedSounds) {
const data = new AssetData({
id: playlist?.id || '',
name: playlist?.name || '',
Expand All @@ -18,13 +19,18 @@ export async function ExtractPlaylistAssets(playlist) {
return data;
}

const sounds = [];
let sounds = [];

const playlistData = CONSTANTS.IsV10orNewer() ? playlist : playlist.data;
if (playlistData.sounds?.size) {
sounds.push(...Array.from(playlistData.sounds.values()));
}

if (selectedSounds.size) {
// Remove sounds that are not selected
sounds = sounds.filter(sound => selectedSounds.has(sound.id));
}

for (const sound of sounds) {
const soundData = CONSTANTS.IsV10orNewer() ? sound : sound?.data;
const path = soundData?.path || sound?.path;
Expand Down
37 changes: 29 additions & 8 deletions scripts/export-import/exporter-progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ export default class ExporterProgress extends FormApplication {
}
};

// Determine which PlaylistSounds should be included
const playlistSounds = new Set(
this.selected.filter((d) => d.dataset.type === "PlaylistSound")
.map((d) => d.value)
);

// 'Playlist', 'Macro', 'Item', 'Actor', 'Cards', 'RollTable', 'JournalEntry', 'Scene'
for (const type of CONSTANTS.PACK_IMPORT_ORDER) {
if (!CONFIG[type]) {
Expand All @@ -184,9 +190,16 @@ export default class ExporterProgress extends FormApplication {
.filter((d) => d.dataset.type === type)
.map((d) => d.value);
ScenePacker.logType(CONSTANTS.MODULE_NAME, 'info', true, `Exporter | Processing ${ids.length} ${CONSTANTS.TYPE_HUMANISE[type]}.`);
const documents = CONFIG[type].collection.instance.filter((s) =>
ids.includes(s.id),
);
const documents = CONFIG[type].collection.instance.filter((s) => {
if (ids.includes(s.id)) {
return true;
}

if (type === 'Playlist' && playlistSounds.size) {
const sounds = (s.sounds ?? s.data.sounds)?.keys() || [];
return sounds.some((id) => playlistSounds.has(id));
}
});

if (type !== 'Scene') {
// Add all documents by default as unrelated, later on remove those that have relations.
Expand All @@ -197,10 +210,18 @@ export default class ExporterProgress extends FormApplication {

const out = documents.map((d) => (d.toJSON ? d.toJSON() : d)) || [];
for (const document of out) {
if (type === 'Playlist') {
// Remove sounds that are not in the selected list
const sounds = document.sounds.filter((s) => playlistSounds.has(s._id));
if (sounds.length !== document.sounds.length) {
foundry.utils.setProperty(document, 'sounds', sounds);
}
}

const hash = Hash.SHA1(document);
setProperty(document, 'flags.scene-packer.hash', hash);
setProperty(document, 'flags.scene-packer.moulinette-adventure-name', this.packageName);
setProperty(document, 'flags.scene-packer.moulinette-adventure-version', this.exporterData?.version || '1.0.0');
foundry.utils.setProperty(document, 'flags.scene-packer.hash', hash);
foundry.utils.setProperty(document, 'flags.scene-packer.moulinette-adventure-name', this.packageName);
foundry.utils.setProperty(document, 'flags.scene-packer.moulinette-adventure-version', this.exporterData?.version || '1.0.0');
}
dataZip.AddToZip(out, `data/${type}.json`);
updateTotalSize({
Expand All @@ -212,7 +233,7 @@ export default class ExporterProgress extends FormApplication {
},
)}</p>`,
});
if (!ids.length) {
if (!documents.length) {
continue;
}

Expand Down Expand Up @@ -242,7 +263,7 @@ export default class ExporterProgress extends FormApplication {
this.assetsMap.AddAssets(assetData.assets);
break;
case 'Playlist':
assetData = await ExtractPlaylistAssets(document);
assetData = await ExtractPlaylistAssets(document, playlistSounds);
this.assetsMap.AddAssets(assetData.assets);
break;
case 'JournalEntry':
Expand Down
25 changes: 25 additions & 0 deletions scripts/export-import/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,24 @@ export default class Exporter extends FormApplication {
if (isBeingTicked) {
$input.parents('.directory-item.folder').children('header').find('input[type="checkbox"]').prop('checked', isBeingTicked);
}

const playlistSounds = $(element).closest('ul.playlist-sounds');
if (playlistSounds.length) {
// Allow playlist sounds to be individually selected within a playlist
const checkboxes = playlistSounds.find('input[type="checkbox"]');
const numChecked = checkboxes.filter(':checked').length;
const allChecked = checkboxes.length === numChecked;
if (numChecked > 0 && !allChecked) {
// Indeterminate state
playlistSounds.siblings('label').find('input[type="checkbox"]').prop('checked', false).prop('indeterminate', true);
} else if (allChecked) {
// All checked
playlistSounds.siblings('label').find('input[type="checkbox"]').prop('checked', true).prop('indeterminate', false);
} else {
// None checked
playlistSounds.siblings('label').find('input[type="checkbox"]').prop('checked', false).prop('indeterminate', false);
}
}
this._updateCounts();
}

Expand All @@ -540,6 +558,13 @@ export default class Exporter extends FormApplication {
// Clicked a direct link
return;
}

const playlistSounds = $(event.target).closest('ul.playlist-sounds');
if (playlistSounds.length) {
// Bubble the event up to the individual checkbox
return;
}

event.preventDefault();
const element = event.currentTarget;
const $input = $(element).find('input[type="checkbox"]');
Expand Down
88 changes: 74 additions & 14 deletions scripts/export-import/moulinette-importer.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,22 +289,25 @@ export default class MoulinetteImporter extends FormApplication {
}
// Run via the .fromSource method as that operates in a non-strict validation format, allowing
// for older formats to still be parsed in most cases.
const created = await Scene.createDocuments(documents.map(d => Scene.fromSource(d).toObject()), { keepId: true });
for (const id of created.map(s => s.id)) {
const scene = game.scenes.get(id);
if (!scene) {
continue;
}
const thumbData = await scene.createThumbnail();
await scene.update({thumb: thumbData.thumb}, {diff: false});
}
const created = await Scene.createDocuments(documents.map(d => Scene.fromSource({...d, active: false}).toObject()), { keepId: true });

// Check for compendium references within the scenes and update them to local world references
console.groupCollapsed(game.i18n.format('SCENE-PACKER.importer.converting-references', {
count: created.length,
type: Scene.collectionName,
}));
const sceneUpdates = ReplaceCompendiumReferences(Scene, created, availableDocuments, this.scenePackerInfo.name);

for (const scene of created) {
const thumb = await scene.createThumbnail();
const update = sceneUpdates.find(s => s._id === scene.id);
if (update) {
update.thumb = thumb.thumb;
continue;
}
sceneUpdates.push({ _id: scene.id, thumb: thumb.thumb });
}

if (sceneUpdates.length) {
await Scene.updateDocuments(sceneUpdates);
}
Expand Down Expand Up @@ -510,14 +513,48 @@ export default class MoulinetteImporter extends FormApplication {
count: filteredData.length,
})}</p>`,
});
const documents = await this.ensureAssets(filteredData, assetMap, assetData);
let documents = await this.ensureAssets(filteredData, assetMap, assetData);
const folderIDs = documents.map(d => d.folder);
if (folderIDs.length) {
await MoulinetteImporter.CreateFolders(folderIDs, this.folderData);
}
// Run via the .fromSource method as that operates in a non-strict validation format, allowing
// for older formats to still be parsed in most cases.
await Playlist.createDocuments(documents.map(d => Playlist.fromSource(d).toObject()), { keepId: true });
// Playlists might need to have individual sounds merged
const existingDocuments = documents.filter(d => game[Playlist.collectionName].has(d._id));
for (const d of existingDocuments) {
const existingPlaylist = game[Playlist.collectionName].get(d._id);
if (!existingPlaylist) {
continue;
}
const playlistData = existingPlaylist.toJSON();
const sounds = playlistData.sounds || [];
const newPlaylistData = Playlist.fromSource(d).toObject();
const newSounds = [];
let hasUpdates = false;
for (const sound of (newPlaylistData.sounds || [])) {
const i = sounds.findIndex(s => s._id === sound._id);
if (i !== -1) {
sounds[i] = sound;
hasUpdates = true;
continue;
}
newSounds.push(sound);
}

if (hasUpdates) {
await existingPlaylist.updateEmbeddedDocuments('PlaylistSound', sounds);
}
if (newSounds.length) {
await existingPlaylist.createEmbeddedDocuments('PlaylistSound', newSounds);
}
}

documents = documents.filter(d => !existingDocuments.some(e => e._id === d._id));
if (documents.length) {
// Run via the .fromSource method as that operates in a non-strict validation format, allowing
// for older formats to still be parsed in most cases.
await Playlist.createDocuments(documents.map(d => Playlist.fromSource(d)
.toObject()), { keepId: true });
}
}
}

Expand Down Expand Up @@ -710,6 +747,14 @@ export default class MoulinetteImporter extends FormApplication {
actorID: this.actorID,
info: this.scenePackerInfo,
});

Dialog.prompt({
title: game.i18n.localize('SCENE-PACKER.importer.name'),
content: game.i18n.localize('SCENE-PACKER.importer.complete-content'),
callback: () => {
window.location.reload();
}
})
}

/**
Expand Down Expand Up @@ -904,7 +949,22 @@ export default class MoulinetteImporter extends FormApplication {
});
}
const data = await response.json();
let createData = data.filter((a) => collection && !collection.has(a._id));
let createData = data.filter((a) => {
if (!collection.has(a._id)) {
return true;
}

if (collection.documentName === 'Playlist') {
const playlist = collection.get(a._id);
const sounds = playlist?.sounds ?? playlist?.data?.sounds;
if (sounds?.size) {
// Include the playlist if any of the sounds aren't in the collection's playlist sounds.
return a.sounds.map(s => s._id).some(id => !sounds.has(id));
}
}

return false;
});
if (onlyIDs.length) {
createData = createData.filter((a) => onlyIDs.includes(a._id));
}
Expand Down
3 changes: 3 additions & 0 deletions scripts/export-import/related/journals.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export function ExtractRelatedJournalData(journal) {
if (path === 'pages') {
for (const text of content.filter(c => c.type === 'text')) {
const relations = ExtractUUIDsFromContent(text.content, path);
if (text.markdown) {
relations.push(...ExtractUUIDsFromContent(text.markdown, path));
}
if (relations.length) {
relatedData.AddRelations(uuid, relations);
}
Expand Down
Loading

0 comments on commit 7fcc237

Please sign in to comment.