plugins/tasklist
Thomas Brüderli
bruederli at kolabsys.com
Fri Jun 8 14:57:41 CEST 2012
plugins/tasklist/.gitignore | 1
plugins/tasklist/config.inc.php.dist | 4
plugins/tasklist/drivers/database/SQL/mysql.sql | 46
plugins/tasklist/drivers/database/tasklist_database_driver.php | 486 +++++
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php | 422 ++++
plugins/tasklist/drivers/tasklist_driver.php | 184 ++
plugins/tasklist/localization/de_CH.inc | 43
plugins/tasklist/localization/en_US.inc | 43
plugins/tasklist/skins/larry/sprites.png |binary
plugins/tasklist/skins/larry/taskbaricon.png |binary
plugins/tasklist/skins/larry/tasklist.css | 504 +++++
plugins/tasklist/skins/larry/templates/mainview.html | 141 +
plugins/tasklist/tasklist.js | 872 ++++++++++
plugins/tasklist/tasklist.php | 485 +++++
plugins/tasklist/tasklist_ui.php | 181 ++
15 files changed, 3412 insertions(+)
New commits:
commit aed27f7d1147cb93f0f0c072374880852c03371e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Fri Jun 8 14:57:16 2012 +0200
Import of a basic task management module for Roundcube working with libkolab storage
diff --git a/plugins/tasklist/.gitignore b/plugins/tasklist/.gitignore
new file mode 100644
index 0000000..2b9ce0a
--- /dev/null
+++ b/plugins/tasklist/.gitignore
@@ -0,0 +1 @@
+config.inc.php
\ No newline at end of file
diff --git a/plugins/tasklist/config.inc.php.dist b/plugins/tasklist/config.inc.php.dist
new file mode 100644
index 0000000..2fc5e28
--- /dev/null
+++ b/plugins/tasklist/config.inc.php.dist
@@ -0,0 +1,4 @@
+<?php
+
+$rcmail_config['tasklist_driver'] = 'kolab';
+
diff --git a/plugins/tasklist/drivers/database/SQL/mysql.sql b/plugins/tasklist/drivers/database/SQL/mysql.sql
new file mode 100644
index 0000000..eb9b454
--- /dev/null
+++ b/plugins/tasklist/drivers/database/SQL/mysql.sql
@@ -0,0 +1,46 @@
+/**
+ * Roundcube Tasklist plugin database
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ * @copyright (C) 2012, Kolab Systems AG
+ */
+
+CREATE TABLE `tasklists` (
+ `tasklist_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) unsigned NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(2) unsigned NOT NULL DEFAULT '0',
+ PRIMARY KEY (`tasklist_id`),
+ KEY `user_id` (`user_id`),
+ CONSTRAINT `fk_tasklist_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `tasks` (
+ `task_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ `tasklist_id` int(10) unsigned NOT NULL,
+ `parent_id` int(10) unsigned DEFAULT NULL,
+ `uid` varchar(255) NOT NULL,
+ `created` datetime NOT NULL,
+ `changed` datetime NOT NULL,
+ `del` tinyint(1) unsigned NOT NULL DEFAULT '0',
+ `title` varchar(255) NOT NULL,
+ `description` text,
+ `date` varchar(10) DEFAULT NULL,
+ `time` varchar(5) DEFAULT NULL,
+ `flagged` tinyint(4) NOT NULL DEFAULT '0',
+ `complete` float NOT NULL DEFAULT '0',
+ `alarms` varchar(255) NOT NULL,
+ `recurrence` varchar(255) DEFAULT NULL,
+ `organizer` varchar(255) DEFAULT NULL,
+ `attendees` text,
+ `notify` datetime DEFAULT NULL,
+ PRIMARY KEY (`task_id`),
+ KEY `tasklisting` (`tasklist_id`,`del`,`date`),
+ KEY `uid` (`uid`),
+ CONSTRAINT `fk_tasks_tasklist_id` FOREIGN KEY (`tasklist_id`)
+ REFERENCES `tasklists`(`tasklist_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
new file mode 100644
index 0000000..975f96e
--- /dev/null
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -0,0 +1,486 @@
+<?php
+
+/**
+ * Database driver for the Tasklist plugin
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class tasklist_database_driver extends tasklist_driver
+{
+ public $undelete = true; // yes, we can
+ public $sortable = false;
+
+ private $rc;
+ private $plugin;
+ private $cache = array();
+ private $lists = array();
+ private $list_ids = '';
+
+ private $db_tasks = 'tasks';
+ private $db_lists = 'tasklists';
+ private $sequence_tasks = 'task_ids';
+ private $sequence_lists = 'tasklist_ids';
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($plugin)
+ {
+ $this->rc = $plugin->rc;
+ $this->plugin = $plugin;
+
+ // read database config
+ $this->db_lists = $this->rc->config->get('db_table_lists', $this->db_lists);
+ $this->db_tasks = $this->rc->config->get('db_table_tasks', $this->db_tasks);
+ $this->sequence_lists = $this->rc->config->get('db_sequence_lists', $this->sequence_lists);
+ $this->sequence_tasks = $this->rc->config->get('db_sequence_tasks', $this->sequence_tasks);
+
+ $this->_read_lists();
+ }
+
+ /**
+ * Read available calendars for the current user and store them internally
+ */
+ private function _read_lists()
+ {
+ $hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', '')));
+
+ if (!empty($this->rc->user->ID)) {
+ $list_ids = array();
+ $result = $this->rc->db->query(
+ "SELECT *, tasklist_id AS id FROM " . $this->db_lists . "
+ WHERE user_id=?
+ ORDER BY name",
+ $this->rc->user->ID
+ );
+
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $arr['showalarms'] = intval($arr['showalarms']);
+ $arr['active'] = !in_array($arr['id'], $hidden);
+ $this->lists[$arr['id']] = $arr;
+ $list_ids[] = $this->rc->db->quote($arr['id']);
+ }
+ $this->list_ids = join(',', $list_ids);
+ }
+ }
+
+ /**
+ * Get a list of available tasks lists from this source
+ */
+ public function get_lists()
+ {
+ // attempt to create a default list for this user
+ if (empty($this->lists)) {
+ if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
+ $this->_read_lists();
+ }
+
+ return $this->lists;
+ }
+
+ /**
+ * Create a new list assigned to the current user
+ *
+ * @param array Hash array with list properties
+ * @return mixed ID of the new list on success, False on error
+ * @see tasklist_driver::create_list()
+ */
+ public function create_list($prop)
+ {
+ $result = $this->rc->db->query(
+ "INSERT INTO " . $this->db_lists . "
+ (user_id, name, color, showalarms)
+ VALUES (?, ?, ?, ?)",
+ $this->rc->user->ID,
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms']?1:0
+ );
+
+ if ($result)
+ return $this->rc->db->insert_id($this->sequence_lists);
+
+ return false;
+ }
+
+ /**
+ * Update properties of an existing tasklist
+ *
+ * @param array Hash array with list properties
+ * @return boolean True on success, Fales on failure
+ * @see tasklist_driver::edit_list()
+ */
+ public function edit_list($prop)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_lists . "
+ SET name=?, color=?, showalarms=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms']?1:0,
+ $prop['id'],
+ $this->rc->user->ID
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Set active/subscribed state of a list
+ *
+ * @param array Hash array with list properties
+ * @return boolean True on success, Fales on failure
+ * @see tasklist_driver::subscribe_list()
+ */
+ public function subscribe_list($prop)
+ {
+ $hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', '')));
+
+ if ($prop['active'])
+ unset($hidden[$prop['id']]);
+ else
+ $hidden[$prop['id']] = 1;
+
+ return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden))));
+ }
+
+ /**
+ * Delete the given list with all its contents
+ *
+ * @param array Hash array with list properties
+ * @return boolean True on success, Fales on failure
+ * @see tasklist_driver::remove_list()
+ */
+ public function remove_list($prop)
+ {
+ // TODO: implement this
+ return false;
+ }
+
+ /**
+ * Get number of tasks matching the given filter
+ *
+ * @param array List of lists to count tasks of
+ * @return array Hash array with counts grouped by status (all|flagged|today|tomorrow|overdue|nodate)
+ * @see tasklist_driver::count_tasks()
+ */
+ function count_tasks($lists = null)
+ {
+ if (empty($lists))
+ $lists = array_keys($this->lists);
+ else if (is_string($lists))
+ $lists = explode(',', $lists);
+
+ // only allow to select from lists of this user
+ $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
+
+ $today_date = new DateTime('now', $this->plugin->timezone);
+ $today = $today_date->format('Y-m-d');
+ $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
+ $tomorrow = $tomorrow_date->format('Y-m-d');
+
+ $result = $this->rc->db->query(sprintf(
+ "SELECT task_id, flagged, date FROM " . $this->db_tasks . "
+ WHERE tasklist_id IN (%s)
+ AND del=0 AND complete<1",
+ join(',', $list_ids)
+ ));
+
+ $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
+ while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ $counts['all']++;
+ if ($rec['flagged'])
+ $counts['flagged']++;
+ if (empty($rec['date']))
+ $counts['nodate']++;
+ else if ($rec['date'] == $today)
+ $counts['today']++;
+ else if ($rec['date'] == $tomorrow)
+ $counts['tomorrow']++;
+ else if ($rec['date'] < $today)
+ $counts['overdue']++;
+ }
+
+ return $counts;
+ }
+
+ /**
+ * Get all taks records matching the given filter
+ *
+ * @param array Hash array wiht filter criterias
+ * @param array List of lists to get tasks from
+ * @return array List of tasks records matchin the criteria
+ * @see tasklist_driver::list_tasks()
+ */
+ function list_tasks($filter, $lists = null)
+ {
+ if (empty($lists))
+ $lists = array_keys($this->lists);
+ else if (is_string($lists))
+ $lists = explode(',', $lists);
+
+ // only allow to select from lists of this user
+ $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
+ $sql_add = '';
+
+ // add filter criteria
+ if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
+ $sql_add .= ' AND (date IS NULL OR date >= ?)';
+ $datefrom = $filter['from'];
+ }
+ if ($filter['to']) {
+ if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE)
+ $sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')';
+ else
+ $sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')';
+ }
+
+ // special case 'today': also show all events with date before today
+ if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) {
+ $datefrom = date('Y-m-d', 0);
+ }
+
+ if ($filter['mask'] & tasklist::FILTER_MASK_NODATE)
+ $sql_add = ' AND date IS NULL';
+
+ if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
+ $sql_add .= ' AND complete=1';
+ else // don't show complete tasks by default
+ $sql_add .= ' AND complete<1';
+
+ if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED)
+ $sql_add .= ' AND flagged=1';
+
+ // compose (slow) SQL query for searching
+ // FIXME: improve searching using a dedicated col and normalized values
+ if ($filter['search']) {
+ $sql_query = array();
+ foreach (array('title','description','organizer','attendees') as $col)
+ $sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%');
+ $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
+ }
+
+ $tasks = array();
+ if (!empty($list_ids)) {
+ $datecol = $this->rc->db->quote_identifier('date');
+ $timecol = $this->rc->db->quote_identifier('time');
+ $result = $this->rc->db->query(sprintf(
+ "SELECT * FROM " . $this->db_tasks . "
+ WHERE tasklist_id IN (%s)
+ AND del=0
+ %s",
+ join(',', $list_ids),
+ $sql_add
+ ),
+ $datefrom
+ );
+
+ while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ $tasks[] = $this->_read_postprocess($rec);
+ }
+ }
+
+ return $tasks;
+ }
+
+ /**
+ * Return data of a specific task
+ *
+ * @param mixed Hash array with task properties or task UID
+ * @return array Hash array with task properties or false if not found
+ */
+ public function get_task($prop)
+ {
+ if (is_string($prop))
+ $prop['uid'] = $prop;
+
+ $query_col = $prop['id'] ? 'task_id' : 'uid';
+
+ $result = $this->rc->db->query(sprintf(
+ "SELECT * FROM " . $this->db_tasks . "
+ WHERE tasklist_id IN (%s)
+ AND %s=?
+ AND del=0",
+ $this->list_ids,
+ $query_col
+ ),
+ $prop['id'] ? $prop['id'] : $prop['uid']
+ );
+
+ if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ return $this->_read_postprocess($rec);
+ }
+
+ return false;
+ }
+
+ /**
+ * Map some internal database values to match the generic "API"
+ */
+ private function _read_postprocess($rec)
+ {
+ $rec['id'] = $rec['task_id'];
+ $rec['list'] = $rec['tasklist_id'];
+ $rec['changed'] = strtotime($rec['changed']);
+
+ if (!$rec['parent_id'])
+ unset($rec['parent_id']);
+
+ unset($rec['task_id'], $rec['tasklist_id'], $rec['created']);
+ return $rec;
+ }
+
+ /**
+ * Add a single task to the database
+ *
+ * @param array Hash array with task properties (see header of this file)
+ * @return mixed New event ID on success, False on error
+ * @see tasklist_driver::create_task()
+ */
+ public function create_task($prop)
+ {
+ // check list permissions
+ $list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists));
+ if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly'])
+ return false;
+
+ foreach (array('parent_id', 'date', 'time') as $col) {
+ if (empty($prop[$col]))
+ $prop[$col] = null;
+ }
+
+ $result = $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_tasks . "
+ (tasklist_id, uid, parent_id, created, changed, title, date, time)
+ VALUES (?, ?, ?, %s, %s, ?, ?, ?)",
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $list_id,
+ $prop['uid'],
+ $prop['parent_id'],
+ $prop['title'],
+ $prop['date'],
+ $prop['time']
+ );
+
+ if ($result)
+ return $this->rc->db->insert_id($this->sequence_tasks);
+
+ return false;
+ }
+
+ /**
+ * Update an task entry with the given data
+ *
+ * @param array Hash array with task properties
+ * @return boolean True on success, False on error
+ * @see tasklist_driver::edit_task()
+ */
+ public function edit_task($prop)
+ {
+ $sql_set = array();
+ foreach (array('title', 'description', 'flagged', 'complete') as $col) {
+ if (isset($prop[$col]))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]);
+ }
+ foreach (array('parent_id', 'date', 'time') as $col) {
+ if (isset($prop[$col]))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col]));
+ }
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_tasks . "
+ SET changed=%s %s
+ WHERE task_id=?
+ AND tasklist_id IN (%s)",
+ $this->rc->db->now(),
+ ($sql_set ? ', ' . join(', ', $sql_set) : ''),
+ $this->list_ids
+ ),
+ $prop['id']
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Remove a single task from the database
+ *
+ * @param array Hash array with task properties
+ * @param boolean Remove record irreversible
+ * @return boolean True on success, False on error
+ * @see tasklist_driver::delete_task()
+ */
+ public function delete_task($prop, $force = true)
+ {
+ $task_id = $prop['id'];
+
+ if ($task_id && $force) {
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_tasks . "
+ WHERE task_id=?
+ AND tasklist_id IN (" . $this->list_ids . ")",
+ $task_id
+ );
+ }
+ else if ($task_id) {
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_tasks . "
+ SET changed=%s, del=1
+ WHERE task_id=?
+ AND tasklist_id IN (%s)",
+ $this->rc->db->now(),
+ $this->list_ids
+ ),
+ $task_id
+ );
+ }
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Restores a single deleted task (if supported)
+ *
+ * @param array Hash array with task properties
+ * @return boolean True on success, False on error
+ * @see tasklist_driver::undelete_task()
+ */
+ public function undelete_task($prop)
+ {
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_tasks . "
+ SET changed=%s, del=0
+ WHERE task_id=?
+ AND tasklist_id IN (%s)",
+ $this->rc->db->now(),
+ $this->list_ids
+ ),
+ $prop['id']
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
new file mode 100644
index 0000000..5ccb3de
--- /dev/null
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -0,0 +1,422 @@
+<?php
+
+/**
+ * Kolab Groupware driver for the Tasklist plugin
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class tasklist_kolab_driver extends tasklist_driver
+{
+ // features supported by the backend
+ public $alarms = false;
+ public $attachments = false;
+ public $undelete = false; // task undelete action
+
+ private $rc;
+ private $plugin;
+ private $lists;
+ private $folders = array();
+ private $tasks = array();
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($plugin)
+ {
+ $this->rc = $plugin->rc;
+ $this->plugin = $plugin;
+
+ $this->_read_lists();
+ }
+
+ /**
+ * Read available calendars for the current user and store them internally
+ */
+ private function _read_lists()
+ {
+ // already read sources
+ if (isset($this->lists))
+ return $this->lists;
+
+ // get all folders that have type "task"
+ $this->folders = kolab_storage::get_folders('task');
+ $this->lists = array();
+
+ // convert to UTF8 and sort
+ $names = array();
+ foreach ($this->folders as $i => $folder) {
+ $names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
+ $this->folders[$folder->name] = $folder;
+ }
+
+ asort($names, SORT_LOCALE_STRING);
+
+ foreach ($names as $utf7name => $name) {
+ $folder = $this->folders[$utf7name];
+ $tasklist = array(
+ 'id' => kolab_storage::folder_id($utf7name),
+ 'name' => kolab_storage::object_name($utf7name),
+ 'color' => 'CC0000',
+ 'showalarms' => false,
+ 'active' => 1, #$folder->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
+ );
+ $this->lists[$tasklist['id']] = $tasklist;
+ $this->folders[$tasklist['id']] = $folder;
+ }
+ }
+
+ /**
+ * Get a list of available task lists from this source
+ */
+ public function get_lists()
+ {
+ // attempt to create a default list for this user
+ if (empty($this->lists)) {
+ if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
+ $this->_read_lists();
+ }
+
+ return $this->lists;
+ }
+
+ /**
+ * Create a new list assigned to the current user
+ *
+ * @param array Hash array with list properties
+ * name: List name
+ * color: The color of the list
+ * showalarms: True if alarms are enabled
+ * @return mixed ID of the new list on success, False on error
+ */
+ public function create_list($prop)
+ {
+ return false;
+ }
+
+ /**
+ * Update properties of an existing tasklist
+ *
+ * @param array Hash array with list properties
+ * id: List Identifier
+ * name: List name
+ * color: The color of the list
+ * showalarms: True if alarms are enabled (if supported)
+ * @return boolean True on success, Fales on failure
+ */
+ public function edit_list($prop)
+ {
+ return false;
+ }
+
+ /**
+ * Set active/subscribed state of a list
+ *
+ * @param array Hash array with list properties
+ * id: List Identifier
+ * active: True if list is active, false if not
+ * @return boolean True on success, Fales on failure
+ */
+ public function subscribe_list($prop)
+ {
+ return false;
+ }
+
+ /**
+ * Delete the given list with all its contents
+ *
+ * @param array Hash array with list properties
+ * id: list Identifier
+ * @return boolean True on success, Fales on failure
+ */
+ public function remove_list($prop)
+ {
+ return false;
+ }
+
+ /**
+ * Get number of tasks matching the given filter
+ *
+ * @param array List of lists to count tasks of
+ * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
+ */
+ public function count_tasks($lists = null)
+ {
+ if (empty($lists))
+ $lists = array_keys($this->lists);
+ else if (is_string($lists))
+ $lists = explode(',', $lists);
+
+ $today_date = new DateTime('now', $this->plugin->timezone);
+ $today = $today_date->format('Y-m-d');
+ $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
+ $tomorrow = $tomorrow_date->format('Y-m-d');
+
+ $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
+ foreach ($lists as $list_id) {
+ $folder = $this->folders[$list_id];
+ foreach ((array)$folder->select(array(array('tags','!~','complete'))) as $record) {
+ $rec = $this->_to_rcube_task($record);
+
+ if ($rec['complete']) // don't count complete tasks
+ continue;
+
+ $counts['all']++;
+ if ($rec['flagged'])
+ $counts['flagged']++;
+ if (empty($rec['date']))
+ $counts['nodate']++;
+ else if ($rec['date'] == $today)
+ $counts['today']++;
+ else if ($rec['date'] == $tomorrow)
+ $counts['tomorrow']++;
+ else if ($rec['date'] < $today)
+ $counts['overdue']++;
+ }
+ }
+
+ return $counts;
+ }
+
+ /**
+ * Get all taks records matching the given filter
+ *
+ * @param array Hash array with filter criterias:
+ * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
+ * - from: Date range start as string (Y-m-d)
+ * - to: Date range end as string (Y-m-d)
+ * - search: Search query string
+ * @param array List of lists to get tasks from
+ * @return array List of tasks records matchin the criteria
+ */
+ public function list_tasks($filter, $lists = null)
+ {
+ if (empty($lists))
+ $lists = array_keys($this->lists);
+ else if (is_string($lists))
+ $lists = explode(',', $lists);
+
+ $results = array();
+
+ // query Kolab storage
+ $query = array();
+ if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
+ $query[] = array('tags','~','complete');
+ else
+ $query[] = array('tags','!~','complete');
+
+ // full text search (only works with cache enabled)
+ if ($filter['search']) {
+ $search = mb_strtolower($filter['search']);
+ foreach (rcube_utils::normalize_string($search, true) as $word) {
+ $query[] = array('words', '~', $word);
+ }
+ }
+
+ foreach ($lists as $list_id) {
+ $folder = $this->folders[$list_id];
+ foreach ((array)$folder->select($query) as $record) {
+ $task = $this->_to_rcube_task($record);
+ $task['list'] = $list_id;
+
+ // TODO: post-filter tasks returned from storage
+
+ $results[] = $task;
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Return data of a specific task
+ *
+ * @param mixed Hash array with task properties or task UID
+ * @return array Hash array with task properties or false if not found
+ */
+ public function get_task($prop)
+ {
+ $id = is_array($prop) ? $prop['uid'] : $prop;
+ $list_id = is_array($prop) ? $prop['list'] : null;
+ $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
+
+ // find task in the available folders
+ foreach ($folders as $folder) {
+ if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
+ $this->tasks[$id] = $this->_to_rcube_task($object);
+ break;
+ }
+ }
+
+ return $this->tasks[$id];
+ }
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ private function _to_rcube_task($record)
+ {
+ $task = array(
+ 'id' => $record['uid'],
+ 'uid' => $record['uid'],
+ 'title' => $record['title'],
+# 'location' => $record['location'],
+ 'description' => $record['description'],
+ 'flagged' => $record['priority'] == 1,
+ 'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100),
+ 'parent_id' => $record['parent_id'],
+ );
+
+ // convert from DateTime to internal date format
+ if (is_a($record['due'], 'DateTime')) {
+ $task['date'] = $record['due']->format('Y-m-d');
+ $task['time'] = $record['due']->format('h:i');
+ }
+ if (is_a($record['dtstamp'], 'DateTime')) {
+ $task['changed'] = $record['dtstamp']->format('U');
+ }
+
+ return $task;
+ }
+
+ /**
+ * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
+ * (opposite of self::_to_rcube_event())
+ */
+ private function _from_rcube_task($task, $old = array())
+ {
+ $object = $task;
+
+ if (!empty($task['date'])) {
+ $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
+ if (empty($task['time']))
+ $object['due']->_dateonly = true;
+ unset($object['date']);
+ }
+
+ $object['complete'] = $task['complete'] * 100;
+ if ($task['complete'] == 1.0)
+ $object['status'] = 'COMPLETED';
+
+ if ($task['flagged'])
+ $object['priority'] = 1;
+ else
+ $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0;
+
+ // copy meta data (starting with _) from old object
+ foreach ((array)$old as $key => $val) {
+ if (!isset($object[$key]) && $key[0] == '_')
+ $object[$key] = $val;
+ }
+
+ unset($object['tempid'], $object['raw']);
+ return $object;
+ }
+
+ /**
+ * Add a single task to the database
+ *
+ * @param array Hash array with task properties (see header of tasklist_driver.php)
+ * @return mixed New task ID on success, False on error
+ */
+ public function create_task($task)
+ {
+ return $this->edit_task($task);
+ }
+
+ /**
+ * Update an task entry with the given data
+ *
+ * @param array Hash array with task properties (see header of tasklist_driver.php)
+ * @return boolean True on success, False on error
+ */
+ public function edit_task($task)
+ {
+ $list_id = $task['list'];
+ if (!$list_id || !($folder = $this->folders[$list_id]))
+ return false;
+
+ // moved from another folder
+ if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+ if (!$fromfolder->move($task['uid'], $folder->name))
+ return false;
+
+ unset($task['_fromlist']);
+ }
+
+ // load previous version of this task to merge
+ if ($task['id']) {
+ $old = $folder->get_object($task['uid']);
+ if (!$old || PEAR::isError($old))
+ return false;
+ }
+
+ // generate new task object from RC input
+ $object = $this->_from_rcube_task($task, $old);
+ $saved = $folder->save($object, 'task', $task['id']);
+
+ if (!$saved) {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving task object to Kolab server"),
+ true, false);
+ $saved = false;
+ }
+ else {
+ $task['id'] = $task['uid'];
+ $this->tasks[$task['uid']] = $task;
+ }
+
+ return $saved;
+ }
+
+ /**
+ * Remove a single task from the database
+ *
+ * @param array Hash array with task properties:
+ * id: Task identifier
+ * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
+ * @return boolean True on success, False on error
+ */
+ public function delete_task($task, $force = true)
+ {
+ $list_id = $task['list'];
+ if (!$list_id || !($folder = $this->folders[$list_id]))
+ return false;
+
+ return $folder->delete($task['uid']);
+ }
+
+ /**
+ * Restores a single deleted task (if supported)
+ *
+ * @param array Hash array with task properties:
+ * id: Task identifier
+ * @return boolean True on success, False on error
+ */
+ public function undelete_task($prop)
+ {
+ // TODO: implement this
+ return false;
+ }
+
+
+}
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
new file mode 100644
index 0000000..a3b0a85
--- /dev/null
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * Driver interface for the Tasklist plugin
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+ /**
+ * Struct of an internal task object how it is passed from/to the driver classes:
+ *
+ * $task = array(
+ * 'id' => 'Task ID used for editing', // must be unique for the current user
+ * 'parent_id' => 'ID of parent task', // null if top-level task
+ * 'uid' => 'Unique identifier of this task',
+ * 'list' => 'Task list identifier to add the task to or where the task is stored',
+ * 'changed' => <unixtime>, // Last modification date of record
+ * 'title' => 'Event title/summary',
+ * 'description' => 'Event description',
+ * 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set
+ * 'time' => 'Due time', // as string of format hh::ii or null if no due time is set
+ * 'categories' => 'Task category',
+ * 'flagged' => 'Boolean value whether this record is flagged',
+ * 'complete' => 'Float value representing the completeness state (range 0..1)',
+ * 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential)
+ * 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before due time)
+ * '_fromlist' => 'List identifier where the task was stored before',
+ * );
+ */
+
+/**
+ * Driver interface for the Tasklist plugin
+ */
+abstract class tasklist_driver
+{
+ // features supported by the backend
+ public $alarms = false;
+ public $attachments = false;
+ public $undelete = false; // task undelete action
+ public $sortable = false;
+ public $alarm_types = array('DISPLAY');
+ public $last_error;
+
+ /**
+ * Get a list of available task lists from this source
+ */
+ abstract function get_lists();
+
+ /**
+ * Create a new list assigned to the current user
+ *
+ * @param array Hash array with list properties
+ * name: List name
+ * color: The color of the list
+ * showalarms: True if alarms are enabled
+ * @return mixed ID of the new list on success, False on error
+ */
+ abstract function create_list($prop);
+
+ /**
+ * Update properties of an existing tasklist
+ *
+ * @param array Hash array with list properties
+ * id: List Identifier
+ * name: List name
+ * color: The color of the list
+ * showalarms: True if alarms are enabled (if supported)
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function edit_list($prop);
+
+ /**
+ * Set active/subscribed state of a list
+ *
+ * @param array Hash array with list properties
+ * id: List Identifier
+ * active: True if list is active, false if not
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function subscribe_list($prop);
+
+ /**
+ * Delete the given list with all its contents
+ *
+ * @param array Hash array with list properties
+ * id: list Identifier
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function remove_list($prop);
+
+ /**
+ * Get number of tasks matching the given filter
+ *
+ * @param array List of lists to count tasks of
+ * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
+ */
+ abstract function count_tasks($lists = null);
+
+ /**
+ * Get all taks records matching the given filter
+ *
+ * @param array Hash array with filter criterias:
+ * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
+ * - from: Date range start as string (Y-m-d)
+ * - to: Date range end as string (Y-m-d)
+ * - search: Search query string
+ * @param array List of lists to get tasks from
+ * @return array List of tasks records matchin the criteria
+ */
+ abstract function list_tasks($filter, $lists = null);
+
+ /**
+ * Return data of a specific task
+ *
+ * @param mixed Hash array with task properties or task UID
+ * @return array Hash array with task properties or false if not found
+ */
+ abstract public function get_task($prop);
+
+ /**
+ * Add a single task to the database
+ *
+ * @param array Hash array with task properties (see header of this file)
+ * @return mixed New event ID on success, False on error
+ */
+ abstract function create_task($prop);
+
+ /**
+ * Update an task entry with the given data
+ *
+ * @param array Hash array with task properties (see header of this file)
+ * @return boolean True on success, False on error
+ */
+ abstract function edit_task($prop);
+
+ /**
+ * Remove a single task from the database
+ *
+ * @param array Hash array with task properties:
+ * id: Task identifier
+ * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
+ * @return boolean True on success, False on error
+ */
+ abstract function delete_task($prop, $force = true);
+
+ /**
+ * Restores a single deleted task (if supported)
+ *
+ * @param array Hash array with task properties:
+ * id: Task identifier
+ * @return boolean True on success, False on error
+ */
+ public function undelete_task($prop)
+ {
+ return false;
+ }
+
+ /**
+ * List availabale categories
+ * The default implementation reads them from config/user prefs
+ */
+ public function list_categories()
+ {
+ $rcmail = rcmail::get_instance();
+ return $rcmail->config->get('tasklist_categories', array());
+ }
+
+}
diff --git a/plugins/tasklist/localization/de_CH.inc b/plugins/tasklist/localization/de_CH.inc
new file mode 100644
index 0000000..a7b3712
--- /dev/null
+++ b/plugins/tasklist/localization/de_CH.inc
@@ -0,0 +1,43 @@
+<?php
+
+$labels = array();
+$labels['navtitle'] = 'Aufgaben';
+$labels['lists'] = 'Ressourcen';
+$labels['list'] = 'Ressource';
+
+$labels['createnewtask'] = 'Neue Aufgabe eingeben';
+$labels['mark'] = 'Markieren';
+$labels['unmark'] = 'Markierung aufheben';
+$labels['edit'] = 'Bearbeiten';
+$labels['delete'] = 'Löschen';
+$labels['title'] = 'Titel';
+$labels['description'] = 'Beschreibung';
+$labels['datetime'] = 'Datum/Zeit';
+
+$labels['all'] = 'Alle';
+$labels['flagged'] = 'Markiert';
+$labels['complete'] = 'Erledigt';
+$labels['overdue'] = 'Ãberfällig';
+$labels['today'] = 'Heute';
+$labels['tomorrow'] = 'Morgen';
+$labels['next7days'] = 'Nächste 7 Tage';
+$labels['later'] = 'Später';
+$labels['nodate'] = 'kein Datum';
+
+$labels['taskdetails'] = 'Details';
+$labels['newtask'] = 'Neue Aufgabe';
+$labels['edittask'] = 'Aufgabe bearbeiten';
+$labels['save'] = 'Speichern';
+$labels['cancel'] = 'Abbrechen';
+$labels['addsubtask'] = 'Neue Teilaufgabe';
+
+// date words
+$labels['on'] = 'am';
+$labels['at'] = 'um';
+$labels['this'] = 'diesen';
+$labels['next'] = 'nächsten';
+
+// mesages
+$labels['savingdata'] = 'Daten werden gespeichert...';
+$labels['errorsaving'] = 'Fehler beim Speichern.';
+$labels['notasksfound'] = 'Für die aktuellen Kriterien wurden keine Aufgaben gefunden.';
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
new file mode 100644
index 0000000..cbdcffb
--- /dev/null
+++ b/plugins/tasklist/localization/en_US.inc
@@ -0,0 +1,43 @@
+<?php
+
+$labels = array();
+$labels['navtitle'] = 'Tasks';
+$labels['lists'] = 'Resources';
+$labels['list'] = 'Resource';
+
+$labels['createnewtask'] = 'Create new Task';
+$labels['mark'] = 'Mark';
+$labels['unmark'] = 'Unmark';
+$labels['edit'] = 'Edit';
+$labels['delete'] = 'Delete';
+$labels['title'] = 'Title';
+$labels['description'] = 'Description';
+$labels['datetime'] = 'Date/Time';
+
+$labels['all'] = 'All';
+$labels['flagged'] = 'Flagged';
+$labels['complete'] = 'Complete';
+$labels['overdue'] = 'Overdue';
+$labels['today'] = 'Today';
+$labels['tomorrow'] = 'Tomorrow';
+$labels['next7days'] = 'Next 7 days';
+$labels['later'] = 'Later';
+$labels['nodate'] = 'no date';
+
+$labels['taskdetails'] = 'Details';
+$labels['newtask'] = 'New Task';
+$labels['edittask'] = 'Edit Task';
+$labels['save'] = 'Save';
+$labels['cancel'] = 'Cancel';
+$labels['addsubtask'] = 'Add subtask';
+
+// date words
+$labels['on'] = 'on';
+$labels['at'] = 'at';
+$labels['this'] = 'this';
+$labels['next'] = 'next';
+
+// mesages
+$labels['savingdata'] = 'Saving data...';
+$labels['errorsaving'] = 'Failed to save data.';
+$labels['notasksfound'] = 'No tasks found for the given criteria';
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
new file mode 100644
index 0000000..fa00d6e
Binary files /dev/null and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/taskbaricon.png b/plugins/tasklist/skins/larry/taskbaricon.png
new file mode 100644
index 0000000..b0141ff
Binary files /dev/null and b/plugins/tasklist/skins/larry/taskbaricon.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
new file mode 100644
index 0000000..8811766
--- /dev/null
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -0,0 +1,504 @@
+/**
+ * Roundcube Taklist plugin styles for skin "Larry"
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
+ *
+ * The contents are subject to the Creative Commons Attribution-ShareAlike
+ * License. It is allowed to copy, distribute, transmit and to adapt the work
+ * by keeping credits to the original autors in the README file.
+ * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
+ *
+ * $Id$
+ */
+
+#taskbar a.button-tasklist span.button-inner {
+ background-image: url(taskbaricon.png);
+ background-position: 0 0;
+}
+
+#taskbar a.button-tasklist:hover span.button-inner,
+#taskbar a.button-tasklist.button-selected span.button-inner {
+ background-position: 0 -44px;
+}
+
+#sidebar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 240px;
+}
+
+body.tasklistview #searchmenulink {
+ width: 15px;
+}
+
+#selectorbox {
+ position: absolute;
+ top: 42px;
+ left: 0;
+ width: 100%;
+ height: 242px;
+}
+
+#tasklistsbox {
+ position: absolute;
+ top: 300px;
+ left: 0;
+ width: 100%;
+ bottom: 0px;
+}
+
+#taskselector li {
+ position: relative;
+}
+
+#taskselector li:first-child {
+ border-top: 0;
+ border-radius: 4px 4px 0 0;
+}
+
+#taskselector li:last-child {
+ border-bottom: 0;
+ border-radius: 0 0 4px 4px;
+}
+
+#taskselector li.selected {
+ background-color: #c7e3ef;
+}
+
+#taskselector li.overdue a {
+ color: #b72a2a;
+ font-weight: bold;
+}
+
+#taskselector li.inactive a {
+ color: #97b3bf;
+}
+
+#taskselector li .count {
+ display: none;
+ position: absolute;
+ top: 3px;
+ right: 6px;
+ min-width: 1.8em;
+ padding: 2px 4px;
+ background: #d9ecf4;
+ background: -moz-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9ecf4), color-stop(100%,#c7e3ef));
+ background: -o-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+ background: -ms-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+ background: linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+ box-shadow: inset 0 1px 1px 0 #b7d3df;
+ -o-box-shadow: inset 0 1px 1px 0 #b7d3df;
+ -webkit-box-shadow: inset 0 1px 1px 0 #b7d3df;
+ -moz-box-shadow: inset 0 1px 1px 0 #b7d3df;
+ border: 1px solid #a7c3cf;
+ border-radius: 9px;
+ color: #69939e;
+ text-align: center;
+ font-weight: bold;
+ text-shadow: none;
+}
+
+#taskselector li.selected .count {
+ color: #fff;
+ background: #005d76;
+ background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
+ background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
+ background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
+ background: linear-gradient(top, #005d76 0%, #004558 100%);
+ box-shadow: inset 0 1px 1px 0 #003645;
+ -o-box-shadow: inset 0 1px 1px 0 #003645;
+ -webkit-box-shadow: inset 0 1px 1px 0 #003645;
+ -moz-box-shadow: inset 0 1px 1px 0 #003645;
+ border-color: #003645;
+}
+
+#taskselector li.overdue.selected .count {
+ background: #db3333;
+ background: -moz-linear-gradient(top, #db3333 0%, #a82727 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#db3333), color-stop(100%,#a82727));
+ background: -o-linear-gradient(top, #db3333 0%, #a82727 100%);
+ background: -ms-linear-gradient(top, #db3333 0%, #a82727 100%);
+ background: linear-gradient(top, #db3333 0%, #a82727 100%);
+ box-shadow: inset 0 1px 1px 0 #831f1f;
+ -o-box-shadow: inset 0 1px 1px 0 #831f1f;
+ -webkit-box-shadow: inset 0 1px 1px 0 #831f1f;
+ -moz-box-shadow: inset 0 1px 1px 0 #831f1f;
+ border-color: #831f1f;
+}
+
+#tasklists li {
+ margin: 0;
+ height: 20px;
+ padding: 6px 8px 2px;
+ display: block;
+ position: relative;
+ white-space: nowrap;
+}
+
+#tasklists li label {
+ display: block;
+}
+
+#tasklists li span.listname {
+ cursor: default;
+ padding-bottom: 2px;
+ color: #004458;
+}
+
+#tasklists li span.handle {
+ display: none;
+}
+
+#tasklists li input {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+}
+
+#tasklists li.selected {
+ background-color: #c7e3ef;
+}
+
+#tasklists li.selected span.calname {
+ font-weight: bold;
+}
+
+#mainview-right {
+ position: absolute;
+ top: 0;
+ left: 256px;
+ right: 0;
+ bottom: 0;
+}
+
+#taskstoolbar {
+ position: absolute;
+ top: -6px;
+ right: 0;
+ width: 40%;
+ height: 40px;
+ white-space: nowrap;
+ text-align: right;
+}
+
+#quickaddbox {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 60%;
+ height: 32px;
+ white-space: nowrap;
+}
+
+#quickaddinput {
+ width: 85%;
+ margin: 0;
+ padding: 5px 8px;
+ background: #f1f1f1;
+ background: rgba(255, 255, 255, 0.7);
+ border-color: #a3a3a3;
+ font-weight: bold;
+}
+
+#quickaddbox .button {
+ margin-left: 5px;
+ padding: 3px 10px;
+ font-weight: bold;
+}
+
+#tasksview {
+ position: absolute;
+ top: 42px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ padding-bottom: 28px;
+ background: rgba(255, 255, 255, 0.3);
+}
+
+#message.statusbar {
+ border-top: 1px solid #c3c3c3;
+}
+
+#tasksview .scroller {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ bottom: 28px;
+ overflow: auto;
+}
+
+#thelist {
+ padding: 0;
+ margin: 1em;
+ list-style: none;
+}
+
+#listmessagebox {
+ display: none;
+ font-size: 14px;
+ color: #666;
+ margin: 1.5em;
+ text-shadow: 0px 1px 1px #fff;
+ text-align:center;
+}
+
+.taskitem {
+ display: block;
+ margin-bottom: 5px;
+}
+
+.taskitem.dragging {
+ opacity: 0.5;
+}
+
+.taskitem .childtasks {
+ padding: 0;
+ margin: 0.5em 0 0 2em;
+ list-style: none;
+}
+
+.taskhead {
+ position: relative;
+ padding: 4px 5px 3px 5px;
+ border: 1px solid #fff;
+ border-radius: 5px;
+ background: #fff;
+ -webkit-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
+ -moz-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
+ box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
+ padding-right: 11em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: default;
+}
+
+.taskhead.droptarget {
+ border-color: #4787b1;
+ box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+ -moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+ -webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+ -o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+}
+
+.taskhead .complete {
+ margin: -1px 1em 0 0;
+}
+
+.taskhead .title {
+ font-size: 12px;
+}
+
+.taskhead .flagged {
+ display: inline-block;
+ visibility: hidden;
+ width: 16px;
+ height: 16px;
+ background: url(sprites.png) -2px -3px no-repeat;
+ margin: -3px 1em 0 0;
+ vertical-align: middle;
+ cursor: pointer;
+}
+
+.taskhead:hover .flagged {
+ visibility: visible;
+}
+
+.taskhead.flagged .flagged {
+ visibility: visible;
+ background-position: -2px -23px;
+}
+
+.taskhead .date {
+ position: absolute;
+ top: 6px;
+ right: 30px;
+ text-align: right;
+ cursor: pointer;
+}
+
+.taskhead.nodate .date {
+ color: #ddd;
+}
+
+.taskhead.overdue .date {
+ color: #d00;
+}
+
+.taskhead.nodate:hover .date {
+ color: #999;
+}
+
+.taskhead .date input {
+ padding: 1px 2px;
+ border: 1px solid #ddd;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ outline: none;
+ text-align: right;
+}
+
+.taskhead .actions,
+.taskhead .delete {
+ display: block;
+ visibility: hidden;
+ position: absolute;
+ top: 3px;
+ right: 6px;
+ width: 18px;
+ height: 18px;
+ background: url(sprites.png) 0 -80px no-repeat;
+ text-indent: -1000px;
+ overflow: hidden;
+ cursor: pointer;
+}
+
+.taskhead .delete {
+ background-position: 0 -40px;
+}
+
+.taskhead:hover .actions,
+.taskhead:hover .delete {
+ visibility: visible;
+}
+
+.taskhead.complete {
+ opacity: 0.6;
+}
+
+.taskhead.complete .title {
+ text-decoration: line-through;
+}
+
+.taskhead .progressbar {
+ position: absolute;
+ bottom: 1px;
+ left: 6px;
+ right: 6px;
+ height: 2px;
+}
+
+.taskhead.complete .progressbar {
+ display: none;
+}
+
+.taskhead .progressvalue {
+ height: 1px;
+ background: rgba(1, 124, 180, 0.2);
+ border-top: 1px solid #219de6;
+}
+
+ul.toolbarmenu li span.add {
+ background-image: url(sprites.png);
+ background-position: 0 -100px;
+}
+
+ul.toolbarmenu li span.delete {
+ background-position: 0 -1508px;
+}
+
+.taskitem-draghelper {
+/*
+ width: 32px;
+ height: 26px;
+*/
+ background: #444;
+ border: 1px solid #555;
+ border-radius: 4px;
+ box-shadow: 0 2px 6px 0 #333;
+ -moz-box-shadow: 0 2px 6px 0 #333;
+ -webkit-box-shadow: 0 2px 6px 0 #333;
+ -o-box-shadow: 0 2px 6px 0 #333;
+ z-index: 5000;
+ padding: 2px 10px;
+ font-size: 20px;
+ color: #ccc;
+ opacity: 0.92;
+ filter: alpha(opacity=92);
+ text-shadow: 0px 1px 1px #333;
+}
+
+#rootdroppable {
+ display: none;
+ position: absolute;
+ top: 3px;
+ left: 1em;
+ right: 1em;
+ height: 5px;
+ background: #ddd;
+ border-radius: 3px;
+}
+
+#rootdroppable.droptarget {
+ background: #4787b1;
+ box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+ -moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+ -webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+ -o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+
+}
+
+/*** task edit form ***/
+
+#taskedit,
+#taskshow {
+ display:none;
+}
+
+#taskshow h2 {
+ margin-top: -0.5em;
+}
+
+#taskshow label {
+ color: #999;
+}
+
+a.morelink {
+ font-size: 90%;
+ color: #0069a6;
+ text-decoration: none;
+ outline: none;
+}
+
+a.morelink:hover {
+ text-decoration: underline;
+}
+
+#taskeditform input.text,
+#taskeditform textarea {
+ width: 97%;
+}
+
+div.form-section {
+ position: relative;
+ margin-top: 0.2em;
+ margin-bottom: 0.8em;
+}
+
+.form-section label {
+ display: inline-block;
+ min-width: 7em;
+ padding-right: 0.5em;
+}
+
+label.block {
+ display: block;
+ margin-bottom: 0.3em;
+}
+
+#edit-completeness-slider {
+ display: inline-block;
+ margin-left: 2em;
+ width: 30em;
+ height: 0.8em;
+ border: 1px solid #ccc;
+}
+
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
new file mode 100644
index 0000000..fd8fa5c
--- /dev/null
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -0,0 +1,141 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body class="tasklistview noscroll">
+
+<roundcube:include file="/includes/header.html" />
+
+<div id="mainscreen">
+ <div id="sidebar">
+ <div id="quicksearchbar">
+ <roundcube:object name="plugin.searchform" id="quicksearchbox" />
+ <a id="searchmenulink" class="iconbutton searchoptions" > </a>
+ <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
+ </div>
+
+ <div id="selectorbox" class="uibox listbox">
+ <div class="scroller">
+ <ul id="taskselector" class="listing">
+ <li class="all selected"><a href="#all"><roundcube:label name="tasklist.all" /><span class="count"></span></a></li>
+ <li class="overdue inactive"><a href="#overdue"><roundcube:label name="tasklist.overdue" /><span class="count"></span></a></li>
+ <li class="flagged"><a href="#flagged"><roundcube:label name="tasklist.flagged" /><span class="count"></span></a></li>
+ <li class="today"><a href="#today"><roundcube:label name="tasklist.today" /><span class="count"></span></a></li>
+ <li class="tomorrow"><a href="#tomorrow"><roundcube:label name="tasklist.tomorrow" /><span class="count"></span></a></li>
+ <li class="week"><a href="#week"><roundcube:label name="tasklist.next7days" /></a></li>
+ <li class="later"><a href="#later"><roundcube:label name="tasklist.later" /></a></li>
+ <li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
+ <li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
+ </ul>
+ </div>
+ </div>
+
+ <div id="tasklistsbox" class="uibox listbox">
+ <h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
+ <div class="scroller withfooter">
+ <roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
+ </div>
+ <div class="boxfooter">
+ <roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
+ </div>
+ </div>
+ </div>
+
+ <div id="mainview-right">
+
+ <div id="quickaddbox">
+ <roundcube:object name="plugin.quickaddform" />
+ </div>
+
+ <div id="taskstoolbar" class="toolbar">
+ <roundcube:container name="toolbar" id="taskstoolbar" />
+ </div>
+
+ <div id="tasksview" class="uibox">
+ <div class="scroller">
+ <roundcube:object name="plugin.tasks" id="thelist" />
+ <div id="listmessagebox"></div>
+ </div>
+ <div id="rootdroppable"></div>
+ <roundcube:object name="message" id="message" class="statusbar" />
+ </div>
+
+ </div>
+
+</div>
+
+<div id="taskitemmenu" class="popupmenu">
+ <ul class="toolbarmenu iconized">
+ <li><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
+ <li><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
+ <li><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
+ </ul>
+</div>
+
+<div id="taskshow">
+ <div class="form-section">
+ <h2 id="task-title"></h2>
+ </div>
+ <div id="task-description" class="form-section">
+ </div>
+ <div id="task-date" class="form-section">
+ <label><roundcube:label name="tasklist.datetime" /></label>
+ <span class="task-text"></span>
+ <span id="task-time"></span>
+ </div>
+ <div id="task-list" class="form-section">
+ <label><roundcube:label name="tasklist.list" /></label>
+ <span class="task-text"></span>
+ </div>
+ <div id="task-completeness" class="form-section">
+ <label><roundcube:label name="tasklist.complete" /></label>
+ <span class="task-text"></span>
+ </div>
+</div>
+
+<div id="taskedit">
+ <form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
+ <div class="form-section">
+ <label for="edit-title"><roundcube:label name="tasklist.title" /></label>
+ <br />
+ <input type="text" class="text" name="title" id="edit-title" size="40" />
+ </div>
+ <div class="form-section">
+ <label for="edit-description"><roundcube:label name="tasklist.description" /></label>
+ <br />
+ <textarea name="description" id="edit-description" class="text" rows="5" cols="40"></textarea>
+ </div>
+ <div class="form-section">
+ <label for="edit-date"><roundcube:label name="tasklist.datetime" /></label>
+ <input type="text" name="date" size="10" id="edit-date" />
+ <input type="text" name="time" size="6" id="edit-time" />
+ <a href="#nodate" style="margin-left:1em" id="edit-nodate"><roundcube:label name="tasklist.nodate" /></a>
+ </div>
+ <div class="form-section">
+ <label for="edit-completeness"><roundcube:label name="tasklist.complete" /></label>
+ <input type="text" name="title" id="edit-completeness" size="3" /> %
+ <div id="edit-completeness-slider"></div>
+ </div>
+ <div class="form-section" id="tasklist-select">
+ <label for="edit-tasklist"><roundcube:label name="tasklist.list" /></label>
+ <roundcube:object name="plugin.tasklist_select" id="edit-tasklist" />
+ </div>
+ </form>
+</div>
+
+<script type="text/javascript">
+
+// UI startup
+var UI = new rcube_mail_ui();
+
+$(document).ready(function(e){
+ UI.init();
+
+});
+
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
new file mode 100644
index 0000000..c76e411
--- /dev/null
+++ b/plugins/tasklist/tasklist.js
@@ -0,0 +1,872 @@
+/**
+ * Client scripts for the Tasklist plugin
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function rcube_tasklist(settings)
+{
+ /* constants */
+ var FILTER_MASK_ALL = 0;
+ var FILTER_MASK_TODAY = 1;
+ var FILTER_MASK_TOMORROW = 2;
+ var FILTER_MASK_WEEK = 4;
+ var FILTER_MASK_LATER = 8;
+ var FILTER_MASK_NODATE = 16;
+ var FILTER_MASK_OVERDUE = 32;
+ var FILTER_MASK_FLAGGED = 64;
+ var FILTER_MASK_COMPLETE = 128;
+
+ var filter_masks = {
+ all: FILTER_MASK_ALL,
+ today: FILTER_MASK_TODAY,
+ tomorrow: FILTER_MASK_TOMORROW,
+ week: FILTER_MASK_WEEK,
+ later: FILTER_MASK_LATER,
+ nodate: FILTER_MASK_NODATE,
+ overdue: FILTER_MASK_OVERDUE,
+ flagged: FILTER_MASK_FLAGGED,
+ complete: FILTER_MASK_COMPLETE
+ };
+
+ /* private vars */
+ var selector = 'all';
+ var filtermask = FILTER_MASK_ALL;
+ var idcount = 0;
+ var selected_list;
+ var saving_lock;
+ var ui_loading;
+ var taskcounts = {};
+ var listdata = {};
+ var draghelper;
+ var completeness_slider;
+ var search_request;
+ var search_query;
+ var me = this;
+
+ // general datepicker settings
+ var datepicker_settings = {
+ // translate from PHP format to datepicker format
+ dateFormat: settings['date_format'].replace(/m/, 'mm').replace(/n/g, 'm').replace(/F/, 'MM').replace(/l/, 'DD').replace(/dd/, 'D').replace(/d/, 'dd').replace(/j/, 'd').replace(/Y/g, 'yy'),
+ firstDay : settings['first_day'],
+// dayNamesMin: settings['days_short'],
+// monthNames: settings['months'],
+// monthNamesShort: settings['months'],
+ changeMonth: false,
+ showOtherMonths: true,
+ selectOtherMonths: true
+ };
+ var extended_datepicker_settings;
+
+ /* public members */
+ this.tasklists = rcmail.env.tasklists;
+ this.selected_task;
+
+ /* public methods */
+ this.init = init;
+ this.edit_task = task_edit_dialog;
+ this.delete_task = delete_task;
+ this.add_childtask = add_childtask;
+ this.quicksearch = quicksearch;
+ this.reset_search = reset_search;
+
+
+ /**
+ * initialize the tasks UI
+ */
+ function init()
+ {
+ // select the first task list
+ for (var s in me.tasklists) {
+ selected_list = s;
+ break;
+ };
+
+ // register server callbacks
+ rcmail.addEventListener('plugin.data_ready', data_ready);
+ rcmail.addEventListener('plugin.refresh_task', update_taskitem);
+ rcmail.addEventListener('plugin.update_counts', update_counts);
+ rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); });
+ rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, saving_lock); });
+
+ // start loading tasks
+ fetch_counts();
+ list_tasks();
+
+ // register event handlers for UI elements
+ $('#taskselector a').click(function(e){
+ if (!$(this).parent().hasClass('inactive'))
+ list_tasks(this.href.replace(/^.*#/, ''));
+ return false;
+ });
+
+ // quick-add a task
+ $(rcmail.gui_objects.quickaddform).submit(function(e){
+ var tasktext = this.elements.text.value;
+ var rec = { id:-(++idcount), title:tasktext, readonly:true, mask:0, complete:0 };
+
+ save_task({ tempid:rec.id, raw:tasktext, list:selected_list }, 'new');
+ render_task(rec);
+
+ // clear form
+ this.reset();
+ return false;
+ });
+
+ // click-handler on task list items (delegate)
+ $(rcmail.gui_objects.resultlist).click(function(e){
+ var item = $(e.target);
+
+ if (!item.hasClass('taskhead'))
+ item = item.closest('div.taskhead');
+
+ // ignore
+ if (!item.length)
+ return;
+
+ var id = item.data('id'),
+ li = item.parent(),
+ rec = listdata[id];
+
+ switch (e.target.className) {
+ case 'complete':
+ rec.complete = e.target.checked ? 1 : 0;
+ li.toggleClass('complete');
+ save_task(rec, 'edit');
+ return true;
+
+ case 'flagged':
+ rec.flagged = rec.flagged ? 0 : 1;
+ li.toggleClass('flagged');
+ save_task(rec, 'edit');
+ break;
+
+ case 'date':
+ var link = $(e.target).html(''),
+ input = $('<input type="text" size="10" />').appendTo(link).val(rec.date || '')
+
+ input.datepicker($.extend({
+ onClose: function(dateText, inst) {
+ if (dateText != rec.date) {
+ rec.date = dateText;
+ save_task(rec, 'edit');
+ }
+ input.datepicker('destroy').remove();
+ link.html(dateText || rcmail.gettext('nodate','tasklist'));
+ },
+ }, extended_datepicker_settings)
+ )
+ .datepicker('setDate', rec.date)
+ .datepicker('show');
+ break;
+
+ case 'delete':
+ delete_task(id);
+ break;
+
+ case 'actions':
+ var pos, ref = $(e.target),
+ menu = $('#taskitemmenu');
+ if (menu.is(':visible') && menu.data('refid') == id) {
+ menu.hide();
+ }
+ else {
+ pos = ref.offset();
+ pos.top += ref.outerHeight();
+ pos.left += ref.width() - menu.outerWidth();
+ menu.css({ top:pos.top+'px', left:pos.left+'px' }).show();
+ menu.data('refid', id);
+ me.selected_task = rec;
+ }
+ e.bubble = false;
+ break;
+
+ default:
+ if (e.target.nodeName != 'INPUT')
+ task_show_dialog(id);
+ break;
+ }
+
+ return false;
+ })
+ .dblclick(function(e){
+ var id, rec, item = $(e.target);
+ if (!item.hasClass('taskhead'))
+ item = item.closest('div.taskhead');
+
+ if (item.length && (id = item.data('id')) && (rec = listdata[id])) {
+ var list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
+ if (rec.readonly || list.readonly)
+ task_show_dialog(id);
+ else
+ task_edit_dialog(id, 'edit');
+ clearSelection();
+ }
+ });
+
+ completeness_slider = $('#edit-completeness-slider').slider({
+ range: 'min',
+ slide: function(e, ui){
+ var v = completeness_slider.slider('value');
+ if (v >= 98) v = 100;
+ if (v <= 2) v = 0;
+ $('#edit-completeness').val(v);
+ }
+ });
+ $('#edit-completeness').change(function(e){ completeness_slider.slider('value', parseInt(this.value)) });
+
+ // handle global document clicks: close popup menus
+ $(document.body).click(clear_popups);
+
+ // extended datepicker settings
+ extended_datepicker_settings = $.extend({
+ showButtonPanel: true,
+ beforeShow: function(input, inst) {
+ setTimeout(function(){
+ $(input).datepicker('widget').find('button.ui-datepicker-close')
+ .html(rcmail.gettext('nodate','tasklist'))
+ .attr('onclick', '')
+ .click(function(e){
+ $(input).datepicker('setDate', null).datepicker('hide');
+ });
+ }, 1);
+ },
+ }, datepicker_settings);
+ }
+
+ /**
+ * Request counts from the server
+ */
+ function fetch_counts()
+ {
+ rcmail.http_request('counts');
+ }
+
+ /**
+ * fetch tasks from server
+ */
+ function list_tasks(sel)
+ {
+ if (sel && filter_masks[sel] !== undefined) {
+ filtermask = filter_masks[sel];
+ selector = sel;
+ }
+
+ ui_loading = rcmail.set_busy(true, 'loading');
+ rcmail.http_request('fetch', { filter:filtermask, q:search_query }, true);
+
+ $('#taskselector li.selected').removeClass('selected');
+ $('#taskselector li.'+selector).addClass('selected');
+ }
+
+ /**
+ * callback if task data from server is ready
+ */
+ function data_ready(data)
+ {
+ // clear display
+ var msgbox = $('#listmessagebox').hide(),
+ list = $(rcmail.gui_objects.resultlist).html('');
+ listdata = {};
+
+ for (var i=0; i < data.length; i++) {
+ listdata[data[i].id] = data[i];
+ render_task(data[i]);
+ }
+
+ if (!data.length)
+ msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
+
+ rcmail.set_busy(false, 'loading', ui_loading);
+ }
+
+ /**
+ *
+ */
+ function update_counts(counts)
+ {
+ // got new data
+ if (counts)
+ taskcounts = counts;
+
+ // iterate over all selector links and update counts
+ $('#taskselector a').each(function(i, elem){
+ var link = $(elem),
+ f = link.parent().attr('class').replace(/\s\w+/, '');
+ link.children('span').html(taskcounts[f] || '')[(taskcounts[f] ? 'show' : 'hide')]();
+ });
+
+ // spacial case: overdue
+ $('#taskselector li.overdue')[(taskcounts.overdue ? 'removeClass' : 'addClass')]('inactive');
+ }
+
+ /**
+ * Callback from server to update a single task item
+ */
+ function update_taskitem(rec)
+ {
+ var id = rec.id;
+ listdata[id] = rec;
+ render_task(rec, rec.tempid || id);
+ }
+
+ /**
+ * Submit the given (changed) task record to the server
+ */
+ function save_task(rec, action)
+ {
+ if (!rcmail.busy) {
+ saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+ rcmail.http_post('task', { action:action, t:rec, filter:filtermask });
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Render the given task into the tasks list
+ */
+ function render_task(rec, replace)
+ {
+ var div = $('<div>').addClass('taskhead').html(
+ '<div class="progressbar"><div class="progressvalue" style="width:' + (rec.complete * 100) + '%"></div></div>' +
+ '<input type="checkbox" name="completed[]" value="1" class="complete" ' + (rec.complete == 1.0 ? 'checked="checked" ' : '') + '/>' +
+ '<span class="flagged"></span>' +
+ '<span class="title">' + Q(rec.title) + '</span>' +
+ '<span class="date">' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '</span>' +
+ '<a href="#" class="actions">V</a>'
+ )
+ .data('id', rec.id)
+ .draggable({
+ revert: 'invalid',
+ addClasses: false,
+ cursorAt: { left:-10, top:12 },
+ helper: draggable_helper,
+ appendTo: 'body',
+ start: draggable_start,
+ stop: draggable_stop,
+ revertDuration: 300
+ });
+
+ if (rec.complete == 1.0)
+ div.addClass('complete');
+ if (rec.flagged)
+ div.addClass('flagged');
+ if (!rec.date)
+ div.addClass('nodate');
+ if ((rec.mask & FILTER_MASK_OVERDUE))
+ div.addClass('overdue');
+console.log(replace)
+ var li, parent;
+ if (replace && (li = $('li[rel="'+replace+'"]', rcmail.gui_objects.resultlist)) && li.length) {
+ li.children('div.taskhead').first().replaceWith(div);
+ li.attr('rel', rec.id);
+ }
+ else {
+ li = $('<li>')
+ .attr('rel', rec.id)
+ .addClass('taskitem')
+ .append(div)
+ .append('<ul class="childtasks"></ul>');
+
+ if (rec.parent_id && (parent = $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist)) && parent.length)
+ li.appendTo(parent);
+ else
+ li.appendTo(rcmail.gui_objects.resultlist);
+ }
+
+ if (replace) {
+ resort_task(rec, li, true);
+ // TODO: remove the item after a while if it doesn't match the current filter anymore
+ }
+ }
+
+ /**
+ * Move the given task item to the right place in the list
+ */
+ function resort_task(rec, li, animated)
+ {
+ var dir = 0, next_li, next_id, next_rec;
+
+ // animated moving
+ var insert_animated = function(li, before, after) {
+ if (before && li.next().get(0) == before.get(0))
+ return; // nothing to do
+ else if (after && li.prev().get(0) == after.get(0))
+ return; // nothing to do
+
+ var speed = 300;
+ li.slideUp(speed, function(){
+ if (before) li.insertBefore(before);
+ else if (after) li.insertAfter(after);
+ li.slideDown(speed);
+ });
+ }
+
+ // find the right place to insert the task item
+ li.siblings().each(function(i, elem){
+ next_li = $(elem);
+ next_id = next_li.attr('rel');
+ next_rec = listdata[next_id];
+
+ if (next_id == rec.id) {
+ next_li = null;
+ return 1; // continue
+ }
+
+ if (next_rec && task_cmp(rec, next_rec) > 0) {
+ return 1; // continue;
+ }
+ else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) {
+ if (animated) insert_animated(li, next_li);
+ else li.insertBefore(next_li)
+ next_li = null;
+ return false;
+ }
+ });
+
+ if (next_li) {
+ if (animated) insert_animated(li, null, next_li);
+ else li.insertAfter(next_li);
+ }
+ return;
+ }
+
+ /**
+ * Compare function of two task records.
+ * (used for sorting)
+ */
+ function task_cmp(a, b)
+ {
+ var d = Math.floor(a.complete) - Math.floor(b.complete);
+ if (!d) d = (b._hasdate-0) - (a._hasdate-0);
+ if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999);
+ return d;
+ }
+
+
+ /* Helper functions for drag & drop functionality */
+
+ function draggable_helper()
+ {
+ if (!draghelper)
+ draghelper = $('<div class="taskitem-draghelper">✔</div>');
+
+ return draghelper;
+ }
+
+ function draggable_start(event, ui)
+ {
+ $('.taskhead, #rootdroppable').droppable({
+ hoverClass: 'droptarget',
+ accept: droppable_accept,
+ drop: draggable_dropped,
+ addClasses: false
+ });
+
+ $(this).parent().addClass('dragging');
+ $('#rootdroppable').show();
+ }
+
+ function draggable_stop(event, ui)
+ {
+ $(this).parent().removeClass('dragging');
+ $('#rootdroppable').hide();
+ }
+
+ function droppable_accept(draggable)
+ {
+ var drag_id = draggable.data('id'),
+ parent_id = $(this).data('id'),
+ rec = listdata[parent_id];
+
+ if (parent_id == listdata[drag_id].parent_id)
+ return false;
+
+ while (rec && rec.parent_id) {
+ if (rec.parent_id == drag_id)
+ return false;
+ rec = listdata[rec.parent_id];
+ }
+
+ return true;
+ }
+
+ function draggable_dropped(event, ui)
+ {
+ var parent_id = $(this).data('id'),
+ task_id = ui.draggable.data('id'),
+ parent = parent_id ? $('li[rel="'+parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : $(rcmail.gui_objects.resultlist),
+ rec = listdata[task_id],
+ li;
+
+ if (rec && parent.length) {
+ // submit changes to server
+ rec.parent_id = parent_id || 0;
+ save_task(rec, 'edit');
+
+ li = ui.draggable.parent();
+ li.slideUp(300, function(){
+ li.appendTo(parent);
+ resort_task(rec, li);
+ li.slideDown(300);
+ });
+ }
+ }
+
+
+ /**
+ * Show task details in a dialog
+ */
+ function task_show_dialog(id)
+ {
+ var $dialog = $('#taskshow').dialog('close'), rec;;
+
+ if (!(rec = listdata[id]) || clear_popups({}))
+ return;
+
+ me.selected_task = rec;
+
+ // fill dialog data
+ $('#task-title').html(Q(rec.title || ''));
+ $('#task-description').html(text2html(rec.description || '', 300, 6))[(rec.description ? 'show' : 'hide')]();
+ $('#task-date')[(rec.date ? 'show' : 'hide')]().children('.task-text').html(Q(rec.date || rcmail.gettext('nodate','tasklist')));
+ $('#task-time').html(Q(rec.time || ''));
+ $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
+ $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
+
+ // define dialog buttons
+ var buttons = {};
+ buttons[rcmail.gettext('edit','tasklist')] = function() {
+ task_edit_dialog(me.selected_task.id, 'edit');
+ $dialog.dialog('close');
+ };
+
+ buttons[rcmail.gettext('delete','tasklist')] = function() {
+ if (delete_task(me.selected_task.id))
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('taskdetails', 'tasklist'),
+ close: function() {
+ $dialog.dialog('destroy').appendTo(document.body);
+ },
+ buttons: buttons,
+ minWidth: 500,
+ width: 580
+ }).show();
+ }
+
+ /**
+ * Opens the dialog to edit a task
+ */
+ function task_edit_dialog(id, action, presets)
+ {
+ $('#taskshow').dialog('close');
+
+ var rec = listdata[id] || presets,
+ $dialog = $('<div>'),
+ editform = $('#taskedit'),
+ list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] :
+ (selected_list ? me.tasklists[selected_list] : { editable: action=='new' });
+
+ if (list.readonly || (action == 'edit' && (!rec || rec.readonly || rec.temp)))
+ return false;
+
+ me.selected_task = $.extend({}, rec); // clone task object
+
+ // fill form data
+ var title = $('#edit-title').val(rec.title || '');
+ var description = $('#edit-description').val(rec.description || '');
+ var recdate = $('#edit-date').val(rec.date || '').datepicker(datepicker_settings);
+ var rectime = $('#edit-time').val(rec.time || '');
+ var complete = $('#edit-completeness').val((rec.complete || 0) * 100);
+ completeness_slider.slider('value', complete.val());
+ var tasklist = $('#edit-tasklist').val(rec.list || 0);
+
+ $('#edit-nodate').unbind('click').click(function(){
+ recdate.val('');
+ rectime.val('');
+ return false;
+ })
+
+ // define dialog buttons
+ var buttons = {};
+ buttons[rcmail.gettext('save', 'tasklist')] = function() {
+ me.selected_task.title = title.val();
+ me.selected_task.description = description.val();
+ me.selected_task.date = recdate.val();
+ me.selected_task.time = rectime.val();
+ me.selected_task.list = tasklist.val();
+
+ if (me.selected_task.list && me.selected_task.list != rec.list)
+ me.selected_task._fromlist = rec.list;
+
+ me.selected_task.complete = complete.val() / 100;
+ if (isNaN(me.selected_task.complete))
+ me.selected_task.complete = null;
+
+ if (!me.selected_task.list && list.id)
+ me.selected_task.list = list.id;
+
+ if (save_task(me.selected_task, action))
+ $dialog.dialog('close');
+ };
+
+ if (rec.id) {
+ buttons[rcmail.gettext('delete', 'tasklist')] = function() {
+ if (delete_task(rec.id))
+ $dialog.dialog('close');
+ };
+ }
+
+ buttons[rcmail.gettext('cancel', 'tasklist')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
+ closeOnEscape: false,
+ title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'),
+ close: function() {
+ editform.hide().appendTo(document.body);
+ $dialog.dialog('destroy').remove();
+ },
+ buttons: buttons,
+ minWidth: 500,
+ width: 580
+ }).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
+
+ title.select();
+ }
+
+ /**
+ *
+ */
+ function add_childtask(id)
+ {
+ task_edit_dialog(null, 'new', { parent_id:id });
+ }
+
+ /**
+ * Delete the given task
+ */
+ function delete_task(id)
+ {
+ var rec = listdata[id];
+ if (rec && confirm("Delete this?")) {
+ saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+ rcmail.http_post('task', { action:'delete', t:rec, filter:filtermask });
+ $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the given task matches the current filtermask
+ */
+ function match_filter(rec)
+ {
+ // TBD.
+ return true;
+ }
+
+ /**
+ * Execute search
+ */
+ function quicksearch()
+ {
+ var q;
+ if (rcmail.gui_objects.qsearchbox && (q = rcmail.gui_objects.qsearchbox.value)) {
+ var id = 'search-'+q;
+ var resources = [];
+
+ for (var rid in me.tasklists) {
+ if (me.tasklists[rid].active) {
+ resources.push(rid);
+ }
+ }
+ id += '@'+resources.join(',');
+
+ // ignore if query didn't change
+ if (search_request == id)
+ return;
+
+ search_request = id;
+ search_query = q;
+
+ list_tasks('all');
+ }
+ else // empty search input equals reset
+ this.reset_search();
+ }
+
+ /**
+ * Reset search and get back to normal listing
+ */
+ function reset_search()
+ {
+ $(rcmail.gui_objects.qsearchbox).val('');
+
+ if (search_request) {
+ search_request = search_query = null;
+ list_tasks();
+ }
+ }
+
+
+ /**** Utility functions ****/
+
+ /**
+ * quote html entities
+ */
+ function Q(str)
+ {
+ return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
+ }
+
+ /**
+ * Name says it all
+ * (cloned from calendar plugin)
+ */
+ function text2html(str, maxlen, maxlines)
+ {
+ var html = Q(String(str));
+
+ // limit visible text length
+ if (maxlen) {
+ var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','tasklist')+'</a><span style="display:none">',
+ lines = html.split(/\r?\n/),
+ words, out = '', len = 0;
+
+ for (var i=0; i < lines.length; i++) {
+ len += lines[i].length;
+ if (maxlines && i == maxlines - 1) {
+ out += lines[i] + '\n' + morelink;
+ maxlen = html.length * 2;
+ }
+ else if (len > maxlen) {
+ len = out.length;
+ words = lines[i].split(' ');
+ for (var j=0; j < words.length; j++) {
+ len += words[j].length + 1;
+ out += words[j] + ' ';
+ if (len > maxlen) {
+ out += morelink;
+ maxlen = html.length * 2;
+ }
+ }
+ out += '\n';
+ }
+ else
+ out += lines[i] + '\n';
+ }
+
+ if (maxlen > str.length)
+ out += '</span>';
+
+ html = out;
+ }
+
+ // simple link parser (similar to rcube_string_replacer class in PHP)
+ var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
+ var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
+ var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
+ var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
+
+ return html
+ .replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
+ .replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
+ .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
+ .replace(/\n/g, "<br/>");
+ }
+
+ /**
+ * Clear any text selection
+ * (text is probably selected when double-clicking somewhere)
+ */
+ function clearSelection()
+ {
+ if (document.selection && document.selection.empty) {
+ document.selection.empty() ;
+ }
+ else if (window.getSelection) {
+ var sel = window.getSelection();
+ if (sel && sel.removeAllRanges)
+ sel.removeAllRanges();
+ }
+ }
+
+ /**
+ * Hide all open popup menus
+ */
+ function clear_popups(e)
+ {
+ var count = 0;
+ $('.popupmenu:visible').each(function(i, elem){
+ var menu = $(elem);
+ if (!menu.data('sticky') || !target_overlaps(e.target, elem)) {
+ menu.hide();
+ count++;
+ }
+ });
+ return count;
+ }
+
+ /**
+ * Check whether the event target is a descentand of the given element
+ */
+ function target_overlaps(target, elem)
+ {
+ while (target.parentNode) {
+ if (target.parentNode == elem)
+ return true;
+ target = target.parentNode;
+ }
+ return false;
+ }
+
+}
+
+
+/* tasklist plugin UI initialization */
+var rctasks;
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+
+ rctasks = new rcube_tasklist(rcmail.env.tasklist_settings);
+
+ // register button commands
+ //rcmail.register_command('addtask', function(){ tasks.add_task(); }, true);
+ //rcmail.register_command('print', function(){ tasks.print_list(); }, true);
+
+ rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
+ rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
+
+ rctasks.init();
+});
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
new file mode 100644
index 0000000..2a2d815
--- /dev/null
+++ b/plugins/tasklist/tasklist.php
@@ -0,0 +1,485 @@
+<?php
+
+/**
+ * Tasks plugin for Roundcube webmail
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class tasklist extends rcube_plugin
+{
+ const FILTER_MASK_TODAY = 1;
+ const FILTER_MASK_TOMORROW = 2;
+ const FILTER_MASK_WEEK = 4;
+ const FILTER_MASK_LATER = 8;
+ const FILTER_MASK_NODATE = 16;
+ const FILTER_MASK_OVERDUE = 32;
+ const FILTER_MASK_FLAGGED = 64;
+ const FILTER_MASK_COMPLETE = 128;
+
+ public static $filter_masks = array(
+ 'today' => self::FILTER_MASK_TODAY,
+ 'tomorrow' => self::FILTER_MASK_TOMORROW,
+ 'week' => self::FILTER_MASK_WEEK,
+ 'later' => self::FILTER_MASK_LATER,
+ 'nodate' => self::FILTER_MASK_NODATE,
+ 'overdue' => self::FILTER_MASK_OVERDUE,
+ 'flagged' => self::FILTER_MASK_FLAGGED,
+ 'complete' => self::FILTER_MASK_COMPLETE,
+ );
+
+ public $task = '?(?!login|logout).*';
+ public $rc;
+ public $driver;
+ public $timezone;
+ public $ui;
+
+ public $defaults = array(
+ 'date_format' => "Y-m-d",
+ 'time_format' => "H:i",
+ 'first_day' => 1,
+ );
+
+
+ /**
+ * Plugin initialization.
+ */
+ function init()
+ {
+ $this->rc = rcmail::get_instance();
+
+ $this->register_task('tasks', 'tasklist');
+
+ // load plugin configuration
+ $this->load_config();
+
+ // load localizations
+ $this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print'));
+
+ if ($this->rc->task == 'tasks' && $this->rc->action != 'save-pref') {
+ $this->load_driver();
+
+ // register calendar actions
+ $this->register_action('index', array($this, 'tasklist_view'));
+ $this->register_action('task', array($this, 'task_action'));
+ $this->register_action('tasklist', array($this, 'tasklist_action'));
+ $this->register_action('counts', array($this, 'fetch_counts'));
+ $this->register_action('fetch', array($this, 'fetch_tasks'));
+ }
+
+ if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
+ require_once($this->home . '/tasklist_ui.php');
+ $this->ui = new tasklist_ui($this);
+ $this->ui->init();
+ }
+ }
+
+
+ /**
+ * Helper method to load the backend driver according to local config
+ */
+ private function load_driver()
+ {
+ if (is_object($this->driver))
+ return;
+
+ $driver_name = $this->rc->config->get('tasklist_driver', 'database');
+ $driver_class = 'tasklist_' . $driver_name . '_driver';
+
+ require_once($this->home . '/drivers/tasklist_driver.php');
+ require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+
+ switch ($driver_name) {
+ case "kolab":
+ $this->require_plugin('libkolab');
+ default:
+ $this->driver = new $driver_class($this);
+ break;
+ }
+
+ // get user's timezone
+ $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
+ }
+
+
+ /**
+ *
+ */
+ public function task_action()
+ {
+ $action = get_input_value('action', RCUBE_INPUT_GPC);
+ $rec = get_input_value('t', RCUBE_INPUT_POST, true);
+ $oldrec = $rec;
+ $success = $refresh = false;
+
+ switch ($action) {
+ case 'new':
+ $oldrec = null;
+ $rec = $this->prepare_task($rec);
+ $rec['uid'] = $this->generate_uid();
+ $temp_id = $rec['tempid'];
+ if ($success = $this->driver->create_task($rec)) {
+ $refresh = $this->driver->get_task($rec);
+ if ($temp_id) $refresh['tempid'] = $temp_id;
+ }
+ break;
+
+ case 'edit':
+ $rec = $this->prepare_task($rec);
+ if ($success = $this->driver->edit_task($rec))
+ $refresh = $this->driver->get_task($rec);
+ break;
+
+ case 'delete':
+ if (!($success = $this->driver->delete_task($rec, false)))
+ $this->rc->output->command('plugin.reload_data');
+ break;
+
+ case 'undelete':
+ if ($success = $this->driver->undelete_task($rec))
+ $refresh = $this->driver->get_task($rec);
+ break;
+ }
+
+ if ($success) {
+ $this->rc->output->show_message('successfullysaved', 'confirmation');
+ $this->update_counts($oldrec, $refresh);
+ }
+ else
+ $this->rc->output->show_message('tasklist.errorsaving', 'error');
+
+ // unlock client
+ $this->rc->output->command('plugin.unlock_saving');
+
+ if ($refresh) {
+ $this->encode_task($refresh);
+ $this->rc->output->command('plugin.refresh_task', $refresh);
+ }
+ }
+
+ /**
+ * repares new/edited task properties before save
+ */
+ private function prepare_task($rec)
+ {
+ // try to be smart and extract date from raw input
+ if ($rec['raw']) {
+ foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
+ $locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
+ $normwords[] = $word;
+ $datewords[] = $word;
+ }
+ foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
+ $locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
+ $normwords[] = $month;
+ $datewords[] = $month;
+ }
+ foreach (array('on','this','next','at') as $word) {
+ $fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
+ $fillwords[] = $word;
+ }
+
+ $raw = trim($rec['raw']);
+ $date_str = '';
+
+ // translate localized keywords
+ $raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
+ $raw = preg_replace($locwords, $normwords, $raw);
+
+ // find date pattern
+ $date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
+ if (preg_match($date_pattern, $raw, $m)) {
+ $date_str .= $m[1] . $m[2] . $m[3];
+ $raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
+ // add year to date string
+ if ($m[1] && !$m[3])
+ $date_str .= date('Y');
+ }
+
+ // find time pattern
+ $time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
+ if (preg_match($time_pattern, $raw, $m)) {
+ $has_time = true;
+ $date_str .= ($date_str ? ' ' : 'today ') . $m[1];
+ $raw = preg_replace($time_pattern, '', $raw);
+ }
+
+ // yes, raw input matched a (valid) date
+ if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
+ $rec['date'] = $date->format('Y-m-d');
+ if ($has_time)
+ $rec['time'] = $date->format('H:i');
+ $rec['title'] = $raw;
+ }
+ else
+ $rec['title'] = $rec['raw'];
+ }
+
+ // normalize input from client
+ if (isset($rec['complete'])) {
+ $rec['complete'] = floatval($rec['complete']);
+ if ($rec['complete'] > 1)
+ $rec['complete'] /= 100;
+ }
+ if (isset($rec['flagged']))
+ $rec['flagged'] = intval($rec['flagged']);
+
+ // fix for garbage input
+ if ($rec['description'] == 'null')
+ $rec['description'] = '';
+
+ foreach ($rec as $key => $val) {
+ if ($val == 'null')
+ $rec[$key] = null;
+ }
+
+ if (!empty($rec['date'])) {
+ try {
+ $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
+ $rec['date'] = $date->format('Y-m-d');
+ if (!empty($rec['time']))
+ $rec['time'] = $date->format('H:i');
+ }
+ catch (Exception $e) {
+ $rec['date'] = $rec['time'] = null;
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ *
+ */
+ public function tasklist_action()
+ {
+ $action = get_input_value('action', RCUBE_INPUT_GPC);
+ $list = get_input_value('l', RCUBE_INPUT_POST, true);
+ $success = false;
+
+ switch ($action) {
+
+ }
+
+ if ($success)
+ $this->rc->output->show_message('successfullysaved', 'confirmation');
+ else
+ $this->rc->output->show_message('tasklist.errorsaving', 'error');
+ }
+
+ /**
+ *
+ */
+ public function fetch_counts()
+ {
+ $lists = null;
+ $counts = $this->driver->count_tasks($lists);
+ $this->rc->output->command('plugin.update_counts', $counts);
+ }
+
+ /**
+ * Adjust the cached counts after changing a task
+ *
+ *
+ */
+ public function update_counts($oldrec, $newrec)
+ {
+ // rebuild counts until this function is finally implemented
+ $this->fetch_counts();
+
+ // $this->rc->output->command('plugin.update_counts', $counts);
+ }
+
+ /**
+ *
+ */
+ public function fetch_tasks()
+ {
+ $f = intval(get_input_value('filter', RCUBE_INPUT_GPC));
+ $search = get_input_value('q', RCUBE_INPUT_GPC);
+ $filter = array('mask' => $f, 'search' => $search);
+ $lists = null;
+
+ // convert magic date filters into a real date range
+ switch ($f) {
+ case self::FILTER_MASK_TODAY:
+ $today = new DateTime('now', $this->timezone);
+ $filter['from'] = $filter['to'] = $today->format('Y-m-d');
+ break;
+
+ case self::FILTER_MASK_TOMORROW:
+ $tomorrow = new DateTime('now + 1 day', $this->timezone);
+ $filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
+ break;
+
+ case self::FILTER_MASK_OVERDUE:
+ $yesterday = new DateTime('yesterday', $this->timezone);
+ $filter['to'] = $yesterday->format('Y-m-d');
+ break;
+
+ case self::FILTER_MASK_WEEK:
+ $today = new DateTime('now', $this->timezone);
+ $filter['from'] = $today->format('Y-m-d');
+ $weekend = new DateTime('now + 7 days', $this->timezone);
+ $filter['to'] = $weekend->format('Y-m-d');
+ break;
+
+ case self::FILTER_MASK_LATER:
+ $date = new DateTime('now + 8 days', $this->timezone);
+ $filter['from'] = $date->format('Y-m-d');
+ break;
+
+ }
+
+ $data = $this->task_tree = $this->tasks_childs = array();
+ foreach ($this->driver->list_tasks($filter, $lists) as $rec) {
+ if ($rec['parent_id']) {
+ $this->tasks_childs[$rec['parent_id']]++;
+ $this->task_tree[$rec['id']] = $rec['parent_id'];
+ }
+ $this->encode_task($rec);
+
+ // apply filter; don't trust the driver on this :-)
+ if ((!$f && $rec['complete'] < 1.0) || ($rec['mask'] & $f))
+ $data[] = $rec;
+ }
+
+ // sort tasks according to their hierarchy level and due date
+ usort($data, array($this, 'task_sort_cmp'));
+
+ $this->rc->output->command('plugin.data_ready', $data);
+ }
+
+ /**
+ * Prepare the given task record before sending it to the client
+ */
+ private function encode_task(&$rec)
+ {
+ $rec['mask'] = $this->filter_mask($rec);
+ $rec['flagged'] = intval($rec['flagged']);
+ $rec['complete'] = floatval($rec['complete']);
+
+ if ($rec['date']) {
+ try {
+ $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
+ $rec['datetime'] = intval($date->format('U'));
+ $rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
+ $rec['_hasdate'] = 1;
+ }
+ catch (Exception $e) {
+ $rec['date'] = $rec['datetime'] = null;
+ }
+ }
+ else {
+ $rec['date'] = $rec['datetime'] = null;
+ $rec['_hasdate'] = 0;
+ }
+
+ if ($this->tasks_childs[$rec['id']])
+ $rec['_haschilds'] = $this->tasks_childs[$rec['id']];
+
+ if (!isset($rec['_depth'])) {
+ $rec['_depth'] = 0;
+ $parent_id = $this->task_tree[$rec['id']];
+ while ($parent_id) {
+ $rec['_depth']++;
+ $parent_id = $this->task_tree[$parent_id];
+ }
+ }
+ }
+
+ /**
+ * Compare function for task list sorting.
+ * Nested tasks need to be sorted to the end.
+ */
+ private function task_sort_cmp($a, $b)
+ {
+ $d = $a['_depth'] - $b['_depth'];
+ if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
+ if (!$d) $d = $a['datetime'] - $b['datetime'];
+ return $d;
+ }
+
+ /**
+ * Compute the filter mask of the given task
+ *
+ * @param array Hash array with Task record properties
+ * @return int Filter mask
+ */
+ public function filter_mask($rec)
+ {
+ static $today, $tomorrow, $weeklimit;
+
+ if (!$today) {
+ $today_date = new DateTime('now', $this->timezone);
+ $today = $today_date->format('Y-m-d');
+ $tomorrow_date = new DateTime('now + 1 day', $this->timezone);
+ $tomorrow = $tomorrow_date->format('Y-m-d');
+ $week_date = new DateTime('now + 7 days', $this->timezone);
+ $weeklimit = $week_date->format('Y-m-d');
+ }
+
+ $mask = 0;
+ if ($rec['flagged'])
+ $mask |= self::FILTER_MASK_FLAGGED;
+ if ($rec['complete'] == 1.0)
+ $mask |= self::FILTER_MASK_COMPLETE;
+ if (empty($rec['date']))
+ $mask |= self::FILTER_MASK_NODATE;
+ else if ($rec['date'] == $today)
+ $mask |= self::FILTER_MASK_TODAY;
+ else if ($rec['date'] == $tomorrow)
+ $mask |= self::FILTER_MASK_TOMORROW;
+ else if ($rec['date'] < $today)
+ $mask |= self::FILTER_MASK_OVERDUE;
+ else if ($rec['date'] > $tomorrow && $rec['date'] <= $weeklimit)
+ $mask |= self::FILTER_MASK_LATER;
+ else if ($rec['date'] > $weeklimit)
+ $mask |= self::FILTER_MASK_LATER;
+
+ return $mask;
+ }
+
+
+ /******* UI functions ********/
+
+ /**
+ * Render main view of the tasklist task
+ */
+ public function tasklist_view()
+ {
+ $this->ui->init();
+ $this->ui->init_templates();
+ $this->rc->output->set_pagetitle($this->gettext('navtitle'));
+ $this->rc->output->send('tasklist.mainview');
+ }
+
+
+ /******* Utility functions *******/
+
+ /**
+ * Generate a unique identifier for an event
+ */
+ public function generate_uid()
+ {
+ return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
+ }
+
+}
+
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
new file mode 100644
index 0000000..50ae64a
--- /dev/null
+++ b/plugins/tasklist/tasklist_ui.php
@@ -0,0 +1,181 @@
+<?php
+/**
+ * User Interface class for the Tasklist plugin
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class tasklist_ui
+{
+ private $rc;
+ private $plugin;
+ private $ready = false;
+
+ function __construct($plugin)
+ {
+ $this->plugin = $plugin;
+ $this->rc = $plugin->rc;
+ }
+
+ /**
+ * Calendar UI initialization and requests handlers
+ */
+ public function init()
+ {
+ if ($this->ready) // already done
+ return;
+
+ // add taskbar button
+ $this->plugin->add_button(array(
+ 'command' => 'tasks',
+ 'class' => 'button-tasklist',
+ 'classsel' => 'button-tasklist button-selected',
+ 'innerclass' => 'button-inner',
+ 'label' => 'tasklist.navtitle',
+ ), 'taskbar');
+
+ $skin = $this->rc->config->get('skin');
+ $this->plugin->include_stylesheet('skins/' . $skin . '/tasklist.css');
+ $this->ready = true;
+ }
+
+ /**
+ * Register handler methods for the template engine
+ */
+ public function init_templates()
+ {
+ $this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists'));
+ $this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select'));
+ $this->plugin->register_handler('plugin.category_select', array($this, 'category_select'));
+ $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
+ $this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form'));
+ $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview'));
+
+ $this->plugin->include_script('tasklist.js');
+
+ // copy config to client
+ $defaults = $this->plugin->defaults;
+ $settings = array(
+ 'date_format' => $this->rc->config->get('date_format', $defaults['date_format']),
+ 'time_format' => $this->rc->config->get('time_format', $defaults['time_format']),
+ 'first_day' => $this->rc->config->get('calendar_first_day', $defaults['first_day']),
+ );
+
+ $this->rc->output->set_env('tasklist_settings', $settings);
+ }
+
+ /**
+ *
+ */
+ function tasklists($attrib = array())
+ {
+ $lists = $this->plugin->driver->get_lists();
+
+ $li = '';
+ foreach ((array)$lists as $id => $prop) {
+ if ($attrib['activeonly'] && !$prop['active'])
+ continue;
+
+ unset($prop['user_id']);
+ $prop['alarms'] = $this->plugin->driver->alarms;
+ $prop['undelete'] = $this->plugin->driver->undelete;
+ $prop['sortable'] = $this->plugin->driver->sortable;
+ $jsenv[$id] = $prop;
+
+ $html_id = html_identifier($id);
+ $class = 'tasks-' . asciiwords($id, true);
+
+ if ($prop['readonly'])
+ $class .= ' readonly';
+ if ($prop['class_name'])
+ $class .= ' '.$prop['class_name'];
+
+ $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
+ html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'disabled' => true)) .
+ html::span('handle', ' ') .
+ html::span('listname', Q($prop['name'])));
+ }
+
+ $this->rc->output->set_env('tasklists', $jsenv);
+ $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+
+ return html::tag('ul', $attrib, $li, html::$common_attrib);
+ }
+
+
+ /**
+ * Render a HTML select box for list selection
+ */
+ function tasklist_select($attrib = array())
+ {
+ $attrib['name'] = 'list';
+ $select = new html_select($attrib);
+ foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) {
+ if (!$prop['readonly'])
+ $select->add($prop['name'], $id);
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box to select a task category
+ */
+ function category_select($attrib = array())
+ {
+ $attrib['name'] = 'categories';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ foreach ((array)$this->plugin->driver->list_categories() as $cat => $color) {
+ $select->add($cat, $cat);
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ *
+ */
+ function quickadd_form($attrib)
+ {
+ $attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform');
+
+ $input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput', 'placeholder' => $this->plugin->gettext('createnewtask')));
+ $button = html::tag('input', array('type' => 'submit', 'value' => '+', 'class' => 'button mainaction'));
+
+ $this->rc->output->add_gui_object('quickaddform', $attrib['id']);
+ return html::tag('form', $attrib, $input->show() . $button);
+ }
+
+ /**
+ * The result view
+ */
+ function tasks_resultview($attrib)
+ {
+ $attrib += array('id' => 'rcmtaskslist');
+
+ $this->rc->output->add_gui_object('resultlist', $attrib['id']);
+
+ unset($attrib['name']);
+ return html::tag('ul', $attrib, '');
+ }
+
+
+}
More information about the commits
mailing list