Mercurial > snowl
changeset 183:b497c5463801
move the collections view into an overlay to facilitate its reuse inside the river view
author | Myk Melez <myk@mozilla.org> |
---|---|
date | Sun, 20 Jul 2008 20:21:27 -0700 |
parents | 489b8c2db12d |
children | e005621994a3 |
files | extension/chrome.manifest extension/content/collections.css extension/content/collections.js extension/content/sidebar.css extension/content/sidebar.js extension/content/sidebar.xul |
diffstat | 6 files changed, 435 insertions(+), 479 deletions(-) [+] |
line wrap: on
line diff
--- a/extension/chrome.manifest Sun Jul 20 19:13:25 2008 -0700 +++ b/extension/chrome.manifest Sun Jul 20 20:21:27 2008 -0700 @@ -1,4 +1,5 @@ content snowl content/ locale snowl en-US locale/en-US/ overlay chrome://browser/content/browser.xul chrome://snowl/content/snowl.xul +overlay chrome://snowl/content/sidebar.xul chrome://snowl/content/collections.xul resource snowl ./
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extension/content/collections.css Sun Jul 20 20:21:27 2008 -0700 @@ -0,0 +1,7 @@ +toolbarbutton { + -moz-appearance: none; +} + +.toolbarbutton-icon { + -moz-margin-end: 0; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extension/content/collections.js Sun Jul 20 20:21:27 2008 -0700 @@ -0,0 +1,422 @@ +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://snowl/modules/service.js"); +Cu.import("resource://snowl/modules/datastore.js"); +Cu.import("resource://snowl/modules/log4moz.js"); +Cu.import("resource://snowl/modules/source.js"); +Cu.import("resource://snowl/modules/feed.js"); +Cu.import("resource://snowl/modules/URI.js"); +Cu.import("resource://snowl/modules/identity.js"); +Cu.import("resource://snowl/modules/collection.js"); + +// FIXME: call this SnowlViewWindow to facilitate reuse of this sidebar code +// in the river view, where the window it will reference will not be a browser +// window. +var gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIDocShellTreeItem). + rootTreeItem. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindow); + +SourcesView = { + _log: null, + + // Observer Service + get _obsSvc() { + let obsSvc = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + delete this._obsSvc; + this._obsSvc = obsSvc; + return this._obsSvc; + }, + + get _tree() { + delete this._tree; + return this._tree = document.getElementById("sourcesView"); + }, + + get _children() { + delete this._children; + return this._children = this._tree.getElementsByTagName("treechildren")[0]; + }, + + + //**************************************************************************// + // Initialization & Destruction + + init: function() { + this._log = Log4Moz.Service.getLogger("Snowl.Sidebar"); + this._obsSvc.addObserver(this, "sources:changed", true); + this._getCollections(); + this._tree.view = this; + + // Add a capturing click listener to the tree so we can find out if the user + // clicked on a row that is already selected (in which case we let them edit + // the collection name). + // FIXME: disable this for names that can't be changed. + this._tree.addEventListener("mousedown", function(aEvent) { SourcesView.onClick(aEvent) }, true); + }, + + + //**************************************************************************// + // nsITreeView + + selection: null, + + get rowCount() { + return this._rows.length; + }, + + // FIXME: consolidate these two references. + _treebox: null, + setTree: function(treeBox) { + this._treeBox = treeBox; + }, + + getCellText : function(row, column) { + return this._rows[row].name; + }, + + isContainer: function(row) { + //this._log.info("isContainer: " + (this._rows[row].groups ? true : false)); + return (this._rows[row].groups ? true : false); + }, + isContainerOpen: function(row) { + //this._log.info("isContainerOpen: " + this._rows[row].isOpen); + return this._rows[row].isOpen; + }, + isContainerEmpty: function(row) { + //this._log.info("isContainerEmpty: " + row + " " + this._rows[row].groups.length + " " + (this._rows[row].groups.length == 0)); + return (this._rows[row].groups.length == 0); + }, + + isSeparator: function(row) { return false }, + isSorted: function() { return false }, + + // FIXME: make this return true for collection names that are editable, + // and then implement name editing on the new architecture. + isEditable: function(row, column) { return false }, + + getParentIndex: function(row) { + //this._log.info("getParentIndex: " + row); + + let thisLevel = this.getLevel(row); + + if (this._rows[row].level == 0) + return -1; + for (let t = row - 1; t >= 0; t--) + if (this.getLevel(t) < thisLevel) + return t; + + throw "getParentIndex: couldn't figure out parent index for row " + row; + }, + + getLevel: function(row) { + //this._log.info("getLevel: " + row); + + return this._rows[row].level; + }, + + hasNextSibling: function(idx, after) { + //this._log.info("hasNextSibling: " + idx + " " + after); + + let thisLevel = this.getLevel(idx); + for (let t = idx + 1; t < this._rows.length; t++) { + let nextLevel = this.getLevel(t); + if (nextLevel == thisLevel) + return true; + if (nextLevel < thisLevel) + return false; + } + + return false; + }, + + getImageSrc: function(row, column) { + if (column.id == "nameCol") { + let faviconURI = this._rows[row].faviconURI; + if (faviconURI) + return faviconURI.spec; + } + + return null; + }, + + toggleOpenState: function(idx) { + //this._log.info("toggleOpenState: " + idx); + + let item = this._rows[idx]; + if (!item.groups) + return; + + if (item.isOpen) { + item.isOpen = false; + + let thisLevel = this.getLevel(idx); + let numToDelete = 0; + for (let t = idx + 1; t < this._rows.length; t++) { + if (this.getLevel(t) > thisLevel) + numToDelete++; + else + break; + } + if (numToDelete) { + this._rows.splice(idx + 1, numToDelete); + this._treeBox.rowCountChanged(idx + 1, -numToDelete); + } + } + else { + item.isOpen = true; + + let groups = this._rows[idx].groups; + for (let i = 0; i < groups.length; i++) + this._rows.splice(idx + 1 + i, 0, groups[i]); + this._treeBox.rowCountChanged(idx + 1, groups.length); + } + }, + + getRowProperties: function (aRow, aProperties) {}, + getCellProperties: function (aRow, aColumn, aProperties) {}, + getColumnProperties: function(aColumnID, aColumn, aProperties) {}, + + setCellText: function(aRow, aCol, aValue) { + let statement = SnowlDatastore.createStatement("UPDATE sources SET name = :name WHERE id = :id"); + statement.params.name = this._rows[aRow].name = aValue; + statement.params.id = this._rows[aRow].id; + + try { + statement.execute(); + } + finally { + statement.reset(); + } + }, + + + //**************************************************************************// + // Misc XPCOM Interface Implementations + + // nsISupports + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIObserver) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_ERROR_NO_INTERFACE; + }, + + // nsIObserver + observe: function(subject, topic, data) { + switch (topic) { + case "sources:changed": + this._getCollections(); + // Rebuild the view to reflect the new collection of messages. + // Since the number of rows might have changed, we do this by reinitializing + // the view instead of merely invalidating the box object (which doesn't + // expect changes to the number of rows). + this._tree.view = this; + break; + } + }, + + _collections: null, + _getCollections: function() { + this._collections = []; + + let all = new SnowlCollection(); + all.name = "All"; + all.defaultFaviconURI = URI.get("chrome://snowl/content/icons/rainbow.png"); + this._collections.push(all); + + let grouping = { + nameColumn: "sources.name", + uriColumn: "sources.humanURI", + // the default favicon for sources + // FIXME: use a source type-specific favicon. + defaultFaviconURI: URI.get("chrome://browser/skin/feeds/feedIcon16.png") + } + let collection = new SnowlCollection(null, null, grouping); + collection.name = "Sources"; + this._collections.push(collection); + + { + let grouping = { + nameColumn: "authors.name" + // FIXME: get a favicon for people + } + let collection = new SnowlCollection(null, null, grouping); + collection.name = "People"; + this._collections.push(collection); + } + + // Build the list of rows in the tree. By default, all containers + // are closed, so this is the same as the list of collections, although + // in the future we might persist and restore the open state. + // XXX Should this work be in a separate function? + this._rows = [collection for each (collection in this._collections)]; + }, + + onSelect: function(aEvent) { + if (this._tree.currentIndex == -1) + return; + + let collection = this._rows[this._tree.currentIndex]; + gBrowserWindow.SnowlView.setCollection(collection); + }, + + onClick: function(aEvent) { +this._log.info("on click"); +//this._log.info(Log4Moz.enumerateProperties(aEvent).join("\n")); +//this._log.info(aEvent.target.nodeName); + + let row = {}, col = {}, child = {}; + this._tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY, row, col, child); + if (this._tree.view.selection.isSelected(row.value)) +this._log.info(row.value + " is selected"); +else +this._log.info(row.value + " is not selected"); + }, + + subscribe: function(event) { + gBrowserWindow.gBrowser.selectedTab = + gBrowserWindow.gBrowser.addTab("chrome://snowl/content/subscribe.xul"); + }, + + unsubscribe: function(aEvent) { + let sourceID = this._collections[this._tree.currentIndex].id; + + SnowlDatastore.dbConnection.beginTransaction(); + try { + SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")"); + SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")"); + SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages WHERE sourceID = " + sourceID); + SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM sources WHERE id = " + sourceID); + SnowlDatastore.dbConnection.commitTransaction(); + } + catch(ex) { + SnowlDatastore.dbConnection.rollbackTransaction(); + throw ex; + } + + this._obsSvc.notifyObservers(null, "sources:changed", null); + this._obsSvc.notifyObservers(null, "messages:changed", null); + }, + + + //**************************************************************************// + // OPML Export + // Based on code in Thunderbird's feed-subscriptions.js + + exportOPML: function() { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, "Export feeds as an OPML file", Ci.nsIFilePicker.modeSave); + fp.appendFilter("OPML Files", "*.opml"); + fp.appendFilters(Ci.nsIFilePicker.filterXML | Ci.nsIFilePicker.filterAll); + fp.defaultString = "feeds.opml"; + fp.defaultExtension = "opml"; + + let rv = fp.show(); + + if (rv == Ci.nsIFilePicker.returnCancel) + return; + + let doc = this._createOPMLDocument(); + + // Format the document with newlines and indentation so it's easier + // for humans to read. + this._prettifyNode(doc.documentElement, 0); + + let serializer = new XMLSerializer(); + let foStream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + // default mode: write | create | truncate + let mode = 0x02 | 0x08 | 0x20; + foStream.init(fp.file, mode, 0666, 0); + serializer.serializeToStream(doc, foStream, "utf-8"); + }, + + _createOPMLDocument: function() { + let doc = document.implementation.createDocument("", "opml", null); + let root = doc.documentElement; + root.setAttribute("version", "1.0"); + + // Create the <head> element. + let head = doc.createElement("head"); + root.appendChild(head); + + let title = doc.createElement("title"); + head.appendChild(title); + title.appendChild(doc.createTextNode("Snowl OPML Export")); + + let dt = doc.createElement("dateCreated"); + head.appendChild(dt); + dt.appendChild(doc.createTextNode((new Date()).toGMTString())); + + // Create the <body> element. + let body = doc.createElement("body"); + root.appendChild(body); + + // Populate the <body> element with <outline> elements. + let sources = SnowlSource.getAll(); + for each (let source in sources) { + let outline = doc.createElement("outline"); + // XXX Should we specify the |type| attribute, and should we specify + // type="atom" for Atom feeds or just type="rss" for all feeds? + // This document says the latter but is three years old: + // http://www.therssweblog.com/?guid=20051003145153 + //outline.setAttribute("type", "rss"); + outline.setAttribute("text", source.name); + outline.setAttribute("url", source.humanURI.spec); + outline.setAttribute("xmlUrl", source.machineURI.spec); + body.appendChild(outline); + } + + return doc; + }, + + _prettifyNode: function(node, level) { + let doc = node.ownerDocument; + + // Create a string containing two spaces for every level deep we are. + let indentString = new Array(level + 1).join(" "); + + // Indent the tag. + if (level > 0) + node.parentNode.insertBefore(doc.createTextNode(indentString), node); + + // Grab the list of nodes to format. We can't just use node.childNodes + // because it'd change under us as we insert formatting nodes. + let childNodesToFormat = []; + for (let i = 0; i < node.childNodes.length; i++) + if (node.childNodes[i].nodeType == node.ELEMENT_NODE) + childNodesToFormat.push(node.childNodes[i]); + + if (childNodesToFormat.length > 0) { + for each (let childNode in childNodesToFormat) + this._prettifyNode(childNode, level + 1); + + // Insert a newline after the opening tag. + node.insertBefore(doc.createTextNode("\n"), node.firstChild); + + // Indent the closing tag. + node.appendChild(doc.createTextNode(indentString)); + } + + // Insert a newline after the tag. + if (level > 0) { + if (node.nextSibling) + node.parentNode.insertBefore(doc.createTextNode("\n"), + node.nextSibling); + else + node.parentNode.appendChild(doc.createTextNode("\n")); + } + } + +}; + +window.addEventListener("load", function() { SourcesView.init() }, false);
--- a/extension/content/sidebar.css Sun Jul 20 19:13:25 2008 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,20 +0,0 @@ - -toolbarbutton { - -moz-appearance: none; -} - -.toolbarbutton-icon { - -moz-margin-end: 0; -} - -.statusBox > .statusIcon { - list-style-image: url("chrome://snowl/content/icons/asterisk_orange.png"); -} - -.statusBox[status="active"] > .statusIcon { - list-style-image: url("chrome://global/skin/icons/loading_16.png"); -} - -.statusBox[status="complete"] > .statusIcon { - list-style-image: url("chrome://snowl/content/icons/tick.png"); -}
--- a/extension/content/sidebar.js Sun Jul 20 19:13:25 2008 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,422 +0,0 @@ -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cr = Components.results; -const Cu = Components.utils; - -Cu.import("resource://snowl/modules/service.js"); -Cu.import("resource://snowl/modules/datastore.js"); -Cu.import("resource://snowl/modules/log4moz.js"); -Cu.import("resource://snowl/modules/source.js"); -Cu.import("resource://snowl/modules/feed.js"); -Cu.import("resource://snowl/modules/URI.js"); -Cu.import("resource://snowl/modules/identity.js"); -Cu.import("resource://snowl/modules/collection.js"); - -// FIXME: call this SnowlViewWindow to facilitate reuse of this sidebar code -// in the river view, where the window it will reference will not be a browser -// window. -var gBrowserWindow = window.QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIWebNavigation). - QueryInterface(Ci.nsIDocShellTreeItem). - rootTreeItem. - QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindow); - -SourcesView = { - _log: null, - - // Observer Service - get _obsSvc() { - let obsSvc = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); - delete this._obsSvc; - this._obsSvc = obsSvc; - return this._obsSvc; - }, - - get _tree() { - delete this._tree; - return this._tree = document.getElementById("sourcesView"); - }, - - get _children() { - delete this._children; - return this._children = this._tree.getElementsByTagName("treechildren")[0]; - }, - - - //**************************************************************************// - // Initialization & Destruction - - init: function() { - this._log = Log4Moz.Service.getLogger("Snowl.Sidebar"); - this._obsSvc.addObserver(this, "sources:changed", true); - this._getCollections(); - this._tree.view = this; - - // Add a capturing click listener to the tree so we can find out if the user - // clicked on a row that is already selected (in which case we let them edit - // the collection name). - // FIXME: disable this for names that can't be changed. - this._tree.addEventListener("mousedown", function(aEvent) { SourcesView.onClick(aEvent) }, true); - }, - - - //**************************************************************************// - // nsITreeView - - selection: null, - - get rowCount() { - return this._rows.length; - }, - - // FIXME: consolidate these two references. - _treebox: null, - setTree: function(treeBox) { - this._treeBox = treeBox; - }, - - getCellText : function(row, column) { - return this._rows[row].name; - }, - - isContainer: function(row) { - //this._log.info("isContainer: " + (this._rows[row].groups ? true : false)); - return (this._rows[row].groups ? true : false); - }, - isContainerOpen: function(row) { - //this._log.info("isContainerOpen: " + this._rows[row].isOpen); - return this._rows[row].isOpen; - }, - isContainerEmpty: function(row) { - //this._log.info("isContainerEmpty: " + row + " " + this._rows[row].groups.length + " " + (this._rows[row].groups.length == 0)); - return (this._rows[row].groups.length == 0); - }, - - isSeparator: function(row) { return false }, - isSorted: function() { return false }, - - // FIXME: make this return true for collection names that are editable, - // and then implement name editing on the new architecture. - isEditable: function(row, column) { return false }, - - getParentIndex: function(row) { - //this._log.info("getParentIndex: " + row); - - let thisLevel = this.getLevel(row); - - if (this._rows[row].level == 0) - return -1; - for (let t = row - 1; t >= 0; t--) - if (this.getLevel(t) < thisLevel) - return t; - - throw "getParentIndex: couldn't figure out parent index for row " + row; - }, - - getLevel: function(row) { - //this._log.info("getLevel: " + row); - - return this._rows[row].level; - }, - - hasNextSibling: function(idx, after) { - //this._log.info("hasNextSibling: " + idx + " " + after); - - let thisLevel = this.getLevel(idx); - for (let t = idx + 1; t < this._rows.length; t++) { - let nextLevel = this.getLevel(t); - if (nextLevel == thisLevel) - return true; - if (nextLevel < thisLevel) - return false; - } - - return false; - }, - - getImageSrc: function(row, column) { - if (column.id == "nameCol") { - let faviconURI = this._rows[row].faviconURI; - if (faviconURI) - return faviconURI.spec; - } - - return null; - }, - - toggleOpenState: function(idx) { - //this._log.info("toggleOpenState: " + idx); - - let item = this._rows[idx]; - if (!item.groups) - return; - - if (item.isOpen) { - item.isOpen = false; - - let thisLevel = this.getLevel(idx); - let numToDelete = 0; - for (let t = idx + 1; t < this._rows.length; t++) { - if (this.getLevel(t) > thisLevel) - numToDelete++; - else - break; - } - if (numToDelete) { - this._rows.splice(idx + 1, numToDelete); - this._treeBox.rowCountChanged(idx + 1, -numToDelete); - } - } - else { - item.isOpen = true; - - let groups = this._rows[idx].groups; - for (let i = 0; i < groups.length; i++) - this._rows.splice(idx + 1 + i, 0, groups[i]); - this._treeBox.rowCountChanged(idx + 1, groups.length); - } - }, - - getRowProperties: function (aRow, aProperties) {}, - getCellProperties: function (aRow, aColumn, aProperties) {}, - getColumnProperties: function(aColumnID, aColumn, aProperties) {}, - - setCellText: function(aRow, aCol, aValue) { - let statement = SnowlDatastore.createStatement("UPDATE sources SET name = :name WHERE id = :id"); - statement.params.name = this._rows[aRow].name = aValue; - statement.params.id = this._rows[aRow].id; - - try { - statement.execute(); - } - finally { - statement.reset(); - } - }, - - - //**************************************************************************// - // Misc XPCOM Interface Implementations - - // nsISupports - QueryInterface: function(aIID) { - if (aIID.equals(Ci.nsIObserver) || - aIID.equals(Ci.nsISupportsWeakReference) || - aIID.equals(Ci.nsISupports)) - return this; - - throw Cr.NS_ERROR_NO_INTERFACE; - }, - - // nsIObserver - observe: function(subject, topic, data) { - switch (topic) { - case "sources:changed": - this._getCollections(); - // Rebuild the view to reflect the new collection of messages. - // Since the number of rows might have changed, we do this by reinitializing - // the view instead of merely invalidating the box object (which doesn't - // expect changes to the number of rows). - this._tree.view = this; - break; - } - }, - - _collections: null, - _getCollections: function() { - this._collections = []; - - let all = new SnowlCollection(); - all.name = "All"; - all.defaultFaviconURI = URI.get("chrome://snowl/content/icons/rainbow.png"); - this._collections.push(all); - - let grouping = { - nameColumn: "sources.name", - uriColumn: "sources.humanURI", - // the default favicon for sources - // FIXME: use a source type-specific favicon. - defaultFaviconURI: URI.get("chrome://browser/skin/feeds/feedIcon16.png") - } - let collection = new SnowlCollection(null, null, grouping); - collection.name = "Sources"; - this._collections.push(collection); - - { - let grouping = { - nameColumn: "authors.name" - // FIXME: get a favicon for people - } - let collection = new SnowlCollection(null, null, grouping); - collection.name = "People"; - this._collections.push(collection); - } - - // Build the list of rows in the tree. By default, all containers - // are closed, so this is the same as the list of collections, although - // in the future we might persist and restore the open state. - // XXX Should this work be in a separate function? - this._rows = [collection for each (collection in this._collections)]; - }, - - onSelect: function(aEvent) { - if (this._tree.currentIndex == -1) - return; - - let collection = this._rows[this._tree.currentIndex]; - gBrowserWindow.SnowlView.setCollection(collection); - }, - - onClick: function(aEvent) { -this._log.info("on click"); -//this._log.info(Log4Moz.enumerateProperties(aEvent).join("\n")); -//this._log.info(aEvent.target.nodeName); - - let row = {}, col = {}, child = {}; - this._tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY, row, col, child); - if (this._tree.view.selection.isSelected(row.value)) -this._log.info(row.value + " is selected"); -else -this._log.info(row.value + " is not selected"); - }, - - subscribe: function(event) { - gBrowserWindow.gBrowser.selectedTab = - gBrowserWindow.gBrowser.addTab("chrome://snowl/content/subscribe.xul"); - }, - - unsubscribe: function(aEvent) { - let sourceID = this._collections[this._tree.currentIndex].id; - - SnowlDatastore.dbConnection.beginTransaction(); - try { - SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM metadata WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")"); - SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM parts WHERE messageID IN (SELECT id FROM messages WHERE sourceID = " + sourceID + ")"); - SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM messages WHERE sourceID = " + sourceID); - SnowlDatastore.dbConnection.executeSimpleSQL("DELETE FROM sources WHERE id = " + sourceID); - SnowlDatastore.dbConnection.commitTransaction(); - } - catch(ex) { - SnowlDatastore.dbConnection.rollbackTransaction(); - throw ex; - } - - this._obsSvc.notifyObservers(null, "sources:changed", null); - this._obsSvc.notifyObservers(null, "messages:changed", null); - }, - - - //**************************************************************************// - // OPML Export - // Based on code in Thunderbird's feed-subscriptions.js - - exportOPML: function() { - let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); - fp.init(window, "Export feeds as an OPML file", Ci.nsIFilePicker.modeSave); - fp.appendFilter("OPML Files", "*.opml"); - fp.appendFilters(Ci.nsIFilePicker.filterXML | Ci.nsIFilePicker.filterAll); - fp.defaultString = "feeds.opml"; - fp.defaultExtension = "opml"; - - let rv = fp.show(); - - if (rv == Ci.nsIFilePicker.returnCancel) - return; - - let doc = this._createOPMLDocument(); - - // Format the document with newlines and indentation so it's easier - // for humans to read. - this._prettifyNode(doc.documentElement, 0); - - let serializer = new XMLSerializer(); - let foStream = Cc["@mozilla.org/network/file-output-stream;1"]. - createInstance(Ci.nsIFileOutputStream); - // default mode: write | create | truncate - let mode = 0x02 | 0x08 | 0x20; - foStream.init(fp.file, mode, 0666, 0); - serializer.serializeToStream(doc, foStream, "utf-8"); - }, - - _createOPMLDocument: function() { - let doc = document.implementation.createDocument("", "opml", null); - let root = doc.documentElement; - root.setAttribute("version", "1.0"); - - // Create the <head> element. - let head = doc.createElement("head"); - root.appendChild(head); - - let title = doc.createElement("title"); - head.appendChild(title); - title.appendChild(doc.createTextNode("Snowl OPML Export")); - - let dt = doc.createElement("dateCreated"); - head.appendChild(dt); - dt.appendChild(doc.createTextNode((new Date()).toGMTString())); - - // Create the <body> element. - let body = doc.createElement("body"); - root.appendChild(body); - - // Populate the <body> element with <outline> elements. - let sources = SnowlSource.getAll(); - for each (let source in sources) { - let outline = doc.createElement("outline"); - // XXX Should we specify the |type| attribute, and should we specify - // type="atom" for Atom feeds or just type="rss" for all feeds? - // This document says the latter but is three years old: - // http://www.therssweblog.com/?guid=20051003145153 - //outline.setAttribute("type", "rss"); - outline.setAttribute("text", source.name); - outline.setAttribute("url", source.humanURI.spec); - outline.setAttribute("xmlUrl", source.machineURI.spec); - body.appendChild(outline); - } - - return doc; - }, - - _prettifyNode: function(node, level) { - let doc = node.ownerDocument; - - // Create a string containing two spaces for every level deep we are. - let indentString = new Array(level + 1).join(" "); - - // Indent the tag. - if (level > 0) - node.parentNode.insertBefore(doc.createTextNode(indentString), node); - - // Grab the list of nodes to format. We can't just use node.childNodes - // because it'd change under us as we insert formatting nodes. - let childNodesToFormat = []; - for (let i = 0; i < node.childNodes.length; i++) - if (node.childNodes[i].nodeType == node.ELEMENT_NODE) - childNodesToFormat.push(node.childNodes[i]); - - if (childNodesToFormat.length > 0) { - for each (let childNode in childNodesToFormat) - this._prettifyNode(childNode, level + 1); - - // Insert a newline after the opening tag. - node.insertBefore(doc.createTextNode("\n"), node.firstChild); - - // Indent the closing tag. - node.appendChild(doc.createTextNode(indentString)); - } - - // Insert a newline after the tag. - if (level > 0) { - if (node.nextSibling) - node.parentNode.insertBefore(doc.createTextNode("\n"), - node.nextSibling); - else - node.parentNode.appendChild(doc.createTextNode("\n")); - } - } - -}; - -window.addEventListener("load", function() { SourcesView.init() }, false);
--- a/extension/content/sidebar.xul Sun Jul 20 19:13:25 2008 -0700 +++ b/extension/content/sidebar.xul Sun Jul 20 20:21:27 2008 -0700 @@ -1,4 +1,5 @@ <?xml version="1.0"?> + <!-- ***** BEGIN LICENSE BLOCK ***** - Version: MPL 1.1/GPL 2.0/LGPL 2.1 - @@ -36,44 +37,11 @@ - ***** END LICENSE BLOCK ***** --> <?xml-stylesheet href="chrome://global/skin/" type"text/css"?> -<?xml-stylesheet href="chrome://snowl/content/sidebar.css" type"text/css"?> - -<page id="snowlSidebar" title="Subscriptions" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - - <script type="application/x-javascript" src="chrome://snowl/content/sidebar.js"/> - <toolbar> - <!-- FIXME: Note in credits that silk icons licensed - - from http://www.famfamfam.com/lab/icons/silk/ - - under http://creativecommons.org/licenses/by/2.5/ --> - <toolbarbutton id="snowlSubscribeButton" - image="chrome://snowl/content/icons/add.png" - oncommand="SourcesView.subscribe()"/> - <toolbarbutton id="snowlUnsubscribeButton" - image="chrome://snowl/content/icons/delete.png" - oncommand="SourcesView.unsubscribe()"/> - <toolbarbutton id="snowlRefreshButton" - image="chrome://snowl/content/icons/arrow_refresh_small.png" - oncommand="SnowlService.refreshAllSources()"/> - <!-- FIXME: Note in credits that OPML icon licensed from opmlicons.com - - under http://creativecommons.org/licenses/by-sa/2.5/ --> - <toolbarbutton id="snowlExportOPMLButton" - image="chrome://snowl/content/icons/opml-icon-16x16.png" - oncommand="SourcesView.exportOPML()"/> - </toolbar> +<page id="snowlSidebar" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Snowl Sidebar"> - <tree id="sourcesView" flex="1" context="sourcesContextMenu" editable="true" - onselect="SourcesView.onSelect(event)"> - <treecols> - <treecol id="nameCol" label="Name" primary="true" flex="1"/> - </treecols> - - <treechildren flex="1"/> - </tree> - - <menupopup id="sourcesContextMenu"> - <menuitem label="Unsubscribe" oncommand="SourcesView.unsubscribe(event)"/> - </menupopup> + <vbox id="collectionsViewBox" flex="1"/> </page>