Merge branch 'master' into titlebar-warning

This commit is contained in:
Bleak Grey 2018-05-30 14:42:57 +03:00 committed by GitHub
commit ca127df6e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 283 additions and 168 deletions

View File

@ -22,6 +22,7 @@ executable(
'src/MainWindow.vala',
'src/SettingsManager.vala',
'src/AccountManager.vala',
'src/ImageCache.vala',
'src/NetManager.vala',
'src/Utils.vala',
'src/Notificator.vala',
@ -35,7 +36,6 @@ executable(
'src/API/Notification.vala',
'src/API/NotificationType.vala',
'src/API/Attachment.vala',
'src/Widgets/HeaderBar.vala',
'src/Widgets/AlignedLabel.vala',
'src/Widgets/RichLabel.vala',
'src/Widgets/ImageToggleButton.vala',

View File

@ -10,6 +10,7 @@ namespace Tootle{
public static SettingsManager settings;
public static AccountManager accounts;
public static NetManager network;
public static ImageCache image_cache;
public class Application : Granite.Application {
@ -31,6 +32,7 @@ namespace Tootle{
settings = new SettingsManager ();
accounts = new AccountManager ();
network = new NetManager ();
image_cache = new ImageCache ();
accounts.init ();
app.error.connect (app.on_error);
@ -59,8 +61,6 @@ namespace Tootle{
else {
window = new MainWindow (this);
window.present ();
accounts.switched (accounts.current);
accounts.signal_current ();
}
}
}

143
src/ImageCache.vala Normal file
View File

@ -0,0 +1,143 @@
using Soup;
using GLib;
using Gdk;
using Json;
private struct CachedImage {
public string uri;
public int size;
public CachedImage(string uri, int size) { this.uri=uri; this.size=size; }
public static uint hash(CachedImage? c) {
assert(c != null);
assert(c.uri != null);
return GLib.int64_hash(c.size) ^ c.uri.hash();
}
public static bool equal(CachedImage? a, CachedImage? b) {
if (a == null || b == null)
return false;
return a.size == b.size && a.uri == b.uri;
}
}
public delegate void PixbufCallback (Gdk.Pixbuf pb);
public class Tootle.ImageCache : GLib.Object {
private GLib.HashTable<CachedImage?, Soup.Message> in_progress;
private GLib.HashTable<CachedImage?, Gdk.Pixbuf> pixbufs;
private uint total_size_est;
private uint size_limit;
private string cache_path;
construct {
pixbufs = new GLib.HashTable<CachedImage?, Gdk.Pixbuf>(CachedImage.hash, CachedImage.equal);
in_progress = new GLib.HashTable<CachedImage?, Soup.Message>(CachedImage.hash, CachedImage.equal);
total_size_est = 0;
cache_path = "%s/%s".printf (GLib.Environment.get_user_cache_dir (), app.application_id);
settings.changed.connect (on_settings_changed);
on_settings_changed ();
}
public ImageCache() {
GLib.Object();
}
private void on_settings_changed () {
// assume 32BPP (divide bytes by 4 to get # pixels) and raw, overhead-free storage
// cache_size setting is number of megabytes
size_limit = (1024 * 1024 * settings.cache_size) / 4;
if (settings.cache)
enforce_size_limit ();
else
remove_all ();
}
public void remove_all () {
debug("Image cache cleared");
pixbufs.remove_all ();
total_size_est = 0;
}
public void remove_one (string uri, int size) {
CachedImage ci = CachedImage (uri, size);
bool removed = pixbufs.remove(ci);
if (removed) {
assert (total_size_est >= size * size);
total_size_est -= size * size;
debug("Cache usage: %zd", total_size_est);
}
}
//TODO: fix me
// remove least used image
private void remove_least_used () {
var keys = pixbufs.get_keys();
if (keys.first() != null) {
var ci = keys.first().data;
remove_one(ci.uri, ci.size);
}
}
private void enforce_size_limit () {
debug("Updating size limit (%zd/%zd)", total_size_est, size_limit);
while (total_size_est > size_limit && pixbufs.size() > 0)
remove_least_used ();
assert (total_size_est <= size_limit);
}
private void store_pixbuf (CachedImage ci, Gdk.Pixbuf pixbuf) {
assert (!pixbufs.contains (ci));
pixbufs.insert (ci, pixbuf);
in_progress.remove (ci);
total_size_est += ci.size * ci.size;
enforce_size_limit ();
}
public async void get_image (string uri, int size, owned PixbufCallback? cb = null) {
CachedImage ci = CachedImage (uri, size);
Gdk.Pixbuf? pb = pixbufs.get(ci);
if (pb != null) {
cb (pb);
return;
}
Soup.Message? msg = in_progress.get(ci);
if (msg == null) {
msg = new Soup.Message("GET", uri);
msg.finished.connect(() => {
debug("Caching %s@%d", uri, size);
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
store_pixbuf(ci, pixbuf);
cb(pixbuf);
});
in_progress[ci] = msg;
network.queue_custom (msg);
} else {
msg.finished.connect_after(() => {
cb(pixbufs[ci]);
});
}
}
public void load_avatar (string uri, Granite.Widgets.Avatar avatar, int size = 32) {
get_image.begin(uri, size, (pixbuf) => avatar.pixbuf = pixbuf);
}
public void load_image (string uri, Gtk.Image image) {
load_scaled_image(uri, image, -1);
}
public void load_scaled_image (string uri, Gtk.Image image, int size = 64) {
get_image.begin(uri, size, image.set_from_pixbuf);
}
}

View File

@ -1,19 +1,21 @@
using Gtk;
public class Tootle.MainWindow: Gtk.Window {
private weak SettingsManager settings;
private Gtk.Overlay overlay;
private Granite.Widgets.Toast toast;
private Gtk.Grid grid;
public Tootle.HeaderBar header;
public Stack primary_stack;
public Stack secondary_stack;
private Stack primary_stack;
private Stack secondary_stack;
public Gtk.HeaderBar header;
private Granite.Widgets.ModeButton button_mode;
private AccountsButton button_accounts;
private Spinner spinner;
private Button button_toot;
private Button button_back;
construct {
settings = Tootle.settings;
var provider = new Gtk.CssProvider ();
provider.load_from_resource ("/com/github/bleakgrey/tootle/app.css");
StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
@ -33,7 +35,37 @@ public class Tootle.MainWindow: Gtk.Window {
primary_stack.add_named (secondary_stack, "0");
primary_stack.hexpand = true;
primary_stack.vexpand = true;
header = new Tootle.HeaderBar ();
spinner = new Spinner ();
spinner.active = true;
button_accounts = new AccountsButton ();
button_back = new Button ();
button_back.label = _("Back");
button_back.get_style_context ().add_class (Granite.STYLE_CLASS_BACK_BUTTON);
button_back.clicked.connect (() => back ());
button_toot = new Button ();
button_toot.tooltip_text = _("Toot");
button_toot.image = new Gtk.Image.from_icon_name ("document-edit-symbolic", Gtk.IconSize.LARGE_TOOLBAR);
button_toot.clicked.connect (() => PostDialog.open ());
button_mode = new Granite.Widgets.ModeButton ();
button_mode.get_style_context ().add_class ("mode");
button_mode.mode_changed.connect (widget => {
secondary_stack.set_visible_child_name (widget.tooltip_text);
});
button_mode.show ();
header = new Gtk.HeaderBar ();
header.show_close_button = true;
header.custom_title = button_mode;
header.show_all ();
header.pack_start (button_back);
header.pack_start (button_toot);
header.pack_end (button_accounts);
header.pack_end (spinner);
set_titlebar (header);
grid = new Gtk.Grid ();
@ -41,8 +73,16 @@ public class Tootle.MainWindow: Gtk.Window {
grid.attach (primary_stack, 0, 0, 1, 1);
grid.attach (overlay, 0, 0, 1, 1);
add_header_view (new TimelineView ("home"));
add_header_view (new NotificationsView ());
add_header_view (new LocalView ());
add_header_view (new FederatedView ());
button_mode.set_active (0);
add (grid);
show_all ();
button_mode.valign = Gtk.Align.FILL;
}
public MainWindow (Gtk.Application application) {
@ -52,37 +92,18 @@ public class Tootle.MainWindow: Gtk.Window {
resizable: true
);
window_position = WindowPosition.CENTER;
update_header ();
Tootle.accounts.switched.connect(on_account_switched);
Tootle.app.toast.connect (on_toast);
}
private void reset () {
header.button_mode.clear_children ();
secondary_stack.forall (widget => widget.destroy ());
}
private void on_account_switched(Account? account){
header.button_mode.clear_children ();
secondary_stack.forall (widget => widget.destroy ());
if (account == null)
return;
build_main_view ();
}
private void build_main_view (){
add_header_view (new TimelineView ("home"));
add_header_view (new NotificationsView ());
add_header_view (new LocalView ());
add_header_view (new FederatedView ());
header.update (true);
app.toast.connect (on_toast);
network.started.connect (() => spinner.show ());
network.finished.connect (() => spinner.hide ());
accounts.signal_current ();
}
private void add_header_view (AbstractView view) {
var img = new Gtk.Image.from_icon_name(view.get_icon (), Gtk.IconSize.LARGE_TOOLBAR);
img.tooltip_text = view.get_name ();
header.button_mode.append (img);
button_mode.append (img);
view.image = img;
secondary_stack.add_named(view, view.get_name ());
@ -90,13 +111,34 @@ public class Tootle.MainWindow: Gtk.Window {
img.pixel_size = 20; // For some reason Notifications icon is too small without this
}
public void open_view (Widget widget) {
widget.show ();
var i = int.parse (primary_stack.get_visible_child_name ());
public int get_visible_id () {
return int.parse (primary_stack.get_visible_child_name ());
}
public void open_view (AbstractView widget) {
var i = get_visible_id ();
i++;
widget.stack_pos = i;
widget.show ();
primary_stack.add_named (widget, i.to_string ());
primary_stack.set_visible_child_name (i.to_string ());
header.update (false);
update_header ();
}
public void back () {
var i = get_visible_id ();
var child = primary_stack.get_child_by_name (i.to_string ());
primary_stack.set_visible_child_name ((i-1).to_string ());
child.destroy ();
update_header ();
}
public void reopen_view (int view_id) {
var i = get_visible_id ();
while (i != view_id && view_id != 0) {
back ();
i = get_visible_id ();
}
}
private void on_toast (string msg){
@ -119,8 +161,15 @@ public class Tootle.MainWindow: Gtk.Window {
var theme = is_dark ? "dark" : "light";
provider.load_from_resource ("/com/github/bleakgrey/tootle/%s.css".printf (theme));
StyleContext.add_provider_for_screen (Gdk.Screen.get_default (), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
Gtk.Settings.get_default ().gtk_application_prefer_dark_theme = is_dark;
}
private void update_header () {
bool primary_mode = get_visible_id () == 0;
button_mode.set_visible (primary_mode);
button_toot.set_visible (primary_mode);
button_back.set_visible (!primary_mode);
button_accounts.set_visible (true);
}
}

View File

@ -14,12 +14,8 @@ public class Tootle.NetManager : GLib.Object {
private int requests_processing = 0;
private Soup.Session session;
private Soup.Cache cache;
public string cache_path;
construct {
cache_path = "%s/%s".printf (GLib.Environment.get_user_cache_dir (), Tootle.app.application_id);
cache = new Soup.Cache (cache_path, Soup.CacheType.SINGLE_USER);
session = new Soup.Session ();
session.ssl_strict = true;
session.ssl_use_system_ca_file = true;
@ -31,12 +27,6 @@ public class Tootle.NetManager : GLib.Object {
finished ();
});
Tootle.app.shutdown.connect (() => {
cache.dump ();
});
Tootle.settings.changed.connect (on_settings_changed);
on_settings_changed ();
// Soup.Logger logger = new Soup.Logger (Soup.LoggerLogLevel.BODY, -1);
// session.add_feature (logger);
}
@ -45,23 +35,6 @@ public class Tootle.NetManager : GLib.Object {
GLib.Object();
}
private void on_settings_changed () {
// cache.set_max_size (1024 * 1024 * Tootle.settings.cache_size);
// var has_cache = session.has_feature (cache.get_type ());
// if (Tootle.settings.cache) {
// if (!has_cache) {
// debug ("Turning on cache");
// session.add_feature (cache);
// }
// }
// else {
// if (has_cache) {
// debug ("Turning off cache");
// session.remove_feature (cache);
// }
// }
}
public async WebsocketConnection stream (Soup.Message msg) {
return yield session.websocket_connect_async (msg, null, null, null);
}
@ -108,7 +81,7 @@ public class Tootle.NetManager : GLib.Object {
return msg;
}
public void queue_custom (Soup.Message msg, owned Soup.SessionCallback cb) {
public void queue_custom (Soup.Message msg, owned Soup.SessionCallback? cb = null) {
session.queue_message (msg, cb);
}
@ -133,34 +106,49 @@ public class Tootle.NetManager : GLib.Object {
}
public void load_avatar (string url, Granite.Widgets.Avatar avatar, int size = 32){
if (settings.cache) {
image_cache.load_avatar (url, avatar, size);
return;
}
var msg = new Soup.Message("GET", url);
msg.finished.connect(() => {
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
avatar.pixbuf = pixbuf;
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
avatar.pixbuf = pixbuf;
});
Tootle.network.queue (msg);
}
public void load_image (string url, Gtk.Image image) {
if (settings.cache) {
image_cache.load_image (url, image);
return;
}
var msg = new Soup.Message("GET", url);
msg.finished.connect(() => {
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream (stream);
image.set_from_pixbuf (pixbuf);
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream (stream);
image.set_from_pixbuf (pixbuf);
});
Tootle.network.queue (msg);
}
public void load_scaled_image (string url, Gtk.Image image, int size = 64) {
if (settings.cache) {
image_cache.load_scaled_image (url, image, size);
return;
}
var msg = new Soup.Message("GET", url);
msg.finished.connect(() => {
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
image.set_from_pixbuf (pixbuf);
var data = msg.response_body.data;
var stream = new MemoryInputStream.from_data (data);
var pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
image.set_from_pixbuf (pixbuf);
});
Tootle.network.queue (msg);
}

View File

@ -2,6 +2,7 @@ using Gtk;
public abstract class Tootle.AbstractView : Gtk.ScrolledWindow {
public int stack_pos = -1;
public Gtk.Image? image;
public Gtk.Box view;
protected Gtk.Box? empty;

View File

@ -2,7 +2,8 @@ using Gtk;
public class Tootle.SearchView : AbstractView {
Gtk.Entry entry;
private string query = "";
private Gtk.Entry entry;
construct {
view.margin_bottom = 6;
@ -11,6 +12,7 @@ public class Tootle.SearchView : AbstractView {
entry.placeholder_text = _("Search");
entry.secondary_icon_name = "system-search-symbolic";
entry.width_chars = 25;
entry.text = query;
entry.show ();
Tootle.window.header.pack_start (entry);
@ -56,14 +58,15 @@ public class Tootle.SearchView : AbstractView {
}
private void request () {
if (entry.text == "") {
query = entry.text;
if (query == "") {
clear ();
return;
}
Tootle.window.reopen_view (this.stack_pos);
var query = Soup.URI.encode (entry.text, null);
var url = "%s/api/v1/search?q=%s".printf (Tootle.accounts.formal.instance, query);
var query_encoded = Soup.URI.encode (query, null);
var url = "%s/api/v1/search?q=%s".printf (Tootle.accounts.formal.instance, query_encoded);
var msg = new Soup.Message("GET", url);
Tootle.network.queue(msg, (sess, mess) => {
try{
@ -100,7 +103,6 @@ public class Tootle.SearchView : AbstractView {
}
empty_state ();
}
catch (GLib.Error e) {
warning ("Can't update feed");

View File

@ -105,6 +105,11 @@ public class Tootle.TimelineView : AbstractView {
}
public virtual void request (){
if (accounts.current == null) {
empty_state ();
return;
}
var msg = new Soup.Message("GET", get_url ());
msg.finished.connect (() => empty_state ());
Tootle.network.queue(msg, (sess, mess) => {

View File

@ -44,7 +44,7 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
}
public AccountView (){
button.clicked.connect (() => Tootle.accounts.remove (id));
button.clicked.connect (() => accounts.remove (id));
}
}
@ -62,15 +62,15 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
item_refresh = new Gtk.ModelButton ();
item_refresh.text = _("Refresh");
item_refresh.clicked.connect (() => Tootle.app.refresh ());
item_refresh.clicked.connect (() => app.refresh ());
item_favs = new Gtk.ModelButton ();
item_favs.text = _("Favorites");
item_favs.clicked.connect (() => Tootle.window.open_view (new FavoritesView ()));
item_favs.clicked.connect (() => window.open_view (new FavoritesView ()));
item_search = new Gtk.ModelButton ();
item_search.text = _("Search");
item_search.clicked.connect (() => Tootle.window.open_view (new SearchView ()));
item_search.clicked.connect (() => window.open_view (new SearchView ()));
item_settings = new Gtk.ModelButton ();
item_settings.text = _("Settings");
@ -96,8 +96,8 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
add(avatar);
show_all ();
Tootle.accounts.updated.connect (accounts_updated);
Tootle.accounts.switched.connect (account_switched);
accounts.updated.connect (accounts_updated);
accounts.switched.connect (account_switched);
list.row_activated.connect (row => {
var widget = row as AccountView;
if (widget.id == -1) {
@ -107,7 +107,7 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
if (widget.id == Tootle.settings.current_account)
return;
else
Tootle.accounts.switch_account (widget.id);
accounts.switch_account (widget.id);
});
}
@ -135,7 +135,7 @@ public class Tootle.AccountsButton : Gtk.MenuButton{
if (account == null)
avatar.show_default (24);
else
Tootle.network.load_avatar (account.avatar, avatar, 24);
network.load_avatar (account.avatar, avatar, 24);
}
private void update_selection () {

View File

@ -1,73 +0,0 @@
using Gtk;
public class Tootle.HeaderBar : Gtk.HeaderBar{
public Granite.Widgets.ModeButton button_mode;
AccountsButton button_accounts;
Spinner spinner;
Button button_toot;
Button button_back;
private int last_tab = 0;
construct {
spinner = new Spinner ();
spinner.active = true;
button_accounts = new AccountsButton ();
button_back = new Button ();
button_back.label = _("Back");
button_back.get_style_context ().add_class (Granite.STYLE_CLASS_BACK_BUTTON);
button_back.clicked.connect (() => {
var primary_stack = Tootle.window.primary_stack;
var i = int.parse (primary_stack.get_visible_child_name ());
primary_stack.set_visible_child_name ((i-1).to_string ());
var child = primary_stack.get_child_by_name (i.to_string ());
child.destroy ();
var is_root = primary_stack.get_visible_child_name () == "0";
update (is_root);
});
button_toot = new Button ();
button_toot.tooltip_text = "Toot";
button_toot.image = new Gtk.Image.from_icon_name ("document-edit-symbolic", Gtk.IconSize.LARGE_TOOLBAR);
button_toot.clicked.connect (() => PostDialog.open ());
button_mode = new Granite.Widgets.ModeButton ();
button_mode.get_style_context ().add_class ("mode");
button_mode.mode_changed.connect(widget => {
last_tab = button_mode.selected;
Tootle.window.secondary_stack.set_visible_child_name(widget.tooltip_text);
});
button_mode.show ();
Tootle.network.started.connect (() => spinner.show ());
Tootle.network.finished.connect (() => spinner.hide ());
pack_start (button_back);
pack_start (button_toot);
pack_end (button_accounts);
pack_end (spinner);
}
public HeaderBar () {
custom_title = button_mode;
show_close_button = true;
show ();
button_mode.valign = Gtk.Align.FILL;
}
public void update (bool primary_mode){
button_mode.set_active (last_tab);
//button_mode.opacity = primary_mode ? 1 : 0;
//button_mode.sensitive = primary_mode ? true : false;
button_mode.set_visible (primary_mode);
button_toot.set_visible (primary_mode);
button_back.set_visible (!primary_mode);
button_accounts.set_visible (true);
}
}