junkerstock
 ポモドーロTODO 

<!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-right: 10px;
word-break: break-all;
}

.handle, .delete-btn { display: none; }
.edit-mode .handle {
display: inline-block;
color: #ccc;
margin-right: 10px;
cursor: grab;
font-size: 20px;
}
.edit-mode .delete-btn {
display: block;
background-color: #ff4d4d;
color: white;
border: none;
padding: 5px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
margin-left: 5px;
flex-shrink: 0;
}

.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, // 第1フェーズ (作業) の秒数 (例: 25分なら 1500)
BREAK_TIME_SEC: 300 // 第3フェーズ (休憩) の秒数 (例: 5分なら 300)
};
/* ============================================================ */

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);
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) {
editToggleBtn.textContent = '完了';
editToggleBtn.classList.add('active');
sortable.option('disabled', false);
} else {
editToggleBtn.textContent = '編集';
editToggleBtn.classList.remove('active');
sortable.option('disabled', true);
}
});

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="delete-btn">削除</button>
`;
li.querySelector('.delete-btn').addEventListener('click', (e) => {
e.stopPropagation();
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; // 0:Idle, 1:Work, 2:Eval, 3:Break, 4:Log

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; // 6番用フラグ

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); // 直ちに第1フェーズへ
} 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 )
*
a
addBtn
addLogEntry -------( Function )
addTodo -------( Function )
audio
autocomplete
autoSave
autoStart
blob
btnEarly
btnNext
charset
class
completeStop -------( Function )
CONFIG
container
content
copyLog -------( Function )
currentPhase
currentTaskName
d
download
downloadLog -------( Function )
e
earlyFinish -------( Function )
editToggleBtn
el
endTime
filename
finishSession -------( Function )
formatTime -------( Function )
getLogTimestamp -------( Function )
handleEvaluation -------( Function )
hh
highlightTopTask -------( Function )
href
id
innerHTML
input
inputGroup
isEditMode
isFinish
isNext
isPaused
key
lang
length
li
list
loadTodos -------( Function )
log
logContent
logText
m
memo
memoInput
mm
name
now
onclick
phaseTitle
placeholder
playAudio -------( Function )
resetApp -------( Function )
resultStr
rrentTaskStartTime
s
saveTodos -------( Function )
scalable
scale
sessionLogs
setPhaseTitle -------( Function )
setTaskControlsVisibility -------( Function )
showScreen -------( Function )
skipBreak -------( Function )
skipBreak
sortable
src
startBreakPhase -------( Function )
startPomodoro -------( Function )
stored
style
taskHeader
tasks
text
textContent
timeDisplay
timeLeft
timerInterval
togglePause -------( Function )
topTask
transitionToEvaluation -------( Function )
type
uiEval
uiIdle
uiLog
uiTimer
updateTimer -------( Function )
url
value
width
y