diff --git a/data/sideload.desktop.in b/data/sideload.desktop.in index 487cd608..6d235624 100644 --- a/data/sideload.desktop.in +++ b/data/sideload.desktop.in @@ -10,4 +10,4 @@ Type=Application StartupNotify=true Categories=PackageManager; NoDisplay=true -MimeType=application/vnd.flatpak.ref; +MimeType=application/vnd.flatpak.ref;application/vnd.flatpak.repo; diff --git a/meson.build b/meson.build index 79fe283d..e8cbaa38 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project( - 'io.elementary.sideload', + 'io.elementary.sideload', 'vala', 'c', version: '1.1.0' ) @@ -23,13 +23,16 @@ executable( resources, 'src/Utils/AsyncMutex.vala', 'src/Views/AbstractView.vala', + 'src/Views/AddRepoView.vala', 'src/Views/ErrorView.vala', 'src/Views/MainView.vala', 'src/Views/ProgressView.vala', 'src/Views/SuccessView.vala', + 'src/AddRepoWindow.vala', 'src/Application.vala', - 'src/MainWindow.vala', 'src/FlatpakRefFile.vala', + 'src/FlatpakRepoFile.vala', + 'src/InstallRefWindow.vala', dependencies: [ dependency ('flatpak', version: '>=1.1.2'), dependency ('glib-2.0'), diff --git a/src/AddRepoWindow.vala b/src/AddRepoWindow.vala new file mode 100644 index 00000000..567815f0 --- /dev/null +++ b/src/AddRepoWindow.vala @@ -0,0 +1,57 @@ +/* +* Copyright 2020 elementary, Inc. (https://elementary.io) +* +* 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 3 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 +* +*/ + +public class Sideload.AddRepoWindow : Gtk.ApplicationWindow { + public FlatpakRepoFile file { get; construct; } + + public AddRepoWindow (Gtk.Application application, FlatpakRepoFile file) { + Object ( + application: application, + icon_name: "io.elementary.sideload", + resizable: false, + title: _("Add untrusted software source"), + file: file + ); + } + + construct { + var titlebar = new Gtk.HeaderBar (); + titlebar.get_style_context ().add_class (Gtk.STYLE_CLASS_FLAT); + titlebar.set_custom_title (new Gtk.Grid ()); + + var view = new AddRepoView (); + + view.add_requested.connect (() => { + file.add (); + destroy (); + }); + + add (view); + + file.details_ready.connect (() => { + view.display_details (file.get_title ()); + }); + + file.get_details.begin (); + + get_style_context ().add_class ("rounded"); + set_titlebar (titlebar); + } +} diff --git a/src/Application.vala b/src/Application.vala index cb2958ad..6b580f44 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -19,7 +19,12 @@ */ public class Sideload.Application : Gtk.Application { - private MainWindow main_window; + private const string REF_CONTENT_TYPE = "application/vnd.flatpak.ref"; + private const string REPO_CONTENT_TYPE = "application/vnd.flatpak.repo"; + private const string[] SUPPORTED_CONTENT_TYPES = { + REF_CONTENT_TYPE, + REPO_CONTENT_TYPE + }; public Application () { Object ( @@ -39,15 +44,65 @@ public class Sideload.Application : Gtk.Application { return; } - var ref_file = new FlatpakRefFile (file); - main_window = new MainWindow (this, ref_file); - main_window.show_all (); + hold (); + open_file.begin (file); + } - var quit_action = new SimpleAction ("quit", null); - var launch_action = new SimpleAction ("launch", null); + private async void open_file (File file) { + GLib.FileInfo? file_info = null; + try { + file_info = yield file.query_info_async ( + FileAttribute.STANDARD_CONTENT_TYPE, + FileQueryInfoFlags.NONE + ); + } catch (Error e) { + print ("Unable to query content type of provided file\n"); + release (); + return; + } + if (file_info == null) { + print ("Unable to query content type of provided file\n"); + release (); + return; + } + + var content_type = file_info.get_attribute_string (FileAttribute.STANDARD_CONTENT_TYPE); + if (content_type == null) { + print ("Unable to get content type of provided file\n"); + release (); + return; + } + + if (!(content_type in SUPPORTED_CONTENT_TYPES)) { + print ("This does not appear to be a valid flatpakref/flatpakrepo file\n"); + release (); + return; + } + + Gtk.ApplicationWindow? main_window = null; + + if (content_type == REF_CONTENT_TYPE) { + var ref_file = new FlatpakRefFile (file); + main_window = new InstallRefWindow (this, ref_file); + main_window.show_all (); + + var launch_action = new SimpleAction ("launch", null); + add_action (launch_action); + + launch_action.activate.connect (() => { + ref_file.launch.begin (); + activate_action ("quit", null); + }); + } else if (content_type == REPO_CONTENT_TYPE) { + var repo_file = new FlatpakRepoFile (file); + main_window = new AddRepoWindow (this, repo_file); + main_window.show_all (); + } + + var quit_action = new SimpleAction ("quit", null); add_action (quit_action); - add_action (launch_action); + set_accels_for_action ("app.quit", {"q"}); quit_action.activate.connect (() => { @@ -56,10 +111,7 @@ public class Sideload.Application : Gtk.Application { } }); - launch_action.activate.connect (() => { - ref_file.launch.begin (); - activate_action ("quit", null); - }); + release (); } protected override void activate () { @@ -77,7 +129,7 @@ public class Sideload.Application : Gtk.Application { public static int main (string[] args) { if (args.length < 2) { - print ("Usage: %s /path/to/flatpakref\n", args[0]); + print ("Usage: %s /path/to/flatpakref or /path/to/flatpakrepo\n", args[0]); return 1; } diff --git a/src/FlatpakRepoFile.vala b/src/FlatpakRepoFile.vala new file mode 100644 index 00000000..630ee25c --- /dev/null +++ b/src/FlatpakRepoFile.vala @@ -0,0 +1,99 @@ +/* +* Copyright 2020 elementary, Inc. (https://elementary.io) +* +* 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 3 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 +* +*/ + +public class Sideload.FlatpakRepoFile : Object { + public signal void details_ready (); + public signal void loading_failed (); + + public File file { get; construct; } + + private static Flatpak.Installation? installation; + private Bytes? bytes = null; + + public Flatpak.Remote? remote = null; + + static construct { + try { + installation = new Flatpak.Installation.user (); + } catch (Error e) { + warning (e.message); + } + } + + public FlatpakRepoFile (File file) { + Object (file: file); + } + + public async void get_details () { + var basename = file.get_basename (); + + // Build a valid flatpak repo name from the filename + var repo_id = basename.to_ascii (); + + // Strip the extension + repo_id = repo_id[0:repo_id.last_index_of (".")]; + + // Replace any non-alphanumeric characters with underscores + var builder = new StringBuilder (); + for (uint i = 0; repo_id[i] != '\0'; i++) { + if (repo_id[i].isalnum ()) { + builder.append_c (repo_id[i]); + } else { + builder.append_c ('_'); + } + } + + repo_id = builder.str; + + try { + remote = new Flatpak.Remote.from_file (repo_id, yield get_bytes ()); + } catch (Error e) { + critical ("Unable to read flatpak repofile, is it valid? Details: %s", e.message); + loading_failed (); + return; + } + + details_ready (); + } + + public string? get_title () { + return remote.get_title (); + } + + public bool add () { + bool success = false; + try { + success = installation.add_remote (remote, true, null); + } catch (Error e) { + warning ("Error adding flatpak remote: %s", e.message); + } + + return success; + } + + private async Bytes get_bytes () throws Error { + if (bytes != null) { + return bytes; + } + + bytes = yield file.load_bytes_async (null, null); + return bytes; + } +} diff --git a/src/MainWindow.vala b/src/InstallRefWindow.vala similarity index 97% rename from src/MainWindow.vala rename to src/InstallRefWindow.vala index e31b810d..643de015 100644 --- a/src/MainWindow.vala +++ b/src/InstallRefWindow.vala @@ -18,7 +18,7 @@ * */ -public class Sideload.MainWindow : Gtk.ApplicationWindow { +public class Sideload.InstallRefWindow : Gtk.ApplicationWindow { public FlatpakRefFile file { get; construct; } private Cancellable? current_cancellable = null; @@ -28,7 +28,7 @@ public class Sideload.MainWindow : Gtk.ApplicationWindow { private string? app_name = null; - public MainWindow (Gtk.Application application, FlatpakRefFile file) { + public InstallRefWindow (Gtk.Application application, FlatpakRefFile file) { Object ( application: application, icon_name: "io.elementary.sideload", diff --git a/src/Views/AddRepoView.vala b/src/Views/AddRepoView.vala new file mode 100644 index 00000000..f0d8fa14 --- /dev/null +++ b/src/Views/AddRepoView.vala @@ -0,0 +1,120 @@ +/* + * Copyright 2019 elementary, Inc. (https://elementary.io) + * + * 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 3 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, see . + */ + +public class Sideload.AddRepoView : AbstractView { + public signal void add_requested (); + + private static Gtk.CssProvider provider; + + private Gtk.Grid details_grid; + private Gtk.Stack details_stack; + + static construct { + provider = new Gtk.CssProvider (); + provider.load_from_resource ("io/elementary/sideload/colors.css"); + } + + construct { + primary_label.label = _("Add untrusted software source?"); + + secondary_label.label = _("This software source and its apps have not been reviewed by elementary for security, privacy, or system integration."); + + var loading_spinner = new Gtk.Spinner (); + loading_spinner.start (); + + var loading_label = new Gtk.Label (_("Fetching details")); + + var loading_grid = new Gtk.Grid (); + loading_grid.column_spacing = 6; + loading_grid.add (loading_spinner); + loading_grid.add (loading_label); + + var agree_check = new Gtk.CheckButton.with_label (_("I understand")); + agree_check.margin_top = 12; + + var repo_icon = new Gtk.Image.from_icon_name ("system-software-install-symbolic", Gtk.IconSize.BUTTON); + repo_icon.valign = Gtk.Align.START; + + unowned Gtk.StyleContext repo_context = repo_icon.get_style_context (); + repo_context.add_class ("appcenter"); + repo_context.add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + + var appstore_name = ((Sideload.Application) GLib.Application.get_default ()).get_appstore_name (); + + var repo_label = new Gtk.Label (_("Non-curated apps from this source may appear alongside other apps in %s").printf (appstore_name)); + repo_label.selectable = true; + repo_label.max_width_chars = 50; + repo_label.wrap = true; + repo_label.xalign = 0; + + var updates_icon = new Gtk.Image.from_icon_name ("system-software-update-symbolic", Gtk.IconSize.BUTTON); + updates_icon.valign = Gtk.Align.START; + + unowned Gtk.StyleContext updates_context = updates_icon.get_style_context (); + updates_context.add_class ("updates"); + updates_context.add_provider (provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); + + var updates_label = new Gtk.Label (_("Apps and updates from this source will not be reviewed")); + updates_label.selectable = true; + updates_label.max_width_chars = 50; + updates_label.wrap = true; + updates_label.xalign = 0; + + details_grid = new Gtk.Grid (); + details_grid.orientation = Gtk.Orientation.VERTICAL; + details_grid.column_spacing = 6; + details_grid.row_spacing = 12; + details_grid.attach (repo_icon, 0, 0); + details_grid.attach (repo_label, 1, 0); + details_grid.attach (updates_icon, 0, 1); + details_grid.attach (updates_label, 1, 1); + details_grid.attach (agree_check, 0, 2, 2); + + details_stack = new Gtk.Stack (); + details_stack.vhomogeneous = false; + details_stack.add_named (loading_grid, "loading"); + details_stack.add_named (details_grid, "details"); + details_stack.visible_child_name = "loading"; + + content_area.add (details_stack); + + var cancel_button = new Gtk.Button.with_label (_("Cancel")); + cancel_button.action_name = "app.quit"; + + var add_button = new Gtk.Button.with_label (_("Add Anyway")); + add_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + + button_box.add (cancel_button); + button_box.add (add_button); + + show_all (); + + agree_check.bind_property ("active", add_button, "sensitive", GLib.BindingFlags.SYNC_CREATE); + agree_check.grab_focus (); + + add_button.clicked.connect (() => { + add_requested (); + }); + } + + public void display_details (string? title) { + primary_label.label = _("Add untrusted software source ā€œ%sā€".printf (title)); + + details_stack.visible_child_name = "details"; + show_all (); + } +}