Visión general
SDK v1.4 Host ≥ 1.4.0 net8.0 · Windows · C#

Plugin
Developer Docs

FocusTracker tiene un sistema de plugins de carga dinámica que te permite extender la aplicación con comportamientos propios: nuevas pantallas de datos, botones en cualquier sección, ciclos automáticos, integraciones con servicios externos y más — todo sin modificar el código base.

Modelo de seguridad

Cada plugin se carga en un PluginLoadContext aislado. El código del plugin nunca accede directamente a la base de datos, la capa WPF ni ningún servicio interno. Toda la interacción ocurre a través de IPluginContext, que actúa como facade controlado. Los binarios son validados en múltiples pasos antes de ejecutar una sola instrucción.

Pipeline de carga

Cuando el host carga un .focusplugin, ejecuta estos pasos en orden. Si alguno falla, el plugin es rechazado con un mensaje claro en crash.log — sin afectar al resto de la app:

  1. Lee plugin.json — sin tocar ningún DLL todavía.
  2. Valida el manifiesto — campos requeridos, formato del Id, versión del host.
  3. Extrae el archivo — en un directorio temporal con protección path-traversal.
  4. Carga el ensamblado en un PluginLoadContext coleccionable.
  5. Busca la implementación de IFocusPlugin por reflexión.
  6. Instancia la clase vía constructor sin parámetros.
  7. Verifica el Id — debe coincidir exactamente con el manifiesto.
  8. Llama a GetUserHelp() — valida que no sea null ni tenga Summary vacío.
  9. Llama a Initialize() — aquí empieza tu código.
Guía de inicio

Inicio rápido

De cero a un plugin funcionando en menos de 10 minutos usando el template oficial.

Prerrequisitos

  • .NET 8 SDKdescargar en dotnet.microsoft.com
  • PowerShell 5.1+ — incluido en Windows 10/11
  • FocusTracker ≥ 1.4.0 instalado para probar localmente
  • Cualquier editor: Visual Studio, VS Code + C# Dev Kit, o Rider

Pasos

  1. Descargá el template
    Clona o descargá el template desde ESTE ENLACE. Tiene el SDK preconfigurado, el plugin.json y el script de build.
  2. Editá tu identidad en plugin.json
    Cambiá id, name, version, author y description. El id nunca puede cambiar entre versiones.
    {
      "id":             "com.tudominio.mi-plugin",
      "name":           "Mi Plugin",
      "version":        "1.0.0",
      "author":         "Tu Nombre",
      "description":    "Una descripción corta (≤140 chars).",
      "minHostVersion": "1.4.0",
      "entryAssembly":  "MiPlugin.dll"
    }
  3. Implementá IFocusPlugin
    Renombrá MyPlugin.cs y completá los cinco campos de identidad, el método GetUserHelp() (obligatorio) y tu lógica en Initialize / Shutdown. Ver la sección para la referencia completa.
  4. Empaquetá con build.ps1
    Desde PowerShell en la raíz del template:
    .\build.ps1
    Genera dist/com.tudominio.mi-plugin-1.0.0.focusplugin.
  5. Instalá y probá
    En FocusTracker → Plugins → Importar, seleccioná el .focusplugin. El plugin aparece al instante en Mis plugins. Si algo falla, revisá %APPDATA%\FocusTracker\crash.log.
Tip: modo Debug para desarrollo

Usá .\build.ps1 -Configuration Debug para incluir símbolos de debug. Para CI/CD, agregá -NoPause y el script no espera input del usuario al terminar.

Referencia

Estructura del proyecto

Un plugin es una DLL de .NET 8 empaquetada en un ZIP con extensión .focusplugin.

Árbol de archivos del template

PluginTemplate/
  sdk/                       ← SDK (no modificar)
    IFocusPlugin.cs
    IPluginContext.cs
    PluginContribution.cs
    PluginSettingDescriptor.cs
    SdkModels.cs
  src/                       ← tu código aquí
    MyPlugin.cs
    PluginConfig.cs
    MyPlugin.csproj
  plugin.json                 ← manifiesto
  build.ps1                   ← script de empaquetado

Contenido del paquete .focusplugin

ArchivoDescripción
plugin.jsonrequeridoManifiesto con metadatos e Id del plugin.
{EntryAssembly}.dllrequeridoDLL con la clase que implementa IFocusPlugin.
FocusTracker.PluginSDK.dllexcluirNo incluir — el host provee el SDK. El script de build lo omite automáticamente.
Dependencias propiasopcionalDLLs de NuGet que uses. El build las copia, salvo SDK y BCL de .NET.
No incluir el SDK en el paquete

Si empaquetás FocusTracker.PluginSDK.dll dentro del .focusplugin, el host terminará con dos versiones del SDK en memoria y los casteos de tipos fallarán. El script build.ps1 ya excluye el SDK, Microsoft.* y System.* automáticamente.

Carpeta de extracción en el host

El host extrae cada plugin en %APPDATA%\FocusTracker\plugins\_extracted\{id}\. Esta carpeta se limpia y re-extrae cada vez que se instala una nueva versión, por lo que no necesitás hacer nada manual.

Referencia del SDK

IFocusPlugin

Toda clase plugin debe implementar esta interfaz. El host la descubre por reflexión y requiere un constructor público sin parámetros.

public class MiPlugin : IFocusPlugin
{
    // ── Identidad ──────────────────────────────────────────────────
    public string Id          => "com.tudominio.mi-plugin";
    public string Name        => "Mi Plugin";
    public string Version     => "1.0.0";
    public string Author      => "Tu Nombre";
    public string Description => "Descripción corta (≤140 chars).";

    // ── Ayuda (OBLIGATORIO) ────────────────────────────────────────
    public PluginHelpContent GetUserHelp() => new()
    {
        Summary  = "Qué hace el plugin.",
        Sections = new[] { new PluginHelpSection { Heading = "Uso", Body = "..." } }
    };

    // ── Ciclo de vida ──────────────────────────────────────────────
    public void Initialize(IPluginContext context) { /* tu lógica */ }
    public void Shutdown() { /* cleanup */ }
}

Identidad

string Id permanente
Identificador único en formato reverse-DNS (ej: "com.empresa.plugin"). Nunca puede cambiar entre versiones. El host rechaza plugins cuyo Id en código no coincida exactamente con el de plugin.json.
string Name
Nombre legible mostrado en Mis plugins, ayuda y pestaña de Ajustes.
string Version
Versión semántica. Debe coincidir con el campo version de plugin.json y el <Version> del .csproj.
string Author
Tu nombre u organización. Aparece en la pantalla de ayuda.
string Description
Descripción corta, ≤ 140 caracteres. Usada en Mis plugins y en el marketplace.

Documentación — obligatoria

PluginHelpContent GetUserHelp() requerido
Devuelve el contenido de la pantalla Ayuda. Se llama antes de Initialize() — debe ser pura y sin estado. Si retorna null o el Summary está vacío, el plugin es rechazado. Ver referencia completa →

Ciclo de vida

void Initialize(IPluginContext context)
Llamado una vez al arrancar el host (o al activar el plugin). Guardá la referencia a context, suscribite a eventos y registrá tus contribuciones de UI aquí.
void Shutdown()
Llamado al cerrar el host o al desactivar/desinstalar. Siempre desuscribite de todos los eventos para evitar memory leaks. Si usás timers, detelos aquí.
Thread safety en Shutdown

El host puede llamar a Shutdown() desde cualquier thread. Si tenés timers de System.Timers.Timer, detelos y esperá a que el callback actual termine antes de liberar el estado.

Referencia del SDK

plugin.json

El host lee este archivo antes de abrir cualquier DLL. Si la validación falla, el paquete es rechazado sin ejecutar código.

{
  "id":             "com.tudominio.mi-plugin",
  "name":           "Mi Plugin",
  "version":        "1.0.0",
  "author":         "Tu Nombre",
  "description":    "Descripción corta (≤140 chars).",
  "minHostVersion": "1.4.0",
  "entryAssembly":  "MiPlugin.dll"
}

Campos

CampoDescripción
idrequeridoReverse-DNS. Solo letras, dígitos, puntos, guiones y guiones bajos. Máx. 128 chars. Permanente.
namerequeridoNombre legible para la UI.
versionrequeridoSemver (ej: "1.2.0"). Debe coincidir con Version en código y <Version> en el csproj.
authoropcionalNombre del autor u organización.
descriptionopcional≤140 chars. Mostrada en el marketplace antes de instalar.
minHostVersionrequeridoVersión mínima de FocusTracker. Si el usuario tiene una versión anterior, el plugin es rechazado con mensaje claro.
entryAssemblyrequeridoNombre del archivo DLL dentro del paquete (ej: "MiPlugin.dll").
El Id es permanente

El campo id es la clave primaria del plugin en todo el sistema. Cambiarlo en una actualización equivale a crear un plugin nuevo — los usuarios perderían su configuración y el historial de instalación.

Versión consistente en los tres lugares

La versión del plugin debe estar sincronizada en:

  • plugin.json → campo "version"
  • MiPlugin.cs → propiedad Version => "1.0.0"
  • MiPlugin.csproj<Version>1.0.0</Version>

El script build.ps1 usa el version del plugin.json para nombrar el archivo de salida, así que si hay discrepancia el paquete generado tendrá un nombre inconsistente.

Referencia del SDK

IPluginContext

El host entrega una instancia de IPluginContext en Initialize(). Todos los métodos son thread-safe; los callbacks de eventos se ejecutan en el hilo de UI.

Estado de tracking

bool IsTracking
Propiedad síncrona. true mientras hay una sesión activa.
string? GetCurrentFocusedApp()
Nombre de la app o URL en foco ahora mismo, o null.

Eventos

event Action<int?>? TrackingStarted
Justo después de iniciar una sesión. Parámetro: ID del proyecto, o null para sesiones libres.
event Action? TrackingStopped
Justo después de detener la sesión.
event Action<string, TimeSpan>? FocusChanged
Cuando cambia la app en primer plano. Parámetros: nombre de la nueva app y tiempo en la anterior.
event Action<string, string>? AlarmTriggered
Al excederse una alarma de tiempo de sesión o proyecto. Parámetros: título y mensaje.
event Action? IdleDetected
Cuando la detección de inactividad detiene el tracking.
public void Initialize(IPluginContext context)
{
    _ctx = context;
    context.TrackingStarted += OnStart;
    context.TrackingStopped += OnStop;
    context.FocusChanged    += OnFocus;
}

public void Shutdown()
{
    if (_ctx is not null)
    {
        _ctx.TrackingStarted -= OnStart;
        _ctx.TrackingStopped -= OnStop;
        _ctx.FocusChanged    -= OnFocus;
    }
}

Lectura de sesiones

List<TrackingSession> GetAllSessions()
Todas las sesiones ordenadas por fecha descendente.
List<TrackingSession> GetSessionsInRange(DateTime from, DateTime to)
Sesiones cuya hora de inicio cae dentro del rango.
TrackingSession? GetCurrentSession()
Sesión activa, o null si no hay tracking.
List<AppUsageSummary> GetSessionDetail(int sessionId)
Uso por app/URL para una sesión específica.

Lectura de uso y tiempo

List<AppUsageSummary> GetUsageSummaries(DateTime from, DateTime to, int? projectId)
Resúmenes por app en el rango. Ordenados por tiempo total descendente.
List<AppUsageSummary> GetTodaySummaries(int? projectId)
Atajo para hoy (medianoche → ahora).
TimeSpan GetTotalTrackedTime(DateTime from, DateTime to, int? projectId)
Tiempo total (foco + desfoco) en el rango.

Lectura de proyectos

List<Project> GetAllProjects()
Todos los proyectos, ordenados alfabéticamente.
Project? GetProject(int id)
Proyecto por ID, o null.
(main, extras) GetProjectSummaries(Project, DateTime, DateTime)
main: apps explícitamente rastreadas. extras: apps detectadas en sesiones del proyecto pero no en sus claves.

Configuración del host

string GetDataFolder()
Ruta a la carpeta de datos de FocusTracker. Usala para guardar la config de tu plugin.
bool GetNotificationSoundEnabled()
true si el usuario tiene sonido de notificaciones activo.
bool GetIdleDetectionEnabled()
true si la detección de inactividad está activa.

Escritura de proyectos

Uso responsable

Las operaciones de escritura modifican datos persistentes del usuario. Documentá lo que hace tu plugin y obtené confirmación antes de borrar sesiones o proyectos.

int CreateProject(string name, string[] trackedKeys, int? totalAlarmMins, int? sessionAlarmMins)
Crea un proyecto y devuelve su ID. trackedKeys: nombres de proceso (ej: "figma") o hosts URL (ej: "notion.so").
void UpdateProject(int id, string name, string[] trackedKeys, ...)
Actualiza nombre, claves y alarmas.
void DeleteProject(int id)
Elimina el proyecto. Las sesiones se preservan pero quedan sin proyecto. Irreversible.

Escritura de sesiones y tracking

void RenameSession(int sessionId, string name)
String vacío restaura el nombre automático.
void AssignSessionToProject(int sessionId, int? projectId)
null como projectId desvincula la sesión.
void DeleteSession(int sessionId)
Elimina permanentemente la sesión y todos sus eventos. Irreversible.
void AddTrackedApp(string processName, string displayName, bool trackUnfocus)
Agrega una app a la sesión activa. processName sin .exe, en minúsculas. No-op si no hay tracking.
void AddTrackedUrl(string host, string label, bool trackUnfocus)
Agrega un host URL. Los subdominios coinciden automáticamente.

Modelos de datos

PropiedadTipoDescripción
TrackingSession
IdintIdentificador único.
DisplayNamestringUsa Name si está definido, sino "Session #Id".
StartTime / EndTimeDateTime / DateTime?EndTime es null si la sesión está activa.
ProjectIdint?ID del proyecto asignado, o null.
AppUsageSummary
AppDisplayNamestringNombre legible de la app o URL.
FocusSeconds / UnfocusSecondslongTiempo en primer plano vs. abierto sin foco.
TotalTimeTimeSpanSuma de foco + desfoco.
FormattedTimestringTiempo formateado listo para mostrar (ej: "1h 5m 30s").
Contribuciones de UI

Botones y tarjetas

Un plugin puede agregar botones a cualquier pantalla del host y una tarjeta de datos a la pantalla de inicio.

Botones en pantallas

Los botones aparecen en una fila dedicada debajo de los controles nativos de cada pantalla.

context.RegisterButton(new PluginButtonContribution
{
    Screen       = PluginScreenTarget.Home,
    Label        = "Mi acción",
    Icon         = "\uE768",          // Segoe Fluent Icons — opcional
    Style        = PluginButtonStyle.Primary,
    OnClicked    = () => DoSomething(),
    // Visibilidad condicional — evaluada cada segundo
    GetIsVisible = () => context.IsTracking,
});

Pantallas disponibles (PluginScreenTarget)

ValorDescripción
HomePantalla de inicio.
DashboardPanel de análisis de uso.
SessionsLista de sesiones grabadas.
ProjectsGestión de proyectos.
LiveTrackingOverlay visible mientras la sesión corre.

Estilos de botón (PluginButtonStyle)

EstiloAparienciaCuándo usarlo
PrimaryFondo verde lima #C8FF00CTA principal, acción más importante.
DefaultBorde gris, fondo transparenteAcciones generales y neutras.
SecondaryBorde naranja #FF8C42Acciones secundarias o de modificación.
TertiaryBorde azul #4DA6FFAcciones informativas o de navegación.
DangerBorde rojo #FF4D6AAcciones destructivas (stop, eliminar).
GetIsVisible — visibilidad condicional

GetIsVisible es un Func<bool> evaluado periódicamente por el host. Si devuelve false, el botón queda colapsado. Útil para mostrar acciones solo cuando son relevantes, como el botón "+5 min" del Pomodoro que solo aparece cuando hay un ciclo activo. Si es null (por defecto), el botón siempre está visible.

Tarjeta en Home

Una tarjeta es un widget de datos en la pantalla de inicio, con filas clave-valor y auto-refresco configurable.

context.RegisterHomeCard(new PluginCardContribution
{
    Title              = "Mi Plugin",
    Icon               = "\uE9D9",
    AutoRefreshSeconds = 5,  // 0 = estático
    GetRows            = BuildRows,
});

private IReadOnlyList<PluginCardRow> BuildRows() => new[]
{
    new PluginCardRow { Label = "Estado",    Value = "Activo"      },
    new PluginCardRow { Label = "DETALLES",  IsHeader = true  },
    new PluginCardRow { Label = "Restante", Value = "18:32"     },
};

Devolvé una lista vacía para que el host muestre el estado vacío por defecto. Las filas con IsHeader = true se renderizan en negrita y sin columna de valor, útiles para agrupar datos.

Contribuciones de UI

Ajustes en Settings

Tu plugin puede agregar items de configuración a la pantalla de Ajustes. El host crea automáticamente una pestaña con el nombre que indiques.

Tipos de control

TipoDescripciónCallback
ToggleSwitch on/off.OnToggleChanged: Action<bool>
TextInputCampo de texto. Se confirma al presionar Enter o perder el foco.OnTextCommitted: Action<string>
SelectDropdown con lista fija de opciones.OnSelectChanged: Action<string>
ButtonBotón de acción. Estilo rojo con ButtonIsDanger = true.OnButtonClicked: Action
FilePickerRuta de archivo + botón "Browse…". Filtro Win32 configurable.OnPathSelected: Action<string>
FolderPickerRuta de carpeta + botón "Browse…".OnPathSelected: Action<string>

Ejemplos

// Toggle
context.RegisterSetting(new PluginSettingDescriptor
{
    TabName         = "Mi Plugin",
    Title           = "Activar alertas",
    Description     = "Muestra una notificación al cambiar la app.",
    Type            = PluginSettingType.Toggle,
    ToggleDefault   = true,
    OnToggleChanged = v => { _config.Enabled = v; _config.Save(...); },
});

// Select
context.RegisterSetting(new PluginSettingDescriptor
{
    TabName            = "Mi Plugin",
    Title              = "Tipo de alerta",
    Type               = PluginSettingType.Select,
    SelectOptions      = new[] { "Sonido", "Visual", "Ambos" },
    SelectDefaultIndex = 2,
    OnSelectChanged    = v => _config.Mode = v,
});

// TextInput
context.RegisterSetting(new PluginSettingDescriptor
{
    TabName         = "Mi Plugin",
    Title           = "Prefijo",
    Type            = PluginSettingType.TextInput,
    TextPlaceholder = "ej: 🔍 Ahora:",
    TextDefault     = _config.Prefix,
    OnTextCommitted = v => _config.Prefix = v,
});

// Botón peligroso
context.RegisterSetting(new PluginSettingDescriptor
{
    TabName         = "Mi Plugin",
    Title           = "Resetear datos",
    Type            = PluginSettingType.Button,
    ButtonLabel     = "Eliminar todo",
    ButtonIsDanger  = true,
    OnButtonClicked = () => ResetAll(),
});
Tip: Label de sección

Podés agregar un item con Type = PluginSettingType.Label (solo Title + Description, sin control) para crear encabezados de sección dentro de tu pestaña de ajustes.

Contribuciones de UI

Notificaciones y sonido

El host ofrece cuatro variantes para notificar al usuario. Todas se muestran como toasts en la esquina de la pantalla, con el diseño visual de FocusTracker.

Métodos disponibles

ShowNotification(title, message, kind)
Toast temporal que se auto-cierra. Reproduce sonido solo si el usuario tiene sonido habilitado en Ajustes.
ShowPersistentNotification(title, message, kind, actions)
Toast que permanece hasta que el usuario lo cierra. Soporta hasta 3 botones de acción. Ideal para decisiones del usuario.
ShowNotificationWithSound(title, message, kind, sound)
Toast temporal + sonido, sin importar la configuración del usuario. Usar con moderación.
PlaySound(sound)
Solo sonido, sin toast. Respeta la configuración del usuario.
// Toast simple
_ctx.ShowNotification("¡Hecho!", "La tarea se completó.");

// Toast con botones de decisión
_ctx.ShowPersistentNotification(
    "¿Tomás un descanso?",
    "Completaste 25 minutos de trabajo.",
    PluginToastKind.Success,
    new[]
    {
        new PluginToastAction { Label = "Descanso",    OnClicked = StartBreak   },
        new PluginToastAction { Label = "Seguir",      OnClicked = ContinueWork },
        new PluginToastAction { Label = "+ 5 min",    OnClicked = AddFiveMin   },
    });

Tipos de toast (PluginToastKind)

ValorColorCuándo usarlo
SuccessVerde limaAcción completada, ciclo terminado, logro.
InfoAzulInformación neutral, recordatorio.
WarningNaranjaAdvertencia que requiere atención.
ErrorRojoError o fallo.

Sonidos disponibles (WindowsSystemSound)

ValorSonido del sistema
AsteriskInformación (suave, neutro)
BeepBeep genérico
ExclamationExclamación (énfasis moderado)
HandCritical Stop (énfasis fuerte)
QuestionPregunta del sistema
Contribuciones de UI

Pantalla de ayuda

Todos los plugins deben implementar GetUserHelp(). El contenido se muestra en Plugins → Mis plugins → Ayuda, para que los usuarios entiendan cómo usar tu plugin sin salir de la app.

Error de compilación si no está implementado

GetUserHelp() es miembro de la interfaz IFocusPlugin, por lo que cualquier clase que no lo implemente no compilará. Además, el loader verifica en runtime que el retorno no sea null y que Summary no esté vacío — si falla, el plugin es rechazado con mensaje claro en crash.log.

public PluginHelpContent GetUserHelp() => new()
{
    Summary =
        "Un párrafo que explique claramente qué hace el plugin " +
        "y cuál es el beneficio principal para el usuario.",

    Sections = new[]
    {
        new PluginHelpSection
        {
            Heading = "Primeros pasos",
            Body    = "Qué debe hacer el usuario para empezar."
        },
        new PluginHelpSection
        {
            Heading = "Configuración",
            Body    = "Descripción de cada ajuste disponible en Settings."
        },
        // Agregar tantas secciones como sean necesarias.
    }
};

Reglas

  • Summary requerido — no puede ser null ni estar vacío. Al menos una oración.
  • Método puro y sin estado — se llama antes de Initialize(). No uses campos privados ni accedas a _ctx.
  • Sin límite de secciones — agregá las que necesités. Una sola es válida.
  • Solo texto plano — el Body no soporta markdown ni HTML.
Buenas prácticas

Escribí el contenido en el idioma principal de tus usuarios. El Summary debe poder leerse en 10 segundos y responder "¿para qué sirve?". Cada sección debería responder una pregunta concreta: cómo activar, qué configura cada ajuste, qué esperar al usar el plugin.

Guía de publicación

Persistir configuración

El template incluye PluginConfig — un patrón simple de serialización JSON. Usalo directamente o adaptalo con tus propios campos.

// PluginConfig.cs
public class PluginConfig
{
    public bool   EnableAlerts { get; set; } = true;
    public string Prefix       { get; set; } = "";

    public static PluginConfig Load(string folder, string pluginId)
    {
        var path = Path.Combine(folder, $"plugin_{pluginId}.json");
        if (!File.Exists(path)) return new();
        try { return JsonSerializer.Deserialize<PluginConfig>(File.ReadAllText(path)) ?? new(); }
        catch { return new(); }
    }

    public void Save(string folder, string pluginId) =>
        File.WriteAllText(
            Path.Combine(folder, $"plugin_{pluginId}.json"),
            JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }));
}

Uso en Initialize()

private IPluginContext? _ctx;
private PluginConfig    _config = new();

public void Initialize(IPluginContext context)
{
    _ctx    = context;
    _config = PluginConfig.Load(context.GetDataFolder(), Id);

    context.RegisterSetting(new PluginSettingDescriptor {
        ToggleDefault   = _config.EnableAlerts,
        OnToggleChanged = v => {
            _config.EnableAlerts = v;
            _config.Save(context.GetDataFolder(), Id);
        },
        // ...
    });
}
Dónde se guarda

Los archivos de config se guardan en %APPDATA%\FocusTracker\ con el nombre plugin_{id}.json. Son archivos de texto plano que el usuario puede respaldar o migrar.

Guía de publicación

Empaquetar el plugin

El script build.ps1 compila, empaqueta y genera el .focusplugin listo para distribuir en un solo comando.

Uso

# Release (por defecto)
.\build.ps1

# Debug — incluye símbolos para depurar
.\build.ps1 -Configuration Debug

# Sin pausa al terminar — ideal para CI/CD
.\build.ps1 -NoPause

El archivo de salida se genera en dist/{id}-{version}.focusplugin.

Qué hace el script

  1. Compila el SDK en el modo indicado.
  2. Compila tu plugin referenciando el SDK.
  3. Empaqueta: copia el DLL del plugin, plugin.json y tus dependencias propias (excluye SDK, Microsoft.* y System.*) en un ZIP renombrado a .focusplugin.

Instalar para probar

En FocusTracker → Plugins → Importar, seleccioná el .focusplugin generado. El plugin aparece inmediatamente en Mis plugins. Si algo falla, revisá:

%APPDATA%\FocusTracker\crash.log
Actualizaciones

Para actualizar una versión instalada, simplemente volvé a importar el nuevo .focusplugin. El host sobrescribe el archivo anterior, re-extrae y recarga el plugin en el mismo ciclo.

Guía de publicación

Checklist final

Antes de compartir o publicar tu plugin, verificá cada punto de esta lista.

  1. Id único y permanente
    Tu Id en código coincide exactamente con el id en plugin.json. Usás reverse-DNS. Solo letras, dígitos, puntos, guiones y guiones bajos.
  2. Versión consistente en los tres archivos
    Version en código = version en plugin.json = <Version> en el .csproj.
  3. Description ≤ 140 caracteres
    Tanto el campo Description en código como el description en plugin.json.
  4. GetUserHelp() implementado y completo
    Summary no vacío. Al menos una sección explicando cómo usar el plugin. Sin dependencia de estado.
  5. Shutdown() desuscribe todos los eventos
    Cada += tiene su -= correspondiente. Probalo desactivando el plugin desde Mis plugins y volviéndolo a activar.
  6. Thread safety
    Si usás System.Timers.Timer u otros threads, el estado mutable está protegido con lock.
  7. SDK excluido del paquete
    Verificá que FocusTracker.PluginSDK.dll no esté dentro del .focusplugin.
  8. Probado localmente
    Instalado vía Importar, activado, desactivado y vuelto a activar sin errores en %APPDATA%\FocusTracker\crash.log.
Tip: probá el ciclo completo

Desactivá tu plugin desde Mis plugins, cerrá FocusTracker, volvé a abrirlo y reactivalo. Esto verifica que el Shutdown() es correcto, que los eventos no quedan colgados y que la configuración persiste bien.

Publicación

Marketplace de plugins

Distribuí tu plugin a todos los usuarios de FocusTracker directamente desde la pantalla de Plugins de la app.

Próximamente

Portal de desarrolladores

El marketplace está en desarrollo activo. En los próximos meses podrás enviar tu plugin para revisión, elegir un modelo de distribución y llegar a todos los usuarios de FocusTracker.

Lo que se viene

  • Portal de desarrolladores para subir y gestionar tus plugins.
  • Proceso de revisión para garantizar calidad y seguridad antes de publicar.
  • Modelos de monetización: gratuito, donación, compra única y suscripción mensual/anual.
  • Panel de analytics con instalaciones, ratings y retención.
  • Actualizaciones automáticas: los usuarios son notificados cuando publiques una nueva versión.

Mientras tanto

Podés compartir tu .focusplugin directamente — se instala desde Plugins → Importar con un click. Si querés ser de los primeros en publicar cuando el marketplace abra, o tenés preguntas sobre el SDK, escribime a hola@santiagorada.com.