// ==UserScript==
// @name SkebMemo
// @namespace http://tampermonkey.net/
// @version 1.2.3
// @description Save memo for user at skeb.jp.
// @author A. A.
// @match *://skeb.jp/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=skeb.jp
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
'use strict';
let fontCJE = 'Microsoft Yahei, SimHei, Noto Sans JP, Arial, sans-serif';
function handleExportNotes() {
let notes = JSON.parse(localStorage.getItem('notes') || '{}');
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(notes, null, 2));
let downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "skeb_memo.json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
};
function handleImportNotes() {
let input = document.createElement('input');
let notes = JSON.parse(localStorage.getItem('notes') || '{}');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.addEventListener('change', function (event) {
let file = event.target.files[0];
if (file) {
let reader = new FileReader();
reader.onload = function (e) {
try {
let importedNotes = JSON.parse(e.target.result);
// Merge imported notes with existing notes
notes = { ...notes, ...importedNotes };
localStorage.setItem('notes', JSON.stringify(notes));
alert(languages[currentLanguage].importSuccess);
notes = JSON.parse(localStorage.getItem('notes') || '{}');
} catch (error) {
console.error('SkebMemo: Error parsing imported JSON.', error);
alert(languages[currentLanguage].importError);
}
};
reader.readAsText(file);
}
});
input.click();
};
const languages = {
en: {
viewNotes: 'Show All Notes',
exportNotes: 'Export',
importNotes: 'Import',
clearNotes: 'Remove All',
notesList: 'Notes List',
searchNotes: 'Search Notes',
delete: 'Delete',
confirmClear: 'Are you sure you want to remove all notes? This action cannot be undone.',
notesCleared: 'All notes have been removed.',
importSuccess: 'Notes imported successfully!',
importError: 'Import failed, please check if the file format is correct.',
settings: 'Settings',
language: 'Language',
notesPerPage: 'Notes per page',
firstPage: 'First',
lastPage: 'Last',
boxComment: 'The memo will be saved for this work.'
},
cn: {
viewNotes: '查看所有笔记',
exportNotes: '导出笔记',
importNotes: '导入笔记',
clearNotes: '清空笔记',
notesList: '笔记列表',
searchNotes: '搜索笔记',
delete: '删除',
confirmClear: '确定要清空所有笔记吗?此操作不可恢复。',
notesCleared: '所有笔记已清空。',
importSuccess: '笔记导入成功!',
importError: '导入失败,请检查文件格式是否正确。',
settings: '设置',
language: '语言',
notesPerPage: '每页显示笔记数',
firstPage: '首',
lastPage: '末',
boxComment: '笔记内容将为此稿件保存。'
},
jp: {
viewNotes: 'メモ一覧',
exportNotes: 'エクスポート',
importNotes: 'インポート',
clearNotes: 'メモ一括削除',
notesList: 'メモ一覧',
searchNotes: '検索',
delete: '削除',
confirmClear: 'すべてのメモを削除しますか? この操作は元に戻せません。',
notesCleared: 'すべてのメモが削除されました。',
importSuccess: 'メモがインポートされました!',
importError: 'インポートに失敗しました。ファイル形式が正しいか確認してください。',
settings: '設定',
language: '言語',
notesPerPage: 'ページごとのメモ数',
firstPage: '最初',
lastPage: '最後',
boxComment: 'このメモはイラストに保存されます。'
}
};
function openSettings() {
if (document.querySelector('.SkebMemoSettings')) {
return;
}
let settingsDiv = document.createElement("div");
settingsDiv.className = 'SkebMemoSettings';
settingsDiv.style.position = "fixed";
settingsDiv.style.top = "50%";
settingsDiv.style.left = "50%";
settingsDiv.style.transform = "translate(-50%, -50%)";
settingsDiv.style.backgroundColor = "#fff";
settingsDiv.style.padding = "30px";
settingsDiv.style.border = "1px solid #ccc";
settingsDiv.style.zIndex = "9999";
let header = document.createElement('h2');
header.textContent = languages[currentLanguage].settings;
header.style.textAlign = 'center';
header.style.fontFamily = fontCJE;
settingsDiv.appendChild(header);
let languageLabel = document.createElement('label');
languageLabel.textContent = languages[currentLanguage].language;
languageLabel.style.display = 'block';
languageLabel.style.marginBottom = '10px';
languageLabel.style.fontFamily = fontCJE;
settingsDiv.appendChild(languageLabel);
let cnRadio = document.createElement('input');
cnRadio.type = 'radio';
cnRadio.name = 'SkebMemoLang';
cnRadio.value = 'cn';
cnRadio.id = 'cnRadio';
let cnLabel = document.createElement('label');
cnLabel.textContent = '中文';
cnLabel.style.marginRight = '10px';
cnLabel.htmlFor = 'cnRadio';
cnLabel.style.fontFamily = 'Microsoft Yahei, SimHei, sans-serif';
let enRadio = document.createElement('input');
enRadio.type = 'radio';
enRadio.name = 'SkebMemoLang';
enRadio.value = 'en';
enRadio.id = 'enRadio';
let enLabel = document.createElement('label');
enLabel.textContent = 'English';
enLabel.style.marginRight = '10px';
enLabel.htmlFor = 'enRadio';
enLabel.style.fontFamily = 'Arial, sans-serif';
let jpRadio = document.createElement('input');
jpRadio.type = 'radio';
jpRadio.name = 'SkebMemoLang';
jpRadio.value = 'jp';
jpRadio.id = 'jpRadio';
let jpLabel = document.createElement('label');
jpLabel.textContent = '日本語';
jpLabel.style.marginRight = '10px';
jpLabel.htmlFor = 'jpRadio';
jpLabel.style.fontFamily = 'Noto Sans JP, sans-serif';
settingsDiv.appendChild(cnRadio);
settingsDiv.appendChild(cnLabel);
settingsDiv.appendChild(enRadio);
settingsDiv.appendChild(enLabel);
settingsDiv.appendChild(jpRadio);
settingsDiv.appendChild(jpLabel);
let notesPerPageLabel = document.createElement('label');
notesPerPageLabel.textContent = languages[currentLanguage].notesPerPage;
notesPerPageLabel.style.display = 'block';
notesPerPageLabel.style.marginTop = '20px';
notesPerPageLabel.style.fontFamily = fontCJE;
settingsDiv.appendChild(notesPerPageLabel);
let notesPerPageInput = document.createElement('input');
notesPerPageInput.type = 'number';
notesPerPageInput.value = notesPerPage || '10';
notesPerPageInput.min = '1';
notesPerPageInput.style.width = '100%';
notesPerPageInput.style.marginBottom = '10px';
notesPerPageInput.style.marginRight = '10px';
settingsDiv.appendChild(notesPerPageInput);
let exportNotesButton = document.createElement('button');
exportNotesButton.textContent = languages[currentLanguage].exportNotes;
exportNotesButton.style.marginBottom = '10px';
exportNotesButton.style.marginRight = '10px';
exportNotesButton.style.fontFamily = fontCJE;
settingsDiv.appendChild(exportNotesButton);
let importNotesButton = document.createElement('button');
importNotesButton.textContent = languages[currentLanguage].importNotes;
importNotesButton.style.marginBottom = '10px';
importNotesButton.style.marginRight = '10px';
importNotesButton.style.fontFamily = fontCJE;
settingsDiv.appendChild(importNotesButton);
let clearNotesButton = document.createElement('button');
clearNotesButton.textContent = languages[currentLanguage].clearNotes;
clearNotesButton.style.marginBottom = '10px';
clearNotesButton.style.fontFamily = fontCJE;
settingsDiv.appendChild(clearNotesButton);
document.body.appendChild(settingsDiv);
exportNotesButton.addEventListener('click', handleExportNotes);
importNotesButton.addEventListener('click', handleImportNotes);
clearNotesButton.addEventListener('click', function () {
let confirmClear = confirm(languages[currentLanguage].confirmClear);
if (confirmClear) {
localStorage.removeItem('notes');
notes = {};
alert(languages[currentLanguage].notesCleared);
}
});
// container.appendChild(document.createElement("br"));
let closeButton = document.createElement("span");
closeButton.textContent = "×";
closeButton.style.position = "absolute";
closeButton.style.top = "5px";
closeButton.style.right = "5px";
closeButton.style.fontSize = "20px";
closeButton.style.cursor = "pointer";
closeButton.addEventListener("click", function () {
document.body.removeChild(settingsDiv);
});
settingsDiv.appendChild(closeButton);
cnRadio.addEventListener('change', function () {
localStorage.setItem('SkebMemoLang', 'cn');
// location.reload();
});
jpRadio.addEventListener('change', function () {
localStorage.setItem('SkebMemoLang', 'jp');
// location.reload();
});
enRadio.addEventListener('change', function () {
localStorage.setItem('SkebMemoLang', 'en');
// location.reload();
});
let savedLanguge = localStorage.getItem('SkebMemoLang');
if (savedLanguge === 'cn') {
cnRadio.checked = true;
currentLanguage = 'cn';
} else if (savedLanguge === 'jp') {
jpRadio.checked = true;
currentLanguage = 'jp';
} else {
enRadio.checked = true;
currentLanguage = 'en';
};
notesPerPageInput.addEventListener("input", function () {
localStorage.setItem("SkebMemoN", notesPerPageInput.value);
});
}
var notesPerPage = parseInt(localStorage.getItem('SkebMemoN')) || 10;
var currentLanguage = localStorage.getItem('SkebMemoLang') || 'en';
GM_registerMenuCommand(languages[currentLanguage].settings, openSettings);
function note_func() {
if (document.querySelector('.memobox')) {
return;
}
let urlPath = window.location.pathname;
// Find user name
let segments = urlPath.split('/');
let pageID = ''
let authorID = segments.find(segment => segment.startsWith('@'));
if (!authorID) {
console.warn('SkebMemo: Not a user page or work page.');
return;
} else {
pageID = authorID;
}
if (window.location.pathname.includes('/works/')) {
pageID = authorID + '/works/' + segments[3];
}
let nickname = '';
setTimeout(function() {
try {
if (window.location.pathname.includes('/works/')) {
nickname = document.querySelector('.title.is-5').textContent.trim() + '/' + segments[3];
} else {
nickname = document.querySelector('.title.is-4').textContent.trim();
}
} catch (error) {
console.error('SkebMemo: Error extracting nickname:', error);
nickname = '';
}
}, 100);
// Initialize notes object
let notes = JSON.parse(localStorage.getItem('notes') || '{}');
// Find info box
let targetDiv = document.querySelector('.is-box');
if (!targetDiv) {
console.error('SkebMemo: .is-box not found.');
return;
}
// let divbox = document.createElement('div');
// divbox.className = 'is-box';
let container = document.createElement('div');
container.style.marginTop = '20px';
container.className = 'is-box';
container.classList.add('memobox');
// divbox.appendChild(container);
targetDiv.parentNode.insertBefore(container, targetDiv.nextSibling);
// Create text box
let textBox = document.createElement('textarea');
textBox.id = 'myTextBox';
textBox.style.width = '100%';
textBox.style.height = '200px';
textBox.style.marginBottom = '10px';
textBox.style.resize = 'vertical';
textBox.style.fontFamily = fontCJE;
textBox.style.fontSize = '15px';
container.appendChild(textBox);
let viewNotesButton = document.createElement('button');
viewNotesButton.textContent = languages[currentLanguage].viewNotes;;
viewNotesButton.style.fontFamily = fontCJE;
viewNotesButton.style.fontSize = '15px';
viewNotesButton.style.marginBottom = '0px';
viewNotesButton.style.backgroundColor = '#28837f';
viewNotesButton.style.padding = '3px 1em'
viewNotesButton.style.borderColor = 'transparent';
viewNotesButton.style.borderRadius = '4px';
viewNotesButton.style.color = 'white';
viewNotesButton.addEventListener('mouseover', function() {
viewNotesButton.style.backgroundColor = '#257976';
});
viewNotesButton.addEventListener('mouseout', function() {
viewNotesButton.style.backgroundColor = '#28837f';
});
viewNotesButton.addEventListener('mousedown', function() {
viewNotesButton.style.backgroundColor = '#226F6C';
});
viewNotesButton.addEventListener('mouseup', function() {
viewNotesButton.style.backgroundColor = '#28837f';
});
container.appendChild(viewNotesButton);
if (window.location.pathname.includes('/works/')) {
container.style.position = 'relative'
let explaination = document.createElement('div');
explaination.textContent = languages[currentLanguage].boxComment;
explaination.style.fontFamily = fontCJE;
explaination.style.fontSize = '13px';
explaination.style.color = '#c5c5c5';
explaination.style.position = 'absolute';
explaination.style.bottom = '30px';
explaination.style.left = '20px';
container.appendChild(explaination);
}
// Use flexbox to align items
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'flex-end';
// Load notes from local storage
try {
textBox.value = notes[pageID].memo || '';
} catch (error) {
textBox.value = ''; // No memo
}
// Save notes to local storage
// textBox.addEventListener('input', function() {
// notes[pageID] = textBox.value;
// localStorage.setItem('notes', JSON.stringify(notes));
// });
textBox.addEventListener('input', function () {
if (textBox.value.trim() === '') {
delete notes[pageID];
} else {
notes[pageID] = {
memo: textBox.value,
name: nickname
};
}
localStorage.setItem('notes', JSON.stringify(notes));
});
// View all notes
viewNotesButton.addEventListener('click', function () {
if (document.querySelector('.notesList')) {
return;
}
let notesList = document.createElement('div');
notesList.className = 'notesList';
notesList.style.position = 'fixed';
notesList.style.top = '50%';
notesList.style.left = '50%';
notesList.style.transform = 'translate(-50%, -50%)';
notesList.style.width = '80%';
notesList.style.height = '80%';
notesList.style.overflow = 'auto';
notesList.style.backgroundColor = 'white';
notesList.style.zIndex = '1000';
notesList.style.border = '1px solid black';
notesList.style.padding = '20px';
notesList.style.boxShadow = '0px 0px 10px rgba(0,0,0,0.5)';
notesList.style.fontFamily = fontCJE;
notesList.id = 'notesList';
let header = document.createElement('h2');
header.textContent = languages[currentLanguage].notesList;
header.style.textAlign = 'center';
header.style.fontFamily = fontCJE;
notesList.appendChild(header);
let searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = languages[currentLanguage].searchNotes;
searchInput.style.width = '100%';
searchInput.style.marginBottom = '10px';
searchInput.style.fontFamily = fontCJE;
searchInput.style.border = '1px solid #ccc';
searchInput.style.padding = '5px';
notesList.appendChild(searchInput);
let notesContainer = document.createElement('div');
notesContainer.style.display = 'grid';
notesContainer.style.gridTemplateColumns = '3fr 3fr 12fr 1fr';
notesContainer.style.gap = '10px';
notesContainer.style.fontFamily = fontCJE;
notesList.appendChild(notesContainer);
let buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.position = 'absolute';
buttonContainer.style.top = '10px';
buttonContainer.style.left = '10px';
notesList.appendChild(buttonContainer);
// Export notes button
let exportNotesButton = document.createElement('button');
exportNotesButton.textContent = languages[currentLanguage].exportNotes;
exportNotesButton.style.marginRight = '10px';
exportNotesButton.style.fontFamily = fontCJE;
buttonContainer.appendChild(exportNotesButton);
// Import notes button
let importNotesButton = document.createElement('button');
importNotesButton.textContent = languages[currentLanguage].importNotes;
importNotesButton.style.marginRight = '10px';
importNotesButton.style.fontFamily = fontCJE;
buttonContainer.appendChild(importNotesButton);
// Remove all notes button
let clearNotesButton = document.createElement('button');
clearNotesButton.textContent = languages[currentLanguage].clearNotes;
clearNotesButton.style.marginRight = '10px';
clearNotesButton.style.fontFamily = fontCJE;
buttonContainer.appendChild(clearNotesButton);
// Setting button
let settingButton = document.createElement('button');
settingButton.textContent = languages[currentLanguage].settings;
settingButton.style.marginRight = '10px';
settingButton.style.fontFamily = fontCJE;
buttonContainer.appendChild(settingButton);
let closeButton = document.createElement("span");
closeButton.textContent = "×";
closeButton.style.position = "absolute";
closeButton.style.top = "5px";
closeButton.style.right = "15px";
closeButton.style.fontSize = "20px";
closeButton.style.cursor = "pointer";
closeButton.addEventListener('click', function () {
document.body.removeChild(notesList);
});
notesList.appendChild(closeButton);
let paginationContainer = document.createElement('div');
paginationContainer.style.display = 'flex';
paginationContainer.style.justifyContent = 'center';
paginationContainer.style.marginTop = '10px';
paginationContainer.style.fontFamily = fontCJE;
notesList.appendChild(paginationContainer);
document.body.appendChild(notesList);
let currentPage = 1;
let filteredNotes = Object.keys(notes).filter(id => notes[id].memo.includes(''));
let maxVisiblePages = 7;
function renderNotes(filter = '') {
notesContainer.innerHTML = '';
paginationContainer.innerHTML = '';
filteredNotes = Object.keys(notes).filter(id => notes[id].memo.includes(filter));
let totalPages = Math.ceil(filteredNotes.length / notesPerPage);
function createPageButton(page, text) {
let pageButton = document.createElement('button');
pageButton.textContent = text;
pageButton.style.margin = '0 5px';
pageButton.style.fontFamily = fontCJE;
pageButton.style.border = 'none'; // Remove border
if (page === currentPage) {
pageButton.disabled = true;
pageButton.style.fontWeight = 'bold';
} else {
pageButton.addEventListener('click', function () {
currentPage = page;
renderNotes(filter);
});
}
paginationContainer.appendChild(pageButton);
}
if (totalPages > 1) {
createPageButton(1, languages[currentLanguage].firstPage);
if (currentPage > 1) {
createPageButton(currentPage - 1, '<');
}
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
createPageButton(i, i);
}
if (currentPage < totalPages) {
createPageButton(currentPage + 1, '>');
}
createPageButton(totalPages, languages[currentLanguage].lastPage);
}
let start = (currentPage - 1) * notesPerPage;
let end = start + notesPerPage;
let notesToDisplay = filteredNotes.slice(start, end).reverse(); // Sort notes from newest to oldest
for (let id of notesToDisplay) {
let noteItem = document.createElement('div');
noteItem.style.display = 'contents';
let noteID = document.createElement('a');
noteID.href = `https://skeb.jp/${id}`;
noteID.textContent = id;
noteID.target = '_blank';
noteID.style.textDecoration = 'none';
noteID.style.color = 'blue';
noteID.style.fontFamily = fontCJE;
notesContainer.appendChild(noteID);
let noteName = document.createElement('a');
noteName.href = `https://skeb.jp/${id}`;
noteName.textContent = notes[id].name;
noteName.target = '_blank';
noteName.style.textDecoration = 'none';
noteName.style.fontFamily = fontCJE;
notesContainer.appendChild(noteName);
let noteText = document.createElement('div');
noteText.textContent = notes[id].memo;
noteText.style.fontFamily = fontCJE;
noteText.style.whiteSpace = 'pre-wrap';
notesContainer.appendChild(noteText);
let deleteButton = document.createElement('button');
deleteButton.textContent = languages[currentLanguage].delete;
deleteButton.style.marginLeft = '10px';
deleteButton.style.float = 'right';
deleteButton.style.fontFamily = fontCJE;
deleteButton.style.padding = '1px 1px';
deleteButton.style.fontWeight = 'bold';
deleteButton.style.alignSelf = 'center'
deleteButton.addEventListener('click', function (id) {
return function () {
delete notes[id];
localStorage.setItem('notes', JSON.stringify(notes));
renderNotes(filter);
};
}(id));
notesContainer.appendChild(deleteButton);
}
}
searchInput.addEventListener('input', function () {
currentPage = 1;
renderNotes(searchInput.value);
});
renderNotes();
exportNotesButton.addEventListener('click', handleExportNotes);
importNotesButton.addEventListener('click', function () {
let input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.addEventListener('change', function (event) {
let file = event.target.files[0];
if (file) {
let reader = new FileReader();
reader.onload = function (e) {
try {
let importedNotes = JSON.parse(e.target.result);
// Merge imported notes with existing notes
notes = { ...notes, ...importedNotes };
localStorage.setItem('notes', JSON.stringify(notes));
alert(languages[currentLanguage].importSuccess);
// notes = JSON.parse(localStorage.getItem('notes') || '{}');
document.body.removeChild(notesList);
viewNotesButton.click();
} catch (error) {
console.error('SkebMemo: Error parsing imported JSON.', error);
alert(languages[currentLanguage].importError);
}
};
reader.readAsText(file);
}
});
input.click();
});
clearNotesButton.addEventListener('click', function () {
let confirmClear = confirm(languages[currentLanguage].confirmClear);
if (confirmClear) {
localStorage.removeItem('notes');
notes = {};
alert(languages[currentLanguage].notesCleared);
document.body.removeChild(notesList);
viewNotesButton.click();
}
});
settingButton.addEventListener('click', openSettings);
});
}
function add_observer() {
let body = document.body;
let observer = new MutationObserver(mutations => {
note_func();
});
observer.observe(body, { childList: true, subtree: true });
}
add_observer();
})();