This is the consolidated reference for creating new destination plugins in XerahS.
It aligns with the current code contracts under src/XerahS.Uploaders/PluginSystem.
Destination Settings -> Browse Files (Media Explorer) only works when all of these are true:
- Your provider implements
IUploaderExplorer. ValidateSettings(settingsJson)returnstruefor the selected instance.ListAsyncreturnsMediaItementries.GetThumbnailAsyncreturns image bytes for image items (otherwise you only see file icons, not thumbnails).
Code path:
- UI checks
provider is IUploaderExplorer. - Button enabled state depends on
ValidateSettings. - Explorer window calls:
ListAsyncGetThumbnailAsyncGetContentAsyncDeleteAsync
Practical guidance:
- Override
ValidateSettingsif your provider needs secrets or required fields. The base implementation only checks JSON deserialization. - In each
MediaItem, setName,Path,MimeType, andUrlwhen possible. - Use
MediaItem.Metadatafor provider-specific context required by later calls (DeleteAsync,GetContentAsync, etc.).
Create a class library plugin project (usually net10.0):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<EnableDynamicLoading>true</EnableDynamicLoading>
<PluginId>myplugin</PluginId>
<PluginName>My Destination</PluginName>
</PropertyGroup>
<ItemGroup>
<!-- Match host package versions used by the app -->
<PackageReference Include="Avalonia" Version="11.3.12">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\..\\XerahS.Uploaders\\XerahS.Uploaders.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
<ProjectReference Include="..\\..\\XerahS.Common\\XerahS.Common.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>Why ExcludeAssets=runtime matters:
- If you copy framework/shared DLLs into plugin folders, config view loading can break due to assembly identity conflicts.
- Keep plugin output minimal: plugin DLL,
plugin.json, and only true plugin-specific dependencies.
Minimum valid manifest:
{
"pluginId": "myplugin",
"name": "My Destination",
"version": "1.0.0",
"author": "Your Name",
"description": "Uploads files to My Service",
"apiVersion": "1.0",
"entryPoint": "MyPlugin.MyProvider",
"assemblyFileName": "MyPlugin.dll",
"supportedCategories": ["Image", "File"]
}Important fields:
pluginId: must be unique; should matchProviderId.apiVersion: must be compatible with current plugin API (1.0).entryPoint: full type name implementingIUploaderProvider.assemblyFileName: optional but recommended.supportedCategories: at least one category required.supportsExplorer: optional metadata flag; settruewhen provider implementsIUploaderExplorer.
Use UploaderProviderBase unless you need full custom behavior:
using Newtonsoft.Json;
using XerahS.Uploaders;
using XerahS.Uploaders.FileUploaders;
using XerahS.Uploaders.PluginSystem;
namespace MyPlugin;
public sealed class MyProvider : UploaderProviderBase
{
public override string ProviderId => "myplugin";
public override string Name => "My Destination";
public override string Description => "Uploads files to My Service";
public override Version Version => new(1, 0, 0);
public override UploaderCategory[] SupportedCategories => new[] { UploaderCategory.Image, UploaderCategory.File };
public override Type ConfigModelType => typeof(MyConfigModel);
public override Uploader CreateInstance(string settingsJson)
{
var config = JsonConvert.DeserializeObject<MyConfigModel>(settingsJson)
?? throw new InvalidOperationException("Invalid settings JSON");
return new MyUploader(config);
}
public override Dictionary<UploaderCategory, string[]> GetSupportedFileTypes() =>
new()
{
[UploaderCategory.Image] = new[] { "png", "jpg", "jpeg", "gif", "webp" },
[UploaderCategory.File] = new[] { "zip", "pdf", "txt" }
};
public override object? CreateConfigView() => new Views.MyConfigView();
public override IUploaderConfigViewModel? CreateConfigViewModel() => new ViewModels.MyConfigViewModel();
public override bool ValidateSettings(string settingsJson)
{
var cfg = JsonConvert.DeserializeObject<MyConfigModel>(settingsJson);
return cfg != null && !string.IsNullOrWhiteSpace(cfg.ApiBaseUrl);
}
}Config view model contract:
- Implement
IUploaderConfigViewModel:LoadFromJsonToJsonValidate
Secrets:
- Do not store secrets directly in
settingsJson. - Store a generated
SecretKeyin settings. - Save actual credentials in
ISecretStore(GetSecret,SetSecret,DeleteSecret). - If your provider or config VM needs host services, implement
IProviderContextAware.
Implement IUploaderExplorer to enable Browse Files.
Required methods:
ListAsync(ExplorerQuery query, ...)GetThumbnailAsync(MediaItem item, ...)GetContentAsync(MediaItem item, ...)DeleteAsync(MediaItem item, ...)CreateFolderAsync(...)
Skeleton:
using XerahS.Uploaders.PluginSystem;
public sealed class MyProvider : UploaderProviderBase, IUploaderExplorer
{
public bool SupportsFolders => true;
public Task<ExplorerPage> ListAsync(ExplorerQuery query, CancellationToken cancellation = default)
{
// query.SettingsJson has the instance config for each call.
// Return files/folders as MediaItem entries.
throw new NotImplementedException();
}
public Task<byte[]?> GetThumbnailAsync(MediaItem item, int maxWidthPx = 180, CancellationToken cancellation = default)
{
// Return JPEG/PNG bytes for image items.
throw new NotImplementedException();
}
public Task<Stream?> GetContentAsync(MediaItem item, CancellationToken cancellation = default)
=> Task.FromResult<Stream?>(null);
public Task<bool> DeleteAsync(MediaItem item, CancellationToken cancellation = default)
=> Task.FromResult(false);
public Task<bool> CreateFolderAsync(string parentPath, string folderName, CancellationToken cancellation = default)
=> Task.FromResult(false);
}Explorer-specific data tips:
MediaItem.Url: used for open/copy URL actions.MediaItem.ThumbnailUrl: can be used by your ownGetThumbnailAsynclogic.MediaItem.Metadata: include IDs, paths, serialized settings, etc. needed by follow-up calls.- Respect paging: set
ExplorerPage.ContinuationTokenwhen more data exists.
Manual local deployment:
- Build plugin project.
- Create plugin folder:
<AppOutput>/Plugins/<pluginId>/
- Copy:
- plugin DLL
plugin.json- plugin-only dependencies
- Start XerahS, open Destination Settings, and add plugin from catalog.
Quick validation checklist:
- Provider loads and appears in catalog.
- Config view renders.
- Settings round-trip (
LoadFromJson/ToJson) works. - Upload works for each declared category.
- If explorer implemented:
- Browse Files button visible.
- Browse Files button enabled with valid settings.
ListAsyncreturns items.- image thumbnails appear from
GetThumbnailAsync.
The .xsdp (XerahS Data/Distribution Package) is a ZIP archive containing your plugin's plugin.json, DLL, and supporting files. Use the PluginExporter CLI to create one:
dotnet run --project src/desktop/tools/XerahS.PluginExporter -- <pluginDirectory> [outputPath]Examples:
# Output to current directory (plugin-name.xsdp)
dotnet run --project src/desktop/tools/XerahS.PluginExporter -- "C:\Path\To\MyPlugin"
# Specify exact output path
dotnet run --project src/desktop/tools/XerahS.PluginExporter -- "C:\Path\To\MyPlugin" -o "C:\Output\MyPlugin.xsdp"
# Output to directory (creates plugin-name.xsdp inside)
dotnet run --project src/desktop/tools/XerahS.PluginExporter -- "C:\Path\To\MyPlugin" -o "C:\Output"CLI behavior:
plugin.jsonis required; packaging fails if missing or invalid.- Max package size is 100 MB.
- Output extension is always
.xsdp.
Plugins published to the Community Plugin Registry must:
- Use HTTPS for
downloadUrl - Include a SHA-256
checksum(prefix withsha256:) - Target
minAppVersionand compatibleapiVersion
See community-plugin-registry.md for the full entry schema and security requirements.
If you do not distribute via the community registry, manually copy your .xsdp to <AppOutput>/Plugins/<pluginId>/ and extract it, or use the in-app Install Plugin dialog (Settings → Uploaders → Install Plugin...). The installer UI filters for .xsdp files.
Use these plugins as examples:
src/Plugins/ShareX.AmazonS3.Plugin(uploader + explorer + secure credential flow)src/Plugins/ShareX.Imgur.Plugin(uploader + explorer with album hierarchy)src/Plugins/ShareX.GitHubGist.Plugin(text uploader with OAuth token storage)src/Plugins/ShareX.Paste2.Plugin(simple text uploader)