diff --git a/src/bavarder.gresource.xml b/src/bavarder.gresource.xml index 720db5a..1d1ab1f 100644 --- a/src/bavarder.gresource.xml +++ b/src/bavarder.gresource.xml @@ -2,7 +2,9 @@ views/window.ui + views/export_dialog.ui views/preferences_window.ui + views/save_dialog.ui widgets/thread_item.ui widgets/item.ui providers/provider_item.ui diff --git a/src/meson.build b/src/meson.build index b77f402..9affbd5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,7 +5,9 @@ python = import('python') blueprints = custom_target('blueprints', input: files( 'gtk/help-overlay.blp', + 'views/export_dialog.blp', 'views/preferences_window.blp', + 'views/save_dialog.blp', 'views/window.blp', 'widgets/thread_item.blp', 'widgets/item.blp', diff --git a/src/views/export_dialog.blp b/src/views/export_dialog.blp new file mode 100644 index 0000000..1195f82 --- /dev/null +++ b/src/views/export_dialog.blp @@ -0,0 +1,54 @@ +using Gtk 4.0; +using Adw 1; +using GtkSource 5; + +template $ExportDialog : Adw.MessageDialog { + heading: _("Export Thread ?"); + response => $handle_response(); + + extra-child: Overlay { + [overlay] + Button { + styles [ + "circular", + ] + icon-name: "edit-copy-symbolic"; + halign: end; + valign: end; + margin-bottom: 7; + margin-end: 7; + clicked => $copy(); + } + + Box box { + orientation: vertical; + vexpand: true; + hexpand: true; + + Gtk.ScrolledWindow view { + vexpand: true; + hexpand: true; + min-content-height: 200; + + GtkSource.View source_view { + vexpand: true; + hexpand: true; + buffer: GtkSource.Buffer buffer {}; + editable: false; + monospace: true; + show-line-marks: false; + show-line-numbers: false; + smart-backspace: false; + margin-top: 5; + margin-bottom: 5; + styles [ "codeview", "card" ] + } + } + } + }; + + responses [ + close: _("Close"), + export: _("Export") suggested, + ] +} diff --git a/src/views/export_dialog.py b/src/views/export_dialog.py new file mode 100644 index 0000000..69cf060 --- /dev/null +++ b/src/views/export_dialog.py @@ -0,0 +1,43 @@ +from gi.repository import Gtk, Adw, Gio, GtkSource, Gdk + +from bavarder.constants import app_id, rootdir +from bavarder.views.save_dialog import SaveDialog + +GtkSource.init() + +@Gtk.Template(resource_path=f"{rootdir}/ui/export_dialog.ui") +class ExportDialog(Adw.MessageDialog): + __gtype_name__ = "ExportDialog" + + buffer = Gtk.Template.Child() + source_view = Gtk.Template.Child() + + def __init__(self, parent, chat, **kwargs): + super().__init__(**kwargs) + + self.text: str = "" + self.parent = parent + for i, x in zip(chat, range(len(chat))): + self.text += f"{i['role']}: {i['content']}\n" + + if (x % 2) != 0: + self.text += "\n" + + self.text = self.text[:-2] + self.buffer.set_text(self.text) + + if Adw.StyleManager().get_dark(): + self.buffer.set_style_scheme(GtkSource.StyleSchemeManager().get_scheme("Adwaita-dark")) + else: + self.buffer.set_style_scheme(GtkSource.StyleSchemeManager().get_scheme("Adwaita")) + + + @Gtk.Template.Callback() + def copy(self, *args, **kwargs): + Gdk.Display.get_default().get_clipboard().set(self.text) + + @Gtk.Template.Callback() + def handle_response(self, dialog, response, *args, **kwargs): + if response == "export": + dialog = SaveDialog(self.parent, self.text) + dialog.present() diff --git a/src/views/meson.build b/src/views/meson.build index bac55cf..cddd714 100644 --- a/src/views/meson.build +++ b/src/views/meson.build @@ -3,7 +3,9 @@ views_dir = join_paths(MODULE_DIR, 'views') views_sources = [ '__init__.py', 'about_window.py', + 'export_dialog.py', 'preferences_window.py', + 'save_dialog.py', 'window.py', ] diff --git a/src/views/preferences_window.py b/src/views/preferences_window.py index cd3e74a..9954759 100644 --- a/src/views/preferences_window.py +++ b/src/views/preferences_window.py @@ -95,13 +95,13 @@ class PreferencesWindow(Adw.PreferencesWindow): @Gtk.Template.Callback() def on_bot_entry_apply(self, user_data, *args): - self.app.bot_name = user_data.get_text().capitalize() + self.app.bot_name = user_data.get_text() self.app.load_bot_and_user_name() @Gtk.Template.Callback() def on_user_entry_apply(self, user_data, *args): - self.app.user_name = user_data.get_text().capitalize() + self.app.user_name = user_data.get_text() self.app.load_bot_and_user_name() \ No newline at end of file diff --git a/src/views/save_dialog.blp b/src/views/save_dialog.blp new file mode 100644 index 0000000..da4b487 --- /dev/null +++ b/src/views/save_dialog.blp @@ -0,0 +1,69 @@ +using Gtk 4.0; +using Adw 1; + +template $SaveDialog : Adw.MessageDialog { + response => $handle_response(); + responses [ + cancel: _("Cancel"), + disacard: _("Discard") destructive, + save: _("Save") suggested disabled, + ] + close-response: "cancel"; + modal: true; + heading: _("Export Thread?"); + body: _(""); + + extra-child: Box { + margin-top: 12; + orientation: vertical; + spacing: 24; + + ListBox { + selection-mode: none; + styles ["boxed-list"] + + Adw.EntryRow filename { + title: _("File Name"); + entry-activated => $on_entry_activated(); + } + } + + Box { + orientation: vertical; + + ListBox { + selection-mode: none; + styles ["boxed-list"] + + Adw.ActionRow location { + title: _("Location"); + subtitle: "Select Location"; + activatable-widget: button_location; + + Button button_location { + icon-name: "folder-symbolic"; + valign: center; + styles ["flat"] + clicked => $on_location_button_clicked(); + } + } + } + + Label { + margin-start: 12; + margin-top: 12; + halign: start; + label: _("The export of the Thread will be saved in this directory."); + styles ["dim-label", "caption"] + justify: left; + } + } + }; +} + +Gtk.FileDialog file_chooser { + title: _("Choose a directory"); + modal: true; + //action: open; + //response => $on_filechooser_response(); +} diff --git a/src/views/save_dialog.py b/src/views/save_dialog.py new file mode 100644 index 0000000..a8eefca --- /dev/null +++ b/src/views/save_dialog.py @@ -0,0 +1,57 @@ +from gi.repository import Gtk, Adw, Gio, Gdk + +from bavarder.constants import app_id, rootdir + +@Gtk.Template(resource_path=f"{rootdir}/ui/save_dialog.ui") +class SaveDialog(Adw.MessageDialog): + __gtype_name__ = "SaveDialog" + + filename = Gtk.Template.Child() + file_chooser = Gtk.Template.Child() + location = Gtk.Template.Child() + + def __init__(self, parent, text, **kwargs): + super().__init__(**kwargs) + + self.text: str = text + self.parent = parent + + @Gtk.Template.Callback() + def handle_response(self, dialog, response, *args, **kwargs): + if response == "save": + filename = self.filename.get_text() + path = f"{self.directory}/{filename}.md" + + toast = Adw.Toast() + try: + with open(path, "w") as f: + f.write(self.text) + except FileNotFoundError: + toast.set_title(_("Unable to save the Thread")) + else: + toast.set_title(_("Thread successfully saved!")) + self.parent.toast_overlay.add_toast(toast) + + @Gtk.Template.Callback() + def on_location_button_clicked(self, widget, *args): + self.file_chooser.select_folder(self, None, self.on_filechooser_response) + + def on_filechooser_response(self, widget, response): + self.directory = self.file_chooser.select_folder_finish(response).get_path() + self.location.set_subtitle(self.directory) + self.update_save_status() + + @Gtk.Template.Callback() + def on_entry_activated(self, widget, *args): + self.update_save_status() + + def update_save_status(self): + try: + self.directory + except Exception: + self.set_response_enabled("save", False) + else: + if self.filename.get_text().strip(): + self.set_response_enabled("save", True) + else: + self.set_response_enabled("save", False) diff --git a/src/views/window.py b/src/views/window.py index a71d005..97a137b 100644 --- a/src/views/window.py +++ b/src/views/window.py @@ -28,6 +28,7 @@ from bavarder.constants import app_id, build_type, rootdir from bavarder.widgets.thread_item import ThreadItem from bavarder.widgets.item import Item from bavarder.threading import KillableThread +from bavarder.views.export_dialog import ExportDialog class CustomEntry(Gtk.TextView): def __init__(self, **kwargs): @@ -85,6 +86,7 @@ class BavarderWindow(Adw.ApplicationWindow): self.create_action("cancel", self.cancel, ["Escape"]) self.create_action("clear_all", self.on_clear_all) + self.create_action("export", self.on_export, ["e"]) self.settings.bind( "width", self, "default-width", Gio.SettingsBindFlags.DEFAULT @@ -202,6 +204,12 @@ class BavarderWindow(Adw.ApplicationWindow): del self.chat["content"] self.stack.set_visible_child(self.status_no_chat) + def on_export(self, *args): + if self.content: + dialog = ExportDialog(self, self.chat["content"]) + dialog.set_transient_for(self) + dialog.present() + # PROVIDER - ONLINE def load_provider_selector(self): provider_menu = Gio.Menu() @@ -229,6 +237,11 @@ class BavarderWindow(Adw.ApplicationWindow): item_provider.set_action_and_target_value("win.clear_all", None) section.append_item(item_provider) + item_provider = Gio.MenuItem() + item_provider.set_label(_("Export")) + item_provider.set_action_and_target_value("win.export", None) + section.append_item(item_provider) + provider_menu.append_section(None, section) self.provider_selector_button.set_menu_model(provider_menu) @@ -262,6 +275,11 @@ class BavarderWindow(Adw.ApplicationWindow): item_provider.set_action_and_target_value("win.clear_all", None) section.append_item(item_provider) + item_provider = Gio.MenuItem() + item_provider.set_label(_("Export")) + item_provider.set_action_and_target_value("win.export", None) + section.append_item(item_provider) + provider_menu.append_section(None, section) self.model_selector_button.set_menu_model(provider_menu) diff --git a/src/widgets/item.py b/src/widgets/item.py index 3f05da2..6239df7 100644 --- a/src/widgets/item.py +++ b/src/widgets/item.py @@ -109,7 +109,7 @@ class Item(Gtk.Box): label.set_halign(Gtk.Align.START) self.content.append(label) - t = self.item["role"].capitalize() + t = self.item["role"].lower() self.parent = parent self.settings = parent.settings @@ -117,11 +117,11 @@ class Item(Gtk.Box): self.app = self.parent.get_application() self.win = self.app.get_active_window() - if t == self.app.user_name: # User + if t == self.app.user_name.lower(): # User self.message_bubble.add_css_class("message-bubble-user") self.avatar.add_css_class("avatar-user") role = self.app.user_name - elif t == self.app.bot_name: # Assistant + elif t == self.app.bot_name.lower(): # Assistant self.avatar.set_icon_name("bot-symbolic") self.user.add_css_class("warning") role = self.app.bot_name