summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
authorglen@chromium.org <glen@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-02-19 22:25:22 +0000
committerglen@chromium.org <glen@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2009-02-19 22:25:22 +0000
commit12e14fcf977c5a4be6dd8e2778ac13382e2bf08a (patch)
treecba94d23134369e2db9174b152a635272450e0eb /chrome
parent0603139039825ae34f1c9f659827eb78dada2728 (diff)
downloadchromium_src-12e14fcf977c5a4be6dd8e2778ac13382e2bf08a.zip
chromium_src-12e14fcf977c5a4be6dd8e2778ac13382e2bf08a.tar.gz
chromium_src-12e14fcf977c5a4be6dd8e2778ac13382e2bf08a.tar.bz2
Initial checkin of the HTML downloads UI. It's not yet complete (lacking interface polish). But is enough for us to begin removing the native UI.ojan, please review downloads.htmlpaul, please review everything else
Review URL: http://codereview.chromium.org/20110 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@10050 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r--chrome/app/generated_resources.grd10
-rw-r--r--chrome/browser/browser.vcproj16
-rw-r--r--chrome/browser/browser_resources.grd1
-rw-r--r--chrome/browser/dom_ui/dom_ui.cc46
-rw-r--r--chrome/browser/dom_ui/dom_ui.h7
-rw-r--r--chrome/browser/dom_ui/dom_ui_contents.cc5
-rw-r--r--chrome/browser/dom_ui/downloads_ui.cc369
-rw-r--r--chrome/browser/dom_ui/downloads_ui.h123
-rw-r--r--chrome/browser/dom_ui/fileicon_source.cc63
-rw-r--r--chrome/browser/dom_ui/fileicon_source.h45
-rw-r--r--chrome/browser/resources/downloads.html551
11 files changed, 1234 insertions, 2 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 601fdc6..10a76e8 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -1230,6 +1230,12 @@ each locale. -->
<message name="IDS_DOWNLOAD_SEARCH_BUTTON" desc="Title of the button in the download page that triggers a search">
Search downloads
</message>
+ <message name="IDS_DOWNLOAD_NO_RESULTS" desc="Descriptive text when no results are found.">
+ No results
+ </message>
+ <message name="IDS_DOWNLOAD_SEARCHRESULTSFOR" desc="Format string for search results">
+ Search results for '<ph name="SEARCH_STRING">%s</ph>'
+ </message>
<message name="IDS_DOWNLOAD_LINK_RESUME"
desc="In the download view, 'Resume' link text">
Resume
@@ -3407,6 +3413,10 @@ each locale. -->
Search:
</message>
+ <message name="IDS_DEFAULT_FILENAME" desc="Default name for saved files when we have no idea what they could be.">
+ unknown
+ </message>
+
</messages>
</release>
</grit>
diff --git a/chrome/browser/browser.vcproj b/chrome/browser/browser.vcproj
index ff3a63e..11070f7 100644
--- a/chrome/browser/browser.vcproj
+++ b/chrome/browser/browser.vcproj
@@ -1522,6 +1522,22 @@
>
</File>
<File
+ RelativePath=".\dom_ui\downloads_ui.cc"
+ >
+ </File>
+ <File
+ RelativePath=".\dom_ui\downloads_ui.h"
+ >
+ </File>
+ <File
+ RelativePath=".\dom_ui\fileicon_source.cc"
+ >
+ </File>
+ <File
+ RelativePath=".\dom_ui\fileicon_source.h"
+ >
+ </File>
+ <File
RelativePath=".\dom_ui\history_ui.cc"
>
</File>
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index f41a269..232b88d 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -22,6 +22,7 @@
<include name="IDR_INCOGNITO_TAB_HTML" file="resources\incognito_tab.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_CREDITS_HTML" file="resources\about_credits.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_HISTORY_HTML" file="resources\history.html" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_DOWNLOADS_HTML" file="resources\downloads.html" flattenhtml="true" type="BINDATA" />
</includes>
</release>
</grit>
diff --git a/chrome/browser/dom_ui/dom_ui.cc b/chrome/browser/dom_ui/dom_ui.cc
index 1fe8d99..94116b3 100644
--- a/chrome/browser/dom_ui/dom_ui.cc
+++ b/chrome/browser/dom_ui/dom_ui.cc
@@ -48,6 +48,11 @@ void DOMUI::ProcessDOMUIMessage(const std::string& message,
callback->second->Run(value.get());
}
+void DOMUI::CallJavascriptFunction(const std::wstring& function_name) {
+ std::wstring javascript = function_name + L"();";
+ ExecuteJavascript(javascript);
+}
+
void DOMUI::CallJavascriptFunction(const std::wstring& function_name,
const Value& arg) {
std::string json;
@@ -123,4 +128,43 @@ void DOMMessageHandler::SetURLAndTitle(DictionaryValue* dictionary,
}
}
dictionary->SetString(L"title", title_to_set);
-} \ No newline at end of file
+}
+
+bool DOMMessageHandler::ExtractIntegerValue(const Value* value, int* out_int) {
+ if (value && value->GetType() == Value::TYPE_LIST) {
+ const ListValue* list_value = static_cast<const ListValue*>(value);
+ Value* list_member;
+
+ // Get id.
+ if (list_value->Get(0, &list_member) &&
+ list_member->GetType() == Value::TYPE_STRING) {
+ const StringValue* string_value =
+ static_cast<const StringValue*>(list_member);
+ std::wstring wstring_value;
+ string_value->GetAsString(&wstring_value);
+ *out_int = StringToInt(wstring_value);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+std::wstring DOMMessageHandler::ExtractStringValue(const Value* value) {
+ if (value && value->GetType() == Value::TYPE_LIST) {
+ const ListValue* list_value = static_cast<const ListValue*>(value);
+ Value* list_member;
+
+ // Get id.
+ if (list_value->Get(0, &list_member) &&
+ list_member->GetType() == Value::TYPE_STRING) {
+ const StringValue* string_value =
+ static_cast<const StringValue*>(list_member);
+ std::wstring wstring_value;
+ string_value->GetAsString(&wstring_value);
+ return wstring_value;
+ }
+ }
+ return std::wstring();
+}
+
diff --git a/chrome/browser/dom_ui/dom_ui.h b/chrome/browser/dom_ui/dom_ui.h
index 8b0286b..ff0f96f 100644
--- a/chrome/browser/dom_ui/dom_ui.h
+++ b/chrome/browser/dom_ui/dom_ui.h
@@ -35,6 +35,7 @@ class DOMUI {
// of the call, and should be thought of more like sending a message to
// the page.
// There are two function variants for one-arg and two-arg calls.
+ void CallJavascriptFunction(const std::wstring& function_name);
void CallJavascriptFunction(const std::wstring& function_name,
const Value& arg);
void CallJavascriptFunction(const std::wstring& function_name,
@@ -77,6 +78,12 @@ class DOMMessageHandler {
std::wstring title,
const GURL& gurl);
+ // Extract an integer value from a Value.
+ bool ExtractIntegerValue(const Value* value, int* out_int);
+
+ // Extract a string value from a Value.
+ std::wstring ExtractStringValue(const Value* value);
+
DOMUI* const dom_ui_;
private:
diff --git a/chrome/browser/dom_ui/dom_ui_contents.cc b/chrome/browser/dom_ui/dom_ui_contents.cc
index 4e0a3f2..89c35f2 100644
--- a/chrome/browser/dom_ui/dom_ui_contents.cc
+++ b/chrome/browser/dom_ui/dom_ui_contents.cc
@@ -6,6 +6,7 @@
#include "chrome/browser/dom_ui/dom_ui.h"
#include "chrome/browser/dom_ui/history_ui.h"
+#include "chrome/browser/dom_ui/downloads_ui.h"
#include "chrome/browser/renderer_host/render_view_host.h"
#include "chrome/browser/tab_contents/navigation_entry.h"
#include "chrome/common/resource_bundle.h"
@@ -192,7 +193,9 @@ bool DOMUIContents::NavigateToPendingEntry(bool reload) {
DOMUI* DOMUIContents::GetDOMUIForURL(const GURL &url) {
if (url.host() == HistoryUI::GetBaseURL().host())
return new HistoryUI(this);
-
+ else if (url.host() == DownloadsUI::GetBaseURL().host())
+ return new DownloadsUI(this);
+
return NULL;
}
diff --git a/chrome/browser/dom_ui/downloads_ui.cc b/chrome/browser/dom_ui/downloads_ui.cc
new file mode 100644
index 0000000..49a6726
--- /dev/null
+++ b/chrome/browser/dom_ui/downloads_ui.cc
@@ -0,0 +1,369 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/dom_ui/downloads_ui.h"
+
+#include "base/gfx/png_encoder.h"
+#include "base/string_piece.h"
+#include "base/thread.h"
+#include "base/time.h"
+#include "base/time_format.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/dom_ui/fileicon_source.h"
+#include "chrome/browser/download/download_util.h"
+#include "chrome/browser/metrics/user_metrics.h"
+#include "chrome/browser/profile.h"
+#include "chrome/common/jstemplate_builder.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/time_format.h"
+
+// Generated by GRIT
+#include "browser_resources.h"
+#include "generated_resources.h"
+
+using base::Time;
+
+// DownloadsUI is accessible from chrome-ui://downloads.
+static const char kDownloadsHost[] = "downloads";
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// DownloadsHTMLSource
+//
+///////////////////////////////////////////////////////////////////////////////
+
+DownloadsUIHTMLSource::DownloadsUIHTMLSource()
+ : DataSource(kDownloadsHost, MessageLoop::current()) {
+}
+
+void DownloadsUIHTMLSource::StartDataRequest(const std::string& path,
+ int request_id) {
+ DictionaryValue localized_strings;
+ localized_strings.SetString(L"title",
+ l10n_util::GetString(IDS_DOWNLOAD_TITLE));
+ localized_strings.SetString(L"searchbutton",
+ l10n_util::GetString(IDS_DOWNLOAD_SEARCH_BUTTON));
+ localized_strings.SetString(L"no_results",
+ l10n_util::GetString(IDS_DOWNLOAD_SEARCH_BUTTON));
+ localized_strings.SetString(L"searchresultsfor",
+ l10n_util::GetString(IDS_DOWNLOAD_SEARCHRESULTSFOR));
+ localized_strings.SetString(L"downloads",
+ l10n_util::GetString(IDS_DOWNLOAD_TITLE));
+
+ // Status.
+ localized_strings.SetString(L"status_cancelled",
+ l10n_util::GetString(IDS_DOWNLOAD_TAB_CANCELLED));
+ localized_strings.SetString(L"status_paused",
+ l10n_util::GetString(IDS_DOWNLOAD_PROGRESS_PAUSED));
+
+ // Dangerous file.
+ localized_strings.SetString(L"danger_desc",
+ l10n_util::GetStringF(IDS_PROMPT_DANGEROUS_DOWNLOAD, L"%s"));
+ localized_strings.SetString(L"danger_save",
+ l10n_util::GetString(IDS_SAVE_DOWNLOAD));
+ localized_strings.SetString(L"danger_discard",
+ l10n_util::GetString(IDS_DISCARD_DOWNLOAD));
+
+ // Controls.
+ localized_strings.SetString(L"control_pause",
+ l10n_util::GetString(IDS_DOWNLOAD_LINK_PAUSE));
+ localized_strings.SetString(L"control_showinfolder",
+ l10n_util::GetString(IDS_DOWNLOAD_LINK_SHOW));
+ localized_strings.SetString(L"control_cancel",
+ l10n_util::GetString(IDS_DOWNLOAD_LINK_CANCEL));
+ localized_strings.SetString(L"control_resume",
+ l10n_util::GetString(IDS_DOWNLOAD_LINK_RESUME));
+
+ static const StringPiece downloads_html(
+ ResourceBundle::GetSharedInstance().GetRawDataResource(
+ IDR_DOWNLOADS_HTML));
+ const std::string full_html = jstemplate_builder::GetTemplateHtml(
+ downloads_html, &localized_strings, "t");
+
+ scoped_refptr<RefCountedBytes> html_bytes(new RefCountedBytes);
+ html_bytes->data.resize(full_html.size());
+ std::copy(full_html.begin(), full_html.end(), html_bytes->data.begin());
+
+ SendResponse(request_id, html_bytes);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// DownloadsDOMHandler
+//
+///////////////////////////////////////////////////////////////////////////////
+
+// Sort DownloadItems into descending order by their start time.
+class DownloadItemSorter : public std::binary_function<DownloadItem*,
+ DownloadItem*,
+ bool> {
+ public:
+ bool operator()(const DownloadItem* lhs, const DownloadItem* rhs) {
+ return lhs->start_time() > rhs->start_time();
+ }
+};
+
+DownloadsDOMHandler::DownloadsDOMHandler(DOMUI* dom_ui, DownloadManager* dlm)
+ : DOMMessageHandler(dom_ui),
+ download_manager_(dlm),
+ search_text_() {
+ dom_ui_->RegisterMessageCallback("getDownloads",
+ NewCallback(this, &DownloadsDOMHandler::HandleGetDownloads));
+ dom_ui_->RegisterMessageCallback("openFile",
+ NewCallback(this, &DownloadsDOMHandler::HandleOpenFile));
+
+ dom_ui_->RegisterMessageCallback("drag",
+ NewCallback(this, &DownloadsDOMHandler::HandleDrag));
+
+ dom_ui_->RegisterMessageCallback("saveDangerous",
+ NewCallback(this, &DownloadsDOMHandler::HandleSaveDangerous));
+ dom_ui_->RegisterMessageCallback("discardDangerous",
+ NewCallback(this, &DownloadsDOMHandler::HandleDiscardDangerous));
+ dom_ui_->RegisterMessageCallback("show",
+ NewCallback(this, &DownloadsDOMHandler::HandleShow));
+ dom_ui_->RegisterMessageCallback("pause",
+ NewCallback(this, &DownloadsDOMHandler::HandlePause));
+ dom_ui_->RegisterMessageCallback("cancel",
+ NewCallback(this, &DownloadsDOMHandler::HandleCancel));
+
+ // Create our fileicon data source.
+ g_browser_process->io_thread()->message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(&chrome_url_data_manager,
+ &ChromeURLDataManager::AddDataSource,
+ new FileIconSource()));
+}
+
+DownloadsDOMHandler::~DownloadsDOMHandler() {
+ ClearDownloadItems();
+ download_manager_->RemoveObserver(this);
+}
+
+// DownloadsDOMHandler, public: -----------------------------------------------
+
+void DownloadsDOMHandler::Init() {
+ download_manager_->AddObserver(this);
+}
+
+void DownloadsDOMHandler::OnDownloadUpdated(DownloadItem* download) {
+ // Get the id for the download. Our downloads are sorted latest to first,
+ // and the id is the index into that list. We should be careful of sync
+ // errors between the UI and the download_items_ list (we may wish to use
+ // something other than 'id').
+ OrderedDownloads::iterator it = find(download_items_.begin(),
+ download_items_.end(),
+ download);
+ if (it == download_items_.end())
+ return;
+ const int id = static_cast<int>(it - download_items_.begin());
+
+ ListValue results_value;
+ results_value.Append(CreateDownloadItemValue(download, id));
+ dom_ui_->CallJavascriptFunction(L"downloadUpdated", results_value);
+}
+
+// A download has started or been deleted. Query our DownloadManager for the
+// current set of downloads, which will call us back in SetDownloads once it
+// has retrieved them.
+void DownloadsDOMHandler::ModelChanged() {
+ ClearDownloadItems();
+ download_manager_->GetDownloads(this, search_text_);
+}
+
+void DownloadsDOMHandler::SetDownloads(
+ std::vector<DownloadItem*>& downloads) {
+ ClearDownloadItems();
+
+ // Swap new downloads in.
+ download_items_.swap(downloads);
+ sort(download_items_.begin(), download_items_.end(), DownloadItemSorter());
+
+ // Scan for any in progress downloads and add ourself to them as an observer.
+ for (OrderedDownloads::iterator it = download_items_.begin();
+ it != download_items_.end(); ++it) {
+ DownloadItem* download = *it;
+ if (download->state() == DownloadItem::IN_PROGRESS) {
+ // We want to know what happens as the download progresses.
+ download->AddObserver(this);
+ } else if (download->safety_state() == DownloadItem::DANGEROUS) {
+ // We need to be notified when the user validates the dangerous download.
+ download->AddObserver(this);
+ }
+ }
+
+ SendCurrentDownloads();
+}
+
+void DownloadsDOMHandler::HandleGetDownloads(const Value* value) {
+ std::wstring new_search = ExtractStringValue(value);
+ if (search_text_.compare(new_search) != 0) {
+ search_text_ = new_search;
+ ClearDownloadItems();
+ download_manager_->GetDownloads(this, search_text_);
+ } else {
+ SendCurrentDownloads();
+ }
+}
+
+void DownloadsDOMHandler::HandleOpenFile(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file)
+ download_manager_->OpenDownloadInShell(file, NULL);
+}
+
+void DownloadsDOMHandler::HandleDrag(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file) {
+ IconManager* im = g_browser_process->icon_manager();
+ SkBitmap* icon = im->LookupIcon(file->full_path().ToWStringHack(),
+ IconLoader::NORMAL);
+ download_util::DragDownload(file, icon);
+ }
+}
+
+void DownloadsDOMHandler::HandleSaveDangerous(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file)
+ download_manager_->DangerousDownloadValidated(file);
+}
+
+void DownloadsDOMHandler::HandleDiscardDangerous(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file)
+ file->Remove(true);
+}
+
+void DownloadsDOMHandler::HandleShow(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file)
+ download_manager_->ShowDownloadInShell(file);
+}
+
+void DownloadsDOMHandler::HandlePause(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file)
+ file->TogglePause();
+}
+
+void DownloadsDOMHandler::HandleCancel(const Value* value) {
+ DownloadItem* file = GetDownloadByValue(value);
+ if (file)
+ file->Cancel(true);
+}
+
+// DownloadsDOMHandler, private: ----------------------------------------------
+
+void DownloadsDOMHandler::SendCurrentDownloads() {
+ ListValue results_value;
+
+ for (OrderedDownloads::iterator it = download_items_.begin();
+ it != download_items_.end(); ++it) {
+ results_value.Append(CreateDownloadItemValue(*it,
+ static_cast<int>(it - download_items_.begin())));
+ }
+
+ dom_ui_->CallJavascriptFunction(L"downloadsList", results_value);
+}
+
+DictionaryValue* DownloadsDOMHandler::CreateDownloadItemValue(
+ DownloadItem* download, int id) {
+ DictionaryValue* file_value = new DictionaryValue();
+
+ file_value->SetInteger(L"time",
+ static_cast<int>(download->start_time().ToTimeT()));
+ file_value->SetInteger(L"id", id);
+ file_value->SetString(L"file_path", download->full_path().ToWStringHack());
+ file_value->SetString(L"file_name", download->GetFileName().ToWStringHack());
+ file_value->SetString(L"url", download->url().spec());
+
+ if (download->state() == DownloadItem::IN_PROGRESS) {
+ if (download->safety_state() == DownloadItem::DANGEROUS) {
+ file_value->SetString(L"state", L"DANGEROUS");
+ } else if (download->is_paused()) {
+ file_value->SetString(L"state", L"PAUSED");
+ } else {
+ file_value->SetString(L"state", L"IN_PROGRESS");
+ }
+ file_value->SetInteger(L"speed",
+ static_cast<int>(download->CurrentSpeed()));
+ file_value->SetInteger(L"percent",
+ static_cast<int>(download->PercentComplete()));
+ file_value->SetInteger(L"received",
+ static_cast<int>(download->received_bytes()));
+ } else if (download->state() == DownloadItem::CANCELLED) {
+ file_value->SetString(L"state", L"CANCELLED");
+ } else if (download->state() == DownloadItem::COMPLETE) {
+ if (download->safety_state() == DownloadItem::DANGEROUS) {
+ file_value->SetString(L"state", L"DANGEROUS");
+ } else {
+ file_value->SetString(L"state", L"COMPLETE");
+ }
+ }
+
+ file_value->SetInteger(L"total",
+ static_cast<int>(download->total_bytes()));
+
+ return file_value;
+}
+
+void DownloadsDOMHandler::ClearDownloadItems() {
+ // Clear out old state and remove self as observer for each download.
+ for (OrderedDownloads::iterator it = download_items_.begin();
+ it != download_items_.end(); ++it) {
+ (*it)->RemoveObserver(this);
+ }
+ download_items_.clear();
+}
+
+DownloadItem* DownloadsDOMHandler::GetDownloadById(int id) {
+ for (OrderedDownloads::iterator it = download_items_.begin();
+ it != download_items_.end(); ++it) {
+ if (static_cast<int>(it - download_items_.begin() == id)) {
+ return (*it);
+ }
+ }
+
+ return NULL;
+}
+
+DownloadItem* DownloadsDOMHandler::GetDownloadByValue(const Value* value) {
+ int id;
+ if (ExtractIntegerValue(value, &id)) {
+ return GetDownloadById(id);
+ }
+ return NULL;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//
+// DownloadsUI
+//
+///////////////////////////////////////////////////////////////////////////////
+
+DownloadsUI::DownloadsUI(DOMUIContents* contents) : DOMUI(contents) {
+}
+
+void DownloadsUI::Init() {
+ DownloadManager* dlm = get_profile()->GetDownloadManager();
+
+ DownloadsDOMHandler* handler = new DownloadsDOMHandler(this, dlm);
+ AddMessageHandler(handler);
+ handler->Init();
+
+ DownloadsUIHTMLSource* html_source = new DownloadsUIHTMLSource();
+
+ // Set up the chrome-ui://downloads/ source.
+ g_browser_process->io_thread()->message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(&chrome_url_data_manager,
+ &ChromeURLDataManager::AddDataSource,
+ html_source));
+}
+
+// static
+GURL DownloadsUI::GetBaseURL() {
+ std::string url = DOMUIContents::GetScheme();
+ url += "://";
+ url += kDownloadsHost;
+ return GURL(url);
+}
+
diff --git a/chrome/browser/dom_ui/downloads_ui.h b/chrome/browser/dom_ui/downloads_ui.h
new file mode 100644
index 0000000..9209000
--- /dev/null
+++ b/chrome/browser/dom_ui/downloads_ui.h
@@ -0,0 +1,123 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_DOM_UI_DOWNLOADS_UI_H_
+#define CHROME_BROWSER_DOM_UI_DOWNLOADS_UI_H_
+
+#include "chrome/browser/dom_ui/chrome_url_data_manager.h"
+#include "chrome/browser/dom_ui/dom_ui.h"
+#include "chrome/browser/dom_ui/dom_ui_contents.h"
+#include "chrome/browser/download/download_manager.h"
+
+class GURL;
+
+class DownloadsUIHTMLSource : public ChromeURLDataManager::DataSource {
+ public:
+ DownloadsUIHTMLSource();
+
+ // Called when the network layer has requested a resource underneath
+ // the path we registered.
+ virtual void StartDataRequest(const std::string& path, int request_id);
+ virtual std::string GetMimeType(const std::string&) const {
+ return "text/html";
+ }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(DownloadsUIHTMLSource);
+};
+
+// The handler for Javascript messages related to the "downloads" view,
+// also observes changes to the download manager.
+class DownloadsDOMHandler : public DOMMessageHandler,
+ public DownloadManager::Observer,
+ public DownloadItem::Observer {
+ public:
+ explicit DownloadsDOMHandler(DOMUI* dom_ui, DownloadManager* dlm);
+ virtual ~DownloadsDOMHandler();
+
+ void Init();
+
+ // DownloadItem::Observer interface
+ virtual void OnDownloadUpdated(DownloadItem* download);
+
+ // DownloadManager::Observer interface
+ virtual void ModelChanged();
+ virtual void SetDownloads(std::vector<DownloadItem*>& downloads);
+
+ // Callback for the "getDownloads" message.
+ void HandleGetDownloads(const Value* value);
+
+ // Callback for the "openFile" message - opens the file in the shell.
+ void HandleOpenFile(const Value* value);
+
+ // Callback for the "drag" message - initiates a file object drag.
+ void HandleDrag(const Value* value);
+
+ // Callback for the "saveDangerous" message - specifies that the user
+ // wishes to save a dangerous file.
+ void HandleSaveDangerous(const Value* value);
+
+ // Callback for the "discardDangerous" message - specifies that the user
+ // wishes to discard (remove) a dangerous file.
+ void HandleDiscardDangerous(const Value* value);
+
+ // Callback for the "show" message - shows the file in explorer.
+ void HandleShow(const Value* value);
+
+ // Callback for the "pause" message - pauses the file download.
+ void HandlePause(const Value* value);
+
+ // Callback for the "cancel" message - cancels the download.
+ void HandleCancel(const Value* value);
+
+ private:
+ // Send the current list of downloads to the page.
+ void SendCurrentDownloads();
+
+ // Creates a representation of a download in a format that the downloads
+ // HTML page can understand.
+ DictionaryValue* CreateDownloadItemValue(DownloadItem* download, int id);
+
+ // Clear all download items and their observers.
+ void ClearDownloadItems();
+
+ // Return the download that corresponds to a given id.
+ DownloadItem* GetDownloadById(int id);
+
+ // Return the download that is referred to in a given value.
+ DownloadItem* GetDownloadByValue(const Value* value);
+
+ // Current search text.
+ std::wstring search_text_;
+
+ // Our model
+ DownloadManager* download_manager_;
+
+ // The current set of visible DownloadItems for this view received from the
+ // DownloadManager. DownloadManager owns the DownloadItems. The vector is
+ // kept in order, sorted by ascending start time.
+ typedef std::vector<DownloadItem*> OrderedDownloads;
+ OrderedDownloads download_items_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadsDOMHandler);
+};
+
+class DownloadsUI : public DOMUI {
+ public:
+ explicit DownloadsUI(DOMUIContents* contents);
+
+ // Return the URL for the front page of this UI.
+ static GURL GetBaseURL();
+
+ // DOMUI Implementation
+ virtual void Init();
+
+ private:
+ DOMUIContents* contents_;
+
+ DISALLOW_COPY_AND_ASSIGN(DownloadsUI);
+};
+
+#endif // CHROME_BROWSER_DOM_UI_DOWNLOADS_UI_H_
+
diff --git a/chrome/browser/dom_ui/fileicon_source.cc b/chrome/browser/dom_ui/fileicon_source.cc
new file mode 100644
index 0000000..36fb30f
--- /dev/null
+++ b/chrome/browser/dom_ui/fileicon_source.cc
@@ -0,0 +1,63 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/dom_ui/fileicon_source.h"
+
+#include "base/gfx/png_encoder.h"
+#include "base/string_util.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/common/time_format.h"
+
+#include "generated_resources.h"
+
+// The path used in internal URLs to file icon data.
+static const char kFileIconPath[] = "fileicon";
+
+FileIconSource::FileIconSource()
+ : DataSource(kFileIconPath, MessageLoop::current()) {}
+
+FileIconSource::~FileIconSource() {
+ cancelable_consumer_.CancelAllRequests();
+}
+
+void FileIconSource::StartDataRequest(const std::string& path,
+ int request_id) {
+ IconManager* im = g_browser_process->icon_manager();
+
+ // Fast look up.
+ SkBitmap* icon = im->LookupIcon(UTF8ToWide(path), IconLoader::NORMAL);
+
+ if (icon) {
+ std::vector<unsigned char> png_bytes;
+ PNGEncoder::EncodeBGRASkBitmap(*icon, false, &png_bytes);
+
+ scoped_refptr<RefCountedBytes> icon_data = new RefCountedBytes(png_bytes);
+ SendResponse(request_id, icon_data);
+ } else {
+ // Icon was not in cache, go fetch it slowly.
+ IconManager::Handle h = im->LoadIcon(UTF8ToWide(path), IconLoader::NORMAL,
+ &cancelable_consumer_,
+ NewCallback(this, &FileIconSource::OnFileIconDataAvailable));
+
+ // Attach the ChromeURLDataManager request ID to the history request.
+ cancelable_consumer_.SetClientData(im, h, request_id);
+ }
+}
+
+void FileIconSource::OnFileIconDataAvailable(IconManager::Handle handle,
+ SkBitmap* icon) {
+ IconManager* im = g_browser_process->icon_manager();
+ int request_id = cancelable_consumer_.GetClientData(im, handle);
+
+ if (icon) {
+ std::vector<unsigned char> png_bytes;
+ PNGEncoder::EncodeBGRASkBitmap(*icon, false, &png_bytes);
+
+ scoped_refptr<RefCountedBytes> icon_data = new RefCountedBytes(png_bytes);
+ SendResponse(request_id, icon_data);
+ } else {
+ // TODO(glen): send a dummy icon.
+ SendResponse(request_id, NULL);
+ }
+} \ No newline at end of file
diff --git a/chrome/browser/dom_ui/fileicon_source.h b/chrome/browser/dom_ui/fileicon_source.h
new file mode 100644
index 0000000..e56996c
--- /dev/null
+++ b/chrome/browser/dom_ui/fileicon_source.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_DOM_UI_FILEICON_SOURCE_H_
+#define CHROME_BROWSER_DOM_UI_FILEICON_SOURCE_H_
+
+#include "chrome/browser/dom_ui/chrome_url_data_manager.h"
+#include "chrome/browser/icon_manager.h"
+#include "chrome/common/resource_bundle.h"
+
+class GURL;
+
+// FileIconSource is the gateway between network-level chrome:
+// requests for favicons and the history backend that serves these.
+class FileIconSource : public ChromeURLDataManager::DataSource {
+ public:
+ explicit FileIconSource();
+ virtual ~FileIconSource();
+
+ // Called when the network layer has requested a resource underneath
+ // the path we registered.
+ virtual void StartDataRequest(const std::string& path, int request_id);
+
+ virtual std::string GetMimeType(const std::string&) const {
+ // Rely on image decoder inferring the correct type.
+ return std::string();
+ }
+
+ // Called when favicon data is available from the history backend.
+ void OnFileIconDataAvailable(
+ IconManager::Handle request_handle,
+ SkBitmap* icon);
+
+ private:
+ CancelableRequestConsumerT<int, 0> cancelable_consumer_;
+
+ // Raw PNG representation of the favicon to show when the favicon
+ // database doesn't have a favicon for a webpage.
+ scoped_refptr<RefCountedBytes> default_favicon_;
+
+ DISALLOW_COPY_AND_ASSIGN(FileIconSource);
+};
+#endif // CHROME_BROWSER_DOM_UI_FILEICON_SOURCE_H_
+
diff --git a/chrome/browser/resources/downloads.html b/chrome/browser/resources/downloads.html
new file mode 100644
index 0000000..5c38db8
--- /dev/null
+++ b/chrome/browser/resources/downloads.html
@@ -0,0 +1,551 @@
+<!DOCTYPE HTML>
+<html id="t">
+<head>
+<meta charset="utf-8">
+<title jscontent="title"></title>
+<style type="text/css">
+body {
+ font-family:arial;
+ background-color:white;
+ color:black;
+ font-size:84%;
+ margin:10px;
+}
+.header {
+ overflow:auto;
+ clear:both;
+}
+.header .logo {
+ float:left;
+}
+.header .form {
+ float:left;
+ margin-top:22px;
+ margin-left:12px;
+}
+#downloads-summary {
+ margin-top:12px;
+ border-top:1px solid #9cc2ef;
+ background-color:#ebeff9;
+ font-weight:bold;
+ padding:3px;
+ margin-bottom:6px;
+}
+#downloads-display {
+ max-width:740px;
+}
+.download {
+ position:relative;
+ padding-left:45px;
+ margin-bottom:16px;
+}
+.download .icon {
+ position:absolute;
+ top:2px;
+ left:2px;
+ width:32px;
+ height:32px;
+}
+.name {
+ display:none;
+ padding-right:16px;
+ max-width:450px;
+ text-overflow:ellipsis;
+}
+.download .status {
+ display:inline;
+ color:#999;
+}
+.download .url {
+ color:#080;
+ width:500px;
+ white-space:nowrap;
+ overflow:hidden;
+ text-overflow:ellipsis;
+}
+.controls a {
+ color:#777;
+}
+#downloads-pagination {
+ padding-top:24px;
+ margin-left:18px;
+}
+.page-navigation {
+ padding:8px;
+ background-color:#ebeff9;
+ margin-right:4px;
+}
+.footer {
+ height:24px;
+}
+</style>
+<script type="text/javascript">
+///////////////////////////////////////////////////////////////////////////////
+// localStrings:
+/**
+ * We get strings into the page by using JSTemplate to populate some elements
+ * with localized content, then reading the content of those elements into
+ * this global strings object.
+ * @param {Node} node The DOM node containing all our strings.
+ */
+function LocalStrings(node) {
+ this.strings_ = {};
+
+ var children = node.childNodes;
+ for (var i = 0, child; child = children[i]; i++) {
+ var id = child.id;
+ if (id) {
+ this.strings_[id] = child.innerHTML;
+ }
+ }
+}
+
+/**
+ * Gets a localized string by its id.
+ * @param {string} s The id of the string we want
+ * @return {string} The localized string
+ */
+LocalStrings.prototype.getString = function(s) {
+ return (s in this.strings_) ? this.strings_[s] : '';
+}
+
+/**
+ * Returns a formatted localized string (where all %s contents are replaced
+ * by the second argument).
+ * @param {string} s The id of the string we want
+ * @param {string} d The string to include in the formatted string
+ * @return {string} The formatted string.
+ */
+LocalStrings.prototype.formatString = function(s, d) {
+ return (s in this.strings_) ? this.strings_[s].replace(/\%s/, d) : '';
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Helper functions
+function $(o) {return document.getElementById(o);}
+
+function bind(fn, selfObj, var_args) {
+ var boundArgs = Array.prototype.slice.call(arguments, 2);
+ return function() {
+ var args = Array.prototype.slice.call(arguments);
+ args.unshift.apply(args, boundArgs);
+ return fn.apply(selfObj, args);
+ }
+}
+
+/**
+ * Sets the display style of a node.
+ */
+function showInline(node, isShow) {
+ node.style.display = isShow ? 'inline' : 'none';
+}
+
+/**
+ * Creates an element of a specified type with a specified class name.
+ * @param {String} type The node type.
+ * @param {String} className The class name to use.
+ */
+function createElementWithClassName(type, className) {
+ var elm = document.createElement(type);
+ elm.className = className;
+ return elm;
+}
+
+/**
+ * Creates a link with a specified onclick handler and content
+ * @param {String} onclick The onclick handler
+ * @param {String} value The link text
+ */
+function createLink(onclick, value) {
+ var link = document.createElement("a");
+ link.onclick = onclick;
+ link.href = '#';
+ link.innerHTML = value;
+ return link;
+}
+
+/**
+ * Creates a button with a specified onclick handler and content
+ * @param {String} onclick The onclick handler
+ * @param {String} value The button text
+ */
+function createButton(onclick, value) {
+ var button = document.createElement("input");
+ button.type = 'button';
+ button.value = value;
+ button.onclick = onclick;
+ return button;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Downloads
+/**
+ * Class to hold all the information about the visible downloads.
+ */
+function Downloads() {
+ this.downloads_ = {};
+ this.node_ = $('downloads-display');
+ this.summary_ = $('downloads-summary');
+ this.searchText_ = "";
+}
+
+/**
+ * Called when a download has been updated or added.
+ * @param {Object} download A backend download object (see downloads_ui.cc)
+ */
+Downloads.prototype.updated = function(download) {
+ var id = download.id;
+ if (!!this.downloads_[id]) {
+ this.downloads_[id].update(download);
+ } else {
+ this.downloads_[id] = new Download(download);
+ // We get downloads in display order, so we don't have to worry about
+ // maintaining correct order for now.
+ this.node_.insertBefore(this.downloads_[id].node,
+ this.node_.firstChild);
+ }
+}
+
+/**
+ * Set our display search text.
+ * @param {String} searchText The string we're searching for.
+ */
+Downloads.prototype.setSearchText = function(searchText) {
+ this.searchText_ = searchText;
+}
+
+/**
+ * Update the summary block above the results
+ */
+Downloads.prototype.updateSummary = function() {
+ if (this.searchText_) {
+ this.summary_.innerHTML = localStrings.formatString(
+ 'searchresultsfor', this.searchText_);
+ } else {
+ this.summary_.innerHTML = localStrings.getString('downloads');
+ }
+
+ var hasDownloads = false;
+ for (var i in this.downloads_) {
+ hasDownloads = true;
+ break;
+ }
+
+ if (!hasDownloads) {
+ this.node_.innerHTML = localStrings.getString('noresults');
+ }
+}
+
+/**
+ * Remove a download.
+ * @param {Number} id The id of the download to remove.
+ */
+Downloads.prototype.remove = function(id) {
+ this.node_.removeChild(this.downloads_[id].node);
+ delete this.downloads_[id];
+}
+
+/**
+ * Clear all downloads and reset us back to a null state.
+ */
+Downloads.prototype.clear = function() {
+ for (var id in this.downloads_) {
+ this.downloads_[id].clear();
+ this.remove(id);
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Download
+/**
+ * A download and the DOM representation for that download.
+ * @param {Object} download A backend download object (see downloads_ui.cc)
+ */
+function Download(download) {
+ // Create DOM
+ this.node = createElementWithClassName('div', 'download');
+
+ // Container for all 'safe download' UI.
+ this.safe_ = document.createElement("div");
+ this.safe_.ondragstart = bind(this.drag_, this);
+ this.node.appendChild(this.safe_);
+
+ this.nodeImg_ = createElementWithClassName('img', 'icon');
+ this.safe_.appendChild(this.nodeImg_);
+
+ // FileLink is used for completed downloads, otherwise we show FileName.
+ this.nodeTitleArea_ = createElementWithClassName('div', 'title-area');
+ this.safe_.appendChild(this.nodeTitleArea_);
+
+ this.nodeFileLink_ = createLink(bind(this.openFile_, this), '');
+ this.nodeFileLink_.className = 'name';
+ this.nodeFileLink_.style.display = 'none';
+ this.nodeTitleArea_.appendChild(this.nodeFileLink_);
+
+ this.nodeFileName_ = createElementWithClassName('span', 'name');
+ this.nodeFileName_.style.display = 'none';
+ this.nodeTitleArea_.appendChild(this.nodeFileName_);
+
+ this.nodeStatus_ = createElementWithClassName('span', 'status');
+ this.nodeTitleArea_.appendChild(this.nodeStatus_);
+
+ this.nodeURL_ = createElementWithClassName('div', 'url');
+ this.safe_.appendChild(this.nodeURL_);
+
+ // Controls.
+ this.nodeControls_ = createElementWithClassName('div', 'controls');
+ this.safe_.appendChild(this.nodeControls_);
+
+ this.controlShow_ = createLink(bind(this.show_, this),
+ localStrings.getString('control_showinfolder'));
+ this.nodeControls_.appendChild(this.controlShow_);
+
+ this.controlPause_ = createLink(bind(this.pause_, this),
+ localStrings.getString('control_pause'));
+ this.nodeControls_.appendChild(this.controlPause_);
+
+ this.controlCancel_ = createLink(bind(this.cancel_, this),
+ localStrings.getString('control_cancel'));
+ this.nodeControls_.appendChild(this.controlCancel_);
+
+ // Container for 'unsafe download' UI.
+ this.danger_ = createElementWithClassName('div', 'show-dangerous');
+ this.node.appendChild(this.danger_);
+
+ this.dangerDesc_ = document.createElement("div");
+ this.danger_.appendChild(this.dangerDesc_);
+
+ this.dangerSave_ = createButton(bind(this.saveDangerous_, this),
+ localStrings.getString("danger_save"));
+ this.danger_.appendChild(this.dangerSave_);
+
+ this.dangerDiscard_ = createButton(bind(this.discardDangerous_, this),
+ localStrings.getString("danger_discard"));
+ this.danger_.appendChild(this.dangerDiscard_);
+
+ // Update member vars.
+ this.update(download);
+}
+
+/**
+ * The states a download can be in. These correspond to states defined in
+ * DownloadsDOMHandler::CreateDownloadItemValue
+ */
+Download.States = {
+ IN_PROGRESS : "IN_PROGRESS",
+ CANCELLED : "CANCELLED",
+ COMPLETE : "COMPLETE",
+ PAUSED : "PAUSED",
+ DANGEROUS : "DANGEROUS",
+}
+
+/**
+ * Updates the download to reflect new data.
+ * @param {Object} download A backend download object (see downloads_ui.cc)
+ */
+Download.prototype.update = function(download) {
+ this.id_ = download.id;
+ this.filePath_ = download.file_path;
+ this.fileName_ = download.file_name;
+ this.url_ = download.url;
+ this.state_ = download.state;
+ this.percent_ = download.percent;
+ this.speed_ = download.speed;
+ this.received_ = download.received;
+
+ if (this.state_ == Download.States.DANGEROUS) {
+ this.dangerDesc_.innerHTML = localStrings.formatString('danger_desc',
+ this.fileName_);
+ this.danger_.style.display = 'block';
+ this.safe_.style.display = 'none';
+ } else {
+ this.nodeImg_.src = 'chrome-ui://fileicon/' + this.filePath_;
+
+ if (this.state_ == Download.States.COMPLETE) {
+ this.nodeFileLink_.innerHTML = this.fileName_;
+ this.nodeFileLink_.href = this.filePath_;
+ } else {
+ this.nodeFileName_.innerHTML = this.fileName_;
+ }
+
+ showInline(this.nodeFileLink_, this.state_ == Download.States.COMPLETE);
+ showInline(this.nodeFileName_, this.state_ != Download.States.COMPLETE);
+
+ showInline(this.controlShow_, this.state_ == Download.States.COMPLETE);
+ showInline(this.controlPause_, this.state_ == Download.States.IN_PROGRESS);
+ showInline(this.controlCancel_, this.state_ == Download.States.IN_PROGRESS);
+
+ this.nodeURL_.innerHTML = this.url_;
+ this.nodeStatus_.innerHTML = this.getStatusText_();
+
+ this.danger_.style.display = 'none';
+ this.safe_.style.display = 'block';
+ }
+}
+
+/**
+ * Removes applicable bits from the DOM in preparation for deletion.
+ */
+Download.prototype.clear = function() {
+ this.safe_.ondragstart = null;
+ this.nodeFileLink_.onclick = null;
+ this.controlShow_.onclick = null;
+ this.controlCancel_.onclick = null;
+ this.controlPause_.onclick = null;
+ this.dangerDiscard_.onclick = null;
+
+ this.node.innerHTML = '';
+}
+
+/**
+ * @return {String} User-visible status update text.
+ */
+Download.prototype.getStatusText_ = function() {
+ switch (this.state_) {
+ case Download.States.IN_PROGRESS:
+ // TODO(glen): Make pretty, localize, etc.
+ return this.percent_ + '% at ' + this.speed_;
+ case Download.States.CANCELLED:
+ return localStrings.getString('status_cancelled');
+ case Download.States.PAUSED:
+ return localStrings.getString('status_paused');
+ case Download.States.DANGEROUS:
+ return localStrings.getString('danger_desc');
+ case Download.States.COMPLETE:
+ return "";
+ }
+}
+
+/**
+ * Tells the backend to initiate a drag, allowing users to drag
+ * files from the download page and have them appear as native file
+ * drags.
+ */
+Download.prototype.drag_ = function() {
+ chrome.send("drag", [this.id_.toString()]);
+ return false;
+}
+
+/**
+ * Tells the backend to open this file.
+ */
+Download.prototype.openFile_ = function() {
+ chrome.send("openFile", [this.id_.toString()]);
+ return false;
+}
+
+/**
+ * Tells the backend that the user chose to save a dangerous file.
+ */
+Download.prototype.saveDangerous_ = function() {
+ chrome.send("saveDangerous", [this.id_.toString()]);
+ return false;
+}
+
+/**
+ * Tells the backend that the user chose to discard a dangerous file.
+ */
+Download.prototype.discardDangerous_ = function() {
+ chrome.send("discardDangerous", [this.id_.toString()]);
+ downloads.remove(this.id_);
+ return false;
+}
+
+/**
+ * Tells the backend to show the file in explorer.
+ */
+Download.prototype.show_ = function() {
+ chrome.send("show", [this.id_.toString()]);
+ return false;
+}
+
+/**
+ * Tells the backend to pause this download.
+ */
+Download.prototype.pause_ = function() {
+ chrome.send("pause", [this.id_.toString()]);
+ return false;
+}
+
+/**
+ * Tells the backend to cancel this download.
+ */
+Download.prototype.cancel_ = function() {
+ chrome.send("cancel", [this.id_.toString()]);
+ return false;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Page:
+var downloads, localStrings;
+
+function load() {
+ localStrings = new LocalStrings($('l10n'));
+ downloads = new Downloads();
+ setSearch("");
+}
+
+function setSearch(searchText) {
+ downloads.clear();
+ downloads.setSearchText(searchText);
+ chrome.send("getDownloads", [searchText.toString()]);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Chrome callbacks:
+/**
+ * Our history system calls this function with results from searches or when
+ * downloads are added or removed.
+ */
+function downloadsList(results) {
+ downloadUpdated(results);
+ downloads.updateSummary();
+}
+
+/**
+ * When a download is updated (progress, state change), this is called.
+ */
+function downloadUpdated(results) {
+ for (var i = 0, thisResult; thisResult = results[i]; i++) {
+ downloads.updated(thisResult);
+ }
+}
+</script>
+</head>
+<body onload="load();">
+<div class="header">
+ <a href="" onclick="setSearch(''); return false;">
+ <img src="../../app/theme/downloads_section.png"
+ width="67" height="67" class="logo" border="0" /></a>
+ <form method="post" action=""
+ onsubmit="setSearch(this.term.value); return false;"
+ class="form">
+ <input type="text" name="term" id="term" />
+ <input type="submit" name="submit" jsvalues="value:searchbutton" />
+ </form>
+</div>
+<div class="main">
+ <div id="downloads-summary"></div>
+ <div id="downloads-display"></div>
+</div>
+<div class="footer">
+</div>
+<div id="l10n" style="display:none;">
+ <span id="searchresultsfor" jscontent="searchresultsfor">Search results for '%s'</span>
+ <span id="no_results" jscontent="no_results">No results found.</span>
+ <span id="downloads" jscontent="downloads">Downloads</span>
+
+ <span id="status_cancelled" jscontent="status_cancelled">Cancelled</span>
+ <span id="status_paused" jscontent="status_paused">Paused</span>
+
+ <span id="danger_desc" jscontent="danger_desc">This is a dangerous file</span>
+ <span id="danger_save" jscontent="danger_save">Save</span>
+ <span id="danger_discard" jscontent="danger_discard">Discard</span>
+
+ <span id="control_pause" jscontent="control_pause">Pause</span>
+ <span id="control_showinfolder" jscontent="control_showinfolder">Show in folder</span>
+ <span id="control_cancel" jscontent="control_cancel">Cancel</span>
+ <span id="control_resume" jscontent="control_resume">Resume</span>
+</div>
+</body>
+</html> \ No newline at end of file