fractal/session/view/content/room_details/history_viewer/
audio.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone, CompositeTemplate};
3use tracing::error;
4
5use super::{AudioRow, HistoryViewerEvent, HistoryViewerEventType, HistoryViewerTimeline};
6use crate::{
7 components::LoadingRow,
8 prelude::*,
9 spawn,
10 utils::{BoundConstructOnlyObject, LoadingState},
11};
12
13const MIN_N_ITEMS: u32 = 20;
15
16mod imp {
17 use std::ops::ControlFlow;
18
19 use glib::subclass::InitializingObject;
20
21 use super::*;
22
23 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
24 #[template(
25 resource = "/org/gnome/Fractal/ui/session/view/content/room_details/history_viewer/audio.ui"
26 )]
27 #[properties(wrapper_type = super::AudioHistoryViewer)]
28 pub struct AudioHistoryViewer {
29 #[template_child]
30 stack: TemplateChild<gtk::Stack>,
31 #[template_child]
32 list_view: TemplateChild<gtk::ListView>,
33 #[property(get, set = Self::set_timeline, construct_only)]
35 timeline: BoundConstructOnlyObject<HistoryViewerTimeline>,
36 }
37
38 #[glib::object_subclass]
39 impl ObjectSubclass for AudioHistoryViewer {
40 const NAME: &'static str = "ContentAudioHistoryViewer";
41 type Type = super::AudioHistoryViewer;
42 type ParentType = adw::NavigationPage;
43
44 fn class_init(klass: &mut Self::Class) {
45 Self::bind_template(klass);
46 Self::bind_template_callbacks(klass);
47
48 klass.set_css_name("audio-history-viewer");
49 }
50
51 fn instance_init(obj: &InitializingObject<Self>) {
52 obj.init_template();
53 }
54 }
55
56 #[glib::derived_properties]
57 impl ObjectImpl for AudioHistoryViewer {
58 fn constructed(&self) {
59 self.parent_constructed();
60
61 let factory = gtk::SignalListItemFactory::new();
62
63 factory.connect_bind(move |_, list_item| {
64 let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
65 error!("List item factory did not receive a list item: {list_item:?}");
66 return;
67 };
68
69 list_item.set_activatable(false);
70 list_item.set_selectable(false);
71 });
72 factory.connect_bind(move |_, list_item| {
73 let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
74 error!("List item factory did not receive a list item: {list_item:?}");
75 return;
76 };
77
78 let item = list_item.item();
79
80 if let Some(loading_row) = item
81 .and_downcast_ref::<LoadingRow>()
82 .filter(|_| !list_item.child().is_some_and(|c| c.is::<LoadingRow>()))
83 {
84 loading_row.unparent();
85 loading_row.set_width_request(-1);
86 loading_row.set_height_request(-1);
87
88 list_item.set_child(Some(loading_row));
89 } else if let Some(event) = item.and_downcast::<HistoryViewerEvent>() {
90 let audio_row = list_item.child_or_default::<AudioRow>();
91 audio_row.set_event(Some(event));
92 }
93 });
94
95 self.list_view.set_factory(Some(&factory));
96 }
97 }
98
99 impl WidgetImpl for AudioHistoryViewer {}
100 impl NavigationPageImpl for AudioHistoryViewer {}
101
102 #[gtk::template_callbacks]
103 impl AudioHistoryViewer {
104 fn set_timeline(&self, timeline: HistoryViewerTimeline) {
106 let filter = gtk::CustomFilter::new(|obj| {
107 obj.downcast_ref::<HistoryViewerEvent>()
108 .is_some_and(|e| e.event_type() == HistoryViewerEventType::Audio)
109 || obj.is::<LoadingRow>()
110 });
111 let filter_model =
112 gtk::FilterListModel::new(Some(timeline.with_loading_item().clone()), Some(filter));
113
114 let model = gtk::NoSelection::new(Some(filter_model));
115 model.connect_items_changed(clone!(
116 #[weak(rename_to = imp)]
117 self,
118 move |_, _, _, _| {
119 imp.update_state();
120 }
121 ));
122 self.list_view.set_model(Some(&model));
123
124 let timeline_state_handler = timeline.connect_state_notify(clone!(
125 #[weak(rename_to = imp)]
126 self,
127 move |_| {
128 imp.update_state();
129 }
130 ));
131
132 self.timeline.set(timeline, vec![timeline_state_handler]);
133 self.update_state();
134
135 spawn!(clone!(
136 #[weak(rename_to = imp)]
137 self,
138 async move {
139 imp.init_timeline().await;
140 }
141 ));
142 }
143
144 async fn init_timeline(&self) {
146 self.load_more_items().await;
148
149 let adj = self
150 .list_view
151 .vadjustment()
152 .expect("GtkListView has a vadjustment");
153 adj.connect_value_notify(clone!(
154 #[weak(rename_to = imp)]
155 self,
156 move |_| {
157 if imp.needs_more_items() {
158 spawn!(async move {
159 imp.load_more_items().await;
160 });
161 }
162 }
163 ));
164 }
165
166 #[template_callback]
168 async fn load_more_items(&self) {
169 self.timeline
170 .obj()
171 .load(clone!(
172 #[weak(rename_to = imp)]
173 self,
174 #[upgrade_or]
175 ControlFlow::Break(()),
176 move || {
177 if imp.needs_more_items() {
178 ControlFlow::Continue(())
179 } else {
180 ControlFlow::Break(())
181 }
182 }
183 ))
184 .await;
185 }
186
187 fn needs_more_items(&self) -> bool {
189 let Some(model) = self.list_view.model() else {
190 return false;
191 };
192
193 if model.n_items() < MIN_N_ITEMS {
195 return true;
196 }
197
198 let adj = self
199 .list_view
200 .vadjustment()
201 .expect("GtkListView has a vadjustment");
202 adj.value() + adj.page_size() * 2.0 >= adj.upper()
203 }
204
205 fn update_state(&self) {
207 let Some(model) = self.list_view.model() else {
208 return;
209 };
210 let timeline = self.timeline.obj();
211
212 let visible_child_name = match timeline.state() {
213 LoadingState::Initial => "loading",
214 LoadingState::Error => "error",
215 LoadingState::Ready if model.n_items() == 0 => "empty",
216 LoadingState::Loading => {
217 if model.n_items() == 0
218 || (model.n_items() == 1
219 && model.item(0).is_some_and(|item| item.is::<LoadingRow>()))
220 {
221 "loading"
222 } else {
223 "content"
224 }
225 }
226 LoadingState::Ready => "content",
227 };
228 self.stack.set_visible_child_name(visible_child_name);
229 }
230 }
231}
232
233glib::wrapper! {
234 pub struct AudioHistoryViewer(ObjectSubclass<imp::AudioHistoryViewer>)
236 @extends gtk::Widget, adw::NavigationPage;
237}
238
239impl AudioHistoryViewer {
240 pub fn new(timeline: &HistoryViewerTimeline) -> Self {
241 glib::Object::builder()
242 .property("timeline", timeline)
243 .build()
244 }
245}