Skip to content

Commit 45a25f3

Browse files
committed
feat(#279): 本地图片支持相对路径引入,文件管理器增加图片预览 (#294)
* feat: 支持相对路径图片 * fix: 下载配置未正常保存导致同步失败 * feat: 文件设置增加写作资源路径,可自定义保存静态资源的路径 * feat: 写作文件管理器中可展示图片文件 * feat: 写作资源文件夹图标、图片文件图标 * feat: 图片静态资源预览
1 parent 79b9200 commit 45a25f3

File tree

17 files changed

+327
-166
lines changed

17 files changed

+327
-166
lines changed

messages/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@
255255
"errorDesc": "Unable to select workspace directory, please try again",
256256
"resetError": "Workspace Reset Failed",
257257
"resetErrorDesc": "Unable to reset to default workspace, please try again"
258+
},
259+
"assets": {
260+
"title": "Assets Path",
261+
"desc": "Set the path where resources (e.g. images, videos, files etc.) used in writing will be saved. Resources will be saved at the same level as the currently edited markdown file.",
262+
"select": "Set the path where resources used in writing will be saved"
258263
}
259264
}
260265
},

messages/ja.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@
248248
"errorDesc": "ワークスペースディレクトリを選択できません。再試行してください。",
249249
"resetError": "ワークスペースのリセットに失敗しました",
250250
"resetErrorDesc": "デフォルトのワークスペースにリセットできません。再試行してください。"
251+
},
252+
"assets": {
253+
"title": "アセット",
254+
"desc": "執筆中のリソース(例:画像、ビデオ、ファイル等)の保存パスを設定します。現在編集中の markdown ファイルと同じレベルに保存されます。",
255+
"select": "執筆中のリソースの保存パスを設定してください"
251256
}
252257
}
253258
},

messages/zh.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@
248248
"errorDesc": "无法选择工作区目录,请重试",
249249
"resetError": "重置工作区失败",
250250
"resetErrorDesc": "无法重置为默认工作区,请重试"
251+
},
252+
"assets": {
253+
"title": "写作资源路径",
254+
"desc": "设置写作资源的保存路径,例如图片、视频、文件等,与当前编辑的 markdown 文件同级。",
255+
"select": "请设置写作资源路径,例如:assets"
251256
}
252257
}
253258
},

src-tauri/capabilities/default.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
},
2929
{
3030
"path": "$APPDATA/**"
31+
},
32+
{
33+
"path": "**"
3134
}
3235
]
3336
},

src/app/core/article/file/file-item.tsx

Lines changed: 110 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator,
22
import { Input } from "@/components/ui/input";
33
import useArticleStore, { DirTree } from "@/stores/article";
44
import { BaseDirectory, exists, readTextFile, remove, rename, writeTextFile } from "@tauri-apps/plugin-fs";
5-
import { appDataDir } from '@tauri-apps/api/path';
6-
import { Cloud, CloudDownload, File } from "lucide-react"
5+
import { Cloud, CloudDownload, File, ImageIcon } from "lucide-react"
76
import { useEffect, useRef, useState } from "react";
87
import { ask } from '@tauri-apps/plugin-dialog';
98
import { Store } from '@tauri-apps/plugin-store';
@@ -14,6 +13,9 @@ import { computedParentPath, getCurrentFolder } from "@/lib/path";
1413
import { toast } from "@/hooks/use-toast";
1514
import { useTranslations } from "next-intl";
1615
import useClipboardStore from "@/stores/clipboard";
16+
import { PhotoProvider, PhotoView } from "react-photo-view";
17+
import { convertImageByWorkspace } from "@/lib/utils";
18+
import { appDataDir } from '@tauri-apps/api/path';
1719

1820
export function FileItem({ item }: { item: DirTree }) {
1921
const [isEditing, setIsEditing] = useState(item.isEditing)
@@ -22,16 +24,28 @@ export function FileItem({ item }: { item: DirTree }) {
2224
const { activeFilePath, setActiveFilePath, readArticle, setCurrentArticle, fileTree, setFileTree, loadFileTree } = useArticleStore()
2325
const { setClipboardItem, clipboardItem, clipboardOperation } = useClipboardStore()
2426
const t = useTranslations('article.file')
27+
const [imageUrl, setImageUrl] = useState('')
2528

2629
const path = computedParentPath(item)
2730
const isRoot = path.split('/').length === 1
2831
const folderPath = path.includes('/') ? path.split('/').slice(0, -1).join('/') : ''
2932
const cacheTree = cloneDeep(fileTree)
3033
const currentFolder = getCurrentFolder(folderPath, cacheTree)
3134

32-
function handleSelectFile() {
33-
setActiveFilePath(computedParentPath(item))
34-
readArticle(computedParentPath(item), item.sha, item.isLocale)
35+
async function handleSelectFile() {
36+
if (item.name.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i)) {
37+
let path = ''
38+
if (item.isLocale) {
39+
path = computedParentPath(item)
40+
} else {
41+
path = activeFilePath
42+
}
43+
const url = await convertImageByWorkspace(path)
44+
setImageUrl(url)
45+
} else {
46+
setActiveFilePath(computedParentPath(item))
47+
readArticle(computedParentPath(item), item.sha, item.isLocale)
48+
}
3549
}
3650

3751
async function handleDeleteFile() {
@@ -381,72 +395,96 @@ export function FileItem({ item }: { item: DirTree }) {
381395
}, [item])
382396

383397
return (
384-
<ContextMenu>
385-
<ContextMenuTrigger>
386-
<div
387-
className={`${path === activeFilePath ? 'file-manange-item active' : 'file-manange-item'} ${!isRoot && 'translate-x-5 !w-[calc(100%-22px)]'}`}
388-
onClick={handleSelectFile}
389-
onContextMenu={handleSelectFile}
390-
>
391-
{
392-
isEditing ?
393-
<div className="flex gap-1 items-center w-full select-none">
394-
<span className={item.parent ? 'size-0' : 'size-4 ml-1'} />
395-
<File className="size-4" />
396-
<Input
397-
ref={inputRef}
398-
className="h-5 rounded-sm text-xs px-1 font-normal flex-1 mr-1"
399-
value={name}
400-
onBlur={handleRename}
401-
onChange={(e) => { setName(e.target.value) }}
402-
onKeyDown={(e) => {
403-
if (e.code === 'Enter' && !e.nativeEvent.isComposing) {
404-
handleRename()
405-
} else if (e.code === 'Escape') {
406-
handleEditEnd()
407-
}
408-
}}
409-
/>
410-
</div> :
411-
<span draggable onDragStart={handleDragStart}
412-
className={`${item.isLocale ? '' : 'opacity-50'} flex justify-between flex-1 select-none items-center gap-1 dark:hover:text-white`}>
413-
<div className="flex flex-1 gap-1 select-none relative">
414-
<span className={item.parent ? 'size-0' : 'size-4 ml-1'}></span>
415-
<div className="relative">
416-
{ item.isLocale ? <File className="size-4" /> : <CloudDownload className="size-4" /> }
417-
{ item.sha && item.isLocale && <Cloud className="size-2.5 absolute left-0 bottom-0 z-10 bg-primary-foreground" /> }
418-
</div>
419-
<span className="text-xs flex-1 line-clamp-1">{item.name}</span>
420-
</div>
421-
</span>
422-
}
423-
</div>
424-
</ContextMenuTrigger>
425-
<ContextMenuContent>
426-
<ContextMenuItem inset onClick={handleShowFileManager}>
427-
{t('context.viewDirectory')}
428-
</ContextMenuItem>
429-
<ContextMenuSeparator />
430-
<ContextMenuItem inset disabled={!item.isLocale} onClick={handleCutFile}>
431-
{t('context.cut')}
432-
</ContextMenuItem>
433-
<ContextMenuItem inset onClick={handleCopyFile}>
434-
{t('context.copy')}
435-
</ContextMenuItem>
436-
<ContextMenuItem inset disabled={!clipboardItem} onClick={handlePasteFile}>
437-
{t('context.paste')}
438-
</ContextMenuItem>
439-
<ContextMenuSeparator />
440-
<ContextMenuItem disabled={!item.isLocale} inset onClick={handleStartRename}>
441-
{t('context.rename')}
442-
</ContextMenuItem>
443-
<ContextMenuItem disabled={!item.sha} inset className="text-red-900" onClick={handleDeleteSyncFile}>
444-
{t('context.deleteSyncFile')}
445-
</ContextMenuItem>
446-
<ContextMenuItem disabled={!item.isLocale} inset className="text-red-900" onClick={handleDeleteFile}>
447-
{t('context.deleteLocalFile')}
448-
</ContextMenuItem>
449-
</ContextMenuContent>
450-
</ContextMenu>
398+
<>
399+
<ContextMenu>
400+
<ContextMenuTrigger>
401+
<div
402+
className={`${path === activeFilePath ? 'file-manange-item active' : 'file-manange-item'} ${!isRoot && 'translate-x-5 !w-[calc(100%-22px)]'}`}
403+
onClick={handleSelectFile}
404+
onContextMenu={handleSelectFile}
405+
>
406+
{
407+
isEditing ?
408+
<div className="flex gap-1 items-center w-full select-none">
409+
<span className={item.parent ? 'size-0' : 'size-4 ml-1'} />
410+
<File className="size-4" />
411+
<Input
412+
ref={inputRef}
413+
className="h-5 rounded-sm text-xs px-1 font-normal flex-1 mr-1"
414+
value={name}
415+
onBlur={handleRename}
416+
onChange={(e) => { setName(e.target.value) }}
417+
onKeyDown={(e) => {
418+
if (e.code === 'Enter' && !e.nativeEvent.isComposing) {
419+
handleRename()
420+
} else if (e.code === 'Escape') {
421+
handleEditEnd()
422+
}
423+
}}
424+
/>
425+
</div> :
426+
item.name.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i) ?
427+
<PhotoProvider>
428+
<PhotoView src={imageUrl}>
429+
<span
430+
draggable
431+
onDragStart={handleDragStart}
432+
title={item.name}
433+
className={`${item.isLocale ? '' : 'opacity-50'} flex justify-between flex-1 select-none items-center gap-1 dark:hover:text-white`}>
434+
<div className="flex flex-1 gap-1 select-none relative">
435+
<span className={item.parent ? 'size-0' : 'size-4 ml-1'}></span>
436+
<div className="relative">
437+
<ImageIcon className="size-4" />
438+
{ item.sha && item.isLocale && <Cloud className="size-2.5 absolute left-0 bottom-0 z-10 bg-primary-foreground" /> }
439+
</div>
440+
<span className="text-xs flex-1 line-clamp-1">{item.name}</span>
441+
</div>
442+
</span>
443+
</PhotoView>
444+
</PhotoProvider> :
445+
<span
446+
draggable
447+
onDragStart={handleDragStart}
448+
title={item.name}
449+
className={`${item.isLocale ? '' : 'opacity-50'} flex justify-between flex-1 select-none items-center gap-1 dark:hover:text-white`}>
450+
<div className="flex flex-1 gap-1 select-none relative">
451+
<span className={item.parent ? 'size-0' : 'size-4 ml-1'}></span>
452+
<div className="relative">
453+
{ item.isLocale ? <File className="size-4" /> : <CloudDownload className="size-4" /> }
454+
{ item.sha && item.isLocale && <Cloud className="size-2.5 absolute left-0 bottom-0 z-10 bg-primary-foreground" /> }
455+
</div>
456+
<span className="text-xs flex-1 line-clamp-1">{item.name}</span>
457+
</div>
458+
</span>
459+
}
460+
</div>
461+
</ContextMenuTrigger>
462+
<ContextMenuContent>
463+
<ContextMenuItem inset onClick={handleShowFileManager}>
464+
{t('context.viewDirectory')}
465+
</ContextMenuItem>
466+
<ContextMenuSeparator />
467+
<ContextMenuItem inset disabled={!item.isLocale} onClick={handleCutFile}>
468+
{t('context.cut')}
469+
</ContextMenuItem>
470+
<ContextMenuItem inset onClick={handleCopyFile}>
471+
{t('context.copy')}
472+
</ContextMenuItem>
473+
<ContextMenuItem inset disabled={!clipboardItem} onClick={handlePasteFile}>
474+
{t('context.paste')}
475+
</ContextMenuItem>
476+
<ContextMenuSeparator />
477+
<ContextMenuItem disabled={!item.isLocale} inset onClick={handleStartRename}>
478+
{t('context.rename')}
479+
</ContextMenuItem>
480+
<ContextMenuItem disabled={!item.sha} inset className="text-red-900" onClick={handleDeleteSyncFile}>
481+
{t('context.deleteSyncFile')}
482+
</ContextMenuItem>
483+
<ContextMenuItem disabled={!item.isLocale} inset className="text-red-900" onClick={handleDeleteFile}>
484+
{t('context.deleteLocalFile')}
485+
</ContextMenuItem>
486+
</ContextMenuContent>
487+
</ContextMenu>
488+
</>
451489
)
452490
}

src/app/core/article/file/file-manager.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import React, { useEffect, useState } from "react"
1111
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"
1212
import useArticleStore, { DirTree } from "@/stores/article"
13-
import { BaseDirectory, rename, writeTextFile } from "@tauri-apps/plugin-fs"
13+
import { BaseDirectory, rename, writeTextFile, writeFile } from "@tauri-apps/plugin-fs"
1414
import { FileItem } from './file-item'
1515
import { FolderItem } from "./folder-item"
1616
import { computedParentPath } from "@/lib/path"
@@ -86,6 +86,7 @@ export function FileManager() {
8686
const files = e.dataTransfer.files
8787
for (let i = 0; i < files.length; i += 1) {
8888
const file = files[i]
89+
// 接受 markdown 和图片文件
8990
if (file.name.endsWith('.md')) {
9091
const text = await file.text()
9192
await writeTextFile(`article/${file.name}`, text, { baseDir: BaseDirectory.AppData })
@@ -97,6 +98,19 @@ export function FileManager() {
9798
isFile: true,
9899
isSymlink: false
99100
})
101+
} else if (file.name.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i)) {
102+
// 处理图片文件
103+
const arrayBuffer = await file.arrayBuffer()
104+
const uint8Array = new Uint8Array(arrayBuffer)
105+
await writeFile(`article/${file.name}`, uint8Array, { baseDir: BaseDirectory.AppData })
106+
addFile({
107+
name: file.name,
108+
isEditing: false,
109+
isLocale: true,
110+
isDirectory: false,
111+
isFile: true,
112+
isSymlink: false
113+
})
100114
}
101115
}
102116
}

src/app/core/article/file/folder-item.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Input } from "@/components/ui/input";
33
import useArticleStore, { DirTree } from "@/stores/article";
44
import { BaseDirectory, exists, mkdir, readDir, readTextFile, remove, rename, writeTextFile } from "@tauri-apps/plugin-fs";
55
import { appDataDir } from '@tauri-apps/api/path';
6-
import { ChevronRight, Cloud, Folder, FolderDown } from "lucide-react"
6+
import { ChevronRight, Cloud, Folder, FolderDot, FolderDown, FolderOpen, FolderOpenDot } from "lucide-react"
77
import { useEffect, useRef, useState } from "react";
88
import { CollapsibleTrigger } from "@/components/ui/collapsible";
99
import { toast } from "@/hooks/use-toast";
@@ -13,6 +13,7 @@ import { computedParentPath, getCurrentFolder } from "@/lib/path";
1313
import { useTranslations } from "next-intl";
1414
import useClipboardStore from "@/stores/clipboard";
1515
import { ask } from '@tauri-apps/plugin-dialog';
16+
import useSettingStore from '@/stores/setting'
1617

1718
export function FolderItem({ item }: { item: DirTree }) {
1819
const [isEditing, setIsEditing] = useState(item.isEditing)
@@ -21,6 +22,7 @@ export function FolderItem({ item }: { item: DirTree }) {
2122
const inputRef = useRef<HTMLInputElement>(null)
2223
const t = useTranslations('article.file')
2324
const { setClipboardItem, clipboardItem, clipboardOperation } = useClipboardStore()
25+
const { assetsPath } = useSettingStore()
2426

2527
const {
2628
activeFilePath,
@@ -442,7 +444,10 @@ export function FolderItem({ item }: { item: DirTree }) {
442444
>
443445
<div className="flex flex-1 gap-1 select-none relative">
444446
<div className="relative">
445-
{item.isLocale ? <Folder className="size-4" /> : <FolderDown className="size-4" /> }
447+
{collapsibleList.includes(path) ?
448+
(assetsPath === item.name ? <FolderOpenDot className="size-4" /> : <FolderOpen className="size-4" />) :
449+
(assetsPath === item.name ? <FolderDot className="size-4" /> : <Folder className="size-4" />)
450+
}
446451
{item.sha && item.isLocale && <Cloud className="size-2.5 absolute left-0 bottom-0 z-10 bg-primary-foreground" />}
447452
</div>
448453
<span className="text-xs line-clamp-1">{item.name}</span>

0 commit comments

Comments
 (0)