週記を作るボタンを表示するUserScript
/cak/週記を作るボタンを表示するUserScript案
#WIP 2025-08-20 17:31
ボタンを押すとhttps://scrapbox.io/projectname/週記YYYY-MM-DD〜YYYY-MM-DDが開かれる
既存のページがあれば既存のページが開かれる✅
既存のページがない場合
新しいページを作成
作成時テンプレートが入っているようにする
欲しい要素
2025年 第34週
週記YYYY-MM-DD〜YYYY-MM-DD←週記YYYY-MM-DD〜YYYY-MM-DD→週記YYYY-MM-DD〜YYYY-MM-DD
前後の週記に飛ぶリンクが欲しいのでページの一番下にこれがほしい
ピン留めを確認し、古い週記がpinされていたらならばpinを外し、新しい周記にpinを付け替える
週記」から始まるページがピン留めされている場合は、タイトルを確認すればよい?
projectnameにはプロジェクト名が入っている
最初のYYYY-MM-DDにはその週の最初の日、最後のYYYY-MM-DDにはその週の最後の日が入っている
週は日曜始まりとする
code:script.js!
// デバッグ用:最小限の週記作成スクリプト
scrapbox.PageMenu.addMenu({
title: '週記を作成(テスト)',
image: 'https://img.icons8.com/?size=100&id=7-8_UvuipWez&format=png&color=000000',
onClick: () => {
const now = new Date();
const dayOfWeek = now.getDay();
const startDate = new Date(now);
startDate.setDate(now.getDate() - dayOfWeek);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return ${year}-${month}-${day};
};
const pageTitle = 週記${formatDate(startDate)}〜${formatDate(endDate)};
const projectName = scrapbox.Project.name;
// まずはシンプルにページを開くだけ
const pageUrl = https://scrapbox.io/${projectName}/${encodeURIComponent(pageTitle)};
window.open(pageUrl, '_self');
console.log('週記ページを開きました:', pageTitle);
},
});
code:script.js!
// 週記作成UserScript
scrapbox.PageMenu.addMenu({
title: '週記を作成',
image: 'https://img.icons8.com/?size=100&id=7-8_UvuipWez&format=png',
onClick: () => {
createWeeklyReport();
},
});
function createWeeklyReport() {
const now = new Date();
const { startDate, endDate } = getWeekRange(now);
const projectName = scrapbox.Project.name;
const startDateStr = formatDate(startDate);
const endDateStr = formatDate(endDate);
const pageTitle = 週記${startDateStr}〜${endDateStr};
// 週記を作成・管理
createWeeklyReportWithWebSocket(projectName, pageTitle, startDate, endDate);
}
// 日曜始まりの週の開始日と終了日を取得
function getWeekRange(date) {
const dayOfWeek = date.getDay(); // 0=日曜, 1=月曜, ..., 6=土曜
const startDate = new Date(date);
startDate.setDate(date.getDate() - dayOfWeek);
const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 6);
return { startDate, endDate };
}
// 日付をYYYY-MM-DD形式でフォーマット
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return ${year}-${month}-${day};
}
// 年と週番号を取得
function getYearAndWeek(date) {
const startOfYear = new Date(date.getFullYear(), 0, 1);
const pastDaysOfYear = (date - startOfYear) / 86400000;
const weekNum = Math.ceil((pastDaysOfYear + startOfYear.getDay() + 1) / 7);
return { year: date.getFullYear(), week: weekNum };
}
// WebSocket版メイン処理(pin-diary-6のpinDiary関数を参考)
async function createWeeklyReportWithWebSocket(project, pageTitle, startDate, endDate) {
let socket;
try {
console.log('週記作成処理開始...');
// WebSocket接続を作成
socket = await makeSocket();
// 1. 古い週記のピンを外す
console.log('古い週記のピンを解除中...');
for await (const { title } of listPinnedPages(project)) {
if (isOldWeeklyReport(title, startDate)) {
await unpinPage(project, title, { socket });
console.log(古い週記のピンを解除: ${title});
}
}
// 2. 新しい週記ページをピン留め(存在しない場合は作成)
console.log(週記をピン留め: ${pageTitle});
await pinPage(project, pageTitle, { socket, create: true });
// 3. テンプレートを挿入
console.log('テンプレートを挿入中...');
const { header, footer } = makeWeeklyTemplate(startDate, endDate);
await patchPage(project, pageTitle, (lines) => {
return formatTemplate(
lines.slice(1).map(line => line.text),
header,
footer
);
}, { socket });
// 4. ページを開く
const pageUrl = https://scrapbox.io/${project}/${encodeURIComponent(pageTitle)};
window.open(pageUrl, '_self');
console.log('週記作成完了');
} catch (error) {
console.error('週記作成エラー:', error);
// エラーが発生してもページは開く
const pageUrl = https://scrapbox.io/${project}/${encodeURIComponent(pageTitle)};
window.open(pageUrl, '_self');
} finally {
if (socket) {
await disconnectSocket(socket);
}
}
}
// WebSocket接続を作成
async function makeSocket() {
return new Promise((resolve, reject) => {
if (!scrapbox || !scrapbox.Project) {
reject(new Error('Scrapbox not available'));
return;
}
const projectName = scrapbox.Project.name;
const ws = new WebSocket(wss://scrapbox.io/socket.io/?projectName=${projectName}&EIO=3&transport=websocket);
ws.onopen = () => {
console.log('WebSocket connected');
resolve(ws);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
// タイムアウト設定
setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
ws.close();
reject(new Error('WebSocket connection timeout'));
}
}, 5000);
});
}
// WebSocket接続を切断
async function disconnectSocket(socket) {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close();
}
}
// listPages API呼び出し
async function listPages(project, options = {}) {
const { limit = 1000, skip = 0 } = options;
try {
const url = https://scrapbox.io/api/pages/${project}?limit=${limit}&skip=${skip};
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
return {
ok: false,
value: {
name: 'FetchError',
message: Failed to fetch pages: ${response.status}
}
};
}
const data = await response.json();
return {
ok: true,
value: data
};
} catch (error) {
return {
ok: false,
value: {
name: error.name || 'Error',
message: error.message || 'Unknown error'
}
};
}
}
async function ensureList(project, skip) {
const result = await listPages(project, {
limit: 1000,
skip,
});
if (!result.ok) {
const error = new Error();
error.name = result.value.name;
error.message = result.value.message;
throw error;
}
return result.value;
}
// 全てのピン留めされたページを取得する(pin-diary-6と同じ実装)
async function* listPinnedPages(project, skip = 0) {
const { count, pages } = await ensureList(project, skip);
for (const page of pages) {
if (page.pin === 0) continue;
yield page;
}
if ((pages.at(-1)?.pin ?? 0) === 0) return;
yield* listPinnedPages(project, skip + 1000);
}
// 古い週記かどうかを判断
function isOldWeeklyReport(title, currentStartDate) {
if (!title.startsWith('週記')) return false;
// 現在の週記タイトルと同じ場合はfalse
const currentTitle = 週記${formatDate(currentStartDate)}〜${formatDate(new Date(currentStartDate.getTime() + 6 * 24 * 60 * 60 * 1000))};
return title !== currentTitle;
}
// 週記テンプレートを作成
function makeWeeklyTemplate(startDate, endDate) {
const { year, week } = getYearAndWeek(startDate);
// 前後の週の日付を計算
const prevWeekStart = new Date(startDate);
prevWeekStart.setDate(startDate.getDate() - 7);
const prevWeekEnd = new Date(endDate);
prevWeekEnd.setDate(endDate.getDate() - 7);
const nextWeekStart = new Date(startDate);
nextWeekStart.setDate(startDate.getDate() + 7);
const nextWeekEnd = new Date(endDate);
nextWeekEnd.setDate(endDate.getDate() + 7);
const prevWeekTitle = 週記${formatDate(prevWeekStart)}〜${formatDate(prevWeekEnd)};
const nextWeekTitle = 週記${formatDate(nextWeekStart)}〜${formatDate(nextWeekEnd)};
const currentTitle = 週記${formatDate(startDate)}〜${formatDate(endDate)};
return {
header: [
[${year}年 第${week}週],
''
],
footer: [
'',
[${prevWeekTitle}]←[${currentTitle}]→[${nextWeekTitle}]
]
};
}
// テンプレートをフォーマット(pin-diary-6のformat関数を簡略化)
function formatTemplate(lines, header, footer) {
const result = [];
// ヘッダーを追加
result.push(...header);
// 既存のコンテンツを追加(空でない場合)
if (lines.length > 0 && lines.some(line => line.trim())) {
result.push(...lines);
}
// フッターを追加
result.push(...footer);
return result;
}
// WebSocket経由でページをピン留め
async function pinPage(project, title, options = {}) {
const { socket, create = false } = options;
return new Promise((resolve, reject) => {
if (!socket || socket.readyState !== WebSocket.OPEN) {
reject(new Error('WebSocket not available'));
return;
}
const messageId = Math.random().toString(36).substr(2, 9);
const handleMessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.id === messageId) {
socket.removeEventListener('message', handleMessage);
if (data.error) {
reject(new Error(data.error));
} else {
resolve(data);
}
}
} catch (error) {
// JSONパースエラーは無視
}
};
socket.addEventListener('message', handleMessage);
const message = JSON.stringify({
id: messageId,
type: 'pin',
project,
page: title,
create
});
socket.send(message);
// タイムアウト設定
setTimeout(() => {
socket.removeEventListener('message', handleMessage);
reject(new Error('Pin timeout'));
}, 10000);
});
}
// WebSocket経由でページのピンを外す
async function unpinPage(project, title, options = {}) {
const { socket } = options;
return new Promise((resolve, reject) => {
if (!socket || socket.readyState !== WebSocket.OPEN) {
reject(new Error('WebSocket not available'));
return;
}
const messageId = Math.random().toString(36).substr(2, 9);
const handleMessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.id === messageId) {
socket.removeEventListener('message', handleMessage);
if (data.error) {
reject(new Error(data.error));
} else {
resolve(data);
}
}
} catch (error) {
// JSONパースエラーは無視
}
};
socket.addEventListener('message', handleMessage);
const message = JSON.stringify({
id: messageId,
type: 'unpin',
project,
page: title
});
socket.send(message);
setTimeout(() => {
socket.removeEventListener('message', handleMessage);
reject(new Error('Unpin timeout'));
}, 10000);
});
}
// WebSocket経由でページ内容を更新
async function patchPage(project, title, callback, options = {}) {
const { socket } = options;
return new Promise((resolve, reject) => {
if (!socket || socket.readyState !== WebSocket.OPEN) {
reject(new Error('WebSocket not available'));
return;
}
const messageId = Math.random().toString(36).substr(2, 9);
const handleMessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.id === messageId) {
socket.removeEventListener('message', handleMessage);
if (data.error) {
reject(new Error(data.error));
} else {
resolve(data);
}
}
} catch (error) {
// JSONパースエラーは無視
}
};
socket.addEventListener('message', handleMessage);
// まず現在のページ内容を取得
fetch(https://scrapbox.io/api/pages/${project}/${encodeURIComponent(title)}, {
credentials: 'include'
})
.then(response => response.json())
.then(pageData => {
const lines = pageData.lines || [];
const newLines = callback(lines);
const message = JSON.stringify({
id: messageId,
type: 'patch',
project,
page: title,
lines: newLines
});
socket.send(message);
})
.catch(reject);
setTimeout(() => {
socket.removeEventListener('message', handleMessage);
reject(new Error('Patch timeout'));
}, 10000);
});
}