chore: initialiser le projet imagod-web (Rust)
- Structure du projet Cargo avec dépendances (clap, axum, reqwest, rusqlite, arboard, tray-icon) - CLI avec sous-commandes start, serve, stop, status, config, install, uninstall, logs - Module config: parsing TOML, chemins Windows/Linux - Module dedup: dédoublonnage SHA-256 avec cache SQLite - Module imago_client: client HTTP pour API Imago (upload, search, get) - Module watcher: boucle de polling stub - Module server: serveur web axum avec routes API REST stub - Platform-specific: clipboard, tray, service, window_info (Windows) - Frontend placeholder HTML - Config par défaut (imagod.toml) - README.md, LICENSE (MIT), .gitignore
This commit is contained in:
+80
@@ -0,0 +1,80 @@
|
||||
//! Parsing CLI avec clap
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "imagod-web",
|
||||
version = env!("CARGO_PKG_VERSION"),
|
||||
about = "Imago Daemon + Dashboard — Capture automatique de screenshots + Dashboard web embarqué",
|
||||
long_about = "Service de capture automatique de screenshots vers Imago avec dashboard web embarqué.\n\n\
|
||||
Modes :\n \
|
||||
Windows : imagod-web start → watcher clipboard + dashboard :9090\n \
|
||||
Linux : imagod-web serve --port 9090 --no-watcher → dashboard uniquement"
|
||||
)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Lancer le watcher + dashboard (Windows uniquement)
|
||||
Start {
|
||||
/// Port du dashboard web
|
||||
#[arg(long, default_value = "9090")]
|
||||
port: u16,
|
||||
/// Ouvrir le dashboard dans le navigateur
|
||||
#[arg(long)]
|
||||
open_browser: bool,
|
||||
},
|
||||
/// Lancer le dashboard web uniquement (Linux/serveur)
|
||||
Serve {
|
||||
/// Port du dashboard web
|
||||
#[arg(long, default_value = "9090")]
|
||||
port: u16,
|
||||
/// Interface d'écoute
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
/// Désactiver le watcher clipboard
|
||||
#[arg(long)]
|
||||
no_watcher: bool,
|
||||
/// Ouvrir le dashboard dans le navigateur
|
||||
#[arg(long)]
|
||||
open_browser: bool,
|
||||
},
|
||||
/// Arrêter le service
|
||||
Stop,
|
||||
/// Afficher l'état du service
|
||||
Status,
|
||||
/// Gérer la configuration
|
||||
Config {
|
||||
#[command(subcommand)]
|
||||
action: ConfigAction,
|
||||
},
|
||||
/// Installer comme service Windows
|
||||
Install,
|
||||
/// Désinstaller le service Windows
|
||||
Uninstall,
|
||||
/// Afficher les derniers logs
|
||||
Logs,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ConfigAction {
|
||||
/// Afficher la configuration actuelle
|
||||
Show,
|
||||
/// Modifier un paramètre
|
||||
Set {
|
||||
/// Clé de configuration (ex: imago.url)
|
||||
key: String,
|
||||
/// Valeur
|
||||
value: String,
|
||||
},
|
||||
/// Réinitialiser la configuration par défaut
|
||||
Reset,
|
||||
}
|
||||
|
||||
pub fn parse() -> Args {
|
||||
Args::parse()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Clipboard — Abstraction arboard (Windows)
|
||||
//!
|
||||
//! Phase 3: Lecture du clipboard Windows, détection d'images
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Vérifie si le clipboard contient une image
|
||||
pub fn has_image() -> Result<bool> {
|
||||
// Phase 3: Implémentation arboard
|
||||
todo!("Phase 3: Clipboard detection")
|
||||
}
|
||||
|
||||
/// Récupère l'image du clipboard en bytes
|
||||
pub fn get_image_bytes() -> Result<Vec<u8>> {
|
||||
todo!("Phase 3: Clipboard image extraction")
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
//! Gestion de la configuration TOML
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Configuration complète d'imagod-web
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub imago: ImagoConfig,
|
||||
#[serde(default)]
|
||||
pub watcher: WatcherConfig,
|
||||
#[serde(default)]
|
||||
pub dedup: DedupConfig,
|
||||
#[serde(default)]
|
||||
pub ui: UiConfig,
|
||||
#[serde(default)]
|
||||
pub logging: LoggingConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImagoConfig {
|
||||
pub url: String,
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatcherConfig {
|
||||
pub capture_images: bool,
|
||||
pub poll_interval_ms: u64,
|
||||
pub filters: WatcherFilters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WatcherFilters {
|
||||
pub only_apps: Vec<String>,
|
||||
pub exclude_apps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DedupConfig {
|
||||
pub method: String,
|
||||
pub cache_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UiConfig {
|
||||
pub show_notifications: bool,
|
||||
pub show_tray_icon: bool,
|
||||
pub notification_timeout_sec: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
imago: ImagoConfig {
|
||||
url: "http://imago.dracodev.net:8000".into(),
|
||||
api_key: String::new(),
|
||||
},
|
||||
watcher: WatcherConfig {
|
||||
capture_images: true,
|
||||
poll_interval_ms: 500,
|
||||
filters: WatcherFilters {
|
||||
only_apps: vec![],
|
||||
exclude_apps: vec!["mstsc".into()],
|
||||
},
|
||||
},
|
||||
dedup: DedupConfig {
|
||||
method: "sha256".into(),
|
||||
cache_size: 10000,
|
||||
},
|
||||
ui: UiConfig {
|
||||
show_notifications: true,
|
||||
show_tray_icon: true,
|
||||
notification_timeout_sec: 3,
|
||||
},
|
||||
logging: LoggingConfig {
|
||||
level: "info".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Charge la configuration depuis `%APPDATA%/imagod-web/imagod.toml`
|
||||
pub fn load() -> Result<Self> {
|
||||
let path = Self::config_path()?;
|
||||
if path.exists() {
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Impossible de lire {}", path.display()))?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.with_context(|| "Erreur de parsing du fichier de configuration")?;
|
||||
Ok(config)
|
||||
} else {
|
||||
// Premier lancement — créer la config par défaut
|
||||
let config = Config::default();
|
||||
config.save()?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sauvegarde la configuration
|
||||
pub fn save(&self) -> Result<()> {
|
||||
let path = Self::config_path()?;
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
std::fs::write(&path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Chemin du fichier de configuration
|
||||
fn config_path() -> Result<PathBuf> {
|
||||
let base = if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA")
|
||||
.context("Variable APPDATA non définie")?;
|
||||
PathBuf::from(appdata)
|
||||
} else {
|
||||
let home = std::env::var("HOME")
|
||||
.context("Variable HOME non définie")?;
|
||||
PathBuf::from(home).join(".config")
|
||||
};
|
||||
Ok(base.join("imagod-web").join("imagod.toml"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//! Dédoublonnage des images par hash SHA-256
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
/// Gestionnaire de dédoublonnage
|
||||
pub struct Dedup {
|
||||
conn: Connection,
|
||||
cache_size: usize,
|
||||
}
|
||||
|
||||
impl Dedup {
|
||||
/// Ouvre ou crée la base de dédoublonnage locale
|
||||
pub fn open(db_path: &Path, cache_size: usize) -> Result<Self> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS hashes (
|
||||
hash BLOB PRIMARY KEY,
|
||||
image_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_hashes_created ON hashes(created_at);"
|
||||
)?;
|
||||
Ok(Self { conn, cache_size })
|
||||
}
|
||||
|
||||
/// Calcule le hash SHA-256 d'un buffer
|
||||
pub fn hash_bytes(data: &[u8]) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
/// Vérifie si un hash a déjà été vu
|
||||
pub fn already_seen(&self, hash: &[u8]) -> Result<bool> {
|
||||
let exists: bool = self.conn.query_row(
|
||||
"SELECT COUNT(*) > 0 FROM hashes WHERE hash = ?1",
|
||||
rusqlite::params![hash],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Marque un hash comme vu
|
||||
pub fn mark_seen(&self, hash: &[u8], image_id: &str) -> Result<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR IGNORE INTO hashes (hash, image_id) VALUES (?1, ?2)",
|
||||
rusqlite::params![hash, image_id],
|
||||
)?;
|
||||
// Nettoyage périodique si dépassement du cache
|
||||
self.conn.execute(
|
||||
"DELETE FROM hashes WHERE rowid NOT IN (
|
||||
SELECT rowid FROM hashes ORDER BY created_at DESC LIMIT ?1
|
||||
)",
|
||||
rusqlite::params![self.cache_size],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Nombre de hashes en cache
|
||||
pub fn count(&self) -> Result<usize> {
|
||||
let count: usize = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM hashes",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Imago Dashboard</title>
|
||||
<style>
|
||||
/* Phase 9: Dashboard Svelte — ici juste un placeholder */
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--fg: #c9d1d9;
|
||||
--accent: #58a6ff;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 { color: var(--accent); margin-bottom: 1rem; }
|
||||
p { color: #8b949e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="placeholder">
|
||||
<h1>🖼️ Imago Dashboard</h1>
|
||||
<p>Dashboard web en construction — Phase 9</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
//! Client HTTP pour l'API Imago
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::{Client, multipart};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UploadResponse {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ImageInfo {
|
||||
pub id: String,
|
||||
pub filename: String,
|
||||
pub tags: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub ocr_text: Option<String>,
|
||||
pub created_at: String,
|
||||
pub width: Option<u32>,
|
||||
pub height: Option<u32>,
|
||||
pub size_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SearchResponse {
|
||||
pub results: Vec<ImageInfo>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
/// Client pour l'API Imago
|
||||
pub struct ImagoClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl ImagoClient {
|
||||
pub fn new(base_url: String, api_key: String) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.build()?;
|
||||
Ok(Self { client, base_url, api_key })
|
||||
}
|
||||
|
||||
/// Upload une image vers Imago
|
||||
pub async fn upload_image(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
filename: &str,
|
||||
window_title: Option<&str>,
|
||||
) -> Result<UploadResponse> {
|
||||
let part = multipart::Part::bytes(data)
|
||||
.file_name(filename.to_string())
|
||||
.mime_str("image/png")?;
|
||||
|
||||
let mut form = multipart::Form::new()
|
||||
.part("file", part);
|
||||
|
||||
if let Some(title) = window_title {
|
||||
form = form.text("window_title", title.to_string());
|
||||
}
|
||||
|
||||
let response = self.client
|
||||
.post(format!("{}/images/upload", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let upload: UploadResponse = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(upload)
|
||||
}
|
||||
|
||||
/// Recherche des images
|
||||
pub async fn search(&self, query: &str, limit: usize) -> Result<SearchResponse> {
|
||||
let response = self.client
|
||||
.get(format!("{}/images/search", self.base_url))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.query(&[("q", query), ("limit", &limit.to_string())])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: SearchResponse = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Récupère les infos d'une image
|
||||
pub async fn get_image(&self, id: &str) -> Result<ImageInfo> {
|
||||
let response = self.client
|
||||
.get(format!("{}/images/{}", self.base_url, id))
|
||||
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let info: ImageInfo = response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
//! imagod-web — Imago Daemon + Dashboard
|
||||
//!
|
||||
//! Point d'entrée CLI. Dispatch vers les sous-commandes :
|
||||
//! - `start` : Lance le watcher clipboard + dashboard web (Windows)
|
||||
//! - `serve` : Lance le dashboard web uniquement (Linux/serveur)
|
||||
//! - `stop` : Arrête le service
|
||||
//! - `status`: Affiche l'état
|
||||
//! - `config`: Gère la configuration
|
||||
//! - `install` / `uninstall`: Service Windows
|
||||
|
||||
mod cli;
|
||||
mod config;
|
||||
mod dedup;
|
||||
mod imago_client;
|
||||
mod watcher;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod clipboard;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod tray;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod service;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod window_info;
|
||||
|
||||
mod server;
|
||||
|
||||
use anyhow::Result;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
fn setup_logging() {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.init();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
setup_logging();
|
||||
|
||||
let args = cli::parse();
|
||||
|
||||
match args.command {
|
||||
cli::Command::Start { port, open_browser } => {
|
||||
tracing::info!("Démarrage imagod-web — watcher + dashboard");
|
||||
// Lance le watcher et le serveur web en parallèle
|
||||
// ...
|
||||
todo!("Phase 6-7: Watcher + serveur web")
|
||||
}
|
||||
cli::Command::Serve { port, host, no_watcher, open_browser } => {
|
||||
tracing::info!("Démarrage dashboard web uniquement sur {}:{}", host, port);
|
||||
// Lance uniquement le serveur web
|
||||
// ...
|
||||
todo!("Phase 9: Dashboard web")
|
||||
}
|
||||
cli::Command::Stop => {
|
||||
tracing::info!("Arrêt du service");
|
||||
todo!("Phase 7: Stop")
|
||||
}
|
||||
cli::Command::Status => {
|
||||
tracing::info!("Statut du service");
|
||||
todo!("Phase 7: Status")
|
||||
}
|
||||
cli::Command::Config { action } => {
|
||||
tracing::info!("Configuration: {:?}", action);
|
||||
todo!("Phase 2: Config")
|
||||
}
|
||||
cli::Command::Install => {
|
||||
tracing::info!("Installation comme service Windows");
|
||||
todo!("Phase 10: Service install")
|
||||
}
|
||||
cli::Command::Uninstall => {
|
||||
tracing::info!("Désinstallation du service Windows");
|
||||
todo!("Phase 10: Service uninstall")
|
||||
}
|
||||
cli::Command::Logs => {
|
||||
tracing::info!("Logs");
|
||||
todo!("Phase 7: Logs")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! Serveur web — Dashboard embarqué (axum)
|
||||
//!
|
||||
//! Phase 9: API REST + WebSocket + frontend Svelte embarqué via rust-embed
|
||||
|
||||
pub mod routes;
|
||||
pub mod ws;
|
||||
|
||||
use axum::Router;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Démarre le serveur web
|
||||
pub async fn serve(host: &str, port: u16) -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.merge(routes::api_routes())
|
||||
.merge(routes::static_routes());
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
||||
tracing::info!("Dashboard web démarré sur http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! Routes API REST du dashboard
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
/// Routes de l'API REST
|
||||
pub fn api_routes() -> Router {
|
||||
Router::new()
|
||||
.route("/api/health", get(health_check))
|
||||
.route("/api/images", get(list_images))
|
||||
.route("/api/images/search", get(search_images))
|
||||
.route("/api/stats", get(stats))
|
||||
}
|
||||
|
||||
/// Routes statiques (frontend embarqué)
|
||||
pub fn static_routes() -> Router {
|
||||
// Phase 9: Servir le frontend Svelte via rust-embed
|
||||
Router::new()
|
||||
}
|
||||
|
||||
async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
async fn list_images() -> &'static str {
|
||||
// Phase 9: Liste paginée des images
|
||||
"[]"
|
||||
}
|
||||
|
||||
async fn search_images() -> &'static str {
|
||||
// Phase 9: Recherche full-text
|
||||
"[]"
|
||||
}
|
||||
|
||||
async fn stats() -> &'static str {
|
||||
// Phase 9: Statistiques
|
||||
r#"{"total_images":0,"total_storage_bytes":0}"#
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! WebSocket — Mise à jour en temps réel du dashboard
|
||||
//!
|
||||
//! Phase 9: Notifie le frontend quand un nouveau screenshot est uploadé
|
||||
|
||||
// Stub — à implémenter dans Phase 9
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Service Windows (NSSM / windows-service)
|
||||
//!
|
||||
//! Phase 10: Installation comme service Windows
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn install() -> Result<()> {
|
||||
todo!("Phase 10: Service install via NSSM")
|
||||
}
|
||||
|
||||
pub fn uninstall() -> Result<()> {
|
||||
todo!("Phase 10: Service uninstall")
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//! System Tray (Windows)
|
||||
//!
|
||||
//! Phase 8: Icône system tray avec menu contextuel
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run() -> Result<()> {
|
||||
// Phase 8: Icône tray avec Start/Stop/Status/Quit
|
||||
todo!("Phase 8: System tray")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//! Watcher — Boucle principale de polling du clipboard
|
||||
//!
|
||||
//! Surveille le clipboard à intervalle régulier, détecte les nouvelles images,
|
||||
//! les dédoublonne et les envoie à Imago.
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::config::Config;
|
||||
use crate::dedup::Dedup;
|
||||
use crate::imago_client::ImagoClient;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Watcher {
|
||||
config: Config,
|
||||
dedup: Dedup,
|
||||
imago: ImagoClient,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
pub async fn new(config: Config) -> Result<Self> {
|
||||
let db_path = PathBuf::from("imagod.db");
|
||||
let dedup = Dedup::open(&db_path, config.dedup.cache_size)?;
|
||||
let imago = ImagoClient::new(
|
||||
config.imago.url.clone(),
|
||||
config.imago.api_key.clone(),
|
||||
)?;
|
||||
|
||||
Ok(Self { config, dedup, imago })
|
||||
}
|
||||
|
||||
/// Lance la boucle de polling
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
let interval = Duration::from_millis(self.config.watcher.poll_interval_ms);
|
||||
|
||||
tracing::info!(
|
||||
"Watcher démarré — polling toutes les {}ms",
|
||||
self.config.watcher.poll_interval_ms
|
||||
);
|
||||
|
||||
// TODO: Phase 6 — Boucle principale
|
||||
// loop {
|
||||
// 1. Vérifier le clipboard (Windows: arboard, Linux: N/A)
|
||||
// 2. Si nouvelle image → hash SHA-256
|
||||
// 3. Vérifier dédoublonnage
|
||||
// 4. Si nouveau → upload vers Imago
|
||||
// 5. Marquer comme vu
|
||||
// sleep(interval)
|
||||
// }
|
||||
tracing::warn!("Watcher::run() — implémentation Phase 6 à venir");
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Window Info — Titre de la fenêtre active (Windows API)
|
||||
//!
|
||||
//! Phase 6: Capture du contexte (titre fenêtre) pour enrichir les tags
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
/// Récupère le titre de la fenêtre active
|
||||
pub fn get_active_window_title() -> Result<Option<String>> {
|
||||
// Phase 6: Windows API — GetForegroundWindow + GetWindowText
|
||||
todo!("Phase 6: Active window title")
|
||||
}
|
||||
Reference in New Issue
Block a user