Search functionality
This commit is contained in:
39
3/coursework/src/Cargo.lock
generated
39
3/coursework/src/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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"
|
||||
|
103
3/coursework/src/assets/init/1-data.sql
Normal file
103
3/coursework/src/assets/init/1-data.sql
Normal 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');
|
@ -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")?,
|
||||
});
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
}
|
||||
|
333
3/coursework/src/src/search.rs
Normal file
333
3/coursework/src/src/search.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user