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