当前位置: 首页 > article >正文

字玩FontPlayer开发笔记8 Tauri2文件系统

字玩FontPlayer开发笔记8 Tauri2文件系统

字玩FontPlayer是笔者开源的一款字体设计工具,使用Vue3 + ElementUI开发,源代码:
github: https://github.com/HiToysMaker/fontplayer
gitee: https://gitee.com/toysmaker/fontplayer

笔记

字玩目前是用Electron进行桌面端应用打包,但是性能体验不太好,一直想替换成Tauri。Tauri的功能和Electron类似,都可以把前端代码打包生成桌面端(比如Windows和Mac)应用。Tauri只使用系统提供的WebView,不像Electron一样内置Chromium和Node.js,性能体验更佳。

近几天开始着手将Electron替换成Tauri,前两天完成了系统原生菜单的基本设置,今天将菜单功能实装。笔者项目中菜单功能最常用的就是文件存储和读取,所以今天主要学习了文件系统的内容。Tauri2提供了js端可调用的plugin,可以方便前端轻松实现文件操作。

权限配置

文件操作需要进行权限配置,Tauri2去掉了tauri.conf.json中的allowList一项,变成在src-tauri/capabilities/default.json中进行权限设置。
具体权限对应的选项在文档中Permission Table一栏中有详述:https://tauri.app/plugin/file-system/

笔者的配置:
src-tauri/capabilities/default.json

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "enables the default permissions",
  "windows": [
    "main"
  ],
  "permissions": [
    {
      "identifier": "fs:scope",
      "allow": [
        {
          "path": "$APPDATA"
        },
        {
          "path": "$APPDATA/**"
        }
      ]
    },
    "core:default",
    "fs:read-files",
    "fs:write-files",
    "fs:allow-appdata-read-recursive",
    "fs:allow-appdata-write-recursive",
    "fs:default",
    "dialog:default"
  ]
}
文件选择对话框

文件选择对话框的插件和文件操作的插件是分开的,首先安装对话框插件:

npm run tauri add dialog

打开文件选择窗口:

import { open } from '@tauri-apps/plugin-dialog'

const file = await open({
  multiple: false,
  directory: false,
})

打开文件存储窗口:

import { save } from '@tauri-apps/plugin-dialog'

const path = await save({
  defaultPath: 'untitled',
  filters: [
    {
      name: 'My Filter',
      extensions: ['png', 'jpeg'],
    },
  ],
});
文件操作

文件操作封装在fs插件中,插件分别提供对纯文本读写和二进制读写的方法。

安装插件:

npm run tauri add fs

读取纯文本:

import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'

const configToml = await readTextFile('config.toml', {
  baseDir: BaseDirectory.AppConfig,
})

读取二进制文本:

const icon = await readFile('icon.png', {
  baseDir: BaseDirectory.Resources,
})

写入纯文本:

import { writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'

const contents = JSON.stringify({ notifications: true });
await writeTextFile('config.json', contents, {
  baseDir: BaseDirectory.AppConfig,
});

写入二进制:

import { writeFile, BaseDirectory } from '@tauri-apps/plugin-fs'

const contents = new Uint8Array();
await writeFile('config', contents, {
  baseDir: BaseDirectory.AppConfig,
});
涉及文件操作的具体逻辑实现
保存文本文件
const nativeSaveText = async (data, filename, formats) => {
	const path = await save({
		defaultPath: filename,
		filters: [
			{
				name: 'Filter',
				extensions: formats,
			},
		],
	})
	if (path) {
		await writeTextFile(path, data)
	}
}
保存二进制文件
const nativeSaveBinary = async (data, filename, formats) => {
	const path = await save({
		defaultPath: filename,
		filters: [
			{
				name: 'Filter',
				extensions: formats,
			},
		],
	})
	if (path) {
		await writeFile(path, data)
	}
}
打开文本文件
const nativeImportTextFile = async (formats) => {
	const path = await open({
		filters: [
			{
				name: 'Filter',
				extensions: formats,
			},
		],
	})
	let data = null
	let name = 'untitled'
	if (path) {
		data = await readTextFile(path)
		name = path.split('/').pop().split('.')[0]
	}
	return {
		data,
		name,
	}
}
打开二进制文件
const nativeImportFile = async (formats) => {
	const path = await open({
		filters: [
			{
				name: 'Filter',
				extensions: formats,
			},
		],
	})
	let uint8Array = null
	let name = 'untitled'
	if (path) {
		uint8Array = await readFile(path)
		name = path.split('/').pop().split('.')[0]
	}
	return {
		uint8Array,
		name,
	}
}
打开工程
const openFile_tauri = async (rawdata) => {
	if (files.value && files.value.length) {
		tips.value = '目前字玩仅支持同时编辑一个工程,请关闭当前工程再打开新工程。注意,关闭工程前请保存工程以避免数据丢失。'
		tipsDialogVisible.value = true
	} else {
		const { data } = await nativeImportTextFile(['json'])
		await _openFile_electron(data)
	}
}
保存工程
const saveFile_tauri = async () => {
	setSaveDialogVisible(true)
}
另存为工程
const saveAs_tauri = async () => {
	setSaveDialogVisible(true)
}
导入字体库
const importFont_tauri = async () => {
	if (files.value && files.value.length) {
		tips.value = '目前字玩仅支持同时编辑一个工程,请关闭当前工程再导入字体。注意,关闭工程前请保存工程以避免数据丢失。'
		tipsDialogVisible.value = true
	} else {
		const options = await nativeImportFile(['otf', 'ttf'])
		await _importFont_tauri(options)
	}
}
导入字形
const importGlyphs_tauri = async () => {
	const { data: rawdata } = await nativeImportTextFile(['json'])
	if (!rawdata) return
	const data = JSON.parse(rawdata)
	const plainGlyphs = data.glyphs
	if (data.constants) {
		for (let n = 0; n < data.constants.length; n++) {
			if (!constantsMap.getByUUID(data.constants[n].uuid)) {
				constants.value.push(data.constants[n])
			}
		}
	}
	if (data.constantGlyphMap) {
		const keys = Object.keys(data.constantGlyphMap)
		for (let n = 0; n < keys.length; n++) {
			constantGlyphMap.set(keys[n], data.constantGlyphMap[keys[n]])
		}
	}
	const _glyphs = plainGlyphs.map((plainGlyph) => instanceGlyph(plainGlyph))
	_glyphs.map((glyph) => {
		addGlyph(glyph, editStatus.value)
		addGlyphTemplate(glyph, editStatus.value)
	})
	if (editStatus.value === Status.GlyphList) {
		emitter.emit('renderGlyphPreviewCanvas')
	} else if (editStatus.value === Status.StrokeGlyphList) {
		emitter.emit('renderStrokeGlyphPreviewCanvas')
	} else if (editStatus.value === Status.RadicalGlyphList) {
		emitter.emit('renderRadicalGlyphPreviewCanvas')
	} else if (editStatus.value === Status.CompGlyphList) {
		emitter.emit('renderCompGlyphPreviewCanvas')
	}
}
导入SVG
const importSVG_tauri = async () => {
	const { data: rawdata } = await nativeImportTextFile(['svg'])
	if (!rawdata) return
	const svgEl: HTMLElement = parseStrToSvg(rawdata).childNodes[0] as HTMLElement
	const components = parseSvgToComponents(svgEl as HTMLElement)
	components.forEach((component: IComponent) => {
		addComponentForCurrentCharacterFile(component)
	})
}
识别图片
const importPic_tauri = async () => {
	const options = await nativeImportFile(['jpg', 'png', 'jpeg'])
	const { name, uint8Array } = options
	let binary = ''
  uint8Array.forEach((byte) => {
    binary += String.fromCharCode(byte);
  })
  const base64str = btoa(binary)
	const type = name.split('.')[1] === 'png' ? 'imge/png' : 'image/jpeg'
	const dataUrl = `data:${type};base64,${base64str}`
	total.value = 0
	loaded.value = 0
	loading.value = true
	const img = document.createElement('img')
	img.onload = () => {
		setTimeout(() => {
			thumbnail(dataUrl, img, 1000)
			setEditStatus(Status.Pic)
			loading.value = false
		}, 100)
	}
	img.src = dataUrl
}
导出字体库
const exportFont_tauri = async (options: CreateFontOptions) => {
	const font = createFont(options)
	const buffer = toArrayBuffer(font) as ArrayBuffer
	const filename = `${selectedFile.value.name}.otf`
	nativeSaveBinary(buffer, filename, ['otf'])
}
导出字形
const exportGlyphs_tauri = async () => {
	if (editStatus.value === Status.GlyphList) {
		const _glyphs = glyphs.value.map((glyph: ICustomGlyph) => {
			return plainGlyph(glyph)
		})
		const data = JSON.stringify({
			glyphs: _glyphs,
			constants: constants.value,
			constantGlyphMap: mapToObject(constantGlyphMap),
			version: 1.0,
		})
		await nativeSaveText(data, `glyphs.json`, ['json'])
	} else if (editStatus.value === Status.StrokeGlyphList) {
		const _glyphs = stroke_glyphs.value.map((glyph: ICustomGlyph) => {
			return plainGlyph(glyph)
		})
		const data = JSON.stringify({
			glyphs: _glyphs,
			constants: constants.value,
			constantGlyphMap: mapToObject(constantGlyphMap),
			version: 1.0,
		})
		await nativeSaveText(data, `stroke_glyphs.json`, ['json'])
	} else if (editStatus.value === Status.RadicalGlyphList) {
		const _glyphs = radical_glyphs.value.map((glyph: ICustomGlyph) => {
			return plainGlyph(glyph)
		})
		const data = JSON.stringify({
			glyphs: _glyphs,
			constants: constants.value,
			constantGlyphMap: mapToObject(constantGlyphMap),
			version: 1.0,
		})
		await nativeSaveText(data, `radical_glyphs.json`, ['json'])
	} else if (editStatus.value === Status.CompGlyphList) {
		const _glyphs = comp_glyphs.value.map((glyph: ICustomGlyph) => {
			return plainGlyph(glyph)
		})
		const data = JSON.stringify({
			glyphs: _glyphs,
			constants: constants.value,
			constantGlyphMap: mapToObject(constantGlyphMap),
			version: 1.0,
		})
		nativeSaveText(data, `comp_glyphs.json`, ['json'])
	} else {
		const _glyphs = glyphs.value.map((glyph: ICustomGlyph) => {
			return plainGlyph(glyph)
		})
		const data = JSON.stringify({
			glyphs: _glyphs,
			constants: constants.value,
			constantGlyphMap: mapToObject(constantGlyphMap),
			version: 1.0,
		})
		await nativeSaveText(data, `glyphs.json`, ['json'])
	}
}
导出JPEG图片
const exportJPEG_tauri = async () => {
	// 导出JPEG
	const _canvas = canvas.value as HTMLCanvasElement
	const data = _canvas.toDataURL('image/jpeg')
	const buffer = base64ToArrayBuffer(data)
	const fileName = `${editCharacterFile.value.character.text}.jpg`
	nativeSaveBinary(buffer, fileName, ['jpg', 'jpeg'])
}
导出PNG图片
const exportPNG_tauri = async () => {
	// 导出PNG
	const _canvas = canvas.value as HTMLCanvasElement
	render(_canvas, false)
	const data = _canvas.toDataURL('image/png')
	const buffer = base64ToArrayBuffer(data)
	const fileName = `${editCharacterFile.value.character.text}.png`
	nativeSaveBinary(buffer, fileName, ['png'])
	render(_canvas, true)
}
导出SVG
const exportSVG_tauri = async () => {
	// 导出SVG
	if (editStatus.value !== Status.Edit && editStatus.value !== Status.Glyph ) return
	const components = editStatus.value === Status.Edit ? orderedListWithItemsForCurrentCharacterFile.value : orderedListWithItemsForCurrentGlyph.value
	const data = componentsToSvg(components, selectedFile.value.width, selectedFile.value.height)
	const fileName = `${editCharacterFile.value.character.text}.svg`
	nativeSaveText(data, fileName, ['svg'])
}
前端与Rust端通信

实现点击菜单按钮事件需要前后端通信,菜单由Rust生成并监听事件,但具体事件逻辑由前端实现。
Rust端代码,声明每个菜单按钮的事件函数,函数中逻辑比较简单,就是发送相应消息给前端,让前端知道目前要做的操作,具体逻辑在前端实现。

#[tauri::command]
fn create_file(app: AppHandle) {
  app.emit("create-file", ()).unwrap();
}

#[tauri::command]
fn open_file(app: AppHandle) {
  app.emit("open-file", ()).unwrap();
}

#[tauri::command]
fn save_file(app: AppHandle) {
  app.emit("save-file", ()).unwrap();
}

#[tauri::command]
fn save_as(app: AppHandle) {
  app.emit("save-as", ()).unwrap();
}

#[tauri::command]
fn undo(app: AppHandle) {
  app.emit("undo", ()).unwrap();
}

#[tauri::command]
fn redo(app: AppHandle) {
  app.emit("redo", ()).unwrap();
}

#[tauri::command]
fn cut(app: AppHandle) {
  app.emit("cut", ()).unwrap();
}

#[tauri::command]
fn copy(app: AppHandle) {
  app.emit("copy", ()).unwrap();
}

#[tauri::command]
fn paste(app: AppHandle) {
  app.emit("paste", ()).unwrap();
}

#[tauri::command]
fn del(app: AppHandle) {
  app.emit("delete", ()).unwrap();
}

#[tauri::command]
fn import_font_file(app: AppHandle) {
  app.emit("import-font-file", ()).unwrap();
}

#[tauri::command]
fn import_templates_file(app: AppHandle) {
  app.emit("import-templates-file", ()).unwrap();
}

#[tauri::command]
fn import_glyphs(app: AppHandle) {
  app.emit("import-glyphs", ()).unwrap();
}

#[tauri::command]
fn import_pic(app: AppHandle) {
  app.emit("import-pic", ()).unwrap();
}

#[tauri::command]
fn import_svg(app: AppHandle) {
  app.emit("import-svg", ()).unwrap();
}

#[tauri::command]
fn export_font_file(app: AppHandle) {
  app.emit("export-font-file", ()).unwrap();
}

#[tauri::command]
fn export_glyphs(app: AppHandle) {
  app.emit("export-glyphs", ()).unwrap();
}

#[tauri::command]
fn export_jpeg(app: AppHandle) {
  app.emit("export-jpeg", ()).unwrap();
}

#[tauri::command]
fn export_png(app: AppHandle) {
  app.emit("export-png", ()).unwrap();
}

#[tauri::command]
fn export_svg(app: AppHandle) {
  app.emit("export-svg", ()).unwrap();
}

#[tauri::command]
fn add_character(app: AppHandle) {
  app.emit("add-character", ()).unwrap();
}

#[tauri::command]
fn add_icon(app: AppHandle) {
  app.emit("add-icon", ()).unwrap();
}

#[tauri::command]
fn font_settings(app: AppHandle) {
  app.emit("font-settings", ()).unwrap();
}

#[tauri::command]
fn preference_settings(app: AppHandle) {
  app.emit("preference-settings", ()).unwrap();
}

#[tauri::command]
fn language_settings(app: AppHandle) {
  app.emit("language-settings", ()).unwrap();
}

#[tauri::command]
fn import_template1(app: AppHandle) {
  app.emit("template-1", ()).unwrap();
}

#[tauri::command]
fn remove_overlap(app: AppHandle) {
  app.emit("remove_overlap", ()).unwrap();
}

Rust端代码,监听事件:

app.on_menu_event(move |app, event| {
  if event.id() == "create-file" {
    create_file(app.app_handle().clone())
  } else if event.id() == "open-file" {
    open_file(app.app_handle().clone())
  } else if event.id() == "save-file" {
    save_file(app.app_handle().clone())
  } else if event.id() == "save-as" {
    save_as(app.app_handle().clone())
  } else if event.id() == "undo" {
    undo(app.app_handle().clone())
  } else if event.id() == "redo" {
    redo(app.app_handle().clone())
  } else if event.id() == "cut" {
    cut(app.app_handle().clone())
  } else if event.id() == "copy" {
    copy(app.app_handle().clone())
  } else if event.id() == "paste" {
    paste(app.app_handle().clone())
  } else if event.id() == "delete" {
    del(app.app_handle().clone())
  } else if event.id() == "import-font-file" {
    import_font_file(app.app_handle().clone())
  } else if event.id() == "import-templates-file" {
    import_templates_file(app.app_handle().clone())
  } else if event.id() == "import-glyphs" {
    import_glyphs(app.app_handle().clone())
  } else if event.id() == "import-pic" {
    import_pic(app.app_handle().clone())
  } else if event.id() == "import-svg" {
    import_svg(app.app_handle().clone())
  } else if event.id() == "export-font-file" {
    export_font_file(app.app_handle().clone())
  } else if event.id() == "export-glyphs" {
    export_glyphs(app.app_handle().clone())
  } else if event.id() == "export-jpeg" {
    export_jpeg(app.app_handle().clone())
  } else if event.id() == "export-png" {
    export_png(app.app_handle().clone())
  } else if event.id() == "export-svg" {
    export_svg(app.app_handle().clone())
  } else if event.id() == "add-character" {
    add_character(app.app_handle().clone())
  } else if event.id() == "add-icon" {
    add_icon(app.app_handle().clone())
  } else if event.id() == "font-settings" {
    font_settings(app.app_handle().clone())
  } else if event.id() == "preference-settings" {
    preference_settings(app.app_handle().clone())
  } else if event.id() == "language-settings" {
    language_settings(app.app_handle().clone())
  } else if event.id() == "template-1" {
    import_template1(app.app_handle().clone())
  } else if event.id() == "remove_overlap" {
    remove_overlap(app.app_handle().clone())
  }        
});

前端代码,监听消息:

const initTauri = () => {
	const keys = Object.keys(tauri_handlers)
	for (let i = 0; i < keys.length; i++) {
		const key = keys[i]
		listen(key, (event) => {
			tauri_handlers[key]()
		})
	}
}

http://www.kler.cn/a/470247.html

相关文章:

  • Java100道面试题
  • Android Telephony | 协议测试针对 test SIM attach network 的问题解决(3GPP TS 36523-1-i60)
  • 【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
  • 【前端系列01】优化axios响应拦截器
  • 防止密码爆破debian系统
  • 【数电尾灯设计】2022-8-16
  • Opencv查找、绘制轮廓、圆形矩形轮廓和近似轮廓
  • ffmpeg八大开发库
  • 深入理解 pytest_runtest_makereport:如何在 pytest 中自定义测试报告
  • OKHttp调用第三方接口,响应转string报错okhttp3.internal.http.RealResponseBody@4a3d0218
  • 平安产险安徽分公司携手安徽中医药临床研究中心附属医院 共筑儿童安全防护网
  • SQLark:高效数据库连接管理的新篇章
  • 懒人不下床型遥控方案--手机对电脑的简单遥控(无收费方案)
  • jupyter执行指令的快捷键
  • 根据自己的需求安装 docker、docker-compose【2025】
  • Chapter4.3:Implementing a feed forward network with GELU activations
  • vue3+Echarts+ts实现甘特图
  • 《OpenCV 4.10.0 实例:开启图像处理新世界》
  • C#: button 防止按钮在短时间内被连续点击的方法
  • 3D内容生成技术:驱动数字世界创新的关键力量
  • OSCP - Proving Grounds - Snookums
  • 在Linux系统上使用nmcli命令配置各种网络(有线、无线、vlan、vxlan、路由、网桥等)
  • 头歌python实验:网络安全应用实践3-验证码识别
  • 【姿态估计实战】使用OpenCV和Mediapipe构建锻炼跟踪器【附完整源码与详细说明】
  • 【软考网工笔记】计算机基础理论与安全——网络规划与设计
  • jrc水体分类对水体二值掩码修正