Filter options
All checks were successful
Android Build Final Fixed / build-android (push) Successful in 12m29s

This commit is contained in:
2026-03-06 16:55:28 +01:00
parent 9518385718
commit ef98a11bb8
4 changed files with 330 additions and 60 deletions

View File

@@ -23,6 +23,13 @@ pub struct NewsArticle {
pub content: String,
pub author: String,
pub created_at: String,
pub category: String,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct RssFeed {
pub url: String,
pub category: String,
}
#[derive(Serialize)]
@@ -30,14 +37,23 @@ struct StringArgs {
key: String,
}
#[derive(Serialize)]
struct UrlsArgs {
urls: Vec<String>,
struct FeedsArgs {
feeds: Vec<RssFeed>,
}
#[derive(Serialize)]
struct AiArgs {
provider: String,
}
const CATEGORIES: &[&str] = &[
"Alle",
"Technik",
"Nachrichten",
"Wissenschaft",
"KI",
"Open Source",
];
#[function_component(News)]
pub fn news() -> Html {
let articles = use_state(|| None::<Vec<NewsArticle>>);
@@ -45,20 +61,22 @@ pub fn news() -> Html {
let show_config = use_state(|| false);
let current_mode = use_state(|| NewsMode::Ai);
let ai_provider = use_state(|| "openrouter".to_string());
let rss_urls = use_state(Vec::<String>::new);
let rss_feeds = use_state(Vec::<RssFeed>::new);
let selected_category = use_state(|| "Alle".to_string());
let or_key_ref = use_node_ref();
let groq_key_ref = use_node_ref();
let new_url_ref = use_node_ref();
let new_cat_ref = use_node_ref();
// Init: RSS laden
// Init: RSS Feeds laden
{
let rss_urls = rss_urls.clone();
let rss_feeds = rss_feeds.clone();
use_effect_with((), move |_| {
spawn_local(async move {
if let Ok(res) = invoke("load_rss_config", JsValue::NULL).await {
if let Ok(urls) = serde_wasm_bindgen::from_value::<Vec<String>>(res) {
rss_urls.set(urls);
if let Ok(feeds) = serde_wasm_bindgen::from_value::<Vec<RssFeed>>(res) {
rss_feeds.set(feeds);
}
}
});
@@ -98,7 +116,7 @@ pub fn news() -> Html {
})
};
// Auto-Reload bei Switch
// Auto-Reload bei Mode/Provider-Switch
{
let load_news = load_news.clone();
use_effect_with(
@@ -111,7 +129,7 @@ pub fn news() -> Html {
}
let save_settings = {
let rss_urls = rss_urls.clone();
let rss_feeds = rss_feeds.clone();
let or_ref = or_key_ref.clone();
let groq_ref = groq_key_ref.clone();
let show_config = show_config.clone();
@@ -125,7 +143,7 @@ pub fn news() -> Html {
.cast::<HtmlInputElement>()
.map(|i| i.value())
.unwrap_or_default();
let urls = (*rss_urls).clone();
let feeds = (*rss_feeds).clone();
let show_config = show_config.clone();
let load_news = load_news.clone();
spawn_local(async move {
@@ -141,7 +159,7 @@ pub fn news() -> Html {
.await;
let _ = invoke(
"save_rss_urls",
serde_wasm_bindgen::to_value(&UrlsArgs { urls }).unwrap(),
serde_wasm_bindgen::to_value(&FeedsArgs { feeds }).unwrap(),
)
.await;
show_config.set(false);
@@ -150,6 +168,14 @@ pub fn news() -> Html {
})
};
// Gefilterte Artikel für die Anzeige
let filtered_articles: Vec<NewsArticle> = (*articles)
.clone()
.unwrap_or_default()
.into_iter()
.filter(|a| *selected_category == "Alle" || a.category == *selected_category)
.collect();
html! {
<div class="feed-container">
<div class="feed-header">
@@ -178,6 +204,24 @@ pub fn news() -> Html {
</div>
</div>
// ── Kategorie-Filterleiste ──────────────────────────────────────
if *current_mode == NewsMode::Rss {
<div class="category-bar">
{ for CATEGORIES.iter().map(|&cat| {
let selected_category = selected_category.clone();
let is_active = *selected_category == cat;
html! {
<button
class={if is_active { "cat-btn active" } else { "cat-btn" }}
onclick={Callback::from(move |_| selected_category.set(cat.to_string()))}>
{ cat }
</button>
}
}) }
</div>
}
// ── Einstellungen ───────────────────────────────────────────────
if *show_config {
<div class="config-panel">
<div class="config-section">
@@ -190,19 +234,51 @@ pub fn news() -> Html {
</div>
<div class="config-section">
<label>{"RSS FEEDS"}</label>
<div class="rss-input-row">
<input ref={new_url_ref.clone()} type="text" class="config-input" placeholder="URL..." />
// Bestehende Feeds auflisten
<div class="rss-list-scroll">
{ for (*rss_feeds).iter().enumerate().map(|(i, feed)| {
let rss_feeds = rss_feeds.clone();
let url_display = feed.url.clone();
let cat_display = feed.category.clone();
let on_delete = Callback::from(move |_| {
let mut list = (*rss_feeds).clone();
list.remove(i);
rss_feeds.set(list);
});
html! {
<div class="rss-url-entry" key={url_display.clone()}>
<span class="url-text">{ &url_display }</span>
<span class="cat-badge">{ &cat_display }</span>
<button class="delete-btn" onclick={on_delete}>{"🗑️"}</button>
</div>
}
}) }
</div>
// Neue URL + Kategorie hinzufügen
<div class="rss-add-row">
<input ref={new_url_ref.clone()} type="text" class="config-input" placeholder="https://..." />
<select ref={new_cat_ref.clone()} class="cat-select">
{ for CATEGORIES.iter().skip(1).map(|&cat| html! {
<option value={cat}>{ cat }</option>
}) }
</select>
<button class="add-btn" onclick={
let rss_urls = rss_urls.clone();
let new_url_ref = new_url_ref.clone();
let rss_feeds = rss_feeds.clone();
let url_ref = new_url_ref.clone();
let cat_ref = new_cat_ref.clone();
move |_| {
if let Some(input) = new_url_ref.cast::<HtmlInputElement>() {
let val = input.value();
if !val.trim().is_empty() {
let mut list = (*rss_urls).clone();
list.push(val);
rss_urls.set(list);
input.set_value("");
let url_input = url_ref.cast::<HtmlInputElement>();
let cat_input = cat_ref.cast::<HtmlInputElement>();
if let (Some(u), Some(c)) = (url_input, cat_input) {
let url = u.value();
let category = c.value();
if !url.trim().is_empty() {
let mut list = (*rss_feeds).clone();
list.push(RssFeed { url, category });
rss_feeds.set(list);
u.set_value("");
}
}
}
@@ -213,13 +289,16 @@ pub fn news() -> Html {
</div>
}
// ── Artikel-Liste ───────────────────────────────────────────────
<div class="posts-list">
{ if let Some(msg) = (*error_msg).clone() {
html! { <div class="error-box">{ format!("⚠️ Error: {}", msg) }</div> }
} else if let Some(list) = (*articles).clone() {
html! { for list.iter().map(|a| html! { <NewsCard article={a.clone()} /> }) }
} else {
} else if (*articles).is_none() {
html! { <div class="loading-spinner">{"Lade Nachrichten..."}</div> }
} else if filtered_articles.is_empty() {
html! { <div class="empty-msg">{"Keine Artikel in dieser Kategorie."}</div> }
} else {
html! { for filtered_articles.iter().map(|a| html! { <NewsCard article={a.clone()} /> }) }
} }
</div>
</div>
@@ -246,7 +325,10 @@ pub fn news_card(props: &NewsCardProps) -> Html {
<div class="post-card">
<div class="post-header">
<span class="post-author">{ &a.author }</span>
<span class="post-date">{ &a.created_at }</span>
<div class="post-meta">
<span class="post-category-badge">{ &a.category }</span>
<span class="post-date">{ &a.created_at }</span>
</div>
</div>
<div class="post-content markdown-body">
{ Html::from_html_unchecked(AttrValue::from(markdown_html)) }