this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add search primitives: state field, message variant, filter functions

+244 -4
+53 -2
Cargo.lock
··· 2847 2847 source = "registry+https://github.com/rust-lang/crates.io-index" 2848 2848 checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" 2849 2849 dependencies = [ 2850 - "toml_edit", 2850 + "toml_edit 0.23.7", 2851 2851 ] 2852 2852 2853 2853 [[package]] ··· 2900 2900 "reqwest", 2901 2901 "serde", 2902 2902 "serde_json", 2903 + "toml", 2903 2904 "uuid", 2904 2905 ] 2905 2906 ··· 3324 3325 ] 3325 3326 3326 3327 [[package]] 3328 + name = "serde_spanned" 3329 + version = "0.6.9" 3330 + source = "registry+https://github.com/rust-lang/crates.io-index" 3331 + checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 3332 + dependencies = [ 3333 + "serde", 3334 + ] 3335 + 3336 + [[package]] 3327 3337 name = "serde_urlencoded" 3328 3338 version = "0.7.1" 3329 3339 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3773 3783 ] 3774 3784 3775 3785 [[package]] 3786 + name = "toml" 3787 + version = "0.8.23" 3788 + source = "registry+https://github.com/rust-lang/crates.io-index" 3789 + checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 3790 + dependencies = [ 3791 + "serde", 3792 + "serde_spanned", 3793 + "toml_datetime 0.6.11", 3794 + "toml_edit 0.22.27", 3795 + ] 3796 + 3797 + [[package]] 3798 + name = "toml_datetime" 3799 + version = "0.6.11" 3800 + source = "registry+https://github.com/rust-lang/crates.io-index" 3801 + checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 3802 + dependencies = [ 3803 + "serde", 3804 + ] 3805 + 3806 + [[package]] 3776 3807 name = "toml_datetime" 3777 3808 version = "0.7.3" 3778 3809 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3783 3814 3784 3815 [[package]] 3785 3816 name = "toml_edit" 3817 + version = "0.22.27" 3818 + source = "registry+https://github.com/rust-lang/crates.io-index" 3819 + checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 3820 + dependencies = [ 3821 + "indexmap", 3822 + "serde", 3823 + "serde_spanned", 3824 + "toml_datetime 0.6.11", 3825 + "toml_write", 3826 + "winnow", 3827 + ] 3828 + 3829 + [[package]] 3830 + name = "toml_edit" 3786 3831 version = "0.23.7" 3787 3832 source = "registry+https://github.com/rust-lang/crates.io-index" 3788 3833 checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" 3789 3834 dependencies = [ 3790 3835 "indexmap", 3791 - "toml_datetime", 3836 + "toml_datetime 0.7.3", 3792 3837 "toml_parser", 3793 3838 "winnow", 3794 3839 ] ··· 3801 3846 dependencies = [ 3802 3847 "winnow", 3803 3848 ] 3849 + 3850 + [[package]] 3851 + name = "toml_write" 3852 + version = "0.1.2" 3853 + source = "registry+https://github.com/rust-lang/crates.io-index" 3854 + checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 3804 3855 3805 3856 [[package]] 3806 3857 name = "tower"
+1
Cargo.toml
··· 40 40 iced = "0.13" 41 41 serde = { version = "1.0", features = ["derive"] } 42 42 serde_json = "1.0" 43 + toml = "0.8" 43 44 reqwest = { version = "0.12", features = ["json", "blocking"] } 44 45 uuid = { version = "1.0", features = ["v4", "v5"] } 45 46 chrono = "0.4"
+59 -2
src/main.rs
··· 1 1 mod palette; 2 2 mod panel; 3 3 mod track; 4 + mod view; 4 5 5 - use iced::widget::{column, container, horizontal_rule, pane_grid, scrollable, text}; 6 + use iced::widget::{button, column, container, horizontal_rule, pane_grid, row, scrollable, text}; 6 7 use iced::{Length, Theme}; 7 8 use palette::Palette; 8 9 use panel::{PaneContent, PaneMessage, ViewType}; ··· 27 28 selected_channel: Option<usize>, 28 29 palette: Palette, 29 30 search_query: String, 31 + views: Vec<view::View>, 32 + active_view_idx: usize, 30 33 } 31 34 32 35 impl Default for State { ··· 63 66 .and_then(|json| serde_json::from_str(&json).ok()) 64 67 .unwrap_or_default(); 65 68 69 + // Load views from config, fallback to builtins 70 + let mut views = view::load_views().unwrap_or_default(); 71 + if views.is_empty() { 72 + views = view::View::builtin_views(); 73 + } 74 + 66 75 Self { 67 76 panes, 68 77 channels, ··· 70 79 selected_channel: None, 71 80 palette: Palette::default(), 72 81 search_query: String::new(), 82 + views, 83 + active_view_idx: 0, 73 84 } 74 85 } 75 86 } ··· 79 90 ChannelSelected(usize), 80 91 Pane(PaneMessage), 81 92 SearchChanged(String), 93 + ViewSelected(usize), 82 94 } 83 95 84 96 // Search/filter functions ··· 121 133 Message::SearchChanged(query) => { 122 134 state.search_query = query; 123 135 } 136 + Message::ViewSelected(idx) => { 137 + if idx < state.views.len() { 138 + state.active_view_idx = idx; 139 + } 140 + } 124 141 Message::Pane(pane_msg) => match pane_msg { 125 142 PaneMessage::Resized(resize_event) => { 126 143 state.panes.resize(resize_event.split, resize_event.ratio); ··· 145 162 } 146 163 147 164 fn view(state: &State) -> iced::Element<Message> { 165 + let palette = &state.palette; 166 + 167 + // View tabs 168 + let tabs = row( 169 + state 170 + .views 171 + .iter() 172 + .enumerate() 173 + .map(|(idx, view)| { 174 + let is_active = idx == state.active_view_idx; 175 + button(text(&view.name).size(14)) 176 + .on_press(Message::ViewSelected(idx)) 177 + .padding(12) 178 + .style(move |theme: &Theme, status| { 179 + let mut style = button::Style::default(); 180 + style.background = if is_active { 181 + Some(iced::Background::Color(palette.bg2)) 182 + } else { 183 + Some(iced::Background::Color(palette.bg1)) 184 + }; 185 + style.text_color = if is_active { 186 + palette.red 187 + } else { 188 + theme.palette().text 189 + }; 190 + style 191 + }) 192 + .into() 193 + }) 194 + .collect::<Vec<_>>(), 195 + ) 196 + .spacing(2); 197 + 198 + let tab_bar = container(tabs) 199 + .padding(4) 200 + .style(move |_theme: &Theme| container::Style { 201 + background: Some(iced::Background::Color(palette.gray1)), 202 + ..Default::default() 203 + }); 204 + 148 205 let pane_grid = pane_grid::PaneGrid::new(&state.panes, |_pane, content, _is_maximized| { 149 206 pane_grid::Content::new(render_pane_content(&content.view_type, state)) 150 207 }) ··· 154 211 .width(Length::Fill) 155 212 .height(Length::Fill); 156 213 157 - container(pane_grid) 214 + column![tab_bar, pane_grid] 158 215 .width(Length::Fill) 159 216 .height(Length::Fill) 160 217 .into()
+131
src/view.rs
··· 1 + /// View system for user-configurable data presentation 2 + /// 3 + /// Views are persistent, user-defined configurations that determine 4 + /// what data to show and how to sort it. 5 + /// 6 + /// Stored in ~/.config/radio4000-desktop/views/ 7 + 8 + use serde::{Deserialize, Serialize}; 9 + use std::path::{Path, PathBuf}; 10 + 11 + #[derive(Debug, Clone, Serialize, Deserialize)] 12 + pub struct View { 13 + pub id: String, 14 + pub name: String, 15 + pub source: DataSource, 16 + pub sort: Sort, 17 + } 18 + 19 + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 20 + pub enum DataSource { 21 + Channels, 22 + Tracks, 23 + } 24 + 25 + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 26 + pub struct Sort { 27 + pub field: SortField, 28 + pub direction: SortDirection, 29 + } 30 + 31 + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 32 + pub enum SortField { 33 + Name, 34 + CreatedAt, 35 + TrackCount, 36 + } 37 + 38 + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 39 + pub enum SortDirection { 40 + Asc, 41 + Desc, 42 + } 43 + 44 + impl View { 45 + pub fn builtin_all_channels() -> Self { 46 + Self { 47 + id: "all-channels".into(), 48 + name: "All Channels".into(), 49 + source: DataSource::Channels, 50 + sort: Sort { 51 + field: SortField::Name, 52 + direction: SortDirection::Asc, 53 + }, 54 + } 55 + } 56 + 57 + pub fn builtin_all_tracks() -> Self { 58 + Self { 59 + id: "all-tracks".into(), 60 + name: "All Tracks".into(), 61 + source: DataSource::Tracks, 62 + sort: Sort { 63 + field: SortField::CreatedAt, 64 + direction: SortDirection::Desc, 65 + }, 66 + } 67 + } 68 + 69 + pub fn builtin_views() -> Vec<Self> { 70 + vec![Self::builtin_all_channels(), Self::builtin_all_tracks()] 71 + } 72 + } 73 + 74 + /// Get config directory path 75 + pub fn config_dir() -> PathBuf { 76 + let home = std::env::var("HOME").expect("HOME not set"); 77 + Path::new(&home) 78 + .join(".config") 79 + .join("r4") 80 + } 81 + 82 + /// Get views directory path 83 + pub fn views_dir() -> PathBuf { 84 + config_dir().join("views") 85 + } 86 + 87 + /// Load all views from disk 88 + pub fn load_views() -> Result<Vec<View>, std::io::Error> { 89 + let views_path = views_dir(); 90 + 91 + // Create directory if it doesn't exist 92 + std::fs::create_dir_all(&views_path)?; 93 + 94 + let mut views = Vec::new(); 95 + 96 + // Read all .toml files in views directory 97 + for entry in std::fs::read_dir(&views_path)? { 98 + let entry = entry?; 99 + let path = entry.path(); 100 + 101 + if path.extension().and_then(|s| s.to_str()) == Some("toml") { 102 + if let Ok(contents) = std::fs::read_to_string(&path) { 103 + if let Ok(view) = toml::from_str::<View>(&contents) { 104 + views.push(view); 105 + } 106 + } 107 + } 108 + } 109 + 110 + Ok(views) 111 + } 112 + 113 + /// Save a view to disk 114 + pub fn save_view(view: &View) -> Result<(), std::io::Error> { 115 + let views_path = views_dir(); 116 + std::fs::create_dir_all(&views_path)?; 117 + 118 + let file_path = views_path.join(format!("{}.toml", view.id)); 119 + let contents = toml::to_string_pretty(view).map_err(|e| { 120 + std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()) 121 + })?; 122 + 123 + std::fs::write(&file_path, contents)?; 124 + Ok(()) 125 + } 126 + 127 + /// Delete a view from disk 128 + pub fn delete_view(id: &str) -> Result<(), std::io::Error> { 129 + let file_path = views_dir().join(format!("{}.toml", id)); 130 + std::fs::remove_file(file_path) 131 + }