/* Copyright (C) 2011-2012 Christian Dywan This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. See the file COPYING for the full license text. */ namespace Katze { extern static string mkdir_with_parents (string pathname, int mode); } namespace Sokoke { extern static string js_script_eval (void* ctx, string script, void* error); } namespace Midori { public class SpeedDial : GLib.Object { string filename; public GLib.KeyFile keyfile; string? html = null; List thumb_queue = null; WebKit.WebView thumb_view = null; Spec? spec = null; public class Spec { public string dial_id; public string uri; public Spec (string dial_id, string uri) { this.dial_id = dial_id; this.uri = uri; } } public SpeedDial (string new_filename, string? fallback = null) { filename = new_filename; keyfile = new GLib.KeyFile (); try { keyfile.load_from_file (filename, GLib.KeyFileFlags.NONE); } catch (GLib.Error io_error) { string json; size_t len; try { FileUtils.get_contents (fallback ?? (filename + ".json"), out json, out len); } catch (GLib.Error fallback_error) { json = "'{}'"; len = 4; } var script = new StringBuilder.sized (len); script.append ("var json = JSON.parse ("); script.append_len (json, (ssize_t)len); script.append (""" ); var keyfile = ''; for (var i in json['shortcuts']) { var tile = json['shortcuts'][i]; keyfile += '[Dial ' + tile['id'].substring (1) + ']\n' + 'uri=' + tile['href'] + '\n' + 'img=' + tile['img'] + '\n' + 'title=' + tile['title'] + '\n\n'; } var columns = json['width'] ? json['width'] : 3; var rows = json['shortcuts'] ? json['shortcuts'].length / columns : 0; keyfile += '[settings]\n' + 'columns=' + columns + '\n' + 'rows=' + (rows > 3 ? rows : 3) + '\n\n'; keyfile; """); try { keyfile.load_from_data ( Sokoke.js_script_eval (null, script.str, null), -1, 0); } catch (GLib.Error eval_error) { GLib.critical ("Failed to parse %s as speed dial JSON: %s", fallback ?? (filename + ".json"), eval_error.message); } Katze.mkdir_with_parents ( Path.build_path (Path.DIR_SEPARATOR_S, Environment.get_user_cache_dir (), "midori", "thumbnails"), 0700); foreach (string tile in keyfile.get_groups ()) { try { string img = keyfile.get_string (tile, "img"); string uri = keyfile.get_string (tile, "uri"); if (img != null && uri[0] != '\0' && uri[0] != '#') { uchar[] decoded = Base64.decode (img); FileUtils.set_data (build_thumbnail_path (uri), decoded); } keyfile.remove_key (tile, "img"); } catch (GLib.Error img_error) { /* img and uri can be missing */ } } } } public string get_next_free_slot () { uint slot_count = 0; foreach (string tile in keyfile.get_groups ()) { try { if (keyfile.has_key (tile, "uri")) slot_count++; } catch (KeyFileError error) { } } uint slot = 1; while (slot <= slot_count) { string tile = "Dial %u".printf (slot); if (!keyfile.has_group (tile)) return "Dial %u".printf (slot); slot++; } return "Dial %u".printf (slot_count + 1); } public void add (string uri, string title, Gdk.Pixbuf img) { string id = get_next_free_slot (); add_with_id (id, uri, title, img); } public void add_with_id (string id, string uri, string title, Gdk.Pixbuf img) { keyfile.set_string (id, "uri", uri); keyfile.set_string (id, "title", title); Katze.mkdir_with_parents (Path.build_path (Path.DIR_SEPARATOR_S, Paths.get_cache_dir (), "thumbnails"), 0700); string filename = build_thumbnail_path (uri); try { img.save (filename, "png", null, "compression", "7", null); } catch (Error error) { critical ("Failed to save speed dial thumbnail: %s", error.message); } save (); } string build_thumbnail_path (string filename) { string thumbnail = Checksum.compute_for_string (ChecksumType.MD5, filename) + ".png"; return Path.build_filename (Paths.get_cache_dir (), "thumbnails", thumbnail); } public unowned string get_html (bool close_buttons_left, GLib.Object view) throws Error { bool load_missing = true; if (html != null) return html; string? head = null; string filename = Paths.get_res_filename ("speeddial-head.html"); if (keyfile != null && FileUtils.get_contents (filename, out head, null)) { string header = head.replace ("{title}", _("Speed Dial")). replace ("{click_to_add}", _("Click to add a shortcut")). replace ("{enter_shortcut_address}", _("Enter shortcut address")). replace ("{enter_shortcut_name}", _("Enter shortcut title")). replace ("{are_you_sure}", _("Are you sure you want to delete this shortcut?")); var markup = new StringBuilder (header); uint slot_count = 1; foreach (string tile in keyfile.get_groups ()) { try { if (keyfile.has_key (tile, "uri")) slot_count++; } catch (KeyFileError error) { } } /* Try to guess the best X by X grid size */ uint grid_index = 3; while ((grid_index * grid_index) < slot_count) grid_index++; /* Percent width size of one slot */ uint slot_size = (100 / grid_index); /* No editing in private/ app mode or without scripts */ markup.append_printf ( "%s%s" + "\n", Paths.is_readonly () ? "" : "", slot_size + 1, slot_size - 4); /* Combined width of slots should always be less than 100%. * Use half of the remaining percentage as a margin size */ uint div_factor; if (slot_size * grid_index >= 100 && grid_index > 4) div_factor = 8; else div_factor = 2; uint margin = (100 - ((slot_size - 4) * grid_index)) / div_factor; if (margin > 9) margin = margin % 10; markup.append_printf ( "", margin); if (close_buttons_left) markup.append_printf ( ""); foreach (string tile in keyfile.get_groups ()) { try { string uri = keyfile.get_string (tile, "uri"); if (uri != null && uri.str ("://") != null && tile.has_prefix ("Dial ")) { string title = keyfile.get_string (tile, "title"); string thumb_filename = build_thumbnail_path (uri); uint slot = tile.substring (5, -1).to_int (); string encoded; try { uint8[] thumb; FileUtils.get_data (thumb_filename, out thumb); encoded = Base64.encode (thumb); } catch (FileError error) { encoded = null; if (load_missing) get_thumb (tile, uri); } markup.append_printf ("""
%s
""", slot, slot, uri, encoded ?? "", title, slot, title ?? ""); } else if (tile != "settings") keyfile.remove_group (tile); } catch (KeyFileError error) { } } markup.append_printf ("""
%s
""", slot_count + 1, slot_count + 1, _("Click to add a shortcut")); markup.append_printf ("\n\n\n"); html = markup.str; } else html = ""; return html; } public void save_message (string message) throws Error { string msg = message.substring (16, -1); string[] parts = msg.split (" ", 4); string action = parts[0]; if (action == "add" || action == "rename" || action == "delete" || action == "swap") { uint slot_id = parts[1].next_char().to_int () ; string dial_id = "Dial %u".printf (slot_id); if (action == "delete") { string uri = keyfile.get_string (dial_id, "uri"); string file_path = build_thumbnail_path (uri); keyfile.remove_group (dial_id); FileUtils.unlink (file_path); } else if (action == "add") { keyfile.set_string (dial_id, "uri", parts[2]); get_thumb (dial_id, parts[2]); } else if (action == "rename") { uint offset = parts[0].length + parts[1].length + 2; string title = msg.substring (offset, -1); keyfile.set_string (dial_id, "title", title); } else if (action == "swap") { uint slot2_id = parts[2].next_char().to_int (); string dial2_id = "Dial %u".printf (slot2_id); string uri = keyfile.get_string (dial_id, "uri"); string title = keyfile.get_string (dial_id, "title"); string uri2 = keyfile.get_string (dial2_id, "uri"); string title2 = keyfile.get_string (dial2_id, "title"); keyfile.set_string (dial_id, "uri", uri2); keyfile.set_string (dial2_id, "uri", uri); keyfile.set_string (dial_id, "title", title2); keyfile.set_string (dial2_id, "title", title); } } save (); } void save () { html = null; try { FileUtils.set_contents (filename, keyfile.to_data ()); } catch (Error error) { critical ("Failed to update speed dial: %s", error.message); } /* FIXME Refresh all open views */ } void load_status (GLib.Object thumb_view_, ParamSpec pspec) { if (thumb_view.load_status != WebKit.LoadStatus.FINISHED) return; return_if_fail (spec != null); #if HAVE_OFFSCREEN var img = (thumb_view.parent as Gtk.OffscreenWindow).get_pixbuf (); var pixbuf_scaled = img.scale_simple (240, 160, Gdk.InterpType.TILES); img = pixbuf_scaled; #else thumb_view.realize (); var img = midori_view_web_view_get_snapshot (thumb_view, 240, 160); #endif unowned string title = thumb_view.get_title (); add_with_id (spec.dial_id, spec.uri, title ?? spec.uri, img); thumb_queue.remove (spec); if (thumb_queue != null && thumb_queue.data != null) { spec = thumb_queue.data; thumb_view.load_uri (spec.uri); } else /* disconnect_by_func (thumb_view, load_status) */; } void get_thumb (string dial_id, string uri) { if (thumb_view == null) { thumb_view = new WebKit.WebView (); var settings = new WebKit.WebSettings (); settings. set ("enable-scripts", false, "enable-plugins", false, "auto-load-images", true, "enable-html5-database", false, "enable-html5-local-storage", false); if (settings.get_class ().find_property ("enable-java-applet") != null) settings.set ("enable-java-applet", false); thumb_view.settings = settings; #if HAVE_OFFSCREEN var offscreen = new Gtk.OffscreenWindow (); offscreen.add (thumb_view); thumb_view.set_size_request (800, 600); offscreen.show_all (); #else /* What we are doing here is a bit of a hack. In order to render a thumbnail we need a new view and load the url in it. But it has to be visible and packed in a container. So we secretly pack it into the notebook of the parent browser. */ notebook.add (thumb_view); thumb_view.destroy.connect (Gtk.widget_destroyed); /* We use an empty label. It's not invisible but hard to spot. */ notebook.set_tab_label (thumb_view, new Gtk.EventBox ()); thumb_view.show (); #endif } thumb_queue.append (new Spec (dial_id, uri)); if (thumb_queue.nth_data (1) != null) return; spec = thumb_queue.data; thumb_view.notify["load-status"].connect (load_status); thumb_view.load_uri (spec.uri); } } }