vault backup: 2024-09-30 23:15:41
This commit is contained in:
parent
29cfb8ab81
commit
6c7d6baf4c
|
@ -3,5 +3,9 @@
|
||||||
"obsidian-style-settings",
|
"obsidian-style-settings",
|
||||||
"obsidian-icon-folder",
|
"obsidian-icon-folder",
|
||||||
"obsidian-file-color",
|
"obsidian-file-color",
|
||||||
"obsidian-git"
|
"obsidian-git",
|
||||||
|
"tag-wrangler",
|
||||||
|
"obsidian-thumbnails",
|
||||||
|
"dataview",
|
||||||
|
"awesome-image"
|
||||||
]
|
]
|
|
@ -0,0 +1,291 @@
|
||||||
|
let Plugin = class {}
|
||||||
|
let MarkdownRenderer = {}
|
||||||
|
let MarkdownRenderChild = class {}
|
||||||
|
let htmlToMarkdown = (html) => html
|
||||||
|
|
||||||
|
if (isObsidian()) {
|
||||||
|
const obsidian = require('obsidian')
|
||||||
|
Plugin = obsidian.Plugin
|
||||||
|
MarkdownRenderer = obsidian.MarkdownRenderer
|
||||||
|
MarkdownRenderChild = obsidian.MarkdownRenderChild
|
||||||
|
htmlToMarkdown = obsidian.htmlToMarkdown
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeblockId = 'table-of-contents'
|
||||||
|
const codeblockIdShort = 'toc'
|
||||||
|
const availableOptions = {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
comment: '',
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: 'value',
|
||||||
|
default: 'nestedList',
|
||||||
|
values: ['nestedList', 'nestedOrderedList', 'inlineFirstLevel'],
|
||||||
|
comment: 'TOC style (nestedList|nestedOrderedList|inlineFirstLevel)',
|
||||||
|
},
|
||||||
|
minLevel: {
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
comment: 'Include headings from the specified level',
|
||||||
|
},
|
||||||
|
maxLevel: {
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
comment: 'Include headings up to the specified level',
|
||||||
|
},
|
||||||
|
includeLinks: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
comment: 'Make headings clickable',
|
||||||
|
},
|
||||||
|
debugInConsole: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
comment: 'Print debug info in Obsidian console',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
class ObsidianAutomaticTableOfContents extends Plugin {
|
||||||
|
async onload() {
|
||||||
|
const handler = (sourceText, element, context) => {
|
||||||
|
context.addChild(new Renderer(this.app, element, context.sourcePath, sourceText))
|
||||||
|
}
|
||||||
|
this.registerMarkdownCodeBlockProcessor(codeblockId, handler)
|
||||||
|
this.registerMarkdownCodeBlockProcessor(codeblockIdShort, handler)
|
||||||
|
this.addCommand({
|
||||||
|
id: 'insert-automatic-table-of-contents',
|
||||||
|
name: 'Insert table of contents',
|
||||||
|
editorCallback: onInsertToc,
|
||||||
|
})
|
||||||
|
this.addCommand({
|
||||||
|
id: 'insert-automatic-table-of-contents-docs',
|
||||||
|
name: 'Insert table of contents (documented)',
|
||||||
|
editorCallback: onInsertTocWithDocs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInsertToc(editor) {
|
||||||
|
const markdown = '```' + codeblockId + '\n```'
|
||||||
|
editor.replaceRange(markdown, editor.getCursor())
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInsertTocWithDocs(editor) {
|
||||||
|
let markdown = ['```' + codeblockId]
|
||||||
|
Object.keys(availableOptions).forEach((optionName) => {
|
||||||
|
const option = availableOptions[optionName]
|
||||||
|
const comment = option.comment.length > 0 ? ` # ${option.comment}` : ''
|
||||||
|
markdown.push(`${optionName}: ${option.default}${comment}`)
|
||||||
|
})
|
||||||
|
markdown.push('```')
|
||||||
|
editor.replaceRange(markdown.join('\n'), editor.getCursor())
|
||||||
|
}
|
||||||
|
|
||||||
|
class Renderer extends MarkdownRenderChild {
|
||||||
|
constructor(app, element, sourcePath, sourceText) {
|
||||||
|
super(element)
|
||||||
|
this.app = app
|
||||||
|
this.element = element
|
||||||
|
this.sourcePath = sourcePath
|
||||||
|
this.sourceText = sourceText
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render on load
|
||||||
|
onload() {
|
||||||
|
this.render()
|
||||||
|
this.registerEvent(this.app.metadataCache.on('changed', this.onMetadataChange.bind(this)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render on file change
|
||||||
|
onMetadataChange() {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
try {
|
||||||
|
const options = parseOptionsFromSourceText(this.sourceText)
|
||||||
|
if (options.debugInConsole) debug('Options', options)
|
||||||
|
|
||||||
|
const metadata = this.app.metadataCache.getCache(this.sourcePath)
|
||||||
|
const headings = metadata && metadata.headings ? metadata.headings : []
|
||||||
|
if (options.debugInConsole) debug('Headings', headings)
|
||||||
|
|
||||||
|
const markdown = getMarkdownFromHeadings(headings, options)
|
||||||
|
if (options.debugInConsole) debug('Markdown', markdown)
|
||||||
|
|
||||||
|
this.element.empty()
|
||||||
|
MarkdownRenderer.renderMarkdown(markdown, this.element, this.sourcePath, this)
|
||||||
|
} catch(error) {
|
||||||
|
const readableError = `_💥 Could not render table of contents (${error.message})_`
|
||||||
|
MarkdownRenderer.renderMarkdown(readableError, this.element, this.sourcePath, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownFromHeadings(headings, options) {
|
||||||
|
const markdownHandlersByStyle = {
|
||||||
|
nestedList: getMarkdownNestedListFromHeadings,
|
||||||
|
nestedOrderedList: getMarkdownNestedOrderedListFromHeadings,
|
||||||
|
inlineFirstLevel: getMarkdownInlineFirstLevelFromHeadings,
|
||||||
|
}
|
||||||
|
let markdown = ''
|
||||||
|
if (options.title && options.title.length > 0) {
|
||||||
|
markdown += options.title + '\n'
|
||||||
|
}
|
||||||
|
const noHeadingMessage = '_Table of contents: no headings found_'
|
||||||
|
markdown += markdownHandlersByStyle[options.style](headings, options) || noHeadingMessage
|
||||||
|
return markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownNestedListFromHeadings(headings, options) {
|
||||||
|
return getMarkdownListFromHeadings(headings, false, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownNestedOrderedListFromHeadings(headings, options) {
|
||||||
|
return getMarkdownListFromHeadings(headings, true, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownListFromHeadings(headings, isOrdered, options) {
|
||||||
|
const prefix = isOrdered ? '1.' : '-'
|
||||||
|
const lines = []
|
||||||
|
const minLevel = options.minLevel > 0
|
||||||
|
? options.minLevel
|
||||||
|
: Math.min(...headings.map((heading) => heading.level))
|
||||||
|
headings.forEach((heading) => {
|
||||||
|
if (heading.level < minLevel) return
|
||||||
|
if (options.maxLevel > 0 && heading.level > options.maxLevel) return
|
||||||
|
lines.push(`${'\t'.repeat(heading.level - minLevel)}${prefix} ${getMarkdownHeading(heading, options)}`)
|
||||||
|
})
|
||||||
|
return lines.length > 0 ? lines.join('\n') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownInlineFirstLevelFromHeadings(headings, options) {
|
||||||
|
const minLevel = options.minLevel > 0
|
||||||
|
? options.minLevel
|
||||||
|
: Math.min(...headings.map((heading) => heading.level))
|
||||||
|
const items = headings
|
||||||
|
.filter((heading) => heading.level === minLevel)
|
||||||
|
.map((heading) => {
|
||||||
|
return getMarkdownHeading(heading, options)
|
||||||
|
})
|
||||||
|
return items.length > 0 ? items.join(' | ') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownHeading(heading, options) {
|
||||||
|
const stripMarkdown = (text) => {
|
||||||
|
text = text.replaceAll('*', '').replaceAll('_', '').replaceAll('`', '')
|
||||||
|
text = text.replaceAll('==', '').replaceAll('~~', '')
|
||||||
|
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Strip markdown links
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
const stripHtml = (text) => stripMarkdown(htmlToMarkdown(text))
|
||||||
|
const stripWikilinks = (text, isForLink) => {
|
||||||
|
// Strip [[link|text]] format
|
||||||
|
// For the text part of the final link we only keep "text"
|
||||||
|
// For the link part we need the text + link
|
||||||
|
// Example: "# Some [[file.md|heading]]" must be translated to "[[#Some file.md heading|Some heading]]"
|
||||||
|
text = text.replace(/\[\[([^\]]+)\|([^\]]+)\]\]/g, isForLink ? '$1 $2' : '$2')
|
||||||
|
text = text.replace(/\[\[([^\]]+)\]\]/g, '$1') // Strip [[link]] format
|
||||||
|
// Replace malformed links & reserved wikilinks chars
|
||||||
|
text = text.replaceAll('[[', '').replaceAll('| ', isForLink ? '' : '- ').replaceAll('|', isForLink ? ' ' : '-')
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
const stripTags = (text) => text.replaceAll('#', '')
|
||||||
|
if (options.includeLinks) {
|
||||||
|
// Remove markdown, HTML & wikilinks from text for readability, as they are not rendered in a wikilink
|
||||||
|
let text = heading.heading
|
||||||
|
text = stripMarkdown(text)
|
||||||
|
text = stripHtml(text)
|
||||||
|
text = stripWikilinks(text, false)
|
||||||
|
// Remove wikilinks & tags from link or it won't be clickable (on the other hand HTML & markdown must stay)
|
||||||
|
let link = heading.heading
|
||||||
|
link = stripWikilinks(link, true)
|
||||||
|
link = stripTags(link)
|
||||||
|
|
||||||
|
// Return wiklink style link
|
||||||
|
return `[[#${link}|${text}]]`
|
||||||
|
// Why not markdown links? Because even if it looks like the text part would have a better compatibility
|
||||||
|
// with complex headings (as it would support HTML, markdown, etc) the link part is messy,
|
||||||
|
// because it requires some encoding that looks buggy and undocumented; official docs state the link must be URL encoded
|
||||||
|
// (https://help.obsidian.md/Linking+notes+and+files/Internal+links#Supported+formats+for+internal+links)
|
||||||
|
// but it doesn't work properly, example: "## Some <em>heading</em> with simple HTML" must be encoded as:
|
||||||
|
// [Some <em>heading</em> with simple HTML](#Some%20<em>heading</em>%20with%20simpler%20HTML)
|
||||||
|
// and not
|
||||||
|
// [Some <em>heading</em> with simple HTML](#Some%20%3Cem%3Eheading%3C%2Fem%3E%20with%20simpler%20HTML)
|
||||||
|
// Also it won't be clickable at all if the heading contains #tags or more complex HTML
|
||||||
|
// (example: ## Some <em style="background: red">heading</em> #with-a-tag)
|
||||||
|
// (unless there is a way to encode these use cases that I didn't find)
|
||||||
|
}
|
||||||
|
return heading.heading
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptionsFromSourceText(sourceText = '') {
|
||||||
|
const options = {}
|
||||||
|
Object.keys(availableOptions).forEach((option) => {
|
||||||
|
options[option] = availableOptions[option].default
|
||||||
|
})
|
||||||
|
sourceText.split('\n').forEach((line) => {
|
||||||
|
const option = parseOptionFromSourceLine(line)
|
||||||
|
if (option !== null) {
|
||||||
|
options[option.name] = option.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOptionFromSourceLine(line) {
|
||||||
|
const matches = line.match(/([a-zA-Z0-9._ ]+):(.*)/)
|
||||||
|
if (line.startsWith('#') || !matches) return null
|
||||||
|
const possibleName = matches[1].trim()
|
||||||
|
const optionParams = availableOptions[possibleName]
|
||||||
|
let possibleValue = matches[2].trim()
|
||||||
|
if (!optionParams || optionParams.type !== 'string') {
|
||||||
|
// Strip comments from values except for strings (as a string may contain markdown)
|
||||||
|
possibleValue = possibleValue.replace(/#[^#]*$/, '').trim()
|
||||||
|
}
|
||||||
|
const valueError = new Error(`Invalid value for \`${possibleName}\``)
|
||||||
|
if (optionParams && optionParams.type === 'number') {
|
||||||
|
const value = parseInt(possibleValue)
|
||||||
|
if (value < 0) throw valueError
|
||||||
|
return { name: possibleName, value }
|
||||||
|
}
|
||||||
|
if (optionParams && optionParams.type === 'boolean') {
|
||||||
|
if (!['true', 'false'].includes(possibleValue)) throw valueError
|
||||||
|
return { name: possibleName, value: possibleValue === 'true' }
|
||||||
|
}
|
||||||
|
if (optionParams && optionParams.type === 'value') {
|
||||||
|
if (!optionParams.values.includes(possibleValue)) throw valueError
|
||||||
|
return { name: possibleName, value: possibleValue }
|
||||||
|
}
|
||||||
|
if (optionParams && optionParams.type === 'string') {
|
||||||
|
return { name: possibleName, value: possibleValue }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug(type, data) {
|
||||||
|
console.log(...[
|
||||||
|
`%cAutomatic Table Of Contents %c${type}:\n`,
|
||||||
|
'color: orange; font-weight: bold',
|
||||||
|
'font-weight: bold',
|
||||||
|
data,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObsidian() {
|
||||||
|
if (typeof process !== 'object') {
|
||||||
|
return true // Obsidian mobile doesn't have a global process object
|
||||||
|
}
|
||||||
|
return !process.env || !process.env.JEST_WORKER_ID // Jest runtime is not Obsidian
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObsidian()) {
|
||||||
|
module.exports = ObsidianAutomaticTableOfContents
|
||||||
|
} else {
|
||||||
|
module.exports = {
|
||||||
|
parseOptionsFromSourceText,
|
||||||
|
getMarkdownFromHeadings,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": "automatic-table-of-contents",
|
||||||
|
"name": "Automatic Table Of Contents",
|
||||||
|
"version": "1.4.0",
|
||||||
|
"minAppVersion": "1.3.0",
|
||||||
|
"description": "Create a table of contents in a note, that updates itself when the note changes",
|
||||||
|
"author": "Johan Satgé",
|
||||||
|
"authorUrl": "https://github.com/johansatge",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"viewImageEditor": true,
|
||||||
|
"viewImageInCPB": true,
|
||||||
|
"viewImageWithALink": true,
|
||||||
|
"viewImageOther": true,
|
||||||
|
"pinMode": false,
|
||||||
|
"pinMaximum": 3,
|
||||||
|
"pinCoverMode": true,
|
||||||
|
"imageMoveSpeed": 10,
|
||||||
|
"imgTipToggle": true,
|
||||||
|
"imgFullScreenMode": "FIT",
|
||||||
|
"imgViewBackgroundColor": "#00000000",
|
||||||
|
"imageBorderToggle": false,
|
||||||
|
"imageBorderWidth": "medium",
|
||||||
|
"imageBorderStyle": "solid",
|
||||||
|
"imageBorderColor": "red",
|
||||||
|
"galleryNavbarToggle": true,
|
||||||
|
"galleryNavbarDefaultColor": "#0000001A",
|
||||||
|
"galleryNavbarHoverColor": "#0000004D",
|
||||||
|
"galleryImgBorderActive": true,
|
||||||
|
"galleryImgBorderActiveColor": "#FF0000",
|
||||||
|
"moveTheImageHotkey": "ALT",
|
||||||
|
"switchTheImageHotkey": "NONE",
|
||||||
|
"doubleClickToolbar": "toolbar_full_screen",
|
||||||
|
"viewTriggerHotkey": "CTRL",
|
||||||
|
"realTimeUpdate": true,
|
||||||
|
"excludedFolders": [
|
||||||
|
".git/",
|
||||||
|
".obsidian/",
|
||||||
|
".trash/"
|
||||||
|
],
|
||||||
|
"includedFileRegex": ".*\\.md",
|
||||||
|
"mediaRootDirectory": "Misc/Storage/Attachments"
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": "awesome-image",
|
||||||
|
"name": "Awesome Image",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "One-stop solution for image management.",
|
||||||
|
"author": "AwesomeDog",
|
||||||
|
"authorUrl": "https://github.com/AwesomeDog",
|
||||||
|
"isDesktopOnly": true
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"renderNullAs": "\\-",
|
||||||
|
"taskCompletionTracking": false,
|
||||||
|
"taskCompletionUseEmojiShorthand": false,
|
||||||
|
"taskCompletionText": "completion",
|
||||||
|
"taskCompletionDateFormat": "yyyy-MM-dd",
|
||||||
|
"recursiveSubTaskCompletion": false,
|
||||||
|
"warnOnEmptyResult": true,
|
||||||
|
"refreshEnabled": true,
|
||||||
|
"refreshInterval": 2500,
|
||||||
|
"defaultDateFormat": "dd MMMM yyyy",
|
||||||
|
"defaultDateTimeFormat": "h:mm a - dd MMMM yyyy",
|
||||||
|
"maxRecursiveRenderDepth": 4,
|
||||||
|
"tableIdColumnName": "File",
|
||||||
|
"tableGroupColumnName": "Group",
|
||||||
|
"showResultCount": false,
|
||||||
|
"allowHtml": true,
|
||||||
|
"inlineQueryPrefix": "=",
|
||||||
|
"inlineJsQueryPrefix": "$=",
|
||||||
|
"inlineQueriesInCodeblocks": true,
|
||||||
|
"enableInlineDataview": true,
|
||||||
|
"enableDataviewJs": true,
|
||||||
|
"enableInlineDataviewJs": true,
|
||||||
|
"prettyRenderInlineFields": true,
|
||||||
|
"prettyRenderInlineFieldsInLivePreview": true,
|
||||||
|
"dataviewJsKeyword": "dataviewjs"
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"id": "dataview",
|
||||||
|
"name": "Dataview",
|
||||||
|
"version": "0.5.67",
|
||||||
|
"minAppVersion": "0.13.11",
|
||||||
|
"description": "Complex data views for the data-obsessed.",
|
||||||
|
"author": "Michael Brenan <blacksmithgu@gmail.com>",
|
||||||
|
"authorUrl": "https://github.com/blacksmithgu",
|
||||||
|
"helpUrl": "https://blacksmithgu.github.io/obsidian-dataview/",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/** Live Preview padding fixes, specifically for DataviewJS custom HTML elements. */
|
||||||
|
.is-live-preview .block-language-dataviewjs > p, .is-live-preview .block-language-dataviewjs > span {
|
||||||
|
line-height: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-dataview {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************/
|
||||||
|
/** Table Views **/
|
||||||
|
/*****************/
|
||||||
|
|
||||||
|
/* List View Default Styling; rendered internally as a table. */
|
||||||
|
.table-view-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-view-table > thead > tr, .table-view-table > tbody > tr {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-view-table > tbody > tr:hover {
|
||||||
|
background-color: var(--table-row-background-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-view-table > thead > tr > th {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: larger;
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: solid;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-view-table > tbody > tr > td {
|
||||||
|
text-align: left;
|
||||||
|
border: none;
|
||||||
|
font-weight: 400;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-view-table ul, .table-view-table ol {
|
||||||
|
margin-block-start: 0.2em !important;
|
||||||
|
margin-block-end: 0.2em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rendered value styling for any view. */
|
||||||
|
.dataview-result-list-root-ul {
|
||||||
|
padding: 0em !important;
|
||||||
|
margin: 0em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview-result-list-ul {
|
||||||
|
margin-block-start: 0.2em !important;
|
||||||
|
margin-block-end: 0.2em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generic grouping styling. */
|
||||||
|
.dataview.result-group {
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************/
|
||||||
|
/** Inline Fields **/
|
||||||
|
/*******************/
|
||||||
|
|
||||||
|
.dataview.inline-field-key {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
background-color: var(--background-primary-alt);
|
||||||
|
color: var(--text-nav-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview.inline-field-value {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
background-color: var(--background-secondary-alt);
|
||||||
|
color: var(--text-nav-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview.inline-field-standalone-value {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
font-family: var(--font-monospace);
|
||||||
|
background-color: var(--background-secondary-alt);
|
||||||
|
color: var(--text-nav-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************/
|
||||||
|
/** Task View **/
|
||||||
|
/***************/
|
||||||
|
|
||||||
|
.dataview.task-list-item, .dataview.task-list-basic-item {
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview.task-list-item:hover, .dataview.task-list-basic-item:hover {
|
||||||
|
background-color: var(--text-selection);
|
||||||
|
box-shadow: -40px 0 0 var(--text-selection);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************/
|
||||||
|
/** Error Views **/
|
||||||
|
/*****************/
|
||||||
|
|
||||||
|
div.dataview-error-box {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 150px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 4px dashed var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview-error-message {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************************/
|
||||||
|
/** Additional Metadata **/
|
||||||
|
/*************************/
|
||||||
|
|
||||||
|
.dataview.small-text {
|
||||||
|
font-size: smaller;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview.small-text::before {
|
||||||
|
content: "(";
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataview.small-text::after {
|
||||||
|
content: ")";
|
||||||
|
}
|
|
@ -78,11 +78,11 @@
|
||||||
"color": "6MiKFs3bdHnnogc4PtXTJ"
|
"color": "6MiKFs3bdHnnogc4PtXTJ"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Design",
|
"path": "10 Design",
|
||||||
"color": "CPrDSGPNfbzbEdB5ZVbIp"
|
"color": "CPrDSGPNfbzbEdB5ZVbIp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "Tech",
|
"path": "20 Tech",
|
||||||
"color": "l9aBWg3KMh7IeMZWLwcjR"
|
"color": "l9aBWg3KMh7IeMZWLwcjR"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"storeInfo": true,
|
||||||
|
"saveImages": true,
|
||||||
|
"imageLocation": "specifiedFolder",
|
||||||
|
"imageFolder": "Misc/Storage/Thumbnails",
|
||||||
|
"responsiveCardStyle": true,
|
||||||
|
"youtubeApiKey": ""
|
||||||
|
}
|
|
@ -0,0 +1,616 @@
|
||||||
|
/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
var __create = Object.create;
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __markAsModule = (target) => __defProp(target, "__esModule", { value: true });
|
||||||
|
var __export = (target, all) => {
|
||||||
|
__markAsModule(target);
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __reExport = (target, module2, desc) => {
|
||||||
|
if (module2 && typeof module2 === "object" || typeof module2 === "function") {
|
||||||
|
for (let key of __getOwnPropNames(module2))
|
||||||
|
if (!__hasOwnProp.call(target, key) && key !== "default")
|
||||||
|
__defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
var __toModule = (module2) => {
|
||||||
|
return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2);
|
||||||
|
};
|
||||||
|
var __async = (__this, __arguments, generator) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var fulfilled = (value) => {
|
||||||
|
try {
|
||||||
|
step(generator.next(value));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var rejected = (value) => {
|
||||||
|
try {
|
||||||
|
step(generator.throw(value));
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
||||||
|
step((generator = generator.apply(__this, __arguments)).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
__export(exports, {
|
||||||
|
default: () => ThumbyPlugin
|
||||||
|
});
|
||||||
|
var import_obsidian2 = __toModule(require("obsidian"));
|
||||||
|
|
||||||
|
// settings.ts
|
||||||
|
var import_obsidian = __toModule(require("obsidian"));
|
||||||
|
var ThumbySettingTab = class extends import_obsidian.PluginSettingTab {
|
||||||
|
constructor(app, plugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
display() {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
containerEl.createEl("h2", { text: "Thumbnails Settings" });
|
||||||
|
console.log(this.plugin.settings);
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Save Thumbnail Info").setDesc("Save thumbnail information inside your note, so they work offline").addToggle((toggle) => toggle.setValue(this.plugin.settings.storeInfo).onChange((value) => __async(this, null, function* () {
|
||||||
|
this.plugin.settings.storeInfo = value;
|
||||||
|
yield this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
})));
|
||||||
|
if (this.plugin.settings.storeInfo) {
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Save Images").setDesc("Save thumbnail images locally in vault").addToggle((toggle) => toggle.setValue(this.plugin.settings.saveImages).onChange((value) => __async(this, null, function* () {
|
||||||
|
this.plugin.settings.saveImages = value;
|
||||||
|
yield this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
})));
|
||||||
|
if (this.plugin.settings.saveImages) {
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Image Location").setDesc("Where thumbnail images should be saved").addDropdown((dropdown) => dropdown.addOption("defaultAttachment", "Default attachment location").addOption("specifiedFolder", "In the folder specified below").setValue(this.plugin.settings.imageLocation).onChange((value) => __async(this, null, function* () {
|
||||||
|
this.plugin.settings.imageLocation = value;
|
||||||
|
this.display();
|
||||||
|
yield this.plugin.saveSettings();
|
||||||
|
})));
|
||||||
|
if (this.plugin.settings.imageLocation === "defaultAttachment") {
|
||||||
|
const attachmentLocation = this.app.vault.getConfig("attachmentFolderPath");
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Default attachment location").setDesc("Options > Files & Links > Default location for new attachments").addText((text) => text.setValue(attachmentLocation).setDisabled(true)).setClass("default-attachment-info");
|
||||||
|
} else if (this.plugin.settings.imageLocation === "specifiedFolder") {
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Image Folder").setDesc("The folder where thumbnail images should be saved").addText((text) => text.setPlaceholder("ex: Files/Thumbnails").setValue(this.plugin.settings.imageFolder).onChange((value) => __async(this, null, function* () {
|
||||||
|
this.plugin.settings.imageFolder = value;
|
||||||
|
yield this.plugin.saveSettings();
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Responsive Card-Style Thumbnails").setDesc("Switch to card-style thumbnails for narrow screens").addToggle((toggle) => toggle.setValue(this.plugin.settings.responsiveCardStyle).onChange((value) => __async(this, null, function* () {
|
||||||
|
this.plugin.settings.responsiveCardStyle = value;
|
||||||
|
yield this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
})));
|
||||||
|
new import_obsidian.Setting(containerEl).setName("YouTube API Key (optional)").setDesc("An API Key for the YouTube Data API").addExtraButton((btn) => btn.setIcon("info").setTooltip("A few videos have been discovered that can't be found the normal way. If you provide an API key for the YouTube Data API, this plugin will use the API as a backup.", { placement: "top" }).setDisabled(true)).addText((text) => text.setValue(this.plugin.settings.youtubeApiKey).onChange((value) => __async(this, null, function* () {
|
||||||
|
this.plugin.settings.youtubeApiKey = value;
|
||||||
|
yield this.plugin.saveSettings();
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// main.ts
|
||||||
|
var DEFAULT_SETTINGS = {
|
||||||
|
storeInfo: false,
|
||||||
|
saveImages: false,
|
||||||
|
imageLocation: "defaultAttachment",
|
||||||
|
imageFolder: "",
|
||||||
|
responsiveCardStyle: true,
|
||||||
|
youtubeApiKey: ""
|
||||||
|
};
|
||||||
|
var URL_TYPES = {
|
||||||
|
youtube: [
|
||||||
|
{ match: "https://www.youtube.com/watch?v=", idPattern: /v=([-\w\d]+)/ },
|
||||||
|
{ match: "https://youtu.be/", idPattern: /youtu.be\/([-\w\d]+)/ },
|
||||||
|
{ match: "youtube.com/shorts/", idPattern: /shorts\/([-\w\d]+)/ },
|
||||||
|
{ match: "youtube.com/live/", idPattern: /live\/(\w+)/ }
|
||||||
|
],
|
||||||
|
vimeo: [
|
||||||
|
{ match: "https://vimeo.com/", idPattern: /vimeo.com\/([\w\d]+)/ }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
var ThumbyPlugin = class extends import_obsidian2.Plugin {
|
||||||
|
loadSettings() {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
saveSettings() {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
yield this.saveData(this.settings);
|
||||||
|
const editors = document.querySelectorAll(".cm-editor");
|
||||||
|
for (const key in editors) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(editors, key)) {
|
||||||
|
const editor = editors[key];
|
||||||
|
this.responsiveCardCheck(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
responsiveCardCheck(editor) {
|
||||||
|
const vidBlocks = editor.querySelectorAll(".block-language-vid");
|
||||||
|
for (const key in vidBlocks) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(vidBlocks, key)) {
|
||||||
|
const block = vidBlocks[key];
|
||||||
|
if (this.settings.responsiveCardStyle && block && block.offsetWidth < 370) {
|
||||||
|
block.addClass("thumbnail-card-style");
|
||||||
|
} else {
|
||||||
|
block.removeClass("thumbnail-card-style");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onload() {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
yield this.loadSettings();
|
||||||
|
this.addSettingTab(new ThumbySettingTab(this.app, this));
|
||||||
|
this.editorObserver = new ResizeObserver((entries) => {
|
||||||
|
for (const editor of entries) {
|
||||||
|
this.responsiveCardCheck(editor.target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const editors = document.querySelectorAll(".cm-editor");
|
||||||
|
for (const key in editors) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(editors, key)) {
|
||||||
|
const editor = editors[key];
|
||||||
|
this.editorObserver.observe(editor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.registerMarkdownCodeBlockProcessor("vid", (source, el, ctx) => __async(this, null, function* () {
|
||||||
|
var _a, _b, _c;
|
||||||
|
this.createDummyBlock(el);
|
||||||
|
const sourceLines = source.trim().split("\n");
|
||||||
|
const url = sourceLines[0];
|
||||||
|
let info;
|
||||||
|
if (this.settings.storeInfo) {
|
||||||
|
info = this.parseStoredInfo(source);
|
||||||
|
}
|
||||||
|
if (!this.settings.storeInfo || !info.infoStored) {
|
||||||
|
info = yield this.getVideoInfo(url);
|
||||||
|
}
|
||||||
|
if (info.networkError && !info.infoStored) {
|
||||||
|
this.removeDummyBlock(el);
|
||||||
|
const url2 = source.trim().split("\n")[0];
|
||||||
|
el.createEl("a", { text: url2, href: url2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sourcePath = typeof ctx == "string" ? ctx : (_c = (_b = ctx == null ? void 0 : ctx.sourcePath) != null ? _b : (_a = this.app.workspace.getActiveFile()) == null ? void 0 : _a.path) != null ? _c : "";
|
||||||
|
if (!info.vidFound) {
|
||||||
|
const component = new import_obsidian2.MarkdownRenderChild(el);
|
||||||
|
this.removeDummyBlock(el);
|
||||||
|
import_obsidian2.MarkdownRenderer.renderMarkdown(`>[!WARNING] Cannot find video
|
||||||
|
>${info.url}`, el, sourcePath, component);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.hasManyUrls(sourceLines)) {
|
||||||
|
const component = new import_obsidian2.MarkdownRenderChild(el);
|
||||||
|
this.removeDummyBlock(el);
|
||||||
|
import_obsidian2.MarkdownRenderer.renderMarkdown(`>[!WARNING] Cannot accept multiple URLs yet`, el, sourcePath, component);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.settings.storeInfo && !info.infoStored) {
|
||||||
|
this.storeVideoInfo(info, el, ctx);
|
||||||
|
}
|
||||||
|
if (!this.settings.storeInfo && sourceLines.length > 1) {
|
||||||
|
this.removeStoredInfo(info, el, ctx);
|
||||||
|
}
|
||||||
|
this.removeDummyBlock(el);
|
||||||
|
this.createThumbnail(el, info);
|
||||||
|
}));
|
||||||
|
this.addCommand({
|
||||||
|
id: "insert-thumbnail-from-clipboard",
|
||||||
|
name: "Insert thumbnail from URL in clipboard",
|
||||||
|
editorCallback: (editor, view) => __async(this, null, function* () {
|
||||||
|
const clipText = yield navigator.clipboard.readText();
|
||||||
|
const id = yield this.getVideoId(clipText);
|
||||||
|
if (id === "") {
|
||||||
|
new import_obsidian2.Notice("No valid video in clipboard", 2e3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.replaceSelection(`\`\`\`vid
|
||||||
|
${clipText}
|
||||||
|
\`\`\``);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
this.addCommand({
|
||||||
|
id: "insert-video-title-link",
|
||||||
|
name: "Insert link with video title from URL in clipboard",
|
||||||
|
editorCallback: (editor, view) => __async(this, null, function* () {
|
||||||
|
const clipText = yield navigator.clipboard.readText();
|
||||||
|
const id = yield this.getVideoId(clipText);
|
||||||
|
if (id === "") {
|
||||||
|
new import_obsidian2.Notice("No valid video in clipboard", 2e3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const info = yield this.getVideoInfo(clipText);
|
||||||
|
editor.replaceSelection(`[${info.title}](${info.url})`);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onunload() {
|
||||||
|
this.editorObserver.disconnect();
|
||||||
|
}
|
||||||
|
hasManyUrls(lines) {
|
||||||
|
return lines.length > 1 && lines.every((e) => /^((https*:\/\/)|(www\.))+\S*$/.test(e.trim()));
|
||||||
|
}
|
||||||
|
createThumbnail(el, info) {
|
||||||
|
let thumbnailUrl = info.thumbnail;
|
||||||
|
if (this.pathIsLocal(thumbnailUrl)) {
|
||||||
|
const file = this.app.vault.getAbstractFileByPath(thumbnailUrl);
|
||||||
|
if (file) {
|
||||||
|
thumbnailUrl = this.app.vault.getResourcePath(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const container = el.createEl("a", { href: info.url });
|
||||||
|
container.addClass("thumbnail");
|
||||||
|
container.createEl("img", { attr: { "src": thumbnailUrl } }).addClass("thumbnail-img");
|
||||||
|
const textBox = container.createDiv();
|
||||||
|
textBox.addClass("thumbnail-text");
|
||||||
|
textBox.createDiv({ text: info.title, title: info.title }).addClass("thumbnail-title");
|
||||||
|
textBox.createEl("a", { text: info.author, href: info.authorUrl, title: info.author }).addClass("thumbnail-author");
|
||||||
|
const timestamp = this.getTimestamp(info.url);
|
||||||
|
if (timestamp !== "") {
|
||||||
|
container.createDiv({ text: timestamp }).addClass("timestamp");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createDummyBlock(el) {
|
||||||
|
const container = el.createDiv();
|
||||||
|
container.addClass("dummy-container");
|
||||||
|
}
|
||||||
|
removeDummyBlock(el) {
|
||||||
|
const dummy = el.querySelector(".dummy-container");
|
||||||
|
if (dummy) {
|
||||||
|
el.removeChild(dummy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getTimestamp(url) {
|
||||||
|
let tIndex = url.indexOf("?t=");
|
||||||
|
if (tIndex === -1) {
|
||||||
|
tIndex = url.indexOf("&t=");
|
||||||
|
}
|
||||||
|
if (tIndex === -1) {
|
||||||
|
tIndex = url.indexOf("#t=");
|
||||||
|
}
|
||||||
|
if (tIndex === -1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const search = /[?&#]t=(?:(\d+)h)*(?:(\d+)m)*(?:(\d+)s)*(\d+)*/.exec(url);
|
||||||
|
search.shift();
|
||||||
|
const times = search.map((v) => parseInt(v) || 0);
|
||||||
|
let seconds = times.pop();
|
||||||
|
if (times[2] > 59) {
|
||||||
|
seconds = times[2];
|
||||||
|
}
|
||||||
|
if (seconds) {
|
||||||
|
times[2] = seconds % 60;
|
||||||
|
times[1] = Math.floor(seconds / 60) % 60;
|
||||||
|
times[0] = Math.floor(seconds / 3600);
|
||||||
|
}
|
||||||
|
const secStr = String(times[2]).padStart(2, "0");
|
||||||
|
let minStr = String(times[1]);
|
||||||
|
const hrStr = String(times[0]);
|
||||||
|
let timeStr = `${minStr}:${secStr}`;
|
||||||
|
if (times[0]) {
|
||||||
|
minStr = minStr.padStart(2, "0");
|
||||||
|
timeStr = `${hrStr}:${minStr}:${secStr}`;
|
||||||
|
}
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
pathIsLocal(path) {
|
||||||
|
return path.indexOf("https://") !== 0;
|
||||||
|
}
|
||||||
|
parseStoredInfo(source) {
|
||||||
|
const info = {
|
||||||
|
url: "",
|
||||||
|
thumbnail: "",
|
||||||
|
title: "",
|
||||||
|
author: "",
|
||||||
|
authorUrl: "",
|
||||||
|
vidFound: false,
|
||||||
|
networkError: false,
|
||||||
|
infoStored: false,
|
||||||
|
imageSaved: false
|
||||||
|
};
|
||||||
|
const input = source.trim().split("\n");
|
||||||
|
if (input.length !== 5) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const parsedInput = {
|
||||||
|
Url: "",
|
||||||
|
Title: "",
|
||||||
|
Author: "",
|
||||||
|
Thumbnail: "",
|
||||||
|
AuthorUrl: ""
|
||||||
|
};
|
||||||
|
for (const [i, line] of input.entries()) {
|
||||||
|
if (i !== 0) {
|
||||||
|
const matches = line.match(/(\w+): (.+)/);
|
||||||
|
if (matches === null) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
const key = matches[1];
|
||||||
|
const val = matches[2];
|
||||||
|
parsedInput[key] = val;
|
||||||
|
} else {
|
||||||
|
parsedInput["Url"] = input[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key in parsedInput) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(parsedInput, key)) {
|
||||||
|
const value = parsedInput[key];
|
||||||
|
if (!value || value === "") {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.url = parsedInput["Url"];
|
||||||
|
info.title = parsedInput["Title"];
|
||||||
|
info.author = parsedInput["Author"];
|
||||||
|
info.thumbnail = parsedInput["Thumbnail"];
|
||||||
|
info.authorUrl = parsedInput["AuthorUrl"];
|
||||||
|
info.vidFound = true;
|
||||||
|
if (this.pathIsLocal(info.thumbnail)) {
|
||||||
|
const existingFile = this.app.vault.getAbstractFileByPath(info.thumbnail);
|
||||||
|
if (existingFile) {
|
||||||
|
info.imageSaved = true;
|
||||||
|
} else if (this.settings.saveImages) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
if (!this.settings.saveImages) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
} else if (this.settings.saveImages) {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
info.infoStored = true;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
storeVideoInfo(info, el, ctx) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
const section = ctx.getSectionInfo(el);
|
||||||
|
if (!section) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.settings.saveImages && !info.imageSaved) {
|
||||||
|
info.thumbnail = yield this.saveImage(info);
|
||||||
|
}
|
||||||
|
const content = `\`\`\`vid
|
||||||
|
${info.url}
|
||||||
|
Title: ${info.title}
|
||||||
|
Author: ${info.author}
|
||||||
|
Thumbnail: ${info.thumbnail}
|
||||||
|
AuthorUrl: ${info.authorUrl}
|
||||||
|
\`\`\``;
|
||||||
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
|
||||||
|
if (view) {
|
||||||
|
const startPos = {
|
||||||
|
line: section.lineStart,
|
||||||
|
ch: 0
|
||||||
|
};
|
||||||
|
const endPos = {
|
||||||
|
line: section.lineEnd,
|
||||||
|
ch: view.editor.getLine(section.lineEnd).length
|
||||||
|
};
|
||||||
|
view.editor.replaceRange(content, startPos, endPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
saveImage(info) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
const id = yield this.getVideoId(info.url);
|
||||||
|
let filePath = "";
|
||||||
|
const currentNote = this.app.workspace.getActiveFile();
|
||||||
|
if (this.settings.imageLocation === "specifiedFolder") {
|
||||||
|
filePath = `${this.settings.imageFolder}/${id}.jpg`;
|
||||||
|
} else {
|
||||||
|
filePath = yield this.app.vault.getAvailablePathForAttachments(id, "jpg", currentNote);
|
||||||
|
const pathRegex = /(.*) \d+\.jpg/;
|
||||||
|
filePath = filePath.replace(pathRegex, "$1.jpg");
|
||||||
|
}
|
||||||
|
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
||||||
|
if (existingFile) {
|
||||||
|
return existingFile.path;
|
||||||
|
}
|
||||||
|
const folderMatch = filePath.match(/(.+)\/.+\.jpg/);
|
||||||
|
if (folderMatch) {
|
||||||
|
const folderPath = folderMatch[1];
|
||||||
|
const existingFolder = this.app.vault.getAbstractFileByPath(folderPath);
|
||||||
|
if (this.settings.imageLocation === "specifiedFolder" && !existingFolder) {
|
||||||
|
new import_obsidian2.Notice(`Thumbnails: The folder you specified (${this.settings.imageFolder}) does not exist.`);
|
||||||
|
return info.thumbnail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const reqParam = {
|
||||||
|
url: info.thumbnail
|
||||||
|
};
|
||||||
|
let file;
|
||||||
|
try {
|
||||||
|
const req = yield (0, import_obsidian2.requestUrl)(reqParam);
|
||||||
|
if (req.status === 200) {
|
||||||
|
file = yield this.app.vault.createBinary(filePath, req.arrayBuffer);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return info.thumbnail;
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
const localUrl = file.path;
|
||||||
|
return localUrl;
|
||||||
|
}
|
||||||
|
return info.thumbnail;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getTrimmedResourcePath(file) {
|
||||||
|
const path = this.app.vault.getResourcePath(file);
|
||||||
|
const endPos = path.indexOf(".jpg") + 4;
|
||||||
|
return path.substring(0, endPos);
|
||||||
|
}
|
||||||
|
removeStoredInfo(info, el, ctx) {
|
||||||
|
const section = ctx.getSectionInfo(el);
|
||||||
|
if (!section) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = `\`\`\`vid
|
||||||
|
${info.url}
|
||||||
|
\`\`\``;
|
||||||
|
const view = this.app.workspace.getActiveViewOfType(import_obsidian2.MarkdownView);
|
||||||
|
if (view) {
|
||||||
|
const startPos = {
|
||||||
|
line: section.lineStart,
|
||||||
|
ch: 0
|
||||||
|
};
|
||||||
|
const endPos = {
|
||||||
|
line: section.lineEnd,
|
||||||
|
ch: view.editor.getLine(section.lineEnd).length
|
||||||
|
};
|
||||||
|
view.editor.replaceRange(content, startPos, endPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getVideoInfo(url) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
const info = {
|
||||||
|
url,
|
||||||
|
thumbnail: "",
|
||||||
|
title: "",
|
||||||
|
author: "",
|
||||||
|
authorUrl: "",
|
||||||
|
vidFound: false,
|
||||||
|
networkError: false,
|
||||||
|
infoStored: false,
|
||||||
|
imageSaved: false
|
||||||
|
};
|
||||||
|
let reqUrl = "";
|
||||||
|
let isYoutube = false;
|
||||||
|
for (const type of URL_TYPES.youtube) {
|
||||||
|
if (url.includes(type.match)) {
|
||||||
|
isYoutube = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let isVimeo = false;
|
||||||
|
for (const type of URL_TYPES.vimeo) {
|
||||||
|
if (url.includes(type.match)) {
|
||||||
|
isVimeo = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isYoutube) {
|
||||||
|
reqUrl = `https://www.youtube.com/oembed?format=json&url=${url}`;
|
||||||
|
} else if (isVimeo) {
|
||||||
|
reqUrl = `https://vimeo.com/api/oembed.json?url=${url}`;
|
||||||
|
} else {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const reqParam = {
|
||||||
|
url: reqUrl,
|
||||||
|
throw: false
|
||||||
|
};
|
||||||
|
const res = yield (0, import_obsidian2.requestUrl)(reqParam);
|
||||||
|
if (res.status === 200) {
|
||||||
|
info.title = res.json.title;
|
||||||
|
info.author = res.json.author_name;
|
||||||
|
info.authorUrl = res.json.author_url;
|
||||||
|
info.vidFound = true;
|
||||||
|
} else if (this.settings.youtubeApiKey && isYoutube) {
|
||||||
|
console.log("Thumbnails: Oembed failed, using YouTube API");
|
||||||
|
const videoId = yield this.getVideoId(url);
|
||||||
|
const youtubeUrl = `https://youtube.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${this.settings.youtubeApiKey}`;
|
||||||
|
const youtubeReqParam = {
|
||||||
|
url: youtubeUrl,
|
||||||
|
throw: false
|
||||||
|
};
|
||||||
|
const youtubeApiRes = yield (0, import_obsidian2.requestUrl)(youtubeReqParam);
|
||||||
|
if (youtubeApiRes.status === 200) {
|
||||||
|
const vidSnippet = youtubeApiRes.json.items[0].snippet;
|
||||||
|
info.authorUrl = "javascript:void(0)";
|
||||||
|
const channelQueryUrl = `https://youtube.googleapis.com/youtube/v3/channels?part=snippet&id=${vidSnippet.channelId}&key=${this.settings.youtubeApiKey}`;
|
||||||
|
const channelQueryParam = {
|
||||||
|
url: channelQueryUrl,
|
||||||
|
throw: false
|
||||||
|
};
|
||||||
|
const channelQueryRes = yield (0, import_obsidian2.requestUrl)(channelQueryParam);
|
||||||
|
if (channelQueryRes.status === 200) {
|
||||||
|
const channelSnippet = channelQueryRes.json.items[0].snippet;
|
||||||
|
const channelCustomUrl = channelSnippet.customUrl;
|
||||||
|
const channelUrl = `https://www.youtube.com/${channelCustomUrl}`;
|
||||||
|
info.authorUrl = channelUrl;
|
||||||
|
}
|
||||||
|
info.title = vidSnippet.title;
|
||||||
|
info.author = vidSnippet.channelTitle;
|
||||||
|
info.vidFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.vidFound) {
|
||||||
|
if (isYoutube) {
|
||||||
|
const videoId = yield this.getVideoId(url);
|
||||||
|
info.thumbnail = `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`;
|
||||||
|
} else {
|
||||||
|
info.thumbnail = res.json.thumbnail_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
info.networkError = true;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getVideoId(url) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
let id = "";
|
||||||
|
for (const type of URL_TYPES.youtube) {
|
||||||
|
if (url.includes(type.match)) {
|
||||||
|
const matches = url.match(type.idPattern);
|
||||||
|
if (matches !== null) {
|
||||||
|
id = matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const vimeoType = URL_TYPES.vimeo[0];
|
||||||
|
if (url.includes(vimeoType.match)) {
|
||||||
|
const matches = url.match(vimeoType.idPattern);
|
||||||
|
if (matches !== null) {
|
||||||
|
id = matches[1];
|
||||||
|
if (!/^[0-9]+$/.exec(id)) {
|
||||||
|
id = yield this.fetchVimeoVideoId(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fetchVimeoVideoId(url) {
|
||||||
|
return __async(this, null, function* () {
|
||||||
|
let id = "";
|
||||||
|
try {
|
||||||
|
const reqParam = {
|
||||||
|
url: `https://vimeo.com/api/oembed.json?url=${url}`
|
||||||
|
};
|
||||||
|
const res = yield (0, import_obsidian2.requestUrl)(reqParam);
|
||||||
|
if (res.status === 200 && res.json.video_id) {
|
||||||
|
id = res.json.video_id.toString();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"id": "obsidian-thumbnails",
|
||||||
|
"name": "Thumbnails",
|
||||||
|
"version": "1.3.0",
|
||||||
|
"minAppVersion": "1.0.0",
|
||||||
|
"description": "Insert video thumbnails into your notes",
|
||||||
|
"author": "Michael Harris",
|
||||||
|
"authorUrl": "https://github.com/meikul",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
.block-language-vid{
|
||||||
|
position: relative;
|
||||||
|
/* display: block; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid~.edit-block-button{
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid>a.thumbnail{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
color: var(--text-default);
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 10px 0;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
/* border-radius: 8px; */
|
||||||
|
/* overflow: hidden; */
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail-text{
|
||||||
|
padding: 0 16px;
|
||||||
|
margin-left: -8px;
|
||||||
|
flex-grow: 1;
|
||||||
|
line-height: 1.3;
|
||||||
|
transition: background 0.3s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .block-language-vid>a:hover{
|
||||||
|
box-shadow: 0 .2rem .5rem var(--background-modifier-box-shadow);
|
||||||
|
} */
|
||||||
|
|
||||||
|
.block-language-vid>a.thumbnail:hover .thumbnail-text{
|
||||||
|
background: var(--background-primary-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail-title{
|
||||||
|
position: relative;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail-img{
|
||||||
|
width: 150px;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail .timestamp{
|
||||||
|
font-size: calc(var(--font-text-size) - 5px);
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
left: 4px;
|
||||||
|
padding: 0 5px 0 5px;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail-author{
|
||||||
|
display: block;
|
||||||
|
color: var(--text-faint);
|
||||||
|
font-size: calc(var(--font-text-size) - 3px);
|
||||||
|
width: fit-content;
|
||||||
|
margin-top: 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail-author:hover{
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.block-language-vid .thumbnail-error{
|
||||||
|
color: var(--text-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .dummy-container{
|
||||||
|
position: relative;
|
||||||
|
height: 84.34px;
|
||||||
|
width: 400px;
|
||||||
|
/* background: var(--background-primary-alt); */
|
||||||
|
/* background: linear-gradient(-80deg, var(--background-primary-alt) 45%, rgba(125, 125, 125, 0.2) 50%, var(--background-primary-alt) 55%); */
|
||||||
|
background: linear-gradient(-80deg, rgba(125, 125, 125, 0.05) 45%, rgba(125, 125, 125, 0.2) 50%, rgba(125, 125, 125, 0.05) 55%);
|
||||||
|
animation: gradient 2s linear infinite;
|
||||||
|
background-size: 400% 400%;
|
||||||
|
margin: 10px 0;
|
||||||
|
opacity: 1;
|
||||||
|
/* transition: opacity 1s linear;
|
||||||
|
&.hidden{
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .dummy-image{
|
||||||
|
height: 100%;
|
||||||
|
width: 150px;
|
||||||
|
background: rgba(125, 125, 125, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-language-vid .dummy-title{
|
||||||
|
position: absolute;
|
||||||
|
left: 160px;
|
||||||
|
top: 6px;
|
||||||
|
height: 20px;
|
||||||
|
width: calc(100% - 170px);
|
||||||
|
background: rgba(125, 125, 125, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-settings .setting-item.default-attachment-info{
|
||||||
|
border: none;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-settings .setting-item.default-attachment-info input{
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-settings .setting-item.default-attachment-info .setting-item-description{
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-plugin__item-title .block-language-vid,
|
||||||
|
:not(.kanban-plugin__item-title) .block-language-vid.thumbnail-card-style{
|
||||||
|
&>a.thumbnail{
|
||||||
|
margin: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
.thumbnail-img{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.thumbnail-text{
|
||||||
|
padding: 16px 2px;
|
||||||
|
margin: -8px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* :not(.kanban-plugin__item-title) .block-language-vid.thumbnail-card-style {
|
||||||
|
&>a.thumbnail {
|
||||||
|
margin: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-text {
|
||||||
|
padding: 16px 2px;
|
||||||
|
margin: -8px 0px;
|
||||||
|
}
|
||||||
|
} */
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"id": "tag-wrangler",
|
||||||
|
"name": "Tag Wrangler",
|
||||||
|
"author": "PJ Eby",
|
||||||
|
"authorUrl": "https://github.com/pjeby",
|
||||||
|
"version": "0.6.1",
|
||||||
|
"minAppVersion": "1.2.8",
|
||||||
|
"description": "Rename, merge, toggle, and search tags from the tag pane",
|
||||||
|
"fundingUrl": "https://dirtsimple.org/tips/tag-wrangler",
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
.ophidian-dialog .dialog-text{margin-bottom:.75em}.ophidian-dialog.mod-confirmation input[type=text]:invalid,.ophidian-dialog.mod-confirmation input[type=text]:invalid:enabled:focus,.ophidian-dialog.mod-confirmation input[type=text][aria-invalid=true],.ophidian-dialog.mod-confirmation input[type=text][aria-invalid=true]:enabled:focus{border-color:var(--text-error);background-color:var(--background-modifier-error)}
|
|
@ -13,11 +13,11 @@
|
||||||
"state": {
|
"state": {
|
||||||
"type": "canvas",
|
"type": "canvas",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Design/Hypothèses.canvas",
|
"file": "10 Design/Hypothèses.canvas",
|
||||||
"viewState": {
|
"viewState": {
|
||||||
"x": 1550.8400312707965,
|
"x": 1545.0747584536132,
|
||||||
"y": 668.2292932868143,
|
"y": 711.910177720503,
|
||||||
"zoom": -0.6511609903979585
|
"zoom": -0.6831531004462654
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,8 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
"width": 300
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
},
|
},
|
||||||
"right": {
|
"right": {
|
||||||
"id": "d596c1e5acc9495a",
|
"id": "d596c1e5acc9495a",
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
"state": {
|
"state": {
|
||||||
"type": "backlink",
|
"type": "backlink",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Design/Hypothèses.canvas",
|
"file": "10 Design/Hypothèses.canvas",
|
||||||
"collapseAll": false,
|
"collapseAll": false,
|
||||||
"extraContext": false,
|
"extraContext": false,
|
||||||
"sortOrder": "alphabetical",
|
"sortOrder": "alphabetical",
|
||||||
|
@ -105,7 +106,7 @@
|
||||||
"state": {
|
"state": {
|
||||||
"type": "outgoing-link",
|
"type": "outgoing-link",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Design/Hypothèses.canvas",
|
"file": "10 Design/Hypothèses.canvas",
|
||||||
"linksCollapsed": false,
|
"linksCollapsed": false,
|
||||||
"unlinkedCollapsed": true
|
"unlinkedCollapsed": true
|
||||||
}
|
}
|
||||||
|
@ -128,10 +129,17 @@
|
||||||
"state": {
|
"state": {
|
||||||
"type": "outline",
|
"type": "outline",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Design/Hypothèses.canvas"
|
"file": "10 Design/Hypothèses.canvas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"currentTab": 3
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "d534e6d64903cf46",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
{
|
{
|
||||||
"id": "f00663bec52e7345",
|
"id": "f00663bec52e7345",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
|
@ -140,29 +148,40 @@
|
||||||
"state": {}
|
"state": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"currentTab": 4
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
"width": 336.5
|
"width": 336.5,
|
||||||
|
"collapsed": true
|
||||||
},
|
},
|
||||||
"left-ribbon": {
|
"left-ribbon": {
|
||||||
"hiddenItems": {
|
"hiddenItems": {
|
||||||
"switcher:Open quick switcher": false,
|
"switcher:Open quick switcher": true,
|
||||||
"graph:Open graph view": false,
|
"graph:Open graph view": true,
|
||||||
"canvas:Create new canvas": false,
|
"canvas:Create new canvas": true,
|
||||||
"daily-notes:Open today's daily note": false,
|
"daily-notes:Open today's daily note": true,
|
||||||
"templates:Insert template": false,
|
"templates:Insert template": true,
|
||||||
"command-palette:Open command palette": false,
|
"command-palette:Open command palette": true,
|
||||||
"obsidian-git:Open Git source control": false
|
"obsidian-git:Open Git source control": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "f00663bec52e7345",
|
"active": "f3ec35ffdace1d82",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"Welcome.md",
|
"Misc/How to Obsidian.md",
|
||||||
"Design/Hypothèses.canvas",
|
"Misc/Storage/Attachments/8/2/8",
|
||||||
"Tech",
|
"Misc/Storage/Attachments/8/2",
|
||||||
"Design"
|
"Misc/Storage/Attachments/8",
|
||||||
|
"Misc/Storage/Attachments/8/2/8/828b38e1763369b52b2efba15d4668326e86f70408af7ebaf2d99b9f1210a5ed.png",
|
||||||
|
"Misc/Storage/Attachments/9/4/e",
|
||||||
|
"Misc/Storage/Attachments/9/4",
|
||||||
|
"Misc/Storage/Attachments/9",
|
||||||
|
"Misc/Storage/Attachments/9/4/e/94ebc76d29274d246b1bd9bcb4b1714cc49112ad86de394b437f3e65d4b38ec5.png",
|
||||||
|
"10 Design/Hypothèses.canvas",
|
||||||
|
"Misc/Storage/Attachments",
|
||||||
|
"Misc/Storage/Thumbnails",
|
||||||
|
"Misc/Storage",
|
||||||
|
"Misc",
|
||||||
|
"Welcome.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
|
||||||
|
# Shortcuts
|
||||||
|
|
||||||
|
![[94ebc76d29274d246b1bd9bcb4b1714cc49112ad86de394b437f3e65d4b38ec5.png]]
|
||||||
|
![[828b38e1763369b52b2efba15d4668326e86f70408af7ebaf2d99b9f1210a5ed.png]]
|
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Loading…
Reference in New Issue