|
|
|
|
//! Collection of common methods and types.
|
|
|
|
|
|
|
|
|
|
mod dummy_object;
|
|
|
|
|
pub mod expression;
|
|
|
|
|
mod expression_list_model;
|
|
|
|
|
pub mod macros;
|
|
|
|
|
pub mod matrix;
|
|
|
|
|
pub mod media;
|
|
|
|
|
pub mod message_dialog;
|
|
|
|
|
pub mod notifications;
|
|
|
|
|
pub mod sourceview;
|
|
|
|
|
pub mod template_callbacks;
|
|
|
|
|
|
|
|
|
|
use std::{
|
|
|
|
|
cell::{OnceCell, RefCell},
|
|
|
|
|
rc::{Rc, Weak},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use futures_util::{
|
|
|
|
|
future::{self, Either, Future},
|
|
|
|
|
pin_mut,
|
|
|
|
|
};
|
|
|
|
|
use gtk::{gio, glib, prelude::*};
|
|
|
|
|
use matrix_sdk::ruma::UInt;
|
|
|
|
|
use once_cell::sync::Lazy;
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
use tracing::error;
|
|
|
|
|
|
|
|
|
|
pub use self::{dummy_object::DummyObject, expression_list_model::ExpressionListModel};
|
|
|
|
|
use crate::RUNTIME;
|
|
|
|
|
|
|
|
|
|
/// Converts a `UInt` to `i32`.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `-1` if the conversion didn't work.
|
|
|
|
|
pub fn uint_to_i32(u: Option<UInt>) -> i32 {
|
|
|
|
|
u.and_then(|ui| {
|
|
|
|
|
let u: Option<u16> = ui.try_into().ok();
|
|
|
|
|
u
|
|
|
|
|
})
|
|
|
|
|
.map(|u| {
|
|
|
|
|
let i: i32 = u.into();
|
|
|
|
|
i
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(-1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub enum TimeoutFuture {
|
|
|
|
|
Timeout,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Executes the given future with the given timeout.
|
|
|
|
|
///
|
|
|
|
|
/// If the future didn't resolve before the timeout was reached, this returns
|
|
|
|
|
/// an `Err(TimeoutFuture)`.
|
|
|
|
|
pub async fn timeout_future<T>(
|
|
|
|
|
timeout: std::time::Duration,
|
|
|
|
|
fut: impl Future<Output = T>,
|
|
|
|
|
) -> Result<T, TimeoutFuture> {
|
|
|
|
|
let timeout = glib::timeout_future(timeout);
|
|
|
|
|
pin_mut!(fut);
|
|
|
|
|
|
|
|
|
|
match future::select(fut, timeout).await {
|
|
|
|
|
Either::Left((x, _)) => Ok(x),
|
|
|
|
|
_ => Err(TimeoutFuture::Timeout),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Replace variables in the given string with the given dictionary.
|
|
|
|
|
///
|
|
|
|
|
/// The expected format to replace is `{name}`, where `name` is the first string
|
|
|
|
|
/// in the dictionary entry tuple.
|
|
|
|
|
pub fn freplace(s: String, args: &[(&str, &str)]) -> String {
|
|
|
|
|
let mut s = s;
|
|
|
|
|
|
|
|
|
|
for (k, v) in args {
|
|
|
|
|
s = s.replace(&format!("{{{k}}}"), v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if the given hostname is reachable.
|
|
|
|
|
pub async fn check_if_reachable(hostname: &impl AsRef<str>) -> bool {
|
|
|
|
|
let address = gio::NetworkAddress::parse_uri(hostname.as_ref(), 80).unwrap();
|
|
|
|
|
let monitor = gio::NetworkMonitor::default();
|
|
|
|
|
match monitor.can_reach_future(&address).await {
|
|
|
|
|
Ok(()) => true,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
error!("Homeserver {} isn't reachable: {error}", hostname.as_ref());
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Regex that matches a string that only includes emojis.
|
|
|
|
|
pub static EMOJI_REGEX: Lazy<Regex> = Lazy::new(|| {
|
|
|
|
|
Regex::new(
|
|
|
|
|
r"(?x)
|
|
|
|
|
^
|
|
|
|
|
[\p{White_Space}\p{Emoji_Component}]*
|
|
|
|
|
[\p{Emoji}--\p{Decimal_Number}]+
|
|
|
|
|
[\p{White_Space}\p{Emoji}\p{Emoji_Component}--\p{Decimal_Number}]*
|
|
|
|
|
$
|
|
|
|
|
# That string is made of at least one emoji, except digits, possibly more,
|
|
|
|
|
# possibly with modifiers, possibly with spaces, but nothing else
|
|
|
|
|
",
|
|
|
|
|
)
|
|
|
|
|
.unwrap()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// Inner to manage a bound object.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BoundObjectInner<T: glib::ObjectType> {
|
|
|
|
|
obj: T,
|
|
|
|
|
signal_handler_ids: Vec<glib::SignalHandlerId>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wrapper to manage a bound object.
|
|
|
|
|
///
|
|
|
|
|
/// This keeps a strong reference to the object.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BoundObject<T: glib::ObjectType> {
|
|
|
|
|
inner: RefCell<Option<BoundObjectInner<T>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> BoundObject<T> {
|
|
|
|
|
/// Creates a new empty `BoundObject`.
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set the given object and signal handlers IDs.
|
|
|
|
|
///
|
|
|
|
|
/// Calls `disconnect_signals` first to drop the previous strong reference
|
|
|
|
|
/// and disconnect the previous signal handlers.
|
|
|
|
|
pub fn set(&self, obj: T, signal_handler_ids: Vec<glib::SignalHandlerId>) {
|
|
|
|
|
self.disconnect_signals();
|
|
|
|
|
|
|
|
|
|
let inner = BoundObjectInner {
|
|
|
|
|
obj,
|
|
|
|
|
signal_handler_ids,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.inner.replace(Some(inner));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the object, if any.
|
|
|
|
|
pub fn obj(&self) -> Option<T> {
|
|
|
|
|
self.inner.borrow().as_ref().map(|inner| inner.obj.clone())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Disconnect the signal handlers and drop the strong reference.
|
|
|
|
|
pub fn disconnect_signals(&self) {
|
|
|
|
|
if let Some(inner) = self.inner.take() {
|
|
|
|
|
for signal_handler_id in inner.signal_handler_ids {
|
|
|
|
|
inner.obj.disconnect(signal_handler_id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> Default for BoundObject<T> {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
inner: Default::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> Drop for BoundObject<T> {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
self.disconnect_signals();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: IsA<glib::Object> + glib::HasParamSpec> glib::Property for BoundObject<T> {
|
|
|
|
|
type Value = Option<T>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: IsA<glib::Object>> glib::PropertyGet for BoundObject<T> {
|
|
|
|
|
type Value = Option<T>;
|
|
|
|
|
|
|
|
|
|
fn get<R, F: Fn(&Self::Value) -> R>(&self, f: F) -> R {
|
|
|
|
|
f(&self.obj())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wrapper to manage a bound object.
|
|
|
|
|
///
|
|
|
|
|
/// This keeps a weak reference to the object.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BoundObjectWeakRef<T: glib::ObjectType> {
|
|
|
|
|
weak_obj: glib::WeakRef<T>,
|
|
|
|
|
signal_handler_ids: RefCell<Vec<glib::SignalHandlerId>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> BoundObjectWeakRef<T> {
|
|
|
|
|
/// Creates a new empty `BoundObjectWeakRef`.
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set the given object and signal handlers IDs.
|
|
|
|
|
///
|
|
|
|
|
/// Calls `disconnect_signals` first to remove the previous weak reference
|
|
|
|
|
/// and disconnect the previous signal handlers.
|
|
|
|
|
pub fn set(&self, obj: &T, signal_handler_ids: Vec<glib::SignalHandlerId>) {
|
|
|
|
|
self.disconnect_signals();
|
|
|
|
|
|
|
|
|
|
self.weak_obj.set(Some(obj));
|
|
|
|
|
self.signal_handler_ids.replace(signal_handler_ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a strong reference to the object.
|
|
|
|
|
pub fn obj(&self) -> Option<T> {
|
|
|
|
|
self.weak_obj.upgrade()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Disconnect the signal handlers and drop the weak reference.
|
|
|
|
|
pub fn disconnect_signals(&self) {
|
|
|
|
|
let signal_handler_ids = self.signal_handler_ids.take();
|
|
|
|
|
|
|
|
|
|
if let Some(obj) = self.weak_obj.upgrade() {
|
|
|
|
|
for signal_handler_id in signal_handler_ids {
|
|
|
|
|
obj.disconnect(signal_handler_id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.weak_obj.set(None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> Default for BoundObjectWeakRef<T> {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
weak_obj: Default::default(),
|
|
|
|
|
signal_handler_ids: Default::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> Drop for BoundObjectWeakRef<T> {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
self.disconnect_signals();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: IsA<glib::Object> + glib::HasParamSpec> glib::Property for BoundObjectWeakRef<T> {
|
|
|
|
|
type Value = Option<T>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: IsA<glib::Object>> glib::PropertyGet for BoundObjectWeakRef<T> {
|
|
|
|
|
type Value = Option<T>;
|
|
|
|
|
|
|
|
|
|
fn get<R, F: Fn(&Self::Value) -> R>(&self, f: F) -> R {
|
|
|
|
|
f(&self.obj())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wrapper to manage a bound construct-only object.
|
|
|
|
|
///
|
|
|
|
|
/// This keeps a strong reference to the object.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct BoundConstructOnlyObject<T: glib::ObjectType> {
|
|
|
|
|
obj: OnceCell<T>,
|
|
|
|
|
signal_handler_ids: RefCell<Vec<glib::SignalHandlerId>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> BoundConstructOnlyObject<T> {
|
|
|
|
|
/// Creates a new empty `BoundConstructOnlyObject`.
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set the given object and signal handlers IDs.
|
|
|
|
|
///
|
|
|
|
|
/// Panics if the object was already set.
|
|
|
|
|
pub fn set(&self, obj: T, signal_handler_ids: Vec<glib::SignalHandlerId>) {
|
|
|
|
|
self.obj.set(obj).unwrap();
|
|
|
|
|
self.signal_handler_ids.replace(signal_handler_ids);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a strong reference to the object.
|
|
|
|
|
///
|
|
|
|
|
/// Panics if the object has not been set yet.
|
|
|
|
|
pub fn obj(&self) -> &T {
|
|
|
|
|
self.obj.get().unwrap()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> Default for BoundConstructOnlyObject<T> {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
obj: Default::default(),
|
|
|
|
|
signal_handler_ids: Default::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::ObjectType> Drop for BoundConstructOnlyObject<T> {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let signal_handler_ids = self.signal_handler_ids.take();
|
|
|
|
|
|
|
|
|
|
if let Some(obj) = self.obj.get() {
|
|
|
|
|
for signal_handler_id in signal_handler_ids {
|
|
|
|
|
obj.disconnect(signal_handler_id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: IsA<glib::Object> + glib::HasParamSpec> glib::Property for BoundConstructOnlyObject<T> {
|
|
|
|
|
type Value = T;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: IsA<glib::Object>> glib::PropertyGet for BoundConstructOnlyObject<T> {
|
|
|
|
|
type Value = T;
|
|
|
|
|
|
|
|
|
|
fn get<R, F: Fn(&Self::Value) -> R>(&self, f: F) -> R {
|
|
|
|
|
f(self.obj())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Helper type to keep track of ongoing async actions that can succeed in
|
|
|
|
|
/// different functions.
|
|
|
|
|
///
|
|
|
|
|
/// This type can only have one strong reference and many weak references.
|
|
|
|
|
///
|
|
|
|
|
/// The strong reference should be dropped in the first function where the
|
|
|
|
|
/// action succeeds. Then other functions can drop the weak references when
|
|
|
|
|
/// they can't be upgraded.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct OngoingAsyncAction<T> {
|
|
|
|
|
strong: Rc<AsyncAction<T>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> OngoingAsyncAction<T> {
|
|
|
|
|
/// Create a new async action that sets the given value.
|
|
|
|
|
///
|
|
|
|
|
/// Returns both a strong and a weak reference.
|
|
|
|
|
pub fn set(value: T) -> (Self, WeakOngoingAsyncAction<T>) {
|
|
|
|
|
let strong = Rc::new(AsyncAction::Set(value));
|
|
|
|
|
let weak = Rc::downgrade(&strong);
|
|
|
|
|
(Self { strong }, WeakOngoingAsyncAction { weak })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a new async action that removes a value.
|
|
|
|
|
///
|
|
|
|
|
/// Returns both a strong and a weak reference.
|
|
|
|
|
pub fn remove() -> (Self, WeakOngoingAsyncAction<T>) {
|
|
|
|
|
let strong = Rc::new(AsyncAction::Remove);
|
|
|
|
|
let weak = Rc::downgrade(&strong);
|
|
|
|
|
(Self { strong }, WeakOngoingAsyncAction { weak })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a new weak reference to this async action.
|
|
|
|
|
pub fn downgrade(&self) -> WeakOngoingAsyncAction<T> {
|
|
|
|
|
let weak = Rc::downgrade(&self.strong);
|
|
|
|
|
WeakOngoingAsyncAction { weak }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The inner action.
|
|
|
|
|
pub fn action(&self) -> &AsyncAction<T> {
|
|
|
|
|
&self.strong
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the inner value, if any.
|
|
|
|
|
pub fn as_value(&self) -> Option<&T> {
|
|
|
|
|
self.strong.as_value()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A weak reference to an `OngoingAsyncAction`.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct WeakOngoingAsyncAction<T> {
|
|
|
|
|
weak: Weak<AsyncAction<T>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> WeakOngoingAsyncAction<T> {
|
|
|
|
|
/// Whether this async action is still ongoing (i.e. whether the strong
|
|
|
|
|
/// reference still exists).
|
|
|
|
|
pub fn is_ongoing(&self) -> bool {
|
|
|
|
|
self.weak.strong_count() > 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// An async action.
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub enum AsyncAction<T> {
|
|
|
|
|
/// An async action is ongoing to set this value.
|
|
|
|
|
Set(T),
|
|
|
|
|
|
|
|
|
|
/// An async action is ongoing to remove a value.
|
|
|
|
|
Remove,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> AsyncAction<T> {
|
|
|
|
|
/// Get the inner value, if any.
|
|
|
|
|
pub fn as_value(&self) -> Option<&T> {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Set(value) => Some(value),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A type that requires the tokio runtime to be running when dropped.
|
|
|
|
|
///
|
|
|
|
|
/// This is basically usable as a [`OnceCell`].
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct TokioDrop<T>(OnceCell<T>);
|
|
|
|
|
|
|
|
|
|
impl<T> TokioDrop<T> {
|
|
|
|
|
/// Create a new empty `TokioDrop`;
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
Self::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets a reference to the underlying value.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `None` if the cell is empty.
|
|
|
|
|
pub fn get(&self) -> Option<&T> {
|
|
|
|
|
self.0.get()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sets the contents of this cell to `value`.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `Ok(())` if the cell was empty and `Err(value)` if it was full.
|
|
|
|
|
pub fn set(&self, value: T) -> Result<(), T> {
|
|
|
|
|
self.0.set(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> Default for TokioDrop<T> {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self(Default::default())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> Drop for TokioDrop<T> {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
let _guard = RUNTIME.enter();
|
|
|
|
|
|
|
|
|
|
if let Some(inner) = self.0.take() {
|
|
|
|
|
drop(inner)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T: glib::Property> glib::Property for TokioDrop<T> {
|
|
|
|
|
type Value = T::Value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> glib::PropertyGet for TokioDrop<T> {
|
|
|
|
|
type Value = T;
|
|
|
|
|
|
|
|
|
|
fn get<R, F: Fn(&Self::Value) -> R>(&self, f: F) -> R {
|
|
|
|
|
f(self.get().unwrap())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> glib::PropertySet for TokioDrop<T> {
|
|
|
|
|
type SetValue = T;
|
|
|
|
|
|
|
|
|
|
fn set(&self, v: Self::SetValue) {
|
|
|
|
|
if self.set(v).is_err() {
|
|
|
|
|
panic!("TokioDrop value was already set");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The state of a resource that can be loaded.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, glib::Enum)]
|
|
|
|
|
#[enum_type(name = "LoadingState")]
|
|
|
|
|
pub enum LoadingState {
|
|
|
|
|
/// It hasn't been loaded yet.
|
|
|
|
|
#[default]
|
|
|
|
|
Initial,
|
|
|
|
|
/// It is currently loading.
|
|
|
|
|
Loading,
|
|
|
|
|
/// It has been fully loaded.
|
|
|
|
|
Ready,
|
|
|
|
|
/// An error occurred while loading it.
|
|
|
|
|
Error,
|
|
|
|
|
}
|