5 Commits

Author SHA1 Message Date
ef98a11bb8 Filter options
All checks were successful
Android Build Final Fixed / build-android (push) Successful in 12m29s
2026-03-06 16:55:28 +01:00
9518385718 chore: bump version to v0.5.0
All checks were successful
Android Build Final Fixed / build-android (push) Successful in 12m3s
2026-02-11 14:28:34 +01:00
7cc483b54c chore: bump version to v0.5.0
Some checks failed
Android Build Final Fixed / build-android (push) Has been cancelled
2026-02-11 14:25:56 +01:00
fdae53b7eb Zapstore.yaml
Some checks failed
Android Build Final Fixed / build-android (push) Has been cancelled
2026-02-11 14:15:29 +01:00
53e5b3ba93 fix tauri cli syntax
Some checks failed
Android Build Final Fixed / build-android (push) Has been cancelled
2026-02-11 13:50:30 +01:00
9 changed files with 360 additions and 67 deletions

View File

@@ -34,11 +34,12 @@ jobs:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
rustup target add wasm32-unknown-unknown rustup target add wasm32-unknown-unknown
- name: Install Trunk & Tauri-CLI (Fixed) - name: Install Trunk & Tauri-CLI (Fast & Fixed)
run: | run: |
# Trunk via Binary (schneller als Cargo compile)
wget -qO- https://github.com/trunk-rs/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf- -C /usr/local/bin
# Tauri CLI v2 via Cargo (sicherer für v2)
export PATH="$HOME/.cargo/bin:$PATH" export PATH="$HOME/.cargo/bin:$PATH"
# Installation via Cargo ist sicherer als wget von GitHub Releases
cargo install trunk
cargo install tauri-cli --version "^2.0.0" cargo install tauri-cli --version "^2.0.0"
- name: Setup Android SDK - name: Setup Android SDK
@@ -68,7 +69,9 @@ jobs:
cargo tauri android init cargo tauri android init
fi fi
cargo tauri android build --release --apk # FIX: Bei Android Build v2 ist --release implizit oder wird nicht als Flag akzeptiert
# Wir nutzen genau den Befehl, der lokal bei dir funktioniert hat:
cargo tauri android build --apk
# 3. APK manuell signieren # 3. APK manuell signieren
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | tr -d '[:space:]' > keystore.b64 echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | tr -d '[:space:]' > keystore.b64

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "marstemedia-ui" name = "marstemedia-ui"
version = "0.1.0" version = "0.5.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "marstemedia" name = "marstemedia"
version = "0.1.0" version = "0.5.0"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
edition = "2021" edition = "2021"

Submodule src-tauri/easy-nostr updated: 1becf76264...9845dd025d

View File

@@ -6,7 +6,13 @@ use tauri::{AppHandle, Manager, State};
#[derive(Serialize, Deserialize, Default, Clone)] #[derive(Serialize, Deserialize, Default, Clone)]
pub struct RssConfig { pub struct RssConfig {
pub urls: Vec<String>, pub feeds: Vec<RssFeed>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RssFeed {
pub url: String,
pub category: String,
} }
#[derive(Default)] #[derive(Default)]
@@ -22,6 +28,7 @@ pub struct NewsArticle {
pub content: String, pub content: String,
pub author: String, pub author: String,
pub created_at: String, pub created_at: String,
pub category: String,
} }
fn get_config_path(app: &AppHandle) -> PathBuf { fn get_config_path(app: &AppHandle) -> PathBuf {
@@ -31,6 +38,81 @@ fn get_config_path(app: &AppHandle) -> PathBuf {
.join("rss.ron") .join("rss.ron")
} }
fn default_feeds() -> Vec<RssFeed> {
vec![
// 💻 Technik
RssFeed {
url: "https://www.heise.de/rss/heise-atom.xml".into(),
category: "Technik".into(),
},
RssFeed {
url: "https://www.golem.de/rss.php?feed=ATOM1.0".into(),
category: "Technik".into(),
},
RssFeed {
url: "https://www.theverge.com/rss/index.xml".into(),
category: "Technik".into(),
},
RssFeed {
url: "https://arstechnica.com/feed/".into(),
category: "Technik".into(),
},
RssFeed {
url: "https://www.phoronix.com/rss.php".into(),
category: "Technik".into(),
},
// 📰 Nachrichten
RssFeed {
url: "https://www.tagesschau.de/xml/rss2".into(),
category: "Nachrichten".into(),
},
RssFeed {
url: "https://www.spiegel.de/schlagzeilen/index.rss".into(),
category: "Nachrichten".into(),
},
RssFeed {
url: "https://feeds.bbci.co.uk/news/technology/rss.xml".into(),
category: "Nachrichten".into(),
},
RssFeed {
url: "https://feeds.reuters.com/reuters/technologyNews".into(),
category: "Nachrichten".into(),
},
// 🔭 Wissenschaft
RssFeed {
url: "https://www.nasa.gov/news-release/feed/".into(),
category: "Wissenschaft".into(),
},
RssFeed {
url: "https://www.sciencedaily.com/rss/computers_math/artificial_intelligence.xml"
.into(),
category: "Wissenschaft".into(),
},
RssFeed {
url: "https://deepmind.google/blog/rss.xml".into(),
category: "Wissenschaft".into(),
},
// 🤖 KI
RssFeed {
url: "https://openai.com/blog/rss.xml".into(),
category: "KI".into(),
},
RssFeed {
url: "https://venturebeat.com/feed/".into(),
category: "KI".into(),
},
// 🐧 Open Source
RssFeed {
url: "https://lwn.net/headlines/rss".into(),
category: "Open Source".into(),
},
RssFeed {
url: "https://github.blog/feed/".into(),
category: "Open Source".into(),
},
]
}
#[tauri::command] #[tauri::command]
pub async fn save_openrouter_key(key: String, state: State<'_, NewsState>) -> Result<(), String> { pub async fn save_openrouter_key(key: String, state: State<'_, NewsState>) -> Result<(), String> {
let mut lock = state.openrouter_key.lock().map_err(|_| "Lock failed")?; let mut lock = state.openrouter_key.lock().map_err(|_| "Lock failed")?;
@@ -49,39 +131,45 @@ pub async fn save_groq_key(key: String, state: State<'_, NewsState>) -> Result<(
pub async fn load_rss_config( pub async fn load_rss_config(
app: AppHandle, app: AppHandle,
state: State<'_, NewsState>, state: State<'_, NewsState>,
) -> Result<Vec<String>, String> { ) -> Result<Vec<RssFeed>, String> {
let path = get_config_path(&app); let path = get_config_path(&app);
if !path.exists() { let defaults = default_feeds();
let default_urls = vec![
"https://www.nasa.gov/news-release/feed/".to_string(), let existing_config: Option<RssConfig> = if path.exists() {
"https://www.heise.de/rss/heise-atom.xml".to_string(), let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
]; ron::from_str(&content).ok()
} else {
None
};
// Datei fehlt ODER hat weniger als 3 Einträge → defaults schreiben
let feeds = match existing_config {
Some(config) if config.feeds.len() >= 3 => config.feeds,
_ => {
let config = RssConfig { let config = RssConfig {
urls: default_urls.clone(), feeds: defaults.clone(),
}; };
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?; fs::create_dir_all(parent).map_err(|e| e.to_string())?;
} }
let ron_str = ron::to_string(&config).map_err(|e| e.to_string())?; let ron_str = ron::to_string(&config).map_err(|e| e.to_string())?;
fs::write(&path, ron_str).map_err(|e| e.to_string())?; fs::write(&path, ron_str).map_err(|e| e.to_string())?;
let mut lock = state.rss_config.lock().unwrap(); defaults
lock.urls = default_urls.clone();
return Ok(default_urls);
} }
let content = fs::read_to_string(path).map_err(|e| e.to_string())?; };
let config: RssConfig = ron::from_str(&content).map_err(|e| e.to_string())?;
let mut lock = state.rss_config.lock().unwrap(); let mut lock = state.rss_config.lock().unwrap();
*lock = config.clone(); lock.feeds = feeds.clone();
Ok(config.urls) Ok(feeds)
} }
#[tauri::command] #[tauri::command]
pub async fn save_rss_urls( pub async fn save_rss_urls(
urls: Vec<String>, feeds: Vec<RssFeed>,
app: AppHandle, app: AppHandle,
state: State<'_, NewsState>, state: State<'_, NewsState>,
) -> Result<(), String> { ) -> Result<(), String> {
let config = RssConfig { urls }; let config = RssConfig { feeds };
let path = get_config_path(&app); let path = get_config_path(&app);
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?; fs::create_dir_all(parent).map_err(|e| e.to_string())?;
@@ -152,17 +240,18 @@ pub async fn fetch_ai_news(
content, content,
author: author.into(), author: author.into(),
created_at: "Gerade eben".into(), created_at: "Gerade eben".into(),
category: "KI".into(),
}]) }])
} }
#[tauri::command] #[tauri::command]
pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result<Vec<NewsArticle>, String> { pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result<Vec<NewsArticle>, String> {
let urls = state.rss_config.lock().unwrap().urls.clone(); let feeds = state.rss_config.lock().unwrap().feeds.clone();
let mut all_articles = Vec::new(); let mut all_articles = Vec::new();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
for url in urls { for feed_cfg in feeds {
if let Ok(res) = client.get(&url).send().await { if let Ok(res) = client.get(&feed_cfg.url).send().await {
if let Ok(bytes) = res.bytes().await { if let Ok(bytes) = res.bytes().await {
if let Ok(feed) = feed_rs::parser::parse(&bytes[..]) { if let Ok(feed) = feed_rs::parser::parse(&bytes[..]) {
let source = feed let source = feed
@@ -184,6 +273,7 @@ pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result<Vec<NewsArtic
), ),
author: source.clone(), author: source.clone(),
created_at: date, created_at: date,
category: feed_cfg.category.clone(),
}); });
} }
} }

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "marstemedia", "productName": "marstemedia",
"version": "0.1.0", "version": "0.5.0",
"identifier": "malxte.de", "identifier": "malxte.de",
"build": { "build": {
"beforeDevCommand": "trunk serve", "beforeDevCommand": "trunk serve",

View File

@@ -23,6 +23,13 @@ pub struct NewsArticle {
pub content: String, pub content: String,
pub author: String, pub author: String,
pub created_at: 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)] #[derive(Serialize)]
@@ -30,14 +37,23 @@ struct StringArgs {
key: String, key: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct UrlsArgs { struct FeedsArgs {
urls: Vec<String>, feeds: Vec<RssFeed>,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct AiArgs { struct AiArgs {
provider: String, provider: String,
} }
const CATEGORIES: &[&str] = &[
"Alle",
"Technik",
"Nachrichten",
"Wissenschaft",
"KI",
"Open Source",
];
#[function_component(News)] #[function_component(News)]
pub fn news() -> Html { pub fn news() -> Html {
let articles = use_state(|| None::<Vec<NewsArticle>>); let articles = use_state(|| None::<Vec<NewsArticle>>);
@@ -45,20 +61,22 @@ pub fn news() -> Html {
let show_config = use_state(|| false); let show_config = use_state(|| false);
let current_mode = use_state(|| NewsMode::Ai); let current_mode = use_state(|| NewsMode::Ai);
let ai_provider = use_state(|| "openrouter".to_string()); 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 or_key_ref = use_node_ref();
let groq_key_ref = use_node_ref(); let groq_key_ref = use_node_ref();
let new_url_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 |_| { use_effect_with((), move |_| {
spawn_local(async move { spawn_local(async move {
if let Ok(res) = invoke("load_rss_config", JsValue::NULL).await { if let Ok(res) = invoke("load_rss_config", JsValue::NULL).await {
if let Ok(urls) = serde_wasm_bindgen::from_value::<Vec<String>>(res) { if let Ok(feeds) = serde_wasm_bindgen::from_value::<Vec<RssFeed>>(res) {
rss_urls.set(urls); 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(); let load_news = load_news.clone();
use_effect_with( use_effect_with(
@@ -111,7 +129,7 @@ pub fn news() -> Html {
} }
let save_settings = { let save_settings = {
let rss_urls = rss_urls.clone(); let rss_feeds = rss_feeds.clone();
let or_ref = or_key_ref.clone(); let or_ref = or_key_ref.clone();
let groq_ref = groq_key_ref.clone(); let groq_ref = groq_key_ref.clone();
let show_config = show_config.clone(); let show_config = show_config.clone();
@@ -125,7 +143,7 @@ pub fn news() -> Html {
.cast::<HtmlInputElement>() .cast::<HtmlInputElement>()
.map(|i| i.value()) .map(|i| i.value())
.unwrap_or_default(); .unwrap_or_default();
let urls = (*rss_urls).clone(); let feeds = (*rss_feeds).clone();
let show_config = show_config.clone(); let show_config = show_config.clone();
let load_news = load_news.clone(); let load_news = load_news.clone();
spawn_local(async move { spawn_local(async move {
@@ -141,7 +159,7 @@ pub fn news() -> Html {
.await; .await;
let _ = invoke( let _ = invoke(
"save_rss_urls", "save_rss_urls",
serde_wasm_bindgen::to_value(&UrlsArgs { urls }).unwrap(), serde_wasm_bindgen::to_value(&FeedsArgs { feeds }).unwrap(),
) )
.await; .await;
show_config.set(false); 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! { html! {
<div class="feed-container"> <div class="feed-container">
<div class="feed-header"> <div class="feed-header">
@@ -178,6 +204,24 @@ pub fn news() -> Html {
</div> </div>
</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 { if *show_config {
<div class="config-panel"> <div class="config-panel">
<div class="config-section"> <div class="config-section">
@@ -190,19 +234,51 @@ pub fn news() -> Html {
</div> </div>
<div class="config-section"> <div class="config-section">
<label>{"RSS FEEDS"}</label> <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={ <button class="add-btn" onclick={
let rss_urls = rss_urls.clone(); let rss_feeds = rss_feeds.clone();
let new_url_ref = new_url_ref.clone(); let url_ref = new_url_ref.clone();
let cat_ref = new_cat_ref.clone();
move |_| { move |_| {
if let Some(input) = new_url_ref.cast::<HtmlInputElement>() { let url_input = url_ref.cast::<HtmlInputElement>();
let val = input.value(); let cat_input = cat_ref.cast::<HtmlInputElement>();
if !val.trim().is_empty() { if let (Some(u), Some(c)) = (url_input, cat_input) {
let mut list = (*rss_urls).clone(); let url = u.value();
list.push(val); let category = c.value();
rss_urls.set(list); if !url.trim().is_empty() {
input.set_value(""); 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> </div>
} }
// ── Artikel-Liste ───────────────────────────────────────────────
<div class="posts-list"> <div class="posts-list">
{ if let Some(msg) = (*error_msg).clone() { { if let Some(msg) = (*error_msg).clone() {
html! { <div class="error-box">{ format!("⚠️ Error: {}", msg) }</div> } html! { <div class="error-box">{ format!("⚠️ Error: {}", msg) }</div> }
} else if let Some(list) = (*articles).clone() { } else if (*articles).is_none() {
html! { for list.iter().map(|a| html! { <NewsCard article={a.clone()} /> }) }
} else {
html! { <div class="loading-spinner">{"Lade Nachrichten..."}</div> } 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>
</div> </div>
@@ -246,8 +325,11 @@ pub fn news_card(props: &NewsCardProps) -> Html {
<div class="post-card"> <div class="post-card">
<div class="post-header"> <div class="post-header">
<span class="post-author">{ &a.author }</span> <span class="post-author">{ &a.author }</span>
<div class="post-meta">
<span class="post-category-badge">{ &a.category }</span>
<span class="post-date">{ &a.created_at }</span> <span class="post-date">{ &a.created_at }</span>
</div> </div>
</div>
<div class="post-content markdown-body"> <div class="post-content markdown-body">
{ Html::from_html_unchecked(AttrValue::from(markdown_html)) } { Html::from_html_unchecked(AttrValue::from(markdown_html)) }
</div> </div>

View File

@@ -51,7 +51,8 @@ html {
width: 100%; width: 100%;
max-width: 600px; max-width: 600px;
/* Safe Areas: Top inset and mobile-friendly padding */ /* Safe Areas: Top inset and mobile-friendly padding */
padding: calc(20px + env(safe-area-inset-top)) 16px calc(120px + env(safe-area-inset-bottom)) 16px; padding: calc(20px + env(safe-area-inset-top)) 16px
calc(120px + env(safe-area-inset-bottom)) 16px;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@@ -361,7 +362,6 @@ html {
/* --- Animations --- */ /* --- Animations --- */
@keyframes float { @keyframes float {
0%, 0%,
100% { 100% {
transform: translateY(0px); transform: translateY(0px);
@@ -387,9 +387,11 @@ html {
/* --- Utilities --- */ /* --- Utilities --- */
.reload-btn-large { .reload-btn-large {
cursor: pointer; cursor: pointer;
background: linear-gradient(135deg, background: linear-gradient(
135deg,
rgba(74, 144, 226, 0.15), rgba(74, 144, 226, 0.15),
rgba(74, 144, 226, 0.05)); rgba(74, 144, 226, 0.05)
);
color: var(--accent-color); color: var(--accent-color);
padding: 16px; padding: 16px;
border-radius: 16px; border-radius: 16px;
@@ -590,3 +592,99 @@ html {
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
} }
/* --- Category filter --- */
/* ── Kategorie-Filterleiste ── */
.category-bar {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 12px;
margin-bottom: 20px;
scrollbar-width: none;
}
.category-bar::-webkit-scrollbar {
display: none;
}
.cat-btn {
flex-shrink: 0;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-secondary);
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
white-space: nowrap;
transition: all 0.2s ease;
}
.cat-btn.active {
background: var(--accent-color);
border-color: var(--accent-color);
color: white;
}
.cat-btn:hover:not(.active) {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
/* ── Kategorie-Badge im Feed-Eintrag ── */
.cat-badge {
font-size: 0.7rem;
font-weight: 700;
color: var(--accent-color);
background: rgba(74, 144, 226, 0.12);
border: 1px solid rgba(74, 144, 226, 0.25);
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
margin: 0 8px;
flex-shrink: 0;
}
/* ── Kategorie-Badge auf NewsCard ── */
.post-meta {
display: flex;
align-items: center;
gap: 8px;
}
.post-category-badge {
font-size: 0.7rem;
font-weight: 700;
color: var(--accent-color);
background: rgba(74, 144, 226, 0.12);
border: 1px solid rgba(74, 144, 226, 0.25);
padding: 2px 8px;
border-radius: 10px;
}
/* ── Neue URL-Zeile mit Kategorie-Dropdown ── */
.rss-add-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 16px;
}
.cat-select {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
color: white;
border-radius: 12px;
padding: 14px 10px;
font-size: 0.85rem;
font-family: inherit;
cursor: pointer;
flex-shrink: 0;
width: 130px;
transition: border-color 0.2s;
}
.cat-select:focus {
outline: none;
border-color: var(--accent-color);
}
.cat-select option {
background: #1e1e2e;
color: white;
}

20
zapstore.yaml Normal file
View File

@@ -0,0 +1,20 @@
name: "Marstemedia"
package: "de.malxte.marstemedia" # Deine Android Package ID
description: "A modern, fast, and secure application to stay updated with the latest news from different worlds — from decentralized social networks to AI-generated insights. "
author: "Bytemalte"
# Hier definierst du, woher zsp die APKs beziehen soll
release_source:
type: "gitea" # Oder "web", falls Gitea Probleme macht
url: "https://gitea.malxte.de/Bytemalte/marstemedia"
# Falls du schon Relays im Kopf hast:
relays:
- "wss://relay.zapstore.dev"
- "wss://relay.malxte.de"
- "wss://relay.primal.net"
- "wss://relay.damus.io"
- "wss://nostr.mom"
- "wss://snort.social"
icon_url: "https://gitea.malxte.de/Bytemalte/marstemedia/raw/branch/main/public/logo.png"