mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-28 19:37:32 +09:00
2703 lines
94 KiB
JavaScript
2703 lines
94 KiB
JavaScript
/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
Components.utils.import("resource:///modules/FeedUtils.jsm");
|
|
Components.utils.import("resource:///modules/gloda/log4moz.js");
|
|
Components.utils.import("resource:///modules/mailServices.js");
|
|
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
|
Components.utils.import("resource://gre/modules/PluralForm.jsm");
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
|
|
var {classes: Cc, interfaces: Ci} = Components;
|
|
|
|
var FeedSubscriptions = {
|
|
get mMainWin() { return Services.wm.getMostRecentWindow("mail:3pane"); },
|
|
|
|
get mTree() { return document.getElementById("rssSubscriptionsList"); },
|
|
|
|
mFeedContainers: [],
|
|
mRSSServer : null,
|
|
mActionMode : null,
|
|
kSubscribeMode : 1,
|
|
kUpdateMode : 2,
|
|
kMoveMode : 3,
|
|
kCopyMode : 4,
|
|
kImportingOPML : 5,
|
|
kVerifyUrlMode : 6,
|
|
|
|
get FOLDER_ACTIONS()
|
|
{
|
|
return Ci.nsIMsgFolderNotificationService.folderAdded |
|
|
Ci.nsIMsgFolderNotificationService.folderDeleted |
|
|
Ci.nsIMsgFolderNotificationService.folderRenamed |
|
|
Ci.nsIMsgFolderNotificationService.folderMoveCopyCompleted;
|
|
},
|
|
|
|
onLoad: function ()
|
|
{
|
|
// Extract the folder argument.
|
|
let folder;
|
|
if (window.arguments && window.arguments[0].folder)
|
|
folder = window.arguments[0].folder;
|
|
|
|
// Ensure dialog is fully loaded before selecting, to get visible row.
|
|
setTimeout(function() {
|
|
FeedSubscriptions.refreshSubscriptionView(folder)
|
|
}, 100);
|
|
let message = FeedUtils.strings.GetStringFromName("subscribe-loading");
|
|
this.updateStatusItem("statusText", message);
|
|
|
|
FeedUtils.CANCEL_REQUESTED = false;
|
|
|
|
if (this.mMainWin)
|
|
{
|
|
this.mMainWin.FeedFolderNotificationService = MailServices.mfn;
|
|
this.mMainWin.FeedFolderNotificationService
|
|
.addListener(this.FolderListener, this.FOLDER_ACTIONS);
|
|
}
|
|
},
|
|
|
|
onClose: function ()
|
|
{
|
|
let dismissDialog = true;
|
|
|
|
// If we are in the middle of subscribing to a feed, inform the user that
|
|
// dismissing the dialog right now will abort the feed subscription.
|
|
if (this.mActionMode == this.kSubscribeMode)
|
|
{
|
|
let pTitle = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-cancelSubscriptionTitle");
|
|
let pMessage = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-cancelSubscription");
|
|
dismissDialog =
|
|
!(Services.prompt.confirmEx(window, pTitle, pMessage,
|
|
Ci.nsIPromptService.STD_YES_NO_BUTTONS,
|
|
null, null, null, null, { }));
|
|
}
|
|
|
|
if (dismissDialog)
|
|
{
|
|
FeedUtils.CANCEL_REQUESTED = this.mActionMode == this.kSubscribeMode;
|
|
if (this.mMainWin)
|
|
{
|
|
this.mMainWin.FeedFolderNotificationService
|
|
.removeListener(this.FolderListener, this.FOLDER_ACTIONS);
|
|
delete this.mMainWin.FeedFolderNotificationService;
|
|
}
|
|
}
|
|
|
|
return dismissDialog;
|
|
},
|
|
|
|
refreshSubscriptionView: function(aSelectFolder, aSelectFeedUrl)
|
|
{
|
|
let item = this.mView.currentItem;
|
|
this.loadSubscriptions();
|
|
this.mTree.view = this.mView;
|
|
|
|
if (aSelectFolder && !aSelectFeedUrl)
|
|
this.selectFolder(aSelectFolder);
|
|
else
|
|
{
|
|
// If no folder to select, try to select the pre rebuild selection, in
|
|
// an existing window. For folderpane changes in a feed account.
|
|
if (item)
|
|
{
|
|
let rootFolder = item.container ? item.folder.rootFolder :
|
|
item.parentFolder.rootFolder;
|
|
if (item.container)
|
|
{
|
|
if (!this.selectFolder(item.folder, { open: item.open }))
|
|
// The item no longer exists, an ancestor folder was deleted or
|
|
// renamed/moved.
|
|
this.selectFolder(rootFolder);
|
|
}
|
|
else {
|
|
let url = item.parentFolder == aSelectFolder ? aSelectFeedUrl :
|
|
item.url;
|
|
this.selectFeed({ folder: rootFolder, url: url }, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.mView.treeBox.ensureRowIsVisible(this.mView.selection.currentIndex);
|
|
this.clearStatusInfo();
|
|
},
|
|
|
|
mView:
|
|
{
|
|
kRowIndexUndefined: -1,
|
|
|
|
get currentItem() {
|
|
// Get the current selection, if any.
|
|
let seln = this.selection;
|
|
let currentSelectionIndex = seln ? seln.currentIndex : null;
|
|
let item;
|
|
if (currentSelectionIndex != null)
|
|
item = this.getItemAtIndex(currentSelectionIndex);
|
|
|
|
return item;
|
|
},
|
|
|
|
/* nsITreeView */
|
|
treeBox: null,
|
|
|
|
mRowCount: 0,
|
|
get rowCount() { return this.mRowCount; },
|
|
|
|
_selection: null,
|
|
get selection () { return this._selection; },
|
|
set selection (val) { return this._selection = val; },
|
|
|
|
setTree: function(aTreebox) { this.treeBox = aTreebox; },
|
|
isSeparator: function(aRow) { return false; },
|
|
isSorted: function() { return false; },
|
|
isSelectable: function(aRow, aColumn) { return false; },
|
|
isEditable: function (aRow, aColumn) { return false; },
|
|
|
|
getProgressMode : function(aRow, aCol) {},
|
|
cycleHeader: function(aCol) {},
|
|
cycleCell: function(aRow, aCol) {},
|
|
selectionChanged: function() {},
|
|
performAction: function(aAction) {},
|
|
performActionOnRow: function (aAction, aRow) {},
|
|
performActionOnCell: function(aAction, aRow, aCol) {},
|
|
getRowProperties: function(aRow) { return ""; },
|
|
getColumnProperties: function(aCol) { return ""; },
|
|
getCellValue: function (aRow, aColumn) {},
|
|
setCellValue: function (aRow, aColumn, aValue) {},
|
|
setCellText: function (aRow, aColumn, aValue) {},
|
|
|
|
getCellProperties: function (aRow, aColumn) {
|
|
let item = this.getItemAtIndex(aRow);
|
|
let folder = item && item.folder ? item.folder : null;
|
|
#ifdef MOZ_THUNDERBIRD
|
|
let properties = ["folderNameCol"];
|
|
let hasFeeds = folder ? FeedUtils.getFeedUrlsInFolder(folder) : false;
|
|
let prop = !folder ? "isFeed-true" :
|
|
hasFeeds ? "isFeedFolder-true" :
|
|
folder.isServer ? "serverType-rss isServer-true" : null;
|
|
if (prop)
|
|
properties.push(prop);
|
|
return properties.join(" ");
|
|
#else
|
|
return !folder ? "serverType-rss" :
|
|
folder.isServer ? "serverType-rss isServer-true" : "livemark";
|
|
#endif
|
|
},
|
|
|
|
isContainer: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
return item ? item.container : false;
|
|
},
|
|
|
|
isContainerOpen: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
return item ? item.open : false;
|
|
},
|
|
|
|
isContainerEmpty: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
if (!item)
|
|
return false;
|
|
|
|
return item.children.length == 0;
|
|
},
|
|
|
|
getItemAtIndex: function (aRow)
|
|
{
|
|
if (aRow < 0 || aRow >= FeedSubscriptions.mFeedContainers.length)
|
|
return null;
|
|
|
|
return FeedSubscriptions.mFeedContainers[aRow];
|
|
},
|
|
|
|
getItemInViewIndex: function(aFolder)
|
|
{
|
|
if (!aFolder || !(aFolder instanceof Ci.nsIMsgFolder))
|
|
return null;
|
|
|
|
for (let index = 0; index < this.rowCount; index++)
|
|
{
|
|
// Find the visible folder in the view.
|
|
let item = this.getItemAtIndex(index);
|
|
if (item && item.container && item.url == aFolder.URI)
|
|
return index;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
removeItemAtIndex: function (aRow, aNoSelect)
|
|
{
|
|
let itemToRemove = this.getItemAtIndex(aRow);
|
|
if (!itemToRemove)
|
|
return;
|
|
|
|
if (itemToRemove.container && itemToRemove.open)
|
|
// Close it, if open container.
|
|
this.toggleOpenState(aRow);
|
|
|
|
let parentIndex = this.getParentIndex(aRow);
|
|
let hasNextSibling = this.hasNextSibling(aRow, aRow);
|
|
if (parentIndex != this.kRowIndexUndefined)
|
|
{
|
|
let parent = this.getItemAtIndex(parentIndex);
|
|
if (parent)
|
|
{
|
|
for (let index = 0; index < parent.children.length; index++)
|
|
if (parent.children[index] == itemToRemove)
|
|
{
|
|
parent.children.splice(index, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now remove it from our view.
|
|
FeedSubscriptions.mFeedContainers.splice(aRow, 1);
|
|
|
|
// Now invalidate the correct tree rows.
|
|
this.mRowCount--;
|
|
this.treeBox.rowCountChanged(aRow, -1);
|
|
|
|
// Now update the selection position, unless noSelect (selection is
|
|
// done later or not at all). If the item is the last child, select the
|
|
// parent. Otherwise select the next sibling.
|
|
if (!aNoSelect) {
|
|
if (aRow <= FeedSubscriptions.mFeedContainers.length)
|
|
this.selection.select(hasNextSibling ? aRow : aRow - 1);
|
|
else
|
|
this.selection.clearSelection();
|
|
}
|
|
|
|
// Now refocus the tree.
|
|
FeedSubscriptions.mTree.focus();
|
|
},
|
|
|
|
getCellText: function (aRow, aColumn)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
return (item && aColumn.id == "folderNameCol") ? item.name : "";
|
|
},
|
|
|
|
getImageSrc: function(aRow, aCol)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
if ((item.folder && item.folder.isServer) || item.open)
|
|
return "";
|
|
|
|
if (item.favicon != null)
|
|
return item.favicon;
|
|
|
|
if (item.folder && FeedSubscriptions.mMainWin &&
|
|
"gFolderTreeView" in FeedSubscriptions.mMainWin) {
|
|
let favicon = FeedSubscriptions.mMainWin.gFolderTreeView
|
|
.getFolderCacheProperty(item.folder, "favicon");
|
|
if (favicon != null)
|
|
return item.favicon = favicon;
|
|
}
|
|
|
|
let callback = (iconUrl => {
|
|
item.favicon = iconUrl;
|
|
if (item.folder)
|
|
{
|
|
for (let child of item.children)
|
|
if (!child.container)
|
|
{
|
|
child.favicon = iconUrl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.selection.tree.invalidateRow(aRow);
|
|
});
|
|
|
|
// A closed non server folder.
|
|
if (item.folder)
|
|
{
|
|
for (let child of item.children)
|
|
{
|
|
if (!child.container) {
|
|
if (child.favicon != null)
|
|
return child.favicon;
|
|
|
|
setTimeout(() => {
|
|
FeedUtils.getFavicon(child.parentFolder, child.url, null,
|
|
window, callback);
|
|
}, 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// A feed.
|
|
setTimeout(() => {
|
|
FeedUtils.getFavicon(item.parentFolder, item.url, null,
|
|
window, callback);
|
|
}, 0);
|
|
}
|
|
|
|
// Store empty string to return default while favicons are retrieved.
|
|
return item.favicon = "";
|
|
},
|
|
|
|
canDrop: function (aRow, aOrientation)
|
|
{
|
|
let dropResult = this.extractDragData(aRow);
|
|
return aOrientation == Ci.nsITreeView.DROP_ON && dropResult.canDrop &&
|
|
(dropResult.dropUrl || dropResult.dropOnIndex != this.kRowIndexUndefined);
|
|
},
|
|
|
|
drop: function (aRow, aOrientation)
|
|
{
|
|
let win = FeedSubscriptions;
|
|
let results = this.extractDragData(aRow);
|
|
if (!results.canDrop)
|
|
return;
|
|
|
|
// Preselect the drop folder.
|
|
this.selection.select(aRow);
|
|
|
|
if (results.dropUrl)
|
|
{
|
|
// Don't freeze the app that initiated the drop just because we are
|
|
// in a loop waiting for the user to dimisss the add feed dialog.
|
|
setTimeout(function() {
|
|
win.addFeed(results.dropUrl, null, true, null, win.kSubscribeMode);
|
|
}, 0);
|
|
let folderItem = this.getItemAtIndex(aRow);
|
|
FeedUtils.log.debug("drop: folder, url - " +
|
|
folderItem.folder.name + ", " + results.dropUrl);
|
|
}
|
|
else if (results.dropOnIndex != this.kRowIndexUndefined)
|
|
{
|
|
win.moveCopyFeed(results.dropOnIndex, aRow, results.dropEffect);
|
|
}
|
|
},
|
|
|
|
// Helper function for drag and drop.
|
|
extractDragData: function(aRow)
|
|
{
|
|
let dt = this._currentDataTransfer;
|
|
let dragDataResults = { canDrop: false,
|
|
dropUrl: null,
|
|
dropOnIndex: this.kRowIndexUndefined,
|
|
dropEffect: dt.dropEffect };
|
|
|
|
if (dt.getData("text/x-moz-feed-index"))
|
|
{
|
|
// Dragging a feed in the tree.
|
|
if (this.selection)
|
|
{
|
|
dragDataResults.dropOnIndex = this.selection.currentIndex;
|
|
|
|
let curItem = this.getItemAtIndex(this.selection.currentIndex);
|
|
let newItem = this.getItemAtIndex(aRow);
|
|
let curServer = curItem && curItem.parentFolder ?
|
|
curItem.parentFolder.server : null;
|
|
let newServer = newItem && newItem.folder ?
|
|
newItem.folder.server : null;
|
|
|
|
// No copying within the same account and no moving to the account
|
|
// folder in the same account.
|
|
if (!(curServer == newServer &&
|
|
(dragDataResults.dropEffect == "copy" ||
|
|
newItem.folder == curItem.parentFolder ||
|
|
newItem.folder.isServer)))
|
|
dragDataResults.canDrop = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Try to get a feed url.
|
|
let validUri = FeedUtils.getFeedUriFromDataTransfer(dt);
|
|
|
|
if (validUri)
|
|
{
|
|
dragDataResults.canDrop = true;
|
|
dragDataResults.dropUrl = validUri.spec;
|
|
}
|
|
}
|
|
|
|
return dragDataResults;
|
|
},
|
|
|
|
getParentIndex: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
|
|
if (item)
|
|
{
|
|
for (let index = aRow; index >= 0; index--)
|
|
if (FeedSubscriptions.mFeedContainers[index].level < item.level)
|
|
return index;
|
|
}
|
|
|
|
return this.kRowIndexUndefined;
|
|
},
|
|
|
|
isIndexChildOfParentIndex: function (aRow, aChildRow)
|
|
{
|
|
// For visible tree rows, not if items are children of closed folders.
|
|
let item = this.getItemAtIndex(aRow);
|
|
if (!item || aChildRow <= aRow)
|
|
return false;
|
|
|
|
let targetLevel = this.getItemAtIndex(aRow).level;
|
|
let rows = FeedSubscriptions.mFeedContainers;
|
|
|
|
for (let i = aRow + 1; i < rows.length; i++) {
|
|
if (this.getItemAtIndex(i).level <= targetLevel)
|
|
break;
|
|
if (aChildRow == i)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
hasNextSibling: function(aRow, aAfterIndex) {
|
|
let targetLevel = this.getItemAtIndex(aRow).level;
|
|
let rows = FeedSubscriptions.mFeedContainers;
|
|
for (let i = aAfterIndex + 1; i < rows.length; i++) {
|
|
if (this.getItemAtIndex(i).level == targetLevel)
|
|
return true;
|
|
if (this.getItemAtIndex(i).level < targetLevel)
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
hasPreviousSibling: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
if (item && aRow)
|
|
return this.getItemAtIndex(aRow - 1).level == item.level;
|
|
else
|
|
return false;
|
|
},
|
|
|
|
getLevel: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
if (!item)
|
|
return 0;
|
|
|
|
return item.level;
|
|
},
|
|
|
|
toggleOpenState: function (aRow)
|
|
{
|
|
let item = this.getItemAtIndex(aRow);
|
|
if (!item)
|
|
return;
|
|
|
|
// Save off the current selection item.
|
|
let seln = this.selection;
|
|
let currentSelectionIndex = seln.currentIndex;
|
|
|
|
let rowsChanged = this.toggle(aRow)
|
|
|
|
// Now restore selection, ensuring selection is maintained on toggles.
|
|
if (currentSelectionIndex > aRow)
|
|
seln.currentIndex = currentSelectionIndex + rowsChanged;
|
|
else
|
|
seln.select(currentSelectionIndex);
|
|
|
|
seln.selectEventsSuppressed = false;
|
|
},
|
|
|
|
toggle: function (aRow)
|
|
{
|
|
// Collapse the row, or build sub rows based on open states in the map.
|
|
let item = this.getItemAtIndex(aRow);
|
|
if (!item)
|
|
return null;
|
|
|
|
let rows = FeedSubscriptions.mFeedContainers;
|
|
let rowCount = 0;
|
|
let multiplier;
|
|
|
|
function addDescendants(aItem)
|
|
{
|
|
for (let i = 0; i < aItem.children.length; i++)
|
|
{
|
|
rowCount++;
|
|
let child = aItem.children[i];
|
|
rows.splice(aRow + rowCount, 0, child);
|
|
if (child.open)
|
|
addDescendants(child);
|
|
}
|
|
}
|
|
|
|
if (item.open)
|
|
{
|
|
// Close the container. Add up all subfolders and their descendants
|
|
// who may be open.
|
|
multiplier = -1;
|
|
let nextRow = aRow + 1;
|
|
let nextItem = rows[nextRow];
|
|
while (nextItem && nextItem.level > item.level)
|
|
{
|
|
rowCount++;
|
|
nextItem = rows[++nextRow];
|
|
}
|
|
|
|
rows.splice(aRow + 1, rowCount);
|
|
}
|
|
else
|
|
{
|
|
// Open the container. Restore the open state of all subfolder and
|
|
// their descendants.
|
|
multiplier = 1;
|
|
addDescendants(item);
|
|
}
|
|
|
|
let delta = multiplier * rowCount;
|
|
this.mRowCount += delta;
|
|
|
|
item.open = !item.open;
|
|
// Suppress the select event caused by rowCountChanged.
|
|
this.selection.selectEventsSuppressed = true;
|
|
// Add or remove the children from our view.
|
|
this.treeBox.rowCountChanged(aRow, delta);
|
|
return delta;
|
|
}
|
|
},
|
|
|
|
makeFolderObject: function (aFolder, aCurrentLevel)
|
|
{
|
|
let defaultQuickMode = aFolder.server.getBoolValue("quickMode");
|
|
let optionsAcct = aFolder.isServer ? FeedUtils.getOptionsAcct(aFolder.server) :
|
|
null;
|
|
let open = !aFolder.isServer &&
|
|
aFolder.server == this.mRSSServer &&
|
|
this.mActionMode == this.kImportingOPML ? true : false
|
|
let folderObject = { children : [],
|
|
folder : aFolder,
|
|
name : aFolder.prettyName,
|
|
level : aCurrentLevel,
|
|
url : aFolder.URI,
|
|
quickMode: defaultQuickMode,
|
|
options : optionsAcct,
|
|
open : open,
|
|
container: true,
|
|
favicon : null };
|
|
|
|
// If a feed has any sub folders, add them to the list of children.
|
|
let folderEnumerator = aFolder.subFolders;
|
|
|
|
while (folderEnumerator.hasMoreElements())
|
|
{
|
|
let folder = folderEnumerator.getNext();
|
|
if ((folder instanceof Ci.nsIMsgFolder) &&
|
|
!folder.getFlag(Ci.nsMsgFolderFlags.Trash) &&
|
|
!folder.getFlag(Ci.nsMsgFolderFlags.Virtual))
|
|
{
|
|
folderObject.children
|
|
.push(this.makeFolderObject(folder, aCurrentLevel + 1));
|
|
}
|
|
}
|
|
|
|
let feeds = this.getFeedsInFolder(aFolder);
|
|
for (let feed of feeds)
|
|
{
|
|
// Now add any feed urls for the folder.
|
|
folderObject.children.push(this.makeFeedObject(feed,
|
|
aFolder,
|
|
aCurrentLevel + 1));
|
|
}
|
|
|
|
// Finally, set the folder's quickMode based on the its first feed's
|
|
// quickMode, since that is how the view determines summary mode, and now
|
|
// quickMode is updated to be the same for all feeds in a folder.
|
|
if (feeds && feeds[0])
|
|
folderObject.quickMode = feeds[0].quickMode;
|
|
|
|
folderObject.children = this.folderItemSorter(folderObject.children);
|
|
|
|
return folderObject;
|
|
},
|
|
|
|
folderItemSorter: function (aArray)
|
|
{
|
|
return aArray.sort(function(a, b) { return a.name.toLowerCase() >
|
|
b.name.toLowerCase() }).
|
|
sort(function(a, b) { return a.container < b.container });
|
|
},
|
|
|
|
getFeedsInFolder: function (aFolder)
|
|
{
|
|
let feeds = new Array();
|
|
let feedUrlArray = FeedUtils.getFeedUrlsInFolder(aFolder);
|
|
if (!feedUrlArray)
|
|
// No feedUrls in this folder.
|
|
return feeds;
|
|
|
|
for (let url of feedUrlArray)
|
|
{
|
|
let feedResource = FeedUtils.rdf.GetResource(url);
|
|
let feed = new Feed(feedResource, aFolder.server);
|
|
feeds.push(feed);
|
|
}
|
|
|
|
return feeds;
|
|
},
|
|
|
|
makeFeedObject: function (aFeed, aFolder, aLevel)
|
|
{
|
|
// Look inside the data source for the feed properties.
|
|
let feed = { children : [],
|
|
parentFolder: aFolder,
|
|
name : aFeed.title || aFeed.description || aFeed.url,
|
|
url : aFeed.url,
|
|
quickMode : aFeed.quickMode,
|
|
options : aFeed.options || FeedUtils.optionsTemplate,
|
|
level : aLevel,
|
|
open : false,
|
|
container : false,
|
|
favicon : null };
|
|
return feed;
|
|
},
|
|
|
|
loadSubscriptions: function ()
|
|
{
|
|
// Put together an array of folders. Each feed account level folder is
|
|
// included as the root.
|
|
let numFolders = 0;
|
|
let feedContainers = [];
|
|
// Get all the feed account folders.
|
|
let feedRootFolders = FeedUtils.getAllRssServerRootFolders();
|
|
|
|
feedRootFolders.forEach(function(rootFolder) {
|
|
feedContainers.push(this.makeFolderObject(rootFolder, 0));
|
|
numFolders++;
|
|
}, this);
|
|
|
|
this.mFeedContainers = feedContainers;
|
|
this.mView.mRowCount = numFolders;
|
|
|
|
FeedSubscriptions.mTree.focus();
|
|
},
|
|
|
|
/**
|
|
* Find the folder in the tree. The search may be limited to subfolders of
|
|
* a known folder, or expanded to include the entire tree. This function is
|
|
* also used to insert/remove folders without rebuilding the tree view cache
|
|
* (to avoid position/toggle state loss).
|
|
*
|
|
* @param aFolder nsIMsgFolder - the folder to find.
|
|
* @param [aParams] object - params object, containing:
|
|
*
|
|
* [parentIndex] int - index of folder to start the search; if
|
|
* null (default), the index of the folder's
|
|
* rootFolder will be used.
|
|
* [select] boolean - if true (default) the folder's ancestors
|
|
* will be opened and the folder selected.
|
|
* [open] boolean - if true (default) the folder is opened.
|
|
* [remove] boolean - delete the item from tree row cache if true,
|
|
* false (default) otherwise.
|
|
* [newFolder] nsIMsgFolder - if not null (default) the new folder,
|
|
* for add or rename.
|
|
*
|
|
* @return bool found - true if found, false if not.
|
|
*/
|
|
selectFolder: function(aFolder, aParms)
|
|
{
|
|
let folderURI = aFolder.URI;
|
|
let parentIndex = aParms && ("parentIndex" in aParms) ? aParms.parentIndex : null;
|
|
let selectIt = aParms && ("select" in aParms) ? aParms.select : true;
|
|
let openIt = aParms && ("open" in aParms) ? aParms.open : true;
|
|
let removeIt = aParms && ("remove" in aParms) ? aParms.remove : false;
|
|
let newFolder = aParms && ("newFolder" in aParms) ? aParms.newFolder : null;
|
|
let startIndex, startItem;
|
|
let found = false;
|
|
|
|
let firstVisRow, curFirstVisRow, curLastVisRow;
|
|
if (this.mView.treeBox)
|
|
firstVisRow = this.mView.treeBox.getFirstVisibleRow();
|
|
|
|
if (parentIndex != null)
|
|
{
|
|
// Use the parentIndex if given.
|
|
startIndex = parentIndex;
|
|
if (aFolder.isServer)
|
|
// Fake item for account root folder.
|
|
startItem = { name: "AccountRoot",
|
|
children: [this.mView.getItemAtIndex(startIndex)],
|
|
container: true, open: false, url: null, level: -1};
|
|
else
|
|
startItem = this.mView.getItemAtIndex(startIndex);
|
|
}
|
|
else
|
|
{
|
|
// Get the folder's root parent index.
|
|
let index = 0;
|
|
for (index; index < this.mView.rowCount; index++)
|
|
{
|
|
let item = this.mView.getItemAtIndex(index);
|
|
if (item.url == aFolder.server.rootFolder.URI)
|
|
break;
|
|
}
|
|
startIndex = index;
|
|
if (aFolder.isServer)
|
|
// Fake item for account root folder.
|
|
startItem = { name: "AccountRoot",
|
|
children: [this.mView.getItemAtIndex(startIndex)],
|
|
container: true, open: false, url: null, level: -1};
|
|
else
|
|
startItem = this.mView.getItemAtIndex(startIndex);
|
|
}
|
|
|
|
function containsFolder(aItem)
|
|
{
|
|
// Search for the folder. If it's found, set the open state on all
|
|
// ancestor folders. A toggle() rebuilds the view rows to match the map.
|
|
if (aItem.url == folderURI)
|
|
return found = true;
|
|
|
|
for (let i = 0; i < aItem.children.length; i++) {
|
|
if (aItem.children[i].container && containsFolder(aItem.children[i]))
|
|
{
|
|
if (removeIt && aItem.children[i].url == folderURI)
|
|
{
|
|
// Get all occurences in the tree cache arrays.
|
|
FeedUtils.log.debug("selectFolder: delete in cache, " +
|
|
"parent:children:item:index - "+
|
|
aItem.name + ":" + aItem.children.length + ":" +
|
|
aItem.children[i].name + ":" + i);
|
|
aItem.children.splice(i, 1);
|
|
FeedUtils.log.debug("selectFolder: deleted in cache, " +
|
|
"parent:children - " +
|
|
aItem.name + ":" + aItem.children.length);
|
|
removeIt = false;
|
|
return true;
|
|
}
|
|
if (newFolder)
|
|
{
|
|
let newItem = FeedSubscriptions.makeFolderObject(newFolder,
|
|
aItem.level + 1);
|
|
newItem.open = aItem.children[i].open;
|
|
if (newFolder.isServer)
|
|
FeedSubscriptions.mFeedContainers[startIndex] = newItem;
|
|
else
|
|
{
|
|
aItem.children[i] = newItem;
|
|
aItem.children = FeedSubscriptions.folderItemSorter(aItem.children);
|
|
}
|
|
FeedUtils.log.trace("selectFolder: parentName:newFolderName:newFolderItem - " +
|
|
aItem.name + ":" + newItem.name + ":" + newItem.toSource());
|
|
newFolder = null;
|
|
return true;
|
|
}
|
|
if (!found)
|
|
{
|
|
// For the folder to find.
|
|
found = true;
|
|
aItem.children[i].open = openIt;
|
|
}
|
|
else
|
|
{
|
|
// For ancestor folders.
|
|
if (selectIt || openIt)
|
|
aItem.children[i].open = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (startItem)
|
|
{
|
|
// Find a folder with a specific parent.
|
|
containsFolder(startItem);
|
|
if (!found)
|
|
return false;
|
|
|
|
if (!selectIt)
|
|
return true;
|
|
|
|
if (startItem.open)
|
|
this.mView.toggle(startIndex);
|
|
this.mView.toggleOpenState(startIndex);
|
|
}
|
|
|
|
for (let index = 0; index < this.mView.rowCount && selectIt; index++)
|
|
{
|
|
// The desired folder is now in the view.
|
|
let item = this.mView.getItemAtIndex(index);
|
|
if (!item.container)
|
|
continue;
|
|
if (item.url == folderURI)
|
|
{
|
|
if (item.children.length &&
|
|
((!item.open && openIt) || (item.open && !openIt)))
|
|
this.mView.toggleOpenState(index);
|
|
this.mView.selection.select(index);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Ensure tree position does not jump unnecessarily.
|
|
curFirstVisRow = this.mView.treeBox.getFirstVisibleRow();
|
|
curLastVisRow = this.mView.treeBox.getLastVisibleRow();
|
|
if (firstVisRow >= 0 &&
|
|
this.mView.rowCount - curLastVisRow > firstVisRow - curFirstVisRow)
|
|
this.mView.treeBox.scrollToRow(firstVisRow);
|
|
else
|
|
this.mView.treeBox.ensureRowIsVisible(this.mView.rowCount - 1);
|
|
|
|
FeedUtils.log.debug("selectFolder: curIndex:firstVisRow:" +
|
|
"curFirstVisRow:curLastVisRow:rowCount - " +
|
|
this.mView.selection.currentIndex + ":" +
|
|
firstVisRow + ":" +
|
|
curFirstVisRow + ":" + curLastVisRow + ":" + this.mView.rowCount);
|
|
|
|
return found;
|
|
},
|
|
|
|
/**
|
|
* Find the feed in the tree. The search first gets the feed's folder,
|
|
* then selects the child feed.
|
|
*
|
|
* @param aFeed {Feed object} - the feed to find.
|
|
* @param [aParentIndex] integer - index to start the folder search.
|
|
*
|
|
* @return found bool - true if found, false if not.
|
|
*/
|
|
selectFeed: function(aFeed, aParentIndex)
|
|
{
|
|
let folder = aFeed.folder;
|
|
let found = false;
|
|
|
|
if (aFeed.folder.isServer) {
|
|
// If passed the root folder, the caller wants to get the feed's folder
|
|
// from the db (for cases of an ancestor folder rename/move).
|
|
let itemResource = FeedUtils.rdf.GetResource(aFeed.url);
|
|
let ds = FeedUtils.getSubscriptionsDS(aFeed.folder.server);
|
|
folder = ds.GetTarget(itemResource, FeedUtils.FZ_DESTFOLDER, true);
|
|
}
|
|
|
|
if (this.selectFolder(folder, { parentIndex: aParentIndex }))
|
|
{
|
|
let seln = this.mView.selection;
|
|
let item = this.mView.currentItem;
|
|
if (item) {
|
|
for (let i = seln.currentIndex + 1; i < this.mView.rowCount; i++) {
|
|
if (this.mView.getItemAtIndex(i).url == aFeed.url) {
|
|
this.mView.selection.select(i);
|
|
this.mView.treeBox.ensureRowIsVisible(i);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return found;
|
|
},
|
|
|
|
updateFeedData: function (aItem)
|
|
{
|
|
if (!aItem)
|
|
return;
|
|
|
|
let nameValue = document.getElementById("nameValue");
|
|
let locationValue = document.getElementById("locationValue");
|
|
let locationValidate = document.getElementById("locationValidate");
|
|
let selectFolder = document.getElementById("selectFolder");
|
|
let selectFolderValue = document.getElementById("selectFolderValue");
|
|
let isServer = aItem.folder && aItem.folder.isServer;
|
|
let isFolder = aItem.folder && !aItem.folder.isServer;
|
|
let isFeed = !aItem.container;
|
|
let server, displayFolder;
|
|
|
|
if (isFeed)
|
|
{
|
|
// A feed item. Set the feed location and title info.
|
|
nameValue.value = aItem.name;
|
|
locationValue.value = aItem.url;
|
|
locationValidate.removeAttribute("collapsed");
|
|
|
|
// Root the location picker to the news & blogs server.
|
|
server = aItem.parentFolder.server;
|
|
displayFolder = aItem.parentFolder;
|
|
}
|
|
else
|
|
{
|
|
// A folder/container item.
|
|
nameValue.value = "";
|
|
nameValue.disabled = true;
|
|
locationValue.value = "";
|
|
locationValidate.setAttribute("collapsed", true);
|
|
|
|
server = aItem.folder.server;
|
|
displayFolder = aItem.folder;
|
|
}
|
|
|
|
// Common to both folder and feed items.
|
|
nameValue.disabled = aItem.container;
|
|
this.setFolderPicker(displayFolder, isFeed);
|
|
|
|
// Set quick mode value.
|
|
document.getElementById("quickMode").checked = aItem.quickMode;
|
|
|
|
// Autotag items.
|
|
let autotagEnable = document.getElementById("autotagEnable");
|
|
let autotagUsePrefix = document.getElementById("autotagUsePrefix");
|
|
let autotagPrefix = document.getElementById("autotagPrefix");
|
|
let categoryPrefsAcct = FeedUtils.getOptionsAcct(server).category;
|
|
if (isServer)
|
|
aItem.options = FeedUtils.getOptionsAcct(server);
|
|
let categoryPrefs = aItem.options ? aItem.options.category : null;
|
|
|
|
autotagEnable.checked = categoryPrefs && categoryPrefs.enabled;
|
|
autotagUsePrefix.checked = categoryPrefs && categoryPrefs.prefixEnabled;
|
|
autotagUsePrefix.disabled = !autotagEnable.checked;
|
|
autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
|
|
autotagPrefix.value = categoryPrefs && categoryPrefs.prefix ?
|
|
categoryPrefs.prefix : "";
|
|
},
|
|
|
|
setFolderPicker: function(aFolder, aIsFeed)
|
|
{
|
|
let editFeed = document.getElementById("editFeed");
|
|
let folderPrettyPath = FeedUtils.getFolderPrettyPath(aFolder);
|
|
if (!folderPrettyPath)
|
|
return editFeed.disabled = true;
|
|
|
|
let selectFolder = document.getElementById("selectFolder");
|
|
let selectFolderPopup = document.getElementById("selectFolderPopup");
|
|
let selectFolderValue = document.getElementById("selectFolderValue");
|
|
|
|
selectFolder.setAttribute("hidden", !aIsFeed);
|
|
selectFolder._folder = aFolder;
|
|
selectFolderValue.setAttribute("hidden", aIsFeed);
|
|
selectFolderValue.setAttribute("showfilepath", false);
|
|
|
|
if (aIsFeed)
|
|
{
|
|
selectFolderPopup._ensureInitialized();
|
|
selectFolderPopup.selectFolder(aFolder);
|
|
selectFolder.setAttribute("label", folderPrettyPath);
|
|
selectFolder.setAttribute("uri", aFolder.URI);
|
|
}
|
|
else
|
|
{
|
|
selectFolderValue.value = folderPrettyPath;
|
|
selectFolderValue.setAttribute("prettypath", folderPrettyPath);
|
|
selectFolderValue.setAttribute("filepath", aFolder.filePath.path);
|
|
}
|
|
|
|
return editFeed.disabled = false;
|
|
},
|
|
|
|
onClickSelectFolderValue: function(aEvent)
|
|
{
|
|
let target = aEvent.target;
|
|
if ((("button" in aEvent) &&
|
|
(aEvent.button != 0 ||
|
|
aEvent.originalTarget.localName != "div" ||
|
|
target.selectionStart != target.selectionEnd)) ||
|
|
(aEvent.keyCode && aEvent.keyCode != aEvent.DOM_VK_RETURN))
|
|
return;
|
|
|
|
// Toggle between showing prettyPath and absolute filePath.
|
|
if (target.getAttribute("showfilepath") == "true")
|
|
{
|
|
target.setAttribute("showfilepath", false);
|
|
target.value = target.getAttribute("prettypath");
|
|
}
|
|
else
|
|
{
|
|
target.setAttribute("showfilepath", true);
|
|
target.value = target.getAttribute("filepath");
|
|
}
|
|
},
|
|
|
|
setNewFolder: function(aEvent)
|
|
{
|
|
aEvent.stopPropagation();
|
|
this.setFolderPicker(aEvent.target._folder, true);
|
|
this.editFeed();
|
|
},
|
|
|
|
setSummary: function(aChecked)
|
|
{
|
|
let item = this.mView.currentItem;
|
|
if (!item || !item.folder)
|
|
// Not a folder.
|
|
return;
|
|
|
|
if (item.folder.isServer)
|
|
{
|
|
if (document.getElementById("locationValue").value)
|
|
// Intent is to add a feed/folder to the account, so return.
|
|
return;
|
|
|
|
// An account folder. If it changes, all non feed containing subfolders
|
|
// need to be updated with the new default.
|
|
item.folder.server.setBoolValue("quickMode", aChecked);
|
|
this.FolderListener.folderAdded(item.folder);
|
|
}
|
|
else if (!FeedUtils.getFeedUrlsInFolder(item.folder))
|
|
// Not a folder with feeds.
|
|
return;
|
|
else
|
|
{
|
|
let feedsInFolder = this.getFeedsInFolder(item.folder);
|
|
// Update the feeds database, for each feed in the folder.
|
|
feedsInFolder.forEach(function(feed) { feed.quickMode = aChecked; });
|
|
// Update the folder's feeds properties in the tree map.
|
|
item.children.forEach(function(feed) { feed.quickMode = aChecked; });
|
|
let ds = FeedUtils.getSubscriptionsDS(item.folder.server);
|
|
ds.Flush();
|
|
}
|
|
|
|
// Update the folder in the tree map.
|
|
item.quickMode = aChecked;
|
|
let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
|
|
this.updateStatusItem("statusText", message);
|
|
},
|
|
|
|
setCategoryPrefs: function(aNode)
|
|
{
|
|
let item = this.mView.currentItem;
|
|
if (!item)
|
|
return;
|
|
|
|
let isServer = item.folder && item.folder.isServer;
|
|
let isFolder = item.folder && !item.folder.isServer;
|
|
let autotagEnable = document.getElementById("autotagEnable");
|
|
let autotagUsePrefix = document.getElementById("autotagUsePrefix");
|
|
let autotagPrefix = document.getElementById("autotagPrefix");
|
|
if (isFolder || (isServer && document.getElementById("locationValue").value))
|
|
{
|
|
// Intend to subscribe a feed to a folder, a value must be in the url
|
|
// field. Update states for addFeed() and return.
|
|
autotagUsePrefix.disabled = !autotagEnable.checked;
|
|
autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
|
|
return;
|
|
}
|
|
|
|
switch (aNode.id) {
|
|
case "autotagEnable":
|
|
item.options.category.enabled = aNode.checked;
|
|
break;
|
|
case "autotagUsePrefix":
|
|
item.options.category.prefixEnabled = aNode.checked;
|
|
item.options.category.prefix = autotagPrefix.value;
|
|
break;
|
|
}
|
|
|
|
if (isServer)
|
|
{
|
|
FeedUtils.setOptionsAcct(item.folder.server, item.options)
|
|
}
|
|
else
|
|
{
|
|
let feedResource = FeedUtils.rdf.GetResource(item.url);
|
|
let feed = new Feed(feedResource, item.parentFolder.server);
|
|
feed.options = item.options;
|
|
let ds = FeedUtils.getSubscriptionsDS(item.parentFolder.server);
|
|
ds.Flush();
|
|
}
|
|
|
|
this.updateFeedData(item);
|
|
let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
|
|
this.updateStatusItem("statusText", message);
|
|
},
|
|
|
|
onKeyPress: function(aEvent)
|
|
{
|
|
if (aEvent.keyCode == aEvent.DOM_VK_DELETE &&
|
|
aEvent.target.id == "rssSubscriptionsList")
|
|
this.removeFeed(true);
|
|
|
|
this.clearStatusInfo();
|
|
},
|
|
|
|
onSelect: function ()
|
|
{
|
|
let item = this.mView.currentItem;
|
|
this.updateFeedData(item);
|
|
this.setSummaryFocus();
|
|
this.updateButtons(item);
|
|
},
|
|
|
|
updateButtons: function (aSelectedItem)
|
|
{
|
|
let item = aSelectedItem;
|
|
let isServer = item && item.folder && item.folder.isServer;
|
|
let disable = !item || !item.container || isServer ||
|
|
this.mActionMode == this.kImportingOPML;
|
|
document.getElementById("addFeed").disabled = disable;
|
|
disable = !item || (item.container && !isServer) ||
|
|
this.mActionMode == this.kImportingOPML;
|
|
document.getElementById("editFeed").disabled = disable;
|
|
disable = !item || item.container ||
|
|
this.mActionMode == this.kImportingOPML;
|
|
document.getElementById("removeFeed").disabled = disable;
|
|
disable = !item || !isServer ||
|
|
this.mActionMode == this.kImportingOPML;
|
|
document.getElementById("importOPML").disabled = disable;
|
|
document.getElementById("exportOPML").disabled = disable;
|
|
},
|
|
|
|
onMouseDown: function (aEvent)
|
|
{
|
|
if (aEvent.button != 0 ||
|
|
aEvent.target.id == "validationText" ||
|
|
aEvent.target.id == "addCertException")
|
|
return;
|
|
|
|
this.clearStatusInfo();
|
|
},
|
|
|
|
setSummaryFocus: function ()
|
|
{
|
|
let item = this.mView.currentItem;
|
|
if (!item)
|
|
return;
|
|
|
|
let locationValue = document.getElementById("locationValue");
|
|
let quickMode = document.getElementById("quickMode");
|
|
let autotagEnable = document.getElementById("autotagEnable");
|
|
let autotagUsePrefix = document.getElementById("autotagUsePrefix");
|
|
let autotagPrefix = document.getElementById("autotagPrefix");
|
|
let isServer = item.folder && item.folder.isServer;
|
|
let isFolder = item.folder && !item.folder.isServer;
|
|
let isFeed = !item.container;
|
|
|
|
// Enable summary/autotag by default.
|
|
quickMode.disabled = autotagEnable.disabled = false;
|
|
autotagUsePrefix.disabled = !autotagEnable.checked;
|
|
autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
|
|
|
|
if (isServer)
|
|
{
|
|
let disable = locationValue.hasAttribute("focused") || locationValue.value;
|
|
document.getElementById("addFeed").disabled = !disable;
|
|
document.getElementById("editFeed").disabled = disable;
|
|
|
|
}
|
|
else if (isFolder)
|
|
{
|
|
if (!locationValue.hasAttribute("focused") && !locationValue.value)
|
|
{
|
|
// Enabled for a folder with feeds. Autotag disabled unless intent is
|
|
// to add a feed.
|
|
quickMode.disabled = !FeedUtils.getFeedUrlsInFolder(item.folder);
|
|
autotagEnable.disabled = autotagUsePrefix.disabled =
|
|
autotagPrefix.disabled = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Summary is per folder.
|
|
quickMode.disabled = true;
|
|
}
|
|
},
|
|
|
|
removeFeed: function (aPrompt)
|
|
{
|
|
let seln = this.mView.selection;
|
|
if (seln.count != 1)
|
|
return;
|
|
|
|
let itemToRemove = this.mView.getItemAtIndex(seln.currentIndex);
|
|
|
|
if (!itemToRemove || itemToRemove.container)
|
|
return;
|
|
|
|
if (aPrompt)
|
|
{
|
|
// Confirm unsubscribe prompt.
|
|
let pTitle = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-confirmFeedDeletionTitle");
|
|
let pMessage = FeedUtils.strings.formatStringFromName(
|
|
"subscribe-confirmFeedDeletion", [itemToRemove.name], 1);
|
|
if (Services.prompt.confirmEx(window, pTitle, pMessage,
|
|
Ci.nsIPromptService.STD_YES_NO_BUTTONS,
|
|
null, null, null, null, { }))
|
|
return;
|
|
}
|
|
|
|
FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(itemToRemove.url),
|
|
itemToRemove.parentFolder.server,
|
|
itemToRemove.parentFolder);
|
|
|
|
// Now that we have removed the feed from the datasource, it is time to
|
|
// update our view layer. Update parent folder's quickMode if necessary
|
|
// and remove the child from its parent folder object.
|
|
let parentIndex = this.mView.getParentIndex(seln.currentIndex);
|
|
let parentItem = this.mView.getItemAtIndex(parentIndex);
|
|
this.updateFolderQuickModeInView(itemToRemove, parentItem, true);
|
|
this.mView.removeItemAtIndex(seln.currentIndex, false);
|
|
let message = FeedUtils.strings.GetStringFromName("subscribe-feedRemoved");
|
|
this.updateStatusItem("statusText", message);
|
|
},
|
|
|
|
|
|
/**
|
|
* This addFeed is used by 1) Add button, 1) Update button, 3) Drop of a
|
|
* feed url on a folder (which can be an add or move). If Update, the new
|
|
* url is added and the old removed; thus aParse is false and no new messages
|
|
* are downloaded, the feed is only validated and stored in the db. If dnd,
|
|
* the drop folder is selected and the url is prefilled, so proceed just as
|
|
* though the url were entered manually. This allows a user to see the dnd
|
|
* url better in case of errors.
|
|
*
|
|
* @param [aFeedLocation] string - the feed url; get the url from the
|
|
* input field if null.
|
|
* @param [aFolder] nsIMsgFolder - folder to subscribe, current selected
|
|
* folder if null.
|
|
* @param [aParse] boolean - if true (default) parse and download
|
|
* the feed's articles.
|
|
* @param [aParams] object - additional params.
|
|
* @param [aMode] integer - action mode (default is kSubscribeMode)
|
|
* of the add.
|
|
*
|
|
* @return success boolean - true if edit checks passed and an
|
|
* async download has been initiated.
|
|
*/
|
|
addFeed: function(aFeedLocation, aFolder, aParse, aParams, aMode)
|
|
{
|
|
let message;
|
|
let parse = aParse == null ? true : aParse;
|
|
let mode = aMode == null ? this.kSubscribeMode : aMode;
|
|
let locationValue = document.getElementById("locationValue");
|
|
let quickMode = aParams && ("quickMode" in aParams) ?
|
|
aParams.quickMode : document.getElementById("quickMode").checked;
|
|
let name = aParams && ("name" in aParams) ?
|
|
aParams.name : document.getElementById("nameValue").value;
|
|
let options = aParams && ("options" in aParams) ?
|
|
aParams.options : null;
|
|
|
|
if (aFeedLocation)
|
|
locationValue.value = aFeedLocation;
|
|
let feedLocation = locationValue.value.trim();
|
|
|
|
if (!feedLocation)
|
|
{
|
|
message = locationValue.getAttribute("placeholder");
|
|
this.updateStatusItem("statusText", message);
|
|
return false;
|
|
}
|
|
|
|
if (!FeedUtils.isValidScheme(feedLocation))
|
|
{
|
|
message = FeedUtils.strings.GetStringFromName("subscribe-feedNotValid");
|
|
this.updateStatusItem("statusText", message);
|
|
return false;
|
|
}
|
|
|
|
let addFolder;
|
|
if (aFolder)
|
|
{
|
|
// For Update or if passed a folder.
|
|
if (aFolder instanceof Ci.nsIMsgFolder)
|
|
addFolder = aFolder;
|
|
}
|
|
else
|
|
{
|
|
// A folder must be selected for Add and Drop.
|
|
let index = this.mView.selection.currentIndex;
|
|
let item = this.mView.getItemAtIndex(index);
|
|
if (item && item.container)
|
|
addFolder = item.folder;
|
|
}
|
|
|
|
// Shouldn't happen. Or else not passed an nsIMsgFolder.
|
|
if (!addFolder)
|
|
return false;
|
|
|
|
// Before we go any further, make sure the user is not already subscribed
|
|
// to this feed.
|
|
if (FeedUtils.feedAlreadyExists(feedLocation, addFolder.server))
|
|
{
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-feedAlreadySubscribed");
|
|
this.updateStatusItem("statusText", message);
|
|
return false;
|
|
}
|
|
|
|
if (!options)
|
|
{
|
|
// Not passed a param, get values from the ui.
|
|
options = FeedUtils.optionsTemplate;
|
|
options.category.enabled = document.getElementById("autotagEnable").checked;
|
|
options.category.prefixEnabled = document.getElementById("autotagUsePrefix").checked;
|
|
options.category.prefix = document.getElementById("autotagPrefix").value;
|
|
}
|
|
|
|
let folderURI = addFolder.isServer ? null : addFolder.URI;
|
|
let feedProperties = { feedName : name,
|
|
feedLocation : feedLocation,
|
|
folderURI : folderURI,
|
|
server : addFolder.server,
|
|
quickMode : quickMode,
|
|
options : options };
|
|
|
|
let feed = this.storeFeed(feedProperties);
|
|
if (!feed)
|
|
return false;
|
|
|
|
// Now validate and start downloading the feed.
|
|
message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed");
|
|
this.updateStatusItem("statusText", message);
|
|
this.updateStatusItem("progressMeter", 0);
|
|
document.getElementById("addFeed").setAttribute("disabled", true);
|
|
this.mActionMode = mode;
|
|
feed.download(parse, this.mFeedDownloadCallback);
|
|
return true;
|
|
},
|
|
|
|
// Helper routine used by addFeed and importOPMLFile.
|
|
storeFeed: function(feedProperties)
|
|
{
|
|
let itemResource = FeedUtils.rdf.GetResource(feedProperties.feedLocation);
|
|
let feed = new Feed(itemResource, feedProperties.server);
|
|
|
|
// If the user specified a folder to add the feed to, then set it here.
|
|
if (feedProperties.folderURI)
|
|
{
|
|
let folderResource = FeedUtils.rdf.GetResource(feedProperties.folderURI);
|
|
if (folderResource)
|
|
{
|
|
let folder = folderResource.QueryInterface(Ci.nsIMsgFolder);
|
|
if (folder && !folder.isServer)
|
|
feed.folder = folder;
|
|
}
|
|
}
|
|
|
|
feed.title = feedProperties.feedName;
|
|
feed.quickMode = feedProperties.quickMode;
|
|
feed.options = feedProperties.options;
|
|
return feed;
|
|
},
|
|
|
|
updateAccount: function(aItem)
|
|
{
|
|
// Check to see if the categoryPrefs custom prefix string value changed.
|
|
let editAutotagPrefix = document.getElementById("autotagPrefix").value;
|
|
if (aItem.options.category.prefix != editAutotagPrefix)
|
|
{
|
|
aItem.options.category.prefix = editAutotagPrefix;
|
|
FeedUtils.setOptionsAcct(aItem.folder.server, aItem.options)
|
|
let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
|
|
this.updateStatusItem("statusText", message);
|
|
}
|
|
},
|
|
|
|
editFeed: function()
|
|
{
|
|
let seln = this.mView.selection;
|
|
if (seln.count != 1)
|
|
return;
|
|
|
|
let itemToEdit = this.mView.getItemAtIndex(seln.currentIndex);
|
|
if (itemToEdit.folder && itemToEdit.folder.isServer)
|
|
{
|
|
this.updateAccount(itemToEdit)
|
|
return;
|
|
}
|
|
|
|
if (!itemToEdit || itemToEdit.container || !itemToEdit.parentFolder)
|
|
return;
|
|
|
|
let resource = FeedUtils.rdf.GetResource(itemToEdit.url);
|
|
let currentFolderServer = itemToEdit.parentFolder.server;
|
|
let ds = FeedUtils.getSubscriptionsDS(currentFolderServer);
|
|
let currentFolderURI = itemToEdit.parentFolder.URI;
|
|
let feed = new Feed(resource, currentFolderServer);
|
|
feed.folder = itemToEdit.parentFolder;
|
|
|
|
let editNameValue = document.getElementById("nameValue").value;
|
|
let editFeedLocation = document.getElementById("locationValue").value.trim();
|
|
let selectFolder = document.getElementById("selectFolder");
|
|
let editQuickMode = document.getElementById("quickMode").checked;
|
|
let editAutotagPrefix = document.getElementById("autotagPrefix").value;
|
|
|
|
if (feed.url != editFeedLocation)
|
|
{
|
|
// Updating a url. We need to add the new url and delete the old, to
|
|
// ensure everything is cleaned up correctly.
|
|
this.addFeed(null, itemToEdit.parentFolder, false, null, this.kUpdateMode)
|
|
return;
|
|
}
|
|
|
|
// Did the user change the folder URI for storing the feed?
|
|
let editFolderURI = selectFolder.getAttribute("uri");
|
|
if (currentFolderURI != editFolderURI)
|
|
{
|
|
// Make sure the new folderpicked folder is visible.
|
|
this.selectFolder(selectFolder._folder);
|
|
// Now go back to the feed item.
|
|
this.selectFeed(feed, null);
|
|
// We need to find the index of the new parent folder.
|
|
let newParentIndex = this.mView.kRowIndexUndefined;
|
|
for (let index = 0; index < this.mView.rowCount; index++)
|
|
{
|
|
let item = this.mView.getItemAtIndex(index);
|
|
if (item && item.container && item.url == editFolderURI)
|
|
{
|
|
newParentIndex = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newParentIndex != this.mView.kRowIndexUndefined)
|
|
this.moveCopyFeed(seln.currentIndex, newParentIndex, "move");
|
|
|
|
return;
|
|
}
|
|
|
|
let updated = false;
|
|
let message = "";
|
|
// Disable the button until the update completes and we process the async
|
|
// verify response.
|
|
document.getElementById("editFeed").setAttribute("disabled", true);
|
|
|
|
// Check to see if the title value changed, no blank title allowed.
|
|
if (feed.title != editNameValue)
|
|
{
|
|
if (!editNameValue)
|
|
{
|
|
document.getElementById("nameValue").value = feed.title;
|
|
}
|
|
else
|
|
{
|
|
feed.title = editNameValue;
|
|
itemToEdit.name = editNameValue;
|
|
seln.tree.invalidateRow(seln.currentIndex);
|
|
updated = true;
|
|
}
|
|
}
|
|
|
|
// Check to see if the quickMode value changed.
|
|
if (feed.quickMode != editQuickMode)
|
|
{
|
|
feed.quickMode = editQuickMode;
|
|
itemToEdit.quickMode = editQuickMode;
|
|
updated = true;
|
|
}
|
|
|
|
// Check to see if the categoryPrefs custom prefix string value changed.
|
|
if (itemToEdit.options.category.prefix != editAutotagPrefix &&
|
|
itemToEdit.options.category.prefix != null &&
|
|
editAutotagPrefix != "")
|
|
{
|
|
itemToEdit.options.category.prefix = editAutotagPrefix;
|
|
feed.options = itemToEdit.options;
|
|
updated = true;
|
|
}
|
|
|
|
let verifyDelay = 0;
|
|
if (updated) {
|
|
ds.Flush();
|
|
message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
|
|
this.updateStatusItem("statusText", message);
|
|
verifyDelay = 1500;
|
|
}
|
|
|
|
// Now we want to verify if the stored feed url still works. If it
|
|
// doesn't, show the error. Delay a bit to leave Updated message visible.
|
|
message = FeedUtils.strings.GetStringFromName("subscribe-validating-feed");
|
|
this.mActionMode = this.kVerifyUrlMode;
|
|
setTimeout(() => {
|
|
this.updateStatusItem("statusText", message);
|
|
this.updateStatusItem("progressMeter", "?");
|
|
feed.download(false, this.mFeedDownloadCallback);
|
|
}, verifyDelay);
|
|
},
|
|
|
|
/**
|
|
* Moves or copies a feed to another folder or account.
|
|
*
|
|
* @param int aOldFeedIndex - index in tree of target feed item.
|
|
* @param int aNewParentIndex - index in tree of target parent folder item.
|
|
* @param string aMoveCopy - either "move" or "copy".
|
|
*/
|
|
moveCopyFeed: function(aOldFeedIndex, aNewParentIndex, aMoveCopy)
|
|
{
|
|
let moveFeed = aMoveCopy == "move";
|
|
let currentItem = this.mView.getItemAtIndex(aOldFeedIndex);
|
|
if (!currentItem ||
|
|
this.mView.getParentIndex(aOldFeedIndex) == aNewParentIndex)
|
|
// If the new parent is the same as the current parent, then do nothing.
|
|
return;
|
|
|
|
let currentParentIndex = this.mView.getParentIndex(aOldFeedIndex);
|
|
let currentParentItem = this.mView.getItemAtIndex(currentParentIndex);
|
|
let currentParentResource = FeedUtils.rdf.GetResource(currentParentItem.url);
|
|
let currentFolder = currentParentResource.QueryInterface(Ci.nsIMsgFolder);
|
|
|
|
let newParentItem = this.mView.getItemAtIndex(aNewParentIndex);
|
|
let newParentResource = FeedUtils.rdf.GetResource(newParentItem.url);
|
|
let newFolder = newParentResource.QueryInterface(Ci.nsIMsgFolder);
|
|
|
|
let ds = FeedUtils.getSubscriptionsDS(currentItem.parentFolder.server);
|
|
let resource = FeedUtils.rdf.GetResource(currentItem.url);
|
|
|
|
let accountMoveCopy = false;
|
|
if (currentFolder.rootFolder.URI == newFolder.rootFolder.URI)
|
|
{
|
|
// Moving within the same account/feeds db.
|
|
if (newFolder.isServer || !moveFeed)
|
|
// No moving to account folder if already in the account; can only move,
|
|
// not copy, to folder in the same account.
|
|
return;
|
|
|
|
// Unassert the older URI, add an assertion for the new parent URI.
|
|
ds.Change(resource, FeedUtils.FZ_DESTFOLDER,
|
|
currentParentResource, newParentResource);
|
|
ds.Flush();
|
|
|
|
// Update folderpane favicons.
|
|
FeedUtils.setFolderPaneProperty(currentFolder, "favicon", null, "row");
|
|
FeedUtils.setFolderPaneProperty(newFolder, "favicon", null, "row");
|
|
}
|
|
else
|
|
{
|
|
// Moving/copying to a new account. If dropping on the account folder,
|
|
// a new subfolder is created if necessary.
|
|
accountMoveCopy = true;
|
|
let mode = moveFeed ? this.kMoveMode : this.kCopyMode;
|
|
let params = {quickMode: currentItem.quickMode,
|
|
name: currentItem.name,
|
|
options: currentItem.options};
|
|
// Subscribe to the new folder first. If it already exists in the
|
|
// account or on error, return.
|
|
if (!this.addFeed(currentItem.url, newFolder, false, params, mode))
|
|
return;
|
|
// Unsubscribe the feed from the old folder, if add to the new folder
|
|
// is successfull, and doing a move.
|
|
if (moveFeed)
|
|
FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(currentItem.url),
|
|
currentItem.parentFolder.server,
|
|
currentItem.parentFolder);
|
|
}
|
|
|
|
// Update local favicons.
|
|
currentParentItem.favicon = newParentItem.favicon = null;
|
|
|
|
// Finally, update our view layer. Update old parent folder's quickMode
|
|
// and remove the old row, if move. Otherwise no change to the view.
|
|
if (moveFeed)
|
|
{
|
|
this.updateFolderQuickModeInView(currentItem, currentParentItem, true);
|
|
this.mView.removeItemAtIndex(aOldFeedIndex, true);
|
|
if (aNewParentIndex > aOldFeedIndex)
|
|
aNewParentIndex--;
|
|
}
|
|
|
|
if (accountMoveCopy)
|
|
{
|
|
// If a cross account move/copy, download callback will update the view
|
|
// with the new location. Preselect folder/mode for callback.
|
|
this.selectFolder(newFolder, { parentIndex: aNewParentIndex });
|
|
return;
|
|
}
|
|
|
|
// Add the new row location to the view.
|
|
currentItem.level = newParentItem.level + 1;
|
|
currentItem.parentFolder = newFolder;
|
|
this.updateFolderQuickModeInView(currentItem, newParentItem, false);
|
|
newParentItem.children.push(currentItem);
|
|
|
|
if (newParentItem.open)
|
|
// Close the container, selecting the feed will rebuild the view rows.
|
|
this.mView.toggle(aNewParentIndex);
|
|
|
|
this.selectFeed({folder: newParentItem.folder, url: currentItem.url},
|
|
aNewParentIndex);
|
|
|
|
let message = FeedUtils.strings.GetStringFromName("subscribe-feedMoved");
|
|
this.updateStatusItem("statusText", message);
|
|
},
|
|
|
|
updateFolderQuickModeInView: function (aFeedItem, aParentItem, aRemove)
|
|
{
|
|
let feedItem = aFeedItem;
|
|
let parentItem = aParentItem;
|
|
let feedUrlArray = FeedUtils.getFeedUrlsInFolder(feedItem.parentFolder);
|
|
let feedsInFolder = feedUrlArray ? feedUrlArray.length : 0;
|
|
|
|
if (aRemove && feedsInFolder < 1)
|
|
// Removed only feed in folder; set quickMode to server default.
|
|
parentItem.quickMode = parentItem.folder.server.getBoolValue("quickMode");
|
|
|
|
if (!aRemove)
|
|
{
|
|
// Just added a feed to a folder. If there are already feeds in the
|
|
// folder, the feed must reflect the parent's quickMode. If it is the
|
|
// only feed, update the parent folder to the feed's quickMode.
|
|
if (feedsInFolder > 1)
|
|
{
|
|
let feedResource = FeedUtils.rdf.GetResource(feedItem.url);
|
|
let feed = new Feed(feedResource, feedItem.parentFolder.server);
|
|
feed.quickMode = parentItem.quickMode;
|
|
feedItem.quickMode = parentItem.quickMode;
|
|
}
|
|
else
|
|
parentItem.quickMode = feedItem.quickMode;
|
|
}
|
|
},
|
|
|
|
onDragStart: function (aEvent)
|
|
{
|
|
// Get the selected feed article (if there is one).
|
|
let seln = this.mView.selection;
|
|
if (seln.count != 1)
|
|
return;
|
|
|
|
// Only initiate a drag if the item is a feed (ignore folders/containers).
|
|
let item = this.mView.getItemAtIndex(seln.currentIndex);
|
|
if (!item || item.container)
|
|
return;
|
|
|
|
aEvent.dataTransfer.setData("text/x-moz-feed-index", seln.currentIndex);
|
|
aEvent.dataTransfer.effectAllowed = "copyMove";
|
|
},
|
|
|
|
onDragOver: function (aEvent)
|
|
{
|
|
this.mView._currentDataTransfer = aEvent.dataTransfer;
|
|
},
|
|
|
|
mFeedDownloadCallback:
|
|
{
|
|
mSubscribeMode: true,
|
|
downloaded: function(feed, aErrorCode)
|
|
{
|
|
// Offline check is done in the context of 3pane, return to the subscribe
|
|
// window once the modal prompt is dispatched.
|
|
window.focus();
|
|
// Feed is null if our attempt to parse the feed failed.
|
|
let message = "";
|
|
let win = FeedSubscriptions;
|
|
if (aErrorCode == FeedUtils.kNewsBlogSuccess ||
|
|
aErrorCode == FeedUtils.kNewsBlogNoNewItems)
|
|
{
|
|
win.updateStatusItem("progressMeter", 100);
|
|
|
|
if (win.mActionMode == win.kVerifyUrlMode) {
|
|
// Just checking for errors, if none bye. The (non error) code
|
|
// kNewsBlogNoNewItems can only happen in verify mode.
|
|
win.mActionMode = null;
|
|
win.clearStatusInfo();
|
|
message = FeedUtils.strings.GetStringFromName("subscribe-feedVerified");
|
|
win.updateStatusItem("statusText", message);
|
|
document.getElementById("editFeed").removeAttribute("disabled");
|
|
return;
|
|
}
|
|
|
|
// Add the feed to the databases.
|
|
FeedUtils.addFeed(feed);
|
|
|
|
// Now add the feed to our view. If adding, the current selection will
|
|
// be a folder; if updating it will be a feed. No need to rebuild the
|
|
// entire view, that is too jarring.
|
|
let curIndex = win.mView.selection.currentIndex;
|
|
let curItem = win.mView.getItemAtIndex(curIndex);
|
|
if (curItem)
|
|
{
|
|
let parentIndex, parentItem, newItem, level;
|
|
let rows = win.mFeedContainers;
|
|
if (curItem.container)
|
|
{
|
|
// Open the container, if it exists.
|
|
let folderExists = win.selectFolder(feed.folder,
|
|
{ parentIndex: curIndex });
|
|
if (!folderExists)
|
|
{
|
|
// This means a new folder was created.
|
|
parentIndex = curIndex;
|
|
parentItem = curItem;
|
|
level = curItem.level + 1;
|
|
newItem = win.makeFolderObject(feed.folder, level);
|
|
}
|
|
else
|
|
{
|
|
// If a folder happens to exist which matches one that would
|
|
// have been created, the feed system reuses it. Get the
|
|
// current item again if reusing a previously unselected folder.
|
|
curIndex = win.mView.selection.currentIndex;
|
|
curItem = win.mView.getItemAtIndex(curIndex);
|
|
parentIndex = curIndex;
|
|
parentItem = curItem;
|
|
level = curItem.level + 1;
|
|
newItem = win.makeFeedObject(feed, feed.folder, level);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Adding a feed.
|
|
parentIndex = win.mView.getParentIndex(curIndex);
|
|
parentItem = win.mView.getItemAtIndex(parentIndex);
|
|
level = curItem.level;
|
|
newItem = win.makeFeedObject(feed, feed.folder, level);
|
|
}
|
|
|
|
if (!newItem.container)
|
|
win.updateFolderQuickModeInView(newItem, parentItem, false);
|
|
parentItem.children.push(newItem);
|
|
parentItem.children = win.folderItemSorter(parentItem.children);
|
|
parentItem.favicon = null;
|
|
|
|
if (win.mActionMode == win.kSubscribeMode)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-feedAdded");
|
|
if (win.mActionMode == win.kUpdateMode)
|
|
{
|
|
win.removeFeed(false);
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-feedUpdated");
|
|
}
|
|
if (win.mActionMode == win.kMoveMode)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-feedMoved");
|
|
if (win.mActionMode == win.kCopyMode)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-feedCopied");
|
|
|
|
win.selectFeed(feed, parentIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non success. Remove intermediate traces from the feeds database.
|
|
// But only if we're not in verify mode.
|
|
if (win.mActionMode != win.kVerifyUrlMode &&
|
|
feed && feed.url && feed.server)
|
|
FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url),
|
|
feed.server,
|
|
feed.server.rootFolder);
|
|
|
|
if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-feedNotValid");
|
|
if (aErrorCode == FeedUtils.kNewsBlogRequestFailure)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-networkError");
|
|
if (aErrorCode == FeedUtils.kNewsBlogFileError)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-errorOpeningFile");
|
|
if (aErrorCode == FeedUtils.kNewsBlogBadCertError) {
|
|
let host = Services.io.newURI(feed.url, null, null).host;
|
|
message = FeedUtils.strings.formatStringFromName(
|
|
"newsblog-badCertError", [host], 1);
|
|
}
|
|
if (aErrorCode == FeedUtils.kNewsBlogNoAuthError)
|
|
message = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-noAuthError");
|
|
|
|
if (win.mActionMode != win.kUpdateMode &&
|
|
win.mActionMode != win.kVerifyUrlMode)
|
|
// Re-enable the add button if subscribe failed.
|
|
document.getElementById("addFeed").removeAttribute("disabled");
|
|
if (win.mActionMode == win.kVerifyUrlMode)
|
|
// Re-enable the update button if verify failed.
|
|
document.getElementById("editFeed").removeAttribute("disabled");
|
|
}
|
|
|
|
win.mActionMode = null;
|
|
win.clearStatusInfo();
|
|
let code = feed.url.startsWith("http") ? aErrorCode : null;
|
|
win.updateStatusItem("statusText", message, code);
|
|
},
|
|
|
|
// This gets called after the RSS parser finishes storing a feed item to
|
|
// disk. aCurrentFeedItems is an integer corresponding to how many feed
|
|
// items have been downloaded so far. aMaxFeedItems is an integer
|
|
// corresponding to the total number of feed items to download.
|
|
onFeedItemStored: function (feed, aCurrentFeedItems, aMaxFeedItems)
|
|
{
|
|
window.focus();
|
|
let message = FeedUtils.strings.formatStringFromName(
|
|
"subscribe-gettingFeedItems",
|
|
[aCurrentFeedItems, aMaxFeedItems], 2);
|
|
FeedSubscriptions.updateStatusItem("statusText", message);
|
|
this.onProgress(feed, aCurrentFeedItems, aMaxFeedItems);
|
|
},
|
|
|
|
onProgress: function(feed, aProgress, aProgressMax, aLengthComputable)
|
|
{
|
|
FeedSubscriptions.updateStatusItem("progressMeter",
|
|
(aProgress * 100) / aProgressMax);
|
|
}
|
|
},
|
|
|
|
// Status routines.
|
|
updateStatusItem: function(aID, aValue, aErrorCode)
|
|
{
|
|
let el = document.getElementById(aID);
|
|
if (el.getAttribute("collapsed"))
|
|
el.removeAttribute("collapsed");
|
|
|
|
if (aID == "progressMeter")
|
|
el.setAttribute("mode", aValue == "?" ? "undetermined" : "determined");
|
|
|
|
if (aID == "statusText")
|
|
el.textContent = aValue;
|
|
else
|
|
el.value = aValue;
|
|
|
|
el = document.getElementById("validationText");
|
|
if (aErrorCode == FeedUtils.kNewsBlogInvalidFeed)
|
|
el.removeAttribute("collapsed");
|
|
else
|
|
el.setAttribute("collapsed", true);
|
|
|
|
el = document.getElementById("addCertException");
|
|
if (aErrorCode == FeedUtils.kNewsBlogBadCertError)
|
|
el.removeAttribute("collapsed");
|
|
else
|
|
el.setAttribute("collapsed", true);
|
|
},
|
|
|
|
clearStatusInfo: function()
|
|
{
|
|
document.getElementById("statusText").textContent = "";
|
|
document.getElementById("progressMeter").collapsed = true;
|
|
document.getElementById("validationText").collapsed = true;
|
|
document.getElementById("addCertException").collapsed = true;
|
|
},
|
|
|
|
checkValidation: function(aEvent)
|
|
{
|
|
if (aEvent.button != 0)
|
|
return;
|
|
|
|
let validationSite = "http://validator.w3.org";
|
|
let validationQuery = "http://validator.w3.org/feed/check.cgi?url=";
|
|
|
|
if (this.mMainWin)
|
|
{
|
|
let tabmail = this.mMainWin.document.getElementById("tabmail");
|
|
if (tabmail)
|
|
{
|
|
let feedLocation = document.getElementById("locationValue").value;
|
|
let url = validationQuery + encodeURIComponent(feedLocation);
|
|
|
|
this.mMainWin.focus();
|
|
this.mMainWin.openContentTab(url, "tab", "^" + validationSite);
|
|
FeedUtils.log.debug("checkValidation: query url - " + url);
|
|
}
|
|
}
|
|
aEvent.stopPropagation();
|
|
},
|
|
|
|
addCertExceptionDialog: function()
|
|
{
|
|
let feedURL = document.getElementById("locationValue").value.trim();
|
|
let params = { exceptionAdded : false,
|
|
location: feedURL,
|
|
prefetchCert: true };
|
|
window.openDialog("chrome://pippki/content/exceptionDialog.xul",
|
|
"", "chrome,centerscreen,modal", params);
|
|
if (params.exceptionAdded)
|
|
this.clearStatusInfo();
|
|
},
|
|
|
|
// Listener for folder pane changes.
|
|
FolderListener: {
|
|
get feedWindow() {
|
|
let subscriptionsWindow =
|
|
Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
|
|
return subscriptionsWindow ? subscriptionsWindow.FeedSubscriptions : null;
|
|
},
|
|
|
|
get currentSelectedIndex() {
|
|
return this.feedWindow ? this.feedWindow.mView.selection.currentIndex : -1;
|
|
},
|
|
|
|
get currentSelectedItem() {
|
|
return this.feedWindow ? this.feedWindow.mView.currentItem : null;
|
|
},
|
|
|
|
folderAdded: function(aFolder)
|
|
{
|
|
if (aFolder.server.type != "rss" ||
|
|
FeedUtils.isInTrash(aFolder))
|
|
return;
|
|
|
|
let parentFolder = aFolder.isServer ? aFolder : aFolder.parent;
|
|
FeedUtils.log.debug("folderAdded: folder:parent - " + aFolder.name + ":" +
|
|
(parentFolder ? parentFolder.filePath.path : "(null)"));
|
|
|
|
if (!parentFolder || !this.feedWindow)
|
|
return;
|
|
|
|
let feedWindow = this.feedWindow;
|
|
let curSelIndex = this.currentSelectedIndex;
|
|
let curSelItem = this.currentSelectedItem;
|
|
let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
|
|
let indexInView = feedWindow.mView.getItemInViewIndex(parentFolder);
|
|
let open = indexInView != null;
|
|
|
|
if (aFolder.isServer)
|
|
{
|
|
if (indexInView != null)
|
|
// Existing account root folder in the view.
|
|
open = feedWindow.mView.getItemAtIndex(indexInView).open;
|
|
else
|
|
{
|
|
// Add the account root folder to the view.
|
|
feedWindow.mFeedContainers.push(feedWindow.makeFolderObject(parentFolder, 0));
|
|
feedWindow.mView.mRowCount++;
|
|
feedWindow.mTree.view = feedWindow.mView;
|
|
feedWindow.mView.treeBox.scrollToRow(firstVisRow);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Rebuild the added folder's parent item in the tree row cache.
|
|
feedWindow.selectFolder(parentFolder, { select: false,
|
|
open: open,
|
|
newFolder: parentFolder });
|
|
|
|
if (indexInView == null || !curSelItem)
|
|
// Folder isn't in the tree view, no need to update the view.
|
|
return;
|
|
|
|
let parentIndex = feedWindow.mView.getParentIndex(indexInView);
|
|
if (parentIndex == feedWindow.mView.kRowIndexUndefined)
|
|
// Root folder is its own parent.
|
|
parentIndex = indexInView;
|
|
if (open)
|
|
{
|
|
// Close an open parent (or root) folder.
|
|
feedWindow.mView.toggle(parentIndex);
|
|
feedWindow.mView.toggleOpenState(parentIndex);
|
|
}
|
|
feedWindow.mView.treeBox.scrollToRow(firstVisRow);
|
|
|
|
if (curSelItem.container)
|
|
feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
|
|
else
|
|
feedWindow.selectFeed({ folder: curSelItem.parentFolder,
|
|
url: curSelItem.url }, parentIndex);
|
|
},
|
|
|
|
folderDeleted: function(aFolder)
|
|
{
|
|
if (aFolder.server.type != "rss" || FeedUtils.isInTrash(aFolder))
|
|
return;
|
|
|
|
FeedUtils.log.debug("folderDeleted: folder - " + aFolder.name);
|
|
if (!this.feedWindow)
|
|
return;
|
|
|
|
let feedWindow = this.feedWindow;
|
|
let curSelIndex = this.currentSelectedIndex;
|
|
let indexInView = feedWindow.mView.getItemInViewIndex(aFolder);
|
|
let open = indexInView != null;
|
|
|
|
// Delete the folder from the tree row cache.
|
|
feedWindow.selectFolder(aFolder, { select: false, open: false, remove: true });
|
|
|
|
if (!open || curSelIndex < 0)
|
|
// Folder isn't in the tree view, no need to update the view.
|
|
return;
|
|
|
|
let select =
|
|
indexInView == curSelIndex ||
|
|
feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
|
|
feedWindow.mView.removeItemAtIndex(indexInView, !select);
|
|
},
|
|
|
|
folderRenamed: function(aOrigFolder, aNewFolder)
|
|
{
|
|
if (aNewFolder.server.type != "rss" || FeedUtils.isInTrash(aNewFolder))
|
|
return;
|
|
|
|
FeedUtils.log.debug("folderRenamed: old:new - " +
|
|
aOrigFolder.name + ":" + aNewFolder.name);
|
|
if (!this.feedWindow)
|
|
return;
|
|
|
|
let feedWindow = this.feedWindow;
|
|
let curSelIndex = this.currentSelectedIndex;
|
|
let curSelItem = this.currentSelectedItem;
|
|
let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
|
|
let indexInView = feedWindow.mView.getItemInViewIndex(aOrigFolder);
|
|
let open = indexInView != null;
|
|
|
|
// Rebuild the renamed folder's item in the tree row cache.
|
|
feedWindow.selectFolder(aOrigFolder, { select: false,
|
|
open: open,
|
|
newFolder: aNewFolder });
|
|
|
|
if (!open || !curSelItem)
|
|
// Folder isn't in the tree view, no need to update the view.
|
|
return;
|
|
|
|
let select =
|
|
indexInView == curSelIndex ||
|
|
feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
|
|
let parentIndex = feedWindow.mView.getParentIndex(indexInView);
|
|
if (parentIndex == feedWindow.mView.kRowIndexUndefined)
|
|
// Root folder is its own parent.
|
|
parentIndex = indexInView;
|
|
feedWindow.mView.toggle(parentIndex);
|
|
feedWindow.mView.toggleOpenState(parentIndex);
|
|
feedWindow.mView.treeBox.scrollToRow(firstVisRow);
|
|
|
|
if (curSelItem.container) {
|
|
if (curSelItem.folder == aOrigFolder)
|
|
feedWindow.selectFolder(aNewFolder, { open: curSelItem.open });
|
|
else if (select)
|
|
feedWindow.mView.selection.select(indexInView);
|
|
else
|
|
feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
|
|
}
|
|
else
|
|
feedWindow.selectFeed({ folder: curSelItem.parentFolder.rootFolder,
|
|
url: curSelItem.url }, parentIndex);
|
|
},
|
|
|
|
folderMoveCopyCompleted: function(aMove, aSrcFolder, aDestFolder)
|
|
{
|
|
if (aDestFolder.server.type != "rss")
|
|
return;
|
|
|
|
FeedUtils.log.debug("folderMoveCopyCompleted: move:src:dest - " +
|
|
aMove + ":" + aSrcFolder.name + ":" + aDestFolder.name);
|
|
if (!this.feedWindow)
|
|
return;
|
|
|
|
let feedWindow = this.feedWindow;
|
|
let curSelIndex = this.currentSelectedIndex;
|
|
let curSelItem = this.currentSelectedItem;
|
|
let firstVisRow = feedWindow.mView.treeBox.getFirstVisibleRow();
|
|
let indexInView = feedWindow.mView.getItemInViewIndex(aSrcFolder);
|
|
let destIndexInView = feedWindow.mView.getItemInViewIndex(aDestFolder);
|
|
let open = indexInView != null || destIndexInView != null;
|
|
let parentIndex = feedWindow.mView.getItemInViewIndex(aDestFolder.parent ||
|
|
aDestFolder);
|
|
let select =
|
|
indexInView == curSelIndex ||
|
|
feedWindow.mView.isIndexChildOfParentIndex(indexInView, curSelIndex);
|
|
|
|
if (aMove)
|
|
{
|
|
this.folderDeleted(aSrcFolder);
|
|
if (aDestFolder.getFlag(Ci.nsMsgFolderFlags.Trash))
|
|
return;
|
|
}
|
|
|
|
setTimeout(function() {
|
|
// State on disk needs to settle before a folder object can be rebuilt.
|
|
feedWindow.selectFolder(aDestFolder, { select: false,
|
|
open: open || select,
|
|
newFolder: aDestFolder });
|
|
|
|
if (!open || !curSelItem)
|
|
// Folder isn't in the tree view, no need to update the view.
|
|
return;
|
|
|
|
feedWindow.mView.toggle(parentIndex);
|
|
feedWindow.mView.toggleOpenState(parentIndex);
|
|
feedWindow.mView.treeBox.scrollToRow(firstVisRow);
|
|
|
|
if (curSelItem.container) {
|
|
if (curSelItem.folder == aSrcFolder || select)
|
|
feedWindow.selectFolder(aDestFolder, { open: true });
|
|
else
|
|
feedWindow.selectFolder(curSelItem.folder, { open: curSelItem.open });
|
|
}
|
|
else
|
|
feedWindow.selectFeed({ folder: curSelItem.parentFolder.rootFolder,
|
|
url: curSelItem.url }, null);
|
|
}, 50);
|
|
}
|
|
},
|
|
|
|
/* *************************************************************** */
|
|
/* OPML Functions */
|
|
/* *************************************************************** */
|
|
|
|
get brandShortName() {
|
|
let brandBundle = document.getElementById("bundle_brand");
|
|
return brandBundle ? brandBundle.getString("brandShortName") : "";
|
|
},
|
|
|
|
/**
|
|
* Export feeds as opml file Save As filepicker function.
|
|
*
|
|
* @param bool aList - if true, exporting as list; if false (default)
|
|
* exporting feeds in folder structure - used for title.
|
|
* @return nsILocalFile or null.
|
|
*/
|
|
opmlPickSaveAsFile: function(aList)
|
|
{
|
|
let accountName = this.mRSSServer.rootFolder.prettyName;
|
|
let fileName = FeedUtils.strings.formatStringFromName(
|
|
"subscribe-OPMLExportDefaultFileName",
|
|
[this.brandShortName, accountName], 2);
|
|
let title = aList ? FeedUtils.strings.formatStringFromName(
|
|
"subscribe-OPMLExportTitleList", [accountName], 1) :
|
|
FeedUtils.strings.formatStringFromName(
|
|
"subscribe-OPMLExportTitleStruct", [accountName], 1);
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
|
|
|
fp.defaultString = fileName;
|
|
fp.defaultExtension = "opml";
|
|
if (this.opmlLastSaveAsDir && (this.opmlLastSaveAsDir instanceof Ci.nsILocalFile))
|
|
fp.displayDirectory = this.opmlLastSaveAsDir;
|
|
|
|
let opmlFilterText = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-OPMLExportOPMLFilesFilterText");
|
|
fp.appendFilter(opmlFilterText, "*.opml");
|
|
fp.appendFilters(Ci.nsIFilePicker.filterAll);
|
|
fp.filterIndex = 0;
|
|
fp.init(window, title, Ci.nsIFilePicker.modeSave);
|
|
|
|
if (fp.show() != Ci.nsIFilePicker.returnCancel && fp.file)
|
|
{
|
|
this.opmlLastSaveAsDir = fp.file.parent;
|
|
return fp.file;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Import feeds opml file Open filepicker function.
|
|
*
|
|
* @return nsILocalFile or null.
|
|
*/
|
|
opmlPickOpenFile: function()
|
|
{
|
|
let title = FeedUtils.strings.GetStringFromName("subscribe-OPMLImportTitle");
|
|
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
|
|
|
fp.defaultString = "";
|
|
if (this.opmlLastOpenDir && (this.opmlLastOpenDir instanceof Ci.nsILocalFile))
|
|
fp.displayDirectory = this.opmlLastOpenDir;
|
|
|
|
let opmlFilterText = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-OPMLExportOPMLFilesFilterText");
|
|
fp.appendFilter(opmlFilterText, "*.opml");
|
|
fp.appendFilters(Ci.nsIFilePicker.filterXML);
|
|
fp.appendFilters(Ci.nsIFilePicker.filterAll);
|
|
fp.init(window, title, Ci.nsIFilePicker.modeOpen);
|
|
|
|
if (fp.show() != Ci.nsIFilePicker.returnCancel && fp.file)
|
|
{
|
|
this.opmlLastOpenDir = fp.file.parent;
|
|
return fp.file;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
exportOPML: function(aEvent)
|
|
{
|
|
// Account folder must be selected.
|
|
let item = this.mView.currentItem;
|
|
if (!item || !item.folder || !item.folder.isServer)
|
|
return;
|
|
|
|
this.mRSSServer = item.folder.server;
|
|
let rootFolder = this.mRSSServer.rootFolder;
|
|
let exportAsList = aEvent.ctrlKey;
|
|
let SPACES2 = " ";
|
|
let SPACES4 = " ";
|
|
|
|
if (this.mRSSServer.rootFolder.hasSubFolders)
|
|
{
|
|
let opmlDoc = document.implementation.createDocument("", "opml", null);
|
|
let opmlRoot = opmlDoc.documentElement;
|
|
opmlRoot.setAttribute("version", "1.0");
|
|
opmlRoot.setAttribute("xmlns:fz", "urn:forumzilla:");
|
|
|
|
this.generatePPSpace(opmlRoot, SPACES2);
|
|
|
|
// Make the <head> element.
|
|
let head = opmlDoc.createElement("head");
|
|
this.generatePPSpace(head, SPACES4);
|
|
let titleText = FeedUtils.strings.formatStringFromName(
|
|
"subscribe-OPMLExportFileDialogTitle",
|
|
[this.brandShortName, rootFolder.prettyName], 2);
|
|
let title = opmlDoc.createElement("title");
|
|
title.appendChild(opmlDoc.createTextNode(titleText));
|
|
head.appendChild(title);
|
|
this.generatePPSpace(head, SPACES4);
|
|
let dt = opmlDoc.createElement("dateCreated");
|
|
dt.appendChild(opmlDoc.createTextNode((new Date()).toUTCString()));
|
|
head.appendChild(dt);
|
|
this.generatePPSpace(head, SPACES2);
|
|
opmlRoot.appendChild(head);
|
|
|
|
this.generatePPSpace(opmlRoot, SPACES2);
|
|
|
|
// Add <outline>s to the <body>.
|
|
let body = opmlDoc.createElement("body");
|
|
if (exportAsList)
|
|
this.generateOutlineList(rootFolder, body, SPACES4.length + 2);
|
|
else
|
|
this.generateOutlineStruct(rootFolder, body, SPACES4.length);
|
|
|
|
this.generatePPSpace(body, SPACES2);
|
|
|
|
if (!body.childElementCount)
|
|
// No folders/feeds.
|
|
return;
|
|
|
|
opmlRoot.appendChild(body);
|
|
this.generatePPSpace(opmlRoot, "");
|
|
|
|
let serializer = new XMLSerializer();
|
|
|
|
if (FeedUtils.log.level <= Log4Moz.Level.Debug)
|
|
FeedUtils.log.debug("exportOPML: opmlDoc -\n" +
|
|
serializer.serializeToString(opmlDoc) + "\n");
|
|
|
|
// Get file to save from filepicker.
|
|
let saveAsFile = this.opmlPickSaveAsFile(exportAsList);
|
|
if (!saveAsFile)
|
|
return;
|
|
|
|
let fos = FileUtils.openSafeFileOutputStream(saveAsFile);
|
|
serializer.serializeToStream(opmlDoc, fos, "utf-8");
|
|
FileUtils.closeSafeFileOutputStream(fos);
|
|
|
|
let statusReport = FeedUtils.strings.formatStringFromName(
|
|
"subscribe-OPMLExportDone", [saveAsFile.path], 1);
|
|
this.updateStatusItem("statusText", statusReport);
|
|
}
|
|
},
|
|
|
|
generatePPSpace: function(aNode, indentString)
|
|
{
|
|
aNode.appendChild(aNode.ownerDocument.createTextNode("\n"));
|
|
aNode.appendChild(aNode.ownerDocument.createTextNode(indentString));
|
|
},
|
|
|
|
generateOutlineList: function(baseFolder, parent, indentLevel)
|
|
{
|
|
// Pretty printing.
|
|
let indentString = " ".repeat(indentLevel - 2);
|
|
|
|
let feedOutline;
|
|
let folderEnumerator = baseFolder.subFolders;
|
|
while (folderEnumerator.hasMoreElements())
|
|
{
|
|
let folder = folderEnumerator.getNext().QueryInterface(Ci.nsIMsgFolder);
|
|
FeedUtils.log.debug("generateOutlineList: folder - " +
|
|
folder.filePath.path);
|
|
if (!(folder instanceof Ci.nsIMsgFolder) ||
|
|
folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
|
|
folder.getFlag(Ci.nsMsgFolderFlags.Virtual))
|
|
continue;
|
|
|
|
FeedUtils.log.debug("generateOutlineList: CONTINUE folderName - " +
|
|
folder.name);
|
|
|
|
if (folder.hasSubFolders)
|
|
{
|
|
FeedUtils.log.debug("generateOutlineList: has subfolders - " +
|
|
folder.name);
|
|
// Recurse.
|
|
this.generateOutlineList(folder, parent, indentLevel);
|
|
}
|
|
|
|
// Add outline elements with xmlUrls.
|
|
let feeds = this.getFeedsInFolder(folder);
|
|
for (let feed of feeds)
|
|
{
|
|
FeedUtils.log.debug("generateOutlineList: folder has FEED url - " +
|
|
folder.name + " : " + feed.url);
|
|
feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument);
|
|
this.generatePPSpace(parent, indentString);
|
|
parent.appendChild(feedOutline);
|
|
}
|
|
}
|
|
},
|
|
|
|
generateOutlineStruct: function(baseFolder, parent, indentLevel)
|
|
{
|
|
// Pretty printing.
|
|
function indentString(len) { return " ".repeat(len - 2); };
|
|
|
|
let folderOutline, feedOutline;
|
|
let folderEnumerator = baseFolder.subFolders;
|
|
while (folderEnumerator.hasMoreElements())
|
|
{
|
|
let folder = folderEnumerator.getNext().QueryInterface(Ci.nsIMsgFolder);
|
|
FeedUtils.log.debug("generateOutlineStruct: folder - " +
|
|
folder.filePath.path);
|
|
if (!(folder instanceof Ci.nsIMsgFolder) ||
|
|
folder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
|
|
folder.getFlag(Ci.nsMsgFolderFlags.Virtual))
|
|
continue;
|
|
|
|
FeedUtils.log.debug("generateOutlineStruct: CONTINUE folderName - " +
|
|
folder.name);
|
|
|
|
// Make a folder outline element.
|
|
folderOutline = parent.ownerDocument.createElement("outline");
|
|
folderOutline.setAttribute("title", folder.prettyName);
|
|
this.generatePPSpace(parent, indentString(indentLevel + 2));
|
|
|
|
if (folder.hasSubFolders)
|
|
{
|
|
FeedUtils.log.debug("generateOutlineStruct: has subfolders - " +
|
|
folder.name);
|
|
// Recurse.
|
|
this.generateOutlineStruct(folder, folderOutline, indentLevel + 2);
|
|
}
|
|
|
|
let feeds = this.getFeedsInFolder(folder);
|
|
for (let feed of feeds)
|
|
{
|
|
// Add feed outline elements with xmlUrls.
|
|
FeedUtils.log.debug("generateOutlineStruct: folder has FEED url - "+
|
|
folder.name + " : " + feed.url);
|
|
feedOutline = this.exportOPMLOutline(feed, parent.ownerDocument);
|
|
this.generatePPSpace(folderOutline, indentString(indentLevel + 4));
|
|
folderOutline.appendChild(feedOutline);
|
|
}
|
|
|
|
parent.appendChild(folderOutline);
|
|
}
|
|
},
|
|
|
|
exportOPMLOutline: function(aFeed, aDoc)
|
|
{
|
|
let outRv = aDoc.createElement("outline");
|
|
outRv.setAttribute("type", "rss");
|
|
outRv.setAttribute("title", aFeed.title);
|
|
outRv.setAttribute("text", aFeed.title);
|
|
outRv.setAttribute("version", "RSS");
|
|
outRv.setAttribute("fz:quickMode", aFeed.quickMode);
|
|
outRv.setAttribute("fz:options", JSON.stringify(aFeed.options));
|
|
outRv.setAttribute("xmlUrl", aFeed.url);
|
|
outRv.setAttribute("htmlUrl", aFeed.link);
|
|
return outRv;
|
|
},
|
|
|
|
importOPML: function()
|
|
{
|
|
// Account folder must be selected in subscribe dialog.
|
|
let item = this.mView ? this.mView.currentItem : null;
|
|
if (!item || !item.folder || !item.folder.isServer)
|
|
return;
|
|
|
|
let server = item.folder.server;
|
|
// Get file to open from filepicker.
|
|
let openFile = this.opmlPickOpenFile();
|
|
if (!openFile)
|
|
return;
|
|
|
|
this.mActionMode = this.kImportingOPML;
|
|
this.updateButtons(null);
|
|
this.selectFolder(item.folder, { select: false, open: true });
|
|
let statusReport = FeedUtils.strings.GetStringFromName("subscribe-loading");
|
|
this.updateStatusItem("statusText", statusReport);
|
|
// If there were a getElementsByAttribute in html, we could go determined...
|
|
this.updateStatusItem("progressMeter", "?");
|
|
|
|
if (!this.importOPMLFile(openFile, server, this.importOPMLFinished)) {
|
|
this.mActionMode = null;
|
|
this.updateButtons(item);
|
|
this.clearStatusInfo();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Import opml file into a feed account. Used by the Subscribe dialog and
|
|
* the Import wizard.
|
|
*
|
|
* @param nsILocalFile aFile - the opml file.
|
|
* @param nsIMsgIncomingServer aServer - the account server.
|
|
* @param func aCallback - callback function.
|
|
*
|
|
* @return bool - false if error.
|
|
*/
|
|
importOPMLFile: function(aFile, aServer, aCallback)
|
|
{
|
|
if (aServer && (aServer instanceof Ci.nsIMsgIncomingServer))
|
|
this.mRSSServer = aServer;
|
|
|
|
if (!aFile || !this.mRSSServer || !aCallback)
|
|
return false;
|
|
|
|
let opmlDom, statusReport;
|
|
let stream = Cc["@mozilla.org/network/file-input-stream;1"].
|
|
createInstance(Ci.nsIFileInputStream);
|
|
|
|
// Read in file as raw bytes, so Expat can do the decoding for us.
|
|
try {
|
|
stream.init(aFile, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
|
|
let parser = new DOMParser();
|
|
opmlDom = parser.parseFromStream(stream, null, stream.available(),
|
|
"application/xml");
|
|
}
|
|
catch(e) {
|
|
statusReport = FeedUtils.strings.GetStringFromName(
|
|
"subscribe-errorOpeningFile");
|
|
Services.prompt.alert(window, null, statusReport);
|
|
return false;
|
|
}
|
|
finally {
|
|
stream.close();
|
|
}
|
|
|
|
let body = opmlDom ? opmlDom.querySelector("body") : null;
|
|
|
|
// Return if the OPML file is invalid or empty.
|
|
if (!body || !body.childElementCount ||
|
|
opmlDom.documentElement.tagName != "opml")
|
|
{
|
|
statusReport = FeedUtils.strings.formatStringFromName(
|
|
"subscribe-OPMLImportInvalidFile", [aFile.leafName], 1);
|
|
Services.prompt.alert(window, null, statusReport);
|
|
return false;
|
|
}
|
|
|
|
this.importOPMLOutlines(body, this.mRSSServer, aCallback);
|
|
return true;
|
|
},
|
|
|
|
importOPMLOutlines: function(aBody, aRSSServer, aCallback)
|
|
{
|
|
let win = this;
|
|
let rssServer = aRSSServer;
|
|
let callback = aCallback;
|
|
let outline, feedFolder;
|
|
let badTag = false;
|
|
let firstFeedInFolderQuickMode = null;
|
|
let lastFolder;
|
|
let feedsAdded = 0;
|
|
let rssOutlines = 0;
|
|
let folderOutlines = 0;
|
|
|
|
function processor(aParentNode, aParentFolder)
|
|
{
|
|
FeedUtils.log.trace("importOPMLOutlines: PROCESSOR tag:name:childs - " +
|
|
aParentNode.tagName + ":" +
|
|
aParentNode.getAttribute("text") + ":" +
|
|
aParentNode.childElementCount);
|
|
while (true)
|
|
{
|
|
if (aParentNode.tagName == "body" && !aParentNode.childElementCount)
|
|
{
|
|
// Finished.
|
|
let statusReport = win.importOPMLStatus(feedsAdded, rssOutlines);
|
|
callback(statusReport, lastFolder, win);
|
|
return;
|
|
}
|
|
|
|
outline = aParentNode.firstElementChild;
|
|
if (outline.tagName != "outline")
|
|
{
|
|
FeedUtils.log.info("importOPMLOutlines: skipping, node is not an " +
|
|
"<outline> - <" + outline.tagName + ">");
|
|
badTag = true;
|
|
break;
|
|
}
|
|
|
|
let outlineName = outline.getAttribute("text") ||
|
|
outline.getAttribute("title") ||
|
|
outline.getAttribute("xmlUrl");
|
|
let feedUrl, folderURI;
|
|
|
|
if (outline.getAttribute("type") == "rss")
|
|
{
|
|
// A feed outline.
|
|
feedUrl = outline.getAttribute("xmlUrl") || outline.getAttribute("url");
|
|
if (!feedUrl)
|
|
{
|
|
FeedUtils.log.info("importOPMLOutlines: skipping, type=rss <outline> " +
|
|
"has no url - " + outlineName);
|
|
break;
|
|
}
|
|
|
|
rssOutlines++;
|
|
feedFolder = aParentFolder;
|
|
|
|
if (FeedUtils.feedAlreadyExists(feedUrl, rssServer))
|
|
{
|
|
FeedUtils.log.info("importOPMLOutlines: feed already subscribed in account " +
|
|
rssServer.prettyName + ", url - " + feedUrl);
|
|
break;
|
|
}
|
|
|
|
if (aParentNode.tagName == "outline" &&
|
|
aParentNode.getAttribute("type") != "rss")
|
|
// Parent is a folder, already created.
|
|
folderURI = feedFolder.URI;
|
|
else
|
|
{
|
|
// Parent is not a folder outline, likely the <body> in a flat list.
|
|
// Create feed's folder with feed's name and account rootFolder as
|
|
// parent of feed's folder.
|
|
// NOTE: Assume a type=rss outline must be a leaf and is not a
|
|
// direct parent of another type=rss outline; such a structure
|
|
// may lead to unintended nesting and inaccurate counts.
|
|
}
|
|
|
|
// Create the feed.
|
|
let quickMode = outline.hasAttribute("fz:quickMode") ?
|
|
outline.getAttribute("fz:quickMode") == "true" :
|
|
rssServer.getBoolValue("quickMode");
|
|
let options = outline.getAttribute("fz:options");
|
|
options = options ? JSON.parse(options) : null;
|
|
|
|
if (firstFeedInFolderQuickMode === null)
|
|
// The summary/web page pref applies to all feeds in a folder,
|
|
// though it is a property of an individual feed. This can be
|
|
// set (and is obvious) in the subscribe dialog; ensure import
|
|
// doesn't leave mismatches if mismatched in the opml file.
|
|
firstFeedInFolderQuickMode = quickMode;
|
|
else
|
|
quickMode = firstFeedInFolderQuickMode;
|
|
|
|
let feedProperties = { feedName : outlineName,
|
|
feedLocation : feedUrl,
|
|
server : rssServer,
|
|
folderURI : folderURI,
|
|
quickMode : quickMode,
|
|
options : options };
|
|
|
|
FeedUtils.log.info("importOPMLOutlines: importing feed: name, url - "+
|
|
outlineName + ", " + feedUrl);
|
|
|
|
let feed = win.storeFeed(feedProperties);
|
|
if (outline.hasAttribute("htmlUrl"))
|
|
feed.link = outline.getAttribute("htmlUrl");
|
|
|
|
feed.createFolder();
|
|
if (!feed.folder)
|
|
{
|
|
// Non success. Remove intermediate traces from the feeds database.
|
|
if (feed && feed.url && feed.server)
|
|
FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url),
|
|
feed.server,
|
|
feed.server.rootFolder);
|
|
FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" +
|
|
feed.folderName + "' from outlineName - '" +
|
|
outlineName + "' in parent folder " +
|
|
aParentFolder.filePath.path);
|
|
badTag = true;
|
|
break;
|
|
}
|
|
|
|
// Add the feed to the databases.
|
|
FeedUtils.addFeed(feed);
|
|
// Feed correctly added.
|
|
feedsAdded++;
|
|
lastFolder = feed.folder;
|
|
}
|
|
else
|
|
{
|
|
// A folder outline. If a folder exists in the account structure at
|
|
// the same level as in the opml structure, feeds are placed into the
|
|
// existing folder.
|
|
let defaultName = FeedUtils.strings.GetStringFromName("ImportFeedsNew");
|
|
let folderName = FeedUtils.getSanitizedFolderName(aParentFolder,
|
|
outlineName,
|
|
defaultName,
|
|
false);
|
|
try {
|
|
feedFolder = aParentFolder.getChildNamed(folderName);
|
|
}
|
|
catch (ex) {
|
|
// Folder not found, create it.
|
|
FeedUtils.log.info("importOPMLOutlines: creating folder - '" +
|
|
folderName + "' from outlineName - '" +
|
|
outlineName + "' in parent folder " +
|
|
aParentFolder.filePath.path);
|
|
firstFeedInFolderQuickMode = null;
|
|
try {
|
|
feedFolder = aParentFolder.QueryInterface(Ci.nsIMsgLocalMailFolder).
|
|
createLocalSubfolder(folderName);
|
|
folderOutlines++;
|
|
}
|
|
catch (ex) {
|
|
// An error creating. Skip it.
|
|
FeedUtils.log.info("importOPMLOutlines: skipping, error creating folder - '" +
|
|
folderName + "' from outlineName - '" +
|
|
outlineName + "' in parent folder " +
|
|
aParentFolder.filePath.path);
|
|
let xfolder = aParentFolder.getChildNamed(folderName);
|
|
aParentFolder.propagateDelete(xfolder, true, null);
|
|
badTag = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (!outline.childElementCount || badTag)
|
|
{
|
|
// Remove leaf nodes that are processed or bad tags from the opml dom,
|
|
// and go back to reparse. This method lets us use setTimeout to
|
|
// prevent UI hang, in situations of both deep and shallow trees.
|
|
// A yield/generator.next() method is fine for shallow trees, but not
|
|
// the true recursion required for deeper trees; both the shallow loop
|
|
// and the recurse should give it up.
|
|
outline.remove();
|
|
badTag = false;
|
|
outline = aBody;
|
|
feedFolder = rssServer.rootFolder;
|
|
}
|
|
|
|
setTimeout(function() {
|
|
processor(outline, feedFolder);
|
|
}, 0);
|
|
}
|
|
|
|
processor(aBody, rssServer.rootFolder);
|
|
},
|
|
|
|
importOPMLStatus: function(aFeedsAdded, aRssOutlines, aFolderOutlines)
|
|
{
|
|
let statusReport;
|
|
if (aRssOutlines > aFeedsAdded)
|
|
statusReport = FeedUtils.strings.formatStringFromName("subscribe-OPMLImportStatus",
|
|
[PluralForm.get(aFeedsAdded,
|
|
FeedUtils.strings.GetStringFromName("subscribe-OPMLImportUniqueFeeds"))
|
|
.replace("#1", aFeedsAdded),
|
|
PluralForm.get(aRssOutlines,
|
|
FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFoundFeeds"))
|
|
.replace("#1", aRssOutlines)], 2);
|
|
else
|
|
statusReport = PluralForm.get(aFeedsAdded,
|
|
FeedUtils.strings.GetStringFromName("subscribe-OPMLImportFeedCount"))
|
|
.replace("#1", aFeedsAdded);
|
|
|
|
return statusReport;
|
|
},
|
|
|
|
importOPMLFinished: function(aStatusReport, aLastFolder, aWin)
|
|
{
|
|
if (aLastFolder)
|
|
{
|
|
aWin.selectFolder(aLastFolder, { select: false, newFolder: aLastFolder });
|
|
aWin.selectFolder(aLastFolder.parent);
|
|
}
|
|
aWin.mActionMode = null;
|
|
aWin.updateButtons(aWin.mView.currentItem);
|
|
aWin.clearStatusInfo();
|
|
aWin.updateStatusItem("statusText", aStatusReport);
|
|
}
|
|
|
|
};
|