Merge pull request #50 from LIUBINfighter/image-modal

Image selection modal 图片选取模态框
This commit is contained in:
felix.D 2025-04-27 08:28:40 +08:00 committed by GitHub
commit 6b488e4fc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 267 additions and 31 deletions

View File

@ -1,4 +1,9 @@
releases:
- version: "0.1.7"
features:
- "Added image selector modal, allowing users to select and upload images in chat"
- "Support for searching and selecting images from Obsidian vault"
- "Change bottom for uploading local image files"
- version: "0.1.6"
features:
- "update model select in chat view, add collected models "

View File

@ -5,6 +5,8 @@
[中文文档](README_zh-CN.md)
## New Version
[0.1.7](https://github.com/infiolab/infio-copilot/releases/tag/0.1.7) Added image selector modal, allowing users to search, select, and upload images in obsidian vault or local file browser
[0.1.6](https://github.com/infiolab/infio-copilot/releases/tag/0.1.6) update apply view, you can edit content in apply view
## Features

View File

@ -8,6 +8,8 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/felixduan)
## 最新版本
[0.1.7](https://github.com/infiolab/infio-copilot/releases/tag/0.1.7) 添加图片选择器模态框,允许用户在聊天中搜索、选择和上传图片
## 功能特点

View File

@ -1,7 +1,7 @@
{
"id": "infio-copilot",
"name": "Infio Copilot",
"version": "0.1.6",
"version": "0.1.7",
"minAppVersion": "0.15.0",
"description": "A Cursor-inspired AI assistant for notes that offers smart autocomplete and interactive chat with your selected notes",
"author": "Felix.D",

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,120 @@
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, modalEl } = this
// 添加特定的CSS类以便我们可以定位模态框
modalEl.addClass('mod-image-selector')
const root = createRoot(contentEl)
root.render(
<ImageSelector
onClose={() => this.close()}
onSelectImages={this.onSelectImages}
onSelectVaultImages={this.onSelectVaultImages}
app={this.app}
/>
)
}
onClose(): void {
const { contentEl, modalEl } = this
// 移除特定的CSS类
modalEl.removeClass('mod-image-selector')
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,99 @@ button.infio-chat-input-model-select {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.infio-image-selector {
padding: var(--size-4-3); /* 减小内边距 */
min-width: 450px; /* 减小最小宽度 */
max-width: 700px; /* 减小最大宽度 */
/* 禁用外部容器的滚动 */
overflow: hidden;
/* 确保模态框内容不会超出视口高度 */
max-height: 80vh; /* 减小最大高度 */
display: flex;
flex-direction: column;
}
.infio-image-selector-header {
display: flex;
gap: var(--size-2-1); /* 减小间距 */
margin-bottom: var(--size-4-2); /* 减小底部边距 */
/* 确保头部不会收缩 */
flex-shrink: 0;
}
.infio-image-search {
flex: 1;
padding: var(--size-2-1); /* 减小内边距 */
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background-color: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-small); /* 减小字体大小 */
height: 28px; /* 固定高度 */
}
.infio-upload-button {
padding: var(--size-2-1) var(--size-4-3); /* 减小内边距 */
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: var(--radius-s);
cursor: pointer;
font-size: var(--font-ui-smaller); /* 减小字体大小 */
display: inline-flex;
align-items: center;
gap: var(--size-2-1); /* 减小间距 */
height: 28px; /* 固定高度 */
}
.infio-image-grid {
display: grid;
/* 减小每个图片项的最小宽度,使每行可以显示更多图片 */
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--size-4-2); /* 减小间距 */
/* 允许内部容器滚动 */
overflow-y: auto;
padding: var(--size-2-2);
/* 使用flex-grow确保网格占用剩余空间 */
flex-grow: 1;
/* 减小最大高度,使网格更紧凑 */
max-height: calc(70vh - 80px);
}
.infio-image-item {
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: var(--size-2-1); /* 减小内边距 */
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: 100px;
object-fit: cover;
border-radius: var(--radius-xs);
}
.infio-image-name {
margin-top: var(--size-2-1); /* 减小上边距 */
font-size: var(--font-ui-smallest); /* 使用更小的字体 */
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-muted); /* 使用更淡的颜色 */
line-height: 1.2; /* 减小行高 */
}
/* 禁用Obsidian模态框的滚动 */
.modal.mod-image-selector .modal-content {
overflow: hidden !important;
}