feat: lists re-write (#36)
This commit is contained in:
commit
05390ab856
|
@ -44,6 +44,8 @@
|
|||
<file preprocess="xml-stripblanks">icons/scalable/actions/tooth-contact-new-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/tooth-dock-left-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/tooth-about-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/tooth-error-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">icons/scalable/actions/tooth-minus-large-symbolic.svg</file>
|
||||
|
||||
<file>gtk/dropdown/icon.ui</file>
|
||||
<file>gtk/dropdown/full.ui</file>
|
||||
|
@ -59,14 +61,12 @@
|
|||
<file>ui/widgets/profile_field_row.ui</file>
|
||||
<file>ui/widgets/timeline_menu.ui</file>
|
||||
<file>ui/widgets/list_item.ui</file>
|
||||
<file>ui/widgets/list_editor_item.ui</file>
|
||||
<file>ui/widgets/compose_attachment.ui</file>
|
||||
<file>ui/widgets/votebox.ui</file>
|
||||
<file>ui/dialogs/new_account.ui</file>
|
||||
<file>ui/dialogs/compose.ui</file>
|
||||
<file>ui/dialogs/main.ui</file>
|
||||
<file>ui/dialogs/preferences.ui</file>
|
||||
<file>ui/dialogs/list_editor.ui</file>
|
||||
<file>ui/menus.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 8 0.0390625 c -4.410156 0 -7.9726562 3.5624995 -7.9726562 7.9726565 c 0 4.40625 3.5625002 7.972656 7.9726562 7.972656 c 4.40625 0 7.972656 -3.566406 7.972656 -7.972656 c 0 -4.410157 -3.566406 -7.9726565 -7.972656 -7.9726565 z m -5 6.9726565 h 10 v 2 h -10 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 425 B |
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 1 7 h 14 v 2 h -14 z m 0 0" fill="#222222"/></svg>
|
After Width: | Height: | Size: 188 B |
|
@ -144,3 +144,7 @@
|
|||
.ttl-profile-stat-button:last-child {
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
.ttl-box-no-shadow > revealer > box {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
@ -1,241 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="ToothDialogsListEditor" parent="AdwWindow">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="default_width">300</property>
|
||||
<property name="default_height">400</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">0</property>
|
||||
<property name="width_request">300</property>
|
||||
<property name="height_request">500</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHeaderBar">
|
||||
<property name="visible">0</property>
|
||||
<child type="title">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">0</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">0</property>
|
||||
<property name="label" translatable="yes">Name</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="name_entry">
|
||||
<property name="visible">0</property>
|
||||
<property name="sensitive">0</property>
|
||||
<property name="width_chars">20</property>
|
||||
<signal name="changed" handler="validate" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_btn">
|
||||
<property name="visible">0</property>
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="width_request">80</property>
|
||||
<property name="receives_default">1</property>
|
||||
<signal name="clicked" handler="on_cancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="save_btn">
|
||||
<property name="visible">0</property>
|
||||
<property name="width_request">80</property>
|
||||
<property name="sensitive">0</property>
|
||||
<property name="receives_default">1</property>
|
||||
<signal name="clicked" handler="on_save_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkStack" id="save_btn_stack">
|
||||
<property name="visible">0</property>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">done</property>
|
||||
<property name="child">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">0</property>
|
||||
<property name="label" translatable="yes">Save</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="name">working</property>
|
||||
<property name="position">1</property>
|
||||
<property name="child">
|
||||
<object class="GtkSpinner">
|
||||
<property name="visible">0</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="active">True</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="pack_type">end</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="infobar">
|
||||
<property name="visible">0</property>
|
||||
<property name="message_type">error</property>
|
||||
<property name="show_close_button">1</property>
|
||||
<property name="revealed">0</property>
|
||||
<signal name="response" handler="infobar_response" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">0</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="infobar_label">
|
||||
<property name="visible">0</property>
|
||||
<property name="margin_start">6</property>
|
||||
<property name="margin_end">6</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="wrap">1</property>
|
||||
<property name="wrap_mode">word-char</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search_entry">
|
||||
<property name="visible">0</property>
|
||||
<property name="sensitive">0</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="has_default">1</property>
|
||||
<property name="margin_start">6</property>
|
||||
<property name="margin_end">6</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="width_chars">30</property>
|
||||
<property name="truncate_multiline">True</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">tooth-loupe-large-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text" translatable="yes">Search among people you follow</property>
|
||||
<signal name="search-changed" handler="on_search_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">0</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<property name="child">
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">0</property>
|
||||
<property name="child">
|
||||
<object class="GtkListBox" id="listbox">
|
||||
<property name="visible">0</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
<property name="selection_mode">none</property>
|
||||
<property name="activate_on_single_click">0</property>
|
||||
<child type="placeholder">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">0</property>
|
||||
<property name="opacity">0.35</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="margin_start">12</property>
|
||||
<property name="margin_end">12</property>
|
||||
<property name="margin_top">18</property>
|
||||
<property name="margin_bottom">18</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">0</property>
|
||||
<property name="pixel_size">48</property>
|
||||
<property name="icon_name">tooth-sentiment-dissatisfied-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">0</property>
|
||||
<property name="label" translatable="yes">Nobody here</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="wrap">1</property>
|
||||
<style>
|
||||
<class name="title-2"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="frame"/>
|
||||
</style>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -106,6 +106,11 @@
|
|||
</submenu> -->
|
||||
<!-- </section> -->
|
||||
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lists</attribute>
|
||||
<attribute name="action">view.ar_list</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Refresh</attribute>
|
||||
<attribute name="action">app.refresh</attribute>
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="ToothDialogsListEditorItem" parent="GtkListBoxRow">
|
||||
<property name="activatable">0</property>
|
||||
<property name="child">
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">0</property>
|
||||
<property name="margin_start">8</property>
|
||||
<property name="margin_end">8</property>
|
||||
<property name="margin_top">8</property>
|
||||
<property name="margin_bottom">8</property>
|
||||
<property name="row_spacing">8</property>
|
||||
<property name="column_spacing">8</property>
|
||||
<child>
|
||||
<object class="ToothWidgetsRichLabel" id="label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label">Display Name</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="xalign">0</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"></attribute>
|
||||
</attributes>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="ToothWidgetsRichLabel" id="handle">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="label">@handle</property>
|
||||
<property name="xalign">0</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="status">
|
||||
<property name="visible">0</property>
|
||||
<property name="width_request">32</property>
|
||||
<property name="height_request">32</property>
|
||||
<property name="sensitive">0</property>
|
||||
<property name="receives_default">1</property>
|
||||
<signal name="toggled" handler="on_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">0</property>
|
||||
<property name="icon_name">tooth-check-round-outline-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="flat"/>
|
||||
</style>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">0</property>
|
||||
<property name="row-span">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>
|
|
@ -1,7 +1,7 @@
|
|||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
meson build --prefix=/usr
|
||||
meson setup build --prefix=/usr
|
||||
cd build
|
||||
ninja
|
||||
sudo ninja install
|
||||
|
|
|
@ -65,7 +65,6 @@ sources = files(
|
|||
'src/Dialogs/Composer/EditorPage.vala',
|
||||
'src/Dialogs/Composer/Page.vala',
|
||||
'src/Dialogs/Composer/PollPage.vala',
|
||||
'src/Dialogs/ListEditor.vala',
|
||||
'src/Dialogs/MainWindow.vala',
|
||||
'src/Dialogs/NewAccount.vala',
|
||||
'src/Dialogs/Preferences.vala',
|
||||
|
|
|
@ -4,6 +4,7 @@ public class Tooth.API.List : Entity, Widgetizable {
|
|||
|
||||
public string id { get; set; }
|
||||
public string title { get; set; }
|
||||
public string? replies_policy { get; set; default = null; }
|
||||
|
||||
public static List from (Json.Node node) throws Error {
|
||||
return Entity.from_json (typeof (API.List), node) as API.List;
|
||||
|
|
|
@ -1,280 +0,0 @@
|
|||
using Gtk;
|
||||
|
||||
[GtkTemplate (ui = "/dev/geopjr/tooth/ui/dialogs/list_editor.ui")]
|
||||
public class Tooth.Dialogs.ListEditor: Adw.Window {
|
||||
|
||||
[GtkTemplate (ui = "/dev/geopjr/tooth/ui/widgets/list_editor_item.ui")]
|
||||
class Item : ListBoxRow {
|
||||
|
||||
public ListEditor editor { get; construct set; }
|
||||
public API.Account acc { get; construct set; }
|
||||
public bool committed { get; construct set; }
|
||||
|
||||
[GtkChild] unowned Widgets.RichLabel label;
|
||||
[GtkChild] unowned Widgets.RichLabel handle;
|
||||
[GtkChild] unowned ToggleButton status;
|
||||
|
||||
public Item (ListEditor editor, API.Account acc, bool committed) {
|
||||
this.editor = editor;
|
||||
this.acc = acc;
|
||||
this.committed = committed;
|
||||
acc.bind_property ("display-name", label, "text", BindingFlags.SYNC_CREATE);
|
||||
acc.bind_property ("handle", handle, "text", BindingFlags.SYNC_CREATE);
|
||||
status.active = committed;
|
||||
status.sensitive = true;
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
void on_toggled () {
|
||||
if (!status.sensitive)
|
||||
return;
|
||||
|
||||
if (status.active) {
|
||||
debug (@"To add: $(acc.id)");
|
||||
editor.to_add.add (acc.id);
|
||||
editor.to_remove.remove (acc.id);
|
||||
}
|
||||
else {
|
||||
debug (@"To remove: $(acc.id)");
|
||||
editor.to_add.remove (acc.id);
|
||||
editor.to_remove.add (acc.id);
|
||||
}
|
||||
committed = status.active;
|
||||
if (!editor.working)
|
||||
editor.dirty = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public API.List list { get; set; }
|
||||
public bool working { get; set; default = false; }
|
||||
public bool exists { get; set; default = false; }
|
||||
public bool dirty { get; set; default = false; }
|
||||
|
||||
Soup.Message? search_req = null;
|
||||
|
||||
public Gee.ArrayList<string> to_add = new Gee.ArrayList<string> ();
|
||||
public Gee.ArrayList<string> to_remove = new Gee.ArrayList<string> ();
|
||||
|
||||
[GtkChild] unowned Button save_btn;
|
||||
[GtkChild] unowned Stack save_btn_stack;
|
||||
[GtkChild] unowned Entry name_entry;
|
||||
[GtkChild] unowned SearchEntry search_entry;
|
||||
[GtkChild] unowned ListBox listbox;
|
||||
|
||||
[GtkChild] unowned InfoBar infobar;
|
||||
[GtkChild] unowned Label infobar_label;
|
||||
|
||||
public signal void done ();
|
||||
|
||||
construct {
|
||||
transient_for = app.main_window;
|
||||
show ();
|
||||
}
|
||||
|
||||
public ListEditor.empty () {
|
||||
var obj = new API.List () {
|
||||
title = _("Untitled")
|
||||
};
|
||||
Object (list: obj);
|
||||
init ();
|
||||
}
|
||||
|
||||
public ListEditor (API.List list) {
|
||||
Object (list: list, working: true, exists: true);
|
||||
init ();
|
||||
|
||||
new Request.GET (@"/api/v1/lists/$(list.id)/accounts")
|
||||
.with_account (accounts.active)
|
||||
.with_ctx (this)
|
||||
.on_error (on_error)
|
||||
.then ((sess, msg) => {
|
||||
Network.parse_array (msg, node => {
|
||||
var acc = API.Account.from (node);
|
||||
add_account (acc, true);
|
||||
});
|
||||
working = false;
|
||||
})
|
||||
.exec ();
|
||||
}
|
||||
|
||||
void init () {
|
||||
notify["working"].connect (on_state_changed);
|
||||
list.bind_property ("title", name_entry, "text", BindingFlags.SYNC_CREATE);
|
||||
|
||||
ulong dirty_sigid = 0;
|
||||
dirty_sigid = name_entry.changed.connect (() => {
|
||||
dirty = true;
|
||||
name_entry.disconnect (dirty_sigid);
|
||||
});
|
||||
|
||||
on_state_changed (null);
|
||||
}
|
||||
|
||||
void on_state_changed (ParamSpec? p) {
|
||||
save_btn_stack.visible_child_name = working ? "working" : "done";
|
||||
save_btn.sensitive = search_entry.sensitive = name_entry.sensitive = !working;
|
||||
}
|
||||
|
||||
void on_error (int32 code, string msg) {
|
||||
warning (msg);
|
||||
infobar_label.label = msg;
|
||||
infobar.revealed = true;
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
void infobar_response (int i) {
|
||||
infobar.revealed = false;
|
||||
}
|
||||
|
||||
void request_search (string q) {
|
||||
debug (@"Searching for: \"$q\"...");
|
||||
if (search_req != null) {
|
||||
network.cancel (search_req);
|
||||
search_req = null;
|
||||
}
|
||||
|
||||
search_req = new Request.GET ("/api/v1/accounts/search")
|
||||
.with_account (accounts.active)
|
||||
.with_ctx (this)
|
||||
.with_param ("resolve", "false")
|
||||
.with_param ("limit", "8")
|
||||
.with_param ("following", "true")
|
||||
.with_param ("q", q)
|
||||
.then ((sess, msg) => {
|
||||
Network.parse_array (msg, node => {
|
||||
var acc = API.Account.from (node);
|
||||
add_account (acc, false, 0);
|
||||
});
|
||||
})
|
||||
.on_error (on_error)
|
||||
.exec ();
|
||||
}
|
||||
|
||||
void add_account (API.Account acc, bool added, int order = -1) {
|
||||
var exists = false;
|
||||
// listbox.@foreach (w => {
|
||||
// var i = w as Item;
|
||||
// if (i != null) {
|
||||
// if (i.acc.id == acc.id)
|
||||
// exists = true;
|
||||
// }
|
||||
// });
|
||||
|
||||
if (!exists) {
|
||||
var item = new Item (this, acc, added);
|
||||
listbox.insert (item, order);
|
||||
}
|
||||
}
|
||||
|
||||
void invalidate () {
|
||||
// listbox.@foreach (w => {
|
||||
// var i = w as Item;
|
||||
// if (i != null) {
|
||||
// if (!i.committed)
|
||||
// i.destroy ();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
|
||||
|
||||
[GtkCallback]
|
||||
void validate () {
|
||||
var has_title = name_entry.text.replace (" ", "") != "";
|
||||
save_btn.sensitive = has_title;
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
void on_cancel_clicked () {
|
||||
if (dirty) {
|
||||
var dlg = app.question (
|
||||
_("Discard changes?"),
|
||||
_("You need to save the list if you want to keep them."),
|
||||
this,
|
||||
_("Discard"),
|
||||
Adw.ResponseAppearance.DESTRUCTIVE
|
||||
);
|
||||
|
||||
dlg.response.connect(res => {
|
||||
if (res == "yes") {
|
||||
destroy ();
|
||||
}
|
||||
dlg.destroy();
|
||||
});
|
||||
|
||||
dlg.present ();
|
||||
}
|
||||
else
|
||||
destroy ();
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
void on_search_changed () {
|
||||
var q = search_entry.text.chug ().chomp ();
|
||||
|
||||
if (q.char_count () < 3)
|
||||
invalidate ();
|
||||
else if (q != "") {
|
||||
invalidate ();
|
||||
request_search (q);
|
||||
}
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
void on_save_clicked () {
|
||||
working = true;
|
||||
transaction.begin ((obj, res) => {
|
||||
try {
|
||||
transaction.end (res);
|
||||
|
||||
done ();
|
||||
destroy ();
|
||||
}
|
||||
catch (Error e) {
|
||||
working = false;
|
||||
on_error (0, e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async void transaction () throws Error {
|
||||
if (!exists) {
|
||||
message ("Creating list...");
|
||||
var req = new Request.POST ("/api/v1/lists")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("title", name_entry.text);
|
||||
yield req.await ();
|
||||
|
||||
message ("Received new List entity");
|
||||
var node = network.parse_node (req);
|
||||
list = API.List.from (node);
|
||||
}
|
||||
else {
|
||||
message ("Updating list title...");
|
||||
yield new Request.PUT (@"/api/v1/lists/$(list.id)")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("title", name_entry.text)
|
||||
.await ();
|
||||
}
|
||||
|
||||
if (!to_add.is_empty) {
|
||||
message ("Adding accounts to list...");
|
||||
var id_array = Request.array2string (to_add, "account_ids");
|
||||
yield new Request.POST (@"/api/v1/lists/$(list.id)/accounts/?$id_array")
|
||||
.with_account (accounts.active)
|
||||
.await ();
|
||||
}
|
||||
|
||||
if (!to_remove.is_empty) {
|
||||
message ("Removing accounts from list...");
|
||||
var id_array = Request.array2string (to_remove, "account_ids");
|
||||
yield new Request.DELETE (@"/api/v1/lists/$(list.id)/accounts/?$id_array")
|
||||
.with_account (accounts.active)
|
||||
.await ();
|
||||
}
|
||||
|
||||
message ("OK: List updated");
|
||||
list.title = name_entry.text;
|
||||
}
|
||||
|
||||
}
|
|
@ -3,32 +3,58 @@ using Gtk;
|
|||
// TODO: Lists is borken
|
||||
public class Tooth.Views.Lists : Views.Timeline {
|
||||
|
||||
[GtkTemplate (ui = "/dev/geopjr/tooth/ui/widgets/list_item.ui")]
|
||||
public class Row : ListBoxRow {
|
||||
public class Row : Adw.ActionRow {
|
||||
public API.List? list;
|
||||
Button delete_button;
|
||||
Button edit_button;
|
||||
|
||||
API.List? list;
|
||||
construct {
|
||||
var action_box = new Box(Orientation.HORIZONTAL, 6);
|
||||
|
||||
[GtkChild] unowned Stack stack;
|
||||
[GtkChild] unowned Label title;
|
||||
edit_button = new Button() {
|
||||
icon_name = "tooth-edit-symbolic",
|
||||
valign = Align.CENTER,
|
||||
halign = Align.CENTER
|
||||
};
|
||||
edit_button.add_css_class("flat");
|
||||
edit_button.add_css_class("circular");
|
||||
|
||||
delete_button = new Button() {
|
||||
icon_name = "tooth-trash-symbolic",
|
||||
valign = Align.CENTER,
|
||||
halign = Align.CENTER
|
||||
};
|
||||
delete_button.add_css_class("flat");
|
||||
delete_button.add_css_class("circular");
|
||||
delete_button.add_css_class("error");
|
||||
delete_button.clicked.connect(on_remove_clicked);
|
||||
|
||||
// this.apply.connect(on_apply);
|
||||
action_box.append(edit_button);
|
||||
action_box.append(delete_button);
|
||||
|
||||
this.activated.connect(() => open());
|
||||
this.activatable = true;
|
||||
|
||||
this.add_suffix(action_box);
|
||||
}
|
||||
|
||||
public Row (API.List? list) {
|
||||
this.list = list;
|
||||
|
||||
if (list == null)
|
||||
stack.visible_child_name = "add";
|
||||
else
|
||||
list.bind_property ("title", title, "label", BindingFlags.SYNC_CREATE);
|
||||
if (list != null) {
|
||||
this.list.bind_property ("title", this, "title", BindingFlags.SYNC_CREATE);
|
||||
edit_button.clicked.connect(() => {
|
||||
create_edit_preferences_window(this.list).show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[GtkCallback]
|
||||
void on_edit_clicked () {
|
||||
new Dialogs.ListEditor (this.list);
|
||||
}
|
||||
public virtual signal void remove_from_model () {}
|
||||
|
||||
[GtkCallback]
|
||||
void on_remove_clicked () {
|
||||
var remove = app.question (
|
||||
_("Delete \"%s\"?").printf (list.title),
|
||||
_("Delete \"%s\"?").printf (this.list.title),
|
||||
_("This action cannot be reverted."),
|
||||
app.main_window,
|
||||
_("Delete"),
|
||||
|
@ -38,9 +64,12 @@ public class Tooth.Views.Lists : Views.Timeline {
|
|||
remove.response.connect(res => {
|
||||
if (res == "yes") {
|
||||
new Request.DELETE (@"/api/v1/lists/$(list.id)")
|
||||
.with_account (accounts.active)
|
||||
.then (() => { this.destroy (); })
|
||||
.exec ();
|
||||
.with_account (accounts.active)
|
||||
.then (() => {
|
||||
remove_from_model();
|
||||
this.destroy ();
|
||||
})
|
||||
.exec ();
|
||||
}
|
||||
remove.destroy();
|
||||
});
|
||||
|
@ -48,6 +77,171 @@ public class Tooth.Views.Lists : Views.Timeline {
|
|||
remove.present ();
|
||||
}
|
||||
|
||||
public Adw.PreferencesWindow create_edit_preferences_window(API.List t_list) {
|
||||
var edit_preferences_window = new Adw.PreferencesWindow() {
|
||||
modal = true,
|
||||
title = _("Edit \"%s\"").printf (t_list.title),
|
||||
transient_for = app.main_window
|
||||
};
|
||||
var list_settings_page_general = new Adw.PreferencesPage() {
|
||||
icon_name = "tooth-gear-symbolic",
|
||||
title = _("General")
|
||||
};
|
||||
var info_group = new Adw.PreferencesGroup() {
|
||||
title = _("Info")
|
||||
};
|
||||
var title_row = new Adw.EntryRow() {
|
||||
input_purpose = InputPurpose.FREE_FORM,
|
||||
title = _("List Name"),
|
||||
text = t_list.title
|
||||
};
|
||||
info_group.add(title_row);
|
||||
list_settings_page_general.add(info_group);
|
||||
|
||||
string? replies_policy_active = null;
|
||||
if (t_list.replies_policy != null) {
|
||||
var replies_group = new Adw.PreferencesGroup() {
|
||||
title = _("Replies Policy"),
|
||||
description = _("Show member replies to")
|
||||
};
|
||||
var none_radio = new CheckButton();
|
||||
var none_row = new Adw.ActionRow() {
|
||||
title = _("Nobody"),
|
||||
activatable_widget = none_radio
|
||||
};
|
||||
none_row.add_prefix(none_radio);
|
||||
none_radio.toggled.connect(() => {
|
||||
if (none_radio.active)
|
||||
replies_policy_active = "none";
|
||||
});
|
||||
|
||||
var list_radio = new CheckButton();
|
||||
list_radio.group = none_radio;
|
||||
var list_row = new Adw.ActionRow() {
|
||||
title = _("Other members of the list"),
|
||||
activatable_widget = list_radio
|
||||
};
|
||||
list_row.add_prefix(list_radio);
|
||||
list_radio.toggled.connect(() => {
|
||||
if (list_radio.active)
|
||||
replies_policy_active = "list";
|
||||
});
|
||||
|
||||
var followed_radio = new CheckButton();
|
||||
followed_radio.group = none_radio;
|
||||
var followed_row = new Adw.ActionRow() {
|
||||
title = _("Any followed user"),
|
||||
activatable_widget = followed_radio
|
||||
};
|
||||
followed_row.add_prefix(followed_radio);
|
||||
followed_radio.toggled.connect(() => {
|
||||
if (followed_radio.active)
|
||||
replies_policy_active = "followed";
|
||||
});
|
||||
|
||||
switch (t_list.replies_policy) {
|
||||
case "none":
|
||||
none_radio.active = true;
|
||||
break;
|
||||
case "followed":
|
||||
followed_radio.active = true;
|
||||
break;
|
||||
default:
|
||||
list_radio.active = true;
|
||||
break;
|
||||
}
|
||||
|
||||
replies_group.add(none_row);
|
||||
replies_group.add(list_row);
|
||||
replies_group.add(followed_row);
|
||||
|
||||
list_settings_page_general.add(replies_group);
|
||||
}
|
||||
|
||||
var to_remove = new Gee.ArrayList<string>();
|
||||
new Request.GET (@"/api/v1/lists/$(t_list.id)/accounts")
|
||||
.with_account (accounts.active)
|
||||
.then ((sess, msg) => {
|
||||
if (Network.get_array_size(msg) > 0) {
|
||||
var list_settings_page_members = new Adw.PreferencesPage() {
|
||||
icon_name = "tooth-people-symbolic",
|
||||
title = _("Members")
|
||||
};
|
||||
|
||||
var rm_group = new Adw.PreferencesGroup() {
|
||||
title = _("Remove Members")
|
||||
};
|
||||
|
||||
Network.parse_array (msg, node => {
|
||||
var member = API.Account.from (node);
|
||||
var avi = new Widgets.Avatar() {
|
||||
account = member,
|
||||
size = 32
|
||||
};
|
||||
var m_switch = new Switch() {
|
||||
active = true,
|
||||
state = true,
|
||||
valign = Align.CENTER,
|
||||
halign = Align.CENTER
|
||||
};
|
||||
m_switch.state_set.connect((x) => {
|
||||
if (!x) {
|
||||
to_remove.add(member.id);
|
||||
} else if (to_remove.contains(member.id)) {
|
||||
to_remove.remove(member.id);
|
||||
}
|
||||
|
||||
return x;
|
||||
});
|
||||
|
||||
var member_row = new Adw.ActionRow() {
|
||||
title = member.full_handle
|
||||
};
|
||||
member_row.add_prefix(avi);
|
||||
member_row.add_suffix(m_switch);
|
||||
|
||||
rm_group.add(member_row);
|
||||
});
|
||||
|
||||
list_settings_page_members.add(rm_group);
|
||||
edit_preferences_window.add(list_settings_page_members);
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
|
||||
edit_preferences_window.add(list_settings_page_general);
|
||||
|
||||
edit_preferences_window.close_request.connect(() => {
|
||||
on_apply(t_list, title_row.text, replies_policy_active, to_remove);
|
||||
edit_preferences_window.hide();
|
||||
edit_preferences_window.destroy();
|
||||
return false;
|
||||
});
|
||||
|
||||
return edit_preferences_window;
|
||||
}
|
||||
|
||||
public void on_apply(API.List t_list, string title, string? replies_policy, Gee.ArrayList<string> to_remove) {
|
||||
if (t_list.title != title || t_list.replies_policy != replies_policy) {
|
||||
this.list.title = title;
|
||||
this.list.replies_policy = replies_policy;
|
||||
new Request.PUT (@"/api/v1/lists/$(t_list.id)")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("title", title)
|
||||
.with_param ("replies_policy", replies_policy)
|
||||
.then(() => {})
|
||||
.exec ();
|
||||
}
|
||||
|
||||
if (to_remove.size > 0) {
|
||||
var id_array = Request.array2string (to_remove, "account_ids");
|
||||
new Request.DELETE (@"/api/v1/lists/$(t_list.id)/accounts/?$id_array")
|
||||
.with_account (accounts.active)
|
||||
.then(() => {})
|
||||
.exec ();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual signal void open () {
|
||||
if (this.list == null)
|
||||
return;
|
||||
|
@ -61,6 +255,25 @@ public class Tooth.Views.Lists : Views.Timeline {
|
|||
get { return false; }
|
||||
}
|
||||
|
||||
public override Widget on_create_model_widget(Object obj) {
|
||||
var widget = base.on_create_model_widget(obj);
|
||||
var widget_row = widget as Row;
|
||||
|
||||
if (widget_row != null)
|
||||
widget_row.remove_from_model.connect(() => remove_list(widget_row.list));
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
public void remove_list(API.List? list) {
|
||||
if (list == null) return;
|
||||
|
||||
uint indx;
|
||||
var found = model.find (list, out indx);
|
||||
if (found)
|
||||
model.remove(indx);
|
||||
}
|
||||
|
||||
public Lists () {
|
||||
Object (
|
||||
url: @"/api/v1/lists",
|
||||
|
@ -70,14 +283,58 @@ public class Tooth.Views.Lists : Views.Timeline {
|
|||
accepts = typeof (API.List);
|
||||
}
|
||||
|
||||
public void create_list(string list_name) {
|
||||
new Request.POST ("/api/v1/lists")
|
||||
.with_account (accounts.active)
|
||||
.with_param ("title", list_name)
|
||||
.then ((sess, msg) => {
|
||||
var node = network.parse_node (msg);
|
||||
var list = API.List.from (node);
|
||||
model.insert (0, list);
|
||||
})
|
||||
.exec ();
|
||||
}
|
||||
|
||||
public void on_action_bar_activate(EntryBuffer buffer) {
|
||||
if (buffer.length > 0)
|
||||
create_list(buffer.text);
|
||||
buffer.set_text("".data);
|
||||
}
|
||||
|
||||
construct {
|
||||
var add_action_bar = new ActionBar ();
|
||||
add_action_bar.add_css_class("ttl-box-no-shadow");
|
||||
|
||||
var child_box = new Box(Orientation.HORIZONTAL, 6);
|
||||
var child_entry = new Entry() {
|
||||
input_purpose = InputPurpose.FREE_FORM,
|
||||
placeholder_text = _("New list title")
|
||||
};
|
||||
var add_button = new Button.with_label (_("Add list")) {
|
||||
sensitive = false
|
||||
};
|
||||
|
||||
add_button.clicked.connect(() => {
|
||||
on_action_bar_activate(child_entry.buffer);
|
||||
});
|
||||
child_entry.activate.connect(() => {
|
||||
on_action_bar_activate(child_entry.buffer);
|
||||
});
|
||||
|
||||
child_entry.buffer.bind_property("length", add_button, "sensitive", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
|
||||
target.set_boolean ((uint) src > 0);
|
||||
return true;
|
||||
});
|
||||
|
||||
child_box.append(child_entry);
|
||||
child_box.append(add_button);
|
||||
|
||||
add_action_bar.set_center_widget(child_box);
|
||||
insert_child_after (add_action_bar, header);
|
||||
}
|
||||
|
||||
public override void on_request_finish () {
|
||||
var add_row = new Row (null);
|
||||
add_row.open.connect (() => {
|
||||
var dlg = new Dialogs.ListEditor.empty ();
|
||||
dlg.done.connect (on_refresh);
|
||||
});
|
||||
append (add_row);
|
||||
on_content_changed ();
|
||||
on_content_changed ();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ public class Tooth.Views.Profile : Views.Timeline {
|
|||
protected SimpleAction hiding_reblogs_action;
|
||||
protected SimpleAction blocking_action;
|
||||
protected SimpleAction domain_blocking_action;
|
||||
protected SimpleAction ar_list_action;
|
||||
// protected SimpleAction source_action;
|
||||
|
||||
construct {
|
||||
|
@ -35,6 +36,7 @@ public class Tooth.Views.Profile : Views.Timeline {
|
|||
);
|
||||
cover.bind (profile);
|
||||
build_profile_stats(cover.info);
|
||||
rs.invalidated.connect (() => invalidate_actions(false));
|
||||
}
|
||||
|
||||
[GtkTemplate (ui = "/dev/geopjr/tooth/ui/views/profile_header.ui")]
|
||||
|
@ -164,6 +166,11 @@ public class Tooth.Views.Profile : Views.Timeline {
|
|||
// invalidate_actions (true);
|
||||
// });
|
||||
// actions.add_action (source_action);
|
||||
ar_list_action = new SimpleAction ("ar_list", null);
|
||||
ar_list_action.activate.connect (v => {
|
||||
create_ar_list_dialog().show();
|
||||
});
|
||||
actions.add_action (ar_list_action);
|
||||
|
||||
var mention_action = new SimpleAction ("mention", VariantType.STRING);
|
||||
mention_action.activate.connect (v => {
|
||||
|
@ -268,6 +275,7 @@ public class Tooth.Views.Profile : Views.Timeline {
|
|||
blocking_action.set_state (rs.blocking);
|
||||
domain_blocking_action.set_state (rs.domain_blocking);
|
||||
domain_blocking_action.set_enabled (accounts.active.domain != profile.domain);
|
||||
ar_list_action.set_enabled(profile.id != accounts.active.id && rs.following);
|
||||
|
||||
if (refresh) {
|
||||
page_next = null;
|
||||
|
@ -320,4 +328,142 @@ public class Tooth.Views.Profile : Views.Timeline {
|
|||
network.on_error);
|
||||
}
|
||||
|
||||
public class RowButton : Button {
|
||||
public bool remove { get; set; default = false; }
|
||||
}
|
||||
|
||||
public Adw.Window create_ar_list_dialog() {
|
||||
var spinner = new Spinner() {
|
||||
spinning = true,
|
||||
halign = Align.CENTER,
|
||||
valign = Align.CENTER,
|
||||
vexpand = true,
|
||||
hexpand = true,
|
||||
width_request = 32,
|
||||
height_request = 32
|
||||
};
|
||||
var box = new Box(Orientation.VERTICAL, 6);
|
||||
var headerbar = new Adw.HeaderBar();
|
||||
var toast_overlay = new Adw.ToastOverlay() {
|
||||
vexpand = true,
|
||||
valign = Align.CENTER
|
||||
};
|
||||
toast_overlay.child = spinner;
|
||||
|
||||
box.append(headerbar);
|
||||
box.append(toast_overlay);
|
||||
var dialog = new Adw.Window() {
|
||||
title = _("Add or remove \"%s\" to or from a list").printf (profile.handle),
|
||||
modal = true,
|
||||
transient_for = app.main_window,
|
||||
content = box,
|
||||
default_width = 600,
|
||||
default_height = 550
|
||||
};
|
||||
spinner.start();
|
||||
|
||||
var preferences_page = new Adw.PreferencesPage();
|
||||
var preferences_group = new Adw.PreferencesGroup() {
|
||||
title = _("Select the list to add or remove \"%s\" to or from:").printf (profile.handle)
|
||||
};
|
||||
|
||||
var no_lists_page = new Adw.StatusPage() {
|
||||
icon_name = "tooth-error-symbolic",
|
||||
vexpand = true,
|
||||
title = _("You don't have any lists")
|
||||
};
|
||||
|
||||
new Request.GET (@"/api/v1/lists/")
|
||||
.with_account (accounts.active)
|
||||
.with_ctx (this)
|
||||
.on_error (on_error)
|
||||
.then ((sess, msg) => {
|
||||
if (Network.get_array_size(msg) > 0) {
|
||||
new Request.GET (@"/api/v1/accounts/$(profile.id)/lists")
|
||||
.with_account (accounts.active)
|
||||
.with_ctx (this)
|
||||
.on_error (on_error)
|
||||
.then ((sess2, msg2) => {
|
||||
var added = false;
|
||||
var in_list = new Gee.ArrayList<string>();
|
||||
|
||||
Network.parse_array (msg2, node => {
|
||||
var list = API.List.from (node);
|
||||
in_list.add(list.id);
|
||||
});
|
||||
Network.parse_array (msg, node => {
|
||||
var list = API.List.from (node);
|
||||
var is_already = in_list.contains(list.id);
|
||||
|
||||
var add_button = new RowButton() {
|
||||
icon_name = is_already ? "tooth-minus-large-symbolic" : "tooth-plus-large-symbolic",
|
||||
tooltip_text = is_already ? _("Remove \"%s\" from \"%s\"").printf (profile.handle, list.title) : _("Add \"%s\" to \"%s\"").printf (profile.handle, list.title),
|
||||
halign = Align.CENTER,
|
||||
valign = Align.CENTER
|
||||
};
|
||||
add_button.add_css_class("flat");
|
||||
add_button.add_css_class("circular");
|
||||
add_button.remove = is_already;
|
||||
|
||||
var row = new Adw.ActionRow() {
|
||||
title = list.title
|
||||
};
|
||||
row.add_suffix(add_button);
|
||||
|
||||
add_button.clicked.connect(() => {
|
||||
handle_list_edit(list, row, toast_overlay, add_button);
|
||||
});
|
||||
|
||||
preferences_group.add(row);
|
||||
added = true;
|
||||
});
|
||||
|
||||
if (added) {
|
||||
preferences_page.add(preferences_group);
|
||||
|
||||
toast_overlay.child = preferences_page;
|
||||
toast_overlay.valign = Align.FILL;
|
||||
} else {
|
||||
toast_overlay.child = no_lists_page;
|
||||
}
|
||||
})
|
||||
.exec();
|
||||
} else {
|
||||
toast_overlay.child = no_lists_page;
|
||||
}
|
||||
})
|
||||
.exec ();
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public void handle_list_edit(API.List list, Adw.ActionRow row, Adw.ToastOverlay toast_overlay, RowButton button) {
|
||||
row.sensitive = false;
|
||||
|
||||
var endpoint = @"/api/v1/lists/$(list.id)/accounts/?account_ids[]=$(profile.id)";
|
||||
var req = button.remove ? new Request.DELETE (endpoint) : new Request.POST (endpoint);
|
||||
req
|
||||
.with_account (accounts.active)
|
||||
.with_ctx (this)
|
||||
.on_error (on_error)
|
||||
.then ((sess, msg) => {
|
||||
var toast_msg = "";
|
||||
if (button.remove) {
|
||||
toast_msg = _("User \"%s\" got removed from \"%s\"").printf (profile.handle, list.title);
|
||||
button.icon_name = "tooth-plus-large-symbolic";
|
||||
button.tooltip_text = _("Add \"%s\" to \"%s\"").printf (profile.handle, list.title);
|
||||
} else {
|
||||
toast_msg = _("User \"%s\" got added to \"%s\"").printf (profile.handle, list.title);
|
||||
button.icon_name = "tooth-minus-large-symbolic";
|
||||
button.tooltip_text = _("Remove \"%s\" from \"%s\"").printf (profile.handle, list.title);
|
||||
}
|
||||
|
||||
button.remove = !button.remove;
|
||||
row.sensitive = true;
|
||||
|
||||
var toast = new Adw.Toast(toast_msg);
|
||||
toast_overlay.add_toast(toast);
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue