Compare commits
3 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48221b039e | ||
|
|
60354b66d1 | ||
|
|
0433406288 |
@@ -26,22 +26,23 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
|
# Run Tailwind manually first
|
||||||
npx @tailwindcss/cli -i input.css -o public/tailwind.css
|
npx @tailwindcss/cli -i input.css -o public/tailwind.css
|
||||||
# Trunk'ın optimizasyonunu kapalı (0) tutuyoruz çünkü Cargo.toml'daki opt-level='z' zaten o işi yapıyor.
|
|
||||||
trunk build --release
|
trunk build --release
|
||||||
|
|
||||||
- name: Build Backend (MIPS)
|
- name: Build Backend (MIPS)
|
||||||
env:
|
env:
|
||||||
# -s ve -w ile binary içindeki gereksiz tüm yükleri siliyoruz.
|
# Ensure we are building a fully static binary
|
||||||
RUSTFLAGS: "-C target-feature=+crt-static -C link-self-contained=no -C link-arg=-msoft-float -C link-arg=-s -C link-arg=-w"
|
# -C link-self-contained=no: Let Zig (the linker) handle CRT objects (crt1.o, etc.)
|
||||||
|
RUSTFLAGS: "-C target-feature=+crt-static -C link-self-contained=no -C link-arg=-msoft-float"
|
||||||
CFLAGS_mips_unknown_linux_musl: "-msoft-float"
|
CFLAGS_mips_unknown_linux_musl: "-msoft-float"
|
||||||
run: |
|
run: |
|
||||||
# Sadece gerekli özellikleri derliyoruz (Boyut tasarrufu için swagger kapalı)
|
cd backend
|
||||||
cargo zigbuild -p backend --target mips-unknown-linux-musl --release -Z build-std=std,panic_abort --no-default-features --features push-notifications
|
cargo zigbuild --target mips-unknown-linux-musl --release -Z build-std=std,panic_abort
|
||||||
|
file target/mips-unknown-linux-musl/release/backend
|
||||||
|
|
||||||
- name: Create Release Assets
|
- name: Rename Binary
|
||||||
run: |
|
run: mv target/mips-unknown-linux-musl/release/backend target/mips-unknown-linux-musl/release/vibetorrent-mips
|
||||||
mv target/mips-unknown-linux-musl/release/backend target/mips-unknown-linux-musl/release/vibetorrent-mips
|
|
||||||
|
|
||||||
- name: Generate Release Tag
|
- name: Generate Release Tag
|
||||||
id: tag
|
id: tag
|
||||||
@@ -55,10 +56,8 @@ jobs:
|
|||||||
REPO="admin/vibetorrent"
|
REPO="admin/vibetorrent"
|
||||||
API_URL="${{ gitea.server_url }}/api/v1"
|
API_URL="${{ gitea.server_url }}/api/v1"
|
||||||
|
|
||||||
RELEASE_RESPONSE=$(curl -s -X POST "${API_URL}/repos/${REPO}/releases" \
|
# Create release
|
||||||
-H "Authorization: token ${RELEASE_TOKEN}" \
|
RELEASE_RESPONSE=$(curl -s -X POST "${API_URL}/repos/${REPO}/releases" -H "Authorization: token ${RELEASE_TOKEN}" -H "Content-Type: application/json" -d "{
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"tag_name\": \"${TAG}\",
|
\"tag_name\": \"${TAG}\",
|
||||||
\"name\": \"Release ${TAG}\",
|
\"name\": \"Release ${TAG}\",
|
||||||
\"body\": \"Automated build from commit ${{ gitea.sha }}\",
|
\"body\": \"Automated build from commit ${{ gitea.sha }}\",
|
||||||
@@ -67,9 +66,15 @@ jobs:
|
|||||||
}")
|
}")
|
||||||
|
|
||||||
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||||
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then exit 1; fi
|
echo "Release ID: $RELEASE_ID"
|
||||||
|
|
||||||
curl -s -X POST "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=vibetorrent-mips" \
|
if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||||
-H "Authorization: token ${RELEASE_TOKEN}" \
|
echo "Failed to create release:"
|
||||||
-H "Content-Type: application/octet-stream" \
|
echo "$RELEASE_RESPONSE"
|
||||||
--data-binary @target/mips-unknown-linux-musl/release/vibetorrent-mips
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload binary as release asset
|
||||||
|
curl -s -X POST "${API_URL}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=vibetorrent-mips" -H "Authorization: token ${RELEASE_TOKEN}" -H "Content-Type: application/octet-stream" --data-binary @target/mips-unknown-linux-musl/release/vibetorrent-mips
|
||||||
|
|
||||||
|
echo "Release ${TAG} created with binary attached."
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@@ -2,19 +2,13 @@
|
|||||||
members = ["backend", "frontend", "shared"]
|
members = ["backend", "frontend", "shared"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
# Optimize for size (aggressive)
|
||||||
[profile.release]
|
[profile.release]
|
||||||
# En küçük binary boyutu
|
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
# En derin kod temizliği (dead code elimination)
|
lto = true
|
||||||
lto = "fat"
|
|
||||||
# En iyi optimizasyon için tek birim derleme
|
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
# Hata izleme kodlarını atarak yer kazan
|
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
# Sembolleri ve hata ayıklama bilgilerini kesin sil
|
|
||||||
strip = true
|
strip = true
|
||||||
# Artık (incremental) build'i kapat ki optimizasyon tam olsun
|
|
||||||
incremental = false
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
coarsetime = { path = "third_party/coarsetime" }
|
coarsetime = { path = "third_party/coarsetime" }
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["push-notifications", "swagger"]
|
default = ["push-notifications"]
|
||||||
push-notifications = ["web-push", "openssl"]
|
push-notifications = ["web-push", "openssl"]
|
||||||
swagger = ["utoipa-swagger-ui"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8", features = ["macros", "ws"] }
|
axum = { version = "0.8", features = ["macros", "ws"] }
|
||||||
@@ -30,7 +29,7 @@ shared = { path = "../shared" }
|
|||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"], optional = true }
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||||
web-push = { version = "0.10", default-features = false, features = ["hyper-client"], optional = true }
|
web-push = { version = "0.10", default-features = false, features = ["hyper-client"], optional = true }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
openssl = { version = "0.10", features = ["vendored"], optional = true }
|
openssl = { version = "0.10", features = ["vendored"], optional = true }
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ use tower_http::{
|
|||||||
cors::CorsLayer,
|
cors::CorsLayer,
|
||||||
trace::TraceLayer,
|
trace::TraceLayer,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "swagger")]
|
|
||||||
use utoipa::OpenApi;
|
use utoipa::OpenApi;
|
||||||
#[cfg(feature = "swagger")]
|
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -100,7 +98,6 @@ struct Args {
|
|||||||
reset_password: Option<String>,
|
reset_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "swagger")]
|
|
||||||
#[cfg(feature = "push-notifications")]
|
#[cfg(feature = "push-notifications")]
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
@@ -149,7 +146,6 @@ struct Args {
|
|||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
|
|
||||||
#[cfg(feature = "swagger")]
|
|
||||||
#[cfg(not(feature = "push-notifications"))]
|
#[cfg(not(feature = "push-notifications"))]
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
#[openapi(
|
#[openapi(
|
||||||
@@ -466,13 +462,9 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new();
|
let app = Router::new()
|
||||||
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
||||||
#[cfg(feature = "swagger")]
|
// Setup & Auth Routes
|
||||||
let app = app.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()));
|
|
||||||
|
|
||||||
// Setup & Auth Routes
|
|
||||||
let app = app
|
|
||||||
.route("/api/setup/status", get(handlers::setup::get_setup_status_handler))
|
.route("/api/setup/status", get(handlers::setup::get_setup_status_handler))
|
||||||
.route("/api/setup", post(handlers::setup::setup_handler))
|
.route("/api/setup", post(handlers::setup::setup_handler))
|
||||||
.route(
|
.route(
|
||||||
@@ -524,9 +516,9 @@ async fn main() {
|
|||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.layer(
|
.layer(
|
||||||
CompressionLayer::new()
|
CompressionLayer::new()
|
||||||
.br(false)
|
.br(true)
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.quality(CompressionLevel::Fastest),
|
.quality(CompressionLevel::Best),
|
||||||
)
|
)
|
||||||
.layer(
|
.layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
|
|||||||
@@ -52,5 +52,15 @@ web-sys = { version = "0.3", features = [
|
|||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
tailwind_fuse = "0.3.2"
|
tailwind_fuse = "0.3.2"
|
||||||
js-sys = "0.3.85"
|
js-sys = "0.3.85"
|
||||||
base64 = "0.22.1"
|
|
||||||
serde-wasm-bindgen = "0.6.5"
|
[profile.release]
|
||||||
|
# En yüksek seviyede boyut optimizasyonu
|
||||||
|
opt-level = "z"
|
||||||
|
# Tüm kütüphaneler arasında link-time optimizasyonu yapar (Çok etkilidir)
|
||||||
|
lto = true
|
||||||
|
# Kod üretim birimini 1'e düşürerek daha iyi optimizasyon sağlar
|
||||||
|
codegen-units = 1
|
||||||
|
# Hata durumunda stack unwinding yerine doğrudan durur (Kod boyutunu düşürür)
|
||||||
|
panic = "abort"
|
||||||
|
# Sembolleri ve debug bilgilerini siler
|
||||||
|
strip = true
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use wasm_bindgen::closure::Closure;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
use crate::store::{get_action_messages, show_toast_with_signal};
|
use crate::store::{get_action_messages, show_toast_with_signal};
|
||||||
use shared::NotificationLevel;
|
use shared::NotificationLevel;
|
||||||
|
|
||||||
@@ -421,50 +423,57 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
let _t_hash = t.hash.clone();
|
let _t_hash = t.hash.clone();
|
||||||
let t_hash_click = t.hash.clone();
|
let t_hash_click = t.hash.clone();
|
||||||
|
|
||||||
let (timer_handle, set_timer_handle) = create_signal(Option::<leptos::leptos_dom::helpers::TimeoutHandle>::None);
|
let (timer_id, set_timer_id) = create_signal(Option::<i32>::None);
|
||||||
let t_hash_long = t.hash.clone();
|
let t_hash_long = t.hash.clone();
|
||||||
|
|
||||||
let clear_long_press_timer = move || {
|
let clear_timer = move || {
|
||||||
if let Some(handle) = timer_handle.get_untracked() {
|
if let Some(id) = timer_id.get_untracked() {
|
||||||
handle.clear();
|
window().clear_timeout_with_handle(id);
|
||||||
set_timer_handle.set(None);
|
set_timer_id.set(None);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_touchstart = {
|
let handle_touchstart = {
|
||||||
let t_hash = t_hash_long.clone();
|
let t_hash = t_hash_long.clone();
|
||||||
move |e: web_sys::TouchEvent| {
|
move |e: web_sys::TouchEvent| {
|
||||||
clear_long_press_timer();
|
clear_timer();
|
||||||
if let Some(touch) = e.touches().get(0) {
|
if let Some(touch) = e.touches().get(0) {
|
||||||
let x = touch.client_x();
|
let x = touch.client_x();
|
||||||
let y = touch.client_y();
|
let y = touch.client_y();
|
||||||
let hash = t_hash.clone();
|
let hash = t_hash.clone();
|
||||||
|
|
||||||
// Use Leptos set_timeout: cleaner, safer, no manual Closure needed
|
let closure = Closure::wrap(Box::new(move || {
|
||||||
let handle = set_timeout_with_handle(move || {
|
|
||||||
set_menu_position.set((x, y));
|
set_menu_position.set((x, y));
|
||||||
set_selected_hash.set(Some(hash.clone()));
|
set_selected_hash.set(Some(hash.clone()));
|
||||||
set_menu_visible.set(true);
|
set_menu_visible.set(true);
|
||||||
|
|
||||||
// Haptic feedback
|
// Haptic feedback (iOS Safari doesn't support vibrate)
|
||||||
let navigator = window().navigator();
|
let navigator = window().navigator();
|
||||||
if let Ok(vibrate) = js_sys::Reflect::get(&navigator, &"vibrate".into()) {
|
if js_sys::Reflect::has(&navigator, &wasm_bindgen::JsValue::from_str("vibrate")).unwrap_or(false) {
|
||||||
if vibrate.is_function() {
|
let _ = navigator.vibrate_with_duration(50);
|
||||||
let _ = navigator.vibrate_with_duration(50);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
set_timer_handle.set(None);
|
}) as Box<dyn Fn()>);
|
||||||
}, std::time::Duration::from_millis(600));
|
|
||||||
|
|
||||||
if let Ok(h) = handle {
|
let id = window()
|
||||||
set_timer_handle.set(Some(h));
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
}
|
closure.as_ref().unchecked_ref(),
|
||||||
|
600
|
||||||
|
)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
closure.forget();
|
||||||
|
set_timer_id.set(Some(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_touchmove = move |_| clear_long_press_timer();
|
let handle_touchmove = move |_| {
|
||||||
let handle_touchend = move |_| clear_long_press_timer();
|
clear_timer();
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle_touchend = move |_| {
|
||||||
|
clear_timer();
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -328,46 +328,67 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
|
|
||||||
// First, request notification permission if not already granted
|
// First, request notification permission if not already granted
|
||||||
let window = web_sys::window().expect("window should exist");
|
let window = web_sys::window().expect("window should exist");
|
||||||
|
let permission_granted = if let Ok(notification_class) = js_sys::Reflect::get(&window, &"Notification".into()) {
|
||||||
// Notification.permission is a static property, but web_sys exposes it via the Notification class instance or we check it manually.
|
if notification_class.is_undefined() {
|
||||||
// Actually, Notification::permission() is a static method in web_sys.
|
log::error!("Notification API not available");
|
||||||
match web_sys::Notification::permission() {
|
|
||||||
web_sys::NotificationPermission::Granted => {
|
|
||||||
log::info!("Notification permission already granted");
|
|
||||||
}
|
|
||||||
web_sys::NotificationPermission::Denied => {
|
|
||||||
log::warn!("Notification permission was denied");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
web_sys::NotificationPermission::Default => {
|
|
||||||
|
// Check current permission
|
||||||
|
let current_permission = js_sys::Reflect::get(¬ification_class, &"permission".into())
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.as_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if current_permission == "granted" {
|
||||||
|
log::info!("Notification permission already granted");
|
||||||
|
true
|
||||||
|
} else if current_permission == "denied" {
|
||||||
|
log::warn!("Notification permission was denied");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Permission is "default" - need to request
|
||||||
log::info!("Requesting notification permission...");
|
log::info!("Requesting notification permission...");
|
||||||
let permission_promise = match web_sys::Notification::request_permission() {
|
if let Ok(request_fn) = js_sys::Reflect::get(¬ification_class, &"requestPermission".into()) {
|
||||||
Ok(p) => p,
|
if request_fn.is_function() {
|
||||||
Err(e) => {
|
let request_fn_typed = js_sys::Function::from(request_fn);
|
||||||
log::error!("Failed to request notification permission: {:?}", e);
|
match request_fn_typed.call0(¬ification_class) {
|
||||||
return;
|
Ok(promise_val) => {
|
||||||
}
|
let request_future = wasm_bindgen_futures::JsFuture::from(
|
||||||
};
|
js_sys::Promise::from(promise_val)
|
||||||
|
);
|
||||||
match wasm_bindgen_futures::JsFuture::from(permission_promise).await {
|
match request_future.await {
|
||||||
Ok(val) => {
|
Ok(result) => {
|
||||||
let permission = val.as_string().unwrap_or_default();
|
let result_str = result.as_string().unwrap_or_default();
|
||||||
if permission != "granted" {
|
log::info!("Permission request result: {}", result_str);
|
||||||
log::warn!("Notification permission denied by user");
|
result_str == "granted"
|
||||||
return;
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to request notification permission: {:?}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to call requestPermission: {:?}", e);
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
log::info!("Notification permission granted by user");
|
} else {
|
||||||
}
|
false
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to await notification permission: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
} else {
|
||||||
log::warn!("Unknown notification permission status");
|
log::error!("Cannot access Notification class");
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
if !permission_granted {
|
||||||
|
log::warn!("Notification permission not granted, cannot subscribe to push");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("Notification permission granted! Proceeding with push subscription...");
|
log::info!("Notification permission granted! Proceeding with push subscription...");
|
||||||
@@ -412,6 +433,7 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get service worker registration
|
// Get service worker registration
|
||||||
|
let window = web_sys::window().expect("window should exist");
|
||||||
let navigator = window.navigator();
|
let navigator = window.navigator();
|
||||||
let service_worker = navigator.service_worker();
|
let service_worker = navigator.service_worker();
|
||||||
|
|
||||||
@@ -472,45 +494,38 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
.dyn_into::<web_sys::PushSubscription>()
|
.dyn_into::<web_sys::PushSubscription>()
|
||||||
.expect("should be PushSubscription");
|
.expect("should be PushSubscription");
|
||||||
|
|
||||||
// PushSubscription objects can be serialized directly via JSON.stringify which calls their toJSON method internally.
|
// Get subscription JSON using toJSON() method
|
||||||
// Or we can use Reflect to call toJSON if we want the object directly.
|
let json_result = match js_sys::Reflect::get(&push_subscription, &"toJSON".into()) {
|
||||||
// Let's use the robust way: call toJSON via Reflect but handle it gracefully.
|
|
||||||
let json_val = match js_sys::Reflect::get(&push_subscription, &"toJSON".into()) {
|
|
||||||
Ok(func) if func.is_function() => {
|
Ok(func) if func.is_function() => {
|
||||||
let json_func = js_sys::Function::from(func);
|
let json_func = js_sys::Function::from(func);
|
||||||
match json_func.call0(&push_subscription) {
|
match json_func.call0(&push_subscription) {
|
||||||
Ok(res) => res,
|
Ok(result) => result,
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to call toJSON: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Fallback: try to stringify the object directly
|
|
||||||
// log::warn!("toJSON not found, trying JSON.stringify");
|
|
||||||
let json_str = match js_sys::JSON::stringify(&push_subscription) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to stringify subscription: {:?}", e);
|
log::error!("Failed to call toJSON: {:?}", e);
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Parse back to object to match our expected flow (slightly inefficient but safe)
|
|
||||||
match js_sys::JSON::parse(&String::from(json_str)) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to parse stringified subscription: {:?}", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
|
log::error!("toJSON method not found on PushSubscription");
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert JsValue (JSON object) to PushSubscriptionJSON struct via serde
|
let json_value = match js_sys::JSON::stringify(&json_result) {
|
||||||
// Note: web_sys::PushSubscriptionJSON is not a struct we can directly use with serde_json usually,
|
Ok(val) => val,
|
||||||
// but we can use serde-wasm-bindgen to convert JsValue -> Rust Struct
|
Err(e) => {
|
||||||
let subscription_data: PushSubscriptionData = match serde_wasm_bindgen::from_value(json_val) {
|
log::error!("Failed to stringify subscription: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let subscription_json_str = json_value.as_string().expect("should be string");
|
||||||
|
|
||||||
|
log::info!("Push subscription: {}", subscription_json_str);
|
||||||
|
|
||||||
|
// Parse and send to backend
|
||||||
|
let subscription_data: serde_json::Value = match serde_json::from_str(&subscription_json_str) {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to parse subscription JSON: {:?}", e);
|
log::error!("Failed to parse subscription JSON: {:?}", e);
|
||||||
@@ -518,9 +533,37 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send to backend (subscription_data is already the struct we need)
|
// Extract endpoint and keys
|
||||||
|
let endpoint = subscription_data
|
||||||
|
.get("endpoint")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.expect("endpoint should exist")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let keys_obj = subscription_data
|
||||||
|
.get("keys")
|
||||||
|
.expect("keys should exist");
|
||||||
|
|
||||||
|
let p256dh = keys_obj
|
||||||
|
.get("p256dh")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.expect("p256dh should exist")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let auth = keys_obj
|
||||||
|
.get("auth")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.expect("auth should exist")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let push_data = PushSubscriptionData {
|
||||||
|
endpoint,
|
||||||
|
keys: PushKeys { p256dh, auth },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send to backend
|
||||||
let response = match Request::post("/api/push/subscribe")
|
let response = match Request::post("/api/push/subscribe")
|
||||||
.json(&subscription_data)
|
.json(&push_data)
|
||||||
.expect("serialization should succeed")
|
.expect("serialization should succeed")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -540,15 +583,34 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to convert URL-safe base64 string to Uint8Array
|
/// Helper to convert URL-safe base64 string to Uint8Array
|
||||||
/// Uses pure Rust base64 crate for better safety and performance
|
/// Uses JavaScript to properly decode binary data (avoids UTF-8 encoding issues)
|
||||||
fn url_base64_to_uint8array(base64_string: &str) -> Result<js_sys::Uint8Array, JsValue> {
|
fn url_base64_to_uint8array(base64_string: &str) -> Result<js_sys::Uint8Array, JsValue> {
|
||||||
use base64::{engine::general_purpose, Engine as _};
|
// Add padding
|
||||||
|
let padding = (4 - (base64_string.len() % 4)) % 4;
|
||||||
|
let mut padded = base64_string.to_string();
|
||||||
|
padded.push_str(&"=".repeat(padding));
|
||||||
|
|
||||||
// VAPID keys are URL-safe base64. Try both NO_PAD and padded for robustness.
|
// Replace URL-safe characters
|
||||||
let bytes = general_purpose::URL_SAFE_NO_PAD
|
let standard_base64 = padded.replace('-', "+").replace('_', "/");
|
||||||
.decode(base64_string)
|
|
||||||
.or_else(|_| general_purpose::URL_SAFE.decode(base64_string))
|
|
||||||
.map_err(|e| JsValue::from_str(&format!("Base64 decode error: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(js_sys::Uint8Array::from(&bytes[..]))
|
// Decode using JavaScript to avoid UTF-8 encoding issues
|
||||||
|
// Create a JavaScript function to decode the base64 and convert to Uint8Array
|
||||||
|
let js_code = format!(
|
||||||
|
r#"
|
||||||
|
(function() {{
|
||||||
|
const binaryString = atob('{}');
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {{
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}}
|
||||||
|
return bytes;
|
||||||
|
}})()
|
||||||
|
"#,
|
||||||
|
standard_base64
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = js_sys::eval(&js_code)?;
|
||||||
|
let array = result.dyn_into::<js_sys::Uint8Array>()?;
|
||||||
|
|
||||||
|
Ok(array)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user