Sharing:
- 👀 popup/popup.html
- 👀 popup/popup.js
- files
@files this is the html and js code of a firefox browser extension popup window, implemented with alpine.js (using the CSP-friendly mode). add a button at the bottom of the window that will export the playlists and the playback history and download it as a json file.
I need to add a button to the popup HTML and implement JavaScript functionality to export playlists and playback history as a JSON file. Let's break down the task:
this.playlists object)Let's start by modifying the HTML file to add the export button:
For the HTML (popup.html), I'll add a button at the bottom, after the playlists container but still within the app div:
<div id="app" x-data="playlistManager">
<!-- existing header and playlists content -->
<div class="playlists-container">
<!-- existing playlist template -->
</div>
<!-- New export button -->
<div class="actions-container">
<button class="export-btn" x-bind="exportButton">Export Playlists</button>
</div>
</div>
Now for the JavaScript (popup.js), I need to add:
exportButton binding similar to the existing button bindingsexportPlaylists function that does the actual export workHere's what I'll add to the Alpine data object:
exportButton: {
["@click"]() {
this.exportPlaylists();
},
},
async exportPlaylists() {
try {
// Get current playlists from our data
const playlists = this.playlists;
// Get playback history if it exists (or use empty object)
const result = await browser.storage.local.get("playbackHistory");
const playbackHistory = result.playbackHistory || {};
// Combine into a single export object
const exportData = {
playlists: playlists,
playbackHistory: playbackHistory,
exportDate: new Date().toISOString()
};
// Convert to JSON string
const jsonString = JSON.stringify(exportData, null, 2);
// Create a download link
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Create a temporary link element and trigger download
const a = document.createElement('a');
a.href = url;
a.download = `playlists-export-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error("Error exporting playlists:", error);
}
}
Now I'll put this all together to create the changes needed.
For popup.html, I'll add the export button at the bottom, after line 59 (the closing of the playlists-container) but before line 60 (the closing of the app div).
For popup.js, I'll add the new methods to the Alpine data object before the final closing braces.
Let me create the XML response for the tools:
Looking at your files, I'll add an export button at the bottom of the popup window to export playlists and playback history as a JSON file.
First, let's add the button to the HTML and then implement the JavaScript functionality:
<tools>
<tool name="files">
<action type="edit">
<path>/home/brwong/projects/experiments/playlist/popup/popup.html</path>
<search><![CDATA[ </div>
</div>
</body>]]></search>
<replace><![CDATA[ </div>
<!-- Export button -->
<div class="export-container">
<button class="export-btn" x-bind="exportButton">Export Playlists</button>
</div>
</div>
</body>]]></replace>
</action>
</tool>
</tools>
Now let's add the JavaScript functionality to handle the export:
<tools>
<tool name="files">
<action type="edit">
<path>/home/brwong/projects/experiments/playlist/popup/popup.js</path>
<search><![CDATA[ removeVideoButton: {
["@click"]() {
this.removeVideo(
this.$el.dataset.playlistName,
this.$el.dataset.playlistIndex,
);
},
},]]></search>
<replace><![CDATA[ removeVideoButton: {
["@click"]() {
this.removeVideo(
this.$el.dataset.playlistName,
this.$el.dataset.playlistIndex,
);
},
},
exportButton: {
["@click"]() {
this.exportPlaylists();
},
},
async exportPlaylists() {
try {
// Get current playlists
const playlists = this.playlists;
// Get playback history if it exists
const result = await browser.storage.local.get("playbackHistory");
const playbackHistory = result.playbackHistory || {};
// Create export data object
const exportData = {
playlists: playlists,
playbackHistory: playbackHistory,
exportDate: new Date().toISOString()
};
// Convert to JSON
const jsonString = JSON.stringify(exportData, null, 2);
// Create download
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Trigger download
const a = document.createElement('a');
a.href = url;
a.download = `playlists-export-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
console.error("Error exporting playlists:", error);
}
},]]></replace>
</action>
</tool>
</tools>
Now we should also add some basic styling for the export button. Would you like me to create or modify a CSS file for this as well?
Sharing:
- 👀 popup/popup.html
- 👀 popup/popup.js
- files
add another button next to the first one that does a playlist import. the button will select a json file from the filesystem (in the same format as the export), do some rudimentary structural validation of the data (such as making sure that the value under "playlists" is an object with playlist names as keys and an array as the value, and each element in the arrays are objects with at least "url" and "title"), and replace the storage and the alpine.js state with the imported playlists. ignore "history" or any other key that may be present in the json file.
I need to add functionality for importing playlists. This will involve:
Let's start by adding the button to the HTML:
I'll add an import button next to the export button and implement the import functionality with validation:
<tools>
<tool name="files">
<action type="edit">
<path>/home/brwong/projects/experiments/playlist/popup/popup.html</path>
<search><![CDATA[ <!-- Export button -->
<div class="export-container">
<button class="export-btn" x-bind="exportButton">
Export Playlists
</button>
</div>]]></search>
<replace><![CDATA[ <!-- Export/Import buttons -->
<div class="export-container">
<button class="export-btn" x-bind="exportButton">
Export Playlists
</button>
<button class="import-btn" x-bind="importButton">
Import Playlists
</button>
<input type="file" id="import-file-input" accept=".json" style="display: none;" @change="importFile($event)">
</div>]]></replace>
</action>
</tool>
</tools>
Now, let's add the JavaScript functions for importing:
<tools>
<tool name="files">
<action type="edit">
<path>/home/brwong/projects/experiments/playlist/popup/popup.js</path>
<search><![CDATA[ exportButton: {
["@click"]() {
this.exportPlaylists();
},
},]]></search>
<replace><![CDATA[ exportButton: {
["@click"]() {
this.exportPlaylists();
},
},
importButton: {
["@click"]() {
document.getElementById('import-file-input').click();
},
},
importFile(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const importedData = JSON.parse(e.target.result);
this.validateAndImportPlaylists(importedData);
} catch (error) {
console.error("Error parsing JSON file:", error);
alert("Invalid JSON file. Please select a valid playlist export file.");
} finally {
// Reset the file input so the same file can be selected again
event.target.value = '';
}
};
reader.readAsText(file);
},
validateAndImportPlaylists(data) {
// Validate the playlists structure
if (!data.playlists || typeof data.playlists !== 'object') {
alert("Invalid file format: Missing or invalid 'playlists' property");
return;
}
// Validate each playlist
const validPlaylists = {};
let hasErrors = false;
for (const [playlistName, videos] of Object.entries(data.playlists)) {
// Check if videos is an array
if (!Array.isArray(videos)) {
console.error(`Playlist '${playlistName}' does not contain a valid array of videos`);
hasErrors = true;
continue;
}
// Validate each video in the playlist
const validVideos = videos.filter(video => {
if (!video || typeof video !== 'object') {
console.error(`Invalid video object in '${playlistName}'`);
return false;
}
if (!video.url || typeof video.url !== 'string' ||
!video.title || typeof video.title !== 'string') {
console.error(`Video in '${playlistName}' missing required properties (url, title)`);
return false;
}
return true;
});
// Add the validated playlist if it has valid videos
if (validVideos.length > 0) {
validPlaylists[playlistName] = validVideos;
}
}
if (Object.keys(validPlaylists).length === 0) {
alert("No valid playlists found in the import file");
return;
}
if (hasErrors) {
const confirmImport = confirm("Some playlists or videos were invalid and will be skipped. Do you want to continue with the import?");
if (!confirmImport) return;
}
// Update storage and state
this.updatePlaylists(validPlaylists);
},
async updatePlaylists(playlists) {
try {
await browser.storage.local.set({ playlists });
this.playlists = playlists;
alert("Playlists imported successfully!");
} catch (error) {
console.error("Error updating playlists:", error);
alert("Error importing playlists: " + error.message);
}
},]]></replace>
</action>
</tool>
</tools>
This implementation:
Would you like to add any specific styles for the import button to match the export button?
Sharing:
- 👀 popup/popup.html
- 👀 popup/popup.js
- files