<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Pomodoro TODO</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f0f2f5;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
margin: 0;
-webkit-tap-highlight-color: transparent;
min-height: 100vh;
}
.container, .timer-container {
width: 100%;
max-width: 500px;
background: #fff;
padding: 15px;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 15px;
box-sizing: border-box;
}
/* --- TODOリスト部分 --- */
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
transition: opacity 0.3s;
}
.header.hidden-header { display: none; }
h2 { margin: 0; color: #333; font-size: 1.4rem; }
#edit-toggle-btn {
background-color: #6c757d;
color: white;
border: none;
padding: 5px 10px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
}
#edit-toggle-btn.active { background-color: #28a745; }
.input-group {
display: flex;
margin-bottom: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
border-radius: 8px;
overflow: hidden;
}
#task-input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-right: none;
outline: none;
font-size: 16px;
border-radius: 8px 0 0 8px;
}
#add-btn {
padding: 0 15px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
font-weight: bold;
font-size: 14px;
border-radius: 0 8px 8px 0;
min-width: 60px;
}
.todo-list { list-style: none; padding: 0; margin: 0; min-height: 40px; }
.todo-item {
background-color: #fff;
border-bottom: 1px solid #eee;
padding: 12px 10px;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
touch-action: pan-y;
transition: border 0.3s;
}
.todo-item.active-task {
border: 3px solid #ff4d4d;
border-radius: 8px;
background-color: #fff5f5;
}
.todo-text {
flex-grow: 1;
font-size: 15px;
padding: 5px;
margin-right: 5px;
word-break: break-all;
border-radius: 4px;
border: 1px solid transparent; /* 編集時の枠線用 */
}
/* 編集中のスタイル */
.todo-text.editable {
background-color: #ffffff;
border: 1px solid #007bff;
box-shadow: 0 0 5px rgba(0,123,255,0.2);
outline: none;
cursor: text;
user-select: text; /* 編集時は選択可能に */
}
.handle, .delete-btn, .modify-btn { display: none; }
.edit-mode .handle {
display: inline-block;
color: #ccc;
margin-right: 10px;
cursor: grab;
font-size: 20px;
}
/* ボタン共通スタイル */
.edit-mode .delete-btn, .edit-mode .modify-btn {
display: block;
color: white;
border: none;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
margin-left: 5px;
flex-shrink: 0;
}
.edit-mode .delete-btn { background-color: #ff4d4d; }
.edit-mode .modify-btn { background-color: #17a2b8; } /* 水色 */
.sortable-ghost { opacity: 0.4; background-color: #e3f2fd; }
.sortable-drag { background: #fff; box-shadow: 0 5px 15px rgba(0,0,0,0.15); opacity: 1; }
/* --- タイマーパネル部分 --- */
.timer-container {
text-align: center;
background-color: #2c3e50;
color: white;
}
.timer-header { font-size: 1.1rem; margin-bottom: 10px; color: #ecf0f1; min-height: 1.2em; }
.timer-display {
font-size: 3.5rem;
font-weight: bold;
margin: 10px 0 20px 0;
font-variant-numeric: tabular-nums;
color: #fff;
line-height: 1;
}
.btn-group {
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
align-items: center;
width: 100%;
}
.pomo-btn {
flex: 1;
padding: 12px 5px;
border: none;
border-radius: 8px;
font-size: 0.95rem;
font-weight: bold;
cursor: pointer;
white-space: nowrap;
transition: transform 0.1s, opacity 0.2s;
}
.pomo-btn:active { transform: scale(0.98); }
.btn-start { background-color: #e74c3c; color: white; box-shadow: 0 3px 0 #c0392b; width: 100%; }
.btn-stop { background-color: #7f8c8d; color: white; box-shadow: 0 3px 0 #95a5a6; }
.btn-pause { background-color: #f1c40f; color: #333; box-shadow: 0 3px 0 #d35400; }
.btn-early { background-color: #3498db; color: white; box-shadow: 0 3px 0 #2980b9; }
.btn-next { background-color: #2ecc71; color: white; box-shadow: 0 3px 0 #27ae60; }
.btn-log-action { color: white !important; box-shadow: none; }
/* 評価フェーズ */
.eval-options {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.eval-btn {
background-color: #ecf0f1;
color: #2c3e50;
border: none;
padding: 12px;
border-radius: 6px;
text-align: left;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.2s;
}
.eval-btn:hover { background-color: #bdc3c7; }
/* メモ入力欄 */
.memo-input {
width: 100%;
padding: 10px;
margin-top: 10px;
border: 1px solid #bdc3c7;
border-radius: 6px;
box-sizing: border-box;
font-size: 1rem;
background-color: #fff;
}
/* ログ画面 */
.log-area {
background: #34495e;
color: #ecf0f1;
padding: 10px;
border-radius: 8px;
text-align: left;
font-family: monospace;
font-size: 11px;
height: 180px;
overflow-y: auto;
margin-bottom: 15px;
white-space: pre-wrap;
border: 1px solid #7f8c8d;
}
.hidden { display: none !important; }
.invisible { visibility: hidden; }
</style>
</head>
<body>
<div class="container" id="main-container">
<div class="header" id="task-header">
<h2>TODO Tasklist</h2>
<button id="edit-toggle-btn">編集</button>
</div>
<div class="input-group">
<input type="text" id="task-input" placeholder="タスクを入力..." autocomplete="off">
<button id="add-btn">追加</button>
</div>
<ul id="todo-list" class="todo-list"></ul>
</div>
<div class="timer-container" id="timer-ui">
<div class="timer-header" id="phase-title">ポモドーロタイマー</div>
<div id="screen-idle">
<button class="pomo-btn btn-start" onclick="startPomodoro()">スタート</button>
</div>
<div id="screen-timer" class="hidden">
<div class="timer-display" id="time-display">00:00</div>
<div class="btn-group">
<button class="pomo-btn btn-early" id="btn-early-finish" onclick="earlyFinish()">早期終了</button>
<button class="pomo-btn btn-next" id="btn-next-phase" onclick="skipBreak()">次行く</button>
<button class="pomo-btn btn-pause" onclick="togglePause()">一時停止</button>
<button class="pomo-btn btn-stop" onclick="completeStop()">完全中止</button>
</div>
</div>
<div id="screen-eval" class="hidden">
<p style="margin-bottom:10px; color:#ddd; font-size:0.9rem;">結果を選択してください</p>
<div class="eval-options">
<button class="eval-btn" onclick="handleEvaluation(1)">1. うまくいった、次に進む</button>
<button class="eval-btn" onclick="handleEvaluation(2)">2. うまくいった、しかし終了する</button>
<button class="eval-btn" onclick="handleEvaluation(3)">3. うまくいかなかった、後回しで次</button>
<button class="eval-btn" onclick="handleEvaluation(4)">4. うまくいかなかった、計画破棄で次</button>
<button class="eval-btn" onclick="handleEvaluation(5)">5. うまくいかなかった、終了する</button>
<button class="eval-btn" onclick="handleEvaluation(6)">6. そのまま継続したい</button>
</div>
<input type="text" id="eval-memo" class="memo-input" placeholder="一言メモ (任意)">
</div>
<div id="screen-log" class="hidden">
<div class="log-area" id="log-content"></div>
<div class="btn-group">
<button class="pomo-btn btn-log-action" style="background:#2980b9" onclick="copyLog()">コピー</button>
<button class="pomo-btn btn-log-action" style="background:#27ae60" onclick="downloadLog()">ダウンロード</button>
</div>
<div style="margin-top:15px;">
<button class="pomo-btn btn-start" onclick="resetApp()">最初に戻る</button>
</div>
</div>
</div>
<script>
/* ============================================================
【設定エリア】ここで時間を変更できます
============================================================ */
const CONFIG = {
WORK_TIME_SEC: 1500, // 作業時間 (1500=25分)
BREAK_TIME_SEC: 300 // 休憩時間 (300=5分)
};
/* ============================================================ */
function playAudio(filename) {
try {
const audio = new Audio(filename);
audio.play().catch(e => { console.log('Audio play error:', e); });
} catch (e) { console.log('Audio error:', e); }
}
/* =========================================
TODOリスト ロジック & 保存機能
========================================= */
const input = document.getElementById('task-input');
const addBtn = document.getElementById('add-btn');
const list = document.getElementById('todo-list');
const editToggleBtn = document.getElementById('edit-toggle-btn');
const container = document.getElementById('main-container');
const inputGroup = document.querySelector('.input-group');
const taskHeader = document.getElementById('task-header');
function saveTodos() {
const tasks = Array.from(list.children).map(li => li.querySelector('.todo-text').textContent);
localStorage.setItem('pomodoro_todo_tasks', JSON.stringify(tasks));
}
function loadTodos() {
const stored = localStorage.getItem('pomodoro_todo_tasks');
if (stored) {
const tasks = JSON.parse(stored);
tasks.forEach(text => addTodo(text, false));
} else {
// 初期データがない場合のサンプル
addTodo('資料作成', false);
addTodo('メール返信', false);
saveTodos();
}
}
let sortable = new Sortable(list, {
animation: 150,
ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag',
handle: '.handle',
delay: 0,
disabled: true,
onEnd: function() { saveTodos(); }
});
// 編集モード切替
editToggleBtn.addEventListener('click', () => {
const isEditMode = container.classList.toggle('edit-mode');
// 編集モードを抜けるとき、編集中のテキストがあれば確定させる
if (!isEditMode) {
document.querySelectorAll('.todo-text[contenteditable="true"]').forEach(span => {
// 強制的に編集終了処理
const li = span.closest('.todo-item');
const modifyBtn = li.querySelector('.modify-btn');
finishEditing(span, modifyBtn);
});
}
if (isEditMode) {
editToggleBtn.textContent = '完了';
editToggleBtn.classList.add('active');
sortable.option('disabled', false);
} else {
editToggleBtn.textContent = '編集';
editToggleBtn.classList.remove('active');
sortable.option('disabled', true);
}
});
// テキスト編集完了処理
function finishEditing(span, btn) {
span.contentEditable = "false";
span.classList.remove('editable');
btn.textContent = '修正';
btn.style.backgroundColor = ''; // CSSクラスの色に戻す
saveTodos();
}
function addTodo(text, autoSave = true) {
const li = document.createElement('li');
li.classList.add('todo-item');
li.innerHTML = `
<span class="handle">≡</span>
<span class="todo-text">${text}</span>
<button class="modify-btn">修正</button>
<button class="delete-btn">削除</button>
`;
const textSpan = li.querySelector('.todo-text');
const modifyBtn = li.querySelector('.modify-btn');
// 修正ボタンの動作
modifyBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isEditing = textSpan.isContentEditable;
if (isEditing) {
// 保存処理
finishEditing(textSpan, modifyBtn);
} else {
// 編集開始処理
textSpan.contentEditable = "true";
textSpan.classList.add('editable');
textSpan.focus();
// カーソルを末尾に移動
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(textSpan);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
modifyBtn.textContent = '確定';
modifyBtn.style.backgroundColor = '#007bff'; // 青色に変更
}
});
// テキスト編集中にEnterキーで確定
textSpan.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
finishEditing(textSpan, modifyBtn);
}
});
// 削除ボタンの動作
li.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
if(confirm('削除しますか?')) {
li.remove();
saveTodos();
}
});
list.appendChild(li);
if(autoSave) saveTodos();
}
addBtn.addEventListener('click', () => {
const text = input.value.trim();
if (text) {
addTodo(text);
input.value = '';
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addBtn.click();
});
window.addEventListener('DOMContentLoaded', loadTodos);
/* =========================================
ポモドーロ & ログ機能 (変更なし)
========================================= */
let timerInterval = null;
let timeLeft = 0;
let isPaused = false;
let currentPhase = 0;
let sessionLogs = [];
let currentTaskStartTime = null;
let currentTaskName = "";
const uiIdle = document.getElementById('screen-idle');
const uiTimer = document.getElementById('screen-timer');
const uiEval = document.getElementById('screen-eval');
const uiLog = document.getElementById('screen-log');
const timeDisplay = document.getElementById('time-display');
const phaseTitle = document.getElementById('phase-title');
const btnEarly = document.getElementById('btn-early-finish');
const btnNext = document.getElementById('btn-next-phase');
const logContent = document.getElementById('log-content');
const memoInput = document.getElementById('eval-memo');
function showScreen(screenId) {
[uiIdle, uiTimer, uiEval, uiLog].forEach(el => el.classList.add('hidden'));
document.getElementById(screenId).classList.remove('hidden');
}
function setPhaseTitle(text) {
if (!text) {
phaseTitle.textContent = '';
phaseTitle.classList.add('hidden');
} else {
phaseTitle.textContent = text;
phaseTitle.classList.remove('hidden');
}
}
function setTaskControlsVisibility(visible) {
if (visible) {
inputGroup.classList.remove('hidden');
editToggleBtn.classList.remove('hidden');
taskHeader.classList.remove('hidden-header');
} else {
inputGroup.classList.add('hidden');
editToggleBtn.classList.add('hidden');
taskHeader.classList.add('hidden-header');
if (container.classList.contains('edit-mode')) {
editToggleBtn.click();
}
}
}
function formatTime(seconds) {
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
const s = (seconds % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
function getLogTimestamp() {
const now = new Date();
const y = now.getFullYear();
const m = (now.getMonth() + 1).toString().padStart(2, '0');
const d = now.getDate().toString().padStart(2, '0');
const hh = now.getHours().toString().padStart(2, '0');
const mm = now.getMinutes().toString().padStart(2, '0');
return `${y}/${m}/${d} ${hh}:${mm}`;
}
function updateTimer() {
if (isPaused) return;
if (timeLeft > 0) {
timeLeft--;
timeDisplay.textContent = formatTime(timeLeft);
} else {
clearInterval(timerInterval);
playAudio('BEEP.wav');
if (currentPhase === 1) {
transitionToEvaluation();
} else if (currentPhase === 3) {
startPomodoro(true);
}
}
}
function highlightTopTask(active) {
document.querySelectorAll('.todo-item').forEach(el => el.classList.remove('active-task'));
if (active && list.firstElementChild) {
list.firstElementChild.classList.add('active-task');
}
}
// --- Phase 1: Work ---
function startPomodoro(autoStart = false) {
if (list.children.length === 0) {
finishSession("完了: 全タスク消化");
return;
}
if (!autoStart) {
if (!confirm('スタートしますか?')) return;
}
setTaskControlsVisibility(false);
currentPhase = 1;
setPhaseTitle('作業中');
showScreen('screen-timer');
btnEarly.classList.remove('hidden');
btnNext.classList.add('hidden');
currentTaskStartTime = getLogTimestamp();
currentTaskName = list.firstElementChild.querySelector('.todo-text').textContent;
timeLeft = CONFIG.WORK_TIME_SEC;
timeDisplay.textContent = formatTime(timeLeft);
isPaused = false;
highlightTopTask(true);
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 1000);
}
function togglePause() {
if (!isPaused) {
if (confirm('トイレ、来客ですね。停止しますか?')) {
isPaused = true;
setPhaseTitle('作業中 (停止中)');
}
} else {
isPaused = false;
setPhaseTitle('作業中');
}
}
function completeStop() {
if (confirm('全てを中止しますか?')) {
clearInterval(timerInterval);
if (currentPhase === 1) {
addLogEntry(currentTaskStartTime, getLogTimestamp(), currentTaskName, "完全中止", "");
}
finishSession("中断: ユーザーによる完全中止");
}
}
function earlyFinish() {
if (confirm('計画は早期終了ですか?')) {
clearInterval(timerInterval);
transitionToEvaluation();
}
}
// --- Phase 2: Eval ---
function transitionToEvaluation() {
currentPhase = 2;
setPhaseTitle('');
highlightTopTask(false);
memoInput.value = '';
showScreen('screen-eval');
}
function handleEvaluation(choice) {
const topTask = list.firstElementChild;
if (!topTask && list.children.length > 0) return;
const endTime = getLogTimestamp();
const memo = memoInput.value.trim();
let resultStr = "";
let isFinish = false;
let isNext = false;
let skipBreak = false;
switch (choice) {
case 1:
resultStr = "成功・次へ";
if(topTask) topTask.remove();
isNext = true;
break;
case 2:
resultStr = "成功・終了";
if(topTask) topTask.remove();
isFinish = true;
break;
case 3:
resultStr = "失敗・後回し";
if(topTask) list.appendChild(topTask);
isNext = true;
break;
case 4:
resultStr = "失敗・破棄";
if(topTask) topTask.remove();
isNext = true;
break;
case 5:
resultStr = "失敗・終了";
isFinish = true;
break;
case 6:
resultStr = "延長・継続";
isNext = true;
skipBreak = true;
break;
}
saveTodos();
addLogEntry(currentTaskStartTime, endTime, currentTaskName, resultStr, memo);
if (isFinish) {
finishSession("終了: 評価による終了選択");
} else if (isNext) {
if (list.children.length === 0) {
finishSession("完了: 全タスク消化");
} else {
if (skipBreak) {
startPomodoro(true);
} else {
startBreakPhase();
}
}
}
}
// --- Phase 3: Break ---
function startBreakPhase() {
currentPhase = 3;
setPhaseTitle('休憩');
showScreen('screen-timer');
btnEarly.classList.add('hidden');
btnNext.classList.remove('hidden');
timeLeft = CONFIG.BREAK_TIME_SEC;
timeDisplay.textContent = formatTime(timeLeft);
isPaused = false;
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 1000);
}
function skipBreak() {
if (confirm('休まずに行くでいいですか?')) {
clearInterval(timerInterval);
startPomodoro(true);
}
}
// --- Phase 4: Log / Finish ---
function addLogEntry(start, end, task, result, memo) {
sessionLogs.push({ start, end, task, result, memo });
}
function finishSession(reason) {
currentPhase = 4;
setPhaseTitle('');
highlightTopTask(false);
setTaskControlsVisibility(true);
playAudio('FINAL.wav');
let logText = `--- セッションログ ---\n理由: ${reason}\n\n`;
sessionLogs.forEach(log => {
logText += `${log.start} - ${log.end}\nタスク: ${log.task}\n結果: [${log.result}]\n`;
if (log.memo) {
logText += `メモ: ${log.memo}\n`;
}
logText += `------------------\n`;
});
logContent.textContent = logText;
showScreen('screen-log');
}
function copyLog() {
const text = logContent.textContent;
navigator.clipboard.writeText(text).then(() => {
alert('ログをコピーしました');
});
}
function downloadLog() {
const text = logContent.textContent;
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const now = new Date();
const filename = `todo_log_${now.getFullYear()}${(now.getMonth()+1).toString().padStart(2,'0')}${now.getDate().toString().padStart(2,'0')}.txt`;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
function resetApp() {
sessionLogs = [];
currentPhase = 0;
setPhaseTitle('ポモドーロ');
showScreen('screen-idle');
}
</script>
</body>
</html>
使用変数
| ) { saveTodos -------( Function ) | |
| * | |
| 1500 | |
| 300 | |
| a | |
| addBtn | |
| addLogEntry -------( Function ) | |
| addTodo -------( Function ) | |
| audio | |
| autocomplete | |
| autoSave | |
| autoStart | |
| backgroundColor | |
| blob | |
| btnEarly | |
| btnNext | |
| charset | |
| class | |
| completeStop -------( Function ) | |
| CONFIG | |
| container | |
| content | |
| contenteditable | |
| contentEditable | |
| copyLog -------( Function ) | |
| currentPhase | |
| currentTaskName | |
| d | |
| download | |
| downloadLog -------( Function ) | |
| e | |
| earlyFinish -------( Function ) | |
| editToggleBtn | |
| el | |
| endTime | |
| filename | |
| finishEditing -------( Function ) | |
| finishSession -------( Function ) | |
| formatTime -------( Function ) | |
| getLogTimestamp -------( Function ) | |
| handleEvaluation -------( Function ) | |
| hh | |
| highlightTopTask -------( Function ) | |
| href | |
| id | |
| innerHTML | |
| input | |
| inputGroup | |
| isEditing | |
| isEditMode | |
| isFinish | |
| isNext | |
| isPaused | |
| key | |
| lang | |
| length | |
| li | |
| list | |
| loadTodos -------( Function ) | |
| log | |
| logContent | |
| logText | |
| m | |
| memo | |
| memoInput | |
| mm | |
| modifyBtn | |
| name | |
| now | |
| onclick | |
| phaseTitle | |
| placeholder | |
| playAudio -------( Function ) | |
| range | |
| resetApp -------( Function ) | |
| resultStr | |
| rrentTaskStartTime | |
| s | |
| saveTodos -------( Function ) | |
| scalable | |
| scale | |
| sel | |
| sessionLogs | |
| setPhaseTitle -------( Function ) | |
| setTaskControlsVisibility -------( Function ) | |
| showScreen -------( Function ) | |
| skipBreak -------( Function ) | |
| skipBreak | |
| sortable | |
| span | |
| src | |
| startBreakPhase -------( Function ) | |
| startPomodoro -------( Function ) | |
| stored | |
| style | |
| taskHeader | |
| tasks | |
| text | |
| textContent | |
| textSpan | |
| timeDisplay | |
| timeLeft | |
| timerInterval | |
| togglePause -------( Function ) | |
| topTask | |
| transitionToEvaluation -------( Function ) | |
| type | |
| uiEval | |
| uiIdle | |
| uiLog | |
| uiTimer | |
| updateTimer -------( Function ) | |
| url | |
| value | |
| width | |
| y |