feat: add image-select modal (#49)

This commit is contained in:
JayBridge 2025-04-24 23:18:15 +08:00
parent 1b43bac6a0
commit 6db44f5ea0
3 changed files with 212 additions and 14 deletions

View File

@ -1,30 +1,40 @@
import { ImageIcon } from 'lucide-react'
import { TFile } from 'obsidian'
import { useApp } from '../../../contexts/AppContext'
import { ImageSelectorModal } from '../../modals/ImageSelectorModal'
export function ImageUploadButton({
onUpload,
}: {
onUpload: (files: File[]) => void
}) {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? [])
if (files.length > 0) {
onUpload(files)
const app = useApp()
const handleClick = () => {
const handleVaultImages = async (files: TFile[]) => {
const imageFiles = await Promise.all(
files.map(async (file) => {
const arrayBuffer = await app.vault.readBinary(file)
const blob = new Blob([arrayBuffer], { type: `image/${file.extension}` })
return new File([blob], file.name, { type: `image/${file.extension}` })
})
)
onUpload(imageFiles)
}
new ImageSelectorModal(app, onUpload, handleVaultImages).open()
}
return (
<label className="infio-chat-user-input-submit-button">
<input
type="file"
accept="image/*"
multiple
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<button
className="infio-chat-user-input-submit-button"
onClick={handleClick}
>
<div className="infio-chat-user-input-submit-button-icons">
<ImageIcon size={12} />
</div>
<div>Image</div>
</label>
</button>
)
}

View File

@ -0,0 +1,114 @@
import { App, Modal, TFile } from 'obsidian'
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
type ImageSelectorProps = {
onClose: () => void
onSelectImages: (files: File[]) => void
onSelectVaultImages: (files: TFile[]) => void
app: App
}
const ImageSelector: React.FC<ImageSelectorProps> = ({
onClose,
onSelectImages,
onSelectVaultImages,
app,
}) => {
const [searchTerm, setSearchTerm] = useState('')
const [vaultImages, setVaultImages] = useState<TFile[]>([])
useEffect(() => {
const images = app.vault.getFiles()
.filter((file) => file.extension.match(/png|jpg|jpeg|gif|svg/i))
.filter((file) =>
searchTerm ? file.path.toLowerCase().includes(searchTerm.toLowerCase()) : true
)
setVaultImages(images)
}, [searchTerm, app.vault])
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files ?? [])
if (files.length > 0) {
onSelectImages(files)
onClose()
}
}
return (
<div className="infio-image-selector">
<div className="infio-image-selector-header">
<input
type="text"
placeholder="Search images in vault..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="infio-image-search"
/>
<label className="infio-upload-button">
Upload New Image
<input
type="file"
accept="image/*"
multiple
onChange={handleFileUpload}
style={{ display: 'none' }}
/>
</label>
</div>
<div className="infio-image-grid">
{vaultImages.map((file) => (
<div
key={file.path}
className="infio-image-item"
onClick={() => {
onSelectVaultImages([file])
onClose()
}}
>
<img
src={app.vault.adapter.getResourcePath(file.path)}
alt={file.name}
/>
<div className="infio-image-name">{file.name}</div>
</div>
))}
</div>
</div>
)
}
export class ImageSelectorModal extends Modal {
private readonly onSelectImages: (files: File[]) => void
private readonly onSelectVaultImages: (files: TFile[]) => void
constructor(
app: App,
onSelectImages: (files: File[]) => void,
onSelectVaultImages: (files: TFile[]) => void,
) {
super(app)
this.onSelectImages = onSelectImages
this.onSelectVaultImages = onSelectVaultImages
}
onOpen(): void {
const { contentEl } = this
const root = createRoot(contentEl)
root.render(
<ImageSelector
onClose={() => this.close()}
onSelectImages={this.onSelectImages}
onSelectVaultImages={this.onSelectVaultImages}
app={this.app}
/>
)
}
onClose(): void {
const { contentEl } = this
contentEl.empty()
}
}

View File

@ -2036,8 +2036,9 @@ button.infio-chat-input-model-select {
white-space: pre-wrap;
word-break: break-word;
user-select: text;
display: -webkit-box;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
@ -2148,3 +2149,76 @@ button.infio-chat-input-model-select {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.infio-image-selector {
padding: var(--size-4-4);
min-width: 500px;
max-width: 800px;
}
.infio-image-selector-header {
display: flex;
gap: var(--size-2-2);
margin-bottom: var(--size-4-4);
}
.infio-image-search {
flex: 1;
padding: var(--size-2-2);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background-color: var(--background-primary);
color: var(--text-normal);
}
.infio-upload-button {
padding: var(--size-2-2) var(--size-4-4);
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: var(--radius-s);
cursor: pointer;
font-size: var(--font-ui-small);
display: inline-flex;
align-items: center;
gap: var(--size-2-2);
}
.infio-image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--size-4-4);
max-height: 400px;
overflow-y: auto;
padding: var(--size-2-2);
}
.infio-image-item {
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: var(--size-2-2);
cursor: pointer;
transition: all 0.2s ease;
background-color: var(--background-primary);
}
.infio-image-item:hover {
background-color: var(--background-modifier-hover);
}
.infio-image-item img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: var(--radius-xs);
}
.infio-image-name {
margin-top: var(--size-2-2);
font-size: var(--font-ui-smaller);
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-normal);
}