Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7a0272df3 | |||
| 1e1614b96d |
@@ -31,7 +31,9 @@ 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
|
||||
# Alle Architekturen für Universal APK hinzufügen
|
||||
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
|
||||
run: |
|
||||
@@ -61,17 +63,26 @@ jobs:
|
||||
# 1. Frontend bauen
|
||||
trunk build --release
|
||||
|
||||
# 2. Android Build
|
||||
# 2. Android Build initialisieren falls nötig
|
||||
if [ ! -d "src-tauri/gen/android" ]; then
|
||||
cargo-tauri android init
|
||||
fi
|
||||
cargo-tauri android build --target aarch64 --apk true
|
||||
|
||||
# Universal APK bauen (erzeugt die Datei laut deinem Log)
|
||||
cargo-tauri android build --release --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)
|
||||
# Pfad basierend auf deinem lokalen erfolgreichen Build
|
||||
UNSIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name "app-universal-release-unsigned.apk" | head -n 1)
|
||||
|
||||
# Fallback falls find nichts liefert
|
||||
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,25 +91,24 @@ 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!"
|
||||
echo "Signierung der Universal APK erfolgreich!"
|
||||
env:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
|
||||
- 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
@@ -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
|
||||
|
||||
@@ -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,14 +47,25 @@ 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();
|
||||
|
||||
// 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);
|
||||
// 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,10 +83,73 @@ pub fn home() -> Html {
|
||||
html! {
|
||||
<div class="feed-container">
|
||||
<div class="feed-header">
|
||||
<h1 class="feed-title">{"✨ Entdecken"}</h1>
|
||||
<button class="reload-btn" onclick={load_posts.clone()}>
|
||||
{"🔄"}
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{
|
||||
|
||||
97
styles.css
97
styles.css
@@ -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,
|
||||
rgba(74, 144, 226, 0.15),
|
||||
rgba(74, 144, 226, 0.05)
|
||||
);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(74, 144, 226, 0.15),
|
||||
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%;
|
||||
}
|
||||
Reference in New Issue
Block a user