1
0

Search functionality

This commit is contained in:
2025-02-12 14:33:42 +02:00
parent d01aa8d3c5
commit 61d91247e7
11 changed files with 556 additions and 55 deletions

View File

@ -516,9 +516,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.12"
version = "1.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda"
dependencies = [
"jobserver",
"libc",
@ -2311,9 +2311,9 @@ dependencies = [
[[package]]
name = "miniz_oxide"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
dependencies = [
"adler2",
"simd-adler32",
@ -3257,6 +3257,7 @@ dependencies = [
"data",
"iced",
"service",
"strum",
]
[[package]]
@ -3892,6 +3893,28 @@ dependencies = [
"unicode-properties",
]
[[package]]
name = "strum"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.98",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -4118,9 +4141,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.23"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"toml_datetime",
@ -4999,9 +5022,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
dependencies = [
"memchr",
]

View File

@ -8,6 +8,7 @@ data = { path = "data" }
service = { path = "service" }
iced = { version = "0.13.1", features = ["tokio", "lazy"] }
strum = { version = "0.27.0", features = ["derive"] }
[workspace]
resolver = "2"

View File

@ -0,0 +1,103 @@
-- Insert Users
INSERT INTO Users (name, email, password, last_used) VALUES
('alice', 'alice@example.com', 'password123', NOW()),
('bob', 'bob@example.com', 'securepass', NOW()),
('charlie', 'charlie@example.com', 'charliepwd', NOW()),
('dave', 'dave@example.com', 'davepass', NOW()),
('eve', 'eve@example.com', 'evepwd', NOW()),
('frank', 'frank@example.com', 'frankpass', NOW()),
('grace', 'grace@example.com', 'gracepwd', NOW()),
('heidi', 'heidi@example.com', 'heidipwd', NOW()),
('ivan', 'ivan@example.com', 'ivanpass', NOW()),
('judy', 'judy@example.com', 'judypass', NOW()),
('mallory', 'mallory@example.com', 'mallorypwd', NOW()),
('oscar', 'oscar@example.com', 'oscarpass', NOW()),
('peggy', 'peggy@example.com', 'peggypwd', NOW()),
('trent', 'trent@example.com', 'trentpass', NOW()),
('victor', 'victor@example.com', 'victorpwd', NOW());
-- Insert PackageBases
INSERT INTO PackageBases (name, description) VALUES
('libcore', 'Core system libraries'),
('webframework', 'A modern web framework'),
('dataproc', 'Data processing toolkit'),
('authmodule', 'Authentication and authorization module'),
('networkstack', 'Networking utilities and stack'),
('uikit', 'UI Kit for building interfaces'),
('cryptoengine', 'Cryptographic library'),
('dbconnector', 'Database connectivity drivers'),
('imageproc', 'Image processing library'),
('audiokit', 'Audio toolkit'),
('videokit', 'Video processing toolkit'),
('mlcore', 'Machine Learning core library'),
('analyticspro', 'Advanced analytics toolkit'),
('monitoragent', 'System monitoring agent'),
('filesystem', 'Filesystem utilities');
-- Assign Roles to Users for PackageBases
INSERT INTO PackageBaseUserRoles (base, user, role, comment) VALUES
(1, 1, 1, 'Original submitter'),
(1, 2, 2, 'Packager for latest release'),
(2, 3, 3, 'Maintains stability'),
(2, 4, 4, 'Flags issues'),
(3, 5, 1, 'Initial submission'),
(3, 6, 3, 'Lead maintainer'),
(4, 7, 2, 'Core packager'),
(5, 8, 1, 'Submitted new version'),
(6, 9, 4, 'Flagged for performance issues'),
(7, 10, 3, 'Maintainer for security fixes'),
(8, 11, 2, 'Driver package manager'),
(9, 12, 1, 'Original contributor'),
(10, 13, 3, 'Maintains core features'),
(11, 14, 4, 'Reported critical bug'),
(12, 15, 2, 'Optimized build process');
-- Insert Packages
INSERT INTO Packages (base, name, version, description, url) VALUES
(1, 'libcore-utils', '1.0.0', 'Utilities for libcore', 'http://example.com/libcore-utils'),
(1, 'libcore-extended', '1.1.0', 'Extended functionalities', 'http://example.com/libcore-extended'),
(2, 'webframework-api', '2.0.0', 'REST API module', 'http://example.com/webframework-api'),
(2, 'webframework-cli', '2.1.0', 'Command-line tools', 'http://example.com/webframework-cli'),
(3, 'dataproc-engine', '3.0.1', 'Data processing engine', 'http://example.com/dataproc-engine'),
(4, 'authmodule-oauth', '4.2.0', 'OAuth module', 'http://example.com/authmodule-oauth'),
(5, 'networkstack-core', '5.5.0', 'Core network stack', 'http://example.com/networkstack-core'),
(6, 'uikit-designer', '6.0.3', 'UI designer toolkit', 'http://example.com/uikit-designer'),
(7, 'cryptoengine-hash', '7.1.1', 'Hash algorithms', 'http://example.com/cryptoengine-hash'),
(8, 'dbconnector-mysql', '8.0.0', 'MySQL connector', 'http://example.com/dbconnector-mysql'),
(9, 'imageproc-filters', '9.3.0', 'Image filters library', 'http://example.com/imageproc-filters'),
(10, 'audiokit-mixer', '10.2.1', 'Audio mixing toolkit', 'http://example.com/audiokit-mixer'),
(11, 'videokit-stream', '11.4.0', 'Video streaming tools', 'http://example.com/videokit-stream'),
(12, 'mlcore-algo', '12.0.2', 'ML algorithms', 'http://example.com/mlcore-algo'),
(13, 'analyticspro-dashboard', '13.5.1', 'Analytics dashboard', 'http://example.com/analyticspro-dashboard');
-- Insert PackageDependencies
INSERT INTO PackageDependencies (arch, requirement, description, package, dependency_type, dependency_package_name) VALUES
('x86_64', '>=1.0.0', 'Core dependency', 3, 1, 'libcore-utils'),
('x86_64', '>=2.0.0', 'Required for API', 4, 2, 'webframework-api'),
('arm64', '>=3.0.1', 'Optional analytics', 5, 4, 'analyticspro-dashboard'),
('x86_64', '>=5.5.0', 'Network stack dependency', 6, 1, 'networkstack-core'),
('x86_64', '>=4.2.0', 'Authentication module', 7, 1, 'authmodule-oauth'),
('x86_64', NULL, 'Database driver', 8, 1, 'dbconnector-mysql'),
('arm64', NULL, 'Machine learning algorithms', 9, 3, 'mlcore-algo'),
('x86_64', '>=6.0.3', 'UI designer toolkit', 10, 1, 'uikit-designer'),
('x86_64', NULL, 'Audio toolkit dependency', 11, 2, 'audiokit-mixer'),
('x86_64', '>=7.1.1', 'Hash functions', 12, 1, 'cryptoengine-hash'),
('arm64', NULL, 'Video streaming tools', 13, 4, 'videokit-stream'),
('x86_64', '>=9.3.0', 'Image filters', 14, 1, 'imageproc-filters'),
('x86_64', NULL, 'System monitoring agent', 15, 2, 'monitoragent');
-- Insert PackageRelations
INSERT INTO PackageRelations (arch, requirement, package, relation_type, relation_package_name) VALUES
('x86_64', '>=1.0.0', 3, 1, 'legacy-web-api'), -- conflicts
('x86_64', NULL, 4, 2, 'web-cli-tools'), -- provides
('arm64', NULL, 5, 3, 'old-dataproc'), -- replaces
('x86_64', '>=5.0.0', 6, 1, 'net-tools-legacy'),
('x86_64', NULL, 7, 2, 'crypto-lib'),
('x86_64', '>=4.0.0', 8, 3, 'db-driver-old'),
('arm64', NULL, 9, 1, 'imgproc-v1'),
('x86_64', NULL, 10, 2, 'audio-tools'),
('x86_64', '>=7.0.0', 11, 3, 'video-kit-old'),
('x86_64', NULL, 12, 1, 'ml-core-legacy'),
('x86_64', '>=6.0.0', 13, 2, 'analytics-pro-tools'),
('x86_64', NULL, 14, 3, 'monitor-agent-v1'),
('x86_64', '>=9.0.0', 15, 1, 'filesystem-old');

View File

@ -111,8 +111,8 @@ where
base_name: row.try_get("base_name")?,
url: row.try_get("url")?,
description: row.try_get("description")?,
submitter_id: row.try_get("submitter_id")?,
submitter_name: row.try_get("submitter_name")?,
// submitter_id: row.try_get("submitter_id")?,
// submitter_name: row.try_get("submitter_name")?,
updated_at: row.try_get("updated_at")?,
created_at: row.try_get("created_at")?,
});

View File

@ -37,8 +37,8 @@ pub struct Entry {
pub base_name: Box<str>,
pub url: Option<Box<str>>,
pub description: Box<str>,
pub submitter_id: u64,
pub submitter_name: Box<str>,
// pub submitter_id: u64,
// pub submitter_name: Box<str>,
pub updated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}

View File

@ -1,9 +1,8 @@
use crate::input::Input;
use crate::widget::centerbox;
use service::authentication;
use service::{
Authenticated, AuthenticationContract,
authentication::{Error, LoginData, Result},
authentication::{self, Error, LoginData, Result},
};
use iced::futures::lock::Mutex;

View File

@ -101,13 +101,12 @@ impl<S: AuthenticationContract + 'static> Register<S> {
Message::EmailSubmitted => return Some(self.password.focus().into()),
Message::PasswordSubmitted if self.password.critical() => (),
Message::PasswordSubmitted => return Some(self.repeat.focus().into()),
Message::RepeatSubmitted if self.repeat.error().is_some() => (),
Message::RegisterPressed | Message::RepeatSubmitted
if self.repeat.error().is_some() =>
{
return Some(self.repeat.focus().into());
}
Message::RegisterPressed | Message::RepeatSubmitted => {
if self.repeat.error().is_some() {
return Some(self.repeat.focus().into());
}
let register_data = RegisterData {
name: match self.name.submit() {
Ok(x) => x,
@ -146,7 +145,7 @@ impl<S: AuthenticationContract + 'static> Register<S> {
Message::RequestResult(r) => match &*r {
Ok(a) => {
self.state = State::Success;
return Some(Event::Authenticated(a.clone()))
return Some(Event::Authenticated(a.clone()));
}
Err(e) => {

View File

@ -1,47 +1,45 @@
// mod main_window;
// mod authentication;
mod authentication;
mod input;
mod search;
//mod statistics;
mod widget;
use std::sync::Arc;
use crate::authentication::Authentication;
// use crate::main_window::MainWindow;
use crate::search::Search;
//use crate::statistics::Statistics;
use data::{MySqlPool, MySqlUserAdapter, SqlxPool};
use data::{MySqlPool, MySqlSearchAdapter, MySqlUserAdapter, SqlxPool};
use iced::{
Element, Subscription, Task, Theme,
futures::lock::Mutex,
widget::{center, row},
window,
};
use service::{AuthenticationAdapter, AuthenticationService};
// #[macro_export]
macro_rules! log {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
println!($($arg)*)
};
}
fn main() -> iced::Result {
iced::daemon(Repository::title, Repository::update, Repository::view)
.subscription(Repository::subscription)
.scale_factor(Repository::scale_factor)
.theme(Repository::theme)
.run_with(Repository::new)
}
use service::{
Authenticated, AuthenticationAdapter, AuthenticationService, SearchAdapter, SearchService,
};
struct Repository {
scale_factor: f64,
main_id: window::Id,
screen: Screen,
authenticated: Option<Authenticated>,
search: Search<SearchService<SearchAdapter<MySqlPool, SqlxPool, MySqlSearchAdapter>>>,
authentication: Authentication<
AuthenticationService<AuthenticationAdapter<MySqlPool, SqlxPool, MySqlUserAdapter>>,
>,
}
enum Screen {
Search,
// Statistics,
Authentication,
}
#[derive(Debug)]
enum Message {
ScaleUp,
@ -49,8 +47,8 @@ enum Message {
WindowOpened(window::Id),
WindowClosed(window::Id),
Search(search::Message),
Authentecation(authentication::Message),
// MainWindow(main_window::Message),
}
impl Repository {
@ -70,10 +68,20 @@ impl Repository {
AuthenticationAdapter::new(pool.clone()),
)));
let search_service = Arc::new(Mutex::new(SearchService::new(SearchAdapter::new(
pool.clone(),
))));
(
Self {
scale_factor: 1.4,
main_id,
scale_factor: 1.4,
screen: Screen::Search,
authenticated: None,
search: Search::new(search_service),
authentication: Authentication::new(auth_service),
},
Task::batch([
@ -98,29 +106,48 @@ impl Repository {
}
}
Message::Authentecation(message) => {
if let Some(action) = self.authentication.update(message) {
match action {
if let Some(event) = self.authentication.update(message) {
match event {
authentication::Event::Task(task) => {
return task.map(Message::Authentecation);
}
authentication::Event::Authenticated(authenticated) => {
log!("authenticated via login {:#?}", authenticated);
log!("authenticated as {:#?}", authenticated);
self.authenticated = Some(authenticated);
self.screen = Screen::Search;
}
}
}
} //
// Message::MainWindow(message) => match self.main_window.update(message) {
// main_window::Action::None => (),
// main_window::Action::Task(task) => return task.map(Message::MainWindow),
// },
}
Message::Search(m) => {
if let Some(event) = self.search.update(m) {
match event {
search::Event::Task(task) => {
return task.map(Message::Search);
}
search::Event::OpenPackage(id) => {
log!("opening package {id}")
}
search::Event::OpenBase(id) => {
log!("opening package base {id}")
}
search::Event::OpenURL(url) => {
log!("opening url {url}")
}
}
}
}
}
Task::none()
}
fn view(&self, id: window::Id) -> Element<Message> {
if self.main_id == id {
// self.main_window.view().map(Message::MainWindow)
self.authentication.view().map(Message::Authentecation)
match self.screen {
Screen::Search => self.search.view().map(Message::Search),
Screen::Authentication => self.authentication.view().map(Message::Authentecation),
}
} else {
center(row!["This window is unknown.", "It may be closed."]).into()
}
@ -151,3 +178,19 @@ impl Repository {
Theme::TokyoNight
}
}
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
println!($($arg)*)
};
}
fn main() -> iced::Result {
iced::daemon(Repository::title, Repository::update, Repository::view)
.subscription(Repository::subscription)
.scale_factor(Repository::scale_factor)
.theme(Repository::theme)
.run_with(Repository::new)
}

View File

@ -0,0 +1,333 @@
use crate::input::Input;
use crate::widget::{scroll, tip, url};
use iced::Length::Shrink;
use service::search::Data;
use service::{
SearchContract,
search::{self, Entry, Result},
};
use iced::widget::{Column, button, checkbox, column, container, lazy, pick_list, row, text};
use iced::{Element, Length::Fill, Task, futures::lock::Mutex};
use std::sync::Arc;
use strum::{Display, VariantArray};
pub struct Search<S> {
input: Input<search::Search>,
mode: Mode,
order: Order,
ascending: bool,
exact: bool,
limit: u8,
state: State,
service: Arc<Mutex<S>>,
}
#[derive(Default)]
enum State {
#[default]
None,
Searching,
// Aborted,
Table(Table),
Error(String),
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, VariantArray)]
pub enum Mode {
Url,
Name,
#[strum(to_string = "Package Base")]
PackageBase,
Description,
#[strum(to_string = "Base description")]
BaseDescription,
#[default]
#[strum(to_string = "Name and Description")]
NameAndDescription,
User,
Flagger,
Packager,
Submitter,
Maintainer,
}
impl From<Mode> for search::Mode {
fn from(value: Mode) -> Self {
match value {
Mode::Url => Self::Url,
Mode::Name => Self::Name,
Mode::PackageBase => Self::PackageBase,
Mode::Description => Self::Description,
Mode::BaseDescription => Self::BaseDescription,
Mode::NameAndDescription => Self::NameAndDescription,
Mode::User => Self::User,
Mode::Flagger => Self::Flagger,
Mode::Packager => Self::Packager,
Mode::Submitter => Self::Submitter,
Mode::Maintainer => Self::Maintainer,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, VariantArray)]
pub enum Order {
Name,
Version,
#[strum(to_string = "Base Name")]
BaseName,
// Submitter,
#[default]
#[strum(to_string = "Last update")]
UpdatedAt,
#[strum(to_string = "Created time")]
CreatedAt,
}
impl From<Order> for search::Order {
fn from(value: Order) -> Self {
match value {
Order::Name => Self::Name,
Order::Version => Self::Version,
Order::BaseName => Self::BaseName,
Order::UpdatedAt => Self::UpdatedAt,
Order::CreatedAt => Self::CreatedAt,
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
// Search bar
Reset,
Search,
SearchChanged(String),
ModePicked(Mode),
OrderPicked(Order),
AscendingToggled(bool),
ExactToggled(bool),
ShowEntriesPicked(u8),
// Table
PackagePressed(u64),
BasePressed(u64),
URLPressed(Box<str>),
RequestResult(Arc<Result<Vec<Entry>>>),
}
pub enum Event {
Task(Task<Message>),
OpenPackage(u64),
OpenBase(u64),
OpenURL(Box<str>),
}
impl From<Task<Message>> for Event {
fn from(value: Task<Message>) -> Self {
Self::Task(value)
}
}
#[derive(Debug, Hash)]
struct Table(Vec<Entry>);
impl Table {
pub fn view(&self) -> Element<'static, Message> {
let mut table: Vec<_> = [
"Package", // 0
"Version", // 1
"Base", // 2
"URL", // 3
"Description", // 4
"Last Updated", // 5
"Created", // 6
]
.into_iter()
.map(|s| {
let mut v = Vec::with_capacity(self.0.len());
v.push(s.into());
v.push("".into());
v
})
.collect();
for entry in &self.0 {
table[0].push(url(&entry.name, Message::PackagePressed(entry.id)));
table[1].push(text(entry.version.to_string()).into());
table[2].push(url(&entry.base_name, Message::BasePressed(entry.base_id)));
table[3].push(
entry
.url
.as_ref()
.map_or("-".into(), |s| url(&"link", Message::URLPressed(s.clone()))),
);
table[4].push(text(entry.description.to_string()).into());
table[5].push(text(entry.updated_at.to_string()).into());
table[6].push(text(entry.created_at.to_string()).into());
// table[5].push(Element::from(column( entry .maintainers .iter() .map(|(id, s)| url(s, Message::UserPressed(*id))),)));
}
scroll(
row(table
.into_iter()
.map(|v| Column::from_vec(v).spacing(5).into()))
.spacing(20)
.padding(30),
)
}
}
impl<S: SearchContract + 'static> Search<S> {
pub fn new(service: Arc<Mutex<S>>) -> Self {
Self {
input: Input::new("search_input"),
mode: Mode::NameAndDescription,
order: Order::UpdatedAt,
ascending: false,
exact: false,
limit: 25,
state: State::default(),
service,
}
}
pub fn view(&self) -> Element<Message> {
let search_bar = container(scroll(
column![
row![
self.input
.view("Search")
.on_input(Message::SearchChanged)
.on_submit(Message::Search),
tip(
button("Go").on_press(Message::Search),
"Perform the search",
tip::Position::Bottom,
),
]
.spacing(10),
row![
tip(
button("Reset").on_press(Message::Reset),
"Reset the search bar",
tip::Position::Bottom,
),
tip(
pick_list(Mode::VARIANTS, Some(&self.mode), Message::ModePicked),
"Search mode",
tip::Position::Bottom,
),
tip(
pick_list(Order::VARIANTS, Some(&self.order), Message::OrderPicked),
"Field used to sort the results",
tip::Position::Bottom,
),
tip(
checkbox("Exact", self.exact).on_toggle(Message::ExactToggled),
"Exact search",
tip::Position::Bottom,
),
tip(
checkbox("Ascending", self.ascending).on_toggle(Message::AscendingToggled),
"Sort order of results",
tip::Position::Bottom,
),
tip(
pick_list(
[25, 50, 75, 100],
Some(self.limit),
Message::ShowEntriesPicked
),
"Number of results to show",
tip::Position::Bottom,
),
]
.spacing(10)
]
.padding(20)
.width(750)
.spacing(10),
))
.center_x(Fill);
column![
search_bar,
match &self.state {
State::None => Element::from(""),
State::Searching => "Searching...".into(),
// State::Aborted => "Aborted".into(),
State::Error(e) => text(e).into(),
State::Table(table) => container(lazy(table, |t| t.view())).center_x(Fill).into(),
}
]
.into()
}
pub fn update(&mut self, message: Message) -> Option<Event> {
match message {
Message::SearchChanged(s) => self.input.update(s),
Message::ModePicked(mode) => self.mode = mode,
Message::OrderPicked(order) => self.order = order,
Message::AscendingToggled(b) => self.ascending = b,
Message::ExactToggled(b) => self.exact = b,
Message::ShowEntriesPicked(x) => self.limit = x,
Message::Reset => {
let state = std::mem::take(&mut self.state);
*self = Self::new(self.service.clone());
self.state = state;
}
Message::PackagePressed(id) => return Some(Event::OpenPackage(id)),
Message::BasePressed(id) => return Some(Event::OpenBase(id)),
Message::URLPressed(url) => return Some(Event::OpenURL(url)),
Message::Search => {
let search_data = Data {
mode: self.mode.into(),
order: self.order.into(),
search: match self.input.submit() {
Ok(x) => x,
Err(t) => return Some(t.into()),
},
limit: self.limit.into(),
exact: self.exact,
ascending: self.ascending,
};
self.state = State::Searching;
let arc = self.service.clone();
return Some(
Task::perform(
async move {
let Some(service) = arc.try_lock() else {
return Err("other search request is being performed".into());
};
service.search(search_data).await
},
|r| Message::RequestResult(Arc::new(r)),
)
.into(),
);
}
Message::RequestResult(r) => match &*r {
Ok(v) => self.state = State::Table(Table(v.clone())),
Err(e) => self.state = State::Error(e.to_string()),
},
}
None
}
pub fn title(&self) -> String {
let errors = [self.input.error(), self.input.warning()];
let error = errors.into_iter().flatten().next();
match &self.state {
State::None => error.map_or_else(|| "Search".into(), Into::into),
State::Searching => "Searching...".into(),
State::Table(_) => "Displaying search results".into(),
State::Error(e) => e.into(),
}
}
}

View File

@ -26,7 +26,7 @@ pub fn scroll<'a, Message: 'a>(content: impl Into<Element<'a, Message>>) -> Elem
/// Clickable url
pub fn url<'a, Message: Clone + 'a>(txt: &impl ToString, msg: Message) -> Element<'a, Message> {
Element::from(mouse_area(text(txt.to_string()).color(color!(0xBB_B6_DF))).on_press(msg))
Element::from(mouse_area(text(txt.to_string()).color(color!(0x00_BB_FF))).on_press(msg))
}
pub mod tip {