mirror of
https://github.com/shadiestgoat/obsidian-layout-manager.git
synced 2026-07-22 05:49:12 +00:00
Settings & various fixes
This commit is contained in:
parent
9b1e67544f
commit
08071c1b75
13 changed files with 370 additions and 47 deletions
|
|
@ -1,29 +0,0 @@
|
|||
<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)
|
||||
}}>
|
||||
<!-- 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>
|
||||
<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>
|
||||
11
src/components/PlatformDropdown.svelte
Normal file
11
src/components/PlatformDropdown.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { PlatformMode } from 'src/settings'
|
||||
|
||||
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>
|
||||
19
src/components/interaction.ts
Normal file
19
src/components/interaction.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { KeyboardEventHandler, MouseEventHandler } from 'svelte/elements'
|
||||
|
||||
export function buttonInteraction(cb: () => void): {
|
||||
onkeypress: KeyboardEventHandler<HTMLElement>
|
||||
onclick: MouseEventHandler<HTMLElement>
|
||||
} {
|
||||
return {
|
||||
onclick: (e) => {
|
||||
if (e.currentTarget.hasAttribute('disabled')) return
|
||||
cb()
|
||||
},
|
||||
onkeypress: (e) => {
|
||||
if (e.currentTarget.hasAttribute('disabled')) return
|
||||
if (e.key == 'Enter' || e.key == ' ') {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/components/modals/Confirmation.svelte
Normal file
12
src/components/modals/Confirmation.svelte
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { ConfirmationModalCallback } from "src/modals"
|
||||
import { buttonInteraction } from "../interaction"
|
||||
|
||||
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>
|
||||
</div>
|
||||
31
src/components/modals/NewLayout.svelte
Normal file
31
src/components/modals/NewLayout.svelte
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
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 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.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} />
|
||||
|
||||
<button disabled={!name || !paths || otherNames.contains(name)} type="submit" class="mod-cta"
|
||||
>Save</button
|
||||
>
|
||||
</form>
|
||||
23
src/components/settings/SettingItem.svelte
Normal file
23
src/components/settings/SettingItem.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte"
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
desc?: string
|
||||
children: Snippet
|
||||
}
|
||||
|
||||
let { name, desc, children }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-item-info">
|
||||
<div class="setting-item-name">{name}</div>
|
||||
{#if desc}
|
||||
<div class="setting-item-description">{desc}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="setting-item-control">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
146
src/components/settings/SettingLayout.svelte
Normal file
146
src/components/settings/SettingLayout.svelte
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<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'
|
||||
|
||||
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 editingTitle = $state(false)
|
||||
|
||||
let tmpValue = $state(option.name)
|
||||
</script>
|
||||
|
||||
<details
|
||||
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>
|
||||
|
||||
<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 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>
|
||||
|
||||
<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="Platform Mode" desc="Restrict this layout to only certain platforms">
|
||||
<PlatformDropdown bind:value={option.platformMode} />
|
||||
</SettingItem>
|
||||
</div>
|
||||
</details>
|
||||
33
src/components/settings/Settings.svelte
Normal file
33
src/components/settings/Settings.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<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'
|
||||
|
||||
let { settings, app }: { settings: Writable<SettingData>; app: App } = $props()
|
||||
|
||||
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)
|
||||
|
||||
$settings = $settings
|
||||
}, { desc: `This will delete the layout '${l.name}'` }).open()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
14
src/main.ts
14
src/main.ts
|
|
@ -1,11 +1,11 @@
|
|||
import { FileView, Notice, Platform, Plugin, setIcon, WorkspaceLeaf } from 'obsidian'
|
||||
import { savableLayout, targetedLayout, type AnyContainer, type LayoutData } from './obsidianLayout'
|
||||
import { PlatformMode, type SavedLayout, type Settings } from './settings'
|
||||
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: Settings
|
||||
settings: SettingData
|
||||
switching = false
|
||||
|
||||
/** leaf id -> file path */
|
||||
|
|
@ -32,11 +32,11 @@ export default class LayoutManager extends Plugin {
|
|||
this.settings.push({
|
||||
container: cont,
|
||||
name,
|
||||
patterns: paths.split('\n').filter((v) => !!v),
|
||||
patterns: paths.split('\n').filter((v) => !!v).join("\n"),
|
||||
platformMode: platMode
|
||||
})
|
||||
this.saveSettings()
|
||||
}).open()
|
||||
}, { otherNames: this.settings.map(s => s.name) }).open()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -76,6 +76,8 @@ export default class LayoutManager extends Plugin {
|
|||
this.app.workspace.on('active-leaf-change', (l) => this.onActiveLeafChange(l))
|
||||
)
|
||||
})
|
||||
|
||||
this.addSettingTab(new LayoutMgrSettings(this.app, this))
|
||||
}
|
||||
|
||||
onActiveLeafChange(l: WorkspaceLeaf | null) {
|
||||
|
|
@ -200,8 +202,8 @@ export default class LayoutManager extends Plugin {
|
|||
continue
|
||||
}
|
||||
|
||||
for (const pattern of opt.patterns) {
|
||||
if (minimatch(p, pattern)) {
|
||||
for (const pattern of opt.patterns.split("\n")) {
|
||||
if (pattern && minimatch(p, pattern)) {
|
||||
return opt
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import type { App } from 'obsidian'
|
|||
import { Modal } from 'obsidian'
|
||||
import { mount, unmount, type Component } from 'svelte'
|
||||
import type { PlatformMode } from './settings'
|
||||
import NewLayout from './components/NewLayout.svelte'
|
||||
import OverrideLayout from './components/OverrideLayout.svelte'
|
||||
import NewLayout from './components/modals/NewLayout.svelte'
|
||||
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
|
||||
|
|
@ -36,7 +37,7 @@ abstract class genericSvelteModal<
|
|||
props: {
|
||||
...this.props,
|
||||
[this.cbFuncName]: (...args: unknown[]) => {
|
||||
this.callback(args)
|
||||
this.callback(...args)
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +63,15 @@ export class OverrideLayoutModal extends genericSvelteModal<
|
|||
typeof OverrideLayout
|
||||
> {
|
||||
title = 'Override Layout...'
|
||||
cbFuncName = "asdasd"
|
||||
component = OverrideLayout
|
||||
}
|
||||
|
||||
export type ConfirmationModalCallback = (confirmed: boolean) => void
|
||||
|
||||
export class ConfirmModal extends genericSvelteModal<
|
||||
ConfirmationModalCallback,
|
||||
typeof Confirmation
|
||||
> {
|
||||
title = 'Are you sure?'
|
||||
component = Confirmation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,54 @@
|
|||
import { App, PluginSettingTab } from 'obsidian'
|
||||
import type { AnyContainer } from './obsidianLayout'
|
||||
import { mount, unmount } from 'svelte'
|
||||
import Settings from './components/settings/Settings.svelte'
|
||||
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 Settings = SavedLayout[]
|
||||
export type SettingData = SavedLayout[]
|
||||
|
||||
export class LayoutMgrSettings extends PluginSettingTab {
|
||||
svelteContent: Settings
|
||||
plugin: LayoutManager
|
||||
|
||||
constructor(app: App, plugin: LayoutManager) {
|
||||
super(app, plugin)
|
||||
|
||||
this.plugin = plugin
|
||||
}
|
||||
|
||||
display(): void {
|
||||
this.containerEl.addClass("lm-settings")
|
||||
|
||||
const settingProxy = writable(this.plugin.settings)
|
||||
settingProxy.subscribe((v) => {
|
||||
this.plugin.settings = v
|
||||
this.plugin.saveSettings()
|
||||
})
|
||||
|
||||
this.svelteContent = mount(Settings, {
|
||||
target: this.containerEl,
|
||||
props: {
|
||||
settings: settingProxy,
|
||||
app: this.app,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hide() {
|
||||
unmount(this.svelteContent, { outro: true })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
styles.css
27
styles.css
|
|
@ -11,3 +11,30 @@
|
|||
.workspace .mod-root .workspace-tab-header[data-type="markdown"] .workspace-tab-header-inner-icon.lm-original {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
.lm-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-8);
|
||||
}
|
||||
|
||||
.lm-setting-manage-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--size-4-2)
|
||||
}
|
||||
|
||||
.lm-setting-manage-container .pad {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
summary.lm-name h3 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.lm-dividor {
|
||||
width: 2px;
|
||||
margin: var(--size-2-2) var(--size-4-1);
|
||||
align-self: stretch;
|
||||
background: var(--background-modifier-border);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue