Settings & various fixes

This commit is contained in:
ShadiestGoat 2025-04-16 17:32:19 +01:00
parent 9b1e67544f
commit 08071c1b75
13 changed files with 370 additions and 47 deletions

View file

@ -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>

View 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>

View 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()
}
}
}
}

View 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>

View 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>

View 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>

View 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>

View 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}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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 })
}
}

View file

@ -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);
}