lingxia-shell 0.5.0

Shell product module and host registrations for LingXia
use crate::host::{HostCancel, HostResult, StreamContext, await_or_cancel};
use crate::platform_error::map_platform_error;
use lingxia_platform::PlatformError;
use lingxia_platform::traits::app_runtime::{AppRuntime, OpenUrlRequest, OpenUrlTarget};
use lingxia_platform::traits::file::{FileService, OpenFileRequest, RevealInFileManagerRequest};
use lingxia_transfer::{
    DownloadEvent, DownloadRecord, DownloadStatus, DownloadsError, DownloadsSnapshot,
};
use lxapp::LxApp;
use lxapp::LxAppError;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::broadcast;

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ClearCompletedResult {
    removed: u64,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DownloadTaskIdInput {
    task_id: String,
}

fn file_url_for(path: &Path) -> String {
    format!("file://{}", path.to_string_lossy())
}

fn download_reveal_path(record: &DownloadRecord) -> PathBuf {
    let target_path = PathBuf::from(&record.target_path);
    let part_path = target_path.with_extension("part");

    match record.status {
        DownloadStatus::Downloading => {
            if part_path.exists() {
                return part_path;
            }
            if target_path.exists() {
                return target_path;
            }
        }
        DownloadStatus::Completed => {
            if target_path.exists() {
                return target_path;
            }
            if part_path.exists() {
                return part_path;
            }
        }
        DownloadStatus::Failed => {
            if part_path.exists() {
                return part_path;
            }
            if target_path.exists() {
                return target_path;
            }
        }
        DownloadStatus::Removed => {
            if target_path.exists() {
                return target_path;
            }
            if part_path.exists() {
                return part_path;
            }
        }
    }

    target_path
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or(target_path)
}

fn download_fallback_dir(path: &Path) -> Result<PathBuf, LxAppError> {
    if path.is_dir() {
        return Ok(path.to_path_buf());
    }
    path.parent()
        .map(Path::to_path_buf)
        .ok_or_else(|| LxAppError::InvalidParameter("download has no parent directory".to_string()))
}

async fn open_file_for_user(app: &LxApp, request: OpenFileRequest) -> Result<(), PlatformError> {
    match app.runtime.review_file(request.clone()).await {
        Ok(()) => Ok(()),
        Err(_review_error) => app.runtime.open_external(request).await,
    }
}

fn map_downloads_error(err: DownloadsError) -> LxAppError {
    match err {
        DownloadsError::InvalidParameter(message) => LxAppError::InvalidParameter(message),
        DownloadsError::ResourceNotFound(message) => LxAppError::ResourceNotFound(message),
        DownloadsError::UnsupportedOperation(message) => LxAppError::UnsupportedOperation(message),
        DownloadsError::Runtime(message) => LxAppError::Runtime(message),
        DownloadsError::Io(err) => LxAppError::IoError(err.to_string()),
        DownloadsError::Json(err) => LxAppError::Bridge(format!("JSON Processing Error: {err}")),
        DownloadsError::Settings(err) => LxAppError::Runtime(err.to_string()),
    }
}

#[lingxia::host("downloads.list")]
fn list_downloads(app: Arc<LxApp>) -> HostResult<DownloadsSnapshot> {
    Ok(lingxia_transfer::snapshot(&app.app_data_dir()).map_err(map_downloads_error)?)
}

#[lingxia::host("downloads.clearCompleted")]
fn clear_completed_downloads(app: Arc<LxApp>) -> HostResult<ClearCompletedResult> {
    let removed =
        lingxia_transfer::clear_completed(&app.app_data_dir()).map_err(map_downloads_error)?;
    Ok(ClearCompletedResult { removed })
}

#[lingxia::host("downloads.remove")]
fn remove_download_route(app: Arc<LxApp>, input: DownloadTaskIdInput) -> HostResult<()> {
    if input.task_id.trim().is_empty() {
        return Err(LxAppError::InvalidParameter(
            "downloads.remove requires taskId".to_string(),
        ));
    }
    lingxia_transfer::remove(&app.app_data_dir(), &input.task_id).map_err(map_downloads_error)?;
    Ok(())
}

#[lingxia::host("downloads.cancel")]
fn cancel_download_route(app: Arc<LxApp>, input: DownloadTaskIdInput) -> HostResult<()> {
    if input.task_id.trim().is_empty() {
        return Err(LxAppError::InvalidParameter(
            "downloads.cancel requires taskId".to_string(),
        ));
    }
    lingxia_transfer::cancel(&app.app_data_dir(), &input.task_id).map_err(map_downloads_error)
}

#[lingxia::host("downloads.retry")]
fn retry_download_route(app: Arc<LxApp>, input: DownloadTaskIdInput) -> HostResult<()> {
    if input.task_id.trim().is_empty() {
        return Err(LxAppError::InvalidParameter(
            "downloads.retry requires taskId".to_string(),
        ));
    }
    lingxia_transfer::retry(&app.app_data_dir(), &input.task_id).map_err(map_downloads_error)
}

#[lingxia::host("downloads.open")]
async fn open_download_route(
    app: Arc<LxApp>,
    input: DownloadTaskIdInput,
    mut cancel: HostCancel,
) -> HostResult<()> {
    if input.task_id.trim().is_empty() {
        return Err(LxAppError::InvalidParameter(
            "downloads.open requires taskId".to_string(),
        ));
    }

    let record = lingxia_transfer::record(&app.app_data_dir(), &input.task_id)
        .map_err(map_downloads_error)?
        .ok_or_else(|| {
            LxAppError::ResourceNotFound(format!("download not found: {}", input.task_id))
        })?;
    if record.status != DownloadStatus::Completed {
        return Err(LxAppError::UnsupportedOperation(
            "download is not completed".to_string(),
        ));
    }

    await_or_cancel(&mut cancel, async move {
        open_file_for_user(
            &app,
            OpenFileRequest {
                path: record.target_path,
                mime_type: record.mime_type,
                show_menu: Some(false),
            },
        )
        .await
        .map_err(|e| map_platform_error("downloads.open", e))
    })
    .await
}

#[lingxia::host("downloads.reveal")]
async fn reveal_download_route(
    app: Arc<LxApp>,
    input: DownloadTaskIdInput,
    mut cancel: HostCancel,
) -> HostResult<()> {
    if input.task_id.trim().is_empty() {
        return Err(LxAppError::InvalidParameter(
            "downloads.reveal requires taskId".to_string(),
        ));
    }

    let record = lingxia_transfer::record(&app.app_data_dir(), &input.task_id)
        .map_err(map_downloads_error)?
        .ok_or_else(|| {
            LxAppError::ResourceNotFound(format!("download not found: {}", input.task_id))
        })?;
    let reveal_path = download_reveal_path(&record);
    let fallback_dir = download_fallback_dir(&reveal_path)?;

    await_or_cancel(&mut cancel, async move {
        match app
            .runtime
            .reveal_in_file_manager(RevealInFileManagerRequest {
                path: reveal_path.to_string_lossy().to_string(),
            })
            .await
        {
            Ok(()) => Ok(()),
            Err(PlatformError::NotSupported(_)) => app
                .runtime
                .open_url(OpenUrlRequest {
                    owner_appid: app.appid.clone(),
                    owner_session_id: app.session_id(),
                    url: file_url_for(&fallback_dir),
                    target: OpenUrlTarget::External,
                })
                .map_err(|e| map_platform_error("downloads.reveal", e)),
            Err(e) => Err(map_platform_error("downloads.reveal", e)),
        }
    })
    .await
}

#[lingxia::host("downloads.watch", stream)]
async fn watch_downloads(
    app: Arc<LxApp>,
    mut stream: StreamContext<DownloadEvent>,
) -> HostResult<()> {
    let mut rx: broadcast::Receiver<DownloadEvent> =
        lingxia_transfer::subscribe(&app.app_data_dir()).map_err(map_downloads_error)?;

    loop {
        tokio::select! {
            _ = stream.canceled() => return Ok(()),
            recv = rx.recv() => {
                match recv {
                    Ok(event) => stream.send(event)?,
                    Err(broadcast::error::RecvError::Lagged(skipped)) => {
                        return Err(LxAppError::Bridge(format!(
                            "download stream lagged by {skipped} events"
                        )));
                    }
                    Err(broadcast::error::RecvError::Closed) => return stream.end(()),
                }
            }
        }
    }
}

pub(crate) fn register() {
    crate::register_hosts![
        list_downloads,
        clear_completed_downloads,
        remove_download_route,
        cancel_download_route,
        retry_download_route,
        open_download_route,
        reveal_download_route,
        watch_downloads,
    ];
}