working alpha version up online!

Orual 0f1c03d1 c353b2e0

+305 -130
+1
Cargo.lock
··· 8692 8692 "sqlite-wasm-rs", 8693 8693 "time", 8694 8694 "tokio", 8695 + "tracing", 8695 8696 "wasm-bindgen", 8696 8697 "wasm-bindgen-futures", 8697 8698 "weaver-api",
+1
crates/weaver-app/Cargo.toml
··· 47 47 dioxus-logger = "0.7.1" 48 48 serde_html_form = "0.2.8" 49 49 webbrowser = "1.0.6" 50 + tracing.workspace = true 50 51 51 52 52 53
+33
crates/weaver-app/assets/styling/main.css
··· 7 7 #header { 8 8 max-width: 1200px; 9 9 } 10 + 11 + .record-view-container { 12 + max-width: 1200px; 13 + margin: 2rem auto; 14 + padding: 0 1rem; 15 + } 16 + 17 + .uri-input-section { 18 + margin-bottom: 2.5rem; 19 + } 20 + 21 + .uri-input { 22 + font-family: var(--font-mono); 23 + font-size: 0.9rem; 24 + width: 100%; 25 + max-width: 100%; 26 + box-sizing: border-box; 27 + padding: 0.5rem 0.75rem; 28 + background: var(--color-surface, rgba(0, 0, 0, 0.2)); 29 + border: 1px solid var(--color-border); 30 + color: var(--color-text); 31 + outline: none; 32 + transition: border-color 0.2s; 33 + } 34 + 35 + .uri-input:focus { 36 + border-color: var(--color-primary); 37 + } 38 + 39 + .uri-input::placeholder { 40 + color: var(--color-subtle); 41 + opacity: 0.5; 42 + }
+2 -2
crates/weaver-app/assets/styling/navbar.css
··· 2 2 display: flex; 3 3 flex-direction: row; 4 4 justify-content: space-between; 5 - padding-left: 4rem; 5 + padding-left: 1rem; 6 6 padding-top: 1rem; 7 - padding-right: 4rem; 7 + padding-right: 1rem; 8 8 } 9 9 10 10 .breadcrumbs {
+64
crates/weaver-app/assets/styling/record-view.css
··· 21 21 letter-spacing: 0.15em; 22 22 } 23 23 24 + .uri-input-section { 25 + margin-bottom: 1rem; 26 + } 27 + 28 + .uri-input { 29 + font-family: var(--font-mono); 30 + font-size: 0.9rem; 31 + width: 100%; 32 + max-width: 100%; 33 + box-sizing: border-box; 34 + padding: 0.5rem 0.75rem; 35 + background: var(--color-surface, rgba(0, 0, 0, 0.2)); 36 + border: 1px solid var(--color-border); 37 + color: var(--color-text); 38 + outline: none; 39 + transition: border-color 0.2s; 40 + } 41 + 42 + .uri-input:focus { 43 + border-color: var(--color-primary); 44 + } 45 + 46 + .uri-input::placeholder { 47 + color: var(--color-subtle); 48 + opacity: 0.5; 49 + } 50 + 24 51 .record-metadata { 25 52 display: flex-wrap; 26 53 flex-direction: column; ··· 807 834 font-style: italic; 808 835 padding-top: 0.25rem; 809 836 } 837 + 838 + /* Dialog Actions for Delete Confirmation */ 839 + .dialog-actions { 840 + display: flex; 841 + flex-direction: row; 842 + justify-content: flex-end; 843 + gap: 0; 844 + margin-top: 8px; 845 + } 846 + 847 + .dialog-actions button { 848 + font-family: var(--font-mono); 849 + font-size: 0.75rem; 850 + text-transform: uppercase; 851 + letter-spacing: 0.05em; 852 + padding: 0.5rem 1rem; 853 + background: transparent; 854 + border: none; 855 + border-bottom: 2px solid transparent; 856 + color: var(--color-subtle); 857 + cursor: pointer; 858 + transition: all 0.2s; 859 + } 860 + 861 + .dialog-actions button:hover { 862 + color: var(--color-primary); 863 + border-bottom-color: var(--color-primary); 864 + } 865 + 866 + .dialog-actions button:first-child { 867 + color: var(--color-error, #ff6b6b); 868 + } 869 + 870 + .dialog-actions button:first-child:hover { 871 + color: var(--color-error, #ff5252); 872 + border-bottom-color: var(--color-error, #ff6b6b); 873 + }
+9 -5
crates/weaver-app/src/components/login.rs
··· 44 44 let handle = handle.clone(); 45 45 let fetcher = fetcher.clone(); 46 46 spawn(async move { 47 - if let Err(e) = start_oauth_flow(handle, fetcher).await { 48 - error!("Authentication failed: {}", e); 49 - error.set(Some(format!("Authentication failed: {}", e))); 50 - is_loading.set(false); 47 + match start_oauth_flow(handle, fetcher).await { 48 + Ok(_) => { 49 + open.set(false); 50 + } 51 + Err(e) => { 52 + error!("Authentication failed: {}", e); 53 + error.set(Some(format!("Authentication failed: {}", e))); 54 + is_loading.set(false); 55 + } 51 56 } 52 - open.set(false); 53 57 }); 54 58 }); 55 59 };
+13 -4
crates/weaver-app/src/main.rs
··· 9 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 10 10 use dioxus::fullstack::response::Extension; 11 11 use dioxus_logger::tracing::Level; 12 - use jacquard::oauth::{client::OAuthClient, session::ClientData}; 12 + use jacquard::{ 13 + oauth::{client::OAuthClient, session::ClientData}, 14 + types::aturi::AtUri, 15 + }; 13 16 #[allow(unused)] 14 17 use jacquard::{ 15 18 smol_str::SmolStr, ··· 19 22 use std::sync::Arc; 20 23 use std::sync::{LazyLock, Mutex}; 21 24 #[allow(unused)] 22 - use views::{Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordView}; 25 + use views::{ 26 + Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, 27 + }; 23 28 24 29 use crate::{ 25 30 auth::{AuthState, AuthStore}, ··· 56 61 #[route("/")] 57 62 Home {}, 58 63 #[layout(ErrorLayout)] 59 - #[route("/record#:uri")] 60 - RecordView { uri: SmolStr }, 64 + #[nest("/record")] 65 + #[layout(RecordIndex)] 66 + #[route("/:..uri")] 67 + RecordView { uri: Vec<String> }, 68 + #[end_layout] 69 + #[end_nest] 61 70 #[route("/callback?:state&:iss&:code")] 62 71 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, 63 72 #[nest("/:ident")]
+54 -21
crates/weaver-app/src/views/home.rs
··· 1 - use crate::{components::identity::NotebookCard, fetch}; 1 + use crate::{Route, components::identity::NotebookCard, fetch}; 2 2 use dioxus::prelude::*; 3 + use jacquard::{IntoStatic, smol_str::ToSmolStr, types::aturi::AtUri}; 3 4 4 5 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 5 6 ··· 13 14 let fetcher = fetcher.clone(); 14 15 async move { fetcher.fetch_notebooks_from_ufos().await } 15 16 }); 17 + let navigator = use_navigator(); 18 + let mut uri_input = use_signal(|| String::new()); 19 + 20 + let handle_uri_submit = move || { 21 + let input_uri = uri_input.read().clone(); 22 + if !input_uri.is_empty() { 23 + if let Ok(parsed) = AtUri::new(&input_uri) { 24 + navigator.push(Route::RecordView { 25 + uri: vec![parsed.to_string()], 26 + }); 27 + } 28 + } 29 + }; 16 30 17 31 rsx! { 18 32 document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } 19 - 20 - div { class: "notebooks-list", 21 - match notebooks() { 22 - Some(Ok(notebook_list)) => rsx! { 23 - for notebook in notebook_list.iter() { 24 - { 25 - let view = &notebook.0; 26 - let entries = &notebook.1; 27 - rsx! { 28 - div { 29 - key: "{view.cid}", 30 - NotebookCard { 31 - notebook: view.clone(), 32 - entry_refs: entries.clone() 33 + div { 34 + class: "record-view-container", 35 + div { class: "record-header", 36 + div { class: "uri-input-section", 37 + input { 38 + r#type: "text", 39 + class: "uri-input", 40 + placeholder: "at://did:plc:.../collection/rkey", 41 + value: "{uri_input}", 42 + oninput: move |evt| uri_input.set(evt.value()), 43 + onkeydown: move |evt| { 44 + if evt.key() == Key::Enter { 45 + handle_uri_submit(); 46 + } 47 + }, 48 + } 49 + } 50 + } 51 + div { class: "notebooks-list", 52 + match notebooks() { 53 + Some(Ok(notebook_list)) => rsx! { 54 + for notebook in notebook_list.iter() { 55 + { 56 + let view = &notebook.0; 57 + let entries = &notebook.1; 58 + rsx! { 59 + div { 60 + key: "{view.cid}", 61 + NotebookCard { 62 + notebook: view.clone(), 63 + entry_refs: entries.clone() 64 + } 33 65 } 34 66 } 35 67 } 36 68 } 69 + }, 70 + Some(Err(_)) => rsx! { 71 + div { "Error loading notebooks" } 72 + }, 73 + None => rsx! { 74 + div { "Loading notebooks..." } 37 75 } 38 - }, 39 - Some(Err(_)) => rsx! { 40 - div { "Error loading notebooks" } 41 - }, 42 - None => rsx! { 43 - div { "Loading notebooks..." } 44 76 } 45 77 } 46 78 } 79 + 47 80 } 48 81 }
+1 -1
crates/weaver-app/src/views/mod.rs
··· 21 21 pub use notebook::{Notebook, NotebookIndex}; 22 22 23 23 mod record; 24 - pub use record::RecordView; 24 + pub use record::{RecordIndex, RecordView}; 25 25 26 26 mod callback; 27 27 pub use callback::Callback;
+127 -97
crates/weaver-app/src/views/record.rs
··· 2 2 use crate::auth::AuthState; 3 3 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 4 4 use crate::fetch::CachedFetcher; 5 - use dioxus::prelude::*; 5 + use dioxus::{CapturedError, prelude::*}; 6 6 use humansize::format_size; 7 7 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 8 8 use jacquard::client::AgentError; ··· 12 12 client::AgentSessionExt, 13 13 common::{Data, IntoStatic}, 14 14 identity::lexicon_resolver::LexiconSchemaResolver, 15 - smol_str::SmolStr, 16 15 types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid}, 17 16 }; 18 17 use mime_sniffer::MimeTypeSniffer; ··· 28 27 } 29 28 30 29 #[component] 31 - pub fn RecordView(uri: ReadSignal<SmolStr>) -> Element { 32 - let fetcher = use_context::<CachedFetcher>(); 33 - let at_uri = AtUri::new_owned(uri()); 34 - if let Err(err) = &at_uri { 35 - let error = format!("{:?}", err); 36 - return rsx! { 37 - div { 30 + pub fn RecordIndex() -> Element { 31 + let navigator = use_navigator(); 32 + let mut uri_input = use_signal(|| String::new()); 33 + 34 + let handle_uri_submit = move || { 35 + let input_uri = uri_input.read().clone(); 36 + if !input_uri.is_empty() { 37 + if let Ok(parsed) = AtUri::new(&input_uri) { 38 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, parsed); 39 + navigator.push(link); 40 + } 41 + } 42 + }; 43 + rsx! { 44 + document::Stylesheet { href: asset!("/assets/styling/record-view.css") } 45 + div { 46 + class: "record-view-container", 47 + div { class: "record-header", 38 48 h1 { "Record View" } 39 - p { "URI: {uri}" } 40 - p { "Error: {error}" } 49 + div { class: "uri-input-section", 50 + input { 51 + r#type: "text", 52 + class: "uri-input", 53 + placeholder: "at://did:plc:.../collection/rkey", 54 + value: "{uri_input}", 55 + oninput: move |evt| uri_input.set(evt.value()), 56 + onkeydown: move |evt| { 57 + if evt.key() == Key::Enter { 58 + handle_uri_submit(); 59 + } 60 + }, 61 + } 62 + } 41 63 } 42 - }; 64 + 65 + Outlet::<Route> {} 66 + } 67 + } 68 + } 69 + 70 + #[component] 71 + pub fn RecordView(uri: ReadSignal<Vec<String>>) -> Element { 72 + let fetcher = use_context::<CachedFetcher>(); 73 + info!("Uri:{:?}", uri().join("/")); 74 + let at_uri = AtUri::new_owned(&*uri.read().join("/")); 75 + if at_uri.is_err() { 76 + return rsx! {}; 43 77 } 44 - let uri = use_signal(|| at_uri.unwrap()); 78 + let uri = use_signal(move || AtUri::new_owned(&*uri.read().join("/")).unwrap()); 45 79 let mut view_mode = use_signal(|| ViewMode::Pretty); 46 80 let mut edit_mode = use_signal(|| false); 47 - let navigator = use_navigator(); 48 81 49 82 let client = fetcher.get_client(); 50 83 let record_resource = use_resource(move || { 51 84 let client = client.clone(); 52 - async move { client.fetch_record_slingshot(&uri()).await } 85 + async move { client.fetch_record_slingshot(&*uri.read()).await } 53 86 }); 54 87 55 88 // Check ownership for edit access ··· 61 94 } 62 95 63 96 // authority() returns &AtIdentifier which can be Did or Handle 64 - match uri().authority() { 97 + match &*uri.read().authority() { 65 98 AtIdentifier::Did(record_did) => auth.did.as_ref() == Some(record_did), 66 99 AtIdentifier::Handle(_) => { 67 100 // Can't easily check ownership for handles without async resolution ··· 69 102 } 70 103 } 71 104 }); 72 - if let Some(Ok(record)) = &*record_resource.read_unchecked() { 73 - let mut record_value = use_signal(|| record.value.clone().into_static()); 74 - let json = 75 - use_memo(move || serde_json::to_string_pretty(&record_value()).unwrap_or_default()); 105 + if let Some(Ok(record)) = &*record_resource.read() { 106 + let record_value = record.value.clone().into_static(); 107 + let record = record.clone(); 76 108 77 109 rsx! { 78 - RecordViewLayout { 79 - uri: uri().clone(), 80 - cid: record.cid.clone(), 81 - if edit_mode() { 82 - EditableRecordContent { 83 - record_value: record_value, 84 - uri: uri, 85 - view_mode: view_mode, 86 - edit_mode: edit_mode, 87 - record_resource: record_resource, 88 - } 89 - } else { 90 - div { 91 - class: "tab-bar", 92 - button { 93 - class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, 94 - onclick: move |_| view_mode.set(ViewMode::Pretty), 95 - "View" 110 + Fragment { key: "{uri()}", 111 + RecordViewLayout { 112 + uri: uri().clone(), 113 + cid: record.cid.clone(), 114 + if edit_mode() { 115 + 116 + EditableRecordContent { 117 + record_value: record_value, 118 + uri: uri, 119 + view_mode: view_mode, 120 + edit_mode: edit_mode, 121 + record_resource: record_resource, 96 122 } 97 - button { 98 - class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, 99 - onclick: move |_| view_mode.set(ViewMode::Json), 100 - "JSON" 101 - } 102 - if is_owner() { 123 + } else { 124 + div { 125 + class: "tab-bar", 126 + button { 127 + class: if view_mode() == ViewMode::Pretty { "tab-button active" } else { "tab-button" }, 128 + onclick: move |_| view_mode.set(ViewMode::Pretty), 129 + "View" 130 + } 103 131 button { 104 - class: "tab-button edit-button", 105 - onclick: move |_| edit_mode.set(true), 106 - "Edit" 132 + class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, 133 + onclick: move |_| view_mode.set(ViewMode::Json), 134 + "JSON" 135 + } 136 + if is_owner() { 137 + button { 138 + class: "tab-button edit-button", 139 + onclick: move |_| edit_mode.set(true), 140 + "Edit" 141 + } 107 142 } 108 143 } 109 - } 110 - div { 111 - class: "tab-content", 112 - match view_mode() { 113 - ViewMode::Pretty => rsx! { 114 - PrettyRecordView { record: record_value(), uri: uri().clone() } 115 - }, 116 - ViewMode::Json => rsx! { 117 - CodeView { 118 - code: use_signal(|| json()), 119 - lang: Some("json".to_string()), 120 - } 121 - }, 144 + div { 145 + class: "tab-content", 146 + match view_mode() { 147 + ViewMode::Pretty => rsx! { 148 + PrettyRecordView { record: record_value, uri: uri().clone() } 149 + }, 150 + ViewMode::Json => { 151 + let json = use_memo(use_reactive!(|record| serde_json::to_string_pretty( 152 + &record.value 153 + ) 154 + .unwrap_or_default())); 155 + rsx! { 156 + CodeView { 157 + code: json, 158 + lang: Some("json".to_string()), 159 + } 160 + } 161 + }, 162 + } 122 163 } 123 164 } 124 165 } 125 166 } 126 167 } 127 168 } else { 128 - rsx! { 129 - div { 130 - class: "record-view-container", 131 - h1 { "Record" } 132 - p { "URI: {uri}" } 133 - p { "Loading..." } 134 - } 135 - } 169 + rsx! {} 136 170 } 137 171 } 138 172 ··· 390 424 #[component] 391 425 fn HighlightedUri(uri: AtUri<'static>) -> Element { 392 426 let s = uri.as_str(); 393 - let link = format!("/record#{}", s); 427 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, s); 394 428 395 429 if let Some(rest) = s.strip_prefix("at://") { 396 430 let parts: Vec<&str> = rest.splitn(3, '/').collect(); 397 431 return rsx! { 398 432 a { 399 - href: "{link}", 433 + href: link, 400 434 class: "uri-link", 401 435 span { class: "string-at-uri", 402 436 span { class: "aturi-scheme", "at://" } ··· 419 453 }; 420 454 } 421 455 422 - rsx! { span { class: "string-at-uri", "{s}" } } 456 + rsx! { a { class: "string-at-uri", href: s } } 423 457 } 424 458 425 459 #[component] ··· 2070 2104 #[component] 2071 2105 fn RecordViewLayout(uri: AtUri<'static>, cid: Option<Cid<'static>>, children: Element) -> Element { 2072 2106 rsx! { 2073 - document::Stylesheet { href: asset!("/assets/styling/record-view.css") } 2074 2107 div { 2075 - class: "record-view-container", 2076 - div { 2077 - class: "record-header", 2078 - h1 { "Record" } 2079 - div { 2080 - class: "record-metadata", 2081 - div { class: "metadata-row", 2082 - span { class: "metadata-label", "URI" } 2083 - span { class: "metadata-value", 2084 - HighlightedUri { uri: uri.clone() } 2085 - } 2086 - } 2087 - if let Some(cid) = cid { 2088 - div { class: "metadata-row", 2089 - span { class: "metadata-label", "CID" } 2090 - code { class: "metadata-value", "{cid}" } 2091 - } 2092 - } 2108 + class: "record-metadata", 2109 + div { class: "metadata-row", 2110 + span { class: "metadata-label", "URI" } 2111 + span { class: "metadata-value", 2112 + HighlightedUri { uri: uri.clone() } 2093 2113 } 2094 2114 } 2095 - {children} 2115 + if let Some(cid) = cid { 2116 + div { class: "metadata-row", 2117 + span { class: "metadata-label", "CID" } 2118 + code { class: "metadata-value", "{cid}" } 2119 + } 2120 + } 2096 2121 } 2122 + 2123 + {children} 2124 + 2097 2125 } 2098 2126 } 2099 2127 2100 2128 /// Render some text as markdown. 2101 2129 #[component] 2102 2130 fn EditableRecordContent( 2103 - record_value: Signal<Data<'static>>, 2131 + record_value: Data<'static>, 2104 2132 uri: ReadSignal<AtUri<'static>>, 2105 2133 view_mode: Signal<ViewMode>, 2106 2134 edit_mode: Signal<bool>, 2107 2135 record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>, 2108 2136 ) -> Element { 2109 - let mut edit_data = use_signal(|| record_value()); 2137 + let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); 2110 2138 let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); 2111 2139 let navigator = use_navigator(); 2112 2140 let fetcher = use_context::<CachedFetcher>(); ··· 2150 2178 Ok(output) => { 2151 2179 if output.status() == StatusCode::OK { 2152 2180 dioxus_logger::tracing::info!("Record updated successfully"); 2153 - record_value.set(data); 2181 + edit_data.set(data.clone()); 2154 2182 edit_mode.set(false); 2155 2183 } else { 2156 2184 dioxus_logger::tracing::error!("Unexpected status code: {:?}", output.status()); ··· 2184 2212 Ok(response) => { 2185 2213 if let Ok(output) = response.into_output() { 2186 2214 dioxus_logger::tracing::info!("Record created: {}", output.uri); 2187 - nav.push(Route::RecordView { uri: output.uri.to_smolstr() }); 2215 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 2216 + nav.push(link); 2188 2217 } 2189 2218 } 2190 2219 Err(e) => { ··· 2233 2262 } 2234 2263 2235 2264 dioxus_logger::tracing::info!("Record replaced: {}", create_output.uri); 2236 - nav.push(Route::RecordView { uri: create_output.uri.to_smolstr() }); 2265 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 2266 + nav.push(link); 2237 2267 } 2238 2268 } 2239 2269 Err(e) => { ··· 2275 2305 }); 2276 2306 }, 2277 2307 on_cancel: move |_| { 2278 - edit_data.set(record_value()); 2308 + edit_data.set(record_value.clone()); 2279 2309 edit_mode.set(false); 2280 2310 }, 2281 2311 }