aboutsummaryrefslogtreecommitdiff
path: root/main/src
diff options
context:
space:
mode:
authorfrosty <gabriel@bwaaa.monster>2026-08-27 20:49:23 -0400
committerfrosty <gabriel@bwaaa.monster>2026-08-27 20:49:23 -0400
commit3d7e90d401da02d430a87db0ec7e1db3a6f1a68f (patch)
tree959156c97bb0be4068b2825b83fdfb378c48975e /main/src
parentec49665bd9f06a83787fd3f7072ca0373487fd5e (diff)
feat: video player widget
Diffstat (limited to 'main/src')
-rw-r--r--main/src/ui/conversation_content_view/file_video_widget.vala174
-rw-r--r--main/src/ui/conversation_content_view/file_widget.vala16
2 files changed, 189 insertions, 1 deletions
diff --git a/main/src/ui/conversation_content_view/file_video_widget.vala b/main/src/ui/conversation_content_view/file_video_widget.vala
new file mode 100644
index 00000000..703428d9
--- /dev/null
+++ b/main/src/ui/conversation_content_view/file_video_widget.vala
@@ -0,0 +1,174 @@
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui {
+
+public class FileVideoWidget : Widget {
+ private const int MIN_WIDTH = 100;
+ private const int MIN_HEIGHT = 100;
+ private const int MAX_WIDTH = 600;
+ private const int MAX_HEIGHT = 300;
+
+ private Overlay overlay = new Overlay();
+ private FixedRatioVideo video = new FixedRatioVideo() {
+ min_width = MIN_WIDTH,
+ min_height = MIN_HEIGHT,
+ max_width = MAX_WIDTH,
+ max_height = MAX_HEIGHT
+ };
+ private Gtk.Box video_overlay_toolbar = new Gtk.Box(Orientation.VERTICAL, 0) {
+ halign = Align.END,
+ valign = Align.START,
+ margin_top = 10,
+ margin_start = 10,
+ margin_end = 10,
+ margin_bottom = 10,
+ vexpand = false,
+ visible = false
+ };
+
+ construct {
+ layout_manager = new BinLayout();
+ check_widget_leak(this);
+ }
+
+ public FileVideoWidget(FileTransfer file_transfer) {
+ this.halign = Align.START;
+ this.add_css_class("file-video-widget");
+
+ MenuButton button = new MenuButton();
+ button.icon_name = "dino-view-more";
+ Menu menu_model = new Menu();
+ menu_model.append(_("Open"), "file.open");
+ menu_model.append(_("Save as…"), "file.save_as");
+ Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu.from_model(menu_model);
+ button.popover = popover_menu;
+ video_overlay_toolbar.append(button);
+ video_overlay_toolbar.add_css_class("card");
+ video_overlay_toolbar.add_css_class("toolbar");
+ video_overlay_toolbar.add_css_class("compact-card-toolbar");
+ video_overlay_toolbar.set_cursor_from_name("default");
+
+ video.file = file_transfer.get_file();
+
+ overlay.set_child(video);
+ overlay.add_overlay(video_overlay_toolbar);
+ overlay.set_clip_overlay(video_overlay_toolbar, true);
+ overlay.insert_after(this, null);
+
+ EventControllerMotion motion_events = new EventControllerMotion();
+ this.add_controller(motion_events);
+ motion_events.enter.connect(() => {
+ video_overlay_toolbar.visible = true;
+ });
+ motion_events.leave.connect(() => {
+ if (button.popover != null && button.popover.visible) return;
+ video_overlay_toolbar.visible = false;
+ });
+ }
+
+ public static bool can_display(FileTransfer file_transfer) {
+ if (file_transfer.state != FileTransfer.State.COMPLETE || file_transfer.path == null || file_transfer.content_type == null) {
+ return false;
+ }
+
+ string? mime_type = file_transfer.content_type.get_mime_type();
+ return mime_type != null && mime_type.has_prefix("video/");
+ }
+
+ public override void dispose() {
+ if (video.media_stream != null) video.media_stream.pause();
+ if (overlay != null && overlay.parent != null) overlay.unparent();
+ base.dispose();
+ }
+}
+
+private class FixedRatioVideo : Widget {
+ public int min_width { get; set; default = -1; }
+ public int max_width { get; set; default = int.MAX; }
+ public int min_height { get; set; default = -1; }
+ public int max_height { get; set; default = int.MAX; }
+ public File? file { get { return inner.file; } set { inner.file = value; } }
+ public MediaStream? media_stream { get { return inner.media_stream; } }
+
+ private Gtk.Video inner = new Gtk.Video() {
+ autoplay = false,
+ loop = false
+ };
+
+ construct {
+ set_css_name("video");
+ inner.insert_after(this, null);
+ this.notify.connect(queue_resize);
+ check_widget_leak(this);
+ }
+
+ private void measure_target_size(out int width, out int height) {
+ if (width_request != -1 && height_request != -1) {
+ width = width_request;
+ height = height_request;
+ return;
+ }
+
+ width = min_width;
+ height = min_height;
+
+ if (inner.should_layout()) {
+ int child_min, child_nat, child_min_baseline, child_nat_baseline;
+ inner.measure(Orientation.HORIZONTAL, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
+ width = int.max(child_nat, width);
+ }
+ width = int.min(width, max_width);
+
+ if (inner.should_layout()) {
+ int child_min, child_nat, child_min_baseline, child_nat_baseline;
+ inner.measure(Orientation.VERTICAL, width, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
+ height = int.max(child_nat, height);
+ }
+
+ if (height > max_height) {
+ height = max_height;
+ width = min_width;
+
+ if (inner.should_layout()) {
+ int child_min, child_nat, child_min_baseline, child_nat_baseline;
+ inner.measure(Orientation.HORIZONTAL, max_height, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
+ width = int.max(child_nat, width);
+ }
+ width = int.min(width, max_width);
+ }
+ }
+
+ public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
+ minimum_baseline = -1;
+ natural_baseline = -1;
+
+ int width, height;
+ measure_target_size(out width, out height);
+ if (orientation == Orientation.HORIZONTAL) {
+ minimum = min_width;
+ natural = int.max(min_width, int.min(width, max_width));
+ } else if (for_size == -1) {
+ minimum = min_height;
+ natural = int.max(min_height, int.min(height, max_height));
+ } else {
+ minimum = natural = int.max(min_height, int.min(height * for_size / width, height));
+ }
+ }
+
+ public override void size_allocate(int width, int height, int baseline) {
+ if (inner.should_layout()) inner.allocate(width, height, baseline, null);
+ }
+
+ public override SizeRequestMode get_request_mode() {
+ return SizeRequestMode.HEIGHT_FOR_WIDTH;
+ }
+
+ public override void dispose() {
+ inner.unparent();
+ base.dispose();
+ }
+}
+
+}
diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala
index ecc2d3e9..3c21412a 100644
--- a/main/src/ui/conversation_content_view/file_widget.vala
+++ b/main/src/ui/conversation_content_view/file_widget.vala
@@ -46,6 +46,7 @@ public class FileWidget : SizeRequestBin {
enum State {
IMAGE,
+ VIDEO,
DEFAULT
}
@@ -94,6 +95,7 @@ public class FileWidget : SizeRequestBin {
private async void update_widget() {
bool show_image = FileImageWidget.can_display(file_transfer);
+ bool show_video = FileVideoWidget.can_display(file_transfer);
if (show_image && state != State.IMAGE) {
var content_bak = content;
@@ -114,7 +116,19 @@ public class FileWidget : SizeRequestBin {
} catch (Error e) { }
}
- if (!show_image && state != State.DEFAULT) {
+ if (show_image && state == State.IMAGE) return;
+
+ if (show_video && state != State.VIDEO) {
+ if (content != null) content.unparent();
+ content = new FileVideoWidget(file_transfer);
+ state = State.VIDEO;
+ content.insert_after(this, null);
+ return;
+ }
+
+ if (show_video && state == State.VIDEO) return;
+
+ if (state != State.DEFAULT) {
if (content != null) content.unparent();
FileDefaultWidget default_file_widget = new FileDefaultWidget();
default_widget_controller = new FileDefaultWidgetController(default_file_widget);