This commit is contained in:
ShadiestGoat 2025-04-16 17:37:22 +01:00
parent 08071c1b75
commit 43b37c4a80
15 changed files with 517 additions and 503 deletions

View file

@ -1,10 +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
# 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

View file

@ -1,5 +1,5 @@
{
"useTabs": false,
"useTabs": true,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",

View file

@ -34,6 +34,7 @@
"globals": "^16.0.0",
"obsidian": "latest",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.26.2",
"svelte-check": "^4.1.6",
"svelte-preprocess": "^6.0.3",

View file

@ -1,11 +1,11 @@
<script lang="ts">
import { PlatformMode } from 'src/settings'
import { PlatformMode } from 'src/settings'
let { value = $bindable() }: { value: PlatformMode } = $props()
let { value = $bindable() }: { value: PlatformMode } = $props()
</script>
<select class="dropdown" bind:value={value}>
<option value={PlatformMode.BOTH}>Desktop & Mobile</option>
<option value={PlatformMode.COMPUTER}>Desktop Only</option>
<option value={PlatformMode.MOBILE}>Mobile Only</option>
<select class="dropdown" bind:value>
<option value={PlatformMode.BOTH}>Desktop & Mobile</option>
<option value={PlatformMode.COMPUTER}>Desktop Only</option>
<option value={PlatformMode.MOBILE}>Mobile Only</option>
</select>

View file

@ -1,19 +1,19 @@
import type { KeyboardEventHandler, MouseEventHandler } from 'svelte/elements'
export function buttonInteraction(cb: () => void): {
onkeypress: KeyboardEventHandler<HTMLElement>
onclick: MouseEventHandler<HTMLElement>
onkeypress: KeyboardEventHandler<HTMLElement>
onclick: MouseEventHandler<HTMLElement>
} {
return {
onclick: (e) => {
return {
onclick: (e) => {
if (e.currentTarget.hasAttribute('disabled')) return
cb()
},
onkeypress: (e) => {
cb()
},
onkeypress: (e) => {
if (e.currentTarget.hasAttribute('disabled')) return
if (e.key == 'Enter' || e.key == ' ') {
cb()
}
}
}
if (e.key == 'Enter' || e.key == ' ') {
cb()
}
}
}
}

View file

@ -1,12 +1,12 @@
<script lang="ts">
import type { ConfirmationModalCallback } from "src/modals"
import { buttonInteraction } from "../interaction"
import type { ConfirmationModalCallback } from 'src/modals'
import { buttonInteraction } from '../interaction'
let { onSubmit, desc }: { onSubmit: ConfirmationModalCallback; desc: string } = $props()
let { onSubmit, desc }: { onSubmit: ConfirmationModalCallback; desc: string } = $props()
</script>
<div class="lm-modal-confirm">
<p>{desc}</p>
<button {...buttonInteraction(() => onSubmit(true))} class="mod-cta">Confirm</button>
<button {...buttonInteraction(() => onSubmit(false))} >Cancel</button>
<p>{desc}</p>
<button {...buttonInteraction(() => onSubmit(true))} class="mod-cta">Confirm</button>
<button {...buttonInteraction(() => onSubmit(false))}>Cancel</button>
</div>

View file

@ -1,31 +1,31 @@
<script lang="ts">
import type { NewLayoutCallback } from 'src/modals'
import { PlatformMode } from 'src/settings'
import PlatformDropdown from '../PlatformDropdown.svelte'
import type { NewLayoutCallback } from 'src/modals'
import { PlatformMode } from 'src/settings'
import PlatformDropdown from '../PlatformDropdown.svelte'
let { onSubmit, otherNames }: { onSubmit: NewLayoutCallback; otherNames: string[] } = $props()
let { onSubmit, otherNames }: { onSubmit: NewLayoutCallback; otherNames: string[] } = $props()
let name = $state('')
let paths = $state('')
let platMode = $state(PlatformMode.BOTH)
let name = $state('')
let paths = $state('')
let platMode = $state(PlatformMode.BOTH)
</script>
<form
class="lm-new-layout-container"
onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
class="lm-new-layout-container"
onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
onSubmit(name.trim(), paths.trim(), platMode)
}}
onSubmit(name.trim(), paths.trim(), platMode)
}}
>
<!-- svelte-ignore a11y_autofocus -->
<!-- This is only rendered in modals, which should be autofocused since it IS a jump -->
<input type="text" placeholder="Name" bind:value={name} autofocus />
<textarea placeholder="Paths (glob)" bind:value={paths}></textarea>
<PlatformDropdown bind:value={platMode} />
<!-- svelte-ignore a11y_autofocus -->
<!-- This is only rendered in modals, which should be autofocused since it IS a jump -->
<input type="text" placeholder="Name" bind:value={name} autofocus />
<textarea placeholder="Paths (glob)" bind:value={paths}></textarea>
<PlatformDropdown bind:value={platMode} />
<button disabled={!name || !paths || otherNames.contains(name)} type="submit" class="mod-cta"
>Save</button
>
<button disabled={!name || !paths || otherNames.contains(name)} type="submit" class="mod-cta"
>Save</button
>
</form>

View file

@ -1,20 +1,23 @@
<script lang="ts">
let { onSubmit, options }: { onSubmit: (opt: string) => void, options: string[] } = $props()
let { onSubmit, options }: { onSubmit: (opt: string) => void; options: string[] } = $props()
let name = $state("")
let name = $state('')
</script>
<form class="lm-new-layout-container" onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
<form
class="lm-new-layout-container"
onsubmit={(e) => {
e.preventDefault()
e.stopPropagation()
onSubmit(name)
}}>
onSubmit(name)
}}
>
<select class="dropdown" bind:value={name}>
{#each options as opt (opt)}
<option value="opt" >{opt}</option>
<option value="opt">{opt}</option>
{/each}
</select>
<button disabled={!name} type="submit" class="mod-cta">Save</button>
</form>

View file

@ -1,5 +1,5 @@
<script lang="ts">
import type { Snippet } from "svelte"
import type { Snippet } from 'svelte'
interface Props {
name: string

View file

@ -1,146 +1,145 @@
<script lang="ts">
import type { SavedLayout } from 'src/settings'
import PlatformDropdown from '../PlatformDropdown.svelte'
import { setIcon } from 'obsidian'
import { buttonInteraction } from '../interaction'
import SettingItem from './SettingItem.svelte'
import { ConfirmModal } from 'src/modals'
import type { SavedLayout } from 'src/settings'
import PlatformDropdown from '../PlatformDropdown.svelte'
import { setIcon } from 'obsidian'
import { buttonInteraction } from '../interaction'
import SettingItem from './SettingItem.svelte'
interface Props {
option: SavedLayout
otherNames: string[]
currentI: number
size: number
changeOrder: (target: number) => void
deleteSelf: () => void
}
interface Props {
option: SavedLayout
otherNames: string[]
currentI: number
size: number
changeOrder: (target: number) => void
deleteSelf: () => void
}
let {
option = $bindable(),
otherNames,
currentI,
size,
changeOrder,
deleteSelf
}: Props = $props()
let {
option = $bindable(),
otherNames,
currentI,
size,
changeOrder,
deleteSelf
}: Props = $props()
let editingTitle = $state(false)
let editingTitle = $state(false)
let tmpValue = $state(option.name)
let tmpValue = $state(option.name)
</script>
<details
ontoggle={(e) => {
if (!e.currentTarget.open) {
editingTitle = false
tmpValue = option.name
}
}}
ontoggle={(e) => {
if (!e.currentTarget.open) {
editingTitle = false
tmpValue = option.name
}
}}
>
<summary
class="lm-name"
onkeyup={(e) => {
if (e.key == ' ') {
e.stopPropagation()
e.preventDefault()
}
}}
>
{#if editingTitle}
<input bind:value={tmpValue} type="text" placeholder="Name" />
{:else}
<h3>{option.name}</h3>
{/if}
</summary>
<summary
class="lm-name"
onkeyup={(e) => {
if (e.key == ' ') {
e.stopPropagation()
e.preventDefault()
}
}}
>
{#if editingTitle}
<input bind:value={tmpValue} type="text" placeholder="Name" />
{:else}
<h3>{option.name}</h3>
{/if}
</summary>
<div class="lm-layout-setting">
<div class="setting-item lm-setting-manage-container">
<p>Manage</p>
<div class="pad"></div>
<div class="lm-layout-setting">
<div class="setting-item lm-setting-manage-container">
<p>Manage</p>
<div class="pad"></div>
{#if size > 1}
{#if size > 2}
<button
use:setIcon={'chevrons-up'}
{...buttonInteraction(() => changeOrder(0))}
class="mod-cta"
aria-label="Priorotize to top"
disabled={currentI == 0}
></button>
{/if}
<button
use:setIcon={'chevron-up'}
{...buttonInteraction(() => changeOrder(currentI - 1))}
class="mod-cta"
aria-label="Priorotize"
disabled={currentI == 0}
></button>
<button
use:setIcon={'chevron-down'}
{...buttonInteraction(() => changeOrder(currentI + 1))}
class="mod-cta"
aria-label="Depriorotize"
disabled={currentI == size - 1}
></button>
{#if size > 2}
<button
use:setIcon={'chevrons-down'}
{...buttonInteraction(() => changeOrder(size - 1))}
class="mod-cta"
aria-label="Depriorotize to bottom"
disabled={currentI == size - 1}
></button>
{/if}
{/if}
{#if size > 1}
{#if size > 2}
<button
use:setIcon={'chevrons-up'}
{...buttonInteraction(() => changeOrder(0))}
class="mod-cta"
aria-label="Priorotize to top"
disabled={currentI == 0}
></button>
{/if}
<button
use:setIcon={'chevron-up'}
{...buttonInteraction(() => changeOrder(currentI - 1))}
class="mod-cta"
aria-label="Priorotize"
disabled={currentI == 0}
></button>
<button
use:setIcon={'chevron-down'}
{...buttonInteraction(() => changeOrder(currentI + 1))}
class="mod-cta"
aria-label="Depriorotize"
disabled={currentI == size - 1}
></button>
{#if size > 2}
<button
use:setIcon={'chevrons-down'}
{...buttonInteraction(() => changeOrder(size - 1))}
class="mod-cta"
aria-label="Depriorotize to bottom"
disabled={currentI == size - 1}
></button>
{/if}
{/if}
{#if editingTitle}
<button
use:setIcon={'check'}
{...buttonInteraction(() => {
editingTitle = false
option.name = tmpValue.trim()
})}
class="mod-cta"
aria-label="Save"
disabled={tmpValue.length === 0 || otherNames.contains(tmpValue)}
></button>
<button
use:setIcon={'x'}
{...buttonInteraction(() => {
editingTitle = false
tmpValue = option.name
})}
aria-label="Cancel"
></button>
{:else}
<button
use:setIcon={'pencil'}
{...buttonInteraction(() => {
editingTitle = true
tmpValue = option.name
})}
aria-label="Being Editing Name"
></button>
{/if}
{#if editingTitle}
<button
use:setIcon={'check'}
{...buttonInteraction(() => {
editingTitle = false
option.name = tmpValue.trim()
})}
class="mod-cta"
aria-label="Save"
disabled={tmpValue.length === 0 || otherNames.contains(tmpValue)}
></button>
<button
use:setIcon={'x'}
{...buttonInteraction(() => {
editingTitle = false
tmpValue = option.name
})}
aria-label="Cancel"
></button>
{:else}
<button
use:setIcon={'pencil'}
{...buttonInteraction(() => {
editingTitle = true
tmpValue = option.name
})}
aria-label="Being Editing Name"
></button>
{/if}
<div class="lm-dividor"></div>
<div class="lm-dividor"></div>
<button
use:setIcon={'trash-2'}
{...buttonInteraction(() => deleteSelf())}
aria-label="Delete Layout"
></button>
</div>
<button
use:setIcon={'trash-2'}
{...buttonInteraction(() => deleteSelf())}
aria-label="Delete Layout"
></button>
</div>
<SettingItem
name="Glob Patterns"
desc="If a pattern matches, this layout will be used. Each line is a new pattern"
>
<textarea bind:value={option.patterns} placeholder="Patterns (glob)"></textarea>
</SettingItem>
<SettingItem
name="Glob Patterns"
desc="If a pattern matches, this layout will be used. Each line is a new pattern"
>
<textarea bind:value={option.patterns} placeholder="Patterns (glob)"></textarea>
</SettingItem>
<SettingItem name="Platform Mode" desc="Restrict this layout to only certain platforms">
<PlatformDropdown bind:value={option.platformMode} />
</SettingItem>
</div>
<SettingItem name="Platform Mode" desc="Restrict this layout to only certain platforms">
<PlatformDropdown bind:value={option.platformMode} />
</SettingItem>
</div>
</details>

View file

@ -1,33 +1,37 @@
<script lang="ts">
import type { SettingData } from 'src/settings'
import type { Writable } from 'svelte/store'
import SettingLayout from './SettingLayout.svelte'
import type { App } from 'obsidian'
import { ConfirmModal } from 'src/modals'
import type { SettingData } from 'src/settings'
import type { Writable } from 'svelte/store'
import SettingLayout from './SettingLayout.svelte'
import type { App } from 'obsidian'
import { ConfirmModal } from 'src/modals'
let { settings, app }: { settings: Writable<SettingData>; app: App } = $props()
let { settings, app }: { settings: Writable<SettingData>; app: App } = $props()
let otherNames = $derived($settings.map((s) => s.name))
let otherNames = $derived($settings.map((s) => s.name))
</script>
{#each $settings as l, i (l.name)}
<SettingLayout
bind:option={$settings[i]}
{otherNames}
currentI={i}
size={$settings.length}
changeOrder={(targetI) => {
$settings.splice(i, 1)
$settings.splice(targetI, 0, l)
$settings = $settings
}}
deleteSelf={() => {
new ConfirmModal(app, (success) => {
if (!success) return
$settings.splice(i, 1)
<SettingLayout
bind:option={$settings[i]}
{otherNames}
currentI={i}
size={$settings.length}
changeOrder={(targetI) => {
$settings.splice(i, 1)
$settings.splice(targetI, 0, l)
$settings = $settings
}}
deleteSelf={() => {
new ConfirmModal(
app,
(success) => {
if (!success) return
$settings.splice(i, 1)
$settings = $settings
}, { desc: `This will delete the layout '${l.name}'` }).open()
}}
/>
$settings = $settings
},
{ desc: `This will delete the layout '${l.name}'` }
).open()
}}
/>
{/each}

View file

@ -1,84 +1,92 @@
import { FileView, Notice, Platform, Plugin, setIcon, WorkspaceLeaf } from 'obsidian'
import type { WorkspaceLeaf } from 'obsidian'
import { FileView, Notice, Platform, Plugin, setIcon } from 'obsidian'
import { savableLayout, targetedLayout, type AnyContainer, type LayoutData } from './obsidianLayout'
import { LayoutMgrSettings, PlatformMode, type SavedLayout, type SettingData } from './settings'
import { NewLayoutModal, OverrideLayoutModal } from './modals'
import { minimatch } from 'minimatch'
export default class LayoutManager extends Plugin {
settings: SettingData
switching = false
settings: SettingData
switching = false
/** leaf id -> file path */
lastState = new Map<string, string>()
/**
* The original leaves created by this plugin
* If empty, treat all leaves as original
*/
originalLeafs: string[] = []
/** leaf id -> file path */
lastState = new Map<string, string>()
/**
* The original leaves created by this plugin
* If empty, treat all leaves as original
*/
originalLeafs: string[] = []
async onload(): Promise<void> {
await this.loadSettings()
async onload(): Promise<void> {
await this.loadSettings()
this.addCommand({
id: 'save-layout',
name: 'Save Layout',
callback: () => {
const cont = this.getCurrentLayout()
if (!cont) {
return
}
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).join("\n"),
platformMode: platMode
})
this.saveSettings()
}, { otherNames: this.settings.map(s => s.name) }).open()
}
})
new NewLayoutModal(
this.app,
(name, paths, platMode) => {
this.settings.push({
container: cont,
name,
patterns: paths
.split('\n')
.filter((v) => !!v)
.join('\n'),
platformMode: platMode
})
this.saveSettings()
},
{ otherNames: this.settings.map((s) => s.name) }
).open()
}
})
this.addCommand({
id: 'override-layuout',
name: 'Override Layout',
callback: () => {
const cont = this.getCurrentLayout()
if (!cont) {
return
}
this.addCommand({
id: 'override-layuout',
name: 'Override Layout',
callback: () => {
const cont = this.getCurrentLayout()
if (!cont) {
return
}
new OverrideLayoutModal(
this.app,
(name) => {
const i = this.settings.findIndex((s) => s.name == name)
if (i == -1) {
new Notice("Can't find this Layout")
return
}
this.settings[i].container = cont
new OverrideLayoutModal(
this.app,
(name) => {
const i = this.settings.findIndex((s) => s.name == name)
if (i == -1) {
new Notice("Can't find this Layout")
return
}
this.settings[i].container = cont
this.saveSettings()
},
{ options: this.settings.map((s) => s.name) }
).open()
}
})
this.saveSettings()
},
{ options: this.settings.map((s) => s.name) }
).open()
}
})
this.register(() => this.setOriginals([]))
this.register(() => this.setOriginals([]))
this.app.workspace.onLayoutReady(() => {
this.updateState()
this.app.workspace.onLayoutReady(() => {
this.updateState()
this.registerEvent(
this.registerEvent(
// Method got too big, lives in a seperate file now
this.app.workspace.on('active-leaf-change', (l) => this.onActiveLeafChange(l))
)
})
this.app.workspace.on('active-leaf-change', (l) => this.onActiveLeafChange(l))
)
})
this.addSettingTab(new LayoutMgrSettings(this.app, this))
}
}
onActiveLeafChange(l: WorkspaceLeaf | null) {
const diffs = this.updateState()
@ -130,71 +138,69 @@ export default class LayoutManager extends Plugin {
this.loadLayout(matched.container, path)
}
manageOriginalIcon(id: string, add: boolean) {
// Its there... trust me...
const l = this.app.workspace.getLeafById(id) as { tabHeaderInnerIconEl: HTMLElement } | null
if (!l) return
manageOriginalIcon(id: string, add: boolean) {
// Its there... trust me...
const l = this.app.workspace.getLeafById(id) as { tabHeaderInnerIconEl: HTMLElement } | null
if (!l) return
l.tabHeaderInnerIconEl.toggleClass('lm-original', add)
setIcon(l.tabHeaderInnerIconEl, add ? 'layout-dashboard' : 'file')
}
l.tabHeaderInnerIconEl.toggleClass('lm-original', add)
setIcon(l.tabHeaderInnerIconEl, add ? 'layout-dashboard' : 'file')
}
setOriginals(newOnes: string[]) {
console.log('Unloading', this.originalLeafs, newOnes)
setOriginals(newOnes: string[]) {
// Cleanup or removed ones
this.originalLeafs.forEach((id) => {
if (!newOnes.contains(id)) this.manageOriginalIcon(id, false)
})
// Cleanup or removed ones
this.originalLeafs.forEach((id) => {
if (!newOnes.contains(id)) this.manageOriginalIcon(id, false)
})
// Always refresh the new ones, even if they are present as part of old
// This is to ensure icon is always right
newOnes.forEach((id) => {
this.manageOriginalIcon(id, true)
})
// Always refresh the new ones, even if they are present as part of old
// This is to ensure icon is always right
newOnes.forEach((id) => {
this.manageOriginalIcon(id, true)
})
this.originalLeafs = newOnes
}
this.originalLeafs = newOnes
}
/**
* @returns diffs
*/
updateState(): Record<string, [string | undefined, string | undefined]> {
const curState = new Map<string, string>()
/**
* @returns diffs
*/
updateState(): Record<string, [string | undefined, string | undefined]> {
const curState = new Map<string, string>()
this.app.workspace.iterateRootLeaves((l) => {
if (!l || !(l.view instanceof FileView) || !l.view.file) {
return
}
this.app.workspace.iterateRootLeaves((l) => {
if (!l || !(l.view instanceof FileView) || !l.view.file) {
return
}
curState.set(l.id, l.view.file?.path)
})
curState.set(l.id, l.view.file?.path)
})
const diffs: Record<string, [string | undefined, string | undefined]> = {}
const diffs: Record<string, [string | undefined, string | undefined]> = {}
curState.forEach((v, k) => {
const old = this.lastState.get(k)
if (old != v) {
diffs[k] = [old, v]
}
})
curState.forEach((v, k) => {
const old = this.lastState.get(k)
if (old != v) {
diffs[k] = [old, v]
}
})
this.lastState.forEach((v, k) => {
if (diffs[k]) {
return
}
if (!curState.has(k)) {
diffs[k] = [v, undefined]
}
})
this.lastState.forEach((v, k) => {
if (diffs[k]) {
return
}
if (!curState.has(k)) {
diffs[k] = [v, undefined]
}
})
this.lastState = curState
this.lastState = curState
return diffs
}
return diffs
}
findLayoutForPath(p: string): SavedLayout | null {
for (const opt of this.settings) {
findLayoutForPath(p: string): SavedLayout | null {
for (const opt of this.settings) {
if (opt.platformMode == PlatformMode.COMPUTER && !Platform.isDesktop) {
continue
}
@ -202,58 +208,58 @@ export default class LayoutManager extends Plugin {
continue
}
for (const pattern of opt.patterns.split("\n")) {
if (pattern && minimatch(p, pattern)) {
return opt
}
}
}
for (const pattern of opt.patterns.split('\n')) {
if (pattern && minimatch(p, pattern)) {
return opt
}
}
}
return null
}
return null
}
async saveSettings() {
return this.saveData(this.settings)
}
async saveSettings() {
return this.saveData(this.settings)
}
async loadSettings() {
const d = (await this.loadData()) ?? []
async loadSettings() {
const d = (await this.loadData()) ?? []
this.settings = Array.isArray(d) ? d : []
}
this.settings = Array.isArray(d) ? d : []
}
getCurrentLayout(): AnyContainer | null {
const fileSet = new Set<string>()
const layoutData = this.app.workspace.getLayout() as LayoutData
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
}
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
}
return layout
}
async loadLayout(layout: AnyContainer, path: string) {
this.switching = true
async loadLayout(layout: AnyContainer, path: string) {
this.switching = true
const curLayout = this.app.workspace.getLayout() as LayoutData
const curLayout = this.app.workspace.getLayout() as LayoutData
let activeId = curLayout.active
const ogSet = new Set<string>()
const main = targetedLayout(layout, path, (id) => (activeId = id), ogSet)
let activeId = curLayout.active
const ogSet = new Set<string>()
const main = targetedLayout(layout, path, (id) => (activeId = id), ogSet)
await this.app.workspace.changeLayout({
...curLayout,
main: main,
active: activeId
})
await this.app.workspace.changeLayout({
...curLayout,
main: main,
active: activeId
})
window.setTimeout(() => {
this.switching = false
window.setTimeout(() => {
this.switching = false
this.setOriginals(Array.from(ogSet))
}, 300)
}
this.setOriginals(Array.from(ogSet))
}, 300)
}
}

View file

@ -7,71 +7,71 @@ import OverrideLayout from './components/modals/OverrideLayout.svelte'
import Confirmation from './components/modals/Confirmation.svelte'
abstract class genericSvelteModal<
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
F extends Function,
C extends Component,
P extends Record<string, unknown> = C extends Component<infer Props> ? Props : never
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
F extends Function,
C extends Component,
P extends Record<string, unknown> = C extends Component<infer Props> ? Props : never
> extends Modal {
svelteContent: Record<string, never>
callback: F
svelteContent: Record<string, never>
callback: F
cbFuncName = 'onSubmit'
props: Partial<P> = {}
cbFuncName = 'onSubmit'
props: Partial<P> = {}
abstract title: string
abstract component: C
abstract title: string
abstract component: C
constructor(app: App, cb: F, props: Partial<P> = {}) {
super(app)
constructor(app: App, cb: F, props: Partial<P> = {}) {
super(app)
this.callback = cb
this.props = { ...this.props, ...props }
}
this.callback = cb
this.props = { ...this.props, ...props }
}
onOpen(): void {
this.setTitle(this.title)
this.shouldRestoreSelection = true
onOpen(): void {
this.setTitle(this.title)
this.shouldRestoreSelection = true
this.svelteContent = mount(this.component, {
target: this.contentEl,
props: {
...this.props,
[this.cbFuncName]: (...args: unknown[]) => {
this.callback(...args)
this.close()
}
}
})
}
this.svelteContent = mount(this.component, {
target: this.contentEl,
props: {
...this.props,
[this.cbFuncName]: (...args: unknown[]) => {
this.callback(...args)
this.close()
}
}
})
}
async onClose() {
unmount(this.svelteContent, { outro: true })
}
async onClose() {
unmount(this.svelteContent, { outro: true })
}
}
export type NewLayoutCallback = (name: string, paths: string, platformMode: PlatformMode) => void
export class NewLayoutModal extends genericSvelteModal<NewLayoutCallback, typeof NewLayout> {
title = 'New Layout...'
component = NewLayout
title = 'New Layout...'
component = NewLayout
}
export type OverrideLayoutCallback = (name: string) => void
export class OverrideLayoutModal extends genericSvelteModal<
OverrideLayoutCallback,
typeof OverrideLayout
OverrideLayoutCallback,
typeof OverrideLayout
> {
title = 'Override Layout...'
component = OverrideLayout
title = 'Override Layout...'
component = OverrideLayout
}
export type ConfirmationModalCallback = (confirmed: boolean) => void
export class ConfirmModal extends genericSvelteModal<
ConfirmationModalCallback,
typeof Confirmation
typeof Confirmation
> {
title = 'Are you sure?'
component = Confirmation
title = 'Are you sure?'
component = Confirmation
}

View file

@ -3,38 +3,38 @@
import { type ViewState } from 'obsidian'
export type KnownContainers = {
split: { direction: string }
tabs: Record<string, never>
leaf: { state: ViewState & ViewStateFile; group?: string }
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'>
| GenericContainer<'leaf'>
| GenericContainer<'split'>
| GenericContainer<'tabs'>
export type GenericContainer<N extends keyof KnownContainers> = {
id?: string
active?: boolean
type: N
children?: AnyContainer[]
id?: string
active?: boolean
type: N
children?: AnyContainer[]
} & KnownContainers[N]
export interface ViewStateFile {
type: 'markdown'
state: {
file: string
mode: 'preview' | 'source'
source: boolean
}
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
// 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
}
/**
@ -42,33 +42,33 @@ export type LayoutData = {
* @param setActive When an active leaf is found, it will call this cb
*/
export function targetedLayout(
l: AnyContainer,
f: string,
setActive: (id: string) => void,
ogSet: Set<string>
l: AnyContainer,
f: string,
setActive: (id: string) => void,
ogSet: Set<string>
): AnyContainer {
if (l.active) {
delete l.active
l.id = randomId(16)
setActive(l.id)
}
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, ogSet))
return l
}
if (l.type != 'leaf') {
l.children?.forEach((c) => targetedLayout(c, f, setActive, ogSet))
return l
}
if (!l.active) {
l.id = randomId(16)
}
if (!l.active) {
l.id = randomId(16)
}
ogSet.add(l.id as string)
ogSet.add(l.id as string)
if (l.state.type == 'markdown') {
l.state.state.file = f
}
if (l.state.type == 'markdown') {
l.state.state.file = f
}
return l
return l
}
/**
@ -76,43 +76,43 @@ export function targetedLayout(
* l becomes dirty after this, do not use it again.
*/
export function savableLayout(
l: AnyContainer,
fileSet: Set<string>,
activeID: string
l: AnyContainer,
fileSet: Set<string>,
activeID: string
): AnyContainer {
// This is basically js referance abuse, which is always fun & reliable & easy to debug :3
// This is basically js referance abuse, which is always fun & reliable & easy to debug :3
if (l.id == activeID) {
l.active = true
}
if (l.id == activeID) {
l.active = true
}
delete l.id
delete l.id
if (l.type == 'leaf') {
if (l.state.type == 'markdown') {
fileSet.add(l.state.state.file)
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
// @ts-expect-error Title causes issues
delete l.state.title
// Avoid saving extra metadata, set bare minimums
l.state.state = {
file: '',
mode: l.state.state.mode,
source: l.state.state.source
}
}
} else {
l.children?.forEach((c) => savableLayout(c, fileSet, activeID))
}
// Avoid saving extra metadata, set bare minimums
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
return l
}
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
function randomId(l: number) {
return Array.from(
{ length: l },
() => possible[Math.floor(Math.random() * possible.length)]
).join('')
return Array.from(
{ length: l },
() => possible[Math.floor(Math.random() * possible.length)]
).join('')
}

View file

@ -1,4 +1,5 @@
import { App, PluginSettingTab } from 'obsidian'
import type { App } from 'obsidian'
import { PluginSettingTab } from 'obsidian'
import type { AnyContainer } from './obsidianLayout'
import { mount, unmount } from 'svelte'
import Settings from './components/settings/Settings.svelte'
@ -6,22 +7,22 @@ import { writable } from 'svelte/store'
import type LayoutManager from './main'
export enum PlatformMode {
BOTH = 'both',
COMPUTER = 'computer',
MOBILE = 'mobile'
BOTH = 'both',
COMPUTER = 'computer',
MOBILE = 'mobile'
}
export interface SavedLayout {
platformMode: PlatformMode
container: AnyContainer
name: string
patterns: string
platformMode: PlatformMode
container: AnyContainer
name: string
patterns: string
}
export type SettingData = SavedLayout[]
export class LayoutMgrSettings extends PluginSettingTab {
svelteContent: Settings
svelteContent: Settings
plugin: LayoutManager
constructor(app: App, plugin: LayoutManager) {
@ -30,8 +31,8 @@ export class LayoutMgrSettings extends PluginSettingTab {
this.plugin = plugin
}
display(): void {
this.containerEl.addClass("lm-settings")
display(): void {
this.containerEl.addClass('lm-settings')
const settingProxy = writable(this.plugin.settings)
settingProxy.subscribe((v) => {
@ -39,14 +40,14 @@ export class LayoutMgrSettings extends PluginSettingTab {
this.plugin.saveSettings()
})
this.svelteContent = mount(Settings, {
target: this.containerEl,
props: {
this.svelteContent = mount(Settings, {
target: this.containerEl,
props: {
settings: settingProxy,
app: this.app,
}
})
}
app: this.app
}
})
}
hide() {
unmount(this.svelteContent, { outro: true })