Kanbani Web Viewer (KWV) is a Trello-like browser for sync files produced by Kanbani - a free task manager for Android.
This is only a viewer, it does not have editing capabilities! (yet? ;)
(see online demo board)
Note: if you need to parse or pack the data in Kanbani format use this PHP package.
- It works on iOS, Windows, Linux and any other platform with a web browser, even with a JavaScript-less one
- It lets big boards benefit from large screens, with vertical layout and proper print view
- It supports plugins written in PHP and JavaScript
- It has functions to export and import data in various formats (CSV, Trello, etc.)
- And, just like Kanbani, it is totally (money-)free, ad-free and tracker-free!
If you are syncing to the default Kanbani profile then simply open its Share screen on your device and switch to the Online Viewer tab. There, either scan the QR code or copy or email yourself the URL.
If you have your own sync server then you can use KWV to browse Kanbani boards synced using WebDAV or any other transport to that server:
- Download latest release archive and extract it into the
public_htmldirectory of your web server or to a subdirectory (so you get.../public_html/index.phpor.../public_html/kwv/index.php). - Open your website's page in your browser that is hosting KWV (
http://my.site/kwv/) to finish the setup using KWV's install wizard. - Enjoy!
Upgrading:
- Download and extract the latest release archive, overwriting any existing files.
- Enjoy!
You can set up KWV manually without using the install wizard plugin:
- Create and edit the config file (
config.php). Seeconfig-defaults.phpfor available options. At minimum, specify the location of your Kanbani files underunserialize.path, else you will only be able to browse the bundled Welcome Board:
<?php
return [
"unserialize.path" => "/home/kanbani/ftp",
];- For better performance, ensure that the
cachedirectory exists in KWV's directory (public_html) and that it is writable.
- PHP 7 or above
- The
gdPHP module, if generating QR codes using the included phpqrcode plugin - The
opensslPHP module, if dealing with encrypted boards
Ah, I love this question. Let us drill into details!
Trello is poor when it comes to big boards (hundreds of cards, let alone thousands) - it limits you to a narrow column with only title, and forces you to go one card after another to review them en masse.
Not so with KWV - you can switch to a layout where cards occupy all available screen width and show full descriptions (not just excerpts).
(see online demo)
Even better, built in Table of Contents allows quick navigation between hundreds of cards in some dozen lists without changing your context.
(see online demo)
KWV also has much more sane printing view, both for horizontal and vertical layouts where the latter looks just like a book - great for somebody who is getting started with an existing board and needs to ingest it in the entirety.
Plus, there is 1-click plugin for importing Trello boards to enhance Trello "within" KWV just when you need it.
Filter cards by title, description, related name (author), archived flag, or sort them by any field, or change card color display mode, display description snippets, set board's background and layout, etc. etc. Like in mobile Kanbani, KWV allows most of these applied on the per-list basis.
(see online demo)
Board's view state is stored in the URL so, as the link above demonstrates, you can bookmark it to preserve your settings (because… see the next point).
KWV works by sharing URLs. Install Kanbani and sync a profile to KWV - this gives you a long random "profile ID" acting as a username that any number of people can use to access synced boards. No emails, no passwords†, no database, no tracking (see the next point).
† Not entirely true: if you encrypt the profile then visitors will have to supply the same Secret. Reminds you of file sharing, eh?
Hardly any task manager, and certainly none of the cloud-based services let you retain full control (or any control!) over your data. With Kanbani, you can combine 1-tap encryption† (that hides your data) with your own sync server (that hides the fact you are using it) and not depend on any 3rd party at all, ever.
† Its solidity testified by the source code.
Trello generously lets you export data to JSON, but CSV export requires Business Class and you are out of luck for import options at all. KWV lets you export to JSON, CSV, VCS and plain text and import those plus Trello's JSON out of the box. And if you are a programmer in need, it is trivial to roll your own converter.
Though very simple for now, it lets visitors viewing the same profile exchange text messages. This is also useful for storing quick notes between sessions since messages persist for a few days.
JavaScript in KWV is added on top of the page, not instead of it. This makes for more responsive experience since browser doesn't execute ton of scripts in background, plays better with offline viewing, on-page search, various addons and is better for ecology.
This is the simplest way to create a board from scratch. The format matches mobile Kanbani's text Sharing as goes like this:
List title
Card title
Related name
D-U-E T:M ZONE
Description
more description lines...
---
Card title...
<...as above>
===
List title...
<...as above>
All components except List and Card titles are optional. Blank line before Description is required if the latter is present.
Example:
First List
First card's title
authored by me
2020-09-22 13:45 EST
Lorem lorem
ipsum ipsum...
---
This is another card - no related name, no due, no description
===
List number two
First card in there
myself
KWV is not using any framework and so is very easy to get started with. Fundamentally, it is built around event hooks to allow extraordinary customization - you can override any behavior, disable any element, add a new page, plug an import/export converter and so on.
The best way is to learn by example - most plugins are short and easy to follow, check them out.
There are dozens of events triggered in various situations. For example, when user visits a page, the event called serve_<?do value> takes place. This way, ...kwv/index.php?do=viewBoard triggers serve_viewBoard.
Triggering an event means invoking all of its listeners one after another until the first that returns a non-null value.
Imagine we want to add a new text page with our Terms of Service, which opens at this URL: index.php?do=tos. First, we add a new PHP plugin file, e.g. plugins/my-tos.php:
<?php
$context->hooks->register("serve_tos", function () {
ob_start();
?>
<article class="tos">
<h1><?=$this("Terms of Service")?></h1>
<p>Lorem ipsum dolor...</p>
</article>
<?php
echo $this->hooks->template("empty", [
"bodyAttributes" => ["class" => "body_shaded"],
"body" => ob_get_clean(),
]);
return true;
});- All PHP files in
plugins/are automatically included as if they were part ofindex.php. - Included files are given one variable:
$context, which is aContextobject. - The
hooksproperty of$contextis aHooksobject. We use it to add a new event listener that will be invoked whenever users visit a page with?do=tos. - Every listener that is a
Closure(like in this example) will have its$thisset toContext. This object remains unchanged from request start to end so listeners can use it to pass around any data. - When serving the
tospage, we output some HTML. The$this(...)construct is a short form of writing$this->hooks->trigger("translate", ...)and allows easy localization. If you don't need localization, embed texts directly (like withLorem ipsum dolor...above). - Finally, we wrap our HTML into the
emptytemplate, giving it$vars:bodyAttributesfor setting<body class=...>to one of standard KWV classes (there are others, read below) andbodywith the actual content. return truelets the router know that the request was handled, else it would throw up an error.
Save this file and open the new ToS page in your browser. It's a start but the text is hardly readable:
Thankfully, this is easy to fix. Finish by writing plugins/my-tos.css:
.tos {
background-color: #fff6;
margin: 1em;
padding: .1em 1em;
position: relative; /* overlays .body-shaded's shader */
}- CSS and JS files in
plugins/are added into theemptytemplate automatically. Since all standard pages are usingempty, it means they are added into every standard page. - If using the included
minify.phpplugin, stylesheets and scripts are transparently glued together and compressed.
Ah! Looks better now, doesn't it?
Congratulations on writing your first plugin!
This section outlines most useful things in the Kanbani namespace. Feel free to refer to the source code for details.
$frame- object storing data between calls to event listeners of the same event__invoke()- magic method for quick triggering of thetranslateevent by callingHooksas if it was a function:$hooks("%s seconds", 123)__call()- magic method for quick triggering of any event as if it was a PHP method onHooks:$context->hooks->some_event("arg", ...)trigger($event, $args = [])- invokes listeners of$event: first (registerFirst()), normal (register()), last (registerLast()) and returns first non-nullresult; before returning, calls after-listeners (registerAfter()) giving them the result to be returned as first member in$argsregister($event, $func),registerFirst(),registerLast(),registerAfter()- register a new event listener (hook)template($name, $vars = [])- triggersecho_$nameand returnsechoed output as a string
These properties are always available:
$hooks-Hooksobject$config- array of config values merged fromconfig.phpandconfig-defaults.php$custom- object storing custom data, for use by plugins (do not create new properties directly onConextto avoid conflicts with future versions)$tz- current timezone (matchesdate_default_timezone_get())$locale- current locale (matchessetlocale(0))$language- language tag string (e.g.de_DE), for use in HTMLlangattribute andAccept-Languageheader$request- array of request variables ($_REQUEST, in default PHP configuration this is$_GETplus$_POST)$server- copy of$_SERVER$files- array of uploaded files ($_FILES), omitting failed uploads (with non-0error)unserialize()- triggerunserializeif the context is missing profile data
These properties are available in certain contexts:
$syncFile,$syncData- SyncFile and SyncData objects, when serving board-related pages$currentBoard- object with Kanbani board data, one of$syncData->boardsmembers$profileID- string given as?profilefor identifying the same set of boards; ifnullthen current page was generated on the fly and cannot be accessed again (or it is not board-related)$kanbaniQrCode- QrCodeData object for encoding as a Kanbani shared profile QR code; ifnullthen current board (profile) cannot be accessed by the app; if notnullthen$profileIDis also notnulland indicates a valid Kanbani sync profile ID
Useful methods:
__invoke()- magic method for quick triggering of thetranslateevent by callingContextas if it was a function:$context("%s seconds", 123)syncData($data = null, $file = null)- indicates that this context (page) is board-related; if any argument isnullthen new aSyncData/SyncFileis created;$currentBoardis set to the first$data->boards(which must not be empty)currentBoard($board)- checks that$boardis one of$syncData->boardsand sets$currentBoardpersistentReadOnly($profileID),persistent($profileID, $qrCode)- set$profileIDand$kanbaniQrCode(for board-related context only)
Like all classes, these are also under the Kanbani namespace.
JSON_FLAGS- constant expanding toJSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, makingjson_encode()produce much shorter outputPublicException- class extendingExceptionthat indicates an error that is safe to disclose to the user (i.e. without sensitive details)htmlOptions($values, $titles, $current = null)- returns a string of<option>tagshtmlAttributes($attrs)- returns a space-separated string of HTMLkey="value"pairstimeInterval($to, $now = time())- returns an array intranslateformat that indicates the distance between$nowand$to, such as "in 2d" (2 days into the future)formatTime($time)- returns an array intranslateformat with$timepresented as a locale-specific date or date+time string, plustimeInterval(), such as "1:31 AM 1/2/2021 (in 3mo)"formatNumber($number, $decimals = 0)- like standardnumber_format()but returns a locale-specific string, such as1,234.56
start- the first event to occur, triggered after loading all PHPplugins/to fillContextwith basic data about the request (such as$locale)echo_$template ($vars)-echoes a formatted template;$hooks->template()returns it as a stringtemplate/$template.phpis automatically loaded (if exists) similarly to plugins, buttemplate/*.css|jsare not handled specially
serve_$task- handles current request; if no hooks returnedtrue,index.phpoutputs a 404 error pagetranslate ($format, ...$args)- returns a string; arguments aresprintf()style; example- attention: even without
$args,%symbols in$formatmust be doubled for escaping:translate("100%")will fail,translate("100%%")is correct
- attention: even without
external ($url)- returns an URL used in<a href=...>when$urlpoints to an external resource; example that rewrites links via an intermediate page to conceal potentially sensitive referrer informationformat ($text, $source = null)- returns HTML representation of$text; default implementation only adds line breaks and highlights links; when formatting a card's description,$sourceis an objectunserialize- request to fillContextwith Kanbani data (callsyncData()and others), such as duringserve_viewBoard(returntrueif handled); by convention, profile ID is held in?profilequery parameter; example, anotherdecrypt (array $options)- request tounserializean encrypted profile (returnSyncDataon success);$optionsmembers:filePath,syncFile(optionalSyncFilethat is filled withfilePath's data on success),profileID(optional)updated- returnstrueifContext's profile was updated since lastunserialize(caller earlier within the same request)canonical ($query)- returns a fully qualified URL to this KWV installation, with given parameters;$querymay be a string (a=b&c=d) or an array (["a" => "b", "c" => "d"]); default implementation tries to guess current server set-upfilter (array &$cards)- remove cards not matching current request's filters, sort them, etc.; example
Generally, events occur in this order:
user requests index.php?do=viewBoard
-> plugins/*.php loaded
-> "start" triggered
-> "serve_$do" (serve_viewBoard) triggered
-> "unserialize" triggered
-> "decrypt" triggered
-> "filter" triggered
-> "echo_$template" (echo_board) triggered
-> "templates/$template.php" (templates/board.php) loaded
-> "translate" triggered
These are known serve_$task events:
viewBoard- view a Kanbani boardviewCard- view an individual card; visitors usually access this indirectly via AJAX after clicking on a card's title rather than opening a separate pagedecrypt- access encrypted Kanbani profile; seedecryptpluginexport- download Kanbani data (boards, cards or other) in different format, e.g. CSVimport- turn a file into Kanbani board(s); the inverse ofexportqrImageProfile- generate QR code for Kanbani profile sharing (see SyncData)qrImageWeb- generate QR code for opening this KWV installation (encodes an URL)install- used by the installer pluginchat- used by the simple chat plugin
There are multiple echo_$template events that you can override to customize the interface. For example, to add a label to all cards that have "IMPORTANT" string in their description create this plugin:
plugins/my-important.php:
<?php
$context->hooks->register("echo_cardItem", function (array $vars) {
if (strpos($vars["card"]->description, "IMPORTANT") !== false) {
?>
<mark class="isle important">
<?=$this("Important!")?>
</mark>
<?php
}
});plugins/my-important.css:
.important {
background-color: #faa;
}empty- HTML page with only stylesheets and scripts fromplugins/; variables:$body(<body>content),$bodyAttributes(array for<body ...>),$css/$js(arrays of URLs),$title(for<title>)exception- used byplugins/exception.phpwhen displaying an uncaught exception page; variable:$exceptionboard-viewBoardpage; variable:$filterscard-viewCardpage; variables:$card,$listqrCode- QR code image (can be binary); variables:&$headers(array, at minimum must includeContent-Type: ...),$large(boolean indicating desired target size),$data(string to be encoded),$kanbaniQrCode(QrCodeData, ornullif not encoding a profile QR code); exampledecrypt- a piece of info or a form with controls for providing data for viewing an encrypted profile; may be embedded into another pagedecryptPage- complete HTML page wrapping thedecrypttemplate, to be output instead of the requested content when user has to unlock it
Layout of the viewBoard page, top down:
+-------------------------------------------+
| Page header, single line (boardHeader) |
+-------------------------------------------+
· Additional invisible content (boardBars) ·
+-------------------------------------------+
| The board area, i.e. lists and cards |
| __________ _____________ __________ |
| |___List___| __listItem__| ___...___| >|
| | Card | cardItem | | >|
| | Card | cardItem | | >|
|_|_...______|_..._________|__________|____>|
boardHeader- content of the header, with info buttons and search inputboardShare- content of the "Join"/"Share" info button in the headerboardExport,boardImport- controls for picking formats in the "Convert" info button; variable:$form(idof the associated<form>)boardCustomize- table rows for board filters in the header and list filters inlistItemInfoboardBars- content between the header and the board area (ToC is normally placed here); in horizontal mode (aka "Trello"), having anything visible in this area will add vertical scrollbar to the pagelistItem- the column representing a board list; variables:$list,$attributes(given by reference, represents the parent node's attributes),$cards(array inContext's$cardsformat)afterListItem- content afterlistItem(not necessary between two lists); variable:$listlistItemInfo- content of the hint, including per-list filters, on top of each list; variable:$listcardItem- content of the box representing a card in the list; variables:$card,$attributesafterListItem- content aftercardItem; variable:$cardcardItemInfo- content of the "Info" hint near each card in the list; variable:$card
The viewCard page is split into two columns: card's description (left) and ancillary info (right).
cardInfo- content of the right-side column; variable:$cardcardExport- "Download" links in the right-side column; variable:$card
KWV CSS class names follow BEM notation - "Block" __ "Element" _ "Modifier", with words in individual components separated by -: info-hint__block-img_max-height (info-hint block, block-img element, max-height modifier).
Blocks are isolated components on a page. Elements cannot be located outside of their root block. Element part is missing for block's root node (info-hint). Modifier is optional (info-hint__title). info-hint_relative has no element but has a modifier.
KWV does not use Bootstrap or any similar framework, it only uses normalize.css. Below are classes useful in your plugins, see kwv.css for details:
body.body_shaded- small content area visually overlays page background (example: card view when opened outside of a board)body.body-overlay- similar but overlays a page with its own content (example: card view on top of the board)body.body_full- assumesheight: 100%(example: board page)body.js_no,body.js_yes- indicate if JavaScript is on or off (you can check for them but don't add them).middle- creates a vertically and horizontally centered container (example: exception page); mostly useful onbodywith.body_full.light-shade- fakes a small shadow below the element using its border (example: card box in the card list)textarea.block-area- an edit box filling entire line.isle- adds semitransparent background below (example: info buttons in board's page header).filtered- hides the node (example: list cards that do not match filter criteria).round- addsborder-radiuswith standard value (example: card view on top of the board).switch- groups separate content so that only one is seen at a time (example: board sharing info button)table.tbl- generic table.info-hint- attaches floating content visible when hovering over the small.info-hint__titleelement (example: info buttons in board's page header)






