mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-01 17:15:28 +00:00
implement custom search providers feature
This commit is contained in:
parent
58d4439fcf
commit
b8a8fb49de
16 changed files with 668 additions and 30 deletions
|
|
@ -1,6 +1,7 @@
|
|||
mod database;
|
||||
mod identity;
|
||||
mod primary_icon;
|
||||
mod search;
|
||||
|
||||
use adw::{prelude::AdwDialogExt, AlertDialog};
|
||||
use primary_icon::PrimaryIcon;
|
||||
|
|
@ -44,6 +45,7 @@ pub trait Request {
|
|||
|
||||
fn update_primary_icon(&self, profile: &Profile);
|
||||
fn show_identity_dialog(&self, profile: &Rc<Profile>);
|
||||
fn show_search_dialog(&self, profile: &Rc<Profile>);
|
||||
|
||||
// Setters
|
||||
|
||||
|
|
@ -78,7 +80,13 @@ impl Request for Entry {
|
|||
entry.connect_icon_release({
|
||||
let profile = profile.clone();
|
||||
move |this, position| match position {
|
||||
EntryIconPosition::Primary => this.show_identity_dialog(&profile),
|
||||
EntryIconPosition::Primary => {
|
||||
if matches!(primary_icon::from(&this.text()), PrimaryIcon::Search { .. }) {
|
||||
this.show_search_dialog(&profile)
|
||||
} else {
|
||||
this.show_identity_dialog(&profile)
|
||||
}
|
||||
}
|
||||
EntryIconPosition::Secondary => {
|
||||
this.activate();
|
||||
}
|
||||
|
|
@ -217,11 +225,10 @@ impl Request for Entry {
|
|||
// Update primary icon
|
||||
self.first_child().unwrap().remove_css_class("success"); // @TODO handle
|
||||
|
||||
self.set_primary_icon_activatable(false);
|
||||
self.set_primary_icon_sensitive(false);
|
||||
|
||||
match primary_icon::from(&self.text()) {
|
||||
PrimaryIcon::Download { name, tooltip } => {
|
||||
self.set_primary_icon_activatable(false);
|
||||
self.set_primary_icon_sensitive(false);
|
||||
self.set_primary_icon_name(Some(name));
|
||||
self.set_primary_icon_tooltip_text(Some(tooltip));
|
||||
}
|
||||
|
|
@ -237,17 +244,21 @@ impl Request for Entry {
|
|||
}
|
||||
}
|
||||
PrimaryIcon::Search { name, tooltip } => {
|
||||
self.set_primary_icon_activatable(true);
|
||||
self.set_primary_icon_sensitive(true);
|
||||
self.set_primary_icon_name(Some(name));
|
||||
self.set_primary_icon_tooltip_text(Some(tooltip));
|
||||
}
|
||||
PrimaryIcon::Source { name, tooltip } => {
|
||||
self.set_primary_icon_activatable(false);
|
||||
self.set_primary_icon_sensitive(false);
|
||||
self.set_primary_icon_name(Some(name));
|
||||
self.set_primary_icon_tooltip_text(Some(tooltip));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Present identity [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
|
||||
/// Present Identity [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
|
||||
fn show_identity_dialog(&self, profile: &Rc<Profile>) {
|
||||
// connect identity traits
|
||||
use identity::{Common, Unsupported};
|
||||
|
|
@ -273,6 +284,12 @@ impl Request for Entry {
|
|||
AlertDialog::unsupported().present(Some(self));
|
||||
}
|
||||
|
||||
/// Present Search providers [AlertDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.AlertDialog.html) for `Self`
|
||||
fn show_search_dialog(&self, profile: &Rc<Profile>) {
|
||||
use search::Search;
|
||||
AlertDialog::search(profile).present(Some(self))
|
||||
}
|
||||
|
||||
// Setters
|
||||
|
||||
fn to_download(&self) {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,6 @@ pub fn from(request: &str) -> PrimaryIcon {
|
|||
|
||||
PrimaryIcon::Search {
|
||||
name: "system-search-symbolic",
|
||||
tooltip: "Search",
|
||||
tooltip: "Choose default search provider",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
mod form;
|
||||
|
||||
use crate::Profile;
|
||||
use adw::AlertDialog;
|
||||
use adw::{
|
||||
prelude::{AlertDialogExt, AlertDialogExtManual},
|
||||
ResponseAppearance,
|
||||
};
|
||||
use form::{list::item::Value, list::Item, Form, Query};
|
||||
use gtk::prelude::{EditableExt, WidgetExt};
|
||||
use sourceview::prelude::CastNone;
|
||||
use std::rc::Rc;
|
||||
|
||||
// Response variants
|
||||
const RESPONSE_APPLY: (&str, &str) = ("apply", "Apply");
|
||||
const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel");
|
||||
// const RESPONSE_MANAGE: (&str, &str) = ("manage", "Manage");
|
||||
|
||||
pub trait Search {
|
||||
fn search(profile: &Rc<Profile>) -> Self;
|
||||
}
|
||||
|
||||
impl Search for AlertDialog {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
fn search(profile: &Rc<Profile>) -> Self {
|
||||
// Init child container
|
||||
let form = Rc::new(Form::build(profile));
|
||||
|
||||
// Init main widget
|
||||
let alert_dialog = AlertDialog::builder()
|
||||
.heading("Search")
|
||||
.body("Select default provider")
|
||||
.close_response(RESPONSE_CANCEL.0)
|
||||
.default_response(RESPONSE_APPLY.0)
|
||||
.extra_child(&form.g_box)
|
||||
.build();
|
||||
|
||||
alert_dialog.add_responses(&[
|
||||
RESPONSE_CANCEL,
|
||||
// RESPONSE_MANAGE,
|
||||
RESPONSE_APPLY,
|
||||
]);
|
||||
|
||||
// Init events
|
||||
|
||||
form.list.dropdown.connect_selected_item_notify({
|
||||
let alert_dialog = alert_dialog.clone();
|
||||
let form = form.clone();
|
||||
move |_| update(&alert_dialog, &form)
|
||||
});
|
||||
|
||||
form.query.connect_changed({
|
||||
let alert_dialog = alert_dialog.clone();
|
||||
let form = form.clone();
|
||||
move |_| update(&alert_dialog, &form)
|
||||
});
|
||||
|
||||
alert_dialog.connect_realize({
|
||||
let form = form.clone();
|
||||
move |this| update(this, &form)
|
||||
});
|
||||
|
||||
alert_dialog.connect_response(Some(RESPONSE_APPLY.0), {
|
||||
let form = form.clone();
|
||||
let profile = profile.clone();
|
||||
move |this, response| {
|
||||
// Prevent double-click action
|
||||
this.set_response_enabled(response, false);
|
||||
|
||||
match form.list.selected().value_enum() {
|
||||
Value::ProfileSearchId(profile_search_id) => {
|
||||
if profile.search.set_default(profile_search_id).is_err() {
|
||||
todo!() // unexpected @TODO handle
|
||||
}
|
||||
}
|
||||
Value::Add => {
|
||||
if profile
|
||||
.search
|
||||
.add(&form.query.uri().unwrap(), true)
|
||||
.is_err()
|
||||
{
|
||||
todo!() // unexpected @TODO handle
|
||||
}
|
||||
}
|
||||
} // @TODO thread::spawn(|| {})
|
||||
}
|
||||
});
|
||||
|
||||
// Deactivate not implemented feature @TODO
|
||||
// alert_dialog.set_response_enabled(RESPONSE_MANAGE.0, false);
|
||||
|
||||
// Decorate default response preset
|
||||
alert_dialog.set_response_appearance(RESPONSE_APPLY.0, ResponseAppearance::Suggested);
|
||||
/* contrast issue with Ubuntu orange accents
|
||||
alert_dialog.set_response_appearance(RESPONSE_CANCEL.0, ResponseAppearance::Destructive); */
|
||||
|
||||
// Return new activated `Self`
|
||||
alert_dialog
|
||||
}
|
||||
}
|
||||
|
||||
fn update(alert_dialog: &AlertDialog, form: &Form) {
|
||||
match form
|
||||
.list
|
||||
.dropdown
|
||||
.selected_item()
|
||||
.and_downcast::<Item>()
|
||||
.unwrap()
|
||||
.value_enum()
|
||||
{
|
||||
Value::Add => {
|
||||
form.drop.set_visible(false);
|
||||
form.query.set_visible(true);
|
||||
if form.query.focus_child().is_none() {
|
||||
form.query.grab_focus();
|
||||
}
|
||||
form.query.remove_css_class("error");
|
||||
alert_dialog.set_response_enabled(
|
||||
RESPONSE_APPLY.0,
|
||||
if form.query.is_valid() {
|
||||
true
|
||||
} else {
|
||||
if !form.query.text().is_empty() {
|
||||
form.query.add_css_class("error");
|
||||
}
|
||||
false
|
||||
},
|
||||
);
|
||||
}
|
||||
Value::ProfileSearchId(_) => {
|
||||
form.drop.set_visible(true);
|
||||
form.query.set_visible(false);
|
||||
alert_dialog.set_response_enabled(RESPONSE_APPLY.0, !form.list.selected().is_default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
pub mod drop;
|
||||
pub mod list;
|
||||
pub mod query;
|
||||
|
||||
use crate::Profile;
|
||||
use drop::Drop;
|
||||
use gtk::{prelude::BoxExt, Box, Button, Entry, Orientation};
|
||||
use list::List;
|
||||
pub use query::Query;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct Form {
|
||||
pub drop: Button,
|
||||
pub list: Rc<List>,
|
||||
pub query: Entry,
|
||||
pub g_box: Box,
|
||||
}
|
||||
|
||||
impl Form {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(profile: &Rc<Profile>) -> Self {
|
||||
// Init components
|
||||
let list = Rc::new(List::build(profile));
|
||||
let query = Entry::query();
|
||||
let drop = Button::drop(profile, &list);
|
||||
|
||||
// Init main container
|
||||
let g_box = Box::builder().orientation(Orientation::Vertical).build();
|
||||
|
||||
g_box.append(&list.dropdown);
|
||||
g_box.append(&query);
|
||||
g_box.append(&drop);
|
||||
|
||||
Self {
|
||||
drop,
|
||||
list,
|
||||
query,
|
||||
g_box,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
use super::list::{item::Value, List};
|
||||
use crate::profile::Profile;
|
||||
use adw::{
|
||||
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
|
||||
AlertDialog, ResponseAppearance,
|
||||
};
|
||||
use gtk::{
|
||||
prelude::{ButtonExt, WidgetExt},
|
||||
Button,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub trait Drop {
|
||||
fn drop(profile: &Rc<Profile>, list: &Rc<List>) -> Self;
|
||||
}
|
||||
|
||||
impl Drop for Button {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
fn drop(profile: &Rc<Profile>, list: &Rc<List>) -> Self {
|
||||
// Defaults
|
||||
|
||||
const LABEL: &str = "Delete";
|
||||
const TOOLTIP_TEXT: &str = "Drop selected provider from profile";
|
||||
const MARGIN: i32 = 8;
|
||||
|
||||
const HEADING: &str = "Delete";
|
||||
const BODY: &str = "Delete selected provider from profile?";
|
||||
const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel");
|
||||
const RESPONSE_CONFIRM: (&str, &str) = ("confirm", "Confirm");
|
||||
|
||||
// Init main widget
|
||||
let button = Button::builder()
|
||||
.label(LABEL)
|
||||
.margin_top(MARGIN)
|
||||
.tooltip_text(TOOLTIP_TEXT)
|
||||
.visible(false)
|
||||
.build();
|
||||
|
||||
// Init events
|
||||
button.connect_clicked({
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
move |_| {
|
||||
match list.selected().value_enum() {
|
||||
Value::ProfileSearchId(profile_search_id) => {
|
||||
// Init sub-widget
|
||||
let alert_dialog = AlertDialog::builder()
|
||||
.heading(HEADING)
|
||||
.body(BODY)
|
||||
.close_response(RESPONSE_CANCEL.0)
|
||||
.default_response(RESPONSE_CANCEL.0)
|
||||
.build();
|
||||
|
||||
// Set response variants
|
||||
alert_dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_CONFIRM]);
|
||||
|
||||
// Decorate default response preset
|
||||
alert_dialog.set_response_appearance(
|
||||
RESPONSE_CONFIRM.0,
|
||||
ResponseAppearance::Suggested,
|
||||
);
|
||||
|
||||
/* contrast issue with Ubuntu orange accents
|
||||
alert_dialog.set_response_appearance(
|
||||
RESPONSE_CANCEL.0,
|
||||
ResponseAppearance::Destructive,
|
||||
); */
|
||||
|
||||
// Connect confirmation event
|
||||
alert_dialog.connect_response(Some(RESPONSE_CONFIRM.0), {
|
||||
let button = button.clone();
|
||||
let list = list.clone();
|
||||
let profile = profile.clone();
|
||||
move |_, _| match profile.search.delete(profile_search_id) {
|
||||
Ok(_) => {
|
||||
if list.remove(profile_search_id).is_some() {
|
||||
button.set_css_classes(&["success"]);
|
||||
button.set_label("Provider successfully deleted")
|
||||
} else {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label("List item not found")
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
button.set_css_classes(&["error"]);
|
||||
button.set_label(&e.to_string())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show dialog
|
||||
alert_dialog.present(Some(&button))
|
||||
}
|
||||
_ => todo!(), // unexpected
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return activated `Self`
|
||||
button
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
pub mod item;
|
||||
|
||||
use crate::profile::Profile;
|
||||
use gtk::{
|
||||
gio::{
|
||||
prelude::{Cast, CastNone},
|
||||
ListStore,
|
||||
},
|
||||
prelude::{BoxExt, ListItemExt, ObjectExt, WidgetExt},
|
||||
Align, Box, DropDown, Label, ListItem, Orientation, SignalListItemFactory,
|
||||
};
|
||||
pub use item::Item;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct List {
|
||||
pub dropdown: DropDown,
|
||||
list_store: ListStore,
|
||||
}
|
||||
|
||||
impl List {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
pub fn build(profile: &Rc<Profile>) -> Self {
|
||||
// Init dropdown items
|
||||
let new_search_provider = Item::add();
|
||||
|
||||
// Init model
|
||||
let list_store = ListStore::new::<Item>();
|
||||
|
||||
list_store.append(&new_search_provider);
|
||||
for record in profile.search.records() {
|
||||
list_store.append(&Item::profile_search_id(
|
||||
record.id,
|
||||
&record.query,
|
||||
record.is_default,
|
||||
))
|
||||
}
|
||||
|
||||
// Setup item factory
|
||||
// * wanted only to append items after `DropDown` init
|
||||
let factory = SignalListItemFactory::new();
|
||||
|
||||
factory.connect_setup(|_, this| {
|
||||
// Init widget for dropdown item
|
||||
// * legacy container, exists because maybe some other elements will be added later
|
||||
let child = Box::builder()
|
||||
.orientation(Orientation::Vertical)
|
||||
.valign(Align::Center)
|
||||
.build();
|
||||
|
||||
// Title
|
||||
child.append(&Label::builder().halign(Align::Start).build());
|
||||
|
||||
// Done
|
||||
this.downcast_ref::<ListItem>()
|
||||
.unwrap()
|
||||
.set_child(Some(&child));
|
||||
});
|
||||
|
||||
factory.connect_bind(|_, this| {
|
||||
// Downcast requirements
|
||||
let list_item = this.downcast_ref::<ListItem>().unwrap();
|
||||
let item = list_item.item().and_downcast::<Item>().unwrap();
|
||||
let child = list_item.child().and_downcast::<Box>().unwrap();
|
||||
|
||||
// Bind `title`
|
||||
match child.first_child().and_downcast::<Label>() {
|
||||
Some(label) => {
|
||||
label.set_label(&item.title());
|
||||
label.set_css_classes(if item.is_default() { &["accent"] } else { &[] });
|
||||
item.bind_property("title", &label, "label").build(); // sync label
|
||||
item.bind_property("is-default", &label, "css-classes")
|
||||
.transform_to(|_, is_default| {
|
||||
if is_default {
|
||||
Some(vec!["accent".to_string()])
|
||||
} else {
|
||||
Some(vec![])
|
||||
}
|
||||
})
|
||||
.build(); // sync class by status
|
||||
}
|
||||
None => todo!(),
|
||||
}
|
||||
});
|
||||
|
||||
// Init main widget
|
||||
let dropdown = DropDown::builder()
|
||||
.model(&list_store)
|
||||
.selected(
|
||||
list_store
|
||||
.find_with_equal_func(|item| {
|
||||
item.dynamic_cast_ref::<Item>().unwrap().is_default()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.factory(&factory)
|
||||
.build();
|
||||
|
||||
// Return activated `Self`
|
||||
Self {
|
||||
dropdown,
|
||||
list_store,
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
/// Find list item by `profile_search_id`
|
||||
/// * return `position` found
|
||||
pub fn find(&self, profile_search_id: i64) -> Option<u32> {
|
||||
self.list_store.find_with_equal_func(|this| {
|
||||
profile_search_id == this.downcast_ref::<Item>().unwrap().value()
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove list item by `profile_search_id`
|
||||
/// * return `position` of removed list item
|
||||
pub fn remove(&self, profile_search_id: i64) -> Option<u32> {
|
||||
match self.find(profile_search_id) {
|
||||
Some(position) => {
|
||||
self.list_store.remove(position);
|
||||
Some(position)
|
||||
}
|
||||
None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get selected `Item` GObject
|
||||
pub fn selected(&self) -> Item {
|
||||
self.dropdown
|
||||
.selected_item()
|
||||
.and_downcast::<Item>()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
mod imp;
|
||||
pub mod value;
|
||||
|
||||
use gtk::glib::{self, Object, Uri, UriFlags};
|
||||
pub use value::Value;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Item(ObjectSubclass<imp::Item>);
|
||||
}
|
||||
|
||||
// C-type property `value` conversion for `Item`
|
||||
// * values > 0 reserved for `profile_search_id`
|
||||
const G_VALUE_ADD: i64 = 0;
|
||||
|
||||
impl Item {
|
||||
// Constructors
|
||||
|
||||
pub fn add() -> Self {
|
||||
Object::builder()
|
||||
.property("value", G_VALUE_ADD)
|
||||
.property("title", "Add new..")
|
||||
.property("is-default", false)
|
||||
.build()
|
||||
}
|
||||
|
||||
pub fn profile_search_id(profile_search_id: i64, query: &str, is_default: bool) -> Self {
|
||||
Object::builder()
|
||||
.property("value", profile_search_id)
|
||||
.property(
|
||||
"title",
|
||||
Uri::parse(query, UriFlags::NONE).unwrap().host().unwrap(),
|
||||
) // @TODO handle
|
||||
.property("is-default", is_default)
|
||||
.build()
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
/// Get `Self` C-value as `Value`
|
||||
pub fn value_enum(&self) -> Value {
|
||||
match self.value() {
|
||||
G_VALUE_ADD => Value::Add,
|
||||
value => Value::ProfileSearchId(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
//! Custom `GObject` implementation for dropdown
|
||||
//! [ListStore](https://docs.gtk.org/gio/class.ListStore.html) menu item
|
||||
|
||||
use gtk::{
|
||||
gio::subclass::prelude::{DerivedObjectProperties, ObjectImpl, ObjectImplExt, ObjectSubclass},
|
||||
glib::{self, Object, Properties},
|
||||
prelude::ObjectExt,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[derive(Properties, Default)]
|
||||
#[properties(wrapper_type = super::Item)]
|
||||
pub struct Item {
|
||||
#[property(get, set)]
|
||||
value: Cell<i64>,
|
||||
#[property(get, set)]
|
||||
title: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
is_default: Cell<bool>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Item {
|
||||
const NAME: &'static str = "SearchItem"; // @TODO make globally unique
|
||||
type Type = super::Item;
|
||||
type ParentType = Object;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Item {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#[derive(Debug)]
|
||||
pub enum Value {
|
||||
Add,
|
||||
ProfileSearchId(i64),
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
use gtk::{
|
||||
glib::{Uri, UriFlags},
|
||||
prelude::{EditableExt, EntryExt},
|
||||
Entry,
|
||||
};
|
||||
|
||||
const MIN_LENGTH: u16 = 1;
|
||||
const MAX_LENGTH: u16 = 1024;
|
||||
|
||||
pub trait Query {
|
||||
// Constructors
|
||||
|
||||
fn query() -> Self;
|
||||
|
||||
// Actions
|
||||
|
||||
fn uri(&self) -> Result<Uri, String>;
|
||||
|
||||
// Getters
|
||||
|
||||
fn is_valid(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Query for Entry {
|
||||
// Constructors
|
||||
|
||||
/// Create new `Self`
|
||||
fn query() -> Self {
|
||||
Entry::builder()
|
||||
.margin_top(8)
|
||||
.max_length(MAX_LENGTH as i32)
|
||||
.placeholder_text("Provider query URL")
|
||||
.visible(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn uri(&self) -> Result<Uri, String> {
|
||||
match Uri::parse(&self.text(), UriFlags::NONE) {
|
||||
Ok(uri) => {
|
||||
if !uri.scheme().is_empty()
|
||||
&& uri.host().is_some_and(|host| !host.is_empty())
|
||||
&& uri.query().is_none()
|
||||
{
|
||||
Ok(uri)
|
||||
} else {
|
||||
Err("Invalid query URL".to_string())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
self.text_length() >= MIN_LENGTH && self.text_length() <= MAX_LENGTH && self.uri().is_ok()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue