Skip to content

Commit

Permalink
feat: replace all local links after local image upload
Browse files Browse the repository at this point in the history
  • Loading branch information
gavvvr committed Jun 29, 2024
1 parent ffd7855 commit 2e985ba
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 0 deletions.
130 changes: 130 additions & 0 deletions src/ImgurPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Menu,
Notice,
Plugin,
ReferenceCache,
TFile,
parseLinktext,
} from 'obsidian'
Expand All @@ -26,6 +27,8 @@ import { fixImageTypeIfNeeded } from './utils/misc'
import { createImgurCanvasPasteHandler } from './Canvas'
import { IMGUR_POTENTIALLY_SUPPORTED_FILES_EXTENSIONS } from './imgur/constants'
import { localEmbeddedImageExpectedBoundaries } from './utils/editor'
import UpdateLinksConfirmationModal from './ui/UpdateLinksConfirmationModal'
import InfoModal from './ui/InfoModal'

declare module 'obsidian' {
interface MarkdownSubView {
Expand Down Expand Up @@ -219,11 +222,106 @@ export default class ImgurPlugin extends Plugin {
file,
localImageExpectedStart,
localImageExpectedEnd,
).then((imageUrl) =>
this.proposeToReplaceOtherLocalLinksIfAny(file, imageUrl, {
path: view.file.path,
startPosition: localImageExpectedStart,
}),
),
)
})
}

private proposeToReplaceOtherLocalLinksIfAny(
originalLocalFile: TFile,
remoteImageUrl: string,
originalReference: { path: string; startPosition: EditorPosition },
) {
const otherReferencesByNote = this.getAllCachedReferencesForFile(originalLocalFile)
removeReferenceToOriginalNoteIfPresent(otherReferencesByNote, originalReference)

const notesWithSameLocalFile = Object.keys(otherReferencesByNote)
if (notesWithSameLocalFile.length === 0) return

this.showLinksUpdateDialog(originalLocalFile, remoteImageUrl, otherReferencesByNote)
}

private getAllCachedReferencesForFile(file: TFile) {
const allLinks = this.app.metadataCache.resolvedLinks

const notesWithLinks = []
for (const [notePath, noteLinks] of Object.entries(allLinks)) {
for (const [linkName] of Object.entries(noteLinks)) {
if (linkName === file.name) notesWithLinks.push(notePath)
}
}

const linksByNote = notesWithLinks.reduce(
(acc, note) => {
const noteMetadata = this.app.metadataCache.getCache(note)
const noteLinks = noteMetadata.embeds
if (noteLinks) {
acc[note] = noteLinks.filter((l) => l.link === file.name)
}
return acc
},
{} as Record<string, ReferenceCache[]>,
)
return linksByNote
}

private showLinksUpdateDialog(
localFile: TFile,
remoteImageUrl: string,
otherReferencesByNote: Record<string, ReferenceCache[]>,
) {
const stats = getFilesAndLinksStats(otherReferencesByNote)
const dialogBox = new UpdateLinksConfirmationModal(this.app, localFile.path, stats)
dialogBox.onDoNotUpdateClick(() => dialogBox.close())
dialogBox.onDoUpdateClick(() => {
dialogBox.disableButtons()
dialogBox.setContent('Working...')
this.replaceAllLocalReferencesWithRemoteOne(otherReferencesByNote, remoteImageUrl)
.catch((e) => {
new InfoModal(
this.app,
'Error',
'Unexpected error occurred, check Developer Tools console for details',
).open()
console.error('Something bad happened during links update', e)
})
.finally(() => dialogBox.close())
new Notice(`Updated ${stats.linksCount} links in ${stats.filesCount} files`)
})
dialogBox.open()
}

private async replaceAllLocalReferencesWithRemoteOne(
referencesByNotes: Record<string, ReferenceCache[]>,
remoteImageUrl: string,
) {
for (const [notePath, refs] of Object.entries(referencesByNotes)) {
const noteFile = this.app.vault.getFileByPath(notePath)
const refsStartOffsetsSortedDescending = refs
.map((ref) => ({
start: ref.position.start.offset,
end: ref.position.end.offset,
}))
.sort((ref1, ref2) => ref2.start - ref1.start)

await this.app.vault.process(noteFile, (noteContent) => {
let updatedContent = noteContent
refsStartOffsetsSortedDescending.forEach((refPos) => {
updatedContent =
updatedContent.substring(0, refPos.start) +
`![](${remoteImageUrl})` +
updatedContent.substring(refPos.end)
})
return updatedContent
})
}
}

private async uploadLocalImageFromEditor(
editor: Editor,
file: TFile,
Expand Down Expand Up @@ -343,6 +441,7 @@ export default class ImgurPlugin extends Plugin {
throw e
}
this.embedMarkDownImage(pasteId, imgUrl)
return imgUrl
}

private insertTemporaryText(pasteId: string, atPos?: EditorPosition) {
Expand Down Expand Up @@ -390,3 +489,34 @@ export default class ImgurPlugin extends Plugin {
}
}
}

function removeReferenceToOriginalNoteIfPresent(
otherReferencesByNote: Record<string, ReferenceCache[]>,
originalNote: { path: string; startPosition: EditorPosition },
) {
if (!Object.keys(otherReferencesByNote).includes(originalNote.path)) return

const refsFromOriginalNote = otherReferencesByNote[originalNote.path]
const originalRefStart = originalNote.startPosition
const refForExclusion = refsFromOriginalNote.find(
(r) =>
r.position.start.line === originalRefStart.line &&
r.position.start.col === originalRefStart.ch,
)
if (refForExclusion) {
refsFromOriginalNote.remove(refForExclusion)
if (refsFromOriginalNote.length === 0) {
delete otherReferencesByNote[originalNote.path]
}
}
}

function getFilesAndLinksStats(otherReferencesByNote: Record<string, ReferenceCache[]>) {
return {
filesCount: Object.keys(otherReferencesByNote).length,
linksCount: Object.values(otherReferencesByNote).reduce(
(count, refs) => count + refs.length,
0,
),
}
}
13 changes: 13 additions & 0 deletions src/ui/InfoModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { App, ButtonComponent, Modal } from 'obsidian'

export default class InfoModal extends Modal {
constructor(app: App, title: string, message: string) {
super(app)

this.setTitle(title)
this.contentEl.createEl('p', { text: message })

const buttonsDiv = this.modalEl.createDiv('modal-button-container')
new ButtonComponent(buttonsDiv).setButtonText('Ok')
}
}
36 changes: 36 additions & 0 deletions src/ui/UpdateLinksConfirmationModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { App, ButtonComponent, Modal } from 'obsidian'

export default class UpdateLinksConfirmationModal extends Modal {
private updateOnceButton: ButtonComponent
private doNotUpdateButton: ButtonComponent

constructor(app: App, localFileName: string, stats: { filesCount: number; linksCount: number }) {
super(app)

this.setTitle('Replace links in vault')

this.contentEl.createEl('p', {
text: `Do you want to replace internal links that link to original local file (${localFileName}) with remote link?`,
})
this.contentEl.createEl('p', {
text: `This will affect ${stats.linksCount} links in ${stats.filesCount} files.`,
})

const buttonsDiv = this.modalEl.createDiv('modal-button-container')
this.updateOnceButton = new ButtonComponent(buttonsDiv).setButtonText('Yes')
this.doNotUpdateButton = new ButtonComponent(buttonsDiv).setButtonText('Do not update')
}

onDoUpdateClick(callback: (evt: MouseEvent) => any) {
this.updateOnceButton.onClick(callback)
}

onDoNotUpdateClick(callback: (evt: MouseEvent) => any) {
this.doNotUpdateButton.onClick(callback)
}

disableButtons() {
this.updateOnceButton.setDisabled(true)
this.doNotUpdateButton.setDisabled(true)
}
}

0 comments on commit 2e985ba

Please sign in to comment.