Kanka Autosaves

Saves a copy of the editor's content to local storage at set intervals so you can recover it if you forget to save or lose connection.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Kanka Autosaves
// @namespace    http://tampermonkey.net/
// @version      1
// @description  Saves a copy of the editor's content to local storage at set intervals so you can recover it if you forget to save or lose connection.
// @author       Salvatos
// @license      MIT
// @match        https://app.kanka.io/*
// @icon         https://www.google.com/s2/favicons?domain=kanka.io
// @run-at       document-end
// ==/UserScript==

/* Preferences */
const interval = 5; // in minutes
const snapshots = 10; // how many snapshots to keep in storage

// Wait for Summernote to initialize
$('#entry').on('summernote.init', function() {
    // Collect some info about the entry
    const crumb = document.querySelector('.breadcrumb li:nth-last-child(2) a');
    const nameField = document.querySelector('.entity-form input[name="name"]').value;
    const entityName = (crumb.href.match(/entities/)) ? crumb.title : (nameField.length > 0) ? nameField + " <i>(unsaved entity)</i>)" : "<i>(Unnamed " + crumb.title + " entity)</i>";
    const entryType = window.location.href.match(/posts/) ? "post" : "entry";
    const postName = (entryType == "post") ? (nameField.length > 0) ? nameField : "<i>(New post)</i>" : null;

    var recoveryButton = `
		<button type="button" id="autosavesButton" class="note-btn btn btn-default btn-sm" tabindex="-1" title="View autosaves in local storage" aria-label="View autosaves in local storage" data-original-title="View autosaves in local storage">
			<i class="fa-solid fa-clock-rotate-left"></i>
		</button>
		`;
    document.getElementsByClassName('note-toolbar')[0].insertAdjacentHTML("beforeend", recoveryButton);
    document.body.insertAdjacentHTML("beforeend", `<dialog id="autosaves" class="bg-box rounded border text-base-content"><header class="text-right"><button class="px-3 py-2" autofocus>Close</button></header><output style="max-height: 80vh;overflow-y: auto;padding-inline: 1.25rem;"></output></dialog>`);

    // Grab our button and dialog instances
    recoveryButton = document.getElementById('autosavesButton');
    const dialog = document.querySelector("#autosaves");
    const closeButton = document.querySelector("#autosaves button");

    // Add click event to buttons
    closeButton.addEventListener("click", () => {
        dialog.close();
        dialog.querySelector('output').innerHTML = "";
    });

    recoveryButton.addEventListener('click', ()=>{
        dialog.showModal();
        let savedState = JSON.parse( localStorage.getItem('kanka-autosaves') );

        // If we have existing snapshots, show them
        if (savedState && savedState.length > 0) {
            savedState.forEach( (state) => {
                // Create a Date object from the timestamp
                const date = new Date(state.time);
                const options = {
                    year: 'numeric',
                    month: 'long',
                    day: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit',
                    second: '2-digit',
                    timeZoneName: 'short',
                    hour12: false
                };
                const formattedTime = date.toLocaleString('en-US', options);
                dialog.querySelector('output').insertAdjacentHTML("beforeend", `<h4 class="text-primary my-1">${state.title}</h4><p class="text-accent-content text-xs">${formattedTime}</p><div>${state.content.substring(0,50)}... <p class="text-accent cursor-pointer">&rarr; Paste into editor</p></div><hr class="my-2">`);
            });

            // Copy to Summernote
            dialog.querySelectorAll('output div').forEach(function(elem, i) {
                elem.addEventListener("click", ()=> {
                    try {
                        $('#entry').summernote('pasteHTML', savedState[i].content);
                    } catch {
                        alert("Failed to paste into the editor");
                    }
                });
            });
        }
        else {
            alert("No snapshot found in local storage.");
        }
    });

    // Autosave
    autosave();

    function autosave () {
        setTimeout(() => {
            var editorValue = "";
            // Code editor
            if ($('#entry + div').hasClass('codeview')) {
                editorValue = $('#entry + div').find('.note-codable').val();
            }
            // Visual editor
            else {
                editorValue = $('#entry').val();
            }

            let formattedName = (entryType == "post") ? postName + " (" + entityName + ")" : entityName;

            let jsonItem = {time: Date.now(), title: formattedName, type: entryType, content: editorValue };

            let savedState = JSON.parse( localStorage.getItem('kanka-autosaves') );

            if (savedState) {
                // Remove excess saves
                while (savedState.length > 0 && savedState.length >= snapshots) {
                    savedState.splice(0, 1);
                }
                // Add to array and save
                savedState.push(jsonItem);
                localStorage.setItem(('kanka-autosaves'), JSON.stringify(savedState));
            }
            else {
                // First save, create array
                localStorage.setItem(('kanka-autosaves'), JSON.stringify([jsonItem]));
            }

            // Re-run
            autosave();
        }, (1000 * 60 * interval));
    }
});