Compare commits

...

4 commits

5 changed files with 515 additions and 95 deletions

View file

@ -24,14 +24,16 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Linq;
using System.Diagnostics;
using CsvHelper;
using Gtk;
namespace Buypeeb {
class MainWindow : Window {
private string location;
private JsonSerializerOptions jsonOptions;
private ListStore items;
private Settings settings;
@ -45,11 +47,13 @@ namespace Buypeeb {
private Box selectionViewBox;
private Label endingLabel;
private bool queueActive;
private Dictionary<string, CheckButton> filterChecks = new Dictionary<string, CheckButton>();
// ...to here.
static SemaphoreSlim taskLimit = new SemaphoreSlim(6);
private Queue<string> updateQueue = new Queue<string>();
private IEnumerable<YahooAuctionsItem> filterQuery;
private YahooAuctionsItem selectedItem {
get {
@ -66,6 +70,11 @@ namespace Buypeeb {
public MainWindow() : this(new Builder("main.glade")) { }
private MainWindow(Builder builder) : base(builder.GetObject("wndMain").Handle) {
this.jsonOptions = new JsonSerializerOptions {
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.Create(System.Text.Unicode.UnicodeRanges.All)
};
if (Environment.OSVersion.Platform == PlatformID.Win32NT) {
// C:\Users\Beebus\AppData\Roaming\Lynnear Software\buypeeb
this.location = System.IO.Path.Combine(Environment.ExpandEnvironmentVariables("%APPDATA%"), "Lynnear Software", "buypeeb");
@ -99,13 +108,33 @@ namespace Buypeeb {
this.selectionViewBox = (Box)builder.GetObject("SelectionViewBox");
this.endingLabel = (Label)builder.GetObject("LabelSelectedEnding");
foreach (var name in new List<string> { "Favourites", "NonFavourites", "Active", "Ended", "EndingToday", "EndingAfterToday", "WithWinPrice", "WithNoWinPrice" }) {
this.filterChecks.Add(name, (CheckButton)builder.GetObject($"CheckButtonFilter{name}"));
this.filterChecks[name].Active = false;
}
// father forgive me for i have lynned
this.filterQuery =
from item in this.settings.watchlist.Values.ToList()
where (item.favourite != this.filterChecks["Favourites"].Active ||
item.favourite == this.filterChecks["NonFavourites"].Active) &&
(item.available != this.filterChecks["Active"].Active ||
item.available == this.filterChecks["Ended"].Active) &&
(item.endingToday != this.filterChecks["EndingToday"].Active ||
item.endingToday == this.filterChecks["EndingAfterToday"].Active) &&
(item.hasWinPrice != this.filterChecks["WithWinPrice"].Active ||
item.hasWinPrice == this.filterChecks["WithNoWinPrice"].Active)
select item;
Console.WriteLine(this.filterChecks["Favourites"].Active);
// bind treeview columns to watchlist instead of needing to manually sync its liststore
this.itemTreeView = (TreeView)builder.GetObject("TreeViewItems");
this.items = new ListStore(typeof(YahooAuctionsItem));
this.RenderList();
var filteredItems = new TreeModelFilter(this.items, null);
filteredItems.VisibleFunc = this.ItemFilter;
this.itemTreeView.Model = this.items;
this.itemTreeView.Model = filteredItems;
TreeCellDataFunc[] funcs = {
new TreeCellDataFunc(this.RenderColumnFavourite),
new TreeCellDataFunc(this.RenderColumnName),
@ -120,6 +149,7 @@ namespace Buypeeb {
c.SetCellDataFunc(c.Cells[0], funcs[i]);
}
this.RenderList();
this.UpdateItems();
GLib.Timeout.Add(1000, new GLib.TimeoutHandler(UpdateSelectionEndTime));
@ -137,11 +167,13 @@ namespace Buypeeb {
// TODO: surely there's a better way to do this
TreeIter iter;
this.itemTreeView.Model.GetIterFirst(out iter);
var m = (TreeModelFilter)this.itemTreeView.Model;
for (int i = 0; i < this.itemTreeView.Model.IterNChildren(); i++) {
var x = (YahooAuctionsItem)this.itemTreeView.Model.GetValue(iter, 0);
Console.WriteLine(x);
if (x.id == id) {
return (this.itemTreeView.Model.GetPath(iter), iter);
return (m.ConvertPathToChildPath(m.GetPath(iter)), m.ConvertIterToChildIter(iter));
}
else {
this.itemTreeView.Model.IterNext(ref iter);
@ -153,7 +185,7 @@ namespace Buypeeb {
}
private void SaveSettings() {
string j = JsonSerializer.Serialize(this.settings);
string j = JsonSerializer.Serialize(this.settings, this.jsonOptions);
string p = System.IO.Path.Combine(this.location, "userdata.json");
Console.WriteLine(j);
if (!Directory.Exists(this.location)) {
@ -176,18 +208,23 @@ namespace Buypeeb {
Gtk.Application.Invoke(delegate {
// TODO: find a way to not have to do this. i think we need to avoid actually modifying the items outside of the main thread :/
var pathAndIter = this.GetRow(id);
this.items.EmitRowChanged(pathAndIter.path, pathAndIter.iter);
if (pathAndIter.path != null) {
this.items.EmitRowChanged(pathAndIter.path, pathAndIter.iter);
}
});
using (WebClient client = new WebClient()) {
// TODO: download should have timeout
item.Update(client.DownloadString(item.url));
// item.Update(File.ReadAllText("yahoo.html"));
// item.Update(client.DownloadString(item.url));
Thread.Sleep(5000);
item.Update(File.ReadAllText("yahoo.html"));
}
Gtk.Application.Invoke(delegate {
var pathAndIter = this.GetRow(id);
this.items.EmitRowChanged(pathAndIter.path, pathAndIter.iter);
if (pathAndIter.path != null) {
this.items.EmitRowChanged(pathAndIter.path, pathAndIter.iter);
}
if (item == this.selectedItem) {
// if the user has this item selected and it just became ready, enable the selection box
this.selectionViewBox.Sensitive = true;
@ -272,11 +309,11 @@ namespace Buypeeb {
var info = new Dictionary<string, string> {
{ "Name", item.name },
{ "YahooName", item.originalName },
{ "Price", item.PriceJPY() },
{ "PriceAUD", item.PriceAUD() },
{ "Price", item.priceJPY },
{ "PriceAUD", item.priceAUD },
{ "Ending", "..." },
{ "Bids", $"{item.bids}" },
{ "BuyItNow", item.winPrice == 0 ? "No" : $"¥{item.PriceJPY(true)} (${item.PriceAUD(true)})" },
{ "BuyItNow", item.winPrice == 0 ? "No" : $"{item.winPriceJPY} ({item.winPriceAUD})" },
{ "AutoExtension", item.autoExtension ? "Yes" : "No" },
{ "LastUpdated", "Last updated: heeeenlo" }
};
@ -305,25 +342,31 @@ namespace Buypeeb {
}
}
private MessageDialog OkCancelDialogue(string message) {
private MessageDialog MsgBox(string message, ButtonsType buttonsType = ButtonsType.OkCancel) {
var md = new MessageDialog(
parent_window: this,
flags: DialogFlags.DestroyWithParent | DialogFlags.Modal,
type: MessageType.Question,
bt: ButtonsType.OkCancel,
bt: buttonsType,
format: message
);
md.KeepAbove = true;
md.Resizable = false;
md.FocusOnMap = true;
md.Title = "buypeeb";
md.Title = "Buypeeb";
return md;
}
// show a simple entry dialogue that allows the user to enter text and either cancel or submit it
private (Boolean accepted, string response) EntryDialogue(string title = "Buypeeb", string message = "Hi there!", string prefill = null) {
Dialog ed = new Dialog(title, null, DialogFlags.DestroyWithParent | DialogFlags.Modal, "Cancel", ResponseType.Cancel, "OK", ResponseType.Ok);
Dialog ed = new Dialog(
title: title,
parent: this,
flags: DialogFlags.DestroyWithParent | DialogFlags.Modal,
/* button_data: */ "Cancel", ResponseType.Cancel, "OK", ResponseType.Ok
);
ed.DefaultResponse = ResponseType.Ok;
ed.KeepAbove = true;
@ -353,9 +396,11 @@ namespace Buypeeb {
private void RenderList() {
this.items.Clear();
foreach (var item in this.settings.watchlist) {
items.AppendValues(item.Value);
foreach (var item in this.settings.watchlist.Values) {
items.AppendValues(item);
}
var m = (TreeModelFilter)this.itemTreeView.Model;
m.Refilter();
}
// event handlers
@ -387,7 +432,7 @@ namespace Buypeeb {
private void ButtonClearEndedClicked(object sender, EventArgs a) {
Console.WriteLine("ButtonClearEndedClicked");
var md = this.OkCancelDialogue("Are you sure you want to remove all ended auctions from the list?");
var md = this.MsgBox("Are you sure you want to remove all ended auctions from the list?");
var r = (ResponseType)md.Run();
md.Dispose();
if (r != ResponseType.Ok) {
@ -412,12 +457,120 @@ namespace Buypeeb {
Console.WriteLine("ButtonClearAllClicked");
}
private void ButtonOpenClicked(object sender, EventArgs a) {
var od = new FileChooserDialog(
title: "Open userdata.json",
parent: this,
action: FileChooserAction.Open,
"Cancel", ResponseType.Cancel, "Open", ResponseType.Accept
);
var odf = new FileFilter();
odf.Name = "JSON files";
odf.AddMimeType("application/json");
odf.AddPattern("*.json");
od.AddFilter(odf);
if (od.Run() == (int)ResponseType.Accept) {
try {
string j = File.ReadAllText(od.Filename);
this.settings = JsonSerializer.Deserialize<Settings>(j);
this.RenderList();
this.UpdateItems();
}
catch (Exception e) {
Console.WriteLine(e);
var md = MsgBox($"Failed to load {od.Filename}!\n{e.Message}", ButtonsType.Ok);
md.Run();
md.Dispose();
}
}
od.Dispose();
}
private void ButtonSaveClicked(object sender, EventArgs a) {
this.SaveSettings();
}
private void ButtonSaveAsClicked(object sender, EventArgs a) {
var sd = new FileChooserDialog(
title: "Save userdata.json",
parent: this,
action: FileChooserAction.Save,
"Cancel", ResponseType.Cancel, "Save", ResponseType.Accept
);
sd.CurrentName = "userdata.json";
var sdf = new FileFilter();
sdf.Name = "JSON files";
sdf.AddMimeType("application/json");
sdf.AddPattern("*.json");
sd.AddFilter(sdf);
if (sd.Run() == (int)ResponseType.Accept) {
try {
if (!File.Exists(sd.Filename)) {
var fs = File.CreateText(sd.Filename);
fs.Close();
}
File.WriteAllText(sd.Filename, JsonSerializer.Serialize(this.settings, jsonOptions));
}
catch (Exception e) {
Console.WriteLine(e);
var md = MsgBox($"Failed to write {sd.Filename}!\n{e.Message}.", ButtonsType.Ok);
md.Run();
md.Dispose();
}
}
sd.Dispose();
}
private void ButtonExportClicked(object sender, EventArgs a) {
var readyQuery = from item in this.settings.watchlist.Values.ToList() where !item.ready select item;
foreach (var item in readyQuery) {
Console.WriteLine(item.name);
}
if (readyQuery.Count() != 0) {
var md = MsgBox("Please wait for all items to update before exporting a CSV.", ButtonsType.Ok);
md.Run();
md.Dispose();
return;
}
var sd = new FileChooserDialog(
title: "Export watchlist as CSV",
parent: this,
action: FileChooserAction.Save,
"Cancel", ResponseType.Cancel, "Save", ResponseType.Accept
);
sd.CurrentName = "buypeeb.csv";
var sdf = new FileFilter();
sdf.Name = "CSV files";
sdf.AddMimeType("text/csv");
sdf.AddPattern("*.csv");
sd.AddFilter(sdf);
if (sd.Run() == (int)ResponseType.Accept) {
try {
using (var writer = new StreamWriter(sd.Filename))
using (var csv = new CsvWriter(writer, System.Globalization.CultureInfo.InvariantCulture)) {
csv.WriteRecords(this.settings.watchlist);
}
}
catch (Exception e) {
Console.WriteLine(e);
MsgBox($"Failed to write {sd.Filename}!\n{e.Message}.", ButtonsType.Ok);
}
}
sd.Dispose();
}
private void ButtonQuitClicked(object sender, EventArgs a) {
var md = this.OkCancelDialogue("Are you sure you want to quit?");
var md = this.MsgBox("Are you sure you want to quit?");
ResponseType response = (ResponseType)md.Run();
md.Dispose();
@ -442,7 +595,7 @@ namespace Buypeeb {
private void ButtonSelectedRemoveClicked(object sender, EventArgs a) {
var item = this.selectedItem;
var md = this.OkCancelDialogue($"Are you sure you want to remove the item \"{item.name}\"?"); // TODO: this looks bad being all on one line
var md = this.MsgBox($"Are you sure you want to remove the item \"{item.name}\"?"); // TODO: this looks bad being all on one line
ResponseType response = (ResponseType)md.Run();
md.Dispose();
@ -476,7 +629,9 @@ namespace Buypeeb {
// i don't know why this is necessary
var pathAndIter = this.GetRow(this.selectedItem.id);
this.items.EmitRowChanged(pathAndIter.path, pathAndIter.iter);
if (pathAndIter.path != null) {
this.items.EmitRowChanged(pathAndIter.path, pathAndIter.iter);
}
}
// timers
@ -527,12 +682,12 @@ namespace Buypeeb {
private void RenderColumnPriceYen(Gtk.TreeViewColumn column, Gtk.CellRenderer cell, Gtk.ITreeModel model, Gtk.TreeIter iter) {
YahooAuctionsItem item = (YahooAuctionsItem)model.GetValue(iter, 0);
(cell as Gtk.CellRendererText).Text = item.ready ? item.PriceJPY() : "...";
(cell as Gtk.CellRendererText).Text = item.ready ? item.priceJPY : "...";
}
private void RenderColumnPriceAUD(Gtk.TreeViewColumn column, Gtk.CellRenderer cell, Gtk.ITreeModel model, Gtk.TreeIter iter) {
YahooAuctionsItem item = (YahooAuctionsItem)model.GetValue(iter, 0);
(cell as Gtk.CellRendererText).Text = item.ready ? item.PriceAUD() : "...";
(cell as Gtk.CellRendererText).Text = item.ready ? item.priceAUD : "...";
}
private void RenderColumnEnding(Gtk.TreeViewColumn column, Gtk.CellRenderer cell, Gtk.ITreeModel model, Gtk.TreeIter iter) {
@ -559,5 +714,34 @@ namespace Buypeeb {
}
(cell as Gtk.CellRendererText).Text = item.ready ? ending : "...";
}
// tree filter
private bool ItemFilter(ITreeModel model, TreeIter iter) {
var item = (YahooAuctionsItem)model.GetValue(iter, 0);
bool Filtered(string name) {
return this.filterChecks[name].Active;
}
// first, check to see if any filters are set that would exclude everything, such as hiding both active and ended auctions
// if so, there's no need to run the more expensive linq query
if (
(Filtered("Favourites") && Filtered("NonFavourites")) ||
(Filtered("Active") && Filtered("Ended")) ||
(Filtered("EndingToday") && Filtered("EndingAfterToday")) ||
(Filtered("WithWinPrice") && Filtered("WithNoWinPrice"))
) {
return false;
}
// this.filterQuery.ToList().ForEach(Console.WriteLine);
return this.filterQuery.Contains(item);
}
private void RunFilter(object sender, EventArgs a) {
var m = (TreeModelFilter)this.itemTreeView.Model;
m.Refilter();
}
}
}

View file

@ -18,15 +18,7 @@ namespace Buypeeb {
if (this.watchlist == null) {
// either this is the first time the program has been run, or there's something wrong with userdata.json
this.watchlist = new Dictionary<string, YahooAuctionsItem>();
// this.Watch("https://buypeeb.biz/whatever/k12345", "my thingy");
// this.Watch("https://buypeeb.biz/whatever/z09876", "your thingy");
// this.Watch("https://buypeeb.biz/whatever/h55555", "our thingy");
// for (int i = 0; i < 10; i++) {
// this.Watch($"https://buypeeb.biz/whatever/x{i * 123}", $"filler {i}");
// }
// this.watchlist["k12345"].Update();
}
}
public YahooAuctionsItem Watch(string url, string name) {

View file

@ -25,7 +25,7 @@ namespace Buypeeb {
// there's not really a need for it i guess but it's my program and i can do what i want
public string id { get; set; }
public string name { get; set; }
public int price = 0;
private int price = 0;
public int winPrice;
public string originalName { get; set; }
public string notes { get; set; }
@ -45,6 +45,13 @@ namespace Buypeeb {
}
}
// TODO: don't serialise this stuff
public string priceJPY { get { return $"¥{this.price}"; } }
public string winPriceJPY { get { return $"¥{this.winPrice}"; } }
public string priceAUD { get { return $"${(this.price / 75.0):f2}"; } }
public string winPriceAUD { get { return $"${(this.winPrice / 75.0):f2}"; } }
public bool endingToday { get { return this.endDate.DayOfYear == DateTime.UtcNow.DayOfYear; } }
public bool hasWinPrice { get { return this.winPrice != 0; } }
private bool success { get; set; } // TODO: custom setter that throws an exception if set to false or something idk
public YahooAuctionsItem(string id, string name) {
@ -104,13 +111,8 @@ namespace Buypeeb {
}
}
public string PriceAUD(bool win = false) {
double aud = win ? this.winPrice / 75.0 : this.price / 75.0;
return $"${aud:f2}";
}
public string PriceJPY(bool win = false) {
return win ? $"¥{this.winPrice}" : $"¥{this.price}";
public override string ToString() {
return $"{this.id}: {this.name}";
}
}
}

View file

@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="15.0.5" />
<PackageReference Include="GtkSharp" Version="3.22.25.128" />
</ItemGroup>

View file

@ -3,6 +3,171 @@
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkTextBuffer" id="TextBufferSelectedNotes"/>
<object class="GtkPopover" id="popover1">
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">5</property>
<property name="margin_end">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">3</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Items to hide:</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterFavourites">
<property name="label" translatable="yes">Favourites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterNonFavourites">
<property name="label" translatable="yes">Non-favourites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterActive">
<property name="label" translatable="yes">Active</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterEnded">
<property name="label" translatable="yes">Ended</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterEndingToday">
<property name="label" translatable="yes">Ending today</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterEndingAfterToday">
<property name="label" translatable="yes">Ending after today</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterWithWinPrice">
<property name="label" translatable="yes">With win price</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="CheckButtonFilterWithNoWinPrice">
<property name="label" translatable="yes">With no win price</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="RunFilter" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Show all</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">9</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkWindow" id="wndMain">
<property name="can_focus">False</property>
<property name="default_width">810</property>
@ -152,12 +317,12 @@
<child>
<object class="GtkToolButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Open</property>
<property name="label" translatable="yes">Open</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-open</property>
<signal name="clicked" handler="ButtonOpenClicked" swapped="no"/>
<accelerator key="o" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
@ -184,12 +349,28 @@
<child>
<object class="GtkToolButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Export as...</property>
<property name="label" translatable="yes">Export as...</property>
<property name="tooltip_text" translatable="yes">Save as...</property>
<property name="label" translatable="yes">Save as...</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-save-as</property>
<signal name="clicked" handler="ButtonSaveAsClicked" swapped="no"/>
<accelerator key="s" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Export</property>
<property name="label" translatable="yes">Export</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-convert</property>
<signal name="clicked" handler="ButtonExportClicked" swapped="no"/>
<accelerator key="e" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
</object>
<packing>
@ -263,75 +444,135 @@
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">200</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkTreeView" id="TreeViewItems">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">3</property>
<property name="margin_end">3</property>
<property name="spacing">3</property>
<child>
<object class="GtkSearchEntry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="popover">popover1</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="rules_hint">True</property>
<property name="search_column">1</property>
<property name="activate_on_single_click">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection">
<signal name="changed" handler="TreeViewItemsSelectionChanged" swapped="no"/>
</object>
</child>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">200</property>
<child>
<object class="GtkTreeViewColumn">
<property name="title" translatable="yes">♥</property>
<child>
<object class="GtkCellRendererText"/>
<object class="GtkTreeView" id="TreeViewItems">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="rules_hint">True</property>
<property name="enable_search">False</property>
<property name="search_column">1</property>
<property name="activate_on_single_click">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection">
<signal name="changed" handler="TreeViewItemsSelectionChanged" swapped="no"/>
</object>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed_width">100</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<child>
<object class="GtkCellRendererText"/>
<object class="GtkTreeViewColumn">
<property name="title" translatable="yes">♥</property>
<child>
<object class="GtkCellRendererText"/>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="title" translatable="yes">Price (¥)</property>
<child>
<object class="GtkCellRendererText"/>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed_width">100</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<child>
<object class="GtkCellRendererText"/>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="title" translatable="yes">Price (AUD)</property>
<child>
<object class="GtkCellRendererText"/>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="title" translatable="yes">Price (¥)</property>
<child>
<object class="GtkCellRendererText"/>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="title" translatable="yes">Ending at</property>
<property name="clickable">True</property>
<child>
<object class="GtkCellRendererText"/>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="title" translatable="yes">Price (AUD)</property>
<child>
<object class="GtkCellRendererText"/>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="title" translatable="yes">Ending at</property>
<property name="clickable">True</property>
<child>
<object class="GtkCellRendererText"/>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>