Skip to content

Commit 1ca319e

Browse files
Merge pull request #3 from moritary-capa/feature/pomodoro
ポモドーロタイマー機能を追加
2 parents d411519 + 30cfc4c commit 1ca319e

9 files changed

Lines changed: 367 additions & 1 deletion

File tree

1.pomodoro/app.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1-
# Pomodoro Timer App
1+
2+
from flask import Flask, render_template
3+
4+
app = Flask(__name__)
5+
6+
@app.route('/')
7+
def index():
8+
return render_template('index.html')
9+
10+
if __name__ == '__main__':
11+
app.run()

1.pomodoro/plan.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# ポモドーロタイマーアプリ 段階的実装計画
2+
3+
## ステップ1:最小限のWebアプリの構築
4+
- FlaskでWebサーバーを立ち上げ、`/``index.html`を表示
5+
- ディレクトリ構成(`static/`, `templates/`)の作成
6+
7+
## ステップ2:タイマーUIの作成
8+
- `index.html`にタイマー表示領域と操作ボタン(開始・一時停止・リセット)を配置
9+
- CSSで最低限のレイアウト調整
10+
11+
## ステップ3:タイマーのカウントダウン機能
12+
- JavaScriptでカウントダウンタイマーを実装
13+
- 開始・一時停止・リセットボタンの動作を実装
14+
15+
## ステップ4:ポモドーロ・休憩モード切り替え
16+
- 作業・短い休憩・長い休憩のモード切り替えロジックを追加
17+
- 現在のモード表示
18+
19+
## ステップ5:セッション管理・進捗表示
20+
- セッション(ポモドーロ回数)のカウント
21+
- 進捗バーや回数表示の追加
22+
23+
## ステップ6:通知・アラート機能
24+
- タイマー終了時にアラートや音で通知
25+
26+
## ステップ7:UI/UXの向上
27+
- デザインのブラッシュアップ(色・フォント・レスポンシブ対応など)
28+
- 操作性の改善
29+
30+
## ステップ8:追加・拡張機能(任意)
31+
- タイマー時間のカスタマイズ
32+
- ローカルストレージによる状態保存
33+
- セッション履歴の保存・表示
34+
- サーバーサイドAPI連携やユーザー認証

1.pomodoro/static/timer.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
let isRunning = false;
2+
let timerInterval = null;
3+
// timer.js
4+
const MODES = {
5+
work: { label: 'Work', seconds: 25 * 60 },
6+
short: { label: 'Short Break', seconds: 5 * 60 },
7+
long: { label: 'Long Break', seconds: 15 * 60 }
8+
};
9+
let currentMode = 'work';
10+
let remainingSeconds = MODES[currentMode].seconds;
11+
let sessionCount = 0;
12+
const SESSIONS_PER_SET = 4;
13+
14+
function updateTimerDisplay() {
15+
const min = String(Math.floor(remainingSeconds / 60)).padStart(2, '0');
16+
const sec = String(remainingSeconds % 60).padStart(2, '0');
17+
document.getElementById('timer').textContent = `${min}:${sec}`;
18+
document.getElementById('mode-label').textContent = MODES[currentMode].label;
19+
document.getElementById('session-count').textContent = sessionCount;
20+
// 円形進捗バー更新
21+
const circle = document.getElementById('progress-circle');
22+
const r = 100;
23+
const c = 2 * Math.PI * r;
24+
circle.setAttribute('stroke-dasharray', c);
25+
let total = MODES[currentMode].seconds;
26+
let percent = (total - remainingSeconds) / total;
27+
// stroke-dashoffsetで進捗を表現(0で空、cで全塗り)
28+
circle.setAttribute('stroke-dashoffset', (1 - percent) * c);
29+
// 残り時間が0未満にならないように
30+
if (remainingSeconds < 0) {
31+
document.getElementById('timer').textContent = '00:00';
32+
}
33+
}
34+
35+
36+
function startTimer() {
37+
if (isRunning) return;
38+
isRunning = true;
39+
timerInterval = setInterval(() => {
40+
if (remainingSeconds > 0) {
41+
remainingSeconds--;
42+
updateTimerDisplay();
43+
} else {
44+
clearInterval(timerInterval);
45+
isRunning = false;
46+
if (currentMode === 'work') {
47+
sessionCount++;
48+
updateTimerDisplay();
49+
}
50+
notifyTimerEnd();
51+
}
52+
}, 1000);
53+
function notifyTimerEnd() {
54+
// Web通知
55+
if (window.Notification && Notification.permission === 'granted') {
56+
new Notification('タイマー終了', { body: MODES[currentMode].label + 'タイムが終了しました!' });
57+
} else if (window.Notification && Notification.permission !== 'denied') {
58+
Notification.requestPermission().then(permission => {
59+
if (permission === 'granted') {
60+
new Notification('タイマー終了', { body: MODES[currentMode].label + 'タイムが終了しました!' });
61+
}
62+
});
63+
}
64+
// アラート音(ローカルファイルを使用)
65+
const beep = new Audio('beep.mp3');
66+
// 読み込みエラーのハンドリング
67+
beep.addEventListener('error', (event) => {
68+
console.error('アラート音の読み込みに失敗しました:', event);
69+
});
70+
// 再生時エラーのハンドリング(対応ブラウザでは Promise を利用)
71+
const playResult = beep.play();
72+
if (playResult && typeof playResult.catch === 'function') {
73+
playResult.catch((error) => {
74+
console.error('アラート音の再生に失敗しました:', error);
75+
});
76+
}
77+
}
78+
}
79+
80+
function pauseTimer() {
81+
if (timerInterval) {
82+
clearInterval(timerInterval);
83+
isRunning = false;
84+
}
85+
}
86+
87+
88+
function resetTimer() {
89+
pauseTimer();
90+
remainingSeconds = MODES[currentMode].seconds;
91+
updateTimerDisplay();
92+
}
93+
94+
95+
function switchMode(mode) {
96+
if (!MODES[mode]) return;
97+
// タイマー実行中にモードを切り替える場合はユーザーに確認する
98+
if (isRunning) {
99+
const confirmed = window.confirm('タイマーが実行中です。モードを切り替えると現在のタイマーがリセットされます。続行しますか?');
100+
if (!confirmed) {
101+
return;
102+
}
103+
}
104+
currentMode = mode;
105+
resetTimer();
106+
}
107+
108+
document.addEventListener('DOMContentLoaded', () => {
109+
document.getElementById('start-btn').addEventListener('click', startTimer);
110+
document.getElementById('pause-btn').addEventListener('click', pauseTimer);
111+
document.getElementById('reset-btn').addEventListener('click', resetTimer);
112+
document.getElementById('work-btn').addEventListener('click', () => switchMode('work'));
113+
document.getElementById('short-break-btn').addEventListener('click', () => switchMode('short'));
114+
document.getElementById('long-break-btn').addEventListener('click', () => switchMode('long'));
115+
updateTimerDisplay();
116+
// 通知許可リクエスト
117+
if (window.Notification && Notification.permission !== 'granted') {
118+
Notification.requestPermission();
119+
}
120+
});

1.pomodoro/templates/index.html

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<!DOCTYPE html>
2+
<html lang="ja">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>ポモドーロタイマー</title>
6+
<style>
7+
body {
8+
font-family: 'Segoe UI', 'Meiryo', sans-serif;
9+
background: #f4f6fb;
10+
text-align: center;
11+
margin: 0;
12+
padding: 0;
13+
}
14+
.timer-container {
15+
margin: 60px auto 0 auto;
16+
background: #fff;
17+
border-radius: 24px;
18+
box-shadow: 0 4px 24px rgba(0,0,0,0.10);
19+
display: inline-block;
20+
padding: 40px 60px 32px 60px;
21+
}
22+
.button-group {
23+
margin-top: 24px;
24+
}
25+
.mode-group {
26+
margin-bottom: 16px;
27+
}
28+
.mode-group button {
29+
font-size: 1em;
30+
margin: 0 8px;
31+
padding: 8px 20px;
32+
border: 2px solid #6a82fb;
33+
border-radius: 16px;
34+
background: #fff;
35+
color: #6a82fb;
36+
font-weight: bold;
37+
cursor: pointer;
38+
transition: background 0.2s, color 0.2s;
39+
}
40+
.mode-group button.active, .mode-group button:focus {
41+
background: linear-gradient(135deg, #6a82fb 0%, #fc5c7d 100%);
42+
color: #fff;
43+
outline: none;
44+
}
45+
.mode-group button:hover {
46+
background: #e0e7ff;
47+
}
48+
.control-group button {
49+
font-size: 1.1em;
50+
margin: 0 12px;
51+
padding: 12px 32px;
52+
border: none;
53+
border-radius: 24px;
54+
background: linear-gradient(135deg, #6a82fb 0%, #fc5c7d 100%);
55+
color: #fff;
56+
font-weight: bold;
57+
cursor: pointer;
58+
box-shadow: 0 2px 8px rgba(106,130,251,0.10);
59+
transition: background 0.2s, transform 0.1s;
60+
}
61+
.control-group button:hover {
62+
background: linear-gradient(135deg, #fc5c7d 0%, #6a82fb 100%);
63+
transform: translateY(-2px) scale(1.04);
64+
}
65+
margin-top: 24px;
66+
}
67+
.button-group button {
68+
font-size: 1.1em;
69+
margin: 0 12px;
70+
padding: 12px 32px;
71+
border: none;
72+
border-radius: 24px;
73+
background: linear-gradient(135deg, #6a82fb 0%, #fc5c7d 100%);
74+
color: #fff;
75+
font-weight: bold;
76+
cursor: pointer;
77+
box-shadow: 0 2px 8px rgba(106,130,251,0.10);
78+
transition: background 0.2s, transform 0.1s;
79+
}
80+
.button-group button:hover {
81+
background: linear-gradient(135deg, #fc5c7d 0%, #6a82fb 100%);
82+
transform: translateY(-2px) scale(1.04);
83+
}
84+
h1 {
85+
color: #6a82fb;
86+
font-size: 2.1em;
87+
margin-bottom: 32px;
88+
letter-spacing: 0.08em;
89+
}
90+
</style>
91+
</head>
92+
<body>
93+
<div class="timer-container">
94+
<h1>Pomodoro Timer</h1>
95+
<div id="mode-label" style="font-size:1.2em; color:#6a82fb; margin-bottom:12px;">Work</div>
96+
<div class="circle" style="position:relative;">
97+
<svg width="220" height="220">
98+
<circle cx="110" cy="110" r="100" stroke="#e0e7ff" stroke-width="16" fill="none" />
99+
<circle id="progress-circle" cx="110" cy="110" r="100" stroke="#6a82fb" stroke-width="16" fill="none" stroke-linecap="round" transform="rotate(-90 110 110)" style="transition: stroke-dashoffset 0.3s;" />
100+
</svg>
101+
<div id="timer" style="position:absolute;top:0;left:0;width:220px;height:220px;display:flex;align-items:center;justify-content:center;font-size:3.2em;color:#222;font-weight:bold;text-shadow:0 2px 8px rgba(0,0,0,0.10);background:transparent;">25:00</div>
102+
</div>
103+
<div id="session-info" style="margin: 12px 0 8px 0; font-size:1.1em; color:#333;">
104+
Sessions: <span id="session-count">0</span>
105+
</div>
106+
<div class="mode-group" style="margin-bottom:16px;">
107+
<button id="work-btn" aria-label="作業モードに切り替え">Work</button>
108+
<button id="short-break-btn" aria-label="短い休憩モードに切り替え">Short Break</button>
109+
<button id="long-break-btn" aria-label="長い休憩モードに切り替え">Long Break</button>
110+
</div>
111+
<div class="control-group button-group">
112+
<button id="start-btn" aria-label="タイマーを開始" aria-pressed="false">START</button>
113+
<button id="pause-btn" aria-label="タイマーを一時停止" aria-pressed="false">PAUSE</button>
114+
<button id="reset-btn" aria-label="タイマーをリセット">RESET</button>
115+
</div>
116+
</div>
117+
<script src="/static/timer.js"></script>
118+
</body>
119+
</html>

1.pomodoro/test_app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import pytest
2+
from app import app
3+
4+
def test_index_route():
5+
tester = app.test_client()
6+
response = tester.get('/')
7+
assert response.status_code == 200
8+
assert b'Pomodoro Timer' in response.data
70.9 KB
Loading

architecture.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# ポモドーロタイマーWebアプリ アーキテクチャ案
2+
3+
## 1. 技術スタック
4+
- バックエンド: Flask(Python)
5+
- フロントエンド: HTML, CSS, JavaScript
6+
- テンプレートエンジン: Jinja2(Flask標準)
7+
- 静的ファイル: `static/` ディレクトリ(CSS, JS, 画像等)
8+
- テンプレート: `templates/` ディレクトリ(HTML)
9+
10+
## 2. ディレクトリ構成案
11+
```
12+
1.pomodoro/
13+
app.py
14+
static/
15+
style.css
16+
timer.js
17+
templates/
18+
index.html
19+
```
20+
21+
## 3. 機能要件(想定)
22+
- タイマーの開始・一時停止・リセット
23+
- ポモドーロ(作業)・休憩の切り替え
24+
- 残り時間の表示
25+
- セッション数のカウント
26+
- 必要に応じて通知や音
27+
28+
## 4. アーキテクチャ概要
29+
### バックエンド(Flask)
30+
- ルート `/` でメイン画面(タイマーUI)を表示
31+
- 状態管理は基本的にフロントエンドで行い、サーバー側は画面表示のみ担当
32+
- 必要に応じてAPIエンドポイント(例: `/api/start`, `/api/status`)を追加し、履歴保存や拡張に対応
33+
34+
### フロントエンド(HTML/CSS/JS)
35+
- タイマーのカウントダウンやUI操作はJavaScriptで実装
36+
- ボタン操作(開始・停止・リセット)や状態表示
37+
- 必要に応じてローカルストレージで状態保存
38+
- CSSでデザイン調整(モックに合わせて)
39+
40+
## 5. 実装の流れ(例)
41+
1. Flaskで最小限のWebサーバーを作成し、`index.html`を表示
42+
2. HTMLでタイマーUIを作成
43+
3. CSSでデザインを整える
44+
4. JavaScriptでタイマー機能を実装
45+
5. 必要に応じてFlaskと非同期通信(fetch/Ajax)で拡張
46+
47+
## 6. 拡張性
48+
- ユーザーごとの履歴保存や認証機能追加も容易
49+
- API化してモバイルアプリ等からも利用可能
50+
51+
---
52+
53+
本アーキテクチャ案は、今後の要件追加やUI変更にも柔軟に対応できる構成です。

features.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# ポモドーロタイマーアプリ 実装機能一覧
2+
3+
## 基本機能
4+
1. タイマーのカウントダウン表示
5+
2. タイマーの開始・一時停止・再開・リセット
6+
3. ポモドーロ(作業)・短い休憩・長い休憩の切り替え
7+
4. 残り時間の視覚的表示(数字・進捗バー等)
8+
5. セッション(ポモドーロ回数)のカウント
9+
10+
## UI/UX関連
11+
6. 開始・一時停止・リセットなどの操作ボタン
12+
7. 現在のモード(作業/休憩)の表示
13+
8. セッション数や進捗の表示
14+
9. タイマー終了時の通知(アラートや音)
15+
16+
## 追加・拡張機能(任意)
17+
10. タイマー設定(作業・休憩時間のカスタマイズ)
18+
11. セッション履歴の保存・表示
19+
12. ローカルストレージによる状態保存
20+
13. モバイル対応のレスポンシブデザイン
21+
14. ユーザー認証・個人設定(必要に応じて)
22+
15. サーバーサイドでの履歴管理やAPI連携

pomodoro.png

87.4 KB
Loading

0 commit comments

Comments
 (0)