4 Commits

Author SHA1 Message Date
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
3012f84663 Update android workflow to universal apk
Some checks failed
Android Build Final Fixed / build-android (push) Failing after 9m10s
2026-02-11 13:11:37 +01:00
c7a0272df3 Update android workflow to universal apk
Some checks failed
Android Build Final Fixed / build-android (push) Failing after 1m34s
2026-02-11 13:08:34 +01:00
1e1614b96d new hashtag filter option
All checks were successful
Android Build Final Fixed / build-android (push) Successful in 7m46s
2026-01-31 13:07:57 +01:00
5 changed files with 209 additions and 44 deletions

View File

@@ -2,7 +2,7 @@ name: Android Build Final Fixed
on:
push:
branches: [main]
tags: ["v*"] # Triggert bei v0.1.0, v1.0, etc.
tags: ["v*"]
jobs:
build-android:
@@ -31,13 +31,16 @@ jobs:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
export PATH="$HOME/.cargo/bin:$PATH"
rustup target add aarch64-linux-android wasm32-unknown-unknown
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
rustup target add wasm32-unknown-unknown
- name: Install Trunk & Tauri-CLI
- name: Install Trunk & Tauri-CLI (Fast & Fixed)
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
wget -qO- https://github.com/tauri-apps/tauri/releases/latest/download/cargo-tauri-x86_64-unknown-linux-gnu.tgz | tar -xzf- -C /usr/local/bin
chmod +x /usr/local/bin/trunk /usr/local/bin/cargo-tauri
# Tauri CLI v2 via Cargo (sicherer für v2)
export PATH="$HOME/.cargo/bin:$PATH"
cargo install tauri-cli --version "^2.0.0"
- name: Setup Android SDK
run: |
@@ -63,15 +66,23 @@ jobs:
# 2. Android Build
if [ ! -d "src-tauri/gen/android" ]; then
cargo-tauri android init
cargo tauri android init
fi
cargo-tauri android build --target aarch64 --apk true
# 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
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | tr -d '[:space:]' > keystore.b64
base64 -d keystore.b64 > release.keystore
UNSIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name "*-unsigned.apk" | head -n 1)
UNSIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name "app-universal-release-unsigned.apk" | head -n 1)
if [ -z "$UNSIGNED_APK" ]; then
UNSIGNED_APK="src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release-unsigned.apk"
fi
APKSIGNER=$(find $ANDROID_HOME/build-tools -name apksigner | sort -r | head -n 1)
$APKSIGNER sign --ks release.keystore \
@@ -80,7 +91,7 @@ jobs:
--ks-pass pass:"${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" \
--key-pass pass:"${{ secrets.ANDROID_KEY_PASSWORD }}" \
--v4-signing-enabled true \
--out Marstemedia-Signed.apk \
--out Marstemedia-Universal-Signed.apk \
"$UNSIGNED_APK"
echo "Signierung erfolgreich!"
@@ -90,15 +101,14 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: Marstemedia-Signed
path: Marstemedia-Signed.apk
name: Marstemedia-Universal-Signed
path: Marstemedia-Universal-Signed.apk
- name: Create Gitea Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: Marstemedia-Signed.apk
# Gitea braucht manchmal explizit den Namen/Body
files: Marstemedia-Universal-Signed.apk
name: "Release ${{ github.ref_name }}"
draft: false
prerelease: false

Submodule src-tauri/easy-nostr updated: 7cebc490ca...1becf76264

View File

@@ -11,9 +11,9 @@ pub struct LocalPost {
}
#[tauri::command]
pub async fn fetch_nostr_posts() -> Result<Vec<LocalPost>, String> {
pub async fn fetch_nostr_posts(hashtags: Vec<String>) -> Result<Vec<LocalPost>, String> {
println!("Fetching Nostr posts for hashtags: {:?}", hashtags);
// 1. Temporären Einweg-Schlüssel generieren
// Das erzeugt ein Schlüsselpaar im RAM, das nach dem Funktionsaufruf verschwindet.
let random_keys = Keys::generate();
let temp_nsec = random_keys
.secret_key()
@@ -34,8 +34,14 @@ pub async fn fetch_nostr_posts() -> Result<Vec<LocalPost>, String> {
.await
.map_err(|e| e.to_string())?;
// 4. Posts von der Library holen
let raw_posts = easy.get_random_posts().await.map_err(|e| e.to_string())?;
// 4. Posts von der Library holen - Entweder per Hashtag oder Random
let raw_posts = if hashtags.is_empty() {
easy.get_random_posts().await.map_err(|e| e.to_string())?
} else {
easy.get_posts_by_hashtags(hashtags)
.await
.map_err(|e| e.to_string())?
};
// 5. Mappen: Library-Typ -> Unser serialisierbarer Typ
let mapped_posts = raw_posts

View File

@@ -7,8 +7,8 @@ use yew::prelude::*;
// Tauri 'invoke' Funktion deklarieren
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], catch)]
async fn invoke(cmd: &str, args: JsValue) -> Result<JsValue, JsValue>;
}
// Wir definieren Post lokal, damit Yew weiß, wie die JSON-Daten vom Backend aussehen
@@ -24,13 +24,17 @@ pub struct Post {
pub fn home() -> Html {
let posts = use_state(|| None::<Vec<Post>>);
let error = use_state(|| false);
let hashtag_input = use_state(|| String::new());
let active_tags = use_state(Vec::<String>::new);
let load_posts = {
let posts = posts.clone();
let error = error.clone();
let active_tags = active_tags.clone();
Callback::from(move |_e: MouseEvent| {
let posts = posts.clone();
let error = error.clone();
let tags = (*active_tags).clone();
// UI zurücksetzen & Scrollen
posts.set(None);
@@ -43,15 +47,26 @@ pub fn home() -> Html {
}
spawn_local(async move {
// Daten vom Rust-Backend anfordern
let res = invoke("fetch_nostr_posts", JsValue::NULL).await;
#[derive(Serialize)]
struct HashtagArgs {
hashtags: Vec<String>,
}
let args = serde_wasm_bindgen::to_value(&HashtagArgs { hashtags: tags }).unwrap();
// Daten vom Rust-Backend anfordern
match invoke("fetch_nostr_posts", args).await {
Ok(res) => {
// JSON-Resultat in Vec<Post> umwandeln
if let Ok(new_posts) = serde_wasm_bindgen::from_value::<Vec<Post>>(res) {
posts.set(Some(new_posts));
} else {
error.set(true);
}
}
Err(_) => {
error.set(true);
}
}
});
})
};
@@ -68,11 +83,74 @@ pub fn home() -> Html {
html! {
<div class="feed-container">
<div class="feed-header">
<div class="header-main">
<h1 class="feed-title">{"✨ Entdecken"}</h1>
<button class="reload-btn" onclick={load_posts.clone()}>
{"🔄"}
</button>
</div>
</div>
<div class="hashtag-section">
<div class="hashtag-input-row">
<input
type="text"
class="hashtag-input"
placeholder="Tags hinzufügen (z.B. bitcoin, rust)..."
value={(*hashtag_input).clone()}
oninput={
let hashtag_input = hashtag_input.clone();
move |e: InputEvent| {
let target: web_sys::HtmlInputElement = e.target_unchecked_into();
hashtag_input.set(target.value());
}
}
onkeydown={
let hashtag_input = hashtag_input.clone();
let active_tags = active_tags.clone();
let load_posts = load_posts.clone();
move |e: KeyboardEvent| {
if e.key() == "Enter" {
let val = (*hashtag_input).trim().to_string();
if !val.is_empty() {
let mut tags = (*active_tags).clone();
// Mehrere Tags per Komma trennen
for t in val.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
if !tags.contains(&t.to_string()) {
tags.push(t.to_string());
}
}
active_tags.set(tags);
hashtag_input.set(String::new());
load_posts.emit(MouseEvent::new("click").unwrap());
}
}
}
}
/>
</div>
if !active_tags.is_empty() {
<div class="tag-chips">
{ for active_tags.iter().map(|t| {
let t_clone = t.clone();
let active_tags = active_tags.clone();
let load_posts = load_posts.clone();
let remove = move |_| {
let mut tags = (*active_tags).clone();
tags.retain(|x| x != &t_clone);
active_tags.set(tags);
load_posts.emit(MouseEvent::new("click").unwrap());
};
html! {
<span class="tag-chip" onclick={remove}>
{ format!("#{}", t) }
<span class="tag-remove">{"×"}</span>
</span>
}
}) }
</div>
}
</div>
{
if *error {

View File

@@ -10,13 +10,15 @@
--text-primary: #e2e2e7;
--text-secondary: #a0a0b0;
--accent-color: #4a90e2;
--nav-bg: rgba(30, 30, 46, 0.95); /* Fallback for blur */
--nav-bg: rgba(30, 30, 46, 0.95);
/* Fallback for blur */
--danger: #ef4444;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent; /* UX-Finishing: No blue flash */
-webkit-tap-highlight-color: transparent;
/* UX-Finishing: No blue flash */
}
/* --- Global & Layout --- */
@@ -35,7 +37,8 @@ body {
display: flex;
justify-content: center;
min-height: 100vh;
overflow-x: hidden; /* Performance: Prevent horizontal scroll */
overflow-x: hidden;
/* Performance: Prevent horizontal scroll */
-webkit-tap-highlight-color: transparent;
}
@@ -48,8 +51,7 @@ html {
width: 100%;
max-width: 600px;
/* 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) {
@@ -93,7 +95,8 @@ html {
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* Touch-Ergonomie: Min 44px effective height */
padding: 10px 12px;
user-select: none; /* UX-Finishing */
user-select: none;
/* UX-Finishing */
display: flex;
align-items: center;
justify-content: center;
@@ -358,10 +361,12 @@ html {
/* --- Animations --- */
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-15px);
}
@@ -372,6 +377,7 @@ html {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
@@ -381,11 +387,9 @@ html {
/* --- Utilities --- */
.reload-btn-large {
cursor: pointer;
background: linear-gradient(
135deg,
background: linear-gradient(135deg,
rgba(74, 144, 226, 0.15),
rgba(74, 144, 226, 0.05)
);
rgba(74, 144, 226, 0.05));
color: var(--accent-color);
padding: 16px;
border-radius: 16px;
@@ -418,9 +422,11 @@ html {
0% {
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(74, 144, 226, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(74, 144, 226, 0);
}
@@ -513,9 +519,74 @@ html {
opacity: 0.8;
}
/* --- Hashtag Section --- */
.hashtag-section {
margin-bottom: 24px;
}
.hashtag-input-row {
display: flex;
gap: 10px;
}
.hashtag-input {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 12px 16px;
color: white;
font-size: 0.95rem;
outline: none;
transition: all 0.2s;
}
.hashtag-input:focus {
border-color: var(--accent-color);
background: rgba(255, 255, 255, 0.08);
}
.tag-chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.tag-chip {
background: rgba(74, 144, 226, 0.15);
color: var(--accent-color);
border: 1px solid rgba(74, 144, 226, 0.3);
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.tag-chip:hover {
background: rgba(74, 144, 226, 0.25);
transform: scale(1.05);
}
.tag-remove {
font-size: 1.1rem;
line-height: 1;
opacity: 0.6;
}
.tag-chip:hover .tag-remove {
opacity: 1;
}
/* --- Layout Adjustment für Provider Switch --- */
.header-main {
display: flex;
align-items: center;
gap: 12px;
justify-content: space-between;
width: 100%;
}