experiments in a post-browser web
10
fork

Configure Feed

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

feat(mobile): move reader CSS application from Rust to frontend for HMR support

Add get_reader_content command that returns article content + metadata as JSON.
Add show_inline_html command that displays pre-built HTML in native WKWebView.
App.tsx now imports reader.css via Vite ?raw, builds the full reader HTML template
in the frontend, and passes it to show_inline_html. This means reader.css changes
are picked up by HMR without requiring a Rust rebuild.

The old open_inline_webview_html command is preserved but no longer called.

+133 -10
+8 -2
backend/tauri-mobile/dev-ios-sim.sh
··· 10 10 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 11 11 CONF="$SCRIPT_DIR/src-tauri/tauri.conf.json" 12 12 13 - # Pick a random available port 14 - PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()") 13 + # Use a stable default port, only pick a random one if it's in use 14 + DEFAULT_PORT=5188 15 + if python3 -c "import socket; s=socket.socket(); s.bind(('',${DEFAULT_PORT})); s.close()" 2>/dev/null; then 16 + PORT=$DEFAULT_PORT 17 + else 18 + PORT=$(python3 -c "import socket; s=socket.socket(); s.bind(('',0)); print(s.getsockname()[1]); s.close()") 19 + echo "[dev] Default port $DEFAULT_PORT in use, using $PORT instead" 20 + fi 15 21 echo "[dev] Using port $PORT" 16 22 17 23 # Get the Mac's IP address for simulator to connect (can't use localhost in simulator)
+78 -1
backend/tauri-mobile/src-tauri/src/lib.rs
··· 4388 4388 } 4389 4389 } 4390 4390 4391 + /// Retrieve offline reader content + metadata for frontend HTML assembly 4392 + #[tauri::command] 4393 + fn get_reader_content(item_id: String) -> Result<serde_json::Value, String> { 4394 + let conn = get_connection()?; 4395 + let original_url: Option<String> = conn 4396 + .query_row( 4397 + "SELECT url FROM items WHERE id = ? AND deleted_at IS NULL", 4398 + params![&item_id], 4399 + |row| row.get(0), 4400 + ) 4401 + .ok(); 4402 + 4403 + let container = get_container_path() 4404 + .ok_or_else(|| "No container path".to_string())?; 4405 + let offline_dir = container.join("offline").join(&item_id); 4406 + let content_path = offline_dir.join("content.html"); 4407 + if !content_path.exists() { 4408 + return Err("No offline content available".to_string()); 4409 + } 4410 + let html = fs::read_to_string(&content_path) 4411 + .map_err(|e| format!("Failed to read offline content: {}", e))?; 4412 + 4413 + // Load article metadata if available 4414 + let meta_path = offline_dir.join("meta.json"); 4415 + let meta: serde_json::Value = if meta_path.exists() { 4416 + let meta_str = fs::read_to_string(&meta_path).unwrap_or_default(); 4417 + serde_json::from_str(&meta_str).unwrap_or(serde_json::json!({})) 4418 + } else { 4419 + serde_json::json!({}) 4420 + }; 4421 + let title = meta["title"].as_str().unwrap_or("").to_string(); 4422 + let byline = meta["byline"].as_str().unwrap_or("").to_string(); 4423 + let dir = meta["dir"].as_str().unwrap_or("").to_string(); 4424 + 4425 + // Extract domain from URL 4426 + let domain = original_url.as_ref() 4427 + .and_then(|u| url::Url::parse(u).ok()) 4428 + .and_then(|u| u.host_str().map(|h| h.to_string())) 4429 + .unwrap_or_default(); 4430 + 4431 + Ok(serde_json::json!({ 4432 + "html": html, 4433 + "title": title, 4434 + "byline": byline, 4435 + "dir": dir, 4436 + "domain": domain, 4437 + "url": original_url.unwrap_or_default() 4438 + })) 4439 + } 4440 + 4441 + /// Display pre-built HTML in the inline native WKWebView 4442 + #[tauri::command] 4443 + async fn show_inline_html(html: String, top_offset: f64, base_url: String) -> Result<(), String> { 4444 + println!("[Rust] show_inline_html called, top_offset: {}, base_url: {}", top_offset, base_url); 4445 + 4446 + #[cfg(target_os = "ios")] 4447 + { 4448 + use std::ffi::CString; 4449 + let html_cstr = CString::new(html).map_err(|e| format!("Invalid HTML content: {}", e))?; 4450 + let base_url_cstr = CString::new(base_url.as_str()).map_err(|e| format!("Invalid base URL: {}", e))?; 4451 + let result = unsafe { webview_plugin_open_inline_html(html_cstr.as_ptr(), top_offset, base_url_cstr.as_ptr()) }; 4452 + if result { 4453 + Ok(()) 4454 + } else { 4455 + Err("Failed to open inline HTML webview".to_string()) 4456 + } 4457 + } 4458 + 4459 + #[cfg(not(target_os = "ios"))] 4460 + { 4461 + let _ = (html, top_offset, base_url); 4462 + Ok(()) 4463 + } 4464 + } 4465 + 4391 4466 /// Fetch a URL, extract reader-mode content, and store to the offline directory 4392 4467 async fn fetch_and_store_offline_content(item_id: &str, url: &str) -> Result<(), String> { 4393 4468 println!("[Rust] Fetching offline content for item {} from {}", item_id, url); ··· 5598 5673 // Inline webview 5599 5674 open_inline_webview, 5600 5675 open_inline_webview_html, 5601 - close_inline_webview 5676 + close_inline_webview, 5677 + get_reader_content, 5678 + show_inline_html 5602 5679 ]) 5603 5680 .run(tauri::generate_context!()) 5604 5681 .expect("error while running tauri application");
+1 -1
backend/tauri-mobile/src-tauri/tauri.conf.json
··· 6 6 "build": { 7 7 "beforeBuildCommand": "npm run build", 8 8 "frontendDist": "../dist", 9 - "devUrl": "http://192.168.50.143:64872" 9 + "devUrl": "http://192.168.50.143:5188" 10 10 }, 11 11 "app": { 12 12 "windows": [
+41 -1
backend/tauri-mobile/src/App.tsx
··· 3 3 import { listen } from "@tauri-apps/api/event"; 4 4 import { openUrl } from "@tauri-apps/plugin-opener"; 5 5 import "./App.css"; 6 + import readerCss from "./reader.css?raw"; 6 7 7 8 declare const __BUILD_NUMBER__: string; 8 9 ··· 2917 2918 topOffset = inlineDiv.getBoundingClientRect().top; 2918 2919 } 2919 2920 console.log("[App] Opening offline reader at topOffset:", topOffset, "for item:", itemId); 2920 - await invoke("open_inline_webview_html", { itemId, topOffset }); 2921 + // Fetch article content + metadata from Rust, build HTML in frontend for HMR 2922 + const content = await invoke<{ 2923 + html: string; 2924 + title: string; 2925 + byline: string; 2926 + dir: string; 2927 + domain: string; 2928 + url: string; 2929 + }>("get_reader_content", { itemId }); 2930 + const dirAttr = content.dir ? ` dir="${content.dir}"` : ""; 2931 + const baseTag = content.url ? `<base href="${content.url.replace(/"/g, '&quot;')}">` : ""; 2932 + const domainLink = content.url 2933 + ? `<a class="domain" href="${content.url.replace(/"/g, '&quot;')}">${content.domain}</a>` 2934 + : ""; 2935 + const bylineHtml = content.byline 2936 + ? `<div class="credits">${content.byline}</div>` 2937 + : ""; 2938 + const headerHtml = (content.title || content.domain) 2939 + ? `<div class="header">${domainLink}<h1>${content.title}</h1>${bylineHtml}</div>` 2940 + : ""; 2941 + const fullHtml = `<!DOCTYPE html> 2942 + <html${dirAttr}> 2943 + <head> 2944 + <meta charset="utf-8"> 2945 + <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> 2946 + ${baseTag} 2947 + <style> 2948 + ${readerCss} 2949 + </style> 2950 + </head> 2951 + <body> 2952 + <div class="container"> 2953 + ${headerHtml} 2954 + <div class="reader-content"> 2955 + ${content.html} 2956 + </div> 2957 + </div> 2958 + </body> 2959 + </html>`; 2960 + await invoke("show_inline_html", { html: fullHtml, topOffset, baseUrl: content.url }); 2921 2961 setWebviewLoaded(true); 2922 2962 } catch (error) { 2923 2963 console.error("[App] Failed to open offline reader:", error);
+5 -5
backend/tauri-mobile/src/reader.css
··· 8 8 body { 9 9 margin: 0; padding: 20px; 10 10 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; 11 - color: #333; background: #fff; 11 + color: #0f0f0f; background: #f6f6f6; 12 12 -webkit-text-size-adjust: 100%; 13 13 } 14 14 @media (prefers-color-scheme: dark) { 15 - body { color: #e0e0e0; background: #1a1a2e; } 15 + body { color: #f6f6f6; background: #1c1c1e; } 16 16 } 17 17 18 18 /* Container */ ··· 27 27 .header { margin-bottom: 24px; } 28 28 .domain { 29 29 font-size: 0.85em; 30 - color: #0095dd; 30 + color: #007aff; 31 31 text-decoration: none; 32 32 font-family: -apple-system, sans-serif; 33 33 } ··· 54 54 .reader-content p { margin: 0 0 1em; } 55 55 56 56 /* Links */ 57 - .reader-content a { color: #0095dd; text-decoration: underline; } 57 + .reader-content a { color: #007aff; text-decoration: underline; } 58 58 .reader-content a:visited { color: #cc22ee; } 59 59 @media (prefers-color-scheme: dark) { 60 - .reader-content a { color: #6cb4ee; } 60 + .reader-content a { color: #0a84ff; } 61 61 .reader-content a:visited { color: #cc88ff; } 62 62 } 63 63