Compare commits
26 Commits
be006f08b4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2f2a3114b | ||
|
|
2f71566083 | ||
|
|
80ae544a28 | ||
|
|
bfd981de13 | ||
|
|
453332513a | ||
|
|
abc0113c8e | ||
|
|
52deb5feb4 | ||
|
|
9e07204430 | ||
|
|
cd60429145 | ||
|
|
aad07184fd | ||
|
|
76829afba2 | ||
|
|
05f59a568d | ||
|
|
559bfccd08 | ||
|
|
f8ae4351d6 | ||
|
|
30f200df30 | ||
|
|
58568e2245 | ||
|
|
fd7d06ce69 | ||
|
|
5a9c0a3704 | ||
|
|
e425be5c96 | ||
|
|
84803c45dd | ||
|
|
a526a9f6af | ||
|
|
08e01d8484 | ||
|
|
c88340d5f1 | ||
|
|
550b3cf019 | ||
|
|
2f05b93f51 | ||
|
|
cc266eac7c |
@@ -1,11 +1,11 @@
|
|||||||
# ---- Stage 1: deps (only invalidated when lockfile changes) ----
|
# ---- Stage 1: deps (only invalidated when lockfile changes) ----
|
||||||
FROM node:22-alpine AS deps
|
FROM node:22-alpine AS deps
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ RUN pnpm install --frozen-lockfile
|
|||||||
# ---- Stage 2: build (invalidated on any source change) ----
|
# ---- Stage 2: build (invalidated on any source change) ----
|
||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
40
_scripts/build-monochrome-icon.ts
Normal file
40
_scripts/build-monochrome-icon.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import path from "path"
|
||||||
|
import sharp from "sharp"
|
||||||
|
|
||||||
|
// PWA spec: `purpose: "monochrome"` icons are *masks*. The user agent ignores
|
||||||
|
// RGB and uses only the alpha channel as the silhouette, then paints it with
|
||||||
|
// the platform theme color. So the source PNG must be RGBA with the silhouette
|
||||||
|
// in alpha, NOT a black-on-white RGB image.
|
||||||
|
|
||||||
|
const SRC = path.resolve(__dirname, "../public/favicon.png")
|
||||||
|
const OUT = path.resolve(__dirname, "../public/monochromeicon.png")
|
||||||
|
const SIZE = 1024
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { data, info } = await sharp(SRC)
|
||||||
|
.resize(SIZE, SIZE, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true })
|
||||||
|
|
||||||
|
if (info.channels !== 4) throw new Error(`expected RGBA, got ${info.channels} channels`)
|
||||||
|
|
||||||
|
const out = Buffer.alloc(data.length)
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
out[i] = 0
|
||||||
|
out[i + 1] = 0
|
||||||
|
out[i + 2] = 0
|
||||||
|
out[i + 3] = data[i + 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
await sharp(out, { raw: { width: SIZE, height: SIZE, channels: 4 } })
|
||||||
|
.png({ compressionLevel: 9 })
|
||||||
|
.toFile(OUT)
|
||||||
|
|
||||||
|
console.log(`Wrote ${OUT} (${SIZE}x${SIZE} RGBA)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="emerald">
|
<html lang="en" data-theme="light">
|
||||||
<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" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "remanso",
|
"name": "remanso",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"packageManager": "pnpm@11.0.9",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
core-js: true
|
core-js: true
|
||||||
|
esbuild: true
|
||||||
|
fsevents: true
|
||||||
|
sharp: true
|
||||||
|
vue-demi: true
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB |
BIN
public/monochromeicon.png
Normal file
BIN
public/monochromeicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
@@ -5,6 +5,7 @@ import HeaderNote from "@/components/HeaderNote.vue"
|
|||||||
import SignInGithub from "@/components/SignInGithub.vue"
|
import SignInGithub from "@/components/SignInGithub.vue"
|
||||||
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
import StackedNote from "@/components/StackedNote.vue"
|
import StackedNote from "@/components/StackedNote.vue"
|
||||||
|
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||||
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"
|
||||||
@@ -44,6 +45,7 @@ const { listenToClick } = useLinks("note-display")
|
|||||||
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||||
|
|
||||||
const { titles } = useNoteView()
|
const { titles } = useNoteView()
|
||||||
|
const { isLogged } = useGitHubLogin()
|
||||||
useResizeContainer("note-container", stackedNotes)
|
useResizeContainer("note-container", stackedNotes)
|
||||||
|
|
||||||
const renderedContent = computed(() =>
|
const renderedContent = computed(() =>
|
||||||
@@ -104,8 +106,13 @@ onUnmounted(() => {
|
|||||||
<slot />
|
<slot />
|
||||||
<skeleton-loader v-if="isLoading" />
|
<skeleton-loader v-if="isLoading" />
|
||||||
<div v-else-if="withContent && !hasContent" class="repo-not-found">
|
<div v-else-if="withContent && !hasContent" class="repo-not-found">
|
||||||
<p>This repository is not accessible.</p>
|
<template v-if="isLogged">
|
||||||
<sign-in-github />
|
<p>This repository is not accessible.</p>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p>This repository is private. Sign in to view it.</p>
|
||||||
|
<sign-in-github />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-else-if="withContent && hasContent"
|
v-else-if="withContent && hasContent"
|
||||||
@@ -259,7 +266,7 @@ $header-height: 40px;
|
|||||||
|
|
||||||
.note {
|
.note {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100dvh;
|
height: 100svh;
|
||||||
overflow-y: visible;
|
overflow-y: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ onMounted(() => {
|
|||||||
started editing. If you save now, their changes will be overwritten.
|
started editing. If you save now, their changes will be overwritten.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="modal-action flex-col gap-2 sm:flex-row sm:justify-end">
|
<div class="modal-action flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost"
|
class="btn btn-ghost"
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ defineEmits<{ (e: "click"): void }>()
|
|||||||
const formatTime = (d: Date) =>
|
const formatTime = (d: Date) =>
|
||||||
d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })
|
||||||
|
|
||||||
const minutesAgo = (d: Date) =>
|
|
||||||
Math.max(1, Math.round((Date.now() - d.getTime()) / 60000))
|
|
||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
case "verified":
|
case "verified":
|
||||||
@@ -24,10 +21,6 @@ const label = computed(() => {
|
|||||||
return "Checking…"
|
return "Checking…"
|
||||||
case "outdated":
|
case "outdated":
|
||||||
return "Outdated"
|
return "Outdated"
|
||||||
case "stale-known":
|
|
||||||
return props.lastCheckedAt
|
|
||||||
? `Checked ${minutesAgo(props.lastCheckedAt)}m ago`
|
|
||||||
: "Not checked"
|
|
||||||
case "offline":
|
case "offline":
|
||||||
return "Can’t reach GitHub"
|
return "Can’t reach GitHub"
|
||||||
case "unknown":
|
case "unknown":
|
||||||
@@ -44,8 +37,6 @@ const tooltip = computed(() => {
|
|||||||
: "Click to re-check."
|
: "Click to re-check."
|
||||||
case "outdated":
|
case "outdated":
|
||||||
return "GitHub has a newer version. Click to pull latest."
|
return "GitHub has a newer version. Click to pull latest."
|
||||||
case "stale-known":
|
|
||||||
return "Click to verify against GitHub."
|
|
||||||
case "offline":
|
case "offline":
|
||||||
return "Could not reach GitHub. Click to retry."
|
return "Could not reach GitHub. Click to retry."
|
||||||
case "checking":
|
case "checking":
|
||||||
@@ -88,14 +79,9 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
<path d="M15 19l2 2l4 -4" />
|
<path d="M15 19l2 2l4 -4" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg
|
||||||
v-else-if="status === 'unknown' || status === 'stale-known'"
|
v-else-if="status === 'unknown'"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler"
|
class="icon icon-tabler icon-tabler-cloud-question"
|
||||||
:class="
|
|
||||||
status === 'stale-known'
|
|
||||||
? 'icon-tabler-clock'
|
|
||||||
: 'icon-tabler-cloud-question'
|
|
||||||
"
|
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -105,19 +91,13 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<template v-if="status === 'stale-known'">
|
<path
|
||||||
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
d="M14.5 18.004h-7.843c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.393 -1.762 1.794 -3.2 3.675 -3.773c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99"
|
||||||
<path d="M12 7v5l3 3" />
|
/>
|
||||||
</template>
|
<path d="M19 22v.01" />
|
||||||
<template v-else>
|
<path
|
||||||
<path
|
d="M19 19a2.003 2.003 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"
|
||||||
d="M14.5 18.004h-7.843c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.393 -1.762 1.794 -3.2 3.675 -3.773c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99"
|
/>
|
||||||
/>
|
|
||||||
<path d="M19 22v.01" />
|
|
||||||
<path
|
|
||||||
d="M19 19a2.003 2.003 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</svg>
|
</svg>
|
||||||
<svg
|
<svg
|
||||||
v-else-if="status === 'outdated'"
|
v-else-if="status === 'outdated'"
|
||||||
@@ -212,7 +192,6 @@ const isBusy = computed(() => props.status === "checking")
|
|||||||
}
|
}
|
||||||
|
|
||||||
.state-unknown,
|
.state-unknown,
|
||||||
.state-stale-known,
|
|
||||||
.state-checking {
|
.state-checking {
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
|||||||
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
||||||
import { getFileLanguage, isMarkdownPath } from "@/utils/fileLanguage"
|
import { getFileLanguage, isMarkdownPath } from "@/utils/fileLanguage"
|
||||||
import { filenameToNoteTitle } from "@/utils/noteTitle"
|
import { filenameToNoteTitle } from "@/utils/noteTitle"
|
||||||
|
import { errorMessage } from "@/utils/notif"
|
||||||
|
|
||||||
const LinkedNotes = defineAsyncComponent(
|
const LinkedNotes = defineAsyncComponent(
|
||||||
() => import("@/components/LinkedNotes.vue")
|
() => import("@/components/LinkedNotes.vue")
|
||||||
@@ -241,21 +242,26 @@ const onConflictCancel = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onBadgeClick = async () => {
|
const onBadgeClick = async () => {
|
||||||
if (freshnessStatus.value !== "outdated") {
|
try {
|
||||||
await checkFreshness()
|
if (freshnessStatus.value !== "outdated") {
|
||||||
return
|
await checkFreshness()
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const hasUnsavedEdits = rawContent.value !== initialRawContent.value
|
const hasUnsavedEdits = rawContent.value !== initialRawContent.value
|
||||||
if (hasUnsavedEdits) {
|
if (hasUnsavedEdits) {
|
||||||
conflictOpen.value = true
|
conflictOpen.value = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRaw = await pullLatest()
|
const newRaw = await pullLatest()
|
||||||
if (newRaw !== null) {
|
if (newRaw !== null) {
|
||||||
rawContent.value = newRaw
|
rawContent.value = newRaw
|
||||||
initialRawContent.value = newRaw
|
initialRawContent.value = newRaw
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("freshness badge click failed", error)
|
||||||
|
errorMessage("❌ Couldn't pull latest from GitHub")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -429,7 +435,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.stacked-note {
|
.stacked-note {
|
||||||
padding: 0 0.75rem 1rem;
|
padding: 0 0.75rem 1rem;
|
||||||
height: 100dvh;
|
height: 100svh;
|
||||||
|
|
||||||
section {
|
section {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { computed, Ref, ref } from "vue"
|
import { Ref, ref } from "vue"
|
||||||
|
|
||||||
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
||||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
@@ -10,12 +10,9 @@ export type FreshnessStatus =
|
|||||||
| "unknown"
|
| "unknown"
|
||||||
| "checking"
|
| "checking"
|
||||||
| "verified"
|
| "verified"
|
||||||
| "stale-known"
|
|
||||||
| "outdated"
|
| "outdated"
|
||||||
| "offline"
|
| "offline"
|
||||||
|
|
||||||
const STALE_AFTER_MS = 2 * 60 * 1000
|
|
||||||
|
|
||||||
export const useNoteFreshness = ({
|
export const useNoteFreshness = ({
|
||||||
user,
|
user,
|
||||||
repo,
|
repo,
|
||||||
@@ -32,54 +29,45 @@ export const useNoteFreshness = ({
|
|||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
const { fetchLatestSha } = useGitHubContent({ user, repo })
|
const { fetchLatestSha } = useGitHubContent({ user, repo })
|
||||||
|
|
||||||
const rawStatus = ref<FreshnessStatus>("unknown")
|
const status = ref<FreshnessStatus>("unknown")
|
||||||
const lastCheckedAt = ref<Date | null>(null)
|
const lastCheckedAt = ref<Date | null>(null)
|
||||||
const latestSha = ref<string | null>(null)
|
const latestSha = ref<string | null>(null)
|
||||||
const tick = ref(0)
|
|
||||||
let staleTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
|
|
||||||
const status = computed<FreshnessStatus>(() => {
|
|
||||||
void tick.value
|
|
||||||
if (rawStatus.value !== "verified") return rawStatus.value
|
|
||||||
if (!lastCheckedAt.value) return rawStatus.value
|
|
||||||
const age = Date.now() - lastCheckedAt.value.getTime()
|
|
||||||
return age > STALE_AFTER_MS ? "stale-known" : "verified"
|
|
||||||
})
|
|
||||||
|
|
||||||
const armStaleTimer = () => {
|
|
||||||
if (staleTimer) clearTimeout(staleTimer)
|
|
||||||
staleTimer = setTimeout(() => {
|
|
||||||
tick.value++
|
|
||||||
}, STALE_AFTER_MS + 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedSha = async () => (await getEditedSha()) ?? sha.value
|
const expectedSha = async () => (await getEditedSha()) ?? sha.value
|
||||||
|
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
if (!path.value) return
|
if (!path.value) return
|
||||||
rawStatus.value = "checking"
|
status.value = "checking"
|
||||||
const remoteSha = await fetchLatestSha(path.value)
|
const remoteSha = await fetchLatestSha(path.value)
|
||||||
if (remoteSha === null) {
|
if (remoteSha === null) {
|
||||||
rawStatus.value = "offline"
|
status.value = "offline"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
latestSha.value = remoteSha
|
latestSha.value = remoteSha
|
||||||
lastCheckedAt.value = new Date()
|
lastCheckedAt.value = new Date()
|
||||||
const local = await expectedSha()
|
const local = await expectedSha()
|
||||||
rawStatus.value = remoteSha === local ? "verified" : "outdated"
|
status.value = remoteSha === local ? "verified" : "outdated"
|
||||||
armStaleTimer()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullLatest = async (): Promise<string | null> => {
|
const pullLatest = async (): Promise<string | null> => {
|
||||||
if (!path.value) return null
|
if (!path.value) return null
|
||||||
|
const usedCachedSha = latestSha.value !== null
|
||||||
const remoteSha = latestSha.value ?? (await fetchLatestSha(path.value))
|
const remoteSha = latestSha.value ?? (await fetchLatestSha(path.value))
|
||||||
if (!remoteSha) {
|
if (!remoteSha) {
|
||||||
rawStatus.value = "offline"
|
console.warn("pullLatest: could not resolve remote sha", { path: path.value })
|
||||||
|
status.value = "offline"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const fileContent = await queryFileContent(user, repo, remoteSha)
|
const fileContent = await queryFileContent(user, repo, remoteSha)
|
||||||
if (!fileContent) {
|
if (!fileContent) {
|
||||||
rawStatus.value = "offline"
|
console.warn("pullLatest: failed to fetch blob content", {
|
||||||
|
path: path.value,
|
||||||
|
remoteSha,
|
||||||
|
usedCachedSha
|
||||||
|
})
|
||||||
|
// Cached SHA may be stale — clear so the next click re-resolves it.
|
||||||
|
if (usedCachedSha) latestSha.value = null
|
||||||
|
status.value = "offline"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
||||||
@@ -90,8 +78,7 @@ export const useNoteFreshness = ({
|
|||||||
store.addFile({ path: path.value, sha: remoteSha })
|
store.addFile({ path: path.value, sha: remoteSha })
|
||||||
latestSha.value = remoteSha
|
latestSha.value = remoteSha
|
||||||
lastCheckedAt.value = new Date()
|
lastCheckedAt.value = new Date()
|
||||||
rawStatus.value = "verified"
|
status.value = "verified"
|
||||||
armStaleTimer()
|
|
||||||
const { getRawContent } = markdownBuilder(sha.value)
|
const { getRawContent } = markdownBuilder(sha.value)
|
||||||
return getRawContent(fileContent)
|
return getRawContent(fileContent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,16 +40,14 @@ export const useOverlay = (listen = true) => {
|
|||||||
}, 80)
|
}, 80)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToElement = (element: HTMLElement) => {
|
const scrollToElement = (element: HTMLElement, anchorTop?: number) => {
|
||||||
const mainApp = document.getElementById("main-app")
|
const mainApp = document.getElementById("main-app")
|
||||||
if (!mainApp) return
|
if (mainApp && anchorTop !== undefined) {
|
||||||
|
mainApp.scrollTop = anchorTop
|
||||||
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const top =
|
element.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||||
element.getBoundingClientRect().top -
|
|
||||||
mainApp.getBoundingClientRect().top +
|
|
||||||
mainApp.scrollTop
|
|
||||||
mainApp.scrollTo({ top, behavior: "smooth" })
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const useResizeContainer = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
container.style.height = `${(stackedNotes.value.length + 1) * 100}dvh`
|
container.style.height = `${(stackedNotes.value.length + 1) * 100}svh`
|
||||||
} else {
|
} else {
|
||||||
container.style.minWidth = `${
|
container.style.minWidth = `${
|
||||||
getNoteWidth() * (stackedNotes.value.length + 1)
|
getNoteWidth() * (stackedNotes.value.length + 1)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
const scrollToNoteElement = (
|
const scrollToNoteElement = (
|
||||||
cleanNoteId: string,
|
cleanNoteId: string,
|
||||||
index: number,
|
index: number,
|
||||||
|
anchorTop?: number,
|
||||||
attempts = 30
|
attempts = 30
|
||||||
) => {
|
) => {
|
||||||
const element = document.querySelector(
|
const element = document.querySelector(
|
||||||
@@ -57,7 +58,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
) as HTMLElement | null
|
) as HTMLElement | null
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
scrollToElement(element)
|
scrollToElement(element, anchorTop)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
scrollToNoteElement(cleanNoteId, index, attempts - 1)
|
scrollToNoteElement(cleanNoteId, index, anchorTop, attempts - 1)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,20 +77,22 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
notes?: string[]
|
notes?: string[]
|
||||||
hash?: string
|
hash?: string
|
||||||
smoothHash?: boolean
|
smoothHash?: boolean
|
||||||
|
anchorTop?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToFocusedNote = ({
|
const scrollToFocusedNote = ({
|
||||||
noteId = null,
|
noteId = null,
|
||||||
notes = stackedNotes.value,
|
notes = stackedNotes.value,
|
||||||
hash,
|
hash,
|
||||||
smoothHash = false
|
smoothHash = false,
|
||||||
|
anchorTop
|
||||||
}: ScrollToFocusedNoteOptions = {}) => {
|
}: ScrollToFocusedNoteOptions = {}) => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
||||||
|
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
if (noteId) {
|
if (noteId) {
|
||||||
scrollToNoteElement(noteId.replaceAll(":", "-"), index)
|
scrollToNoteElement(noteId.replaceAll(":", "-"), index, anchorTop)
|
||||||
} else {
|
} else {
|
||||||
scrollToNote(0)
|
scrollToNote(0)
|
||||||
}
|
}
|
||||||
@@ -114,11 +117,15 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
selector?: string,
|
selector?: string,
|
||||||
hash?: string
|
hash?: string
|
||||||
) => {
|
) => {
|
||||||
|
const anchorTop =
|
||||||
|
document.getElementById("main-app")?.scrollTop ?? undefined
|
||||||
|
|
||||||
if (stackedNotes.value.includes(sha)) {
|
if (stackedNotes.value.includes(sha)) {
|
||||||
scrollToFocusedNote({
|
scrollToFocusedNote({
|
||||||
noteId: selector ?? sha,
|
noteId: selector ?? sha,
|
||||||
hash,
|
hash,
|
||||||
smoothHash: true
|
smoothHash: true,
|
||||||
|
anchorTop
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -139,7 +146,7 @@ export const useRouteQueryStackedNotes = () => {
|
|||||||
stackedNotes.value = newStackedNotes
|
stackedNotes.value = newStackedNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToFocusedNote({ noteId: selector ?? sha, hash })
|
scrollToFocusedNote({ noteId: selector ?? sha, hash, anchorTop })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -121,20 +121,23 @@ export const queryFileContent = async (
|
|||||||
repo: string,
|
repo: string,
|
||||||
sha: string
|
sha: string
|
||||||
) => {
|
) => {
|
||||||
const octokit = await getOctokit()
|
|
||||||
|
|
||||||
if (!user || !repo) {
|
if (!user || !repo) {
|
||||||
null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await octokit.request(
|
try {
|
||||||
"GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
|
const octokit = await getOctokit()
|
||||||
{
|
const file = await octokit.request(
|
||||||
owner: user,
|
"GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
|
||||||
repo: repo,
|
{
|
||||||
file_sha: sha
|
owner: user,
|
||||||
}
|
repo: repo,
|
||||||
)
|
file_sha: sha
|
||||||
|
}
|
||||||
return file?.data.content ?? null
|
)
|
||||||
|
return file?.data.content ?? null
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("queryFileContent failed", { user, repo, sha, error })
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { toRaw } from "vue"
|
|
||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
|
import { toRaw } from "vue"
|
||||||
|
|
||||||
import { data, generateId } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
@@ -166,8 +166,10 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
|
|
||||||
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
||||||
if (requestId !== this._requestId) return
|
if (requestId !== this._requestId) return
|
||||||
this.readme = cachedReadme
|
if (cachedReadme) this.readme = cachedReadme
|
||||||
this.readme = await getMainReadme(user, repo)
|
const fetched = await getMainReadme(user, repo)
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
|
this.readme = fetched
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addFile(file: RepoFile) {
|
addFile(file: RepoFile) {
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
|
|
||||||
@plugin 'daisyui' {
|
@plugin 'daisyui' {
|
||||||
themes:
|
themes:
|
||||||
emerald --default,
|
light --default,
|
||||||
forest --prefersdark;
|
dracula --prefersdark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@config '../../tailwind.config.js';
|
@config '../../tailwind.config.js';
|
||||||
@@ -111,6 +111,7 @@ a.title-stacked-note-link {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: "emerald",
|
light: 'light',
|
||||||
dark: "forest"
|
dark: 'dracula'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default defineConfig(({ command }) => {
|
|||||||
"pwa-512x512.png",
|
"pwa-512x512.png",
|
||||||
"masked-icon.png",
|
"masked-icon.png",
|
||||||
"maskable-icon-512x512.png",
|
"maskable-icon-512x512.png",
|
||||||
"monochrome-icon.png",
|
"monochromeicon.png",
|
||||||
"assets/*.svg"
|
"assets/*.svg"
|
||||||
],
|
],
|
||||||
manifest: {
|
manifest: {
|
||||||
@@ -54,7 +54,7 @@ export default defineConfig(({ command }) => {
|
|||||||
purpose: "maskable"
|
purpose: "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "monochrome-icon.png",
|
src: "monochromeicon.png",
|
||||||
sizes: "1024x1024",
|
sizes: "1024x1024",
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
purpose: "monochrome"
|
purpose: "monochrome"
|
||||||
|
|||||||
Reference in New Issue
Block a user