コメントと更新履歴はゼロと無限の間のログ » Todo.phpへどうぞ。
(MOONGIFT風に)みなさんはタスク管理にどんなものを使っているだろうか。Webのサービスでもインストール型のツールでも、TODO管理の方法は色々あるが、いざ探してみると意外に帯に短し襷に長しである。
Remember The Milkは高機能だが重いし、Tracは共有するには良いが個人で使うには冗長、iGoogleのTODOガジェットはシンプルでよいが痒いところに手が届かない。
それならいっそ、自分のサーバで手軽に管理できるTODOツールはどうだろうか。今日紹介するのはPHPが1ファイルのみ、しかもDBも不要なTODO管理ツール、「Todo.php」だ。
下記のソースコードをコピーして適当な名前でPHPファイルとして保存し、Todoクラスの先頭で定義してある定数(const)をお好みで変更してください。また、定数に合わせてデータを保存するディレクトリを作ってください。
なお、このソースコードではJavaScirptとCSSとして、操作性を高めるためにPryn.js/cssを、カレンダーを使うためにYahho CalendarとGCalHolidaysを使っていますが、これら無しでもTodo.phpは動きます。使わない場合はこれらを削除してください。
<?php
/**
* Todo.php - Simple TODO Manager -
* @version 0.2.8
* @see http://0-oo.net/sbox/php-tool-box/todo
* @copyright 2008-2009 dgbadmin@gmail.com
* @license http://0-oo.net/pryn/MIT_license.txt (The MIT license)
*/
class Todo {
/** 文字コード */
const ENCODING = 'UTF-8';
/** サーバに保存するファイル名の文字コード */
//const FILE_NAME_ENCODING = 'UTF-8'; //Linuxその1
const FILE_NAME_ENCODING = 'EUC-JP'; //Linuxその2
//const FILE_NAME_ENCODING = 'Shift_JIS'; //Windows
/** TODOデータを保存するディレクトリ */
const DATA_DIR = 'data';
/** データファイルの拡張子 */
const DATA_EXT = 'txt';
/** カテゴリ名として許可する正規表現 */
const CAT_REGEX = '^[^\\\\./:*?"<>|]{1,20}$';
/** ページタイトル */
const TITLE = 'TODO';
/** バックアップの保存期限 */
const BACKUP_TIME = '-7 day';
/** 優先度の最大値 */
const PRI_MAX = 5;
public $cat;
public $cats;
public $list;
/**
* コンストラクタ
* @param string $cat カテゴリ
*/
public function __construct($cat) {
$this->cat = $cat;
mb_internal_encoding(Todo::ENCODING);
mb_regex_encoding(Todo::ENCODING);
}
/**
* 表示の準備
*/
public function setUp() {
if ($this->isValidCat()) {
if ($_REQUEST['delete'] && $this->_deleteCat()) {
} else {
if ($_POST['update']) {
$this->_updateList();
}
$this->list = $this->_getList();
}
}
$this->cats = $this->_getCategories();
}
/**
* カテゴリチェック
* @return boolean 許可されるカテゴリかどうか
*/
public function isValidCat() {
return mb_eregi(Todo::CAT_REGEX, $this->cat);
}
/**
* TODOリストのファイルパスを取得する
* @return string パス
*/
public function getPath() {
$cat = mb_convert_encoding($this->cat, Todo::FILE_NAME_ENCODING);
return Todo::DATA_DIR . '/' . $cat . '.' . Todo::DATA_EXT;
}
/**
* カテゴリを全て取得する
* @return array 全てのカテゴリのカテゴリ名とファイルサイズ
*/
private function _getCategories() {
$h = opendir(Todo::DATA_DIR);
if (!$h) {
exit('data directory is not found.');
}
$cats = array();
$limit = date('YmdHis', strToTime(Todo::BACKUP_TIME));
while (false !== ($file = readdir($h))) {
if (is_dir($file)) {
continue;
}
$arr = explode('.', $file);
$path = Todo::DATA_DIR . '/' . $file;
if (count($arr) == 3) { //バックアップの場合
if ($arr[2] < $limit) { //期限切れは削除
unlink($path);
}
} else { //最新版の場合
$cat = mb_convert_encoding($arr[0], Todo::ENCODING, Todo::FILE_NAME_ENCODING);
$cats[$cat] = filesize($path);
}
}
closedir($h);
ksort($cats);
return $cats;
}
/**
* TODOリストを更新して保存する
*/
private function _updateList() {
$oldCat = new Todo($_POST['oldcat']); //変更前のカテゴリ
$newPath = $this->getPath();
if (is_file(strtoupper(__FILE__))) { //ファイルパスで大文字小文字を区別しない場合
$change = strcasecmp($oldCat->cat, $this->cat);
} else {
$change = ($oldCat->cat != $this->cat);
}
if ($change && is_file($newPath)) {
return; //変更後のカテゴリが既に存在する場合は更新しない
}
$oldPath = $oldCat->getPath();
if ($oldCat->isValidCat() && is_file($oldPath)) {
rename($oldPath, $oldPath . '.' . date('YmdHis')); //バックアップ
}
foreach ($_POST['todo'] as $post) {
if ($post[1] != '') { //TODO未入力は削除
$data .= implode("\t", $post) . "\n";
}
}
file_put_contents($newPath, $data);
}
/**
* TODOリストを取得する
* @return array TODOリスト
*/
private function _getList() {
$path = $this->getPath();
if (!is_file($path)) { //新規の場合
return array('');
}
$list = explode("\n", file_get_contents($path));
rsort($list); //優先度順でソート
return $list;
}
/**
* カテゴリを削除する
* @return boolean 削除できたかどうか
*/
private function _deleteCat() {
$path = $this->getPath();
if (!is_file($path) || filesize($path)) { //todoが残っている場合は削除させない
return false;
}
$this->cat = null;
return unlink($path);
}
}
//----- Global Functions -----
/**
* HTMLエスケープ
* @param string $val エスケープしたい文字列
* @return string エスケープした文字列
*/
function h($val) {
return htmlspecialchars($val, ENT_QUOTES);
}
/**
* option要素を出力する
* @param string $val optionの値
* @param string $selected selectedにすべき値
*/
function echoOption($val, $selectedVal) {
if ($val == $selectedVal) {
$selected = 'selected="selected"';
}
echo "<option $selected>$val</option>\n";
}
//----------------------------
$todo = new Todo($_REQUEST['cat']);
$todo->setUp();
ini_set('default_charset', Todo::ENCODING);
?>
<<?php ?>?xml version="1.0" encoding="<?php echo Todo::ENCODING; ?>" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=<?php echo Todo::ENCODING; ?>" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<title><?php echo h(Todo::TITLE . ' - ' . $todo->cat); ?></title>
<link rel="stylesheet" type="text/css" href="/css/Pryn.css" />
<style type="text/css">
div#hd{ padding-top: 1em; }
h1{ margin: 1em 0 0; }
h1 a{ text-decoration: none; }
div#bd, li#add{ font-size: 100%; }
div.yui-g{ width: 43em; }
div#cat{ margin-bottom: 1em; font-size: 161.6%; }
tr.row:hover{ background-color: #79a; }
th, td{ border: solid #79a 1px; }
td{ padding: 0; }
select{ margin: 1px; width: 4.1em; height: 1.5em; text-align: center; }
table#todo input, select{ border-width: 0; }
input.todo{ margin: 2px; margin-left: 5px; width: 21.5em; }
input.date{ margin: 1px; width: 5.6em; text-align: center; }
input#update{ float: right; margin-right: 1em; }
li{ padding-top: 0.5em; font-size: 131%; }
input#new-cat{ width: 5em; }
div#ft{ text-align: center; }
</style>
</head>
<body>
<div id="doc" class="yui-t2">
<!-- ヘッダー -->
<div id="hd"><h1><a href="?"><?php echo h(Todo::TITLE); ?></a></h1></div>
<div id="bd">
<div id="yui-main"><div class="yui-b"><div class="yui-g">
<?php
$i = -1;
if ($todo->isValidCat()) {
?>
<form method="post" action="?">
<div id="cat">
カテゴリ : <input type="text" name="cat" value="<?php echo h($todo->cat); ?>" />
<input type="hidden" name="oldcat" value="<?php echo h($todo->cat); ?>" />
</div>
<table id="todo" summary="todo">
<tr><th>優先度</th><th>TODO</th><th>開始日 </th><th>期限</th><th>状態</th></tr>
<?php
$styleClasses = array('', 'todo', 'date han', 'date han');
$statuses = array('<!-- -->', '保留', '完了');
foreach ($todo->list as $i => $row) {
$task = explode("\t", $row);
if ($task[4] == $statuses[2]) { //完了は出力しない
continue;
}
?>
<tr class="row">
<!-- 優先度 -->
<td>
<select name="todo[<?php echo $i; ?>][]">
<?php
for ($j = 1; $j < Todo::PRI_MAX + 1; $j++) {
echoOption($j, $task[0]);
}
?>
</select>
</td>
<?php
//TODO、開始日、期限
for ($j = 1; $j < 4; $j++) {
?>
<td>
<input type="text" name="todo[<?php echo $i; ?>][]"
value="<?php echo h($task[$j]); ?>" class="<?php echo $styleClasses[$j]; ?>" />
</td>
<?php
}
?>
<!-- 状態 -->
<td>
<select name="todo[<?php echo $i; ?>][]">
<?php
foreach ($statuses as $status) {
echoOption($status, $task[4]);
}
?>
</select>
</td>
</tr>
<?php
}
?>
</table>
<div>
<input type="submit" id="update" name="update" value="更新" />
</div>
</form>
<div class="clear"></div>
<?php
} else if ($todo->cat) {
?>
<span class="error">残念ですが、このカテゴリ名( <?php echo h($todo->cat); ?> )は使えません</span>
<?php
}
?>
</div></div></div>
<!-- カテゴリリスト -->
<div id="cats" class="yui-b">
<ul>
<?php
foreach ($todo->cats as $cat => $fileSize) {
$href = '?cat=' . urlencode($cat);
?>
<li>
<a href="<?php echo $href; ?>"><?php echo h($cat); ?></a>
<?php
if (!$fileSize) { //todoが無いカテゴリは削除できる
?>
<a href="<?php echo $href; ?>&delete=do" title="このカテゴリを削除する">[削除]</a>
<?php
}
?>
</li>
<?php
}
?>
<li id="add">
<form method="post" action="?">
<div>
<input type="text" id="new-cat" name="cat" />
<input type="submit" value="追加" />
</div>
</form>
</li>
</ul>
</div>
</div>
<!-- フッター -->
<div id="ft">
powered by <a href="http://0-oo.net/sbox/php-tool-box/todo" class="same-window">Todo.php</a>
</div>
</div>
<script type="text/javascript" src="/js/Pryn.js"></script>
<script type="text/javascript" src="/js/YahhoCal.js"></script>
<script type="text/javascript" src="/js/GCalHolidays.js"></script>
<script type="text/javascript">
YahhoCal.loadYUI();
YahhoCal.setMondayAs1st();
Pryn.addEvent(window, "load", function() {
(function(input) {
var acs = new Pryn.ClassAccessor(input);
if (acs.hasClass("date")) {
Pryn.addEvent(input, "click", function() {
YahhoCal.render(YAHOO.util.Dom.generateId(input));
});
acs.addClass("clickable");
}
}).foreach($T("input"));
});
</script>
</body>
</html>