mirror of
https://github.com/Ascyii/typstar.git
synced 2026-01-01 13:34:24 -05:00
Merge branch 'rnote' into dev
This commit is contained in:
32
README.md
32
README.md
@@ -1,9 +1,9 @@
|
|||||||
# Typstar
|
# Typstar
|
||||||
Neovim plugin for efficient note taking in Typst
|
Neovim plugin for efficient (mathematical) note taking in Typst
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Powerful autosnippets using [LuaSnip](https://github.com/L3MON4D3/LuaSnip/) and [Tree-sitter](https://tree-sitter.github.io/) (inspired by [fastex.nvim](https://github.com/lentilus/fastex.nvim))
|
- Powerful autosnippets using [LuaSnip](https://github.com/L3MON4D3/LuaSnip/) and [Tree-sitter](https://tree-sitter.github.io/) (inspired by [fastex.nvim](https://github.com/lentilus/fastex.nvim))
|
||||||
- Easy insertion of drawings using [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin)
|
- Easy insertion of drawings using [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) or [Rnote](https://github.com/flxzt/rnote)
|
||||||
- Export of [Anki](https://apps.ankiweb.net/) flashcards \[No Neovim required\]
|
- Export of [Anki](https://apps.ankiweb.net/) flashcards \[No Neovim required\]
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -38,9 +38,13 @@ Math snippets:
|
|||||||
|
|
||||||
Note that you can [customize](#custom-snippets) (enable, disable and modify) every snippet.
|
Note that you can [customize](#custom-snippets) (enable, disable and modify) every snippet.
|
||||||
|
|
||||||
### Excalidraw
|
### Excalidraw/Rnote
|
||||||
- Use `:TypstarInsertExcalidraw` to create a new drawing using the configured template, insert a figure displaying it and open it in Obsidian.
|
- Use `:TypstarInsertExcalidraw`/`:TypstarInsertRnote` to
|
||||||
- To open an inserted drawing in Obsidian, simply run `:TypstarOpenExcalidraw` while your cursor is on a line referencing the drawing.
|
create a new drawing using the [configured](#configuration) template,
|
||||||
|
insert a figure displaying it and open it in Obsidian/Rnote.
|
||||||
|
- To open an inserted drawing in Obsidian/Rnote,
|
||||||
|
simply run `:TypstarOpenDrawing` (or `:TypstarOpenExcalidraw`/`:TypstarOpenRnote` if you are using the same file extension for both)
|
||||||
|
while your cursor is on a line referencing the drawing.
|
||||||
|
|
||||||
### Anki
|
### Anki
|
||||||
Use the `flA` snippet to create a new flashcard
|
Use the `flA` snippet to create a new flashcard
|
||||||
@@ -162,7 +166,13 @@ require('typstar').setup({ -- depending on your neovim plugin system
|
|||||||
1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory
|
1. Install [Obsidian](https://obsidian.md/) and create a vault in your typst note taking directory
|
||||||
2. Install the [obsidian-excalidraw-plugin](https://github.com/zsviczian/obsidian-excalidraw-plugin) and enable `Auto-export SVG` (in plugin settings at `Embedding Excalidraw into your Notes and Exporting > Export Settings > Auto-export Settings`)
|
2. Install the [obsidian-excalidraw-plugin](https://github.com/zsviczian/obsidian-excalidraw-plugin) and enable `Auto-export SVG` (in plugin settings at `Embedding Excalidraw into your Notes and Exporting > Export Settings > Auto-export Settings`)
|
||||||
3. Have the `xdg-open` command working or set a different command at `uriOpenCommand` in the [config](#configuration)
|
3. Have the `xdg-open` command working or set a different command at `uriOpenCommand` in the [config](#configuration)
|
||||||
4. If you encounter issues try cloning the repo into `~/typstar` or setting the `typstarRoot` config accordingly, feel free to open an issue
|
4. If you encounter issues with the file creation of drawings, try cloning the repo into `~/typstar` or setting the `typstarRoot` config accordingly; feel free to open an issue
|
||||||
|
|
||||||
|
### Rnote
|
||||||
|
1. Install [Rnote](https://github.com/flxzt/rnote?tab=readme-ov-file#installation); I recommend not using flatpak as that might cause issues with file permissions.
|
||||||
|
2. Make sure `rnote-cli` is available in your `PATH` or set a different command at `exportCommand` in the [config](#configuration)
|
||||||
|
3. Have the `xdg-open` command working with Rnote files or set a different command at `openCommand` in the [config](#configuration)
|
||||||
|
4. See comment 4 above at Excalidraw
|
||||||
|
|
||||||
### Anki
|
### Anki
|
||||||
0. Typst version `0.12.0` or higher is required
|
0. Typst version `0.12.0` or higher is required
|
||||||
@@ -201,6 +211,16 @@ with pkgs; [
|
|||||||
## Configuration
|
## Configuration
|
||||||
Configuration options can be intuitively derived from the table [here](./lua/typstar/config.lua).
|
Configuration options can be intuitively derived from the table [here](./lua/typstar/config.lua).
|
||||||
|
|
||||||
|
### Excalidraw/Rnote templates
|
||||||
|
The `templatePath` option expects a table that maps file patterns to template locations.
|
||||||
|
To for example have a specific template for lectures, you could configure it like this
|
||||||
|
```Lua
|
||||||
|
templatePath = {
|
||||||
|
{ 'lectures/.*%.excalidraw%.md$', '~/Templates/lecture_excalidraw.excalidraw.md' }, -- path contains "lectures"
|
||||||
|
{ '%.excalidraw%.md$', '~/Templates/default_excalidraw.excalidraw.md' }, -- fallback
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
### Custom snippets
|
### Custom snippets
|
||||||
The [config](#configuration) allows you to
|
The [config](#configuration) allows you to
|
||||||
- disable all snippets via `snippets.enable = false`
|
- disable all snippets via `snippets.enable = false`
|
||||||
|
|||||||
@@ -42,6 +42,8 @@
|
|||||||
lua << EOF
|
lua << EOF
|
||||||
print("Welcome to Typstar! This is just a demo.")
|
print("Welcome to Typstar! This is just a demo.")
|
||||||
|
|
||||||
|
vim.g.mapleader = " "
|
||||||
|
|
||||||
require('nvim-treesitter.configs').setup {
|
require('nvim-treesitter.configs').setup {
|
||||||
highlight = { enable = true },
|
highlight = { enable = true },
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,12 @@
|
|||||||
vim.keymap.set({'n', 'i'}, '<M-t>', '<Cmd>TypstarToggleSnippets<CR>', { silent = true, noremap = true })
|
vim.keymap.set({'n', 'i'}, '<M-t>', '<Cmd>TypstarToggleSnippets<CR>', { silent = true, noremap = true })
|
||||||
vim.keymap.set({'s', 'i'}, '<M-j>', '<Cmd>TypstarSmartJump<CR>', { silent = true, noremap = true })
|
vim.keymap.set({'s', 'i'}, '<M-j>', '<Cmd>TypstarSmartJump<CR>', { silent = true, noremap = true })
|
||||||
vim.keymap.set({'s', 'i'}, '<M-k>', '<Cmd>TypstarSmartJumpBack<CR>', { silent = true, noremap = true })
|
vim.keymap.set({'s', 'i'}, '<M-k>', '<Cmd>TypstarSmartJumpBack<CR>', { silent = true, noremap = true })
|
||||||
|
|
||||||
|
vim.keymap.set('n', '<leader>e', '<Cmd>TypstarInsertExcalidraw<CR>', { silent = true, noremap = true })
|
||||||
|
vim.keymap.set('n', '<leader>r', '<Cmd>TypstarInsertRnote<CR>', { silent = true, noremap = true })
|
||||||
|
vim.keymap.set('n', '<leader>o', '<Cmd>TypstarOpenDrawing<CR>', { silent = true, noremap = true })
|
||||||
|
|
||||||
|
vim.keymap.set('n', '<leader>a', '<Cmd>TypstarAnkiScan<CR>', { silent = true, noremap = true })
|
||||||
EOF
|
EOF
|
||||||
'';
|
'';
|
||||||
plugins = [
|
plugins = [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local default_config = {
|
local default_config = {
|
||||||
typstarRoot = nil,
|
typstarRoot = nil, -- typstar installation location required to use default drawing templates (usually determined automatically)
|
||||||
anki = {
|
anki = {
|
||||||
typstarAnkiCmd = 'typstar-anki',
|
typstarAnkiCmd = 'typstar-anki',
|
||||||
typstCmd = 'typst',
|
typstCmd = 'typst',
|
||||||
@@ -13,8 +13,17 @@ local default_config = {
|
|||||||
filename = 'drawing-%Y-%m-%d-%H-%M-%S',
|
filename = 'drawing-%Y-%m-%d-%H-%M-%S',
|
||||||
fileExtension = '.excalidraw.md',
|
fileExtension = '.excalidraw.md',
|
||||||
fileExtensionInserted = '.excalidraw.svg',
|
fileExtensionInserted = '.excalidraw.svg',
|
||||||
uriOpenCommand = 'xdg-open', -- set depending on OS
|
uriOpenCommand = 'xdg-open', -- set depending on OS; try setting it to "obsidian" directly if you encounter problems and have it in your PATH
|
||||||
templatePath = nil,
|
templatePath = {},
|
||||||
|
},
|
||||||
|
rnote = {
|
||||||
|
assetsDir = 'assets',
|
||||||
|
exportCommand = 'rnote-cli export selection --no-background --no-pattern --on-conflict overwrite --output-file %s all %s', -- can be modified to e.g. export full pages
|
||||||
|
filename = 'drawing-%Y-%m-%d-%H-%M-%S',
|
||||||
|
fileExtension = '.rnote',
|
||||||
|
fileExtensionInserted = '.rnote.svg', -- valid rnote export type
|
||||||
|
openCommand = 'xdg-open', -- see comment above for excalidraw
|
||||||
|
templatePath = {},
|
||||||
},
|
},
|
||||||
snippets = {
|
snippets = {
|
||||||
enable = true,
|
enable = true,
|
||||||
@@ -38,10 +47,12 @@ function M.merge_config(args)
|
|||||||
M.config.typstarRoot = M.config.typstarRoot
|
M.config.typstarRoot = M.config.typstarRoot
|
||||||
or debug.getinfo(1).source:match('^@(.*)/lua/typstar/config%.lua$')
|
or debug.getinfo(1).source:match('^@(.*)/lua/typstar/config%.lua$')
|
||||||
or '~/typstar'
|
or '~/typstar'
|
||||||
M.config.excalidraw.templatePath = M.config.excalidraw.templatePath
|
vim.list_extend(M.config.excalidraw.templatePath, {
|
||||||
or {
|
{ '%.excalidraw%.md$', M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md' },
|
||||||
['%.excalidraw%.md$'] = M.config.typstarRoot .. '/res/excalidraw_template.excalidraw.md',
|
})
|
||||||
}
|
vim.list_extend(M.config.rnote.templatePath, {
|
||||||
|
{ '%.rnote$', M.config.typstarRoot .. '/res/rnote_template.rnote' },
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
M.merge_config(nil)
|
M.merge_config(nil)
|
||||||
|
|||||||
108
lua/typstar/drawings.lua
Normal file
108
lua/typstar/drawings.lua
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
local M = {}
|
||||||
|
local config = require('typstar.config')
|
||||||
|
local utils = require('typstar.utils')
|
||||||
|
|
||||||
|
local affix = [[
|
||||||
|
#figure(
|
||||||
|
image("%s"),
|
||||||
|
)
|
||||||
|
]]
|
||||||
|
local config_excalidraw = config.config.excalidraw
|
||||||
|
local config_rnote = config.config.rnote
|
||||||
|
|
||||||
|
local function launch_excalidraw(path, path_inserted)
|
||||||
|
print(string.format('Opening %s in Obsidian Excalidraw', path))
|
||||||
|
utils.run_shell_command(
|
||||||
|
string.format('%s "obsidian://open?path=%s"', config_excalidraw.uriOpenCommand, utils.urlencode(path)),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local rnote_watched = {}
|
||||||
|
|
||||||
|
local function auto_export_rnote(path, path_inserted)
|
||||||
|
if rnote_watched[path] then return end
|
||||||
|
rnote_watched[path] = true
|
||||||
|
local job_id = -1
|
||||||
|
local last_export = 0
|
||||||
|
|
||||||
|
local run_export = function(err, filename)
|
||||||
|
local time = vim.uv.now()
|
||||||
|
if err ~= nil or time - last_export < 800 then return end
|
||||||
|
|
||||||
|
if job_id == -1 then
|
||||||
|
last_export = time
|
||||||
|
local cmd = string.format(config_rnote.exportCommand, path_inserted, path)
|
||||||
|
job_id = utils.run_shell_command(cmd, false, nil, { on_exit = function() job_id = -1 end })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local watcher = vim.uv.new_fs_event()
|
||||||
|
watcher:start(path, {}, vim.schedule_wrap(run_export))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function launch_rnote(path, path_inserted)
|
||||||
|
print(string.format('Opening %s in Rnote', path))
|
||||||
|
utils.run_shell_command(string.format('%s %s', config_rnote.openCommand, path), false)
|
||||||
|
auto_export_rnote(path, path_inserted)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function insert_drawing(provider)
|
||||||
|
local cfg = provider[1]
|
||||||
|
local assets_dir = vim.fn.expand('%:p:h') .. '/' .. cfg.assetsDir
|
||||||
|
local filename = os.date(cfg.filename)
|
||||||
|
local path = assets_dir .. '/' .. filename .. cfg.fileExtension
|
||||||
|
local path_inserted = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted
|
||||||
|
|
||||||
|
if vim.fn.isdirectory(assets_dir) == 0 then vim.fn.mkdir(assets_dir, 'p') end
|
||||||
|
local found_match = false
|
||||||
|
for _, template_config in ipairs(cfg.templatePath) do
|
||||||
|
local pattern = template_config[1]
|
||||||
|
local template_path = template_config[2]
|
||||||
|
if string.match(path, pattern) then
|
||||||
|
found_match = true
|
||||||
|
utils.run_shell_command(string.format('cat %s > %s', template_path, path), false) -- don't copy file metadata
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not found_match then
|
||||||
|
print('No matching template found for path: ' .. path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
utils.insert_text_block(string.format(provider[2], path_inserted))
|
||||||
|
provider[3](path, path_inserted)
|
||||||
|
end
|
||||||
|
|
||||||
|
local excalidraw = {
|
||||||
|
config_excalidraw,
|
||||||
|
affix,
|
||||||
|
launch_excalidraw,
|
||||||
|
}
|
||||||
|
local rnote = {
|
||||||
|
config_rnote,
|
||||||
|
affix,
|
||||||
|
launch_rnote,
|
||||||
|
}
|
||||||
|
local providers = { excalidraw, rnote }
|
||||||
|
|
||||||
|
local open_drawing = function(prov)
|
||||||
|
for _, provider in ipairs(prov) do
|
||||||
|
local cfg = provider[1]
|
||||||
|
local line = vim.api.nvim_get_current_line()
|
||||||
|
local filename = line:match('"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.'))
|
||||||
|
if filename ~= nil and filename:match('^%s*$') == nil then
|
||||||
|
local path = vim.fn.expand('%:p:h') .. '/' .. filename .. cfg.fileExtension
|
||||||
|
local path_inserted = vim.fn.expand('%:p:h') .. '/' .. filename .. cfg.fileExtensionInserted
|
||||||
|
provider[3](path, path_inserted) -- launch program
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.insert_obsidian_excalidraw() insert_drawing(excalidraw) end
|
||||||
|
function M.insert_rnote() insert_drawing(rnote) end
|
||||||
|
function M.open_obsidian_excalidraw() open_drawing({ excalidraw }) end
|
||||||
|
function M.open_rnote() open_drawing({ rnote }) end
|
||||||
|
function M.open_drawing() open_drawing(providers) end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
local M = {}
|
|
||||||
local config = require('typstar.config')
|
|
||||||
local utils = require('typstar.utils')
|
|
||||||
|
|
||||||
local cfg = config.config.excalidraw
|
|
||||||
local affix = [[
|
|
||||||
#figure(
|
|
||||||
image("%s"),
|
|
||||||
)
|
|
||||||
]]
|
|
||||||
|
|
||||||
local function launch_obsidian(path)
|
|
||||||
print(string.format('Opening %s in Excalidraw', path))
|
|
||||||
utils.run_shell_command(
|
|
||||||
string.format('%s "obsidian://open?path=%s"', cfg.uriOpenCommand, utils.urlencode(path)),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.insert_drawing()
|
|
||||||
local assets_dir = vim.fn.expand('%:p:h') .. '/' .. cfg.assetsDir
|
|
||||||
local filename = os.date(cfg.filename)
|
|
||||||
local path = assets_dir .. '/' .. filename .. cfg.fileExtension
|
|
||||||
local path_inserted = cfg.assetsDir .. '/' .. filename .. cfg.fileExtensionInserted
|
|
||||||
|
|
||||||
if vim.fn.isdirectory(assets_dir) == 0 then vim.fn.mkdir(assets_dir, 'p') end
|
|
||||||
local found_match = false
|
|
||||||
for pattern, template_path in pairs(cfg.templatePath) do
|
|
||||||
if string.match(path, pattern) then
|
|
||||||
found_match = true
|
|
||||||
utils.run_shell_command(string.format('cat %s > %s', template_path, path), false) -- don't copy file metadata
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not found_match then
|
|
||||||
print('No matching template found for the path: ' .. path)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
utils.insert_text_block(string.format(affix, path_inserted))
|
|
||||||
launch_obsidian(path)
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.open_drawing()
|
|
||||||
local line = vim.api.nvim_get_current_line()
|
|
||||||
local path = vim.fn.expand('%:p:h')
|
|
||||||
.. '/'
|
|
||||||
.. string.match(line, '"(.*)' .. string.gsub(cfg.fileExtensionInserted, '%.', '%%%.'))
|
|
||||||
.. '.excalidraw.md'
|
|
||||||
launch_obsidian(path)
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
@@ -6,15 +6,18 @@ local luasnip = nil
|
|||||||
M.setup = function(args)
|
M.setup = function(args)
|
||||||
config.merge_config(args)
|
config.merge_config(args)
|
||||||
local autosnippets = require('typstar.autosnippets')
|
local autosnippets = require('typstar.autosnippets')
|
||||||
local excalidraw = require('typstar.excalidraw')
|
local drawings = require('typstar.drawings')
|
||||||
local anki = require('typstar.anki')
|
local anki = require('typstar.anki')
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('TypstarToggleSnippets', autosnippets.toggle_autosnippets, {})
|
vim.api.nvim_create_user_command('TypstarToggleSnippets', autosnippets.toggle_autosnippets, {})
|
||||||
vim.api.nvim_create_user_command('TypstarSmartJump', function() M.smart_jump(1) end, {})
|
vim.api.nvim_create_user_command('TypstarSmartJump', function() M.smart_jump(1) end, {})
|
||||||
vim.api.nvim_create_user_command('TypstarSmartJumpBack', function() M.smart_jump(-1) end, {})
|
vim.api.nvim_create_user_command('TypstarSmartJumpBack', function() M.smart_jump(-1) end, {})
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('TypstarInsertExcalidraw', excalidraw.insert_drawing, {})
|
vim.api.nvim_create_user_command('TypstarInsertExcalidraw', drawings.insert_obsidian_excalidraw, {})
|
||||||
vim.api.nvim_create_user_command('TypstarOpenExcalidraw', excalidraw.open_drawing, {})
|
vim.api.nvim_create_user_command('TypstarInsertRnote', drawings.insert_rnote, {})
|
||||||
|
vim.api.nvim_create_user_command('TypstarOpenExcalidraw', drawings.open_obsidian_excalidraw, {})
|
||||||
|
vim.api.nvim_create_user_command('TypstarOpenRnote', drawings.open_rnote, {})
|
||||||
|
vim.api.nvim_create_user_command('TypstarOpenDrawing', drawings.open_drawing, {})
|
||||||
|
|
||||||
vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {})
|
vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {})
|
||||||
vim.api.nvim_create_user_command('TypstarAnkiReimport', anki.scan_reimport, {})
|
vim.api.nvim_create_user_command('TypstarAnkiReimport', anki.scan_reimport, {})
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ function M.insert_text_block(snip)
|
|||||||
vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), line_num, line_num, false, lines)
|
vim.api.nvim_buf_set_lines(vim.api.nvim_get_current_buf(), line_num, line_num, false, lines)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.run_shell_command(cmd, show_output, extra_handler)
|
function M.run_shell_command(cmd, show_output, extra_handler, opts)
|
||||||
extra_handler = extra_handler or function(msg) end
|
extra_handler = extra_handler or function(msg) end
|
||||||
|
opts = opts or { on_exit = function() end }
|
||||||
local handle_output = function(data, err)
|
local handle_output = function(data, err)
|
||||||
local msg = table.concat(data, '\n')
|
local msg = table.concat(data, '\n')
|
||||||
if not string.match(msg, '^%s*$') then
|
if not string.match(msg, '^%s*$') then
|
||||||
@@ -37,14 +38,17 @@ function M.run_shell_command(cmd, show_output, extra_handler)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if show_output then
|
if show_output then
|
||||||
vim.fn.jobstart(cmd, {
|
return vim.fn.jobstart(
|
||||||
on_stdout = function(_, data, _) handle_output(data, false) end,
|
cmd,
|
||||||
on_stderr = function(_, data, _) handle_output(data, true) end,
|
vim.tbl_deep_extend('force', {
|
||||||
stdout_buffered = false,
|
on_stdout = function(_, data, _) handle_output(data, false) end,
|
||||||
stderr_buffered = true,
|
on_stderr = function(_, data, _) handle_output(data, true) end,
|
||||||
})
|
stdout_buffered = false,
|
||||||
|
stderr_buffered = true,
|
||||||
|
}, opts)
|
||||||
|
)
|
||||||
else
|
else
|
||||||
vim.fn.jobstart(cmd)
|
return vim.fn.jobstart(cmd, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
BIN
res/rnote_template.rnote
Normal file
BIN
res/rnote_template.rnote
Normal file
Binary file not shown.
Reference in New Issue
Block a user