Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Image and Text Side-by-Side Editor</title>
- <!-- We will use the JSZip library to create the zip file. It is included here from a CDN. -->
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
- <style>
- :root {
- --primary-bg: #f4f4f9;
- --secondary-bg: #ffffff;
- --text-colour: #333333;
- --border-colour: #dddddd;
- --button-bg: #005792;
- --button-hover-bg: #007bff;
- --button-text: #ffffff;
- --disabled-bg: #cccccc;
- }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- background-color: var(--primary-bg);
- color: var(--text-colour);
- margin: 0;
- padding: 20px;
- display: flex;
- justify-content: center;
- align-items: flex-start;
- min-height: 100vh;
- }
- main {
- width: 95%;
- max-width: 1400px;
- background-color: var(--secondary-bg);
- border-radius: 8px;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
- padding: 2em;
- }
- h1,
- h2 {
- text-align: center;
- color: var(--button-bg);
- }
- /*--- Setup View ---*/
- #setup-view {
- text-align: center;
- }
- .instructions {
- margin: 2em auto;
- max-width: 700px;
- text-align: left;
- padding: 1em;
- background-color: #eef7ff;
- border-left: 5px solid var(--button-bg);
- }
- .instructions ul {
- padding-left: 20px;
- }
- /*--- Editor View ---*/
- #editor-view {
- display: none; /*Hidden by default*/
- }
- #navigation-bar {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1.5em;
- padding: 1em;
- border-bottom: 1px solid var(--border-colour);
- }
- #status {
- font-size: 1.2em;
- font-weight: 500;
- }
- #content-viewer {
- display: flex;
- gap: 20px;
- min-height: 60vh;
- }
- .panel {
- flex: 1;
- display: flex;
- flex-direction: column;
- border: 1px solid var(--border-colour);
- border-radius: 5px;
- overflow: hidden;
- }
- .panel-header {
- padding: 0.5em 1em;
- background-color: var(--primary-bg);
- font-weight: bold;
- border-bottom: 1px solid var(--border-colour);
- }
- #image-container {
- padding: 10px;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-grow: 1;
- }
- #image-display {
- max-width: 100%;
- max-height: 100%;
- object-fit: contain;
- border-radius: 4px;
- }
- #text-editor {
- flex-grow: 1;
- width: 100%;
- padding: 10px;
- border: none;
- font-family: "Courier New", Courier, monospace;
- font-size: 1rem;
- line-height: 1.5;
- resize: none;
- box-sizing: border-box;
- }
- #text-editor:focus {
- outline: none;
- }
- #download-section {
- text-align: center;
- margin-top: 2em;
- }
- /*--- Common Elements ---*/
- button {
- background-color: var(--button-bg);
- color: var(--button-text);
- border: none;
- padding: 12px 24px;
- font-size: 1rem;
- font-weight: bold;
- border-radius: 5px;
- cursor: pointer;
- transition: background-color 0.2s ease;
- }
- button:hover {
- background-color: var(--button-hover-bg);
- }
- button:disabled {
- background-color: var(--disabled-bg);
- cursor: not-allowed;
- }
- </style>
- </head>
- <body>
- <main>
- <!-- Initial setup view, shown on page load -->
- <div id="setup-view">
- <h1>Side-by-Side Image and Text Editor</h1>
- <div class="instructions">
- <h2>Instructions</h2>
- <p>This tool allows you to view and edit text files alongside their corresponding images.</p>
- <ul>
- <li>Click the button below to start.</li>
- <li>You will be asked to select your <strong>image folder</strong> first.</li>
- <li>Then, you will be asked to select your <strong>text file folder</strong>.</li>
- <li>
- The tool will automatically match files with the same name (e.g.,
- <code>image1.png</code> and <code>image1.txt</code>).
- </li>
- </ul>
- <p>
- <strong>Browser Compatibility:</strong> This tool requires a modern browser like Chrome, Edge,
- or Opera. It will not work in Firefox or Safari.
- </p>
- </div>
- <button id="start-button">Select Folders and Begin</button>
- </div>
- <!-- Main editor view, hidden until files are loaded -->
- <div id="editor-view">
- <div id="navigation-bar">
- <button id="prev-button">Previous</button>
- <div id="status">File 0 of 0</div>
- <button id="next-button">Next</button>
- </div>
- <div id="content-viewer">
- <div class="panel">
- <div class="panel-header" id="image-filename">Image File</div>
- <div id="image-container">
- <img id="image-display" alt="Image will be displayed here." />
- </div>
- </div>
- <div class="panel">
- <div class="panel-header" id="text-filename">Text File</div>
- <textarea id="text-editor" placeholder="Text content will appear here ..."></textarea>
- </div>
- </div>
- <div id="download-section">
- <button id="download-zip-button">Save All Text Files to ZIP</button>
- </div>
- </div>
- </main>
- <script>
- // --- DOM Element References ---
- const setupView = document.getElementById("setup-view");
- const editorView = document.getElementById("editor-view");
- const startButton = document.getElementById("start-button");
- const prevButton = document.getElementById("prev-button");
- const nextButton = document.getElementById("next-button");
- const statusDisplay = document.getElementById("status");
- const imageDisplay = document.getElementById("image-display");
- const textEditor = document.getElementById("text-editor");
- const imageFilenameDisplay = document.getElementById("image-filename");
- const textFilenameDisplay = document.getElementById("text-filename");
- const downloadZipButton = document.getElementById("download-zip-button");
- // --- Application State ---
- let filePairs = [];
- let currentIndex = -1;
- let currentImageObjectUrl = null;
- // --- Event Listeners ---
- startButton.addEventListener("click", initialiseFileSelection);
- prevButton.addEventListener("click", showPreviousPair);
- nextButton.addEventListener("click", showNextPair);
- downloadZipButton.addEventListener("click", createAndDownloadZip);
- /**
- * Checks browser compatibility and starts the folder selection process.
- */
- async function initialiseFileSelection() {
- if (!window.showDirectoryPicker) {
- alert(
- "Your browser does not support the required File System Access API. Please use a recent version of Chrome, Edge, or Opera."
- );
- return;
- }
- try {
- const imageDirHandle = await window.showDirectoryPicker({title: "Select Image Folder"});
- const textDirHandle = await window.showDirectoryPicker({title: "Select Text File Folder"});
- startButton.textContent = "Processing ...";
- startButton.disabled = true;
- await processFolders(imageDirHandle, textDirHandle);
- } catch (error) {
- // This will catch if the user cancels the picker dialogue.
- console.log("Folder selection cancelled or failed:", error.message);
- startButton.textContent = "Select Folders and Begin";
- startButton.disabled = false;
- }
- }
- /**
- * Reads files from selected directories, finds matching pairs, and populates the application state.
- * @param {FileSystemDirectoryHandle} imageDirHandle - Handle for the image directory.
- * @param {FileSystemDirectoryHandle} textDirHandle - Handle for the text directory.
- */
- async function processFolders(imageDirHandle, textDirHandle) {
- const imageFiles = new Map();
- const textFiles = new Map();
- const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"];
- const textExtension = ".txt";
- // Helper to get file base name (without extension)
- const getBaseName = (fileName) => fileName.substring(0, fileName.lastIndexOf("."));
- // Read image files
- for await (const entry of imageDirHandle.values()) {
- if (
- entry.kind === "file" &&
- imageExtensions.some((ext) => entry.name.toLowerCase().endsWith(ext))
- ) {
- const baseName = getBaseName(entry.name);
- imageFiles.set(baseName, await entry.getFile());
- }
- }
- // Read text files
- for await (const entry of textDirHandle.values()) {
- if (entry.kind === "file" && entry.name.toLowerCase().endsWith(textExtension)) {
- const baseName = getBaseName(entry.name);
- textFiles.set(baseName, await entry.getFile());
- }
- }
- // Find pairs
- filePairs = [];
- for (const [baseName, imageFile] of imageFiles.entries()) {
- if (textFiles.has(baseName)) {
- const textFile = textFiles.get(baseName);
- filePairs.push({
- baseName: baseName,
- image: {file: imageFile},
- text: {file: textFile, content: null}, // content will be loaded on demand
- });
- }
- }
- // Sort pairs alphabetically by base name for consistent order
- filePairs.sort((a, b) => a.baseName.localeCompare(b.baseName));
- if (filePairs.length > 0) {
- setupView.style.display = "none";
- editorView.style.display = "block";
- currentIndex = 0;
- displayPair(currentIndex);
- } else {
- alert(
- 'No matching image and text files were found. Please check your folders and ensure filenames match (e.g., "my_image.png" and "my_image.txt").'
- );
- startButton.textContent = "Select Folders and Begin";
- startButton.disabled = false;
- }
- }
- /**
- * Displays a specific image/text pair in the editor.
- * @param {number} index - The index of the pair to display.
- */
- async function displayPair(index) {
- if (index < 0 || index >= filePairs.length) return;
- const pair = filePairs[index];
- // --- Update Image Panel ---
- // Revoke the previous object URL to free up memory
- if (currentImageObjectUrl) {
- URL.revokeObjectURL(currentImageObjectUrl);
- }
- currentImageObjectUrl = URL.createObjectURL(pair.image.file);
- imageDisplay.src = currentImageObjectUrl;
- imageFilenameDisplay.textContent = pair.image.file.name;
- // --- Update Text Panel ---
- textFilenameDisplay.textContent = pair.text.file.name;
- // Load content from memory if already edited, otherwise read from file
- if (pair.text.content !== null) {
- textEditor.value = pair.text.content;
- } else {
- textEditor.value = "Loading text ...";
- try {
- const text = await pair.text.file.text();
- pair.text.content = text; // Cache the content
- textEditor.value = text;
- } catch (e) {
- textEditor.value = "Error loading text file.";
- console.error("Failed to read text file:", e);
- }
- }
- // --- Update UI State ---
- statusDisplay.textContent = `File ${index + 1} of ${filePairs.length}`;
- prevButton.disabled = index === 0;
- nextButton.disabled = index === filePairs.length - 1;
- }
- /**
- * Saves the current text content and navigates to the previous pair.
- */
- function showPreviousPair() {
- if (currentIndex > 0) {
- saveCurrentText();
- currentIndex--;
- displayPair(currentIndex);
- }
- }
- /**
- * Saves the current text content and navigates to the next pair.
- */
- function showNextPair() {
- if (currentIndex < filePairs.length - 1) {
- saveCurrentText();
- currentIndex++;
- displayPair(currentIndex);
- }
- }
- /**
- * Saves the content of the textarea into the current pair's state.
- */
- function saveCurrentText() {
- if (currentIndex >= 0 && currentIndex < filePairs.length) {
- filePairs[currentIndex].text.content = textEditor.value;
- }
- }
- /**
- * Generates a ZIP archive of all text files and prompts the user to download it.
- */
- async function createAndDownloadZip() {
- // Ensure the very last edit is saved before zipping
- saveCurrentText();
- downloadZipButton.textContent = "Zipping ...";
- downloadZipButton.disabled = true;
- const zip = new JSZip();
- for (const pair of filePairs) {
- // Ensure all text contents are loaded before adding to the zip
- if (pair.text.content === null) {
- pair.text.content = await pair.text.file.text();
- }
- zip.file(pair.text.file.name, pair.text.content);
- }
- try {
- const zipBlob = await zip.generateAsync({type: "blob"});
- // Create a temporary link to trigger the download
- const link = document.createElement("a");
- link.href = URL.createObjectURL(zipBlob);
- link.download = "edited_texts.zip";
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- URL.revokeObjectURL(link.href); // Clean up
- } catch (error) {
- alert("An error occurred while creating the ZIP file.");
- console.error("ZIP generation failed:", error);
- } finally {
- downloadZipButton.textContent = "Save All Text Files to ZIP";
- downloadZipButton.disabled = false;
- }
- }
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment