// ==UserScript==
// @name 短语笔记CSV导出
// @namespace http://tampermonkey.net/
// @version 1.9
// @description 本地笔记追加同步到GitHub为纯文本,无同步弹窗,结果写控制台,保留其他弹窗提示,移动端兼容!
// @author ChatGPT
// @match *://*/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @connect api.github.com
// ==/UserScript==
(function () {
'use strict';
const NOTE_KEY = 'mobile_note_list';
const GITHUB_USERNAME = 'moodHappy';
const REPO_NAME = 'HelloWorld';
const FILE_PATH = 'Notes/phrase_note.json'; // 内容纯文本,后缀json无影响
const TOKEN = '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';
// Function to show a temporary message at the highest z-index
function showTemporaryMessage(msg, duration = 3000) {
const messageDiv = document.createElement('div');
messageDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8); /* Slightly darker background for better visibility */
color: white;
padding: 18px 30px; /* Slightly larger padding */
border-radius: 10px; /* Slightly more rounded corners */
z-index: 2147483641; /* Set to a very high z-index */
font-size: 18px; /* Larger font size */
text-align: center;
opacity: 0;
transition: opacity 0.4s ease-in-out; /* Slightly longer transition */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); /* Add a subtle shadow */
`;
messageDiv.textContent = msg;
document.body.appendChild(messageDiv);
// Fade in
setTimeout(() => {
messageDiv.style.opacity = '1';
}, 10);
// Fade out and remove
setTimeout(() => {
messageDiv.style.opacity = '0';
messageDiv.addEventListener('transitionend', () => messageDiv.remove());
}, duration);
console.log('[笔记]', msg);
}
// Original showMessage for critical alerts (e.g., selection issues) that require user action
function showMessage(msg) {
alert(msg);
console.log('[笔记]', msg);
}
function logSync(msg) {
console.log('[同步日志]', msg);
}
function getNotes() {
try {
return JSON.parse(GM_getValue(NOTE_KEY, '[]'));
} catch (e) {
return [];
}
}
function setNotes(notes) {
GM_setValue(NOTE_KEY, JSON.stringify(notes));
}
async function storeNote() { // Made async to await syncToGitHub
const selection = window.getSelection().toString().trim();
if (!selection) return showMessage('❗请先选中文本。'); // Still uses alert for user action
const formatted = selection.replace(/["“”]:?|:/, '|').replace(/["“”]/g, '');
if (!formatted.includes('|')) return showMessage('❗格式不符,请选中形如 xxx:"yyy" 的文本'); // Still uses alert for user action
const notes = getNotes();
notes.push(formatted);
setNotes(notes);
// Changed to showTemporaryMessage for "note saved successfully"
showTemporaryMessage(`✅ 已保存笔记:\n${formatted}`, 3000);
if (notes.length >= 5) {
exportNotes(notes); // This will now also trigger syncToGitHub
setNotes([]);
showTemporaryMessage('📤 自动导出并清空本地笔记(5条)', 3000); // Changed to showTemporaryMessage
}
}
function showNoteCount() {
const notes = getNotes();
showTemporaryMessage(`📊 当前已保存 ${notes.length} 条笔记`, 3000); // Changed to showTemporaryMessage
}
async function manualExport() { // Made async to await syncToGitHub
const notes = getNotes();
if (notes.length === 0) return showTemporaryMessage('⚠️ 当前没有需要导出的笔记', 3000); // Changed to showTemporaryMessage
exportNotes(notes); // This will now also trigger syncToGitHub
setNotes([]);
showTemporaryMessage(`📤 手动导出并清空 ${notes.length} 条笔记`, 3000); // Changed to showTemporaryMessage
}
async function exportNotes(notes) { // Made async to await syncToGitHub
const csv = '\uFEFF' + notes.join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'notes_' + new Date().toISOString().replace(/[:T]/g, '-').slice(0, 19) + '.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
// Trigger GitHub sync after successful CSV export
await syncToGitHub();
}
async function syncToGitHub() {
const newNotes = getNotes();
if (newNotes.length === 0) {
logSync('⚠️ 没有笔记需要同步');
showTemporaryMessage('⚠️ 没有笔记需要同步', 3000); // Also show temp message for no notes
return;
}
logSync('☁️ 开始同步到 GitHub(追加模式)...');
showTemporaryMessage('☁️ 正在同步笔记到 GitHub...', 5000); // Show a syncing message
const url = `https://api.github.com/repos/${GITHUB_USERNAME}/${REPO_NAME}/contents/${FILE_PATH}`;
const headers = {
'Authorization': `Bearer ${TOKEN}`,
'Accept': 'application/vnd.github+json'
};
try {
let oldText = '';
let sha = '';
const res = await fetch(url, { headers });
if (res.status === 200) {
const data = await res.json();
sha = data.sha;
oldText = decodeURIComponent(escape(atob(data.content.replace(/\n/g, ''))));
}
const oldLines = oldText.split('\n').filter(line => line.trim());
const allNotesSet = new Set([...newNotes, ...oldLines]);
const combinedText = '\uFEFF' + Array.from(allNotesSet).join('\n');
const encodedContent = btoa(unescape(encodeURIComponent(combinedText)));
const uploadBody = {
message: '📤 追加同步笔记',
content: encodedContent,
committer: {
name: GITHUB_USERNAME,
email: `${GITHUB_USERNAME}@users.noreply.github.com`
}
};
if (sha) uploadBody.sha = sha;
const putRes = await fetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(uploadBody)
});
if (putRes.ok) {
logSync('✅ 成功追加同步到 GitHub 并清空本地笔记');
showTemporaryMessage('✅ 笔记已成功同步到 GitHub!', 3000); // Success message
setNotes([]);
} else {
const errText = await putRes.text();
console.error('❌ 上传失败:', errText);
logSync(`❌ 同步失败:${putRes.status}`);
showTemporaryMessage('❌ 笔记同步到 GitHub 失败!', 3000); // Failure message
}
} catch (err) {
console.error('❌ 异常:', err);
logSync('❌ 网络错误或Token无效');
showTemporaryMessage('❌ 笔记同步到 GitHub 失败!网络错误或Token无效。', 3000); // Error message
}
}
// 菜单命令
GM_registerMenuCommand('📌 存储笔记(选中文本)', storeNote);
GM_registerMenuCommand('📊 查看笔记数量', showNoteCount);
GM_registerMenuCommand('📤 手动导出CSV', manualExport);
GM_registerMenuCommand('☁️ 同步到GitHub(追加模式)', syncToGitHub);
})();