Adds ui support for attachments, results not yet connected to daemon
This commit is contained in:
@@ -53,7 +53,10 @@ public class MainWindow : Adw.ApplicationWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_transcript_send(string message) {
|
private void on_transcript_send(TranscriptContainerView view) {
|
||||||
|
var body = view.message_body;
|
||||||
|
var attachment_guids = view.attachment_guids;
|
||||||
|
|
||||||
if (transcript_container_view.transcript_view.model == null) {
|
if (transcript_container_view.transcript_view.model == null) {
|
||||||
GLib.warning("No conversation selected");
|
GLib.warning("No conversation selected");
|
||||||
return;
|
return;
|
||||||
@@ -66,7 +69,7 @@ public class MainWindow : Adw.ApplicationWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Repository.get_instance().send_message(selected_conversation, message);
|
Repository.get_instance().send_message(selected_conversation, body);
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
GLib.warning("Failed to send message: %s", e.message);
|
GLib.warning("Failed to send message: %s", e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ sources = [
|
|||||||
'conversation-list/conversation-list-model.vala',
|
'conversation-list/conversation-list-model.vala',
|
||||||
'conversation-list/conversation-row.vala',
|
'conversation-list/conversation-row.vala',
|
||||||
|
|
||||||
|
'transcript/attachment-preview.vala',
|
||||||
'transcript/message-list-model.vala',
|
'transcript/message-list-model.vala',
|
||||||
'transcript/transcript-container-view.vala',
|
'transcript/transcript-container-view.vala',
|
||||||
'transcript/transcript-drawing-area.vala',
|
'transcript/transcript-drawing-area.vala',
|
||||||
|
|||||||
@@ -30,5 +30,22 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attachment-preview-row {
|
||||||
|
background-color: alpha(@window_bg_color, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-preview {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid alpha(@borders, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-preview.completed {
|
||||||
|
border-color: @success_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-image {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|||||||
93
src/transcript/attachment-preview.vala
Normal file
93
src/transcript/attachment-preview.vala
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using Gtk;
|
||||||
|
using Gdk;
|
||||||
|
|
||||||
|
class AttachmentPreview : Gtk.Box {
|
||||||
|
public signal void remove_requested();
|
||||||
|
|
||||||
|
private File file;
|
||||||
|
private string upload_guid;
|
||||||
|
private string? attachment_guid = null;
|
||||||
|
private bool is_completed = false;
|
||||||
|
|
||||||
|
private Overlay overlay;
|
||||||
|
private Image picture;
|
||||||
|
private Spinner spinner;
|
||||||
|
private Button remove_button;
|
||||||
|
|
||||||
|
public AttachmentPreview(File file, string upload_guid) {
|
||||||
|
Object(orientation: Orientation.VERTICAL, spacing: 0);
|
||||||
|
this.file = file;
|
||||||
|
this.upload_guid = upload_guid;
|
||||||
|
|
||||||
|
setup_ui();
|
||||||
|
load_image();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setup_ui() {
|
||||||
|
set_size_request(100, 100);
|
||||||
|
add_css_class("attachment-preview");
|
||||||
|
|
||||||
|
overlay = new Overlay();
|
||||||
|
overlay.set_size_request(100, 100);
|
||||||
|
append(overlay);
|
||||||
|
|
||||||
|
// Image preview
|
||||||
|
picture = new Image();
|
||||||
|
overlay.set_child(picture);
|
||||||
|
|
||||||
|
// Loading spinner
|
||||||
|
spinner = new Spinner();
|
||||||
|
spinner.set_halign(Align.CENTER);
|
||||||
|
spinner.set_valign(Align.CENTER);
|
||||||
|
spinner.set_size_request(24, 24);
|
||||||
|
spinner.start();
|
||||||
|
overlay.add_overlay(spinner);
|
||||||
|
|
||||||
|
// Remove button
|
||||||
|
remove_button = new Button();
|
||||||
|
remove_button.set_icon_name("window-close-symbolic");
|
||||||
|
remove_button.add_css_class("circular");
|
||||||
|
remove_button.add_css_class("destructive-action");
|
||||||
|
remove_button.set_halign(Align.END);
|
||||||
|
remove_button.set_valign(Align.START);
|
||||||
|
remove_button.set_margin_top(4);
|
||||||
|
remove_button.set_margin_end(4);
|
||||||
|
remove_button.set_size_request(20, 20);
|
||||||
|
remove_button.clicked.connect(() => {
|
||||||
|
remove_requested();
|
||||||
|
});
|
||||||
|
overlay.add_overlay(remove_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load_image() {
|
||||||
|
try {
|
||||||
|
picture.set_from_file(file.get_path());
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Failed to load image preview: %s", e.message);
|
||||||
|
|
||||||
|
// Show a placeholder icon if image loading fails
|
||||||
|
var icon = new Image.from_icon_name("image-x-generic");
|
||||||
|
icon.set_pixel_size(48);
|
||||||
|
overlay.set_child(icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_completed(string attachment_guid) {
|
||||||
|
this.attachment_guid = attachment_guid;
|
||||||
|
this.is_completed = true;
|
||||||
|
|
||||||
|
spinner.stop();
|
||||||
|
spinner.set_visible(false);
|
||||||
|
|
||||||
|
// Optionally change visual state to indicate completion
|
||||||
|
add_css_class("completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_attachment_guid() {
|
||||||
|
return attachment_guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool get_is_completed() {
|
||||||
|
return is_completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,67 @@
|
|||||||
using Gtk;
|
using Gtk;
|
||||||
using Adw;
|
using Adw;
|
||||||
|
using Gee;
|
||||||
|
|
||||||
class TranscriptContainerView : Adw.Bin {
|
class TranscriptContainerView : Adw.Bin
|
||||||
|
{
|
||||||
public TranscriptView transcript_view;
|
public TranscriptView transcript_view;
|
||||||
public Entry message_entry;
|
public signal void on_send(TranscriptContainerView view);
|
||||||
public signal void on_send(string message);
|
|
||||||
|
|
||||||
private Box container;
|
private Box container;
|
||||||
private Button send_button;
|
private Button send_button;
|
||||||
|
private FlowBox attachment_flow_box;
|
||||||
|
|
||||||
|
private Entry message_entry;
|
||||||
|
private HashSet<string> pending_uploads;
|
||||||
|
private HashMap<string, AttachmentPreview> attachment_previews;
|
||||||
|
private ArrayList<UploadedAttachment> completed_attachments;
|
||||||
|
|
||||||
|
public string message_body {
|
||||||
|
get {
|
||||||
|
return message_entry.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<string> attachment_guids {
|
||||||
|
owned get {
|
||||||
|
var attachment_guids = new ArrayList<string>();
|
||||||
|
completed_attachments.foreach((attachment) => {
|
||||||
|
attachment_guids.add(attachment.attachment_guid);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return attachment_guids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public TranscriptContainerView () {
|
public TranscriptContainerView () {
|
||||||
container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
|
container = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
|
||||||
set_child (container);
|
set_child (container);
|
||||||
|
|
||||||
|
pending_uploads = new HashSet<string>();
|
||||||
|
attachment_previews = new HashMap<string, AttachmentPreview>();
|
||||||
|
completed_attachments = new ArrayList<UploadedAttachment>();
|
||||||
|
|
||||||
// Create message list view
|
// Create message list view
|
||||||
transcript_view = new TranscriptView();
|
transcript_view = new TranscriptView();
|
||||||
transcript_view.set_vexpand(true);
|
transcript_view.set_vexpand(true);
|
||||||
container.append(transcript_view);
|
container.append(transcript_view);
|
||||||
|
|
||||||
|
// Create attachment preview row (initially hidden)
|
||||||
|
setup_attachment_row();
|
||||||
|
|
||||||
// Create bottom box for input
|
// Create bottom box for input
|
||||||
var input_box = new Box(Orientation.HORIZONTAL, 6);
|
var input_box = new Box(Orientation.HORIZONTAL, 6);
|
||||||
input_box.add_css_class("message-input-box");
|
input_box.add_css_class("message-input-box");
|
||||||
input_box.set_valign(Align.END);
|
input_box.set_valign(Align.END);
|
||||||
input_box.set_spacing(6);
|
input_box.set_spacing(6);
|
||||||
container.append(input_box);
|
container.append(input_box);
|
||||||
|
|
||||||
|
// Setup drag and drop
|
||||||
|
setup_drag_and_drop();
|
||||||
|
|
||||||
|
// Connect to repository signals
|
||||||
|
Repository.get_instance().attachment_uploaded.connect(on_attachment_uploaded);
|
||||||
|
|
||||||
// Create message entry
|
// Create message entry
|
||||||
message_entry = new Entry();
|
message_entry = new Entry();
|
||||||
@@ -42,16 +80,138 @@ class TranscriptContainerView : Adw.Bin {
|
|||||||
send_button.clicked.connect(on_request_send);
|
send_button.clicked.connect(on_request_send);
|
||||||
input_box.append(send_button);
|
input_box.append(send_button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setup_attachment_row() {
|
||||||
|
attachment_flow_box = new FlowBox();
|
||||||
|
attachment_flow_box.set_max_children_per_line(6);
|
||||||
|
attachment_flow_box.set_row_spacing(6);
|
||||||
|
attachment_flow_box.set_column_spacing(6);
|
||||||
|
attachment_flow_box.halign = Align.START;
|
||||||
|
attachment_flow_box.add_css_class("attachment-preview-row");
|
||||||
|
container.append(attachment_flow_box);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setup_drag_and_drop() {
|
||||||
|
var drop_target = new DropTarget(typeof(File), Gdk.DragAction.COPY);
|
||||||
|
drop_target.drop.connect(on_file_dropped);
|
||||||
|
this.add_controller(drop_target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool on_file_dropped(Value val, double x, double y) {
|
||||||
|
if (!val.holds(typeof(File))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = (File)val.get_object();
|
||||||
|
if (file == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an image file
|
||||||
|
try {
|
||||||
|
var file_info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE, FileQueryInfoFlags.NONE);
|
||||||
|
string content_type = file_info.get_content_type();
|
||||||
|
|
||||||
|
if (!content_type.has_prefix("image/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_file(file);
|
||||||
|
return true;
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Failed to get file info: %s", e.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void upload_file(File file) {
|
||||||
|
try {
|
||||||
|
string upload_guid = Repository.get_instance().upload_attachment(file.get_path());
|
||||||
|
pending_uploads.add(upload_guid);
|
||||||
|
|
||||||
|
var preview = new AttachmentPreview(file, upload_guid);
|
||||||
|
preview.remove_requested.connect(() => {
|
||||||
|
remove_attachment(upload_guid);
|
||||||
|
});
|
||||||
|
|
||||||
|
attachment_previews[upload_guid] = preview;
|
||||||
|
attachment_flow_box.append(preview);
|
||||||
|
|
||||||
|
update_attachment_row_visibility();
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Failed to upload attachment: %s", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_attachment_uploaded(string upload_guid, string attachment_guid) {
|
||||||
|
if (attachment_previews.has_key(upload_guid)) {
|
||||||
|
var preview = attachment_previews[upload_guid];
|
||||||
|
preview.set_completed(attachment_guid);
|
||||||
|
completed_attachments.add(new UploadedAttachment(upload_guid, attachment_guid));
|
||||||
|
pending_uploads.remove(upload_guid);
|
||||||
|
update_send_button_sensitivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remove_attachment(string upload_guid) {
|
||||||
|
if (attachment_previews.has_key(upload_guid)) {
|
||||||
|
var preview = attachment_previews[upload_guid];
|
||||||
|
attachment_flow_box.remove(preview);
|
||||||
|
attachment_previews.unset(upload_guid);
|
||||||
|
|
||||||
|
completed_attachments.foreach((attachment) => {
|
||||||
|
if (attachment.upload_guid == upload_guid) {
|
||||||
|
completed_attachments.remove(attachment);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
update_attachment_row_visibility();
|
||||||
|
update_send_button_sensitivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_attachment_row_visibility() {
|
||||||
|
bool has_attachments = attachment_previews.size > 0;
|
||||||
|
attachment_flow_box.set_visible(has_attachments);
|
||||||
|
}
|
||||||
|
|
||||||
private void on_text_changed() {
|
private void on_text_changed() {
|
||||||
send_button.set_sensitive(message_entry.text.length > 0);
|
update_send_button_sensitivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_send_button_sensitivity() {
|
||||||
|
send_button.set_sensitive(message_entry.text.length > 0 && pending_uploads.size == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_request_send() {
|
private void on_request_send() {
|
||||||
if (message_entry.text.length > 0) {
|
if (message_entry.text.length > 0 && pending_uploads.size == 0) {
|
||||||
on_send(message_entry.text);
|
on_send(this);
|
||||||
|
|
||||||
|
// Clear the message entry
|
||||||
message_entry.text = "";
|
message_entry.text = "";
|
||||||
|
|
||||||
|
// Clear the attachment previews
|
||||||
|
attachment_flow_box.remove_all();
|
||||||
|
attachment_previews.clear();
|
||||||
|
completed_attachments.clear();
|
||||||
|
pending_uploads.clear();
|
||||||
|
|
||||||
|
update_send_button_sensitivity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UploadedAttachment
|
||||||
|
{
|
||||||
|
public string upload_guid;
|
||||||
|
public string attachment_guid;
|
||||||
|
|
||||||
|
public UploadedAttachment(string upload_guid, string attachment_guid)
|
||||||
|
{
|
||||||
|
this.upload_guid = upload_guid;
|
||||||
|
this.attachment_guid = attachment_guid;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user