Skip to content

Commit

Permalink
1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
AriTheElk committed Dec 14, 2024
1 parent bd71103 commit f4a0fb1
Show file tree
Hide file tree
Showing 13 changed files with 486 additions and 270 deletions.
8 changes: 8 additions & 0 deletions esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ const context = await esbuild.context({
from: ['versions.json'],
to: ['./versions.json'],
},
{
from: ['dist/main.js'],
to: ['../main.js'],
},
{
from: ['dist/styles.css'],
to: ['../styles.css'],
},
],
}),
],
Expand Down
107 changes: 107 additions & 0 deletions main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Adds a UI panel for quickly selecting images that are in your vault.",
"author": "ari.the.elk.wtf",
"author": "ari.the.elk",
"authorUrl": "https://ari.the.elk.wtf",
"fundingUrl": "https://ari.the.elk.wtf/Donate",
"isDesktopOnly": false
Expand Down
128 changes: 128 additions & 0 deletions src/ImagePicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Plugin, TFile, WorkspaceLeaf } from 'obsidian'
import { pick } from 'lodash'
import { Indexer } from './backend/Indexer'
import {
DEFAULT_SETTINGS,
ImagePickerSettings,
ImagePickerSettingTab,
} from './ImagePickerSettings'
import { ImagePickerView } from './ImagePickerView'
import { VALID_IMAGE_EXTENSIONS, VIEW_TYPE_IMAGE_PICKER } from './constants'

export class ImagePicker extends Plugin {
settings: ImagePickerSettings
images: TFile[] = []
indexer: Indexer = new Indexer(this)

log = (...args: any[]) => {
if (this.settings?.debugMode) {
console.log('ImagePicker -> ', ...args)
}
}

async onload() {
await this.loadSettings()

// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new ImagePickerSettingTab(this.app, this))

this.addRibbonIcon('image', 'Open Image Picker', async () => {
this.activateView()
})

this.registerView(
VIEW_TYPE_IMAGE_PICKER,
(leaf) => new ImagePickerView(this, leaf)
)

this.app.vault.on('create', this.onFileCreate)
this.app.vault.on('modify', this.onFileChange)
this.app.vault.on('delete', this.onFileDelete)
}

onunload() {
this.app.vault.off('create', this.onFileCreate)
this.app.vault.off('modify', this.onFileChange)
this.app.vault.off('delete', this.onFileDelete)
}

onFileCreate = async (file: TFile) => {
if (file instanceof TFile) {
if (
file.path.startsWith(this.settings.imageFolder) &&
VALID_IMAGE_EXTENSIONS.includes(file.extension)
) {
this.log('onFileCreate:', file.path)
this.indexer.setIndex({
[file.path]: {
...pick(file, ['basename', 'extension', 'stat', 'path', 'name']),
uri: this.app.vault.getResourcePath(file),
},
})
this.indexer.notifySubscribers()
}
}
}

onFileDelete = async (file: TFile) => {
if (file instanceof TFile) {
if (
file.path.startsWith(this.settings.imageFolder) &&
VALID_IMAGE_EXTENSIONS.includes(file.extension)
) {
this.log('onFileDelete:', file.path)
this.indexer.removeIndex(file.path)
this.indexer.notifySubscribers()
}
}
}

onFileChange = async (file: TFile) => {
if (file instanceof TFile) {
if (
file.path.startsWith(this.settings.imageFolder) &&
VALID_IMAGE_EXTENSIONS.includes(file.extension)
) {
this.indexer.setIndex({
[file.path]: {
...pick(file, ['basename', 'extension', 'stat', 'path', 'name']),
uri: this.app.vault.getResourcePath(file),
},
})
this.indexer.notifySubscribers()
}
}
}

async activateView() {
const { workspace } = this.app

let leaf: WorkspaceLeaf | null = null
const leaves = workspace.getLeavesOfType(VIEW_TYPE_IMAGE_PICKER)

if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0]
} else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false)
await leaf?.setViewState({ type: VIEW_TYPE_IMAGE_PICKER, active: true })
}

// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf) {
workspace.revealLeaf(leaf)
}
}

async loadSettings() {
this.log('Loading settings...')
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
}

async saveSettings() {
this.log('Saving settings:', this.settings)
await this.saveData(this.settings)
}
}
95 changes: 95 additions & 0 deletions src/ImagePickerSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { App, PluginSettingTab, Setting } from 'obsidian'
import { ImagePicker } from './ImagePicker'

export interface ImagePickerSettings {
imageFolder: string
animateGifs: boolean
debugMode: boolean
}

export const DEFAULT_SETTINGS: ImagePickerSettings = {
imageFolder: '',
animateGifs: false,
debugMode: false,
}

export class ImagePickerSettingTab extends PluginSettingTab {
plugin: ImagePicker

constructor(app: App, plugin: ImagePicker) {
super(app, plugin)
this.plugin = plugin
}

display(): void {
const { containerEl } = this
containerEl.empty()

// Input for selecting the image folder
new Setting(containerEl)
.setName('Image Folder')
.setDesc(
'Image picker will look for images in this folder and its subfolders, by default it will look in the root of the vault'
)
.addText((text) =>
text
.setPlaceholder('Image Folder')
.setValue(this.plugin.settings.imageFolder)
.onChange(async (value) => {
this.plugin.settings.imageFolder = value || ''
await this.plugin.saveSettings()
})
)

// Button for resetting the image index
new Setting(containerEl)
.setName('Reset Image Index')
.setDesc(
'Clears the image index and rebuilds it from the image folder. Obsidian will reload immediately after. Please run this after changing the image folder.'
)
.addButton((button) =>
button.setButtonText('Reset Index').onClick(async () => {
this.plugin.images = []
// delete the database and rebuild it
await this.plugin.indexer.resetDB()
// reload obsidian
// @ts-ignore
this.app.commands.executeCommandById('app:reload')
})
)

// Toggle whether gifs are animated
new Setting(containerEl)
.setName('Animate GIFs')
.setDesc('Warning: large gifs can slow down or crash Obsidian')
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.animateGifs)
.onChange(async (value) => {
this.plugin.settings.animateGifs = value
await this.plugin.saveSettings()
})
)

// Toggle whether to log debug messages
new Setting(containerEl)
.setName('Debug Mode')
.setDesc('Log debug messages to the console')
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.debugMode)
.onChange(async (value) => {
this.plugin.settings.debugMode = value
await this.plugin.saveSettings()
})
)

containerEl.createEl('hr')

const credits = containerEl.createEl('div')
credits.innerHTML = `
Built with 💚 by <a href="https://ari.the.elk.wtf">ari.the.elk</a><br />
📖 <a href="https://ari.the.elk.wtf/obsidian/plugins/image-picker">documentation</a><br />
💝 <a href="https://ari.the.elk.wtf/donate">donate</a>`
}
}
73 changes: 73 additions & 0 deletions src/ImagePickerView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react'
import { ItemView, WorkspaceLeaf } from 'obsidian'
import { Root, createRoot } from 'react-dom/client'
import { ImagePickerView as ReactImagePickerView } from './client/ImagePickerView'
import { ImagePickerContext } from './client/ImagePickerContext'
import { ImagePicker } from './ImagePicker'
import { VIEW_TYPE_IMAGE_PICKER } from './constants'

// Image picker view class
export class ImagePickerView extends ItemView {
root: Root | null = null

constructor(public plugin: ImagePicker, leaf: WorkspaceLeaf) {
super(leaf)
}

getViewType() {
return VIEW_TYPE_IMAGE_PICKER
}

getDisplayText() {
return 'Image Picker'
}

getIcon(): string {
return 'image'
}

mountReact = async () => {
this.root = createRoot(this.containerEl.children[1])
this.root.render(
<ImagePickerContext.Provider
value={{
app: this.app,
plugin: this.plugin,
files: Object.values(await this.plugin.indexer.getIndex()),
}}
>
<ReactImagePickerView />
</ImagePickerContext.Provider>
)
}

unmountReact = () => {
this.root?.unmount()
this.containerEl.children[1].empty()
}

async onOpen() {
this.plugin.log('Opening root:', this.plugin.images.length)
await this.mountReact()

this.plugin.indexer.subscribe(async (newIndex) => {
this.plugin.log('Rerendering root:', Object.keys(newIndex).length)
// this.mountReact()
this.root?.render(
<ImagePickerContext.Provider
value={{
app: this.app,
plugin: this.plugin,
files: Object.values(newIndex),
}}
>
<ReactImagePickerView />
</ImagePickerContext.Provider>
)
})
}

async onClose() {
this.root?.unmount()
}
}
25 changes: 22 additions & 3 deletions src/backend/Indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ export class Indexer {
}

log = (...args: any[]) => {
if (process.env.NODE_ENV === 'production') return
this.plugin.log('Indexer -> ', ...args)
}

Expand All @@ -82,6 +81,13 @@ export class Indexer {
}

getThumbnail = async (node: IndexerNode): Promise<Thumbnail> => {
if (node.extension === 'gif' && this.plugin.settings?.animateGifs) {
return {
id: 'gif',
data: node.uri,
}
}

const cachedThumbnail =
node.thumbnail &&
(await this.db.thumbnails.where('id').equals(node.thumbnail).first())
Expand Down Expand Up @@ -155,8 +161,18 @@ export class Indexer {
*/
removeIndex = async (path: string) => {
this.log('Removing index:', path)
const node = await this.db.index.get(path)
delete this.memory[path]
await this.db.index.delete(path)
if (node?.thumbnail) {
await this.db.thumbnails.delete(node.thumbnail)
}
this.notifySubscribers()
this.backgrounder.enqueue({
type: 'saveIndex',
disableDoubleQueue: true,
action: this.saveIndex,
})
}

getIndex = async (): Promise<IndexerRoot> => {
Expand Down Expand Up @@ -201,12 +217,15 @@ export class Indexer {

subscribe(callback: (index: IndexerRoot) => void) {
this.subscribers = [callback]
return () => {
this.subscribers = this.subscribers.filter((cb) => cb !== callback)
}
}

notifySubscribers = debounce((index?: IndexerRoot) => {
notifySubscribers = (index?: IndexerRoot) => {
this.log('Notifying subscribers:', this.subscribers.length)
this.subscribers.forEach(async (callback) =>
callback(index || (await this.getIndex()))
)
}, 2000)
}
}
Loading

0 comments on commit f4a0fb1

Please sign in to comment.