PoC Commit

This commit is contained in:
ShadiestGoat 2025-04-15 12:46:44 +01:00
commit 0ca0f750aa
23 changed files with 656 additions and 0 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

28
.gitignore vendored Normal file
View file

@ -0,0 +1,28 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# pnpm
pnpm-lock.yaml
# :3
dist

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

19
.prettierrc Normal file
View file

@ -0,0 +1,19 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"semi": false,
"printWidth": 100,
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Lucy Dryaeva
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
README.md Normal file
View file

@ -0,0 +1,19 @@
# Simple Password
TODO: :3
## Features
TODO: :3
## How to use, how it works, etc
TODO: :3
## Preview
TODO: :3
## Contributing
I'm open to issues, PRs, etc!

61
esbuild.config.mjs Normal file
View file

@ -0,0 +1,61 @@
import esbuild from "esbuild"
import process from "process"
import builtins from "builtin-modules"
import esbuildSvelte from 'esbuild-svelte'
import { sveltePreprocess } from 'svelte-preprocess'
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`
const prod = (process.argv[2] === "production")
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts", "styles.css"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outdir: process.env.OUTPUT || "dist/",
entryNames: "[name]",
minify: prod,
plugins: [
esbuildSvelte({
compilerOptions: {
css: 'injected',
runes: true,
},
preprocess: sveltePreprocess(),
}),
]
})
if (prod) {
await context.rebuild()
process.exit(0)
} else {
await context.watch()
}

102
eslint.config.js Normal file
View file

@ -0,0 +1,102 @@
// @ts-check
import js from '@eslint/js'
import ts from 'typescript-eslint'
import prettier from 'eslint-config-prettier'
import globals from 'globals'
import importPlugin from 'eslint-plugin-import-x'
import stylistic from '@stylistic/eslint-plugin'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import svelte from 'eslint-plugin-svelte'
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
importPlugin.flatConfigs.recommended,
{
plugins: {
'@stylistic': stylistic
}
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser,
},
}
},
{
ignores: ['main.js'],
},
{
settings: {
'import-x/resolver-next': [
createTypeScriptImportResolver()
]
}
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/consistent-type-imports': [
'warn',
{
prefer: 'type-imports',
disallowTypeAnnotations: true,
fixStyle: 'separate-type-imports'
}
],
'@stylistic/semi': ['error', 'never'],
'@stylistic/no-trailing-spaces': 'warn',
'@typescript-eslint/no-restricted-imports': [
'error',
{
"patterns": [{
"group": ["moment"],
"allowTypeImports": true,
"message": "Type only import, use obisidan.moment"
}]
}
],
'svelte/no-dupe-on-directives': 'error',
'svelte/no-dupe-use-directives': 'error',
'svelte/no-target-blank': 'warn',
'svelte/block-lang': [
'error',
{
script: ['ts'],
}
],
'svelte/require-event-dispatcher-types': 'warn',
'svelte/require-optimized-style-attribute': 'warn',
'svelte/prefer-style-directive': 'warn',
'svelte/prefer-class-directive': 'warn',
// Breaks svelete >:(
'import-x/no-duplicates': 'off'
}
}
)

BIN
github/preview-blur.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
github/preview-close.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
github/preview-main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "layout-manager",
"name": "Layout Manager",
"version": "1.0.1",
"minAppVersion": "0.15.0",
"description": "Manage layouts with context",
"author": "Lucy Dryaeva",
"authorUrl": "https://lucydryaeva.com",
"isDesktopOnly": false
}

47
package.json Normal file
View file

@ -0,0 +1,47 @@
{
"name": "layout-manager",
"version": "1.0.0",
"description": "Manage layouts with context",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"format:check": "prettier --check ./src",
"format:fix": "prettier --write ./src",
"eslint:check": "eslint ./src",
"eslint:fix": "eslint ./src --fix",
"lint": "pnpm format:check && pnpm eslint:check",
"lint:fix": "pnpm eslint:fix && pnpm format:fix",
"svelte-check": "svelte-check --tsconfig tsconfig.json"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.24.0",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/node": "^16.18.126",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"esbuild-svelte": "^0.9.2",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-import-resolver-typescript": "^4.3.2",
"eslint-plugin-import-x": "^4.10.2",
"eslint-plugin-svelte": "^3.5.1",
"globals": "^16.0.0",
"obsidian": "latest",
"prettier": "^3.5.3",
"svelte": "^5.26.2",
"svelte-check": "^4.1.6",
"svelte-preprocess": "^6.0.3",
"tslib": "2.4.0",
"typescript": "5.8.3",
"typescript-eslint": "^8.29.1"
},
"dependencies": {
"minimatch": "^10.0.1"
}
}

View file

@ -0,0 +1,27 @@
<script lang="ts">
import type { NewLayoutCallback } from "src/modals"
import { PlatformMode } from "src/settings"
let { onSubmit }: { onSubmit: NewLayoutCallback } = $props()
let name = $state("")
let paths = $state("")
let platMode = $state(PlatformMode.BOTH)
</script>
<form class="lm-new-layout-container" onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
onSubmit(name, paths, platMode)
}}>
<input type="text" placeholder="Name" bind:value={name} />
<textarea placeholder="Paths (glob)" bind:value={paths}></textarea>
<select class="dropdown" bind:value={platMode}>
<option value={PlatformMode.BOTH}>Desktop & Mobile</option>
<option value={PlatformMode.COMPUTER}>Desktop Only</option>
<option value={PlatformMode.MOBILE}>Mobile Only</option>
</select>
<button disabled={!name || !paths} type="submit" class="mod-cta">Save</button>
</form>

162
src/main.ts Normal file
View file

@ -0,0 +1,162 @@
import { getFrontMatterInfo, Notice, Plugin } from 'obsidian'
import type { AnyContainer, LayoutData } from './obsidianLayout'
import type { SavedLayout, Settings } from './settings'
import { NewLayoutModal } from './modals'
import { minimatch } from 'minimatch'
export default class LayoutManager extends Plugin {
settings: Settings
switching = false
async onload(): Promise<void> {
await this.loadSettings()
this.addCommand({
id: 'save-layout',
name: 'Save Layout',
callback: () => {
const cont = this.getCurrentLayout()
if (!cont) {
return
}
new NewLayoutModal(this.app, (name, paths, platMode) => {
this.settings.push({
container: cont,
name,
patterns: paths.split('\n').filter(v => !!v),
platformMode: platMode
})
this.saveSettings()
}).open()
}
})
this.registerEvent(
this.app.workspace.on('file-open', (f) => {
if (!f || this.switching || !this.settings) return
const matched = this.findLayoutForPath(f.path)
if (!matched) {
return
}
this.loadLayout(matched.container, f.path)
})
)
}
findLayoutForPath(p: string): SavedLayout | null {
for (const opt of this.settings) {
for (const pattern of opt.patterns) {
if (minimatch(p, pattern)) {
return opt
}
}
}
return null
}
async saveSettings() {
return this.saveData(this.settings)
}
async loadSettings() {
const d = (await this.loadData()) ?? []
this.settings = Array.isArray(d) ? d : []
}
getCurrentLayout(): AnyContainer | null {
const fileSet = new Set<string>()
const layoutData = this.app.workspace.getLayout() as LayoutData
const layout = savableLayout(layoutData.main, fileSet, layoutData.active)
if (fileSet.size > 1) {
new Notice("Can't save: multiple files found in layout")
return null
}
return layout
}
async loadLayout(layout: AnyContainer, path: string) {
this.switching = true
const curLayout = this.app.workspace.getLayout() as LayoutData
let activeId = curLayout.active
const main = targetedLayout(layout, path, (id) => (activeId = id))
this.app.workspace.changeLayout({
...curLayout,
main: main,
active: activeId
})
window.setTimeout(() => {
this.switching = false
}, 300)
}
}
function targetedLayout(l: AnyContainer, f: string, setActive: (id: string) => void): AnyContainer {
if (l.active) {
delete l.active
l.id = randomId(16)
setActive(l.id)
}
if (l.type != 'leaf') {
l.children?.forEach((c) => targetedLayout(c, f, setActive))
return l
}
if (l.state.type == 'markdown') {
l.state.state.file = f
}
return l
}
/**
* Makes savable data out of the raw one
* l becomes dirty after this, do not use it again.
*/
function savableLayout(l: AnyContainer, fileSet: Set<string>, activeID: string): AnyContainer {
// This is basically js referance abuse, which is always fun & reliable & easy to debug :3
if (l.id == activeID) {
l.active = true
}
delete l.id
if (l.type == 'leaf') {
if (l.state.type == 'markdown') {
fileSet.add(l.state.state.file)
// @ts-expect-error Title causes issues
delete l.state.title
// Avoid saving extra metadata
l.state.state = {
file: '',
mode: l.state.state.mode,
source: l.state.state.source
}
}
} else {
l.children?.forEach((c) => savableLayout(c, fileSet, activeID))
}
return l
}
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
function randomId(l: number) {
return Array.from(
{ length: l },
() => possible[Math.floor(Math.random() * possible.length)]
).join('')
}

37
src/modals.ts Normal file
View file

@ -0,0 +1,37 @@
import type { App} from 'obsidian'
import { Modal } from 'obsidian'
import { mount, unmount } from 'svelte'
import type { PlatformMode } from './settings'
import NewLayout from './components/NewLayout.svelte'
export type NewLayoutCallback = (name: string, paths: string, platformMode: PlatformMode) => void
export class NewLayoutModal extends Modal {
svelteContent: Record<string, never>
callback: NewLayoutCallback
constructor(app: App, cb: NewLayoutCallback) {
super(app)
this.callback = cb
}
onOpen(): void {
this.setTitle('New Layout...')
this.shouldRestoreSelection = true
this.svelteContent = mount(NewLayout, {
target: this.contentEl,
props: {
onSubmit: (a, b, c) => {
this.callback(a, b, c)
this.close()
}
}
})
}
async onClose() {
unmount(this.svelteContent, { outro: true })
}
}

39
src/obsidianLayout.ts Normal file
View file

@ -0,0 +1,39 @@
// So obsidian doesn't actually give us layout data so this is the useful stuff that I can gather
import { type ViewState } from 'obsidian'
export type KnownContainers = {
split: { direction: string }
tabs: Record<string, never>
leaf: { state: ViewState & ViewStateFile; group?: string }
}
// I hate typescript
export type AnyContainer =
| GenericContainer<'leaf'>
| GenericContainer<'split'>
| GenericContainer<'tabs'>
export type GenericContainer<N extends keyof KnownContainers> = {
id?: string
active?: boolean
type: N
children?: AnyContainer[]
} & KnownContainers[N]
export interface ViewStateFile {
type: 'markdown'
state: {
file: string
mode: 'preview' | 'source'
source: boolean
}
}
export type LayoutData = {
// We don't care about layout data for anything other than the user content
// Though... future release maybe? If someone asks ig
main: AnyContainer
active: string
}

16
src/settings.ts Normal file
View file

@ -0,0 +1,16 @@
import type { AnyContainer } from './obsidianLayout'
export enum PlatformMode {
BOTH = 'both',
COMPUTER = 'computer',
MOBILE = 'mobile'
}
export interface SavedLayout {
platformMode: PlatformMode
container: AnyContainer
name: string
patterns: string[]
}
export type Settings = SavedLayout[]

9
styles.css Normal file
View file

@ -0,0 +1,9 @@
.lm-new-layout-container {
display: flex;
flex-direction: column;
gap: var(--size-4-3)
}
.lm-new-layout-container textarea {
resize: vertical;
}

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
],
"verbatimModuleSyntax": true,
"skipLibCheck": true,
},
"include": [
"**/*.ts",
"**/*.svelte"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs"
const targetVersion = process.env.npm_package_version
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"))
const { minAppVersion } = manifest
manifest.version = targetVersion
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"))
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"))
versions[targetVersion] = minAppVersion
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"))

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}