From b8ff82ba6897b2359cdb500879375f196e302631 Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Mon, 12 Jan 2009 18:48:27 +0100 Subject: [PATCH] Add basic Ajax interface. No event rendering yet, not much dynamics either. But the general interface is there and works. --- kronolith/config/prefs.php.dist | 11 +- kronolith/docs/CHANGES | 1 + kronolith/index.php | 42 +- kronolith/js/kronolith.js | 1 + kronolith/js/src/kronolith.js | 945 +++++++++++++++++++++++++++ kronolith/lib/Kronolith.php | 415 +++++++++++- kronolith/templates/index/day.inc | 75 +++ kronolith/templates/index/index.inc | 244 +++++++ kronolith/templates/index/month.inc | 170 +++++ kronolith/templates/index/week.inc | 115 ++++ kronolith/templates/javascript_defs.php | 3 + kronolith/themes/ajax.css | 639 ++++++++++++++++++ kronolith/themes/graphics/agenda.png | Bin 0 -> 170 bytes kronolith/themes/graphics/back_quick.png | Bin 0 -> 904 bytes kronolith/themes/graphics/checkbox_off.png | Bin 0 -> 191 bytes kronolith/themes/graphics/checkbox_on.png | Bin 0 -> 416 bytes kronolith/themes/graphics/checkbox_over.png | Bin 0 -> 387 bytes kronolith/themes/graphics/drag-handle.png | Bin 0 -> 168 bytes kronolith/themes/graphics/new_small_fade.png | Bin 0 -> 366 bytes kronolith/themes/graphics/picker.png | Bin 0 -> 368 bytes kronolith/themes/graphics/submit.png | Bin 0 -> 3306 bytes kronolith/themes/graphics/tasks.png | Bin 0 -> 532 bytes 22 files changed, 2657 insertions(+), 4 deletions(-) create mode 100644 kronolith/js/kronolith.js create mode 100644 kronolith/js/src/kronolith.js create mode 100644 kronolith/templates/index/day.inc create mode 100644 kronolith/templates/index/index.inc create mode 100644 kronolith/templates/index/month.inc create mode 100644 kronolith/templates/index/week.inc create mode 100644 kronolith/themes/ajax.css create mode 100644 kronolith/themes/graphics/agenda.png create mode 100644 kronolith/themes/graphics/back_quick.png create mode 100644 kronolith/themes/graphics/checkbox_off.png create mode 100644 kronolith/themes/graphics/checkbox_on.png create mode 100644 kronolith/themes/graphics/checkbox_over.png create mode 100644 kronolith/themes/graphics/drag-handle.png create mode 100644 kronolith/themes/graphics/new_small_fade.png create mode 100644 kronolith/themes/graphics/picker.png create mode 100644 kronolith/themes/graphics/submit.png create mode 100644 kronolith/themes/graphics/tasks.png diff --git a/kronolith/config/prefs.php.dist b/kronolith/config/prefs.php.dist index 9d79b4a34..b9d82a76b 100644 --- a/kronolith/config/prefs.php.dist +++ b/kronolith/config/prefs.php.dist @@ -9,7 +9,7 @@ $prefGroups['view'] = array( 'column' => _("Display Options"), 'label' => _("User Interface"), 'desc' => _("Select confirmation options, how to display the different views and choose default view."), - 'members' => array('confirm_delete', 'defaultview', + 'members' => array('dynamic_view', 'confirm_delete', 'defaultview', 'time_between_days', 'week_start_monday', 'day_hour_start', 'day_hour_end', 'day_hour_force', 'slots_per_hour', 'show_icons', 'show_time', 'show_location', @@ -78,6 +78,15 @@ if ($GLOBALS['registry']->hasMethod('contacts/sources')) { ); } +// show dynamic view? +$_prefs['dynamic_view'] = array( + 'value' => 0, + 'locked' => false, + 'shared' => false, + 'type' => 'checkbox', + 'desc' => _("Show the dynamic view by default, if the browser supports it?") +); + // confirm deletion of events which don't recur? $_prefs['confirm_delete'] = array( 'value' => 1, diff --git a/kronolith/docs/CHANGES b/kronolith/docs/CHANGES index 010e9cc96..d74d11fcd 100644 --- a/kronolith/docs/CHANGES +++ b/kronolith/docs/CHANGES @@ -2,6 +2,7 @@ v3.0-cvs -------- +[jan] Add AJAX interface. [jan] Store events in UTC and convert to the user's timezone on the fly. [jan] Remove alarm reminder code. [jan] Change listEvents API method to return keys as dates instead of diff --git a/kronolith/index.php b/kronolith/index.php index 5076fa794..ed7b9d71d 100644 --- a/kronolith/index.php +++ b/kronolith/index.php @@ -19,4 +19,44 @@ if (!$kronolith_configured) { } require_once KRONOLITH_BASE . '/lib/base.php'; -require KRONOLITH_BASE . '/' . $prefs->getValue('defaultview') . '.php'; + +if (!$prefs->getValue('dynamic_view') || !$browser->hasFeature('xmlhttpreq')) { + include KRONOLITH_BASE . '/' . $prefs->getValue('defaultview') . '.php'; + exit; +} + +require_once 'Horde/Identity.php'; +$identity = Identity::factory(); +$logout_link = Horde::getServiceLink('logout', 'kronolith'); +if ($logout_link) { + $logout_link = Horde::widget($logout_link, _("_Logout"), 'logout'); +} +$help_link = Horde::getServiceLink('help', 'kronolith'); +if ($help_link) { + $help_link = Horde::widget($help_link, _("Help"), 'helplink', 'help', 'popup(this.href); return false;'); +} +$today = Kronolith::currentDate(); +$remote_calendars = @unserialize($prefs->getValue('remote_cals')); +$current_user = Auth::getAuth(); +$my_calendars = array(); +$shared_calendars = array(); +foreach (Kronolith::listCalendars() as $id => $cal) { + if ($cal->get('owner') == $current_user) { + $my_calendars[$id] = $cal; + } else { + $shared_calendars[$id] = $cal; + } +} + +$scripts = array( + array('ContextSensitive.js', 'kronolith', true), + array('dhtmlHistory.js', 'horde', true), + array('redbox.js', 'horde', true), +); +Kronolith::header('', $scripts); +echo "\n"; +require KRONOLITH_TEMPLATES . '/index/index.inc'; +Kronolith::includeScriptFiles(); +Kronolith::outputInlineScript(); +$notification->notify(array('listeners' => array('javascript'))); +echo "\n"; diff --git a/kronolith/js/kronolith.js b/kronolith/js/kronolith.js new file mode 100644 index 000000000..f381f8356 --- /dev/null +++ b/kronolith/js/kronolith.js @@ -0,0 +1 @@ +var frames={horde_main:true},KronolithCore={view:"",remove_gc:[],debug:function(A,B){if(!this.is_logout&&Kronolith.conf.debug){alert(A+": "+(B instanceof Error?B.name+"-"+B.message:Object.inspect(B)))}},setTitle:function(A){document.title=Kronolith.conf.name+" :: "+A},showNotifications:function(A){if(!A.size()||this.is_logout){return}A.find(function(D){switch(D.type){case"kronolith.timeout":this.logout(Kronolith.conf.timeout_url);return true;case"horde.error":case"horde.message":case"horde.success":case"horde.warning":case"kronolith.request":case"kronolith.sticky":var H,I,K,E,J,F,G=$("alerts"),B=new Element("DIV",{className:D.type.replace(".","-")}),C=D.message;if(!G){G=new Element("DIV",{id:"alerts"});$(document.body).insert(G)}if($w("kronolith.request kronolith.sticky").indexOf(D.type)==-1){C=C.unescapeHTML().unescapeHTML()}G.insert(B.update(C));if(Kronolith.conf.is_ie6){K=new Element("DIV",{className:"ie6alertsfix"}).clonePosition(B,{setLeft:false,setTop:false});H=K;K.insert(B.remove());G.insert(K)}else{H=B}I=Effect.Fade.bind(this,B,{duration:1.5,afterFinish:this.removeAlert.bind(this)});H.observe("click",I);if($w("horde.error kronolith.request kronolith.sticky").indexOf(D.type)==-1){I.delay(D.type=="horde.warning"?10:3)}if(D.type=="kronolith.request"){J=function(){I();document.stopObserving("click",J)};document.observe("click",J)}if(F=$("alertslog")){switch(D.type){case"horde.error":E=Kronolith.text.alog_error;break;case"horde.message":E=Kronolith.text.alog_message;break;case"horde.success":E=Kronolith.text.alog_success;break;case"horde.warning":E=Kronolith.text.alog_warning;break}if(E){F=F.down("DIV UL");if(F.down().hasClassName("noalerts")){F.down().remove()}F.insert(new Element("LI").insert(new Element("P",{className:"label"}).insert(E)).insert(new Element("P",{className:"indent"}).insert(C).insert(new Element("SPAN",{className:"alertdate"}).insert("["+(new Date).toLocaleString()+"]"))))}}}},this)},toggleAlertsLog:function(){var A=$("alertsloglink").down("A"),C=$("alertslog").down("DIV"),B={duration:0.5};if(C.visible()){Effect.BlindUp(C,B);A.update(Kronolith.text.showalog)}else{Effect.BlindDown(C,B);A.update(Kronolith.text.hidealog)}},removeAlert:function(C){try{var A=$(C.element),B=A.up();if(B&&B.parentNode){this.addGC(A.remove());if(!B.childElements().size()&&B.hasClassName("ie6alertsfix")){this.addGC(B.remove())}}}catch(D){this.debug("removeAlert",D)}},logout:function(A){this.is_logout=true;this.redirect(A||(Kronolith.conf.URI_IMP+"/LogOut"))},redirect:function(A){A=this.addSID(A);if(parent.frames.horde_main){parent.location=A}else{window.location=A}},addMouseEvents:function(A){this.DMenu.addElement(A.id,"ctx_"+A.type,A)},removeMouseEvents:function(A){this.DMenu.removeElement($(A).readAttribute("id"));this.addGC(A)},addPopdown:function(B,A){var C=$(B);C.insert({after:$($("popdown_img").cloneNode(false)).writeAttribute("id",B+"_img").show()});this.addMouseEvents({id:B+"_img",type:A,offset:C.up(),left:true})},addGC:function(A){this.remove_gc=this.remove_gc.concat(A)},clickObserveHandler:function(A){return A.d.observe("click",KronolithCore._clickFunc.curry(A))},_clickFunc:function(B,A){B.p?B.f(A):B.f();if(!B.ns){A.stop()}},addSID:function(A){if(!Kronolith.conf.SESSION_ID){return A}return this.addURLParam(A,Kronolith.conf.SESSION_ID.toQueryParams())},addURLParam:function(A,C){var B=A.indexOf("?");if(B!=-1){C=$H(A.toQueryParams()).merge(C).toObject();A=A.substring(0,B)}return A+"?"+Object.toQueryString(C)},go:function(A,D){var G,C,E;if(A.startsWith("compose:")){return}var B=A.split(":");var F=B.shift();switch(F){case"day":case"week":case"month":case"year":case"agenda":case"tasks":if(this.view==F){break}var H=F.capitalize();this._addHistory(A);["Day","Week","Month","Year","Tasks","Agenda"].each(function(I){$("kronolithNav"+I).removeClassName("on")});if(this.view){$("kronolithView"+this.view.capitalize()).fade()}if($("kronolithView"+H)){$("kronolithView"+H).appear()}$("kronolithNav"+H).addClassName("on");switch(F){case"day":case"week":case"month":case"year":$("kronolithMinical").select("td").each(function(I){I.removeClassName("kronolithSelected");if(B.length){if(!I.hasClassName("kronolithMinicalWeek")&&(F=="month"||(F=="day"&&I.readAttribute("date")==B[0]))){I.addClassName("kronolithSelected")}}});if(F=="week"&&B.length){$("kronolithMinical").select("td").each(function(J){if(J.readAttribute("date")==B[0]){var I=J.parentNode.childNodes;for(i=0;i0){A+=" ("+C+")"}}else{A=this.viewport.getMetaData("label")}}KronolithCore.setTitle(A)},_keydownHandler:function(D){if(D.findElement("FORM")||RedBox.overlayVisible()){return}var G,F,B,E,C,A=D.keyCode||D.charCode;switch(A){case Event.KEY_ESC:$("kronolithEventForm").fade({duration:0.5});break;case Event.KEY_DELETE:case Event.KEY_BACKSPACE:if(sel.size()==1){B=sel.get("dataob").first();if(D.shiftKey){this.moveSelected(B.rownum+((B.rownum==this.viewport.getMetaData("total_rows"))?-1:1),true)}this.flag("deleted",B)}else{this.flag("deleted")}D.stop();break;case Event.KEY_UP:case Event.KEY_DOWN:if(D.shiftKey&&this.lastrow!=-1){E=this.viewport.createSelection("rownum",this.lastrow+((A==Event.KEY_UP)?-1:1));if(E.size()){E=E.get("dataob").first();this.viewport.scrollTo(E.rownum);this.msgSelect(E.domid,{shift:true})}}else{this.moveSelected(A==Event.KEY_UP?-1:1)}D.stop();break;case Event.KEY_PAGEUP:case Event.KEY_PAGEDOWN:if(!D.ctrlKey&&!D.shiftKey&&!D.altKey&&!D.metaKey){F=this.viewport.getPageSize()-1;move=F*(A==Event.KEY_PAGEUP?-1:1);if(sel.size()==1){G=this.viewport.currentOffset();C=sel.get("rownum").first()-1;switch(A){case Event.KEY_PAGEUP:if(G!=C){move=G-C}break;case Event.KEY_PAGEDOWN:if((G+F)!=C){move=G+F-C}break}}this.moveSelected(move);D.stop()}break;case Event.KEY_HOME:case Event.KEY_END:this.moveSelected(A==Event.KEY_HOME?1:this.viewport.getMetaData("total_rows"),true);D.stop();break;case Event.KEY_RETURN:if(!D.element().match("input")){if(sel.size()==1){this.msgWindow(sel.get("dataob").first())}}D.stop();break;case 65:case 97:if(D.ctrlKey){this.selectAll();D.stop()}break}},_closeRedBox:function(){var A=RedBox.getWindowContents();KronolithCore.addGC([A,A.descendants()].flatten());RedBox.close()},_onLoad:function(){var B,D=KronolithCore.clickObserveHandler,A=KronolithCore.DMenu;if(Horde.dhtmlHistory.initialize()){Horde.dhtmlHistory.addListener(this.go.bind(this))}if(!Horde.dhtmlHistory.getCurrentLocation()){this.go(Kronolith.conf.login_view)}B=$("kronolithLogo");if(B.visible()){D({d:B.down("a"),f:this.go.bind(this,"portal")})}D({d:$("id_fullday"),f:function(){$("kronolithEventForm").select(".edit_at").each(Element.toggle)}});D({d:$("kronolithNewEvent"),f:KronolithCore.editEvent});$("kronolithEventActions").select("input.button").each(function(C){D({d:C,f:function(){Effect.Fade("kronolithEventForm")}})});$("kronolithEventForm").select("div.kronolithTags span").each(function(C){$("id_tags").value=$F("id_tags")+C.getText()+", "});["Day","Week","Month","Year","Tasks","Agenda"].each(function(C){D({d:$("kronolithNav"+C),f:KronolithCore.go.bind(KronolithCore,C.toLowerCase())})});$("kronolithMenu").select("div.kronolithCalendars div").each(function(C){D({d:C,f:KronolithCore.toggleCalendar.bind(KronolithCore,C)});C.observe("mouseover",C.addClassName.curry("kronolithCalOver"));C.observe("mouseout",C.removeClassName.curry("kronolithCalOver"))});D({d:$("kronolithMinicalDate"),f:KronolithCore.go.bind(KronolithCore,"month:"+$("kronolithMinicalDate").readAttribute("date"))});$("kronolithMinical").select("td").each(function(C){D({d:C,f:function(){if(C.hasClassName("kronolithMinicalWeek")){KronolithCore.go("week:"+C.readAttribute("date"))}else{if(!C.hasClassName("empty")){KronolithCore.go("day:"+C.readAttribute("date"))}}}})});$("kronolithViewMonth").select(".kronolithFirstCol").each(function(C){D({d:C,f:KronolithCore.go.bind(KronolithCore,"week")})});$("kronolithViewMonth").select(".kronolithDay").each(function(C){D({d:C,f:KronolithCore.go.bind(KronolithCore,"day")})});this._resizeIE6()},_resizeIE6:function(){var C=(($("kronolithViewMonth").getWidth()-20-2-2-16)/7)-2-2;$("kronolithViewMonth").select(".kronolithCol").invoke("setStyle",{width:C+"px"});var A=(($("kronolithViewMonth").getHeight()-25)/6)-2-2;$("kronolithViewMonth").select(".kronolithViewBody .kronolithCol").invoke("setStyle",{height:A+"px"});$("kronolithViewMonth").select(".kronolithViewBody .kronolithFirstCol").invoke("setStyle",{height:A+"px"});$("kronolithViewWeek").select(".kronolithCol").invoke("setStyle",{width:(C-1)+"px"});var B=$("kronolithViewDay").getWidth()-20-2-2-16-3;var C=((B+7)/7)-1;$("kronolithViewDay").select(".kronolithViewHead .kronolithCol").invoke("setStyle",{width:B+"px"});$("kronolithViewDay").select(".kronolithViewBody .kronolithCol").invoke("setStyle",{width:C+"px"});$("kronolithViewDay").select(".kronolithViewBody .kronolithAllDay .kronolithCol").invoke("setStyle",{width:B+"px"})},_resizeIE6Iframe:function(A){if(Kronolith.conf.is_ie6){A.setStyle({width:$("kronolithmain").getStyle("width"),height:(document.viewport.getHeight()-20)+"px"})}},editEvent:function(){$("kronolithEventForm").appear({duration:0.5})},toggleCalendar:function(A){if(A.hasClassName("on")){A.removeClassName("on")}else{A.addClassName("on")}}};if(typeof ContextSensitive!="undefined"){KronolithCore.DMenu=new ContextSensitive()}document.observe("dom:loaded",function(){try{if(parent.opener&&parent.opener.location.host==window.location.host&&parent.opener.KronolithCore){Kronolith.baseWindow=parent.opener.Kronolith.baseWindow||parent.opener}}catch(A){}new PeriodicalExecuter(function(){if(KronolithCore.remove_gc.size()){try{$A(KronolithCore.remove_gc.splice(0,75)).compact().invoke("stopObserving")}catch(B){KronolithCore.debug("remove_gc[].stopObserving",B)}}},10);KronolithCore._onLoad();document.observe("keydown",KronolithCore._keydownHandler.bind(KronolithCore));Event.observe(window,"resize",KronolithCore._onResize.bind(KronolithCore));if(Kronolith.conf.is_ie6){document.observe("selectstart",Event.stop);$("foobar").compact().invoke("select","LI").flatten().compact().each(function(B){B.observe("mouseover",B.addClassName.curry("over")).observe("mouseout",B.removeClassName.curry("over"))})}});Event.observe(window,"load",function(){KronolithCore.window_load=true});Element.addMethods({setText:function(B,C){var A=0;$A(B.childNodes).each(function(D){if(D.nodeType==3){if(A++){Element.remove(D)}else{D.nodeValue=C}}});if(!A){$(B).insert(C)}},getText:function(B,A){var C="";$A(B.childNodes).each(function(D){if(D.nodeType==3){C+=D.nodeValue}else{if(A&&D.hasChildNodes()){C+=$(D).getText(true)}}});return C}});Object.extend(Array.prototype,{numericSort:function(){return this.sort(function(B,A){if(B>A){return 1}else{if(B + */ + +/* Trick some Horde js into thinking this is the parent Horde window. */ +var frames = { horde_main: true }, + +/* Kronolith object. */ +KronolithCore = { + + view: '', + remove_gc: [], + + debug: function(label, e) + { + if (!this.is_logout && Kronolith.conf.debug) { + alert(label + ': ' + (e instanceof Error ? e.name + '-' + e.message : Object.inspect(e))); + } + }, + + setTitle: function(title) + { + document.title = Kronolith.conf.name + ' :: ' + title; + }, + + showNotifications: function(msgs) + { + if (!msgs.size() || this.is_logout) { + return; + } + + msgs.find(function(m) { + switch (m.type) { + case 'kronolith.timeout': + this.logout(Kronolith.conf.timeout_url); + return true; + + case 'horde.error': + case 'horde.message': + case 'horde.success': + case 'horde.warning': + case 'kronolith.request': + case 'kronolith.sticky': + var clickdiv, fadeeffect, iefix, log, requestfunc, tmp, + alerts = $('alerts'), + div = new Element('DIV', { className: m.type.replace('.', '-') }), + msg = m.message;; + + if (!alerts) { + alerts = new Element('DIV', { id: 'alerts' }); + $(document.body).insert(alerts); + } + + if ($w('kronolith.request kronolith.sticky').indexOf(m.type) == -1) { + msg = msg.unescapeHTML().unescapeHTML(); + } + alerts.insert(div.update(msg)); + + // IE6 has a bug that does not allow the body of a div to be + // clicked to trigger an onclick event for that div (it only + // seems to be an issue if the div is overlaying an element + // that itself contains an image). However, the alert box + // normally displays over the message list, and we use several + // graphics in the default message list layout, so we see this + // buggy behavior 99% of the time. The workaround is to + // overlay the div with a like sized div containing a clear + // gif, which tricks IE into the correct behavior. + if (Kronolith.conf.is_ie6) { + iefix = new Element('DIV', { className: 'ie6alertsfix' }).clonePosition(div, { setLeft: false, setTop: false }); + clickdiv = iefix; + iefix.insert(div.remove()); + alerts.insert(iefix); + } else { + clickdiv = div; + } + + fadeeffect = Effect.Fade.bind(this, div, { duration: 1.5, afterFinish: this.removeAlert.bind(this) }); + + clickdiv.observe('click', fadeeffect); + + if ($w('horde.error kronolith.request kronolith.sticky').indexOf(m.type) == -1) { + fadeeffect.delay(m.type == 'horde.warning' ? 10 : 3); + } + + if (m.type == 'kronolith.request') { + requestfunc = function() { + fadeeffect(); + document.stopObserving('click', requestfunc) + }; + document.observe('click', requestfunc); + } + + if (tmp = $('alertslog')) { + switch (m.type) { + case 'horde.error': + log = Kronolith.text.alog_error; + break; + + case 'horde.message': + log = Kronolith.text.alog_message; + break; + + case 'horde.success': + log = Kronolith.text.alog_success; + break; + + case 'horde.warning': + log = Kronolith.text.alog_warning; + break; + } + + if (log) { + tmp = tmp.down('DIV UL'); + if (tmp.down().hasClassName('noalerts')) { + tmp.down().remove(); + } + tmp.insert(new Element('LI').insert(new Element('P', { className: 'label' }).insert(log)).insert(new Element('P', { className: 'indent' }).insert(msg).insert(new Element('SPAN', { className: 'alertdate'}).insert('[' + (new Date).toLocaleString() + ']')))); + } + } + } + }, this); + }, + + toggleAlertsLog: function() + { + var alink = $('alertsloglink').down('A'), + div = $('alertslog').down('DIV'), + opts = { duration: 0.5 }; + if (div.visible()) { + Effect.BlindUp(div, opts); + alink.update(Kronolith.text.showalog); + } else { + Effect.BlindDown(div, opts); + alink.update(Kronolith.text.hidealog); + } + }, + + removeAlert: function(effect) + { + try { + var elt = $(effect.element), + parent = elt.up(); + // We may have already removed this element from the DOM tree + // (if the user clicked on the notification), so check parentNode + // here - will return null if node is not part of DOM tree. + if (parent && parent.parentNode) { + this.addGC(elt.remove()); + if (!parent.childElements().size() && + parent.hasClassName('ie6alertsfix')) { + this.addGC(parent.remove()); + } + } + } catch (e) { + this.debug('removeAlert', e); + } + }, + + logout: function(url) + { + this.is_logout = true; + this.redirect(url || (Kronolith.conf.URI_IMP + '/LogOut')); + }, + + redirect: function(url) + { + url = this.addSID(url); + if (parent.frames.horde_main) { + parent.location = url; + } else { + window.location = url; + } + }, + + /* Add/remove mouse events on the fly. + * Parameter: object with the following names - id, type, offset + * (optional), left (optional), onShow (optional) + * Valid types: + * 'message', 'draft' -- Message list rows + * 'container', 'special', 'folder' -- Folders + * 'reply', 'forward', 'otheractions' -- Message list buttons + * 'contacts' -- Linked e-mail addresses */ + addMouseEvents: function(p) + { + this.DMenu.addElement(p.id, 'ctx_' + p.type, p); + }, + + /* elt = DOM element */ + removeMouseEvents: function(elt) + { + this.DMenu.removeElement($(elt).readAttribute('id')); + this.addGC(elt); + }, + + /* Add a popdown menu to an actions button. */ + addPopdown: function(bid, ctx) + { + var bidelt = $(bid); + bidelt.insert({ after: $($('popdown_img').cloneNode(false)).writeAttribute('id', bid + '_img').show() }); + this.addMouseEvents({ id: bid + '_img', type: ctx, offset: bidelt.up(), left: true }); + }, + + /* Utility functions. */ + addGC: function(elt) + { + this.remove_gc = this.remove_gc.concat(elt); + }, + + // o: (object) Contains the following items: + // 'd' - (required) The DOM element + // 'f' - (required) The function to bind to the click event + // 'ns' - (optional) If set, don't stop the event's propogation + // 'p' - (optional) If set, passes in the event object to the called + // function + clickObserveHandler: function(o) + { + return o.d.observe('click', KronolithCore._clickFunc.curry(o)); + }, + + _clickFunc: function(o, e) + { + o.p ? o.f(e) : o.f(); + if (!o.ns) { + e.stop(); + } + }, + + addSID: function(url) + { + if (!Kronolith.conf.SESSION_ID) { + return url; + } + return this.addURLParam(url, Kronolith.conf.SESSION_ID.toQueryParams()); + }, + + addURLParam: function(url, params) + { + var q = url.indexOf('?'); + + if (q != -1) { + params = $H(url.toQueryParams()).merge(params).toObject(); + url = url.substring(0, q); + } + return url + '?' + Object.toQueryString(params); + }, + + go: function(fullloc, data) + { + var app, f, separator; + + if (fullloc.startsWith('compose:')) { + return; + } + + /* + $('dimpmain_portal').update(Kronolith.text.loading).show(); + + if (loc.startsWith('app:')) { + app = loc.substr(4); + if (app == 'imp' || app == 'dimp') { + this.go('folder:INBOX'); + return; + } + this.highlightSidebar('app' + app); + this._addHistory(loc, data); + if (data) { + this.iframeContent(loc, data); + } else if (Kronolith.conf.app_urls[app]) { + this.iframeContent(loc, Kronolith.conf.app_urls[app]); + } + return; + } + */ + var locParts = fullloc.split(':'); + var loc = locParts.shift(); + + switch (loc) { + case 'day': + case 'week': + case 'month': + case 'year': + case 'agenda': + case 'tasks': + if (this.view == loc) { + break; + } + + var locCap = loc.capitalize(); + this._addHistory(fullloc); + + [ 'Day', 'Week', 'Month', 'Year', 'Tasks', 'Agenda' ].each(function(a) { + $('kronolithNav' + a).removeClassName('on'); + }); + if (this.view) { + $('kronolithView' + this.view.capitalize()).fade(); + } + if ($('kronolithView' + locCap)) { + $('kronolithView' + locCap).appear(); + } + $('kronolithNav' + locCap).addClassName('on'); + + switch (loc) { + case 'day': + case 'week': + case 'month': + case 'year': + $('kronolithMinical').select('td').each(function(td) { + td.removeClassName('kronolithSelected'); + if (locParts.length) { + if (!td.hasClassName('kronolithMinicalWeek') && + (loc == 'month' || + (loc == 'day' && + td.readAttribute('date') == locParts[0]))) { + td.addClassName('kronolithSelected'); + } + } + }); + if (loc == 'week' && locParts.length) { + $('kronolithMinical').select('td').each(function(td) { + if (td.readAttribute('date') == locParts[0]) { + var tds = td.parentNode.childNodes; + for (i = 0; i < tds.length; i++) { + if (tds.item(i) != td && + tds.item(i).tagName == 'TD') { + $(tds.item(i)).addClassName('kronolithSelected'); + } + } + throw $break; + } + }); + } + $('kronolithBody').select('div.kronolithEvent').each(function(s) { + KronolithCore.clickObserveHandler({ d: s, f: $('kronolithEventForm').appear.bind($('kronolithEventForm')) }); + s.observe('mouseover', s.addClassName.curry('kronolithSelected')); + s.observe('mouseout', s.removeClassName.curry('kronolithSelected')); + }); + + break; + } + + this.view = loc; + break; + + case 'options': + //this.highlightSidebar('appoptions'); + this._addHistory(loc); + KronolithCore.setTitle(Kronolith.text.prefs); + this.iframeContent(loc, Kronolith.conf.prefs_url); + break; + } + }, + + _addHistory: function(loc, data) + { + if (Horde.dhtmlHistory.getCurrentLocation() != loc) { + Horde.dhtmlHistory.add(loc, data); + } + }, + + iframeContent: function(name, loc) + { + if (name === null) { + name = loc; + } + + var container = $('dimpmain_portal'), iframe; + if (!container) { + KronolithCore.showNotifications([ { type: 'horde.error', message: 'Bad portal!' } ]); + return; + } + + iframe = new Element('IFRAME', { id: 'iframe' + name, className: 'iframe', frameBorder: 0, src: loc }); + this._resizeIE6Iframe(iframe); + + // Hide menu in prefs pages. + if (name == 'options') { + iframe.observe('load', function() { $('iframeoptions').contentWindow.document.getElementById('menu').style.display = 'none'; }); + } + + container.insert(iframe); + }, + _onMenuShow: function(ctx) + { + var elts, folder, ob, sel; + + switch (ctx.ctx) { + case 'ctx_folder': + elts = $('ctx_folder_create', 'ctx_folder_rename', 'ctx_folder_delete'); + folder = KronolithCore.DMenu.element(); + if (folder.readAttribute('mbox') == 'INBOX') { + elts.invoke('hide'); + } else if (Kronolith.conf.fixed_folders.indexOf(folder.readAttribute('mbox')) != -1) { + elts.shift(); + elts.invoke('hide'); + } else { + elts.invoke('show'); + } + + if (folder.hasAttribute('u')) { + $('ctx_folder_poll').hide(); + $('ctx_folder_nopoll').show(); + } else { + $('ctx_folder_poll').show(); + $('ctx_folder_nopoll').hide(); + } + break; + + case 'ctx_message': + [ $('ctx_message_reply_list') ].invoke(this.viewport.createSelection('domid', ctx.id).get('dataob').first().listmsg ? 'show' : 'hide'); + break; + + case 'ctx_reply': + sel = this.viewport.getSelected(); + if (sel.size() == 1) { + ob = sel.get('dataob').first(); + } + [ $('ctx_reply_reply_list') ].invoke(ob && ob.listmsg ? 'show' : 'hide'); + break; + + case 'ctx_otheractions': + $('oa_seen', 'oa_unseen', 'oa_flagged', 'oa_clear', 'oa_sep1', 'oa_blacklist', 'oa_whitelist', 'oa_sep2').compact().invoke(this.viewport.getSelected().size() ? 'show' : 'hide'); + break; + } + return true; + }, + + _onResize: function(noupdate, nowait) + { + if (this.viewport) { + this.viewport.onResize(noupdate, nowait); + } + this._resizeIE6(); + }, + + updateTitle: function() + { + var elt, label, unseen; + if (this.viewport.isFiltering()) { + label = Kronolith.text.search + ' :: ' + this.viewport.getMetaData('total_rows') + ' ' + Kronolith.text.resfound; + } else { + elt = $(this.getFolderId(this.folder)); + if (elt) { + unseen = elt.readAttribute('u'); + label = elt.readAttribute('l'); + if (unseen > 0) { + label += ' (' + unseen + ')'; + } + } else { + label = this.viewport.getMetaData('label'); + } + } + KronolithCore.setTitle(label); + }, + + /* Keydown event handler */ + _keydownHandler: function(e) + { + // Only catch keyboard shortcuts in message list view. Disable catching + // when in form elements or the RedBox overlay is visible. + if (e.findElement('FORM') || + RedBox.overlayVisible()) { + return; + } + + var co, ps, r, row, rowoff, + kc = e.keyCode || e.charCode; + + switch (kc) { + case Event.KEY_ESC: + $('kronolithEventForm').fade({ duration: 0.5 }); + break; + + case Event.KEY_DELETE: + case Event.KEY_BACKSPACE: + if (sel.size() == 1) { + r = sel.get('dataob').first(); + if (e.shiftKey) { + this.moveSelected(r.rownum + ((r.rownum == this.viewport.getMetaData('total_rows')) ? -1 : 1), true); + } + this.flag('deleted', r); + } else { + this.flag('deleted'); + } + e.stop(); + break; + + case Event.KEY_UP: + case Event.KEY_DOWN: + if (e.shiftKey && this.lastrow != -1) { + row = this.viewport.createSelection('rownum', this.lastrow + ((kc == Event.KEY_UP) ? -1 : 1)); + if (row.size()) { + row = row.get('dataob').first(); + this.viewport.scrollTo(row.rownum); + this.msgSelect(row.domid, { shift: true }); + } + } else { + this.moveSelected(kc == Event.KEY_UP ? -1 : 1); + } + e.stop(); + break; + + case Event.KEY_PAGEUP: + case Event.KEY_PAGEDOWN: + if (!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { + ps = this.viewport.getPageSize() - 1; + move = ps * (kc == Event.KEY_PAGEUP ? -1 : 1); + if (sel.size() == 1) { + co = this.viewport.currentOffset(); + rowoff = sel.get('rownum').first() - 1; + switch (kc) { + case Event.KEY_PAGEUP: + if (co != rowoff) { + move = co - rowoff; + } + break; + + case Event.KEY_PAGEDOWN: + if ((co + ps) != rowoff) { + move = co + ps - rowoff; + } + break; + } + } + this.moveSelected(move); + e.stop(); + } + break; + + case Event.KEY_HOME: + case Event.KEY_END: + this.moveSelected(kc == Event.KEY_HOME ? 1 : this.viewport.getMetaData('total_rows'), true); + e.stop(); + break; + + case Event.KEY_RETURN: + if (!e.element().match('input')) { + // Popup message window if single message is selected. + if (sel.size() == 1) { + this.msgWindow(sel.get('dataob').first()); + } + } + e.stop(); + break; + + case 65: // A + case 97: // a + if (e.ctrlKey) { + this.selectAll(); + e.stop(); + } + break; + } + }, + + _closeRedBox: function() + { + var c = RedBox.getWindowContents(); + KronolithCore.addGC([ c, c.descendants() ].flatten()); + RedBox.close(); + }, + + /* Onload function. */ + _onLoad: function() { + var tmp, + C = KronolithCore.clickObserveHandler, + dmenu = KronolithCore.DMenu; + + if (Horde.dhtmlHistory.initialize()) { + Horde.dhtmlHistory.addListener(this.go.bind(this)); + } + + /* Initialize the starting page if necessary. addListener() will have + * already fired if there is a current location so only do a go() + * call if there is no current location. */ + if (!Horde.dhtmlHistory.getCurrentLocation()) { + this.go(Kronolith.conf.login_view); + } + + /* Add popdown menus. */ + /* + KronolithCore.addPopdown('button_reply', 'reply'); + dmenu.disable('button_reply_img', true, true); + KronolithCore.addPopdown('button_forward', 'forward'); + dmenu.disable('button_forward_img', true, true); + KronolithCore.addPopdown('button_other', 'otheractions'); + */ + + /* Set up click event observers for elements on main page. */ + tmp = $('kronolithLogo'); + if (tmp.visible()) { + C({ d: tmp.down('a'), f: this.go.bind(this, 'portal') }); + } + + C({ d: $('id_fullday'), f: function() { $('kronolithEventForm').select('.edit_at').each(Element.toggle); } }); + + C({ d: $('kronolithNewEvent'), f: KronolithCore.editEvent }); + + $('kronolithEventActions').select('input.button').each(function(s) { + C({ d: s, f: function() { Effect.Fade('kronolithEventForm');} }); + }); + + $('kronolithEventForm').select('div.kronolithTags span').each(function(s) { + $('id_tags').value = $F('id_tags') + s.getText() + ', '; + }); + + [ 'Day', 'Week', 'Month', 'Year', 'Tasks', 'Agenda' ].each(function(a) { + C({ d: $('kronolithNav' + a), f: KronolithCore.go.bind(KronolithCore, a.toLowerCase()) }); + }); + + $('kronolithMenu').select('div.kronolithCalendars div').each(function(s) { + C({ d: s, f: KronolithCore.toggleCalendar.bind(KronolithCore, s) }); + s.observe('mouseover', s.addClassName.curry('kronolithCalOver')); + s.observe('mouseout', s.removeClassName.curry('kronolithCalOver')); + }); + + C({ d: $('kronolithMinicalDate'), f: KronolithCore.go.bind(KronolithCore, 'month:' + $('kronolithMinicalDate').readAttribute('date')) }); + $('kronolithMinical').select('td').each(function(td) { + C({ d: td, f: function() { + if (td.hasClassName('kronolithMinicalWeek')) { + KronolithCore.go('week:' + td.readAttribute('date')); + } else if (!td.hasClassName('empty')) { + KronolithCore.go('day:' + td.readAttribute('date')); + } + }}); + }); + + /* Set up click event observers for elements on month view. */ + $('kronolithViewMonth').select('.kronolithFirstCol').each(function(l) { + C({ d: l, f: KronolithCore.go.bind(KronolithCore, 'week') }); + }); + $('kronolithViewMonth').select('.kronolithDay').each(function(l) { + C({ d: l, f: KronolithCore.go.bind(KronolithCore, 'day') }); + }); + + /* + C({ d: $('composelink'), f: KronolithCore.compose.bind(KronolithCore, 'new') }); + C({ d: $('checkmaillink'), f: this.pollFolders.bind(this) }); + + [ 'portal', 'options' ].each(function(a) { + var d = $('app' + a); + if (d) { + C({ d: d, f: this.go.bind(this, a) }); + } + }, this); + tmp = $('applogout'); + if (tmp) { + C({ d: tmp, f: function() { $('applogout').down('A').update('[' + KronolithText.onlogout + ']'); KronolithCore.logout(); } }); + } + + tmp = $('applicationfolders'); + if (tmp) { + tmp.select('li.custom a').each(function(s) { + C({ d: s, f: this.go.bind(this, 'app:' + s.readAttribute('app')) }); + }, this); + } + + C({ d: $('newfolder'), f: this.createBaseFolder.bind(this) }); + new Drop('dropbase', this._folderDropConfig); + tmp = $('hometab'); + if (tmp) { + C({ d: tmp, f: this.go.bind(this, 'portal') }); + } + $('tabbar').select('a.applicationtab').each(function(a) { + C({ d: a, f: this.go.bind(this, 'app:' + a.readAttribute('app')) }); + }, this); + C({ d: $('button_reply'), f: this.composeMailbox.bind(this, 'reply'), ns: true }); + C({ d: $('button_forward'), f: this.composeMailbox.bind(this, Kronolith.conf.forward_default), ns: true }); + [ 'spam', 'ham', 'deleted' ].each(function(a) { + var d = $('button_' + a); + if (d) { + C({ d: d, f: this.flag.bind(this, a) }); + } + }, this); + C({ d: $('button_compose').down('A'), f: KronolithCore.compose.bind(KronolithCore, 'new') }); + C({ d: $('button_other'), f: function(e) { dmenu.trigger(e.findElement('A').next(), true); }, p: true }); + C({ d: $('qoptions').down('.qclose a'), f: this.searchfilterClear.bind(this, false) }); + [ 'all', 'current' ].each(function(a) { + var d = $('sf_' + a); + if (d) { + C({ d: d, f: this.updateSearchfilter.bind(this, a, 'folder') }); + } + }, this); + [ 'msgall', 'from', 'to', 'subject' ].each(function(a) { + C({ d: $('sf_' + a), f: this.updateSearchfilter.bind(this, a, 'msg') }); + }, this); + C({ d: $('msglistHeader'), f: this.sort.bind(this), p: true }); + C({ d: $('ctx_folder_create'), f: function() { this.createSubFolder(dmenu.element()); }.bind(this), ns: true }); + C({ d: $('ctx_folder_rename'), f: function() { this.renameFolder(dmenu.element()); }.bind(this), ns: true }); + C({ d: $('ctx_folder_empty'), f: function() { var mbox = dmenu.element().readAttribute('mbox'); dmenu.close(true); if (window.confirm(Kronolith.text.empty_folder)) { KronolithCore.doAction('EmptyFolder', { folder: mbox }, null, this._emptyFolderCallback.bind(this)); } }.bind(this), ns: true }); + C({ d: $('ctx_folder_delete'), f: function() { var mbox = dmenu.element().readAttribute('mbox'); dmenu.close(true); if (window.confirm(Kronolith.text.delete_folder)) { KronolithCore.doAction('DeleteFolder', { folder: mbox }, null, this.bcache.get('folderC') || this.bcache.set('folderC', this._folderCallback.bind(this))); } }.bind(this), ns: true }); + [ 'ctx_folder_seen', 'ctx_folder_unseen' ].each(function(a) { + C({ d: $(a), f: function(type) { this.flag(type, null, dmenu.element().readAttribute('mbox')); }.bind(this, a == 'ctx_folder_seen' ? 'allSeen' : 'allUnseen'), ns: true }); + }, this); + [ 'ctx_folder_poll', 'ctx_folder_nopoll' ].each(function(a) { + C({ d: $(a), f: function(modify) { this.modifyPollFolder(dmenu.element().readAttribute('mbox'), modify); }.bind(this, a == 'ctx_folder_poll'), ns: true }); + }, this); + C({ d: $('ctx_container_create'), f: function() { this.createSubFolder(dmenu.element()); }.bind(this), ns: true }); + C({ d: $('ctx_container_rename'), f: function() { this.renameFolder(dmenu.element()); }.bind(this), ns: true }); + [ 'reply', 'reply_all', 'reply_list', 'forward_all', 'forward_body', 'forward_attachments' ].each(function(a) { + C({ d: $('ctx_message_' + a), f: this.composeMailbox.bind(this, a), ns: true }); + }, this); + [ 'seen', 'unseen', 'flagged', 'clear', 'spam', 'ham', 'blacklist', 'whitelist', 'deleted', 'undeleted' ].each(function(a) { + var d = $('ctx_message_' + a); + if (d) { + C({ d: d, f: this.flag.bind(this, a), ns: true }); + } + }, this); + C({ d: $('ctx_draft_resume'), f: this.composeMailbox.bind(this, 'resume') }); + [ 'flagged', 'clear', 'deleted', 'undeleted' ].each(function(a) { + var d = $('ctx_draft_' + a); + if (d) { + C({ d: d, f: this.flag.bind(this, a), ns: true }); + } + }, this); + [ 'reply', 'reply_all', 'reply_list' ].each(function(a) { + C({ d: $('ctx_reply_' + a), f: this.composeMailbox.bind(this, a), ns: true }); + }, this); + [ 'forward_all', 'forward_body', 'forward_attachments' ].each(function(a) { + C({ d: $('ctx_forward_' + a), f: this.composeMailbox.bind(this, a), ns: true }); + }, this); + C({ d: $('previewtoggle'), f: this.togglePreviewPane.bind(this), ns: true }); + [ 'seen', 'unseen', 'flagged', 'clear', 'blacklist', 'whitelist' ].each(function(a) { + var d = $('oa_' + a); + if (d) { + C({ d: d, f: this.flag.bind(this, a), ns: true }); + } + }, this); + C({ d: $('oa_selectall'), f: this.selectAll.bind(this), ns: true }); + + tmp = $('oa_purge_deleted'); + if (tmp) { + C({ d: tmp, f: this.purgeDeleted.bind(this), ns: true }); + } + + $('toggleHeaders').select('A').each(function(a) { + C({ d: a, f: function() { [ a.up().select('A'), $('msgHeadersColl', 'msgHeaders') ].flatten().invoke('toggle'); }, ns: true }); + }); + $('msg_newwin', 'msg_newwin_options').compact().each(function(a) { + C({ d: a, f: function() { this.msgWindow(this.viewport.getViewportSelection().search({ imapuid: { equal: [ Kronolith.conf.msg_index ] } , view: { equal: [ Kronolith.conf.msg_folder ] } }).get('dataob').first()); }.bind(this) }); + }, this); + */ + + this._resizeIE6(); + }, + + // IE 6 width fixes (See Bug #6793) + _resizeIE6: function() + { + // One width to rule them all: + // 20 label, 2 label border, 2 label margin, 16 scrollbar, + // 7 cols, 2 col border, 2 col margin + var col_width = (($('kronolithViewMonth').getWidth()-20-2-2-16)/7)-2-2; + $('kronolithViewMonth').select('.kronolithCol').invoke('setStyle', { width: col_width + 'px' }); + + // Set month dimensions. + // 6 rows, 2 row border, 2 row margin + var col_height = (($('kronolithViewMonth').getHeight()-25)/6)-2-2; + $('kronolithViewMonth').select('.kronolithViewBody .kronolithCol').invoke('setStyle', { height: col_height + 'px' }); + $('kronolithViewMonth').select('.kronolithViewBody .kronolithFirstCol').invoke('setStyle', { height: col_height + 'px' }); + + // Set week dimensions. + $('kronolithViewWeek').select('.kronolithCol').invoke('setStyle', { width: (col_width - 1) + 'px' }); + + // Set day dimensions. + // 20 label, 2 label border, 2 label margin, 16 scrollbar, 2 col border + var head_col_width = $('kronolithViewDay').getWidth()-20-2-2-16-3; + // 20 label, 2 label border, 2 label margin, 16 scrollbar, 2 col border + // 7 cols + var col_width = ((head_col_width+7)/7)-1; + $('kronolithViewDay').select('.kronolithViewHead .kronolithCol').invoke('setStyle', { width: head_col_width + 'px' }); + $('kronolithViewDay').select('.kronolithViewBody .kronolithCol').invoke('setStyle', { width: col_width + 'px' }); + $('kronolithViewDay').select('.kronolithViewBody .kronolithAllDay .kronolithCol').invoke('setStyle', { width: head_col_width + 'px' }); + + /* + if (Kronolith.conf.is_ie6) { + var tmp = parseInt($('sidebarPanel').getStyle('width'), 10), + tmp1 = document.viewport.getWidth() - tmp - 30; + $('normalfolders').setStyle({ width: tmp + 'px' }); + $('kronlithmain').setStyle({ width: tmp1 + 'px' }); + $('msglist').setStyle({ width: (tmp1 - 5) + 'px' }); + $('msgBody').setStyle({ width: (tmp1 - 25) + 'px' }); + tmp = $('dimpmain_portal').down('IFRAME'); + if (tmp) { + this._resizeIE6Iframe(tmp); + } + } + */ + }, + + _resizeIE6Iframe: function(iframe) + { + if (Kronolith.conf.is_ie6) { + iframe.setStyle({ width: $('kronolithmain').getStyle('width'), height: (document.viewport.getHeight() - 20) + 'px' }); + } + }, + + editEvent: function() + { + $('kronolithEventForm').appear({ duration: 0.5 }); + }, + + toggleCalendar: function(elm) + { + if (elm.hasClassName('on')) { + elm.removeClassName('on'); + } else { + elm.addClassName('on'); + } + } + +}; + +// Initialize DMenu now. Need to init here because IE doesn't load dom:loaded +// in a predictable order. +if (typeof ContextSensitive != 'undefined') { + KronolithCore.DMenu = new ContextSensitive(); +} + +document.observe('dom:loaded', function() { + /* Don't do additional onload stuff if we are in a popup. We need a + * try/catch block here since, if the page was loaded by an opener + * out of this current domain, this will throw an exception. */ + try { + if (parent.opener && + parent.opener.location.host == window.location.host && + parent.opener.KronolithCore) { + Kronolith.baseWindow = parent.opener.Kronolith.baseWindow || parent.opener; + } + } catch (e) {} + + /* Init garbage collection function - runs every 10 seconds. */ + new PeriodicalExecuter(function() { + if (KronolithCore.remove_gc.size()) { + try { + $A(KronolithCore.remove_gc.splice(0, 75)).compact().invoke('stopObserving'); + } catch (e) { + KronolithCore.debug('remove_gc[].stopObserving', e); + } + } + }, 10); + + //$('kronolithLoading').hide(); + //$('kronolithPage').show(); + + /* Start message list loading as soon as possible. */ + KronolithCore._onLoad(); + + /* Bind key shortcuts. */ + document.observe('keydown', KronolithCore._keydownHandler.bind(KronolithCore)); + + /* Resize elements on window size change. */ + Event.observe(window, 'resize', KronolithCore._onResize.bind(KronolithCore)); + + if (Kronolith.conf.is_ie6) { + /* Disable text selection in preview pane for IE 6. */ + document.observe('selectstart', Event.stop); + + /* Since IE 6 doesn't support hover over non-links, use javascript + * events to replicate mouseover CSS behavior. */ + $('foobar').compact().invoke('select', 'LI').flatten().compact().each(function(e) { + e.observe('mouseover', e.addClassName.curry('over')).observe('mouseout', e.removeClassName.curry('over')); + }); + } +}); + +Event.observe(window, 'load', function() { + KronolithCore.window_load = true; +}); + +/* Helper methods for setting/getting element text without mucking + * around with multiple TextNodes. */ +Element.addMethods({ + setText: function(element, text) + { + var t = 0; + $A(element.childNodes).each(function(node) { + if (node.nodeType == 3) { + if (t++) { + Element.remove(node); + } else { + node.nodeValue = text; + } + } + }); + + if (!t) { + $(element).insert(text); + } + }, + + getText: function(element, recursive) + { + var text = ''; + $A(element.childNodes).each(function(node) { + if (node.nodeType == 3) { + text += node.nodeValue; + } else if (recursive && node.hasChildNodes()) { + text += $(node).getText(true); + } + }); + return text; + } +}); + +/* Create some utility functions. */ +Object.extend(Array.prototype, { + numericSort: function() + { + return this.sort(function(a, b) { + if (a > b) { + return 1; + } else if (a < b) { + return -1; + } + return 0; + }); + } +}); + +Object.extend(String.prototype, { + // We define our own version of evalScripts() to make sure that all + // scripts are running in the same scope and that all functions are + // defined in the global scope. This is not the case when using + // prototype's evalScripts(). + evalScripts: function() + { + var re = /function\s+([^\s(]+)/g; + this.extractScripts().each(function(s) { + var func; + eval(s); + while (func = re.exec(s)) { + window[func[1]] = eval(func[1]); + } + }); + } +}); diff --git a/kronolith/lib/Kronolith.php b/kronolith/lib/Kronolith.php index 1510b50e2..79f0b7c8e 100644 --- a/kronolith/lib/Kronolith.php +++ b/kronolith/lib/Kronolith.php @@ -44,6 +44,120 @@ define('PERMS_DELEGATE', 1024); class Kronolith { /** + * Output everything up to but not including the tag. + * + * @since Kronolith 3.0 + * + * @param string $title The title of the page. + * @param array $scripts Any additional scripts that need to be loaded. + * Each entry contains the three elements necessary + * for a Horde::addScriptFile() call. + */ + function header($title, $scripts = array()) + { + // Don't autoload any javascript files. + Horde::disableAutoloadHordeJS(); + + // Need to include script files before we start output + Horde::addScriptFile('prototype.js', 'horde', true); + Horde::addScriptFile('effects.js', 'horde', true); + + // ContextSensitive must be loaded first. + while (list($key, $val) = each($scripts)) { + if (($val[0] == 'ContextSensitive.js') && + ($val[1] == 'kronolith')) { + Horde::addScriptFile($val[0], $val[1], $val[2]); + unset($scripts[$key]); + break; + } + } + Horde::addScriptFile('kronolith.js', 'kronolith', true); + + // Add other scripts now + foreach ($scripts as $val) { + call_user_func_array(array('Horde', 'addScriptFile'), $val); + } + + $page_title = $GLOBALS['registry']->get('name'); + if (!empty($title)) { + $page_title .= ' :: ' . $title; + } + + if (isset($GLOBALS['language'])) { + header('Content-type: text/html; charset=' . NLS::getCharset()); + header('Vary: Accept-Language'); + } + + echo '' . "\n" . + (!empty($GLOBALS['language']) ? '\n". + "\n" . + '' . htmlspecialchars($page_title) . "\n" . + '\n". + Kronolith::wrapInlineScript(Kronolith::includeJSVars()); + + Kronolith::includeStylesheetFiles(true); + + echo "\n"; + + // Send what we have currently output so the browser can start + // loading CSS/JS. See: + // http://developer.yahoo.com/performance/rules.html#flush + flush(); + } + + /** + * Outputs the javascript code which defines all javascript variables + * that are dependent on the local user's account. + * + * @since Kronolith 3.0 + * + * @private + * + * @return string + */ + function includeJSVars() + { + global $browser, $conf, $prefs, $registry; + + require_once 'Horde/Serialize.php'; + + $kronolith_webroot = $registry->get('webroot'); + $horde_webroot = $registry->get('webroot', 'horde'); + + /* Variables used in core javascript files. */ + $code['conf'] = array( + 'URI_AJAX' => Horde::url($kronolith_webroot . '/ajax.php', true, -1), + 'URI_PREFS' => Horde::url($horde_webroot . '/services/prefs/', true, -1), + //'URI_VIEW' => Util::addParameter(Horde::url($imp_webroot . '/view.php', true, -1), array('actionID' => 'view_source', 'id' => 0), null, false), + + 'SESSION_ID' => defined('SID') ? SID : '', + + 'prefs_url' => str_replace('&', '&', Horde::getServiceLink('options', 'kronolith')), + + 'name' => $registry->get('name'), + + 'is_ie6' => ($browser->isBrowser('msie') && ($browser->getMajor() < 7)), + + 'login_view' => $prefs->getValue('defaultview'), + + // Turn debugging on? + 'debug' => !empty($conf['js']['debug']), + ); + + /* Gettext strings used in core javascript files. */ + $code['text'] = array_map('addslashes', array( + )); + for ($i = 1; $i <= 12; ++$i) { + $code['text']['month'][$i - 1] = NLS::getLangInfo(constant('MON_' . $i)); + } + for ($i = 1; $i <= 7; ++$i) { + $code['text']['weekday'][$i] = NLS::getLangInfo(constant('DAY_' . $i)); + } + + return array('var Kronolith = ' . Horde_Serialize::serialize($code, SERIALIZE_JSON, NLS::getCharset()) . ';'); + } + + /** * Add inline javascript to the output buffer. * * @since Kronolith 2.2 @@ -70,8 +184,8 @@ class Kronolith { } $GLOBALS['__kronolith_inline_script'][] = $script; - // If headers have already been sent, we need to output a - // \n"; + } + + /** + * Outputs the necessary script tags, honoring local configuration choices + * as to script caching. + * + * @since Kronolith 3.0 + */ + function includeScriptFiles() + { + global $conf; + + $cache_type = @$conf['server']['cachejs']; + + if (empty($cache_type) || + $cache_type == 'none' || + ($cache_type == 'horde_cache' && + $conf['cache']['driver'] == 'none')) { + Horde::includeScriptFiles(); + return; + } + + $js_tocache = $js_force = array(); + $mtime = array(0); + + $s_list = Horde::listScriptFiles(); + foreach ($s_list as $app => $files) { + foreach ($files as $file) { + if ($file['d'] && ($file['f'][0] != '/')) { + $js_tocache[$file['p'] . $file['f']] = false; + $mtime[] = filemtime($file['p'] . $file['f']); + } else { + $js_force[] = $file['u']; + } + } + } + + require_once KRONOLITH_BASE . '/lib/version.php'; + $sig = md5(serialize($s_list) . max($mtime) . KRONOLITH_VERSION); + + switch ($cache_type) { + case 'filesystem': + $js_filename = '/' . $sig . '.js'; + $js_path = $conf['server']['cachejsparams']['file_location'] . $js_filename; + $js_url = $conf['server']['cachejsparams']['file_url'] . $js_filename; + $exists = file_exists($js_path); + break; + + case 'horde_cache': + require_once 'Horde/Cache.php'; + $cache = &Horde_Cache::singleton($conf['cache']['driver'], Horde::getDriverConfig('cache', $conf['cache']['driver'])); + $exists = $cache->exists($sig, empty($conf['server']['cachejsparams']['lifetime']) ? 0 : $conf['server']['cachejsparams']['lifetime']); + $js_url = Kronolith::getCacheURL('js', $sig); + break; + } + + if (!$exists) { + $out = ''; + foreach ($js_tocache as $key => $val) { + // Separate JS files with a newline since some compressors may + // strip trailing terminators. + if ($val) { + // Minify these files a bit by removing newlines and + // comments. + $out .= preg_replace(array('/\n+/', '/\/\*.*?\*\//'), array('', ''), file_get_contents($key)) . "\n"; + } else { + $out .= file_get_contents($key) . "\n"; + } + } + + switch ($cache_type) { + case 'filesystem': + register_shutdown_function(array('Kronolith', '_filesystemGC'), 'js'); + file_put_contents($js_path, $out); + break; + + case 'horde_cache': + $cache->set($sig, $out); + break; + } + } + + foreach (array_merge(array($js_url), $js_force) as $val) { + echo '' . "\n"; + } + } + + /** + * Outputs the necessary style tags, honoring local configuration choices + * as to stylesheet caching. + * + * @since Kronolith 3.0 + * + * @param boolean $print Include print CSS? + */ + function includeStylesheetFiles($print = false) + { + global $conf, $prefs, $registry; + + $theme = $prefs->getValue('theme'); + $themesfs = $registry->get('themesfs'); + $themesuri = $registry->get('themesuri'); + $css = Horde::getStylesheets('kronolith', $theme); + $css_out = array(); + + // Add print specific stylesheets. + if ($print) { + // Add Horde print stylesheet + $css_out[] = array('u' => $registry->get('themesuri', 'horde') . '/print/screen.css', + 'f' => $registry->get('themesfs', 'horde') . '/print/screen.css', + 'm' => 'print'); + $css_out[] = array('u' => $themesuri . '/print/screen.css', + 'f' => $themesfs . '/print/screen.css', + 'm' => 'print'); + if (file_exists($themesfs . '/' . $theme . '/print.css')) { + $css_out[] = array('u' => $themesuri . '/' . $theme . '/print.css', + 'f' => $themesfs . '/' . $theme . '/print.css', + 'm' => 'print'); + } + } + + $css[] = array('u' => $themesuri . '/ajax.css', + 'f' => $themesfs . '/ajax.css'); + + // Load custom stylesheets. + if (!empty($conf['css_files'])) { + foreach ($conf['css_files'] as $css_file) { + $css[] = array('u' => $themesuri . '/' . $css_file, + 'f' => $themesfs . '/' . $css_file); + } + } + + $cache_type = @$conf['server']['cachecss']; + + if (empty($cache_type) || + $cache_type == 'none' || + ($cache_type == 'horde_cache' && + $conf['cache']['driver'] == 'none')) { + $css_out = array_merge($css, $css_out); + } else { + $mtime = array(0); + $out = ''; + + foreach ($css as $file) { + $mtime[] = filemtime($file['f']); + } + + require_once KRONOLITH_BASE . '/lib/version.php'; + $sig = md5(serialize($css) . max($mtime) . KRONOLITH_VERSION); + + switch ($cache_type) { + case 'filesystem': + $css_filename = '/' . $sig . '.css'; + $css_path = $conf['server']['cachecssparams']['file_location'] . $css_filename; + $css_url = $conf['server']['cachecssparams']['file_url'] . $css_filename; + $exists = file_exists($css_path); + break; + + case 'horde_cache': + require_once 'Horde/Cache.php'; + $cache = &Horde_Cache::singleton($GLOBALS['conf']['cache']['driver'], Horde::getDriverConfig('cache', $GLOBALS['conf']['cache']['driver'])); + $exists = $cache->exists($sig, empty($GLOBALS['conf']['server']['cachecssparams']['lifetime']) ? 0 : $GLOBALS['conf']['server']['cachecssparams']['lifetime']); + $css_url = Kronolith::getCacheURL('css', $sig); + break; + } + + if (!$exists) { + $flags = defined('FILE_IGNORE_NEW_LINES') ? (FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : 0; + foreach ($css as $file) { + $path = substr($file['u'], 0, strrpos($file['u'], '/') + 1); + // Fix relative URLs, remove multiple whitespaces, and + // strip comments. + $out .= preg_replace(array('/(url\(["\']?)([^\/])/i', '/\s+/', '/\/\*.*?\*\//'), array('$1' . $path . '$2', ' ', ''), implode('', file($file['f'], $flags))); + } + + switch ($cache_type) { + case 'filesystem': + register_shutdown_function(array('Kronolith', '_filesystemGC'), 'css'); + file_put_contents($css_path, $out); + break; + + case 'horde_cache': + $cache->set($sig, $out); + break; + } + } + + $css_out = array_merge(array(array('u' => $css_url)), $css_out); + } + + foreach ($css_out as $file) { + echo '' . "\n"; + } + } + + /** + * Creates a URL for cached Kronolith data. + * + * @since Kronolith 3.0 + * + * @param string $type The cache type. + * @param string $cid The cache id. + * + * @return string The URL to the cache page. + */ + function getCacheURL($type, $cid) + { + $parts = array( + $GLOBALS['registry']->get('webroot'), + 'cache.php', + $type, + $cid + ); + return Horde::url(implode('/', $parts)); + } + + /** + * Do garbage collection in the statically served file directory. + * + * @since Kronolith 3.0 + * + * @access private + * + * @param string $type Either 'css' or 'js'. + */ + function _filesystemGC($type) + { + static $dir_list = array(); + + $ptr = $GLOBALS['conf']['server'][(($type == 'css') ? 'cachecssparams' : 'cachejsparams')]; + $dir = $ptr['file_location']; + if (in_array($dir, $dir_list)) { + return; + } + + $c_time = time() - $ptr['lifetime']; + $d = dir($dir); + $dir_list[] = $dir; + + while (($entry = $d->read()) !== false) { + $path = $dir . '/' . $entry; + if (in_array($entry, array('.', '..'))) { + continue; + } + + if ($c_time > filemtime($path)) { + $old_error = error_reporting(0); + unlink($path); + error_reporting($old_error); + } + } + $d->close(); + } + + /** * Returns all the events that happen each day within a time period. * * @param object $startDate The start of the time range. @@ -2031,6 +2413,35 @@ class Kronolith { } /** + * Returns the background color for a calendar. + * + * @param array|Horde_Share_Object $calendar A calendar share or a hash + * from a remote calender + * definition. + * + * @return string A HTML color code. + */ + function backgroundColor($calendar) + { + $color = is_array($calendar) ? @$calendar['color'] : $calendar->get('color'); + return empty($color) ? '#dddddd' : $color; + } + + /** + * Returns the foreground color for a calendar. + * + * @param array|Horde_Share_Object $calendar A calendar share or a hash + * from a remote calender + * definition. + * @return string A HTML color code. + */ + function foregroundColor($calendar) + { + require_once 'Horde/Image.php'; + return Horde_Image::brightness(Kronolith::backgroundColor($calendar)) < 128 ? '#f6f6f6' : '#000'; + } + + /** * Builds Kronolith's list of menu items. */ function getMenu($returnType = 'object') diff --git a/kronolith/templates/index/day.inc b/kronolith/templates/index/day.inc new file mode 100644 index 000000000..0cd9dcfb6 --- /dev/null +++ b/kronolith/templates/index/day.inc @@ -0,0 +1,75 @@ + diff --git a/kronolith/templates/index/index.inc b/kronolith/templates/index/index.inc new file mode 100644 index 000000000..36c983432 --- /dev/null +++ b/kronolith/templates/index/index.inc @@ -0,0 +1,244 @@ + +
+ + + +
+ getName(); if ($logout_link) echo ' · ' . $logout_link ?>
+ · · ' . $help_link ?>
+
+ +
+ + + +
+
+
+
format('l, j') ?>
+
+
+
format('F') ?>
+
+
+
format('Y') ?>
+
+
+ +
+ + + + + + +
+
+ + + +
+ + +
+
+ + + +
+ +

+ + + +

+ + +
+ $cal): ?> +
get('name')) ?>
+ +
+ + +

+ + + +

+ + +
+ $cal): ?> +
get('name')) ?>
+ +
+ + +

+ + + +

+ + +
+ $cal): ?> +
+ +
+ + +
+ + + + + + + + + mday = 1; + $week = $firstDay->weekOfYear(); + while (!$currentDay || $currentDay->month == $today->month): + $currentDay = Horde_Date::firstDayOfWeek($week, $today->year); + ?> + + + + + mday++; endfor; ?> + + + +
+ ">< + ">> + format('F Y') ?> +
 ">">">">">">">
mday ?>
+
+ +

+ + +

+ +
+
+ +
+
+
+ + + + + + + +
+getImageDir('horde')); +require dirname(__FILE__) . '/month.inc'; +require dirname(__FILE__) . '/week.inc'; +require dirname(__FILE__) . '/day.inc'; +?> +
+ + +
+ + + + + diff --git a/kronolith/templates/index/month.inc b/kronolith/templates/index/month.inc new file mode 100644 index 000000000..d66540b84 --- /dev/null +++ b/kronolith/templates/index/month.inc @@ -0,0 +1,170 @@ + diff --git a/kronolith/templates/index/week.inc b/kronolith/templates/index/week.inc new file mode 100644 index 000000000..5040eb86a --- /dev/null +++ b/kronolith/templates/index/week.inc @@ -0,0 +1,115 @@ + diff --git a/kronolith/templates/javascript_defs.php b/kronolith/templates/javascript_defs.php index 075b7e982..120bb7876 100644 --- a/kronolith/templates/javascript_defs.php +++ b/kronolith/templates/javascript_defs.php @@ -1,4 +1,7 @@ <+2 zGL{7S1v5B2yO9RuNO`(AhE&{2N^oHI@!{#|;Zf)~xp<>j0|PSy?;{4o`KHbMKvfK$ Lu6{1-oD!M<-5oJ# literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/back_quick.png b/kronolith/themes/graphics/back_quick.png new file mode 100644 index 0000000000000000000000000000000000000000..c99339a9de4a30958eea75e8b845926417b316ae GIT binary patch literal 904 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCPvc+%vf8J;`wZkrmbgZg1m~xflqVLYGB~E> zC#5QQ<|d}62BjvZR2H60wP9dj*79_545_&F=KMxp1_d5w$M(O|Kc%!Ou?j1&{xy$} zSDlzUchSMM|1ys7u8db;Vq)PCP;h8qU<5KI9M5IEu|8RffkB`l@!-@_CY2rk0yRGJ<`le_dT$@yRS#tS4Q;kZ*gq zLjwng03)N4f&+^Sl)dP+8w2Bw?I2quSejPWU$%I0%;}wO%#B;om1njJggUM{?DVD4 zqF@75A4^9AgCGHR%6g6i=VTuP11eBNrSQLTewc`q_rf_l4?BN8E1Gx9F4)hky})AQ z5ysO3YT|;}C#{!v`N{y-l z$hJX^#SlVu6Id075Ce*vz#bteWZ%p1_U1+|1_2R=i&OspU;aJyd{V*!k7dQDlqNr| zS~=4)*X78ooLzbhdp0oq5E8h|&M;X^hG7p+M*+izr~E7p%NaFm84p}#QBq-uVNK!T zWRPKJVPIilU}SV~U=R=hF{!|V7z8tuM!dSrACF8wi&|tP1G5%`r>mdKI;Vst0M@qJ ASO5S3 literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/checkbox_off.png b/kronolith/themes/graphics/checkbox_off.png new file mode 100644 index 0000000000000000000000000000000000000000..1a78b26e06ae29ecc43cb75f889d69674b6c5c19 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!9%)r1XI!iqT$dN2@jVKAuPb(=;EJ|f?Ovz75 zRq)JBOiv9;O-!jQJeg_(RKywJ6XN>+|Nn^-Cjv=F|3fE$g3KjBe!>6$Gh9E#>j>o9 zdb&7cWMQ jJlc{H1{@o7Iv5xd+U1Hvm|}hd4Px+g^>bP0l+XkK{$e~5 literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/checkbox_on.png b/kronolith/themes/graphics/checkbox_on.png new file mode 100644 index 0000000000000000000000000000000000000000..564a0de6ccfeef3c13c8ec6f580cce80390eebb7 GIT binary patch literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!6%)r1HuVsG}$dN2@jVKAuPb(=;EJ|f?Ovz75 zRq)JBOiv9;O-!jQJeg_(R8$w>6XH5y!i4|-{~ML*_)T%k*_>2+v?z0HoN0xAz-*6c zH@hMi2ehBA?z_@du{XExeB-1mt^U(Inom`imHfV--(e})!#X+g-V|*sL z1kdwoKT{nr-7|k{ivMgkyJm~HrJ;r;+67xvqZS7=A1e=;?Q33RRCzc*dO;A--kKmj zaiB}IN`m}?fn4GMgVBl)F+g2So-U3d5|XS3TN9aqL9p<5#jAJk-t13MEE2stb!P1< zh0VLqoec6ia5!9M-<-MgI)BFbF>xq7_@R6D%<+f*dj098`=;t$d(D~|_TG7wQUk+* zgN*^9TX)}#|I=zV-FS2LJ$t*{*0U#HJ}WLQ*%zZ=viFVdQ&MBb@0Lli)9smFU literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/checkbox_over.png b/kronolith/themes/graphics/checkbox_over.png new file mode 100644 index 0000000000000000000000000000000000000000..86c0894b801fbb99569a6e75633f094460736cd2 GIT binary patch literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!6%)r1HuVsG}$dN2@jVKAuPb(=;EJ|f?Ovz75 zRq)JBOiv9;O-!jQJeg_(RFoOu6XH5y!i4|-|4*4RW!9`&D_5@EwQJYJi4%_;IkIKT zmgUQrFI%>(rlzL0wsyyk9eelgoiu6E!Gi~9`n9+OVQe+f-1TiJAG(~~l7)dvra%}(F_ zJ9kF=Ta^X|rgiHx)2_{)zGi*#4omO;-+wzz+LtSa3OX<(Y!zCyYTEX@kF`o-&Y$_T z@%Ep2t&iXSEiBz}C&!=T?B{=<{uR#>m^JhL=QAw~3``6^*jG;zQxo=)d<1k1gQu&X J%Q~loCIFsV$Q1wp literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/drag-handle.png b/kronolith/themes/graphics/drag-handle.png new file mode 100644 index 0000000000000000000000000000000000000000..d55eb52a86e37620ef8d8d6b1e6e432d8968f37e GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^B0$W_!OXzG$a;%sI*=n-;u=vBoS#-wo>-L1;Fyx1 zl&avFo0y&&l$w}QS$Hzl2B?TFz$e7@{rmU-|Nl=5Hh%>aVJZpo3;xgWbi08Ukf-J8 z;uunKD=8r*g@r?b&CRWWfk{b$fkPohNTESNMS_7vz){Vi!BBvgK}d?h_V`tKAD}J< MPgg&ebxsLQ0A#)_r~m)} literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/new_small_fade.png b/kronolith/themes/graphics/new_small_fade.png new file mode 100644 index 0000000000000000000000000000000000000000..87405b40159f2d35fe0fff3484cea087f785e2c1 GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^93afW3?x5a^xFxf7>k44ofy`glX(f`ObhS{aRt)M z%*?E;tXy1Nyg3p^r=85p>QL70(Y)*K0-AbW|YuPgfvZV@2^ z{g3K9`+-6(o-U3d9M@&LJ^308c$oio1vDU;lqK?eqa|&cEzd-%j7^0h-6)>FVdQ&MBb@0Er%k ASO5S3 literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/picker.png b/kronolith/themes/graphics/picker.png new file mode 100644 index 0000000000000000000000000000000000000000..2712746411c9d05bb343df1e3d6b6168a9f752f9 GIT binary patch literal 368 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}U4T!BE0F#V0zdx$|9$$@ft4#3 zOr6quVE^{XlX_;)nzCxe!tGnvZQHW;@2?M2CiTpiHu2Ao_xpBl*}Y@qrE|wl9^H58 z-0}ZA+-6RjxZ2n8dz8=N0FNI(eti7+@y(kz4<0VfmhE0gZNZnrz6T65%cROt3ynNA$v-L+*JgbUd8 zCTR3MnKWlh`7P1NZHz?|XBu=hzw#^1PqX@ycZQu?+k3vqv%T-T{_|>f3GP%ne!EnJ d({^4pGe?!!v&+wvy@5_(@O1TaS?83{1OTA8!dw6V literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/submit.png b/kronolith/themes/graphics/submit.png new file mode 100644 index 0000000000000000000000000000000000000000..56b32a73007235befd7c50f1e109b1173a1dba44 GIT binary patch literal 3306 zcmai1X*d+z9v@rAjETxJwk(xh$`)hGnwgO$+YH$$c*Kx+x>Lk=eZxw?>y&x`2T;)|9m)iY^*LpK%yW3004nom|i*D zU;kV{b^w6QL7&I{@E{Uqh6=L__6dvhyx|Qn!USLQmVyU)V!f|;dt#zP-+1c*0K9kM zrbhNrQ`dHPHsv@n0Rs!eg-?CPS=+0JxH{AM)LZBT$M-G|O~QIlDqm1@)$vktX%wcC z|31Q!{J4{%aNp)tz(no6SEqPpzkeAJ&f2WlhvwPJ8K&*-;{w(NYzr5uQKi9p@dx`v ze{bn?!JRl(#LmVS9UYxVt@Ky-#O!ZQbS506#Okqg@_+Vq)y-?B_EaAP6$A4Tlo%z! zK{7U7fZwAG5lDve){EB zH|D?HB<_h}CHnPX^2UU;FM66Lgfxd&zE<7DQ&((ersI$&f6I(rfs54Y_m}_)HKR{iS~FmW74I+S(cr1lri#bkB4f5r4o+>I9%?fy86TDefK5tR5ZSE zLEXY6Af&v8dLv*AeaJmrh*1^uAZw#b=q2?&Ys+xv3^P^LwZfmDcBjJqD#zZ2S0|qd|`hm zN=if46M{c&T~wk1Z*z2tFmaVcsz;zazhzsFK>c@7AW78A>-GU&eB;lGuj z0a5_g)@H^zXe!%>oU_IV4l`+}1xn!dg9a0-0?96@6KIJmOfb1wI|||PB_1CwrIKR z%qh~-HG~Z#dvjoUDSDIvutAg6a_`2~yVcKZPo!)a*xjsdIk_v46t!RPt;tzL#weIL zc9hpL(PU^aXj-@Fv1kZXm$M(1t~}bsF7&e*KY;Euejo7nM|TT5 zTXF7?fQ0usTD^W050`vG<^vnP*`lCK&oofG3UK*M<%QU_npA$o6JD8^J2~=OsVALN z^m#2ggL3T#6{zm3wXm?cuR|2HF1F3}IPa_IMFUPqM`!B2_V^!rM;9KeZXf0L)9I7o z6jG}rPbv%S3lXk>xIFeaNC%rWlZ!ufC)aRjm1&EygHc_^hl&59%STNe#4( zzESu5I$|b461QH{=ND+@la%e=7Hh?ePdT4*TZ-h9#Vu4wnm=wRZz~{3iJXXp%&ZlP z+_s9x+9KKvo=epFTGp5m$dlu^PkUwD!?Xgk2QRv0l0w!g2*^a}yw=dM_SQz++UNM5q?$Z|8ptCS}_m?fL(MXeZX&J_L z?f_srL^FktYx)}FQE%qv+F{x$Xc$Lb5T#%6>;7DS?6gsnd=>ps;q3ULLcPY zW=~!b@NVJ6d&t%^$Tk5MMC+|qG!gWLqJZdnh2Ba2UvXUo{Cca0$pL?kvY?GLCeyZM zI0N4#NIt8gGBi9qy|NXhXP~3A^gZT3vPPG}G(>4jVSieb{vFB1&bDiEtfWD{?1jfj zpq5f5;^7-ilk+8W^U^|#Na&#+jG73cH>zMeN>98GzPT8py>?out)Vn!l>S{)Q}e`$ z6G!kbd>Y_hzV)a>B^lbWT2~&IK2uZ#o5*z7xesI`o6mzXt=y`2vImoLa&nZZ z++G@t7pXe;L>J~Bs-vIe-wBnHhn+>H@K@bDN69uWE>j68euRuoPcRzKq(~ z)060*R-M@$zO}`@yt5Nso~YS(;K1QI%LXD`;5I@=v1)&6?P?s=;al*(!Pt+1V-injLttvI^c8}O8O9$(!E4fYL(q0(N zIzp#McRc6m+2Bu<^R(8w#*ncq53!BJ&S-|8OHf&pqM{iSr+>jbLAQfGzqC4&;MZ5p zdlcl^E9CevON(i85;^*{Bjg!w!#w)!Y=fZiX*83(Y58nhl_=}UDwkQ6aPl#h)vdMZ z5QK@RXDUU$4`(vP`ZoAhUn>MeYM7Q<*r)XmSZ|T7O7t<+OXAmAPiyuzr=BUxg7J7` z6dsR1IA{&uovM&141`-hI}7vIDy_!Q?0q+?+C$^Mz0JSh=}^DIf2yaddznu60-;z~ zWg*97*>xzCnckY4G!UG`)qR-E{~}!sT>S?jHf}_9c~d1UGPQO8E^{{nF$#SMiQSgV zX3)y8YN{WsQOJW2dF^3uuCDt|FQ9ARlWI505X8eUEH#kjQ;*2&7r zwY9c_Kp>K1wc9g3PWHLBBY0^|&8C`~%VUmcv|q#IFTYhtTie^QdIcLB8@ah658r59 zxX?WMxaIBJw_7poGEbRI=G@#InXGn%I~jXufxn4~2^@Y$yU_=WH492kPDZ2AbK%X? zAE&2v_4SDv85#Nc(_>@yPELKlP1Y2>_iky-ziF-*@d46%lh)Oh7^QD$=pI*HQ6Z;!D%1b~ literal 0 HcmV?d00001 diff --git a/kronolith/themes/graphics/tasks.png b/kronolith/themes/graphics/tasks.png new file mode 100644 index 0000000000000000000000000000000000000000..b39605d30f93df2e792280ce2126f6be07c413da GIT binary patch literal 532 zcmV+v0_**WP)?lY?=Vg>#pNbeV{Bnuv6oh;^HZb+LbQmX?;@ zfNGkXnwy)Op`oF-x3{~yy5X;*;jg5=zQ4b}z{A7C!^FeH#KgtL#mC3T$jHdb%F4^j z%goHo&d<-!(9h4%(9qG)(b3V=)6~<{)Ya6~*Vx$E+1T3J+T7jT;o;%s<>lw+=ji9> z>FMa{>FVt4?CkCA?(XjK@bK~S^7Z!i0001~OspgT0004WQchCzqaKv3&f5hcVuLjV-ukHJT1d zoG}69iyl{0Y68f6MO9D`#30000