Merge branch 'main' of github.com:remanso-space/remanso
This commit is contained in:
@@ -1 +1 @@
|
|||||||
22.12.0
|
24
|
||||||
@@ -12,7 +12,7 @@ export const commitTheme = (mode: string, newTheme: string) => {
|
|||||||
|
|
||||||
console.log(`Commit créé avec succès: "${commitMessage}"`)
|
console.log(`Commit créé avec succès: "${commitMessage}"`)
|
||||||
|
|
||||||
execSync(`git push"`, { stdio: "inherit" })
|
execSync(`git push`, { stdio: "inherit" })
|
||||||
|
|
||||||
console.log(`Push sur origin`)
|
console.log(`Push sur origin`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="retro">
|
<html lang="en" data-theme="garden">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -15,6 +15,8 @@
|
|||||||
"generate-pwa-assets": "pwa-assets-generator"
|
"generate-pwa-assets": "pwa-assets-generator"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-fetch/fetch": "^1.1.21",
|
||||||
|
"@better-fetch/logger": "^1.1.21",
|
||||||
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||||
"@octokit/core": "^7.0.6",
|
"@octokit/core": "^7.0.6",
|
||||||
"@octokit/rest": "^22.0.1",
|
"@octokit/rest": "^22.0.1",
|
||||||
@@ -22,8 +24,10 @@
|
|||||||
"@tanstack/vue-query": "^5.92.9",
|
"@tanstack/vue-query": "^5.92.9",
|
||||||
"@toycode/markdown-it-class": "^1.2.4",
|
"@toycode/markdown-it-class": "^1.2.4",
|
||||||
"@vscode/markdown-it-katex": "^1.1.2",
|
"@vscode/markdown-it-katex": "^1.1.2",
|
||||||
|
"@vueuse/components": "^14.2.1",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.6.0",
|
||||||
"@vueuse/router": "^13.6.0",
|
"@vueuse/router": "^13.6.0",
|
||||||
|
"arktype": "^2.1.29",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"font-color-contrast": "^11.1.0",
|
"font-color-contrast": "^11.1.0",
|
||||||
@@ -63,21 +67,22 @@
|
|||||||
"@typescript-eslint/parser": "^8.46.2",
|
"@typescript-eslint/parser": "^8.46.2",
|
||||||
"@vite-pwa/assets-generator": "^1.0.2",
|
"@vite-pwa/assets-generator": "^1.0.2",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/compiler-sfc": "^3.5.22",
|
"@vue/compiler-sfc": "^3.5.28",
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^14.6.0",
|
"@vue/eslint-config-typescript": "^14.6.0",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.24",
|
||||||
"daisyui": "^5.3.11",
|
"daisyui": "^5.5.18",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier-vue": "^5.0.0",
|
"eslint-plugin-prettier-vue": "^5.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"eslint-plugin-vue": "^9.31.0",
|
"eslint-plugin-vue": "^10.8.0",
|
||||||
"esno": "^4.8.0",
|
"esno": "^4.8.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-vue": "^1.1.2",
|
||||||
"sass": "^1.93.3",
|
"sass": "^1.93.3",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
|
|||||||
3563
pnpm-lock.yaml
generated
3563
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import { createEventBus } from "retrobus"
|
import { createEventBus } from "retrobus"
|
||||||
|
|
||||||
interface EventBusParams {
|
interface EventBusParams {
|
||||||
path: string
|
atUri: string
|
||||||
currentNoteRkey?: string
|
currentNoteRkey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from "vue-router"
|
import { useRouter, type RouteLocationRaw } from "vue-router"
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{ fallback?: RouteLocationRaw; preferFallback?: boolean }>(),
|
||||||
|
{ preferFallback: true },
|
||||||
|
)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const goBack = () => router.back()
|
const goBack = () => {
|
||||||
|
if (props.preferFallback && props.fallback) {
|
||||||
|
router.push(props.fallback)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.history.state?.back) {
|
||||||
|
router.back()
|
||||||
|
} else if (props.fallback) {
|
||||||
|
router.push(props.fallback)
|
||||||
|
} else {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import StackedNote from "@/components/StackedNote.vue"
|
|||||||
import { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
import { useNoteView } from "@/hooks/useNoteView.hook"
|
import { useNoteView } from "@/hooks/useNoteView.hook"
|
||||||
|
import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
|
||||||
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
import { useVisitRepo } from "@/modules/history/hooks/useVisitRepo.hook"
|
import { useVisitRepo } from "@/modules/history/hooks/useVisitRepo.hook"
|
||||||
import CacheAllNotes from "@/modules/note/components/CacheAllNote.vue"
|
import CacheAllNotes from "@/modules/note/components/CacheAllNote.vue"
|
||||||
@@ -52,7 +53,8 @@ const { toHTML } = markdownBuilder(repo)
|
|||||||
const { listenToClick } = useLinks("note-display")
|
const { listenToClick } = useLinks("note-display")
|
||||||
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||||
|
|
||||||
const { titles } = useNoteView("note-container")
|
const { titles } = useNoteView()
|
||||||
|
useResizeContainer("note-container", stackedNotes)
|
||||||
|
|
||||||
const renderedContent = computed(() =>
|
const renderedContent = computed(() =>
|
||||||
props.content !== null
|
props.content !== null
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { useUserRepoStore } from "../modules/repo/store/userRepo.store"
|
|
||||||
import ThemeSwap from "@/components/ThemeSwap.vue"
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||||
|
|
||||||
|
import { useUserRepoStore } from "../modules/repo/store/userRepo.store"
|
||||||
|
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
|
|
||||||
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
|
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
|
||||||
|
|||||||
192
src/components/StackedPublicNote.vue
Normal file
192
src/components/StackedPublicNote.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, watch } from "vue"
|
||||||
|
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
|
||||||
|
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
|
||||||
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
|
import { computedAsync } from "@vueuse/core"
|
||||||
|
import { getUrl } from "@/modules/atproto/getUrl"
|
||||||
|
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
|
||||||
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||||
|
import { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
||||||
|
import { parseAtUri } from "@/modules/atproto/parseAtUri"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
atUri: string
|
||||||
|
index: number
|
||||||
|
title?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const atUri = computed(() => props.atUri)
|
||||||
|
const atUriProps = computed(() => parseAtUri(atUri.value))
|
||||||
|
const did = computed(() => atUriProps.value.did)
|
||||||
|
const rkey = computed(() => atUriProps.value.rkey)
|
||||||
|
|
||||||
|
const index = computed(() => props.index)
|
||||||
|
|
||||||
|
const author = computedAsync(async () => getAuthor(did.value))
|
||||||
|
const url = computedAsync(async () =>
|
||||||
|
getUrl({ did: did.value, rkey: rkey.value }),
|
||||||
|
)
|
||||||
|
|
||||||
|
const className = computed(() => `stacked-note-${props.index}`)
|
||||||
|
const titleClassName = computed(() => `title-${className.value}`)
|
||||||
|
|
||||||
|
const { scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||||
|
const { listenToClick } = useATProtoLinks(className.value, atUri)
|
||||||
|
const { displayNoteOverlay } = useNoteOverlay(className.value, index)
|
||||||
|
|
||||||
|
const noteRecord = computedAsync(async () =>
|
||||||
|
url.value
|
||||||
|
? ((await fetch(url.value).then()).json() as Promise<PublicNoteRecord>)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
const { toHTML } = markdownBuilder()
|
||||||
|
const title = computed(() => noteRecord.value?.value.title)
|
||||||
|
const content = computed(() =>
|
||||||
|
noteRecord.value?.value.content && author.value
|
||||||
|
? toHTML(
|
||||||
|
withATProtoImages(noteRecord.value.value.content, {
|
||||||
|
pds: author.value.pds,
|
||||||
|
did: did.value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: "",
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
content,
|
||||||
|
async () => {
|
||||||
|
await nextTick()
|
||||||
|
listenToClick()
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="stacked-note"
|
||||||
|
:class="{
|
||||||
|
[className]: true,
|
||||||
|
overlay: displayNoteOverlay,
|
||||||
|
[`note-${rkey}`]: true,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="title-stacked-note-link"
|
||||||
|
@click.prevent="scrollToFocusedNote(rkey)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="title-stacked-note breadcrumbs text-sm"
|
||||||
|
:class="titleClassName"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<section class="text-content">
|
||||||
|
<div class="note-content" v-html="content"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$border-color: rgba(18, 19, 58, 0.2);
|
||||||
|
|
||||||
|
.stacked-note {
|
||||||
|
padding: 0 1.5rem 1rem;
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
color: var(--color-base-content);
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&.overlay {
|
||||||
|
box-shadow: -3px 0 0.4em $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 0 0.5rem 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.offline-ready {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-stacked-note {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
color: var(--color-base-content);
|
||||||
|
font-size: 0.8em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
flex: 1;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
div {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
float: right;
|
||||||
|
margin: 0.2rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.stacked-note {
|
||||||
|
padding: 0 0.75rem 1rem;
|
||||||
|
|
||||||
|
section {
|
||||||
|
padding: 1rem 0 2rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-content {
|
||||||
|
padding: 0;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
.stacked-note {
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-stacked-note {
|
||||||
|
padding: 0 1rem;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.stacked-note {
|
||||||
|
break-after: always;
|
||||||
|
|
||||||
|
&.overlay {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import RepoList from "@/components/RepoList.vue"
|
||||||
|
import SignInGithub from "@/components/SignInGithub.vue"
|
||||||
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||||
|
import { useForm } from "@/hooks/useForm.hook"
|
||||||
|
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||||
|
import LastVisited from "@/modules/history/components/LastVisited.vue"
|
||||||
|
|
||||||
|
const { isLogged } = useGitHubLogin()
|
||||||
|
const { userInput, repoInput, submit } = useForm()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="welcome-world">
|
<div class="welcome-world">
|
||||||
<h1 class="title is-1">
|
<h1 class="title is-1">
|
||||||
@@ -70,18 +82,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import RepoList from "@/components/RepoList.vue"
|
|
||||||
import SignInGithub from "@/components/SignInGithub.vue"
|
|
||||||
import ThemeSwap from "@/components/ThemeSwap.vue"
|
|
||||||
import { useForm } from "@/hooks/useForm.hook"
|
|
||||||
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
|
||||||
import LastVisited from "@/modules/history/components/LastVisited.vue"
|
|
||||||
|
|
||||||
const { isLogged } = useGitHubLogin()
|
|
||||||
const { userInput, repoInput, submit } = useForm()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
h1 {
|
h1 {
|
||||||
img {
|
img {
|
||||||
|
|||||||
5
src/constants/bookmark-width.ts
Normal file
5
src/constants/bookmark-width.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const BOOKMARK_WIDTH_REM = 2
|
||||||
|
|
||||||
|
export const getBookmarkWidthPx = () =>
|
||||||
|
BOOKMARK_WIDTH_REM *
|
||||||
|
parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||||
@@ -1 +1,12 @@
|
|||||||
export const NOTE_WIDTH = 620
|
let cached: number | undefined
|
||||||
|
|
||||||
|
export const getNoteWidth = () => {
|
||||||
|
if (cached === undefined) {
|
||||||
|
cached = parseInt(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
|
"--note-width",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
import { ComputedRef, onUnmounted, Ref, toValue } from "vue"
|
import { ComputedRef, onUnmounted, Ref, toValue } from "vue"
|
||||||
|
|
||||||
import { isExternalLink } from "@/utils/link"
|
import { isExternalLink } from "@/utils/link"
|
||||||
import { publicNoteEventBus } from "@/bus/publicNoteEventBus"
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
|
import { parseAtUri } from "@/modules/atproto/parseAtUri"
|
||||||
|
|
||||||
export const useATProtoLinks = (
|
export const useATProtoLinks = (
|
||||||
className: ComputedRef<string> | string,
|
className: ComputedRef<string> | string,
|
||||||
rkey?: Ref<string> | string,
|
currentAtUri?: Ref<string> | string,
|
||||||
) => {
|
) => {
|
||||||
const linkNote: EventListener = (event) => {
|
const { addStackedNote } = useRouteQueryStackedNotes()
|
||||||
|
const linkNote = (event: Event) => {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
const href = target.getAttribute("href")
|
const atUri = target.getAttribute("href")
|
||||||
|
|
||||||
if (!href) {
|
if (!atUri) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href.startsWith("#")) {
|
if (atUri.startsWith("#")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
if (isExternalLink(href)) {
|
if (isExternalLink(atUri)) {
|
||||||
window.open(href, "_blank")
|
window.open(atUri, "_blank")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const { rkey } = parseAtUri(atUri)
|
||||||
|
|
||||||
publicNoteEventBus.emit({
|
addStackedNote(toValue(currentAtUri) ?? "", atUri, rkey)
|
||||||
path: href,
|
|
||||||
currentNoteRkey: toValue(rkey),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LINK_SELECTOR = `.${toValue(className)} a`
|
const LINK_SELECTOR = `.${toValue(className)} a`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import markdownItLatex from "@vscode/markdown-it-katex"
|
import markdownItKatex from "@vscode/markdown-it-katex"
|
||||||
import MarkdownIt, { Options, Renderer, Token } from "markdown-it"
|
import MarkdownIt, { Options } from "markdown-it"
|
||||||
import blockEmbedPlugin from "markdown-it-block-embed"
|
import blockEmbedPlugin from "markdown-it-block-embed"
|
||||||
import markdownItCheckbox from "markdown-it-checkbox"
|
import markdownItCheckbox from "markdown-it-checkbox"
|
||||||
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
|
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
|
||||||
@@ -9,8 +9,9 @@ import { Ref, toValue } from "vue"
|
|||||||
|
|
||||||
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
||||||
import { html5Media } from "@/utils/markdown/markdown-html5-media"
|
import { html5Media } from "@/utils/markdown/markdown-html5-media"
|
||||||
import { twitterPlugin } from "@/utils/markdown/markdown-it-twitter"
|
|
||||||
import mermaid from "mermaid"
|
import mermaid from "mermaid"
|
||||||
|
import type Token from "markdown-it/lib/token.mjs"
|
||||||
|
import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs"
|
||||||
|
|
||||||
const markdownItMermaidExtractor = (md: MarkdownIt) => {
|
const markdownItMermaidExtractor = (md: MarkdownIt) => {
|
||||||
const defaultFence =
|
const defaultFence =
|
||||||
@@ -55,9 +56,8 @@ const md = new MarkdownIt({
|
|||||||
height: 300,
|
height: 300,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.use(twitterPlugin)
|
|
||||||
.use(markdownItCheckbox)
|
.use(markdownItCheckbox)
|
||||||
.use(markdownItLatex)
|
.use(markdownItKatex)
|
||||||
.use(markdownItIframe, {
|
.use(markdownItIframe, {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
})
|
})
|
||||||
@@ -108,7 +108,7 @@ export const runMermaid = (querySelector: string) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const rules: Renderer.RenderRuleRecord = {
|
const rules: RenderRuleRecord = {
|
||||||
table_open: () =>
|
table_open: () =>
|
||||||
'<div class="overflow-x-auto"><table class="table table-zebra">',
|
'<div class="overflow-x-auto"><table class="table table-zebra">',
|
||||||
table_close: () => "</table></div>",
|
table_close: () => "</table></div>",
|
||||||
@@ -116,17 +116,24 @@ const rules: Renderer.RenderRuleRecord = {
|
|||||||
|
|
||||||
md.renderer.rules = { ...md.renderer.rules, ...rules }
|
md.renderer.rules = { ...md.renderer.rules, ...rules }
|
||||||
|
|
||||||
|
const stripFrontmatter = (content: string): string => {
|
||||||
|
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/)
|
||||||
|
return match ? content.slice(match[0].length) : content
|
||||||
|
}
|
||||||
|
|
||||||
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
||||||
const getRawContent = (content: string) => decodeBase64ToUTF8(content)
|
const getRawContent = (content: string) => decodeBase64ToUTF8(content)
|
||||||
const renderFromUTF8 = (content: string, prefix?: string) =>
|
const renderFromUTF8 = (content: string, prefix?: string) => {
|
||||||
content
|
return content
|
||||||
? md.render(content, {
|
? md.render(stripFrontmatter(content), {
|
||||||
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? ""),
|
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? ""),
|
||||||
})
|
})
|
||||||
: ""
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toHTML: (content: string) => (content ? md.render(content) : ""),
|
toHTML: (content: string) =>
|
||||||
|
content ? md.render(stripFrontmatter(content)) : "",
|
||||||
render: (content: string, prefix?: string) =>
|
render: (content: string, prefix?: string) =>
|
||||||
renderFromUTF8(decodeBase64ToUTF8(content), prefix),
|
renderFromUTF8(decodeBase64ToUTF8(content), prefix),
|
||||||
renderFromUTF8,
|
renderFromUTF8,
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { computed, onMounted, Ref, ref, toValue } from "vue"
|
import { computed, onMounted, Ref, ref, toValue } from "vue"
|
||||||
|
|
||||||
import { NOTE_WIDTH } from "@/constants/note-width"
|
import { BOOKMARK_WIDTH_REM, getBookmarkWidthPx } from "@/constants/bookmark-width"
|
||||||
|
import { getNoteWidth } from "@/constants/note-width"
|
||||||
import { useOverlay } from "@/hooks/useOverlay.hook"
|
import { useOverlay } from "@/hooks/useOverlay.hook"
|
||||||
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
|
|
||||||
const BOOKMARK_WIDTH = 2
|
|
||||||
const OFFSET = 32 // stacked-note padding
|
|
||||||
|
|
||||||
export const useNoteOverlay = (
|
export const useNoteOverlay = (
|
||||||
className: string,
|
className: string,
|
||||||
index: Ref<number> | number,
|
index: Ref<number> | number,
|
||||||
@@ -20,7 +18,7 @@ export const useNoteOverlay = (
|
|||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
return y.value > valueIndex * noteHeight.value
|
return y.value > valueIndex * noteHeight.value
|
||||||
} else {
|
} else {
|
||||||
return x.value > valueIndex * NOTE_WIDTH - valueIndex * OFFSET
|
return x.value > valueIndex * getNoteWidth() - valueIndex * getBookmarkWidthPx()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -28,25 +26,26 @@ export const useNoteOverlay = (
|
|||||||
const { stackedNotes } = useRouteQueryStackedNotes()
|
const { stackedNotes } = useRouteQueryStackedNotes()
|
||||||
const noteElement = document.querySelector(
|
const noteElement = document.querySelector(
|
||||||
`.${className}`,
|
`.${className}`,
|
||||||
) as HTMLElement | null
|
) satisfies HTMLElement | null
|
||||||
|
|
||||||
if (!noteElement) {
|
if (!noteElement) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
noteHeight.value = noteElement.clientHeight
|
noteHeight.value = noteElement.clientHeight
|
||||||
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
noteElement.style.top = `0`
|
noteElement.style.top = `0`
|
||||||
} else {
|
} else {
|
||||||
noteElement.style.left = `${(toValue(index) + 1) * BOOKMARK_WIDTH}rem`
|
noteElement.style.left = `${(toValue(index) + 1) * BOOKMARK_WIDTH_REM}rem`
|
||||||
|
|
||||||
const stackedNoteContainers = document.querySelectorAll(
|
const stackedNoteContainers = document.querySelectorAll(
|
||||||
".stacked-note",
|
".stacked-note",
|
||||||
) as NodeListOf<HTMLElement>
|
) satisfies NodeListOf<HTMLElement>
|
||||||
|
|
||||||
stackedNoteContainers.forEach((stackedNote, ind) => {
|
stackedNoteContainers.forEach((stackedNote, ind) => {
|
||||||
stackedNote.style.right = `calc(-${NOTE_WIDTH}px + ${
|
stackedNote.style.right = `calc(-${getNoteWidth()}px + ${
|
||||||
(stackedNotes.value.length - ind) * BOOKMARK_WIDTH
|
(stackedNotes.value.length - ind) * BOOKMARK_WIDTH_REM
|
||||||
}rem)`
|
}rem)`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { computed, onMounted, onUnmounted, watch } from "vue"
|
import { computed, onUnmounted } from "vue"
|
||||||
|
|
||||||
import { noteEventBus } from "@/bus/noteEventBus"
|
import { noteEventBus } from "@/bus/noteEventBus"
|
||||||
import { NOTE_WIDTH } from "@/constants/note-width"
|
|
||||||
import { useOverlay } from "@/hooks/useOverlay.hook"
|
|
||||||
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
import { resolvePath } from "@/modules/repo/services/resolvePath"
|
import { resolvePath } from "@/modules/repo/services/resolvePath"
|
||||||
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
import { pathToNotePathTitle } from "@/utils/noteTitle"
|
import { pathToNotePathTitle } from "@/utils/noteTitle"
|
||||||
import { errorMessage } from "@/utils/notif"
|
import { errorMessage } from "@/utils/notif"
|
||||||
|
|
||||||
export const useNoteView = (containerClass: string) => {
|
export const useNoteView = () => {
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
const { isMobile } = useOverlay(false)
|
|
||||||
const { stackedNotes, addStackedNote } = useRouteQueryStackedNotes()
|
const { stackedNotes, addStackedNote } = useRouteQueryStackedNotes()
|
||||||
|
|
||||||
const titles = computed(() =>
|
const titles = computed(() =>
|
||||||
@@ -45,36 +42,10 @@ export const useNoteView = (containerClass: string) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const resizeContainer = () => {
|
|
||||||
const container = document.querySelector(
|
|
||||||
`.${containerClass}`,
|
|
||||||
) as HTMLElement | null
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobile.value) {
|
|
||||||
container.style.height = `${(stackedNotes.value.length + 1) * 100}vh`
|
|
||||||
} else {
|
|
||||||
container.style.width = `${
|
|
||||||
NOTE_WIDTH * (stackedNotes.value.length + 1)
|
|
||||||
}px`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
resizeContainer()
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unsubscribeLink()
|
unsubscribeLink()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(stackedNotes, resizeContainer, {
|
|
||||||
immediate: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
titles,
|
titles,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,41 @@
|
|||||||
import { useEventListener, useWindowSize } from '@vueuse/core'
|
import { useEventListener, useWindowSize } from "@vueuse/core"
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from "vue"
|
||||||
|
|
||||||
import { MOBILE_BREAKPOINT } from '@/constants/mobile'
|
import { MOBILE_BREAKPOINT } from "@/constants/mobile"
|
||||||
|
|
||||||
export const useOverlay = (listen = true) => {
|
export const useOverlay = (listen = true) => {
|
||||||
const body = document.body
|
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
const isMobile = computed(() => width.value <= MOBILE_BREAKPOINT)
|
const isMobile = computed(() => width.value <= MOBILE_BREAKPOINT)
|
||||||
|
|
||||||
if (listen) {
|
if (listen) {
|
||||||
useEventListener(
|
// In Firefox/Chrome, body is the horizontal scroll container (body has
|
||||||
body,
|
// computed overflow-x: auto from overflow-y: hidden). In Safari, the
|
||||||
'scroll',
|
// viewport (documentElement) is used instead. Listen on both.
|
||||||
(event) => {
|
const updateScroll = () => {
|
||||||
const target = event.target as HTMLElement
|
x.value = document.body.scrollLeft || window.scrollX
|
||||||
x.value = target.scrollLeft
|
y.value = document.body.scrollTop || window.scrollY
|
||||||
y.value = target.scrollTop
|
}
|
||||||
},
|
useEventListener(window, "scroll", updateScroll, {
|
||||||
{
|
passive: true,
|
||||||
passive: true,
|
capture: false,
|
||||||
capture: false
|
})
|
||||||
}
|
useEventListener(document.body, "scroll", updateScroll, {
|
||||||
)
|
passive: true,
|
||||||
|
capture: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToNote = (to: number) => {
|
const scrollToNote = (to: number) => {
|
||||||
const go = () => {
|
const go = () => {
|
||||||
const scrollOptions = isMobile.value
|
if (isMobile.value) {
|
||||||
? {
|
document.body.scrollTop = to
|
||||||
top: to
|
document.documentElement.scrollTop = to
|
||||||
}
|
} else {
|
||||||
: {
|
document.body.scrollLeft = to
|
||||||
left: to
|
document.documentElement.scrollLeft = to
|
||||||
}
|
}
|
||||||
|
|
||||||
body.scroll(scrollOptions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -48,6 +47,6 @@ export const useOverlay = (listen = true) => {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
isMobile,
|
isMobile,
|
||||||
scrollToNote
|
scrollToNote,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/hooks/usePublicNoteList.hook.ts
Normal file
50
src/hooks/usePublicNoteList.hook.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Author, getAuthors } from "@/modules/atproto/getAuthor"
|
||||||
|
import { PublicNoteListItem } from "@/modules/note/models/Note"
|
||||||
|
import { computedAsync } from "@vueuse/core"
|
||||||
|
import { computed, ref, Ref } from "vue"
|
||||||
|
|
||||||
|
export function usePublicNoteList(did?: Ref<string | undefined>) {
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const notes = ref<PublicNoteListItem[]>([])
|
||||||
|
const cursor = ref<string | null | undefined>(null)
|
||||||
|
const canLoadMore = computed(() => cursor.value !== undefined)
|
||||||
|
|
||||||
|
const onLoadMore = async () => {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
const path = did?.value ? `/${did.value}/notes` : "/notes"
|
||||||
|
const noteAPI = new URL(path, "https://api.litenote.li212.fr")
|
||||||
|
|
||||||
|
if (cursor.value) {
|
||||||
|
noteAPI.searchParams.set("cursor", cursor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(noteAPI)
|
||||||
|
const data: { notes: PublicNoteListItem[]; cursor: string | undefined } =
|
||||||
|
await response.json()
|
||||||
|
|
||||||
|
notes.value.push(...data.notes)
|
||||||
|
cursor.value = data.cursor
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const authors = computedAsync<Map<string, Author>>(async () => {
|
||||||
|
if (notes.value.length === 0) {
|
||||||
|
return new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthors(new Set(notes.value.map((n) => n.did)))
|
||||||
|
}, new Map())
|
||||||
|
|
||||||
|
const getAuthor = (did: string) =>
|
||||||
|
authors.value.has(did) ? authors.value.get(did)?.handle : ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
notes,
|
||||||
|
isLoading,
|
||||||
|
canLoadMore,
|
||||||
|
onLoadMore,
|
||||||
|
authors,
|
||||||
|
getAuthor,
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/hooks/useResizeContainer.hook.ts
Normal file
37
src/hooks/useResizeContainer.hook.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { onMounted, watch, type Ref } from "vue"
|
||||||
|
|
||||||
|
import { getNoteWidth } from "@/constants/note-width"
|
||||||
|
import { useOverlay } from "@/hooks/useOverlay.hook"
|
||||||
|
|
||||||
|
export const useResizeContainer = (
|
||||||
|
containerClass: string,
|
||||||
|
stackedNotes: Readonly<Ref<readonly string[]>>,
|
||||||
|
) => {
|
||||||
|
const { isMobile } = useOverlay(false)
|
||||||
|
|
||||||
|
const resizeContainer = () => {
|
||||||
|
const container = document.querySelector(
|
||||||
|
`.${containerClass}`,
|
||||||
|
) as HTMLElement | null
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMobile.value) {
|
||||||
|
container.style.height = `${(stackedNotes.value.length + 1) * 100}vh`
|
||||||
|
} else {
|
||||||
|
container.style.width = `${
|
||||||
|
getNoteWidth() * (stackedNotes.value.length + 1)
|
||||||
|
}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
resizeContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(stackedNotes, resizeContainer, {
|
||||||
|
immediate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ import { useWindowSize } from "@vueuse/core"
|
|||||||
import { useRouteQuery } from "@vueuse/router"
|
import { useRouteQuery } from "@vueuse/router"
|
||||||
import { nextTick, readonly } from "vue"
|
import { nextTick, readonly } from "vue"
|
||||||
|
|
||||||
import { NOTE_WIDTH } from "@/constants/note-width"
|
import { getBookmarkWidthPx } from "@/constants/bookmark-width"
|
||||||
|
import { getNoteWidth } from "@/constants/note-width"
|
||||||
import { useOverlay } from "@/hooks/useOverlay.hook"
|
import { useOverlay } from "@/hooks/useOverlay.hook"
|
||||||
|
|
||||||
export const useRouteQueryStackedNotes = () => {
|
export const useRouteQueryStackedNotes = () => {
|
||||||
@@ -20,24 +21,26 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
const { scrollToNote, isMobile } = useOverlay(false)
|
const { scrollToNote, isMobile } = useOverlay(false)
|
||||||
|
|
||||||
const scrollToFocusedNote = (
|
const scrollToFocusedNote = (
|
||||||
sha: string | null = null,
|
noteId: string | null = null,
|
||||||
notes: string[] = stackedNotes.value,
|
notes: string[] = stackedNotes.value,
|
||||||
) => {
|
) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const index = sha ? notes.findIndex((noteSHA) => noteSHA === sha) : 0
|
const index = noteId ? notes.findIndex((nid) => nid.includes(noteId)) : 0
|
||||||
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
if (sha) {
|
if (noteId) {
|
||||||
const element = document.querySelector(`.note-${sha}`) as HTMLElement
|
const element = document.querySelector(
|
||||||
|
`.note-${noteId}`,
|
||||||
|
) as HTMLElement
|
||||||
|
|
||||||
const top = (index + 1) * (element?.clientHeight ?? height.value)
|
const top = (index + 1) * (element?.clientHeight ?? height.value)
|
||||||
scrollToNote(top)
|
scrollToNote(top)
|
||||||
} else {
|
} else {
|
||||||
scrollToNote(0)
|
scrollToNote(0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (sha) {
|
if (noteId) {
|
||||||
const margin = index * 44
|
const left = (index + 1) * (getNoteWidth() - getBookmarkWidthPx())
|
||||||
const left = (index + 1) * NOTE_WIDTH - margin
|
|
||||||
scrollToNote(left)
|
scrollToNote(left)
|
||||||
} else {
|
} else {
|
||||||
scrollToNote(0)
|
scrollToNote(0)
|
||||||
@@ -46,9 +49,13 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addStackedNote = (currentSha: string, sha: string) => {
|
const addStackedNote = (
|
||||||
|
currentSha: string,
|
||||||
|
sha: string,
|
||||||
|
selector?: string,
|
||||||
|
) => {
|
||||||
if (stackedNotes.value.includes(sha)) {
|
if (stackedNotes.value.includes(sha)) {
|
||||||
scrollToFocusedNote(sha)
|
scrollToFocusedNote(selector ?? sha)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +75,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
stackedNotes.value = newStackedNotes
|
stackedNotes.value = newStackedNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToFocusedNote(sha, stackedNotes.value)
|
scrollToFocusedNote(selector ?? sha, stackedNotes.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import "notyf/notyf.min.css"
|
import "notyf/notyf.min.css"
|
||||||
import "./styles/app.css"
|
import "./styles/app.css"
|
||||||
|
|
||||||
|
import { VueQueryPlugin } from "@tanstack/vue-query"
|
||||||
import { createPinia } from "pinia"
|
import { createPinia } from "pinia"
|
||||||
import { createApp } from "vue"
|
import { createApp } from "vue"
|
||||||
import { createI18n } from "vue-i18n"
|
import { createI18n } from "vue-i18n"
|
||||||
|
|
||||||
import { messages } from "@/locales/message"
|
import { messages } from "@/locales/message"
|
||||||
import { router } from "@/router/router"
|
import { router } from "@/router/router"
|
||||||
import { VueQueryPlugin } from "@tanstack/vue-query"
|
|
||||||
|
|
||||||
import App from "./App.vue"
|
import App from "./App.vue"
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
export type Author = { alias: string; endpoint: string }
|
|
||||||
|
|
||||||
const correspondanceCache = new Map<string, Author>()
|
|
||||||
|
|
||||||
console.log({ correspondanceCache })
|
|
||||||
|
|
||||||
export const getUniqueAka = async (did: string): Promise<Author> => {
|
|
||||||
if (correspondanceCache.has(did)) {
|
|
||||||
return correspondanceCache.get(did) as Author
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`https://plc.directory/${did}`)
|
|
||||||
const {
|
|
||||||
alsoKnownAs: [aka],
|
|
||||||
service: [{ serviceEndpoint }],
|
|
||||||
} = await response.json()
|
|
||||||
|
|
||||||
const alias = aka.replace("at://", "")
|
|
||||||
const author = { alias, endpoint: serviceEndpoint }
|
|
||||||
|
|
||||||
correspondanceCache.set(did, author)
|
|
||||||
|
|
||||||
return author
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAka = async (dids: Set<string>) => {
|
|
||||||
const correspondance = await Promise.all(
|
|
||||||
[...dids].map(async (did) => {
|
|
||||||
if (correspondanceCache.has(did)) {
|
|
||||||
return [did, correspondanceCache.get(did)] as [string, Author]
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`https://plc.directory/${did}`)
|
|
||||||
const {
|
|
||||||
alsoKnownAs: [aka],
|
|
||||||
service: [{ serviceEndpoint }],
|
|
||||||
} = await response.json()
|
|
||||||
|
|
||||||
const alias = aka.replace("at://", "")
|
|
||||||
const author = { alias, endpoint: serviceEndpoint }
|
|
||||||
|
|
||||||
correspondanceCache.set(did, author)
|
|
||||||
|
|
||||||
return [did, author] as [string, Author]
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Map(correspondance)
|
|
||||||
}
|
|
||||||
79
src/modules/atproto/getAuthor.ts
Normal file
79
src/modules/atproto/getAuthor.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { createSchema, createFetch } from "@better-fetch/fetch"
|
||||||
|
import { type } from "arktype"
|
||||||
|
|
||||||
|
export type Author = { handle: string; pds: string }
|
||||||
|
|
||||||
|
const correspondanceCache = new Map<string, Author>()
|
||||||
|
|
||||||
|
const schema = createSchema(
|
||||||
|
{
|
||||||
|
"/xrpc/blue.microcosm.identity.resolveMiniDoc": {
|
||||||
|
output: type({
|
||||||
|
did: "string",
|
||||||
|
handle: "string",
|
||||||
|
pds: "string",
|
||||||
|
signing_key: "string",
|
||||||
|
}),
|
||||||
|
query: type({
|
||||||
|
identifier: "string",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ strict: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const microcosmSlingshot = createFetch({
|
||||||
|
baseURL: "https://slingshot.microcosm.blue",
|
||||||
|
// plugins: [logger()],
|
||||||
|
schema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getAuthor = async (did: string): Promise<Author | null> => {
|
||||||
|
if (correspondanceCache.has(did)) {
|
||||||
|
return correspondanceCache.get(did) as Author
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: author, error } = await microcosmSlingshot(
|
||||||
|
"/xrpc/blue.microcosm.identity.resolveMiniDoc",
|
||||||
|
{ query: { identifier: did } },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!author) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
correspondanceCache.set(did, author)
|
||||||
|
|
||||||
|
return author
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAuthors = async (dids: Set<string>) => {
|
||||||
|
const correspondance = await Promise.all(
|
||||||
|
[...dids].map(async (did) => {
|
||||||
|
if (correspondanceCache.has(did)) {
|
||||||
|
return [did, correspondanceCache.get(did)] as [string, Author | null]
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: author } = await microcosmSlingshot(
|
||||||
|
"/xrpc/blue.microcosm.identity.resolveMiniDoc",
|
||||||
|
{ query: { identifier: did } },
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!author) {
|
||||||
|
return [did, null] as [string, Author | null]
|
||||||
|
}
|
||||||
|
|
||||||
|
correspondanceCache.set(did, author)
|
||||||
|
|
||||||
|
return [did, author] as [string, Author | null]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return new Map(correspondance)
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||||
|
|
||||||
const endpointCache = new Map<string, string>()
|
const endpointCache = new Map<string, string>()
|
||||||
|
|
||||||
const getEndpoint = async (did: string) => {
|
const getEndpoint = async (did: string) => {
|
||||||
@@ -15,10 +17,13 @@ const getEndpoint = async (did: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getUrl = async ({ did, rkey }: { did: string; rkey: string }) => {
|
export const getUrl = async ({ did, rkey }: { did: string; rkey: string }) => {
|
||||||
const url = new URL(
|
const author = await getAuthor(did)
|
||||||
"/xrpc/com.atproto.repo.getRecord",
|
|
||||||
await getEndpoint(did),
|
if (!author) {
|
||||||
)
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL("/xrpc/com.atproto.repo.getRecord", author.pds)
|
||||||
url.searchParams.set("repo", did)
|
url.searchParams.set("repo", did)
|
||||||
url.searchParams.set("collection", "space.remanso.note")
|
url.searchParams.set("collection", "space.remanso.note")
|
||||||
url.searchParams.set("rkey", rkey)
|
url.searchParams.set("rkey", rkey)
|
||||||
|
|||||||
7
src/modules/atproto/parseAtUri.ts
Normal file
7
src/modules/atproto/parseAtUri.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const parseAtUri = (atUri: string): { did: string; rkey: string } => {
|
||||||
|
const match = atUri.match(/^at:\/\/(did:[^/]+)\/[^/]+\/(.+)$/)
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Invalid AT URI: ${atUri}`)
|
||||||
|
}
|
||||||
|
return { did: match[1], rkey: match[2] }
|
||||||
|
}
|
||||||
29
src/modules/atproto/publicNote.types.ts
Normal file
29
src/modules/atproto/publicNote.types.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export interface PublicNoteRecord {
|
||||||
|
uri: string
|
||||||
|
cid: string
|
||||||
|
value: PublicNote
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicNote {
|
||||||
|
$type: string
|
||||||
|
title: string
|
||||||
|
images: PublicNoteImage[]
|
||||||
|
content: string
|
||||||
|
createdAt: string
|
||||||
|
publishedAt: string
|
||||||
|
theme?: string
|
||||||
|
fontFamily?: string
|
||||||
|
fontSize?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicNoteImage {
|
||||||
|
alt: string
|
||||||
|
image: PublicNoteBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicNoteBlob {
|
||||||
|
$type: string
|
||||||
|
ref: { $link: string }
|
||||||
|
mimeType: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
13
src/modules/atproto/withATProtoImages.ts
Normal file
13
src/modules/atproto/withATProtoImages.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const withATProtoImages = (
|
||||||
|
markdown: string,
|
||||||
|
{ pds, did }: { pds: string; did: string },
|
||||||
|
): string => {
|
||||||
|
const imageLinkPattern = /!\[([^\]]*)\]\((bafkrei[a-z0-9]+)\)/g
|
||||||
|
|
||||||
|
return markdown.replace(imageLinkPattern, (_, altText, cid) => {
|
||||||
|
const imageUrl = new URL("/xrpc/com.atproto.sync.getBlob", pds)
|
||||||
|
imageUrl.searchParams.set("did", did)
|
||||||
|
imageUrl.searchParams.set("cid", cid)
|
||||||
|
return `})`
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
|
|
||||||
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
|
|
||||||
export const useNotes = () => {
|
export const useNotes = () => {
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ export const useUserSettings = () => {
|
|||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (store.userSettings === undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
|
||||||
const fontFamily = store.userSettings?.chosenFontFamily
|
const fontFamily = store.userSettings?.chosenFontFamily
|
||||||
|
|||||||
@@ -20,12 +20,18 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import("@/views/PublicNoteListView.vue"),
|
component: () => import("@/views/PublicNoteListView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/notes",
|
path: "/pub",
|
||||||
name: "PublicNoteListView",
|
name: "PublicNoteListView",
|
||||||
component: () => import("@/views/PublicNoteListView.vue"),
|
component: () => import("@/views/PublicNoteListView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/notes/:did/:rkey",
|
path: "/pub/:did",
|
||||||
|
name: "PublicNoteListByDidView",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/views/PublicNoteListByDidView.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/pub/:did/:rkey/:slug?",
|
||||||
name: "PublicNoteView",
|
name: "PublicNoteView",
|
||||||
props: true,
|
props: true,
|
||||||
component: () => import("@/views/PublicNoteView.vue"),
|
component: () => import("@/views/PublicNoteView.vue"),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
--link: #445fb9;
|
--link: #445fb9;
|
||||||
--light-link: lighten(#445fb9, 45%);
|
--light-link: lighten(#445fb9, 45%);
|
||||||
--background-color: #ffffff;
|
--background-color: #ffffff;
|
||||||
--note-width: 480px;
|
--note-width: 500px;
|
||||||
--color-contrast-content: var(--color-success);
|
--color-contrast-content: var(--color-success);
|
||||||
--notyf-margin: 0.5rem;
|
--notyf-margin: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
|
|
||||||
@plugin 'daisyui' {
|
@plugin 'daisyui' {
|
||||||
themes:
|
themes:
|
||||||
retro --default,
|
garden --default,
|
||||||
coffee --prefersdark;
|
night --prefersdark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@config '../../tailwind.config.js';
|
@config '../../tailwind.config.js';
|
||||||
@@ -109,7 +109,7 @@ a.title-stacked-note-link {
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
@media only screen and (max-width: 500px) {
|
||||||
.notyf__toast {
|
.notyf__toast {
|
||||||
margin: var(--notyf-margin);
|
margin: var(--notyf-margin);
|
||||||
width: calc(100% - 2 * var(--notyf-margin));
|
width: calc(100% - 2 * var(--notyf-margin));
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
// Update these values to change the light and dark themes
|
// Update these values to change the light and dark themes
|
||||||
|
|
||||||
export const themeConfig = {
|
export const themeConfig = {
|
||||||
light: 'retro',
|
light: 'garden',
|
||||||
dark: 'coffee',
|
dark: 'night',
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/utils/slugify.ts
Normal file
9
src/utils/slugify.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function slugify(text: string): string {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "")
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import GoBack from '@/components/GoBack.vue'
|
import GoBack from "@/components/GoBack.vue"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
104
src/views/PublicNoteListByDidView.vue
Normal file
104
src/views/PublicNoteListByDidView.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import BackButton from "@/components/BackButton.vue"
|
||||||
|
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
|
||||||
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||||
|
import { slugify } from "@/utils/slugify"
|
||||||
|
import { computedAsync } from "@vueuse/core"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { vInfiniteScroll } from "@vueuse/components"
|
||||||
|
|
||||||
|
const props = defineProps<{ did: string }>()
|
||||||
|
const did = computed(() => props.did)
|
||||||
|
|
||||||
|
const { notes, isLoading, canLoadMore, onLoadMore } = usePublicNoteList(did)
|
||||||
|
|
||||||
|
const author = computedAsync(async () => getAuthor(did.value))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="public-note-list-view">
|
||||||
|
<div class="header">
|
||||||
|
<back-button class="back-button" :fallback="{ name: 'Home' }" />
|
||||||
|
<h1>{{ author?.handle ?? did }}</h1>
|
||||||
|
</div>
|
||||||
|
<div v-if="isLoading"></div>
|
||||||
|
<div v-else>
|
||||||
|
<ul
|
||||||
|
class="list rounded-box shadow-sm"
|
||||||
|
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
|
||||||
|
>
|
||||||
|
<li v-for="note in notes" class="list-row">
|
||||||
|
<div class="list-col">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'PublicNoteView',
|
||||||
|
params: { did: note.did, rkey: note.rkey, slug: slugify(note.title) },
|
||||||
|
}"
|
||||||
|
class="btn btn-link"
|
||||||
|
>{{ note.title }}</router-link
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="text-xs opacity-80 alias">
|
||||||
|
<span v-if="note.publishedAt">
|
||||||
|
{{ new Date(note.publishedAt).toLocaleDateString() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.public-note-list-view {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.list-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias {
|
||||||
|
text-align: right;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,64 +1,64 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import BackButton from "@/components/BackButton.vue"
|
import BackButton from "@/components/BackButton.vue"
|
||||||
import { Author, getAka } from "@/modules/atproto/getAka"
|
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
|
||||||
import { PublicNoteListItem } from "@/modules/note/models/Note"
|
import { slugify } from "@/utils/slugify"
|
||||||
import { computedAsync, useAsyncState } from "@vueuse/core"
|
import { vInfiniteScroll } from "@vueuse/components"
|
||||||
|
|
||||||
const { state, isLoading } = useAsyncState<{
|
const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
|
||||||
notes: PublicNoteListItem[]
|
usePublicNoteList()
|
||||||
}>(
|
|
||||||
async () => {
|
|
||||||
const response = await fetch("https://api.litenote.li212.fr/notes")
|
|
||||||
return response.json()
|
|
||||||
},
|
|
||||||
{ notes: [] },
|
|
||||||
)
|
|
||||||
|
|
||||||
const aka = computedAsync<Map<string, Author>>(async () => {
|
|
||||||
if (state.value.notes.length === 0) {
|
|
||||||
return new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAka(new Set(state.value.notes.map((n) => n.did)))
|
|
||||||
}, new Map())
|
|
||||||
|
|
||||||
const getAlias = (did: string) =>
|
|
||||||
aka.value.has(did) ? aka.value.get(did)?.alias : ""
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="public-note-list-view">
|
<main class="public-note-list-view">
|
||||||
<h1>Remanso notes</h1>
|
<div class="header">
|
||||||
|
<back-button class="back-button" :fallback="{ name: 'Home' }" />
|
||||||
|
<h1>Remanso notes</h1>
|
||||||
|
</div>
|
||||||
<div v-if="isLoading"></div>
|
<div v-if="isLoading"></div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ul class="list rounded-box shadow-sm">
|
<ul
|
||||||
<li v-for="note in state.notes" class="list-row">
|
class="list rounded-box shadow-sm"
|
||||||
|
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
|
||||||
|
>
|
||||||
|
<li v-for="note in notes" class="list-row">
|
||||||
<div class="list-col">
|
<div class="list-col">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'PublicNoteView',
|
name: 'PublicNoteView',
|
||||||
params: { did: note.did, rkey: note.rkey },
|
params: {
|
||||||
|
did: note.did,
|
||||||
|
rkey: note.rkey,
|
||||||
|
slug: slugify(note.title),
|
||||||
|
},
|
||||||
}"
|
}"
|
||||||
class="btn btn-link"
|
class="btn btn-link"
|
||||||
>{{ note.title }}</router-link
|
>{{ note.title }}</router-link
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="text-xs opacity-90 alias">
|
<div class="text-xs opacity-80 alias">
|
||||||
<span v-if="getAlias(note.did)">
|
<router-link
|
||||||
{{ getAlias(note.did) }}
|
v-if="getAuthor(note.did)"
|
||||||
</span>
|
:to="{
|
||||||
<span v-if="note.publishedAt"
|
name: 'PublicNoteListByDidView',
|
||||||
> • {{
|
params: { did: note.did },
|
||||||
|
}"
|
||||||
|
class="link link-hover"
|
||||||
|
>
|
||||||
|
{{ getAuthor(note.did) }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<template v-if="note.publishedAt">
|
||||||
|
<span> • </span>
|
||||||
|
<span>{{
|
||||||
new Date(note.publishedAt).toLocaleDateString()
|
new Date(note.publishedAt).toLocaleDateString()
|
||||||
}}
|
}}</span>
|
||||||
</span>
|
</template>
|
||||||
<div v-else class="skeleton h-4 w-20"></div>
|
<div v-else class="skeleton h-4 w-20"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<BackButton class="back-button" />
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -67,10 +67,20 @@ const getAlias = (did: string) =>
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 1rem;
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 1rem;
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
@@ -98,5 +108,9 @@ const getAlias = (did: string) =>
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,103 +2,83 @@
|
|||||||
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
|
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
|
||||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
import BackButton from "@/components/BackButton.vue"
|
import BackButton from "@/components/BackButton.vue"
|
||||||
|
import StackedPublicNote from "@/components/StackedPublicNote.vue"
|
||||||
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
import { getUniqueAka } from "@/modules/atproto/getAka"
|
import { getAuthor } from "@/modules/atproto/getAuthor"
|
||||||
|
import type { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
|
||||||
|
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
|
||||||
import { getUrl } from "@/modules/atproto/getUrl"
|
import { getUrl } from "@/modules/atproto/getUrl"
|
||||||
import { downloadFont } from "@/utils/downloadFont"
|
import { downloadFont } from "@/utils/downloadFont"
|
||||||
|
import { slugify } from "@/utils/slugify"
|
||||||
import { computedAsync } from "@vueuse/core"
|
import { computedAsync } from "@vueuse/core"
|
||||||
import { computed, nextTick, watch } from "vue"
|
import { computed, nextTick, watch } from "vue"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
import { useResizeContainer } from "@/hooks/useResizeContainer.hook"
|
||||||
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||||
|
|
||||||
export interface Root {
|
const props = defineProps<{ did: string; rkey: string; slug?: string }>()
|
||||||
uri: string
|
const router = useRouter()
|
||||||
cid: string
|
|
||||||
value: Value
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Value {
|
|
||||||
$type: string
|
|
||||||
title: string
|
|
||||||
images: Image[]
|
|
||||||
content: string
|
|
||||||
createdAt: string
|
|
||||||
publishedAt: string
|
|
||||||
theme?: string
|
|
||||||
fontFamily?: string
|
|
||||||
fontSize?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Image {
|
|
||||||
alt: string
|
|
||||||
image: Image2
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Image2 {
|
|
||||||
$type: string
|
|
||||||
ref: Ref
|
|
||||||
mimeType: string
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Ref {
|
|
||||||
$link: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{ did: string; rkey: string }>()
|
|
||||||
const did = computed(() => props.did)
|
const did = computed(() => props.did)
|
||||||
const rkey = computed(() => props.rkey)
|
const rkey = computed(() => props.rkey)
|
||||||
const { scrollToFocusedNote } = useRouteQueryStackedNotes()
|
|
||||||
|
|
||||||
const author = computedAsync(async () => getUniqueAka(did.value))
|
const author = computedAsync(async () => getAuthor(did.value))
|
||||||
const url = computedAsync(async () =>
|
const url = computedAsync(
|
||||||
getUrl({ did: did.value, rkey: rkey.value }),
|
async () => getUrl({ did: did.value, rkey: rkey.value }),
|
||||||
|
null,
|
||||||
)
|
)
|
||||||
|
|
||||||
const article = computedAsync(async () =>
|
const noteRecord = computedAsync(async () =>
|
||||||
url.value ? ((await fetch(url.value).then()).json() as Promise<Root>) : null,
|
url.value
|
||||||
|
? ((await fetch(url.value).then()).json() as Promise<PublicNoteRecord>)
|
||||||
|
: null,
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(article, () => {
|
watch(noteRecord, () => {
|
||||||
if (article.value?.value.fontFamily) {
|
if (
|
||||||
downloadFont(article.value.value.fontFamily)
|
noteRecord.value?.value.title &&
|
||||||
|
props.slug &&
|
||||||
|
props.slug !== slugify(noteRecord.value.value.title)
|
||||||
|
) {
|
||||||
|
router.replace({ name: "SpaceCowboy" })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (article.value?.value.fontSize) {
|
if (noteRecord.value?.value.fontFamily) {
|
||||||
|
downloadFont(noteRecord.value.value.fontFamily)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteRecord.value?.value.fontSize) {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
root.style.setProperty("--font-size", `${article.value.value.fontSize}pt`)
|
root.style.setProperty(
|
||||||
|
"--font-size",
|
||||||
|
`${noteRecord.value.value.fontSize}pt`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { toHTML } = markdownBuilder()
|
const { toHTML } = markdownBuilder()
|
||||||
const withATProtoImages = (markdown: string) => {
|
|
||||||
if (!author.value) {
|
|
||||||
return markdown
|
|
||||||
}
|
|
||||||
|
|
||||||
const endpoint = author.value.endpoint
|
const title = computed(() => noteRecord.value?.value.title)
|
||||||
|
|
||||||
const imageLinkPattern = /!\[([^\]]*)\]\((bafkrei[a-z0-9]+)\)/g
|
|
||||||
|
|
||||||
return markdown.replace(imageLinkPattern, (_, altText, cid) => {
|
|
||||||
const imageUrl = new URL("/xrpc/com.atproto.sync.getBlob", endpoint)
|
|
||||||
imageUrl.searchParams.set("did", did.value)
|
|
||||||
imageUrl.searchParams.set("cid", cid)
|
|
||||||
return `})`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = computed(() => article.value?.value.title)
|
|
||||||
const content = computed(() =>
|
const content = computed(() =>
|
||||||
article.value?.value.content
|
noteRecord.value?.value.content && author.value
|
||||||
? toHTML(withATProtoImages(article.value?.value.content))
|
? toHTML(
|
||||||
|
withATProtoImages(noteRecord.value.value.content, {
|
||||||
|
pds: author.value.pds,
|
||||||
|
did: did.value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
: "",
|
: "",
|
||||||
)
|
)
|
||||||
|
|
||||||
const publishedAt = computed(() =>
|
const publishedAt = computed(() =>
|
||||||
article.value?.value.publishedAt
|
noteRecord.value?.value.publishedAt
|
||||||
? new Date(article.value?.value.publishedAt).toLocaleDateString()
|
? new Date(noteRecord.value?.value.publishedAt).toLocaleDateString()
|
||||||
: null,
|
: null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||||
const { listenToClick } = useATProtoLinks("note-display")
|
const { listenToClick } = useATProtoLinks("note-display")
|
||||||
|
useResizeContainer("note-container", stackedNotes)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
content,
|
content,
|
||||||
@@ -111,8 +91,32 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="public-note-view repo-note">
|
<div class="public-note-view repo-note note-container">
|
||||||
<div class="note article">
|
<div class="note article">
|
||||||
|
<div class="header">
|
||||||
|
<back-button
|
||||||
|
:fallback="{ name: 'PublicNoteListByDidView', params: { did } }"
|
||||||
|
:prefer-fallback="false"
|
||||||
|
/>
|
||||||
|
<theme-swap />
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="badge badge-author badge-soft badge-accent"
|
||||||
|
v-if="author && content"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'PublicNoteListByDidView', params: { did: did } }"
|
||||||
|
class="link link-hover"
|
||||||
|
>
|
||||||
|
{{ author.handle }}
|
||||||
|
</router-link>
|
||||||
|
<template v-if="publishedAt">
|
||||||
|
<span> • </span>
|
||||||
|
<span>{{ publishedAt }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<div class="badge skeleton h-4 w-50" v-else></div>
|
||||||
|
</div>
|
||||||
<div class="repo-title-breadcrumb">
|
<div class="repo-title-breadcrumb">
|
||||||
<a
|
<a
|
||||||
class="title-stacked-note-link"
|
class="title-stacked-note-link"
|
||||||
@@ -122,13 +126,15 @@ watch(
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="badge badge-author" v-if="author">
|
|
||||||
{{ author.alias }}
|
|
||||||
<span v-if="publishedAt"> • {{ publishedAt }}</span>
|
|
||||||
</span>
|
|
||||||
<article class="note-display" v-html="content"></article>
|
<article class="note-display" v-html="content"></article>
|
||||||
<BackButton />
|
|
||||||
</div>
|
</div>
|
||||||
|
<stacked-public-note
|
||||||
|
v-for="(stackedNote, index) in stackedNotes"
|
||||||
|
:key="stackedNote"
|
||||||
|
class="note"
|
||||||
|
:index="index"
|
||||||
|
:at-uri="stackedNote"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -137,32 +143,22 @@ watch(
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.back-button {
|
.header {
|
||||||
position: absolute;
|
margin-top: 1rem;
|
||||||
left: 1.5rem;
|
|
||||||
top: 0.4rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 2rem;
|
||||||
}
|
|
||||||
|
|
||||||
.badge-author {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.4rem;
|
|
||||||
right: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article {
|
.article {
|
||||||
position: sticky;
|
|
||||||
padding: 0 2rem;
|
padding: 0 2rem;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
article {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.content {
|
&.content {
|
||||||
@@ -211,6 +207,7 @@ watch(
|
|||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
@@ -224,5 +221,21 @@ watch(
|
|||||||
max-width: var(--note-width);
|
max-width: var(--note-width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.repo-title-breadcrumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article article {
|
||||||
|
margin-top: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
|||||||
import { queryFileContent } from "@/modules/repo/services/repo"
|
import { queryFileContent } from "@/modules/repo/services/repo"
|
||||||
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
||||||
|
|
||||||
|
type Prop = {
|
||||||
|
user: string
|
||||||
|
repo: string
|
||||||
|
}
|
||||||
|
|
||||||
const FluxNote = defineAsyncComponent(() => import("@/components/FluxNote.vue"))
|
const FluxNote = defineAsyncComponent(() => import("@/components/FluxNote.vue"))
|
||||||
const props = defineProps<{ user: string; repo: string }>()
|
const props = defineProps<Prop>()
|
||||||
const user = computed(() => props.user)
|
const user = computed(() => props.user)
|
||||||
const repo = computed(() => props.repo)
|
const repo = computed(() => props.repo)
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ module.exports = {
|
|||||||
p: {
|
p: {
|
||||||
"margin-top": "0.8em",
|
"margin-top": "0.8em",
|
||||||
"margin-bottom": "0.8em",
|
"margin-bottom": "0.8em",
|
||||||
"text-align": "justify",
|
"text-align": "left",
|
||||||
// "text-wrap": "balance",
|
// "text-wrap": "balance",
|
||||||
},
|
},
|
||||||
"img, video": {
|
"img, video": {
|
||||||
|
|||||||
Reference in New Issue
Block a user