electron
This commit is contained in:
@@ -1,133 +0,0 @@
|
||||
//! Background sync module for Matrix client
|
||||
//!
|
||||
//! This module provides native Rust-based Matrix sync that runs independently
|
||||
//! of the WebView, allowing notifications to work even when the app is backgrounded.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Credentials needed to connect to Matrix homeserver
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MatrixCredentials {
|
||||
pub homeserver_url: String,
|
||||
pub user_id: String,
|
||||
pub access_token: String,
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
/// State of the background sync
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SyncState {
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
/// Background sync manager that handles Matrix sync in native Rust
|
||||
pub struct BackgroundSyncManager {
|
||||
credentials: RwLock<Option<MatrixCredentials>>,
|
||||
sync_state: RwLock<SyncState>,
|
||||
stop_flag: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl BackgroundSyncManager {
|
||||
/// Creates a new BackgroundSyncManager
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
credentials: RwLock::new(None),
|
||||
sync_state: RwLock::new(SyncState::Stopped),
|
||||
stop_flag: Mutex::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the Matrix credentials for syncing
|
||||
pub async fn set_credentials(&self, credentials: MatrixCredentials) {
|
||||
let mut creds = self.credentials.write().await;
|
||||
*creds = Some(credentials);
|
||||
}
|
||||
|
||||
/// Clears the stored credentials
|
||||
pub async fn clear_credentials(&self) {
|
||||
let mut creds = self.credentials.write().await;
|
||||
*creds = None;
|
||||
}
|
||||
|
||||
/// Gets the current sync state
|
||||
pub async fn get_state(&self) -> SyncState {
|
||||
self.sync_state.read().await.clone()
|
||||
}
|
||||
|
||||
/// Starts the background sync
|
||||
pub async fn start_sync(&self) -> Result<(), String> {
|
||||
// Check if we have credentials
|
||||
let creds = self.credentials.read().await;
|
||||
let credentials = creds.as_ref().ok_or("No credentials set")?;
|
||||
|
||||
// Update state
|
||||
{
|
||||
let mut state = self.sync_state.write().await;
|
||||
*state = SyncState::Starting;
|
||||
}
|
||||
|
||||
// Reset stop flag
|
||||
{
|
||||
let mut stop = self.stop_flag.lock().await;
|
||||
*stop = false;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Background sync starting for user: {}",
|
||||
credentials.user_id
|
||||
);
|
||||
|
||||
// Update state to running
|
||||
{
|
||||
let mut state = self.sync_state.write().await;
|
||||
*state = SyncState::Running;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops the background sync
|
||||
pub async fn stop_sync(&self) {
|
||||
{
|
||||
let mut stop = self.stop_flag.lock().await;
|
||||
*stop = true;
|
||||
}
|
||||
|
||||
let mut state = self.sync_state.write().await;
|
||||
*state = SyncState::Stopped;
|
||||
|
||||
log::info!("Background sync stopped");
|
||||
}
|
||||
|
||||
/// Checks if sync should stop
|
||||
pub async fn should_stop(&self) -> bool {
|
||||
*self.stop_flag.lock().await
|
||||
}
|
||||
|
||||
/// Sets the sync state to an error
|
||||
pub async fn set_error(&self, error: String) {
|
||||
let mut state = self.sync_state.write().await;
|
||||
*state = SyncState::Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BackgroundSyncManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Global instance of the background sync manager
|
||||
static SYNC_MANAGER: std::sync::OnceLock<Arc<BackgroundSyncManager>> = std::sync::OnceLock::new();
|
||||
|
||||
/// Gets or creates the global sync manager instance
|
||||
pub fn get_sync_manager() -> Arc<BackgroundSyncManager> {
|
||||
SYNC_MANAGER
|
||||
.get_or_init(|| Arc::new(BackgroundSyncManager::new()))
|
||||
.clone()
|
||||
}
|
||||
@@ -1,475 +0,0 @@
|
||||
//! Paarrot Desktop library for cross-platform builds including Android
|
||||
|
||||
#[cfg(mobile)]
|
||||
mod mobile;
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
mod background_sync;
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
mod matrix_sync;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
use tauri::{
|
||||
Manager,
|
||||
WebviewUrl,
|
||||
menu::{Menu, MenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
webview::WebviewWindowBuilder,
|
||||
WindowEvent,
|
||||
};
|
||||
|
||||
// Linux: Import for permission handling
|
||||
#[cfg(target_os = "linux")]
|
||||
use webkit2gtk::{PermissionRequestExt, WebViewExt};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gtk::prelude::*;
|
||||
|
||||
/// Read image from clipboard on Linux using arboard with Wayland support
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tauri::command]
|
||||
fn read_clipboard_image() -> Result<Option<String>, String> {
|
||||
use arboard::Clipboard;
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
|
||||
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
|
||||
|
||||
match clipboard.get_image() {
|
||||
Ok(img) => {
|
||||
// Convert RGBA image data to PNG
|
||||
let width = img.width as u32;
|
||||
let height = img.height as u32;
|
||||
|
||||
let mut png_data = Vec::new();
|
||||
{
|
||||
let mut encoder = png::Encoder::new(&mut png_data, width, height);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header().map_err(|e| e.to_string())?;
|
||||
writer.write_image_data(&img.bytes).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
let base64_data = BASE64.encode(&png_data);
|
||||
Ok(Some(format!("data:image/png;base64,{}", base64_data)))
|
||||
}
|
||||
Err(_) => Ok(None), // No image in clipboard
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub for non-Linux platforms - returns None
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[tauri::command]
|
||||
fn read_clipboard_image() -> Result<Option<String>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Open a URL in the default browser (bypasses ACL issues with localhost plugin)
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
fn open_external_url(url: String) -> Result<(), String> {
|
||||
open::that(&url).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Open a URL in the default browser on mobile platforms
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[tauri::command]
|
||||
fn open_external_url(url: String) -> Result<(), String> {
|
||||
tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// YouTube stream info returned by yt-dlp
|
||||
#[derive(serde::Serialize)]
|
||||
struct YouTubeStreamInfo {
|
||||
/// Direct video stream URL (may include video+audio or video only)
|
||||
video_url: String,
|
||||
/// Title of the video
|
||||
title: String,
|
||||
}
|
||||
|
||||
/// Extract direct YouTube stream URL using yt-dlp
|
||||
/// Requires yt-dlp to be installed and available in PATH
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn get_youtube_stream(url: String) -> Result<YouTubeStreamInfo, String> {
|
||||
use std::process::Command;
|
||||
|
||||
// First get the title
|
||||
let title_output = Command::new("yt-dlp")
|
||||
.args(["--get-title", &url])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run yt-dlp (is it installed?): {}", e))?;
|
||||
|
||||
let title = if title_output.status.success() {
|
||||
String::from_utf8_lossy(&title_output.stdout).trim().to_string()
|
||||
} else {
|
||||
"YouTube Video".to_string()
|
||||
};
|
||||
|
||||
// Get the best format with video+audio combined (up to 1080p)
|
||||
// -f "best[height<=1080]" gets combined format
|
||||
// Fallback to "bestvideo[height<=1080]+bestaudio/best" for separate streams
|
||||
let output = Command::new("yt-dlp")
|
||||
.args([
|
||||
"-g", // Get URL only
|
||||
"-f", "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best",
|
||||
&url
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run yt-dlp: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("yt-dlp error: {}", stderr));
|
||||
}
|
||||
|
||||
let video_url = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if video_url.is_empty() {
|
||||
return Err("yt-dlp returned empty URL".to_string());
|
||||
}
|
||||
|
||||
Ok(YouTubeStreamInfo { video_url, title })
|
||||
}
|
||||
|
||||
/// Stub for mobile platforms - YouTube streaming not supported
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[tauri::command]
|
||||
async fn get_youtube_stream(_url: String) -> Result<YouTubeStreamInfo, String> {
|
||||
Err("YouTube streaming not supported on mobile".to_string())
|
||||
}
|
||||
|
||||
/// Start background Matrix sync with the given credentials (mobile only)
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[tauri::command]
|
||||
async fn start_background_sync(
|
||||
app_handle: tauri::AppHandle,
|
||||
homeserver_url: String,
|
||||
user_id: String,
|
||||
access_token: String,
|
||||
device_id: String,
|
||||
) -> Result<(), String> {
|
||||
use crate::background_sync::{MatrixCredentials, get_sync_manager};
|
||||
use crate::matrix_sync::{init_client, run_sync_loop};
|
||||
|
||||
let credentials = MatrixCredentials {
|
||||
homeserver_url,
|
||||
user_id,
|
||||
access_token,
|
||||
device_id,
|
||||
};
|
||||
|
||||
// Set credentials
|
||||
let manager = get_sync_manager();
|
||||
manager.set_credentials(credentials.clone()).await;
|
||||
|
||||
// Initialize the Matrix client
|
||||
init_client(&credentials).await?;
|
||||
|
||||
// Start sync in background task
|
||||
let app = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = run_sync_loop(app).await {
|
||||
log::error!("Sync loop error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop background Matrix sync (mobile only)
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[tauri::command]
|
||||
async fn stop_background_sync() -> Result<(), String> {
|
||||
crate::matrix_sync::stop_sync().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get background sync state (mobile only)
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
#[tauri::command]
|
||||
async fn get_background_sync_state() -> Result<String, String> {
|
||||
use crate::background_sync::get_sync_manager;
|
||||
|
||||
let manager = get_sync_manager();
|
||||
let state = manager.get_state().await;
|
||||
|
||||
Ok(format!("{:?}", state))
|
||||
}
|
||||
|
||||
/// Stub commands for desktop (no-op since background sync is mobile-only)
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn start_background_sync(
|
||||
_app_handle: tauri::AppHandle,
|
||||
_homeserver_url: String,
|
||||
_user_id: String,
|
||||
_access_token: String,
|
||||
_device_id: String,
|
||||
) -> Result<(), String> {
|
||||
Ok(()) // Desktop doesn't need background sync
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn stop_background_sync() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn get_background_sync_state() -> Result<String, String> {
|
||||
Ok("NotApplicable".to_string())
|
||||
}
|
||||
|
||||
/// Window control commands for custom decorations
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn window_minimize(window: tauri::Window) -> Result<(), String> {
|
||||
window.minimize().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn window_maximize(window: tauri::Window) -> Result<(), String> {
|
||||
window.maximize().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn window_unmaximize(window: tauri::Window) -> Result<(), String> {
|
||||
window.unmaximize().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn window_close(window: tauri::Window) -> Result<(), String> {
|
||||
window.close().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn window_is_maximized(window: tauri::Window) -> Result<bool, String> {
|
||||
window.is_maximized().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
#[tauri::command]
|
||||
async fn window_start_drag(window: tauri::Window) -> Result<(), String> {
|
||||
window.start_dragging().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Runs the Tauri application
|
||||
pub fn run() {
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
let port = 44548;
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_localhost::Builder::new(port).build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
// When a second instance tries to launch, focus the existing window
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}))
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
Some(vec!["--minimized"]),
|
||||
))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.setup(move |app| {
|
||||
// In dev mode, use Vite dev server; in production, use localhost plugin
|
||||
let url: tauri::Url = if cfg!(dev) {
|
||||
"http://localhost:8080".parse().unwrap()
|
||||
} else {
|
||||
format!("http://localhost:{}", port).parse().unwrap()
|
||||
};
|
||||
|
||||
// Create the main window manually with navigation handler for external links
|
||||
let window = WebviewWindowBuilder::new(app, "main", WebviewUrl::External(url))
|
||||
.title("Paarrot")
|
||||
.inner_size(1280.0, 905.0)
|
||||
.center()
|
||||
.resizable(true)
|
||||
.decorations(false)
|
||||
.disable_drag_drop_handler()
|
||||
.on_navigation(|url| {
|
||||
let url_str = url.as_str();
|
||||
// Allow navigation to localhost (our app) and special protocols
|
||||
if url_str.starts_with("http://localhost")
|
||||
|| url_str.starts_with("https://localhost")
|
||||
|| url_str.starts_with("tauri://")
|
||||
|| url_str.starts_with("blob:")
|
||||
|| url_str.starts_with("data:")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// Block external URLs - open them in default browser
|
||||
if url_str.starts_with("http://") || url_str.starts_with("https://") {
|
||||
let _ = tauri_plugin_opener::open_url(url_str, None::<&str>);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.build()?;
|
||||
|
||||
// Explicitly ensure decorations are disabled (important for Linux)
|
||||
window.set_decorations(false)?;
|
||||
|
||||
// Linux: Set up permission handler to auto-allow microphone/camera access
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(webview_window) = app.get_webview_window("main") {
|
||||
let _ = webview_window.with_webview(|webview| {
|
||||
use webkit2gtk::UserMediaPermissionRequestExt;
|
||||
|
||||
let wv = webview.inner();
|
||||
wv.connect_permission_request(|_webview, permission_request| {
|
||||
// Check if this is a user media (microphone/camera) permission request
|
||||
if let Some(user_media_request) = permission_request.downcast_ref::<webkit2gtk::UserMediaPermissionRequest>() {
|
||||
// Log what's being requested
|
||||
let is_audio = user_media_request.is_for_audio_device();
|
||||
let is_video = user_media_request.is_for_video_device();
|
||||
eprintln!("Paarrot: Media permission request - audio: {}, video: {}", is_audio, is_video);
|
||||
|
||||
// Allow the request
|
||||
permission_request.allow();
|
||||
return true;
|
||||
}
|
||||
|
||||
// For other permission types, allow by default
|
||||
permission_request.allow();
|
||||
true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create system tray
|
||||
let show_item = MenuItem::with_id(app, "show", "Show Paarrot", true, None::<&str>)?;
|
||||
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&show_item, &quit_item])?;
|
||||
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let app = tray.app_handle();
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
// Force decorations off one final time after all plugins have initialized
|
||||
// This ensures window-state plugin doesn't override our settings
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.set_decorations(false);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
// Minimize to tray on close instead of quitting
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
window.hide().unwrap();
|
||||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
read_clipboard_image,
|
||||
open_external_url,
|
||||
get_youtube_stream,
|
||||
start_background_sync,
|
||||
stop_background_sync,
|
||||
get_background_sync_state,
|
||||
window_minimize,
|
||||
window_maximize,
|
||||
window_unmaximize,
|
||||
window_close,
|
||||
window_is_maximized,
|
||||
window_start_drag
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
use tauri::{WebviewUrl, webview::WebviewWindowBuilder};
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.setup(|app| {
|
||||
// Create the main window for mobile with navigation handler for external links
|
||||
WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
|
||||
.on_navigation(|url| {
|
||||
let url_str = url.as_str();
|
||||
// Allow navigation to app resources and special protocols
|
||||
if url_str.starts_with("tauri://")
|
||||
|| url_str.starts_with("http://tauri.localhost")
|
||||
|| url_str.starts_with("https://tauri.localhost")
|
||||
|| url_str.starts_with("blob:")
|
||||
|| url_str.starts_with("data:")
|
||||
|| url_str.starts_with("about:")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// External URLs - open in default browser
|
||||
if url_str.starts_with("http://") || url_str.starts_with("https://") {
|
||||
let _ = tauri_plugin_opener::open_url(url_str, None::<&str>);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.build()?;
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
read_clipboard_image,
|
||||
open_external_url,
|
||||
get_youtube_stream,
|
||||
start_background_sync,
|
||||
stop_background_sync,
|
||||
get_background_sync_state
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
fn main() {
|
||||
paarrot_lib::run();
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
//! Matrix sync implementation using matrix-sdk
|
||||
//!
|
||||
//! This module handles the actual Matrix sync loop and notification triggering.
|
||||
|
||||
use matrix_sdk::{
|
||||
Client,
|
||||
config::SyncSettings,
|
||||
matrix_auth::MatrixSession,
|
||||
ruma::{
|
||||
events::room::message::{MessageType, SyncRoomMessageEvent},
|
||||
OwnedUserId, OwnedDeviceId,
|
||||
},
|
||||
};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::background_sync::{MatrixCredentials, get_sync_manager};
|
||||
|
||||
/// Active Matrix client for background sync
|
||||
static MATRIX_CLIENT: std::sync::OnceLock<RwLock<Option<Client>>> = std::sync::OnceLock::new();
|
||||
|
||||
fn get_client_lock() -> &'static RwLock<Option<Client>> {
|
||||
MATRIX_CLIENT.get_or_init(|| RwLock::new(None))
|
||||
}
|
||||
|
||||
/// Initializes the Matrix client with the given credentials
|
||||
pub async fn init_client(credentials: &MatrixCredentials) -> Result<Client, String> {
|
||||
// Pass the URL string directly - ClientBuilder::homeserver_url accepts impl AsRef<str>
|
||||
let client = Client::builder()
|
||||
.homeserver_url(&credentials.homeserver_url)
|
||||
.build()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to build client: {}", e))?;
|
||||
|
||||
// Restore the session
|
||||
let user_id: OwnedUserId = credentials.user_id.parse()
|
||||
.map_err(|e| format!("Invalid user ID: {}", e))?;
|
||||
|
||||
let device_id: OwnedDeviceId = credentials.device_id.clone().into();
|
||||
|
||||
let session = MatrixSession {
|
||||
meta: matrix_sdk::SessionMeta {
|
||||
user_id,
|
||||
device_id,
|
||||
},
|
||||
tokens: matrix_sdk::matrix_auth::MatrixSessionTokens {
|
||||
access_token: credentials.access_token.clone(),
|
||||
refresh_token: None,
|
||||
},
|
||||
};
|
||||
|
||||
client.matrix_auth().restore_session(session).await
|
||||
.map_err(|e| format!("Failed to restore session: {}", e))?;
|
||||
|
||||
// Store the client
|
||||
{
|
||||
let mut lock = get_client_lock().write().await;
|
||||
*lock = Some(client.clone());
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Runs the sync loop and triggers notifications for new messages
|
||||
pub async fn run_sync_loop<R: tauri::Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
let manager = get_sync_manager();
|
||||
|
||||
let client = {
|
||||
let lock = get_client_lock().read().await;
|
||||
lock.clone().ok_or("Client not initialized")?
|
||||
};
|
||||
|
||||
let own_user_id = client.user_id()
|
||||
.ok_or("Not logged in")?
|
||||
.to_owned();
|
||||
|
||||
log::info!("Starting sync loop for {}", own_user_id);
|
||||
|
||||
// Set up event handler for room messages
|
||||
let app_handle_clone = app_handle.clone();
|
||||
let own_user_clone = own_user_id.clone();
|
||||
|
||||
client.add_event_handler(move |event: SyncRoomMessageEvent, room: matrix_sdk::Room| {
|
||||
let app = app_handle_clone.clone();
|
||||
let own_user = own_user_clone.clone();
|
||||
|
||||
async move {
|
||||
// Only process original messages (not edits/reactions)
|
||||
if let SyncRoomMessageEvent::Original(original) = event {
|
||||
// Don't notify for our own messages
|
||||
if original.sender == own_user {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get room name for notification
|
||||
let room_name = room.display_name().await
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|_| "Unknown room".to_string());
|
||||
|
||||
// Get sender display name
|
||||
let sender_name = room.get_member(&original.sender).await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|m| m.display_name().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| original.sender.to_string());
|
||||
|
||||
// Get message body
|
||||
let body = match &original.content.msgtype {
|
||||
MessageType::Text(text) => text.body.clone(),
|
||||
MessageType::Image(_) => "📷 Image".to_string(),
|
||||
MessageType::Video(_) => "🎥 Video".to_string(),
|
||||
MessageType::Audio(_) => "🎵 Audio".to_string(),
|
||||
MessageType::File(_) => "📎 File".to_string(),
|
||||
MessageType::Location(_) => "📍 Location".to_string(),
|
||||
MessageType::Emote(emote) => format!("* {} {}", sender_name, emote.body),
|
||||
_ => "New message".to_string(),
|
||||
};
|
||||
|
||||
// Send notification
|
||||
let title = format!("{} in {}", sender_name, room_name);
|
||||
|
||||
if let Err(e) = app.notification()
|
||||
.builder()
|
||||
.title(&title)
|
||||
.body(&body)
|
||||
.show()
|
||||
{
|
||||
log::error!("Failed to show notification: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Run the sync loop
|
||||
let settings = SyncSettings::default();
|
||||
|
||||
loop {
|
||||
// Check if we should stop
|
||||
if manager.should_stop().await {
|
||||
log::info!("Sync loop stopping due to stop flag");
|
||||
break;
|
||||
}
|
||||
|
||||
// Perform one sync iteration
|
||||
match client.sync_once(settings.clone()).await {
|
||||
Ok(response) => {
|
||||
log::debug!("Sync completed, next_batch: {}", response.next_batch);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Sync error: {}", e);
|
||||
// Update state to error
|
||||
manager.set_error(e.to_string()).await;
|
||||
// Wait a bit before retrying
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between syncs to avoid hammering the server
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops the sync and cleans up
|
||||
pub async fn stop_sync() {
|
||||
let manager = get_sync_manager();
|
||||
manager.stop_sync().await;
|
||||
|
||||
// Clear the client
|
||||
let mut lock = get_client_lock().write().await;
|
||||
*lock = None;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//! Mobile-specific entry points for Android and iOS
|
||||
|
||||
/// Mobile entry point
|
||||
#[tauri::mobile_entry_point]
|
||||
fn main() {
|
||||
crate::run();
|
||||
}
|
||||
Reference in New Issue
Block a user