working alpha version up online!

Orual 0f1c03d1 c353b2e0

+305 -130
+1
Cargo.lock
··· 8692 "sqlite-wasm-rs", 8693 "time", 8694 "tokio", 8695 "wasm-bindgen", 8696 "wasm-bindgen-futures", 8697 "weaver-api",
··· 8692 "sqlite-wasm-rs", 8693 "time", 8694 "tokio", 8695 + "tracing", 8696 "wasm-bindgen", 8697 "wasm-bindgen-futures", 8698 "weaver-api",
+1
crates/weaver-app/Cargo.toml
··· 47 dioxus-logger = "0.7.1" 48 serde_html_form = "0.2.8" 49 webbrowser = "1.0.6" 50 51 52
··· 47 dioxus-logger = "0.7.1" 48 serde_html_form = "0.2.8" 49 webbrowser = "1.0.6" 50 + tracing.workspace = true 51 52 53
+33
crates/weaver-app/assets/styling/main.css
··· 7 #header { 8 max-width: 1200px; 9 }
··· 7 #header { 8 max-width: 1200px; 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 display: flex; 3 flex-direction: row; 4 justify-content: space-between; 5 - padding-left: 4rem; 6 padding-top: 1rem; 7 - padding-right: 4rem; 8 } 9 10 .breadcrumbs {
··· 2 display: flex; 3 flex-direction: row; 4 justify-content: space-between; 5 + padding-left: 1rem; 6 padding-top: 1rem; 7 + padding-right: 1rem; 8 } 9 10 .breadcrumbs {
+64
crates/weaver-app/assets/styling/record-view.css
··· 21 letter-spacing: 0.15em; 22 } 23 24 .record-metadata { 25 display: flex-wrap; 26 flex-direction: column; ··· 807 font-style: italic; 808 padding-top: 0.25rem; 809 }
··· 21 letter-spacing: 0.15em; 22 } 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 + 51 .record-metadata { 52 display: flex-wrap; 53 flex-direction: column; ··· 834 font-style: italic; 835 padding-top: 0.25rem; 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 let handle = handle.clone(); 45 let fetcher = fetcher.clone(); 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); 51 } 52 - open.set(false); 53 }); 54 }); 55 };
··· 44 let handle = handle.clone(); 45 let fetcher = fetcher.clone(); 46 spawn(async move { 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 + } 56 } 57 }); 58 }); 59 };
+13 -4
crates/weaver-app/src/main.rs
··· 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 10 use dioxus::fullstack::response::Extension; 11 use dioxus_logger::tracing::Level; 12 - use jacquard::oauth::{client::OAuthClient, session::ClientData}; 13 #[allow(unused)] 14 use jacquard::{ 15 smol_str::SmolStr, ··· 19 use std::sync::Arc; 20 use std::sync::{LazyLock, Mutex}; 21 #[allow(unused)] 22 - use views::{Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordView}; 23 24 use crate::{ 25 auth::{AuthState, AuthStore}, ··· 56 #[route("/")] 57 Home {}, 58 #[layout(ErrorLayout)] 59 - #[route("/record#:uri")] 60 - RecordView { uri: SmolStr }, 61 #[route("/callback?:state&:iss&:code")] 62 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, 63 #[nest("/:ident")]
··· 9 #[cfg(all(feature = "fullstack-server", feature = "server"))] 10 use dioxus::fullstack::response::Extension; 11 use dioxus_logger::tracing::Level; 12 + use jacquard::{ 13 + oauth::{client::OAuthClient, session::ClientData}, 14 + types::aturi::AtUri, 15 + }; 16 #[allow(unused)] 17 use jacquard::{ 18 smol_str::SmolStr, ··· 22 use std::sync::Arc; 23 use std::sync::{LazyLock, Mutex}; 24 #[allow(unused)] 25 + use views::{ 26 + Callback, Home, Navbar, Notebook, NotebookIndex, NotebookPage, RecordIndex, RecordView, 27 + }; 28 29 use crate::{ 30 auth::{AuthState, AuthStore}, ··· 61 #[route("/")] 62 Home {}, 63 #[layout(ErrorLayout)] 64 + #[nest("/record")] 65 + #[layout(RecordIndex)] 66 + #[route("/:..uri")] 67 + RecordView { uri: Vec<String> }, 68 + #[end_layout] 69 + #[end_nest] 70 #[route("/callback?:state&:iss&:code")] 71 Callback { state: SmolStr, iss: SmolStr, code: SmolStr }, 72 #[nest("/:ident")]
+54 -21
crates/weaver-app/src/views/home.rs
··· 1 - use crate::{components::identity::NotebookCard, fetch}; 2 use dioxus::prelude::*; 3 4 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 5 ··· 13 let fetcher = fetcher.clone(); 14 async move { fetcher.fetch_notebooks_from_ufos().await } 15 }); 16 17 rsx! { 18 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 } 34 } 35 } 36 } 37 } 38 - }, 39 - Some(Err(_)) => rsx! { 40 - div { "Error loading notebooks" } 41 - }, 42 - None => rsx! { 43 - div { "Loading notebooks..." } 44 } 45 } 46 } 47 } 48 }
··· 1 + use crate::{Route, components::identity::NotebookCard, fetch}; 2 use dioxus::prelude::*; 3 + use jacquard::{IntoStatic, smol_str::ToSmolStr, types::aturi::AtUri}; 4 5 const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); 6 ··· 14 let fetcher = fetcher.clone(); 15 async move { fetcher.fetch_notebooks_from_ufos().await } 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 + }; 30 31 rsx! { 32 document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } 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 + } 65 } 66 } 67 } 68 } 69 + }, 70 + Some(Err(_)) => rsx! { 71 + div { "Error loading notebooks" } 72 + }, 73 + None => rsx! { 74 + div { "Loading notebooks..." } 75 } 76 } 77 } 78 } 79 + 80 } 81 }
+1 -1
crates/weaver-app/src/views/mod.rs
··· 21 pub use notebook::{Notebook, NotebookIndex}; 22 23 mod record; 24 - pub use record::RecordView; 25 26 mod callback; 27 pub use callback::Callback;
··· 21 pub use notebook::{Notebook, NotebookIndex}; 22 23 mod record; 24 + pub use record::{RecordIndex, RecordView}; 25 26 mod callback; 27 pub use callback::Callback;
+127 -97
crates/weaver-app/src/views/record.rs
··· 2 use crate::auth::AuthState; 3 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 4 use crate::fetch::CachedFetcher; 5 - use dioxus::prelude::*; 6 use humansize::format_size; 7 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 8 use jacquard::client::AgentError; ··· 12 client::AgentSessionExt, 13 common::{Data, IntoStatic}, 14 identity::lexicon_resolver::LexiconSchemaResolver, 15 - smol_str::SmolStr, 16 types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid}, 17 }; 18 use mime_sniffer::MimeTypeSniffer; ··· 28 } 29 30 #[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 { 38 h1 { "Record View" } 39 - p { "URI: {uri}" } 40 - p { "Error: {error}" } 41 } 42 - }; 43 } 44 - let uri = use_signal(|| at_uri.unwrap()); 45 let mut view_mode = use_signal(|| ViewMode::Pretty); 46 let mut edit_mode = use_signal(|| false); 47 - let navigator = use_navigator(); 48 49 let client = fetcher.get_client(); 50 let record_resource = use_resource(move || { 51 let client = client.clone(); 52 - async move { client.fetch_record_slingshot(&uri()).await } 53 }); 54 55 // Check ownership for edit access ··· 61 } 62 63 // authority() returns &AtIdentifier which can be Did or Handle 64 - match uri().authority() { 65 AtIdentifier::Did(record_did) => auth.did.as_ref() == Some(record_did), 66 AtIdentifier::Handle(_) => { 67 // Can't easily check ownership for handles without async resolution ··· 69 } 70 } 71 }); 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()); 76 77 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" 96 } 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() { 103 button { 104 - class: "tab-button edit-button", 105 - onclick: move |_| edit_mode.set(true), 106 - "Edit" 107 } 108 } 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 - }, 122 } 123 } 124 } 125 } 126 } 127 } else { 128 - rsx! { 129 - div { 130 - class: "record-view-container", 131 - h1 { "Record" } 132 - p { "URI: {uri}" } 133 - p { "Loading..." } 134 - } 135 - } 136 } 137 } 138 ··· 390 #[component] 391 fn HighlightedUri(uri: AtUri<'static>) -> Element { 392 let s = uri.as_str(); 393 - let link = format!("/record#{}", s); 394 395 if let Some(rest) = s.strip_prefix("at://") { 396 let parts: Vec<&str> = rest.splitn(3, '/').collect(); 397 return rsx! { 398 a { 399 - href: "{link}", 400 class: "uri-link", 401 span { class: "string-at-uri", 402 span { class: "aturi-scheme", "at://" } ··· 419 }; 420 } 421 422 - rsx! { span { class: "string-at-uri", "{s}" } } 423 } 424 425 #[component] ··· 2070 #[component] 2071 fn RecordViewLayout(uri: AtUri<'static>, cid: Option<Cid<'static>>, children: Element) -> Element { 2072 rsx! { 2073 - document::Stylesheet { href: asset!("/assets/styling/record-view.css") } 2074 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 - } 2093 } 2094 } 2095 - {children} 2096 } 2097 } 2098 } 2099 2100 /// Render some text as markdown. 2101 #[component] 2102 fn EditableRecordContent( 2103 - record_value: Signal<Data<'static>>, 2104 uri: ReadSignal<AtUri<'static>>, 2105 view_mode: Signal<ViewMode>, 2106 edit_mode: Signal<bool>, 2107 record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>, 2108 ) -> Element { 2109 - let mut edit_data = use_signal(|| record_value()); 2110 let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); 2111 let navigator = use_navigator(); 2112 let fetcher = use_context::<CachedFetcher>(); ··· 2150 Ok(output) => { 2151 if output.status() == StatusCode::OK { 2152 dioxus_logger::tracing::info!("Record updated successfully"); 2153 - record_value.set(data); 2154 edit_mode.set(false); 2155 } else { 2156 dioxus_logger::tracing::error!("Unexpected status code: {:?}", output.status()); ··· 2184 Ok(response) => { 2185 if let Ok(output) = response.into_output() { 2186 dioxus_logger::tracing::info!("Record created: {}", output.uri); 2187 - nav.push(Route::RecordView { uri: output.uri.to_smolstr() }); 2188 } 2189 } 2190 Err(e) => { ··· 2233 } 2234 2235 dioxus_logger::tracing::info!("Record replaced: {}", create_output.uri); 2236 - nav.push(Route::RecordView { uri: create_output.uri.to_smolstr() }); 2237 } 2238 } 2239 Err(e) => { ··· 2275 }); 2276 }, 2277 on_cancel: move |_| { 2278 - edit_data.set(record_value()); 2279 edit_mode.set(false); 2280 }, 2281 }
··· 2 use crate::auth::AuthState; 3 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 4 use crate::fetch::CachedFetcher; 5 + use dioxus::{CapturedError, prelude::*}; 6 use humansize::format_size; 7 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 8 use jacquard::client::AgentError; ··· 12 client::AgentSessionExt, 13 common::{Data, IntoStatic}, 14 identity::lexicon_resolver::LexiconSchemaResolver, 15 types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid}, 16 }; 17 use mime_sniffer::MimeTypeSniffer; ··· 27 } 28 29 #[component] 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", 48 h1 { "Record View" } 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 + } 63 } 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! {}; 77 } 78 + let uri = use_signal(move || AtUri::new_owned(&*uri.read().join("/")).unwrap()); 79 let mut view_mode = use_signal(|| ViewMode::Pretty); 80 let mut edit_mode = use_signal(|| false); 81 82 let client = fetcher.get_client(); 83 let record_resource = use_resource(move || { 84 let client = client.clone(); 85 + async move { client.fetch_record_slingshot(&*uri.read()).await } 86 }); 87 88 // Check ownership for edit access ··· 94 } 95 96 // authority() returns &AtIdentifier which can be Did or Handle 97 + match &*uri.read().authority() { 98 AtIdentifier::Did(record_did) => auth.did.as_ref() == Some(record_did), 99 AtIdentifier::Handle(_) => { 100 // Can't easily check ownership for handles without async resolution ··· 102 } 103 } 104 }); 105 + if let Some(Ok(record)) = &*record_resource.read() { 106 + let record_value = record.value.clone().into_static(); 107 + let record = record.clone(); 108 109 rsx! { 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, 122 } 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 + } 131 button { 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 + } 142 } 143 } 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 + } 163 } 164 } 165 } 166 } 167 } 168 } else { 169 + rsx! {} 170 } 171 } 172 ··· 424 #[component] 425 fn HighlightedUri(uri: AtUri<'static>) -> Element { 426 let s = uri.as_str(); 427 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, s); 428 429 if let Some(rest) = s.strip_prefix("at://") { 430 let parts: Vec<&str> = rest.splitn(3, '/').collect(); 431 return rsx! { 432 a { 433 + href: link, 434 class: "uri-link", 435 span { class: "string-at-uri", 436 span { class: "aturi-scheme", "at://" } ··· 453 }; 454 } 455 456 + rsx! { a { class: "string-at-uri", href: s } } 457 } 458 459 #[component] ··· 2104 #[component] 2105 fn RecordViewLayout(uri: AtUri<'static>, cid: Option<Cid<'static>>, children: Element) -> Element { 2106 rsx! { 2107 div { 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() } 2113 } 2114 } 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 + } 2121 } 2122 + 2123 + {children} 2124 + 2125 } 2126 } 2127 2128 /// Render some text as markdown. 2129 #[component] 2130 fn EditableRecordContent( 2131 + record_value: Data<'static>, 2132 uri: ReadSignal<AtUri<'static>>, 2133 view_mode: Signal<ViewMode>, 2134 edit_mode: Signal<bool>, 2135 record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>, 2136 ) -> Element { 2137 + let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); 2138 let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); 2139 let navigator = use_navigator(); 2140 let fetcher = use_context::<CachedFetcher>(); ··· 2178 Ok(output) => { 2179 if output.status() == StatusCode::OK { 2180 dioxus_logger::tracing::info!("Record updated successfully"); 2181 + edit_data.set(data.clone()); 2182 edit_mode.set(false); 2183 } else { 2184 dioxus_logger::tracing::error!("Unexpected status code: {:?}", output.status()); ··· 2212 Ok(response) => { 2213 if let Ok(output) = response.into_output() { 2214 dioxus_logger::tracing::info!("Record created: {}", output.uri); 2215 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, output.uri); 2216 + nav.push(link); 2217 } 2218 } 2219 Err(e) => { ··· 2262 } 2263 2264 dioxus_logger::tracing::info!("Record replaced: {}", create_output.uri); 2265 + let link = format!("{}/record/{}", crate::env::WEAVER_APP_DOMAIN, create_output.uri); 2266 + nav.push(link); 2267 } 2268 } 2269 Err(e) => { ··· 2305 }); 2306 }, 2307 on_cancel: move |_| { 2308 + edit_data.set(record_value.clone()); 2309 edit_mode.set(false); 2310 }, 2311 }