Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Root } from 'react-dom/client'
import { Editor, TLDRAW_FILE_EXTENSION, TLStore } from 'tldraw'
import { createReactStatusBarViewMode } from './components/StatusBarViewMode'
import createMain from './components/plugin/TldrawInObsidian'
import { TldrawOfflineFileView } from './obsidian/TldrawOfflineFileView'
import { ReadonlyTldrawView } from './obsidian/TldrawReadonlyView'
import {
DEFAULT_SETTINGS,
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
TLDRAW_ICON_NAME,
VIEW_TYPE_MARKDOWN,
VIEW_TYPE_TLDRAW,
VIEW_TYPE_TLDRAW_OFFLINE,
VIEW_TYPE_TLDRAW_READ_ONLY,
ViewType,
} from './utils/constants'
Expand Down Expand Up @@ -104,6 +106,8 @@ export default class TldrawPlugin extends Plugin {

this.registerView(VIEW_TYPE_TLDRAW_READ_ONLY, (leaf) => new ReadonlyTldrawView(leaf, this))

this.registerView(VIEW_TYPE_TLDRAW_OFFLINE, (leaf) => new TldrawOfflineFileView(leaf))

// settings:
await this.settingsManager.loadSettings()
this.addSettingTab(new TldrawSettingsTab(this.app, this))
Expand Down Expand Up @@ -165,6 +169,14 @@ export default class TldrawPlugin extends Plugin {

this.registerExtensions(['tldr'], VIEW_TYPE_TLDRAW)

try {
this.registerExtensions(['tldraw'], VIEW_TYPE_TLDRAW_OFFLINE)
} catch (e) {
// Obsidian throws if another plugin already owns the extension. Explaining that we can't
// open these files isn't worth taking the file type away from a plugin that can.
console.error(e)
}

const unmount = createMain(this, this.app.dom.statusBarEl)
this.register(() => {
unmount()
Expand Down
73 changes: 73 additions & 0 deletions src/obsidian/TldrawOfflineFileView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FileView, Menu, WorkspaceLeaf } from 'obsidian'
import {
TLDRAW_ICON_NAME,
TLDRAW_OFFLINE_UNSUPPORTED_TITLE,
VIEW_TYPE_TLDRAW_OFFLINE,
} from 'src/utils/constants'
import { appendTldrawOfflineMessage } from 'src/utils/tldraw-offline-message'
import { pluginMenuLabel } from './menu'

const OPEN_IN_DEFAULT_APP = 'Open in default app'

/**
* Shown when a `.tldraw` file is opened. tldraw offline saves documents as an archive holding a
* SQLite database rather than the JSON a `.tldr` file holds, so we can't render one. We claim the
* extension anyway so that opening the file explains that, rather than leaving the user on
* Obsidian's generic "no view for this file type" screen.
*
* Claiming the extension takes away Obsidian's own handling, so this offers "open in default app"
* to keep a route to whatever program can read the file.
*
* This extends `FileView` directly rather than `BaseTldrawFileView`, which reads the file as text
* and parses it as JSON — on an archive that only produces an opaque parse error.
*/
export class TldrawOfflineFileView extends FileView {
constructor(leaf: WorkspaceLeaf) {
super(leaf)
this.navigation = true
}

override getViewType() {
return VIEW_TYPE_TLDRAW_OFFLINE
}

override getIcon() {
return TLDRAW_ICON_NAME
}

override getDisplayText() {
return this.file ? this.file.basename : 'NO_FILE'
}

override onload() {
super.onload()
this.addAction('external-link', OPEN_IN_DEFAULT_APP, () => this.openInDefaultApp())
}

override onPaneMenu(menu: Menu, source: 'more-options' | 'tab-header' | string): void {
super.onPaneMenu(menu, source)
if (!this.file) return

menu
.addItem((item) => pluginMenuLabel(item.setSection('tldraw')))
.addItem((item) =>
item
.setIcon('external-link')
.setSection('tldraw')
.setTitle(OPEN_IN_DEFAULT_APP)
.onClick(() => this.openInDefaultApp())
)
}

override async onOpen() {
this.contentEl.empty()
const container = this.contentEl.createDiv({ cls: 'ptl-offline-unsupported' })
container.createEl('h3', { text: TLDRAW_OFFLINE_UNSUPPORTED_TITLE })
appendTldrawOfflineMessage(container)
}

private openInDefaultApp() {
if (!this.file) return
this.app.openWithDefaultApp(this.file.path)
}
}
2 changes: 2 additions & 0 deletions src/obsidian/plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export function registerCommands(plugin: TldrawPlugin) {
name: 'Import file as new document and open in a new tab',
callback: async () => {
const tFile = await importTldrawFile(plugin)
if (!tFile) return
await plugin.openTldrFile(tFile, 'new-tab')
},
})
Expand All @@ -169,6 +170,7 @@ export function registerCommands(plugin: TldrawPlugin) {
const from = editor.getCursor('from')
const to = editor.getCursor('to')
const tFile = await importTldrawFile(plugin, file)
if (!tFile) return
editorInsert(new TldrawDocument(plugin, tFile), editor, from, to)
},
})
Expand Down
20 changes: 20 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -548,3 +548,23 @@ div[data-type='tldraw-read-only'] .view-content.tldraw-view-content {
.ptl-document-messages-actions button.mod-cta {
margin: 0;
}

.ptl-offline-unsupported {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--size-4-1);
height: 100%;
padding: var(--size-4-4);
text-align: center;
}

.ptl-offline-unsupported h3 {
margin: 0;
}

.ptl-offline-unsupported p {
margin: 0;
color: var(--text-muted);
}
11 changes: 11 additions & 0 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type ViewType = (typeof VIEW_TYPES)[number]
export const VIEW_TYPE_TLDRAW = 'tldraw-view' // custom view type

export const VIEW_TYPE_TLDRAW_READ_ONLY = 'tldraw-read-only' // custom view type
/**
* For `.tldraw` ending files. Deliberately left out of {@link VIEW_TYPES}, since it isn't a mode
* the user can switch a document into — it only exists to explain why we can't open the file.
*/
export const VIEW_TYPE_TLDRAW_OFFLINE = 'tldraw-offline' // custom view type
export const VIEW_TYPE_MARKDOWN = 'markdown' // NOT ACTUALLY A CUSTOM VIEW TYPE, its built in from obsidian
export const VIEW_TYPES = [
VIEW_TYPE_MARKDOWN,
Expand All @@ -15,6 +20,12 @@ export const VIEW_TYPES = [
export const PANE_TARGETS = ['new-window', 'new-tab', 'current-tab', 'split-tab'] as const

export const FILE_EXTENSION = '.md'
/**
* The extension used by tldraw offline. A `.tldraw` file is an archive holding a SQLite database
* and its assets, not the JSON a `.tldr` file holds, so none of our parsing can read one.
*/
export const TLDRAW_OFFLINE_FILE_EXTENSION = '.tldraw'
export const TLDRAW_OFFLINE_UNSUPPORTED_TITLE = 'Can’t open .tldraw files yet'
export const FRONTMATTER_KEY = 'tldraw-file'
export const TLDATA_DELIMITER_START = '!!!_START_OF_TLDRAW_DATA__DO_NOT_CHANGE_THIS_PHRASE_!!!'
export const TLDATA_DELIMITER_END = '!!!_END_OF_TLDRAW_DATA__DO_NOT_CHANGE_THIS_PHRASE_!!!'
Expand Down
25 changes: 22 additions & 3 deletions src/utils/file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Platform, TFile } from 'obsidian'
import { Notice, Platform, TFile } from 'obsidian'
import TldrawPlugin from 'src/main'
import { showSaveFileModal } from 'src/obsidian/modal/save-file-modal'
import {
Expand All @@ -8,7 +8,9 @@ import {
serializeTldrawJsonBlob,
useDefaultHelpers,
} from 'tldraw'
import { TLDRAW_OFFLINE_FILE_EXTENSION, TLDRAW_OFFLINE_UNSUPPORTED_TITLE } from './constants'
import { migrateTldrawFileDataIfNecessary } from './migrate/tl-data-to-tlstore'
import { appendTldrawOfflineMessage } from './tldraw-offline-message'
// import { shouldOverrideDocument } from "src/components/file-menu/shouldOverrideDocument";

export const SAVE_FILE_COPY_ACTION = 'save-file-copy'
Expand Down Expand Up @@ -103,12 +105,16 @@ export function importFileAction(
readonlyOk: true,
async onSelect(source) {
const tFile = await importTldrawFile(plugin)
if (!tFile) return
await plugin.openTldrFile(tFile, 'new-tab')
},
}
}

export async function importTldrawFile(plugin: TldrawPlugin, attachTo?: TFile) {
export async function importTldrawFile(
plugin: TldrawPlugin,
attachTo?: TFile
): Promise<TFile | undefined> {
if ('showOpenFilePicker' in window) {
const [file] = await window.showOpenFilePicker({
id: 'tldraw-open-file',
Expand All @@ -117,13 +123,26 @@ export async function importTldrawFile(plugin: TldrawPlugin, attachTo?: TFile) {
{
description: 'Tldraw Document',
accept: {
'text/tldr': ['.tldr'],
// tldraw offline files are selectable so that we can explain why we can't import
// them yet. Leaving them out would just grey them out with no explanation.
'text/tldr': ['.tldr', TLDRAW_OFFLINE_FILE_EXTENSION],
},
},
],
excludeAcceptAllOption: true,
})

// Case-insensitive: file dialogs on macOS and Windows match their filters that way, so a
// `.TLDRAW` file is selectable and would otherwise fall through to the JSON parser.
if (file.name.toLowerCase().endsWith(TLDRAW_OFFLINE_FILE_EXTENSION)) {
// A fragment rather than a string, so the notice can carry the links.
const notice = document.createDocumentFragment()
notice.createEl('strong', { text: TLDRAW_OFFLINE_UNSUPPORTED_TITLE })
appendTldrawOfflineMessage(notice)
new Notice(notice)
return undefined
}

return plugin.createUntitledTldrFile({
attachTo,
tlStore: migrateTldrawFileDataIfNecessary(await (await file.getFile()).text()),
Expand Down
24 changes: 24 additions & 0 deletions src/utils/tldraw-offline-message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const TLDRAW_OFFLINE_URL = 'https://offline.tldraw.com/'

/** Anchors the "Export as .tldr" section of the tldraw offline user manual. */
const TLDRAW_OFFLINE_EXPORT_URL =
'https://tldraw.notion.site/User-manual-tldraw-offline-39a3e4c324c080e7b2eacc5afd078e85#3aa3e4c324c080669967e2cc3ae2c789'

/**
* Appends the explanation for why a `.tldraw` file can't be opened, and the way forward, into
* {@linkcode parent}.
*
* Built as nodes rather than returned as a string so that both the file view and the import notice
* can carry the links: Obsidian's `Notice` only renders them when given a fragment.
*/
export function appendTldrawOfflineMessage(parent: HTMLElement | DocumentFragment) {
const explanation = parent.createEl('p')
explanation.appendText('We’re working on support for files from ')
explanation.createEl('a', { text: 'tldraw offline', href: TLDRAW_OFFLINE_URL })
explanation.appendText('.')

const prompt = parent.createEl('p')
prompt.appendText('For now, you can ')
prompt.createEl('a', { text: 'export as a .tldr file', href: TLDRAW_OFFLINE_EXPORT_URL })
prompt.appendText(' to use it here.')
}
Loading