Skip to content
This repository has been archived by the owner on Apr 2, 2022. It is now read-only.

Implement downloads #201

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
645a844
Draft downloads UI.
alcinnz Oct 15, 2019
63f5f75
Integrate and fix downloads button.
alcinnz Oct 16, 2019
d1032d9
Fix download click handling.
alcinnz Oct 18, 2019
70fc006
Fix 'Show in folder' button.
alcinnz Oct 18, 2019
0b9ba67
Merge branch 'master' into master
cassidyjames Oct 18, 2019
6d06bd3
Merge branch 'master' into master
cassidyjames Oct 18, 2019
c266dd0
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
8c6eece
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
efee568
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
8f8607f
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
9c770a2
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
144cede
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
b777d07
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
43d95d9
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
37a5c55
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
55ec83f
Update src/MainWindow.vala
alcinnz Oct 18, 2019
ea467c9
Update src/MainWindow.vala
alcinnz Oct 18, 2019
8790da9
Update src/MainWindow.vala
alcinnz Oct 18, 2019
5cadfc7
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
b60300b
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
a918ec7
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
d6d6b1e
Update src/Widgets/DownloadsButton.vala
alcinnz Oct 18, 2019
9694abb
Write namespace in Downloads class names, for consistancy.
alcinnz Oct 18, 2019
c98e482
Codestyle fix (space before function parens).
alcinnz Oct 21, 2019
3cbf1e9
Merge branch 'master' into master
cassidyjames Feb 17, 2020
09b6e4c
Merge branch 'master' into master
cassidyjames Apr 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ executable(
join_paths('src', 'Views', 'ErrorView.vala'),
join_paths('src', 'Views', 'WelcomeView.vala'),
join_paths('src', 'Widgets', 'BrowserButton.vala'),
join_paths('src', 'Widgets', 'DownloadsButton.vala'),
join_paths('src', 'Widgets', 'FindBar.vala'),
join_paths('src', 'Widgets', 'UrlEntry.vala'),
join_paths('src', 'Widgets', 'WebView.vala'),
Expand Down
38 changes: 37 additions & 1 deletion src/MainWindow.vala
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ public class Ephemeral.MainWindow : Gtk.Window {
private Gtk.Button zoom_default_button;
private Gtk.Stack stack;
private WebView web_view;
private Gtk.Overlay web_overlay;
private FindBar find_bar;
private Gtk.Stack refresh_stop_stack;
private Gtk.Button back_button;
private DownloadsButton downloads_button;
private Gtk.Button forward_button;
private Gtk.Button refresh_button;
private Gtk.Button stop_button;
Expand Down Expand Up @@ -62,7 +64,7 @@ public class Ephemeral.MainWindow : Gtk.Window {
var suggestion_toast = new Granite.Widgets.Toast ("");
suggestion_toast.set_default_action (_("Undo"));

var web_overlay = new Gtk.Overlay ();
web_overlay = new Gtk.Overlay ();
web_overlay.add (web_view);
web_overlay.add_overlay (suggestion_toast);

Expand Down Expand Up @@ -108,6 +110,8 @@ public class Ephemeral.MainWindow : Gtk.Window {
browser_button = new BrowserButton (this, web_view);
browser_button.sensitive = false;

downloads_button = new DownloadsButton ();

var settings_button = new Gtk.MenuButton ();
settings_button.image = new Gtk.Image.from_icon_name ("open-menu", Application.instance.icon_size);
settings_button.tooltip_text = _("Menu");
Expand Down Expand Up @@ -273,6 +277,7 @@ public class Ephemeral.MainWindow : Gtk.Window {
header.pack_start (new Gtk.Separator (Gtk.Orientation.VERTICAL));
header.pack_end (settings_button);
header.pack_end (browser_button);
header.pack_end (downloads_button);
header.pack_end (erase_button);
header.pack_end (new Gtk.Separator (Gtk.Orientation.VERTICAL));

Expand Down Expand Up @@ -425,6 +430,11 @@ public class Ephemeral.MainWindow : Gtk.Window {
web_view.notify["estimated-load-progress"].connect (update_progress);
web_view.notify["is-loading"].connect (update_progress);

web_view.web_context.download_started.connect ((download) => {
downloads_button.add_download (download);
download.finished.connect (() => {download_finished (download);});
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
});

web_view.decide_policy.connect ((decision, type) => {
switch (type) {
case WebKit.PolicyDecisionType.NAVIGATION_ACTION:
Expand Down Expand Up @@ -465,6 +475,15 @@ public class Ephemeral.MainWindow : Gtk.Window {
if (is_location (uri)) {
web_view.load_uri (uri);
}
break;
case WebKit.PolicyDecisionType.RESPONSE:
// Download files not supported by WebKit
var action = (WebKit.ResponsePolicyDecision) decision;
if (!action.is_mime_type_supported ()) {
decision.download ();
decision.ignore ();
return true;
}
}
return false;
});
Expand Down Expand Up @@ -760,6 +779,23 @@ public class Ephemeral.MainWindow : Gtk.Window {

}

private void download_finished (WebKit.Download download) {
if (download.estimated_progress != 1.0) return; // Don't get triggered by cancellations.

var filename = Filename.display_basename (download.destination);
var toast = new Granite.Widgets.Toast (_("%s Downloaded").printf (filename));
toast.set_default_action (_("Open"));

toast.default_action.connect (() => {
new DownloadRow(download).open ();
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
});
toast.closed.connect (() => {toast.destroy ();});
alcinnz marked this conversation as resolved.
Show resolved Hide resolved

toast.show_all ();
web_overlay.add_overlay (toast);
toast.send_notification ();
}

private bool is_location (string uri) {
return
uri.has_prefix ("about:") ||
Expand Down
200 changes: 200 additions & 0 deletions src/Widgets/DownloadsButton.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Copyright © 2019 Adrian Cochrane (https://adrian.geek.nz)
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* Authored by: Adrian Cochrane <alcinnz@lavabit.com>
*/
namespace Ephemeral {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I typically avoid the extra indentation level and just namespace the class itself, i.e. Ephemeral.DownloadsButton. I think I'd like to do that here as well for consistency with the rest of the codebase.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I differed for the sake of two helper classes, but I can make that adjustment.

public class DownloadsButton : Gtk.Revealer {
Gtk.ListBox downloads_list;

public DownloadsButton() {
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
Object ();
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
}

construct {
tooltip_text = _("Downloads");
tooltip_markup = Granite.markup_accel_tooltip ({"<Ctrl>j"}, tooltip_text);

reveal_child = false;

var button = new Gtk.MenuButton ();
button.image = new Gtk.Image.from_icon_name (
"folder-download", Application.instance.icon_size
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
);

var popover = new Gtk.Popover (null);
button.popover = popover;

downloads_list = new Gtk.ListBox ();
downloads_list.activate_on_single_click = true;
downloads_list.selection_mode = Gtk.SelectionMode.SINGLE;
downloads_list.show ();

downloads_list.row_activated.connect ((row) => {
var download = (DownloadRow) row;
if (download == null) return;
download.open ();
});

popover.add (downloads_list);
add (button);
}

public void add_download(WebKit.Download download) {
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
var row = new DownloadRow (download);
row.build_ui ();

row.show_all ();
downloads_list.add (row);
reveal_child = true;
}
}

public class DownloadRow : Gtk.ListBoxRow {
public WebKit.Download download;

public DownloadRow(WebKit.Download download) {this.download = download;}
alcinnz marked this conversation as resolved.
Show resolved Hide resolved

public void build_ui() {
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
var image = new Gtk.Image.from_icon_name (
"document-save-as", Gtk.IconSize.DND
);

var label = new Gtk.Label ("");
label.hexpand = true;
label.xalign = 0;

var progressbar = new Gtk.ProgressBar ();
progressbar.get_style_context ().add_class ("osd");
progressbar.fraction = 0;
download.received_data.connect (() => {
progressbar.fraction = download.estimated_progress;
});

var overlay = new Gtk.Overlay ();
overlay.add (label);
overlay.add_overlay (progressbar);

var folder_button = new Gtk.Button.from_icon_name (
"folder-open", Gtk.IconSize.MENU
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
);
folder_button.tooltip_text = _("Open in folder");
folder_button.clicked.connect (() => {
if (download.estimated_progress == 1.0)
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
DBus.Files.show_files ({download.destination});
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
});

var cancel_button = new Gtk.Button.from_icon_name (
"process-stop", Gtk.IconSize.MENU
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
);
cancel_button.tooltip_text = _("Cancel download");
var cancelled = false;
cancel_button.clicked.connect (() => {
download.cancel ();
cancelled = true;
this.destroy ();
});

var secondary_stack = new Gtk.Stack ();
secondary_stack.halign = Gtk.Align.END;
secondary_stack.margin_start = secondary_stack.margin_end = 6;
secondary_stack.add (folder_button);
secondary_stack.add (cancel_button);
secondary_stack.show_all ();
secondary_stack.visible_child = cancel_button;

download.finished.connect (() => {
download.received_data (uint64.MAX); // To ensure initial data is displayed.
if (cancelled) return;

this.selectable = this.activatable = true;
secondary_stack.visible_child = folder_button;
});

var grid = new Gtk.Grid ();
grid.orientation = Gtk.Orientation.HORIZONTAL;
grid.column_spacing = 3;
grid.add (image);
grid.add (overlay);
grid.add (secondary_stack);

add (grid);
tooltip_text = download.destination;
selectable = activatable = false;

ulong download_started = 0;
download_started = download.received_data.connect(() => {
alcinnz marked this conversation as resolved.
Show resolved Hide resolved
var mimetype = download.response.mime_type;
if (mimetype == "application/octet-stream") {
mimetype = ContentType.guess (download.response.uri, null, null);
}

image.gicon = ContentType.get_icon (mimetype);
image.tooltip_text = ContentType.get_description (mimetype);

label.label = Filename.display_basename (download.destination);

download.disconnect (download_started);
});
}

public void open () {
var mimetype = download.response.mime_type;
if (mimetype == "application/octet-stream") {
mimetype = ContentType.guess (download.response.uri, null, null);
}

var app = AppInfo.get_default_for_type (mimetype, false);
if (app == null) return; // TODO It'd be nice to integrate AppCenter here.
alcinnz marked this conversation as resolved.
Show resolved Hide resolved

var uris = new List<string>();
uris.append (download.destination);
try {
app.launch_uris (uris, null);
} catch (Error err) {
warning ("%s", err.message);
}
}
}

[DBus (name = "org.freedesktop.FileManager1")]
interface DBus.Files : Object {
const string FILES_DBUS_ID = "org.freedesktop.FileManager1";
const string FILES_DBUS_PATH = "/org/freedesktop/FileManager1";

public abstract void show_items (string[] uris, string startup_id) throws IOError, DBusError;
public abstract void show_folders (string[] uris, string startup_id) throws IOError, DBusError;

public static void show_files (string[] uris) {
try {

DBus.Files files = Bus.get_proxy_sync (BusType.SESSION, FILES_DBUS_ID, FILES_DBUS_PATH);
files.show_items (uris, "ephemeral");

} catch (DBusError err) {

// Fallback to just opening the Downloads folder
var path = Environment.get_user_special_dir (UserDirectory.DOWNLOAD);
AppInfo.launch_default_for_uri (File.new_for_path (path).get_uri (), null);

} catch (Error err) {
warning ("Failed to open file manager: %s", err.message);
}
}
}
}
5 changes: 5 additions & 0 deletions src/Widgets/WebView.vala
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ public class Ephemeral.WebView : WebKit.WebView {
WebKit.ContextMenuAction.COPY_LINK_TO_CLIPBOARD
)
);
context_menu.append (
new WebKit.ContextMenuItem.from_stock_action (
WebKit.ContextMenuAction.DOWNLOAD_LINK_TO_DISK
)
);

new_window_action.activate.connect (() => {
Application.new_window (hit_test_result.link_uri);
Expand Down