Build tool
This commit is contained in:
+270
@@ -0,0 +1,270 @@
|
|||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# DNX
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
publish_release/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
#*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/packages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/packages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/packages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignoreable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
node_modules/
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# CodeRush
|
||||||
|
.cr/
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
|
|
||||||
|
**/.vs/
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|
||||||
|
publish_release/
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
const string DocsFolderName = "op.docs";
|
||||||
|
const string PluginSourceFolder = "OP_plugin";
|
||||||
|
const string PluginManifestFileName = "manifest.json";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buildProjectDir = GetBuildProjectDirectory();
|
||||||
|
var workspaceRoot = Path.GetFullPath(Path.Combine(buildProjectDir, ".."));
|
||||||
|
var docsRoot = Path.GetFullPath(Path.Combine(workspaceRoot, "..", DocsFolderName));
|
||||||
|
var pluginSourceRoot = Path.GetFullPath(Path.Combine(workspaceRoot, "..", PluginSourceFolder));
|
||||||
|
|
||||||
|
Console.WriteLine($"Workspace root: {workspaceRoot}");
|
||||||
|
Console.WriteLine($"Docs root: {docsRoot}");
|
||||||
|
Console.WriteLine($"Plugin source: {pluginSourceRoot}");
|
||||||
|
|
||||||
|
EnsureDirectoryExists(docsRoot, "Docs folder not found. Make sure op.docs exists in the workspace root.");
|
||||||
|
EnsureDirectoryExists(pluginSourceRoot, "Plugin source folder not found. Make sure OP_plugin exists in the workspace root.");
|
||||||
|
|
||||||
|
var manifestPath = Path.Combine(pluginSourceRoot, PluginManifestFileName);
|
||||||
|
if (!File.Exists(manifestPath))
|
||||||
|
throw new InvalidOperationException($"Plugin manifest not found at {manifestPath}");
|
||||||
|
|
||||||
|
BuildPlugin(pluginSourceRoot);
|
||||||
|
|
||||||
|
var pluginId = ReadPluginId(manifestPath);
|
||||||
|
Console.WriteLine($"Detected plugin id: {pluginId}");
|
||||||
|
|
||||||
|
var userRoot = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
var docsTargets = ScanDocTargets(docsRoot);
|
||||||
|
|
||||||
|
if (!docsTargets.Any())
|
||||||
|
{
|
||||||
|
Console.WriteLine("No docs files with 'category: docs' were found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniqueTargets = docsTargets.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Found {uniqueTargets.Count} unique docs target(s):");
|
||||||
|
foreach (var target in uniqueTargets)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {target}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var targetPath in uniqueTargets)
|
||||||
|
{
|
||||||
|
var targetRoot = BuildAbsolutePathFromUserRoot(userRoot, targetPath);
|
||||||
|
var pluginsRoot = Path.Combine(targetRoot, ".obsidian", "plugins");
|
||||||
|
var pluginDestination = Path.Combine(pluginsRoot, pluginId);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine($"Installing plugin to: {pluginDestination}");
|
||||||
|
|
||||||
|
if (Directory.Exists(pluginDestination))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Existing plugin folder found. Deleting previous version...");
|
||||||
|
Directory.Delete(pluginDestination, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory.CreateDirectory(pluginsRoot);
|
||||||
|
CopyDirectory(pluginSourceRoot, pluginDestination, GetCopyExcludes());
|
||||||
|
Console.WriteLine("Plugin copied successfully.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("ERROR: " + ex.Message);
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GetBuildProjectDirectory()
|
||||||
|
{
|
||||||
|
var baseDir = AppContext.BaseDirectory;
|
||||||
|
// Current baseDir is Build/bin/Debug/net10.0/
|
||||||
|
// We want to go up to the project root (where Build/ and OP.sln are)
|
||||||
|
return Path.GetFullPath(Path.Combine(baseDir, "..", "..", ".."));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void EnsureDirectoryExists(string path, string message)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(path))
|
||||||
|
throw new DirectoryNotFoundException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IReadOnlyCollection<string> ScanDocTargets(string docsRoot)
|
||||||
|
{
|
||||||
|
var docsTargetPaths = new List<string>();
|
||||||
|
var markdownFiles = Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
foreach (var filePath in markdownFiles)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(filePath);
|
||||||
|
var frontMatter = ExtractFrontMatter(content);
|
||||||
|
if (frontMatter is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!frontMatter.TryGetValue("category", out var categoryValue) ||
|
||||||
|
!string.Equals(categoryValue.Trim(), "docs", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frontMatter.TryGetValue("path", out var pathValue) || string.IsNullOrWhiteSpace(pathValue))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: File '{filePath}' has category: docs but missing path.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
docsTargetPaths.Add(pathValue.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return docsTargetPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, string>? ExtractFrontMatter(string content)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(content, @"\A---\r?\n(.*?)\r?\n---", RegexOptions.Singleline);
|
||||||
|
if (!match.Success)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var frontMatter = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var body = match.Groups[1].Value;
|
||||||
|
|
||||||
|
foreach (var rawLine in body.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (line.Length == 0 || line.StartsWith("#"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var separatorIndex = line.IndexOf(':');
|
||||||
|
if (separatorIndex < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var key = line.Substring(0, separatorIndex).Trim();
|
||||||
|
var value = line.Substring(separatorIndex + 1).Trim().Trim('"').Trim();
|
||||||
|
frontMatter[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return frontMatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string BuildAbsolutePathFromUserRoot(string userRoot, string relativePath)
|
||||||
|
{
|
||||||
|
if (Path.IsPathRooted(relativePath))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments = relativePath
|
||||||
|
.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(segment => segment.Trim())
|
||||||
|
.Where(segment => segment.Length > 0)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (segments.Length == 0)
|
||||||
|
throw new InvalidOperationException($"Invalid path value: '{relativePath}'");
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(new[] { userRoot }.Concat(segments).ToArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ReadPluginId(string manifestPath)
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(manifestPath);
|
||||||
|
using var doc = JsonDocument.Parse(stream);
|
||||||
|
if (!doc.RootElement.TryGetProperty("id", out var idElement))
|
||||||
|
throw new InvalidOperationException("Plugin manifest does not contain an 'id' property.");
|
||||||
|
|
||||||
|
var pluginId = idElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(pluginId))
|
||||||
|
throw new InvalidOperationException("Plugin id is empty.");
|
||||||
|
|
||||||
|
return pluginId.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BuildPlugin(string pluginSourceRoot)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Building plugin in OP_plugin...");
|
||||||
|
var npmExecutable = FindNpmExecutable();
|
||||||
|
if (npmExecutable is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Could not find npm on PATH. Install Node.js/npm or add npm to your PATH.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var processStartInfo = new ProcessStartInfo(npmExecutable)
|
||||||
|
{
|
||||||
|
WorkingDirectory = pluginSourceRoot,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
processStartInfo.FileName = "cmd";
|
||||||
|
processStartInfo.ArgumentList.Add("/c");
|
||||||
|
processStartInfo.ArgumentList.Add(npmExecutable);
|
||||||
|
processStartInfo.ArgumentList.Add("run");
|
||||||
|
processStartInfo.ArgumentList.Add("build");
|
||||||
|
|
||||||
|
using var process = Process.Start(processStartInfo) ?? throw new InvalidOperationException($"Failed to start 'cmd /c {npmExecutable}'.");
|
||||||
|
var stdout = process.StandardOutput.ReadToEnd();
|
||||||
|
var stderr = process.StandardError.ReadToEnd();
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
Console.WriteLine(stdout);
|
||||||
|
if (!string.IsNullOrWhiteSpace(stderr))
|
||||||
|
Console.Error.WriteLine(stderr);
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
throw new InvalidOperationException($"Plugin build failed with exit code {process.ExitCode}.");
|
||||||
|
|
||||||
|
Console.WriteLine("Plugin build completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string? FindNpmExecutable()
|
||||||
|
{
|
||||||
|
var candidates = new[] { "npm.cmd", "npm.exe", "npm" };
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processStartInfo = new ProcessStartInfo("cmd", $"/c {candidate} --version")
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
};
|
||||||
|
using var process = Process.Start(processStartInfo);
|
||||||
|
if (process is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool exited = process.WaitForExit(5000);
|
||||||
|
if (!exited)
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.ExitCode == 0)
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HashSet<string> GetCopyExcludes()
|
||||||
|
{
|
||||||
|
return new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"node_modules",
|
||||||
|
"src",
|
||||||
|
"obj",
|
||||||
|
"bin",
|
||||||
|
".git",
|
||||||
|
".idea",
|
||||||
|
".vscode",
|
||||||
|
".editorconfig",
|
||||||
|
".npmrc",
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"tsconfig.json",
|
||||||
|
"esbuild.config.mjs",
|
||||||
|
"eslint.config.mts",
|
||||||
|
"version-bump.mjs",
|
||||||
|
"versions.json",
|
||||||
|
"data.json",
|
||||||
|
"README.md",
|
||||||
|
"AGENTS.md",
|
||||||
|
"LICENSE"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CopyDirectory(string sourceDir, string destinationDir, HashSet<string> excludes)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destinationDir);
|
||||||
|
|
||||||
|
foreach (var filePath in Directory.GetFiles(sourceDir))
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
if (excludes.Contains(fileName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var destinationFile = Path.Combine(destinationDir, fileName);
|
||||||
|
File.Copy(filePath, destinationFile, overwrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var directoryPath in Directory.GetDirectories(sourceDir))
|
||||||
|
{
|
||||||
|
var dirName = Path.GetFileName(directoryPath);
|
||||||
|
if (excludes.Contains(dirName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CopyDirectory(directoryPath, Path.Combine(destinationDir, dirName), excludes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "Build\Build.csproj", "{57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{57A2E434-08CD-47BF-9F3D-E1BF74C34EE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"theme": "obsidian"
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"frontmatter-folder-organizer"
|
||||||
|
]
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"footnotes": false,
|
||||||
|
"properties": true,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": true,
|
||||||
|
"bases": true,
|
||||||
|
"webviewer": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "frontmatter-folder-organizer",
|
||||||
|
"name": "Frontmatter Folder Organizer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "1.0.0",
|
||||||
|
"description": "Organize markdown files into folders based on frontmatter hierarchy.",
|
||||||
|
"author": "Obsidian",
|
||||||
|
"authorUrl": "https://obsidian.md",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
This CSS file will be included with your plugin, and
|
||||||
|
available in the app when your plugin is enabled.
|
||||||
|
|
||||||
|
If your plugin does not need CSS, delete this file.
|
||||||
|
|
||||||
|
*/
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"types": {
|
||||||
|
"aliases": "aliases",
|
||||||
|
"cssclasses": "multitext",
|
||||||
|
"tags": "tags",
|
||||||
|
"shouldInstallPlugins": "checkbox"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+194
@@ -0,0 +1,194 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "eeb36d4e48f0ca25",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "4fe61a13d63c2732",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "362018eb264ab766",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "docs/op.docs.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "op.docs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "b9bf87c6cad06498",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "6685cdba50e4a0a7",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "6ef95b00a28c418d",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"autoReveal": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "Files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c5043b22e38e990c",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b159963fcca61ebc",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "c5e47ea76d041492",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "897865c856161773",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "379d0b312b9d30c9",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "doc.zs.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Backlinks for doc.zs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a8f559c72a82e929",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "doc.zs.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "Outgoing links from doc.zs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e1552c44ca209983",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2d2defa7638fb5a3",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "all-properties",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-archive",
|
||||||
|
"title": "All properties"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "82b06aefc5b6df13",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "doc.zs.md",
|
||||||
|
"followCursor": false,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Outline of doc.zs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"switcher:Open quick switcher": false,
|
||||||
|
"graph:Open graph view": false,
|
||||||
|
"canvas:Create new canvas": false,
|
||||||
|
"daily-notes:Open today's daily note": false,
|
||||||
|
"templates:Insert template": false,
|
||||||
|
"command-palette:Open command palette": false,
|
||||||
|
"bases:Create new base": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "362018eb264ab766",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"docs/doc.zs.md",
|
||||||
|
"docs/op.docs.md",
|
||||||
|
"docs",
|
||||||
|
"os.docs.md",
|
||||||
|
"PLUGIN_SETTINGS_UI_GUIDE.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,604 @@
|
|||||||
|
# Plugin Settings UI Guide
|
||||||
|
|
||||||
|
This guide explains how to structure and format plugin settings UI in Obsidian, with examples for building multiple plugins with distinct features.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Core Concepts](#core-concepts)
|
||||||
|
2. [Setting Types](#setting-types)
|
||||||
|
3. [UI Organization Patterns](#ui-organization-patterns)
|
||||||
|
4. [Multi-Plugin Architecture](#multi-plugin-architecture)
|
||||||
|
5. [Plugin Examples](#plugin-examples)
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Settings Interface & Settings Tab
|
||||||
|
|
||||||
|
Every plugin has:
|
||||||
|
|
||||||
|
1. **Settings Interface** - TypeScript interface defining your data structure
|
||||||
|
2. **Settings Tab Class** - Extends `PluginSettingTab` and defines the UI
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Define the data structure
|
||||||
|
export interface MyPluginSettings {
|
||||||
|
settingName: string;
|
||||||
|
settingValue: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values
|
||||||
|
export const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||||
|
settingName: 'default',
|
||||||
|
settingValue: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI definition
|
||||||
|
export class MySettingTab extends PluginSettingTab {
|
||||||
|
plugin: MyPlugin;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: MyPlugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty(); // Clear previous UI
|
||||||
|
|
||||||
|
// Add UI elements here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Structure
|
||||||
|
|
||||||
|
The `containerEl` is where you build your UI. Always start with `containerEl.empty()` to clear old content when the settings tab reopens.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
// Add heading
|
||||||
|
containerEl.createEl('h2', { text: 'Plugin Name' });
|
||||||
|
|
||||||
|
// Add settings
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Setting Name')
|
||||||
|
.setDesc('Description of what this does')
|
||||||
|
.addText((text) => { /* ... */ });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Types
|
||||||
|
|
||||||
|
### Text Input
|
||||||
|
|
||||||
|
Single-line text input:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Plugin Name')
|
||||||
|
.setDesc('The name of your plugin')
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder('Enter name')
|
||||||
|
.setValue(this.plugin.settings.pluginName)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.pluginName = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text Area
|
||||||
|
|
||||||
|
Multi-line text input:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Configuration')
|
||||||
|
.setDesc('Multi-line configuration')
|
||||||
|
.addTextArea((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder('Enter configuration')
|
||||||
|
.setValue(this.plugin.settings.config)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.config = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toggle
|
||||||
|
|
||||||
|
Boolean switch:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Feature')
|
||||||
|
.setDesc('Turn this feature on or off')
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.enabled)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.enabled = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dropdown
|
||||||
|
|
||||||
|
Select from predefined options:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Theme')
|
||||||
|
.setDesc('Choose a color theme')
|
||||||
|
.addDropdown((dropdown) =>
|
||||||
|
dropdown
|
||||||
|
.addOption('light', 'Light')
|
||||||
|
.addOption('dark', 'Dark')
|
||||||
|
.addOption('auto', 'Auto')
|
||||||
|
.setValue(this.plugin.settings.theme)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.theme = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slider
|
||||||
|
|
||||||
|
Numeric input with range:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Search Depth')
|
||||||
|
.setDesc('How deep to search in folders (1-10)')
|
||||||
|
.addSlider((slider) =>
|
||||||
|
slider
|
||||||
|
.setMin(1)
|
||||||
|
.setMax(10)
|
||||||
|
.setStep(1)
|
||||||
|
.setValue(this.plugin.settings.searchDepth)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.searchDepth = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
Trigger an action:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Process Files')
|
||||||
|
.setDesc('Start processing all files')
|
||||||
|
.addButton((button) =>
|
||||||
|
button
|
||||||
|
.setButtonText('Process Now')
|
||||||
|
.onClick(async () => {
|
||||||
|
await this.plugin.processAllFiles();
|
||||||
|
new Notice('Processing complete!');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Picker
|
||||||
|
|
||||||
|
Select a color:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Tag Color')
|
||||||
|
.setDesc('Color for tags')
|
||||||
|
.addColorPicker((color) =>
|
||||||
|
color
|
||||||
|
.setValue(this.plugin.settings.tagColor)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.tagColor = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## UI Organization Patterns
|
||||||
|
|
||||||
|
### Section Headers
|
||||||
|
|
||||||
|
Organize settings into logical sections:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
// Main heading
|
||||||
|
containerEl.createEl('h2', { text: 'Plugin Settings' });
|
||||||
|
|
||||||
|
// Section 1
|
||||||
|
containerEl.createEl('h3', { text: 'Basic Configuration' });
|
||||||
|
new Setting(containerEl).setName('Setting 1')...
|
||||||
|
new Setting(containerEl).setName('Setting 2')...
|
||||||
|
|
||||||
|
// Section 2
|
||||||
|
containerEl.createEl('h3', { text: 'Advanced Options' });
|
||||||
|
new Setting(containerEl).setName('Setting 3')...
|
||||||
|
new Setting(containerEl).setName('Setting 4')...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditional Display
|
||||||
|
|
||||||
|
Show/hide settings based on other settings:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Advanced')
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.advancedMode)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.advancedMode = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display(); // Redraw to show/hide advanced settings
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.plugin.settings.advancedMode) {
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Advanced Option')
|
||||||
|
.setDesc('This only shows when advanced mode is on')
|
||||||
|
.addText((text) => { /* ... */ });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grouped Controls
|
||||||
|
|
||||||
|
Multiple controls in one setting:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Color')
|
||||||
|
.setDesc('Choose color and opacity')
|
||||||
|
.addColorPicker((color) =>
|
||||||
|
color
|
||||||
|
.setValue(this.plugin.settings.color)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.color = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.addSlider((slider) =>
|
||||||
|
slider
|
||||||
|
.setMin(0)
|
||||||
|
.setMax(100)
|
||||||
|
.setValue(this.plugin.settings.opacity)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.opacity = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Plugin Architecture
|
||||||
|
|
||||||
|
When building multiple plugins within one codebase, consider this structure:
|
||||||
|
|
||||||
|
### Folder Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
main.ts (Plugin entry point)
|
||||||
|
settings.ts (Shared settings)
|
||||||
|
plugins/
|
||||||
|
folder/
|
||||||
|
folderPlugin.ts
|
||||||
|
folderSettings.ts
|
||||||
|
markdown/
|
||||||
|
markdownPlugin.ts
|
||||||
|
markdownSettings.ts
|
||||||
|
kanban/
|
||||||
|
kanbanPlugin.ts
|
||||||
|
kanbanSettings.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unified Settings Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// settings.ts
|
||||||
|
export interface PluginConfig {
|
||||||
|
// Folder Plugin settings
|
||||||
|
folderPlugin: {
|
||||||
|
enabled: boolean;
|
||||||
|
organizationHierarchy: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown Plugin settings
|
||||||
|
markdownPlugin: {
|
||||||
|
enabled: boolean;
|
||||||
|
autoCreatePath: string;
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kanban Plugin settings
|
||||||
|
kanbanPlugin: {
|
||||||
|
enabled: boolean;
|
||||||
|
boardTemplate: string;
|
||||||
|
defaultLayout: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: PluginConfig = {
|
||||||
|
folderPlugin: {
|
||||||
|
enabled: true,
|
||||||
|
organizationHierarchy: 'Category, Status',
|
||||||
|
},
|
||||||
|
markdownPlugin: {
|
||||||
|
enabled: true,
|
||||||
|
autoCreatePath: 'Notes',
|
||||||
|
template: 'default',
|
||||||
|
},
|
||||||
|
kanbanPlugin: {
|
||||||
|
enabled: false,
|
||||||
|
boardTemplate: 'default',
|
||||||
|
defaultLayout: 'columns',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unified Settings Tab
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// settings.ts
|
||||||
|
export class UnifiedSettingTab extends PluginSettingTab {
|
||||||
|
plugin: MultiPluginBase;
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
containerEl.createEl('h2', { text: 'Plugin Suite Settings' });
|
||||||
|
|
||||||
|
// Folder Plugin Section
|
||||||
|
this.displayFolderPluginSettings(containerEl);
|
||||||
|
|
||||||
|
// Markdown Plugin Section
|
||||||
|
this.displayMarkdownPluginSettings(containerEl);
|
||||||
|
|
||||||
|
// Kanban Plugin Section
|
||||||
|
this.displayKanbanPluginSettings(containerEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayFolderPluginSettings(containerEl: HTMLElement): void {
|
||||||
|
containerEl.createEl('h3', { text: 'Folder Organization Plugin' });
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Folder Plugin')
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.folderPlugin.enabled)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.folderPlugin.enabled = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.plugin.settings.folderPlugin.enabled) {
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Organization Hierarchy')
|
||||||
|
.setDesc('Comma-separated frontmatter fields')
|
||||||
|
.addTextArea((text) =>
|
||||||
|
text
|
||||||
|
.setValue(this.plugin.settings.folderPlugin.organizationHierarchy)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.folderPlugin.organizationHierarchy = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayMarkdownPluginSettings(containerEl: HTMLElement): void {
|
||||||
|
containerEl.createEl('h3', { text: 'Markdown Auto-Create Plugin' });
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Markdown Plugin')
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.markdownPlugin.enabled)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.markdownPlugin.enabled = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.plugin.settings.markdownPlugin.enabled) {
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Auto-Create Path')
|
||||||
|
.setDesc('Default folder for auto-created files')
|
||||||
|
.addText((text) =>
|
||||||
|
text
|
||||||
|
.setPlaceholder('Notes')
|
||||||
|
.setValue(this.plugin.settings.markdownPlugin.autoCreatePath)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.markdownPlugin.autoCreatePath = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Template')
|
||||||
|
.setDesc('Template to use for new files')
|
||||||
|
.addDropdown((dropdown) =>
|
||||||
|
dropdown
|
||||||
|
.addOption('default', 'Default')
|
||||||
|
.addOption('minimal', 'Minimal')
|
||||||
|
.addOption('detailed', 'Detailed')
|
||||||
|
.setValue(this.plugin.settings.markdownPlugin.template)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.markdownPlugin.template = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displayKanbanPluginSettings(containerEl: HTMLElement): void {
|
||||||
|
containerEl.createEl('h3', { text: 'Kanban Board Plugin' });
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Enable Kanban Plugin')
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.kanbanPlugin.enabled)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.kanbanPlugin.enabled = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.plugin.settings.kanbanPlugin.enabled) {
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Board Template')
|
||||||
|
.setDesc('Default board template')
|
||||||
|
.addDropdown((dropdown) =>
|
||||||
|
dropdown
|
||||||
|
.addOption('default', 'Default')
|
||||||
|
.addOption('agile', 'Agile')
|
||||||
|
.addOption('gtd', 'Getting Things Done')
|
||||||
|
.setValue(this.plugin.settings.kanbanPlugin.boardTemplate)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.kanbanPlugin.boardTemplate = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('Default Layout')
|
||||||
|
.setDesc('How boards are displayed by default')
|
||||||
|
.addDropdown((dropdown) =>
|
||||||
|
dropdown
|
||||||
|
.addOption('columns', 'Columns')
|
||||||
|
.addOption('rows', 'Rows')
|
||||||
|
.addOption('grid', 'Grid')
|
||||||
|
.setValue(this.plugin.settings.kanbanPlugin.defaultLayout)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.kanbanPlugin.defaultLayout = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin Examples
|
||||||
|
|
||||||
|
### Example 1: Folder Organization Plugin
|
||||||
|
|
||||||
|
**Settings Interface:**
|
||||||
|
```typescript
|
||||||
|
export interface FolderOrganizerSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
organizationHierarchy: string;
|
||||||
|
createMissingFolders: boolean;
|
||||||
|
overwriteExisting: boolean;
|
||||||
|
excludeFolders: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key UI Elements:**
|
||||||
|
- Toggle to enable/disable
|
||||||
|
- Text area for hierarchy configuration
|
||||||
|
- Toggle for auto-create folders
|
||||||
|
- Toggle for overwrite confirmation
|
||||||
|
- Multi-select or list for excluded folders
|
||||||
|
- Action button to organize files
|
||||||
|
|
||||||
|
### Example 2: Markdown Auto-Create Plugin
|
||||||
|
|
||||||
|
**Settings Interface:**
|
||||||
|
```typescript
|
||||||
|
export interface MarkdownAutoCreateSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
defaultPath: string;
|
||||||
|
template: 'default' | 'minimal' | 'detailed';
|
||||||
|
autoOpenNewFile: boolean;
|
||||||
|
defaultFrontmatter: string;
|
||||||
|
hotkey: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key UI Elements:**
|
||||||
|
- Toggle to enable/disable
|
||||||
|
- Text input for default path
|
||||||
|
- Dropdown for template selection
|
||||||
|
- Toggle for auto-open
|
||||||
|
- Text area for frontmatter template
|
||||||
|
- Display current hotkey (or hotkey picker if available)
|
||||||
|
|
||||||
|
### Example 3: Kanban Board Plugin
|
||||||
|
|
||||||
|
**Settings Interface:**
|
||||||
|
```typescript
|
||||||
|
export interface KanbanBoardSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
boardTemplate: 'default' | 'agile' | 'gtd';
|
||||||
|
defaultLayout: 'columns' | 'rows' | 'grid';
|
||||||
|
cardWidth: number;
|
||||||
|
showDates: boolean;
|
||||||
|
showTags: boolean;
|
||||||
|
defaultColumns: string[];
|
||||||
|
darkMode: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key UI Elements:**
|
||||||
|
- Toggle to enable/disable
|
||||||
|
- Dropdown for board templates
|
||||||
|
- Dropdown for default layout
|
||||||
|
- Slider for card width
|
||||||
|
- Toggles for feature flags
|
||||||
|
- Text area for default columns (comma-separated)
|
||||||
|
- Color picker for theme customization
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always save settings** - Call `await this.plugin.saveSettings()` after any change
|
||||||
|
2. **Use descriptive names** - Users should understand what each setting does
|
||||||
|
3. **Group related settings** - Use section headers (h3) to organize
|
||||||
|
4. **Provide defaults** - Always have sensible defaults in `DEFAULT_SETTINGS`
|
||||||
|
5. **Validate input** - Check user input before saving (trim, validate paths, etc.)
|
||||||
|
6. **Use conditionals wisely** - Show/hide advanced options based on basic settings
|
||||||
|
7. **Provide feedback** - Use `new Notice()` to confirm actions
|
||||||
|
8. **Redraw on enable/disable** - Call `this.display()` to update UI when features toggle
|
||||||
|
9. **Document complex settings** - Use `.setDesc()` liberally
|
||||||
|
10. **Test multi-step workflows** - Ensure settings changes work with all plugins enabled
|
||||||
|
|
||||||
|
## Testing Your Settings UI
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In your test file
|
||||||
|
const plugin = new MyPlugin(app, manifest);
|
||||||
|
plugin.loadSettings();
|
||||||
|
plugin.registerSettingTab(new UnifiedSettingTab(app, plugin));
|
||||||
|
|
||||||
|
// Settings should load without errors
|
||||||
|
// Check that all toggles/inputs work
|
||||||
|
// Verify settings persist after reload
|
||||||
|
// Test conditional displays
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This guide provides a foundation for building clean, organized, and user-friendly plugin settings. Adapt these patterns to your specific needs!
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
category: docs
|
||||||
|
path: ZeroSpace\zs.docs
|
||||||
|
shouldInstallPlugins: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
category: docs
|
||||||
|
path: Obsidian-Plugins\op.docs
|
||||||
|
---
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"theme": "obsidian"
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"footnotes": false,
|
||||||
|
"properties": true,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": true,
|
||||||
|
"bases": true,
|
||||||
|
"webviewer": false
|
||||||
|
}
|
||||||
Vendored
+190
@@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "5aefe3eaa01298aa",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "8e01d97cdc20d278",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "46c2d403c5136d23",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "Generate Build Script Project.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "Generate Build Script Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "97c9b935bff66d1d",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "1ecf357cfe02325f",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "19ac1b441316dcfc",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"autoReveal": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "Files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "99feed5ad5a90deb",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "de452041497e4cd8",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "a984cdfdcdf28365",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "a63d615d35bc9daf",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "4b86dc95dc16032b",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "Generate Build Script Project.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Backlinks for Generate Build Script Project"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4c3dbdb8f44e7955",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "Generate Build Script Project.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "Outgoing links from Generate Build Script Project"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "da94bd2266074d5a",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "46b74855b4b336d7",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "all-properties",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-archive",
|
||||||
|
"title": "All properties"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ae88324a14179265",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "Generate Build Script Project.md",
|
||||||
|
"followCursor": false,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Outline of Generate Build Script Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"switcher:Open quick switcher": false,
|
||||||
|
"graph:Open graph view": false,
|
||||||
|
"canvas:Create new canvas": false,
|
||||||
|
"daily-notes:Open today's daily note": false,
|
||||||
|
"templates:Insert template": false,
|
||||||
|
"command-palette:Open command palette": false,
|
||||||
|
"bases:Create new base": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "46c2d403c5136d23",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"Generate Build Script Project.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
category: AI Task
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
In OP/Build project, create a program that scans the markdown files in op.docs.
|
||||||
|
|
||||||
|
It will grab files with the frontmatter of catergory: docs.
|
||||||
|
|
||||||
|
For this doc files, get the frontmatter path. This will tell you the location of the docs relative to the current user root. Such as user/Obsidian-Plugins/op.docs.
|
||||||
|
|
||||||
|
You will want to build the Obsidian plugin located in OP_plugin.
|
||||||
|
|
||||||
|
Once it's built, you copy the contents of OP_plugin and move it to the relative plugin folders, such as ZeroSpace\zs.docs\.obsidian\plugins\
|
||||||
|
|
||||||
|
Delete the previous plugin obviously, if the location is already occupied.
|
||||||
|
|
||||||
Reference in New Issue
Block a user