Compare commits
30 Commits
002cf9a4b1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
816c3687d8 | ||
|
|
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 | ||
|
|
be006f08b4 | ||
|
|
55ee3bddeb | ||
|
|
1f324208d2 |
@@ -1,11 +1,11 @@
|
||||
# ---- Stage 1: deps (only invalidated when lockfile changes) ----
|
||||
FROM node:22-alpine AS deps
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN corepack enable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN pnpm install --frozen-lockfile
|
||||
# ---- Stage 2: build (invalidated on any source change) ----
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
RUN corepack enable
|
||||
|
||||
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>
|
||||
<html lang="en" data-theme="emerald">
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "remanso",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.0.9",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
allowBuilds:
|
||||
'@parcel/watcher': 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 |
@@ -19,8 +19,7 @@ onBeforeMount(async () => {
|
||||
if ("error" in token) {
|
||||
hasError.value = true
|
||||
} else {
|
||||
token.access_token
|
||||
saveCredentials(token)
|
||||
await saveCredentials(token)
|
||||
}
|
||||
|
||||
router.replace({ name: "Home" })
|
||||
|
||||
@@ -5,6 +5,7 @@ import HeaderNote from "@/components/HeaderNote.vue"
|
||||
import SignInGithub from "@/components/SignInGithub.vue"
|
||||
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||
import StackedNote from "@/components/StackedNote.vue"
|
||||
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||
import { useLinks } from "@/hooks/useLinks.hook"
|
||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||
import { useNoteView } from "@/hooks/useNoteView.hook"
|
||||
@@ -44,6 +45,7 @@ const { listenToClick } = useLinks("note-display")
|
||||
const { stackedNotes, scrollToFocusedNote } = useRouteQueryStackedNotes()
|
||||
|
||||
const { titles } = useNoteView()
|
||||
const { isLogged } = useGitHubLogin()
|
||||
useResizeContainer("note-container", stackedNotes)
|
||||
|
||||
const renderedContent = computed(() =>
|
||||
@@ -104,8 +106,13 @@ onUnmounted(() => {
|
||||
<slot />
|
||||
<skeleton-loader v-if="isLoading" />
|
||||
<div v-else-if="withContent && !hasContent" class="repo-not-found">
|
||||
<template v-if="isLogged">
|
||||
<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>
|
||||
<p
|
||||
v-else-if="withContent && hasContent"
|
||||
@@ -259,7 +266,7 @@ $header-height: 40px;
|
||||
|
||||
.note {
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
height: 100svh;
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ onMounted(() => {
|
||||
started editing. If you save now, their changes will be overwritten.
|
||||
</p>
|
||||
|
||||
<div class="modal-action flex-col gap-2 sm:flex-row sm:justify-end">
|
||||
<div class="modal-action flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
|
||||
@@ -13,9 +13,6 @@ defineEmits<{ (e: "click"): void }>()
|
||||
const formatTime = (d: Date) =>
|
||||
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(() => {
|
||||
switch (props.status) {
|
||||
case "verified":
|
||||
@@ -24,10 +21,6 @@ const label = computed(() => {
|
||||
return "Checking…"
|
||||
case "outdated":
|
||||
return "Outdated"
|
||||
case "stale-known":
|
||||
return props.lastCheckedAt
|
||||
? `Checked ${minutesAgo(props.lastCheckedAt)}m ago`
|
||||
: "Not checked"
|
||||
case "offline":
|
||||
return "Can’t reach GitHub"
|
||||
case "unknown":
|
||||
@@ -44,8 +37,6 @@ const tooltip = computed(() => {
|
||||
: "Click to re-check."
|
||||
case "outdated":
|
||||
return "GitHub has a newer version. Click to pull latest."
|
||||
case "stale-known":
|
||||
return "Click to verify against GitHub."
|
||||
case "offline":
|
||||
return "Could not reach GitHub. Click to retry."
|
||||
case "checking":
|
||||
@@ -88,14 +79,9 @@ const isBusy = computed(() => props.status === "checking")
|
||||
<path d="M15 19l2 2l4 -4" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else-if="status === 'unknown' || status === 'stale-known'"
|
||||
v-else-if="status === 'unknown'"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler"
|
||||
:class="
|
||||
status === 'stale-known'
|
||||
? 'icon-tabler-clock'
|
||||
: 'icon-tabler-cloud-question'
|
||||
"
|
||||
class="icon icon-tabler icon-tabler-cloud-question"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -105,11 +91,6 @@ const isBusy = computed(() => props.status === "checking")
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<template v-if="status === 'stale-known'">
|
||||
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
|
||||
<path d="M12 7v5l3 3" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
@@ -117,7 +98,6 @@ const isBusy = computed(() => props.status === "checking")
|
||||
<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
|
||||
v-else-if="status === 'outdated'"
|
||||
@@ -171,7 +151,6 @@ const isBusy = computed(() => props.status === "checking")
|
||||
/>
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
<span class="freshness-label">{{ label }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -213,7 +192,6 @@ const isBusy = computed(() => props.status === "checking")
|
||||
}
|
||||
|
||||
.state-unknown,
|
||||
.state-stale-known,
|
||||
.state-checking {
|
||||
color: var(--color-base-content);
|
||||
opacity: 0.6;
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
|
||||
import { getFileLanguage, isMarkdownPath } from "@/utils/fileLanguage"
|
||||
import { filenameToNoteTitle } from "@/utils/noteTitle"
|
||||
import { errorMessage } from "@/utils/notif"
|
||||
|
||||
const LinkedNotes = defineAsyncComponent(
|
||||
() => import("@/components/LinkedNotes.vue")
|
||||
@@ -241,6 +242,7 @@ const onConflictCancel = () => {
|
||||
}
|
||||
|
||||
const onBadgeClick = async () => {
|
||||
try {
|
||||
if (freshnessStatus.value !== "outdated") {
|
||||
await checkFreshness()
|
||||
return
|
||||
@@ -257,6 +259,10 @@ const onBadgeClick = async () => {
|
||||
rawContent.value = newRaw
|
||||
initialRawContent.value = newRaw
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("freshness badge click failed", error)
|
||||
errorMessage("❌ Couldn't pull latest from GitHub")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -269,27 +275,13 @@ const onBadgeClick = async () => {
|
||||
[`note-${sha}`]: true
|
||||
}"
|
||||
>
|
||||
<a
|
||||
class="title-stacked-note-link"
|
||||
@click.prevent="scrollToFocusedNote({ noteId: props.sha })"
|
||||
>
|
||||
<div
|
||||
class="title-stacked-note breadcrumbs text-sm"
|
||||
:class="titleClassName"
|
||||
>
|
||||
<ul>
|
||||
<li v-for="(part, i) in breadcrumbs" :key="i">
|
||||
{{ part }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
<section class="text-content">
|
||||
<div class="title-stacked-note breadcrumbs text-sm" :class="titleClassName">
|
||||
<div class="action-bar">
|
||||
<note-freshness-badge
|
||||
:status="freshnessStatus"
|
||||
:last-checked-at="lastCheckedAt"
|
||||
@click="onBadgeClick"
|
||||
class="action"
|
||||
/>
|
||||
<button
|
||||
v-if="isMarkdown"
|
||||
@@ -342,6 +334,18 @@ const onBadgeClick = async () => {
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<a
|
||||
class="title-stacked-note-link"
|
||||
@click.prevent="scrollToFocusedNote({ noteId: props.sha })"
|
||||
>
|
||||
<ul>
|
||||
<li v-for="(part, i) in breadcrumbs" :key="i">
|
||||
{{ part }}
|
||||
</li>
|
||||
</ul>
|
||||
</a>
|
||||
</div>
|
||||
<section class="text-content">
|
||||
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
||||
<edit-note v-model="rawContent" />
|
||||
</div>
|
||||
@@ -389,7 +393,6 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
background-color: var(--color-base-100);
|
||||
color: var(--color-base-content);
|
||||
font-size: 0.8em;
|
||||
overflow: hidden;
|
||||
|
||||
ul,
|
||||
li {
|
||||
@@ -415,12 +418,15 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.25rem;
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
@@ -429,7 +435,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
@media screen and (max-width: 768px) {
|
||||
.stacked-note {
|
||||
padding: 0 0.75rem 1rem;
|
||||
height: 100dvh;
|
||||
height: 100svh;
|
||||
|
||||
section {
|
||||
padding: 1rem 0;
|
||||
@@ -452,7 +458,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
}
|
||||
|
||||
.title-stacked-note {
|
||||
padding: 0 1rem;
|
||||
padding: 0;
|
||||
transform-origin: 0 0;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@@ -460,6 +466,12 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
a {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
.action {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -144,7 +144,6 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
background-color: var(--color-base-100);
|
||||
color: var(--color-base-content);
|
||||
font-size: 0.8em;
|
||||
overflow: hidden;
|
||||
|
||||
ul,
|
||||
li {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, Ref, ref } from "vue"
|
||||
import { Ref, ref } from "vue"
|
||||
|
||||
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||
@@ -10,12 +10,9 @@ export type FreshnessStatus =
|
||||
| "unknown"
|
||||
| "checking"
|
||||
| "verified"
|
||||
| "stale-known"
|
||||
| "outdated"
|
||||
| "offline"
|
||||
|
||||
const STALE_AFTER_MS = 2 * 60 * 1000
|
||||
|
||||
export const useNoteFreshness = ({
|
||||
user,
|
||||
repo,
|
||||
@@ -32,54 +29,45 @@ export const useNoteFreshness = ({
|
||||
const store = useUserRepoStore()
|
||||
const { fetchLatestSha } = useGitHubContent({ user, repo })
|
||||
|
||||
const rawStatus = ref<FreshnessStatus>("unknown")
|
||||
const status = ref<FreshnessStatus>("unknown")
|
||||
const lastCheckedAt = ref<Date | 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 check = async () => {
|
||||
if (!path.value) return
|
||||
rawStatus.value = "checking"
|
||||
status.value = "checking"
|
||||
const remoteSha = await fetchLatestSha(path.value)
|
||||
if (remoteSha === null) {
|
||||
rawStatus.value = "offline"
|
||||
status.value = "offline"
|
||||
return
|
||||
}
|
||||
latestSha.value = remoteSha
|
||||
lastCheckedAt.value = new Date()
|
||||
const local = await expectedSha()
|
||||
rawStatus.value = remoteSha === local ? "verified" : "outdated"
|
||||
armStaleTimer()
|
||||
status.value = remoteSha === local ? "verified" : "outdated"
|
||||
}
|
||||
|
||||
const pullLatest = async (): Promise<string | null> => {
|
||||
if (!path.value) return null
|
||||
const usedCachedSha = latestSha.value !== null
|
||||
const remoteSha = latestSha.value ?? (await fetchLatestSha(path.value))
|
||||
if (!remoteSha) {
|
||||
rawStatus.value = "offline"
|
||||
console.warn("pullLatest: could not resolve remote sha", { path: path.value })
|
||||
status.value = "offline"
|
||||
return null
|
||||
}
|
||||
const fileContent = await queryFileContent(user, repo, remoteSha)
|
||||
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
|
||||
}
|
||||
const { saveCacheNote } = prepareNoteCache(sha.value, path.value)
|
||||
@@ -90,8 +78,7 @@ export const useNoteFreshness = ({
|
||||
store.addFile({ path: path.value, sha: remoteSha })
|
||||
latestSha.value = remoteSha
|
||||
lastCheckedAt.value = new Date()
|
||||
rawStatus.value = "verified"
|
||||
armStaleTimer()
|
||||
status.value = "verified"
|
||||
const { getRawContent } = markdownBuilder(sha.value)
|
||||
return getRawContent(fileContent)
|
||||
}
|
||||
|
||||
@@ -40,10 +40,22 @@ export const useOverlay = (listen = true) => {
|
||||
}, 80)
|
||||
}
|
||||
|
||||
const scrollToElement = (element: HTMLElement, anchorTop?: number) => {
|
||||
const mainApp = document.getElementById("main-app")
|
||||
if (mainApp && anchorTop !== undefined) {
|
||||
mainApp.scrollTop = anchorTop
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" })
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
isMobile,
|
||||
scrollToNote
|
||||
scrollToNote,
|
||||
scrollToElement
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { computed, ref } from "vue"
|
||||
import { computed, ref, watch } from "vue"
|
||||
|
||||
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||
import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
|
||||
@@ -15,10 +15,19 @@ const currentPage = ref(0)
|
||||
const totalCount = ref(0)
|
||||
let lastFetchedAt = 0
|
||||
|
||||
export const useRepos = () => {
|
||||
const { username, accessToken } = useGitHubLogin()
|
||||
const { username, accessToken } = useGitHubLogin()
|
||||
|
||||
const loadMore = async () => {
|
||||
const resetState = () => {
|
||||
repos.value = []
|
||||
currentPage.value = 0
|
||||
totalCount.value = 0
|
||||
isReady.value = false
|
||||
isLoading.value = false
|
||||
hasCredentialError.value = false
|
||||
lastFetchedAt = 0
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
if (!accessToken.value || !username.value) {
|
||||
isReady.value = true
|
||||
return
|
||||
@@ -58,8 +67,18 @@ export const useRepos = () => {
|
||||
isReady.value = true
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(accessToken, (next, prev) => {
|
||||
if (next === prev) return
|
||||
resetState()
|
||||
if (next && username.value) {
|
||||
lastFetchedAt = Date.now()
|
||||
loadMore()
|
||||
}
|
||||
})
|
||||
|
||||
export const useRepos = () => {
|
||||
const canLoadMore = computed(
|
||||
() => !isLoading.value && repos.value.length < totalCount.value
|
||||
)
|
||||
@@ -67,12 +86,7 @@ export const useRepos = () => {
|
||||
const isStale = Date.now() - lastFetchedAt > STALE_TIME_MS
|
||||
if (!isReady.value || isStale) {
|
||||
if (isStale && isReady.value) {
|
||||
repos.value = []
|
||||
currentPage.value = 0
|
||||
totalCount.value = 0
|
||||
isReady.value = false
|
||||
isLoading.value = false
|
||||
hasCredentialError.value = false
|
||||
resetState()
|
||||
}
|
||||
lastFetchedAt = Date.now()
|
||||
loadMore()
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useResizeContainer = (
|
||||
}
|
||||
|
||||
if (isMobile.value) {
|
||||
container.style.height = `${(stackedNotes.value.length + 1) * 100}dvh`
|
||||
container.style.height = `${(stackedNotes.value.length + 1) * 100}svh`
|
||||
} else {
|
||||
container.style.minWidth = `${
|
||||
getNoteWidth() * (stackedNotes.value.length + 1)
|
||||
|
||||
@@ -18,7 +18,7 @@ export const useRouteQueryStackedNotes = () => {
|
||||
})
|
||||
const { height } = useWindowSize()
|
||||
|
||||
const { scrollToNote, isMobile } = useOverlay(false)
|
||||
const { scrollToNote, scrollToElement, isMobile } = useOverlay(false)
|
||||
|
||||
const scrollToHashInNote = (
|
||||
cleanSha: string,
|
||||
@@ -50,6 +50,7 @@ export const useRouteQueryStackedNotes = () => {
|
||||
const scrollToNoteElement = (
|
||||
cleanNoteId: string,
|
||||
index: number,
|
||||
anchorTop?: number,
|
||||
attempts = 30
|
||||
) => {
|
||||
const element = document.querySelector(
|
||||
@@ -57,7 +58,7 @@ export const useRouteQueryStackedNotes = () => {
|
||||
) as HTMLElement | null
|
||||
|
||||
if (element) {
|
||||
scrollToNote((index + 1) * element.clientHeight)
|
||||
scrollToElement(element, anchorTop)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ export const useRouteQueryStackedNotes = () => {
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
scrollToNoteElement(cleanNoteId, index, attempts - 1)
|
||||
scrollToNoteElement(cleanNoteId, index, anchorTop, attempts - 1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -76,20 +77,22 @@ export const useRouteQueryStackedNotes = () => {
|
||||
notes?: string[]
|
||||
hash?: string
|
||||
smoothHash?: boolean
|
||||
anchorTop?: number
|
||||
}
|
||||
|
||||
const scrollToFocusedNote = ({
|
||||
noteId = null,
|
||||
notes = stackedNotes.value,
|
||||
hash,
|
||||
smoothHash = false
|
||||
smoothHash = false,
|
||||
anchorTop
|
||||
}: ScrollToFocusedNoteOptions = {}) => {
|
||||
nextTick(() => {
|
||||
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
|
||||
|
||||
if (isMobile.value) {
|
||||
if (noteId) {
|
||||
scrollToNoteElement(noteId.replaceAll(":", "-"), index)
|
||||
scrollToNoteElement(noteId.replaceAll(":", "-"), index, anchorTop)
|
||||
} else {
|
||||
scrollToNote(0)
|
||||
}
|
||||
@@ -114,11 +117,15 @@ export const useRouteQueryStackedNotes = () => {
|
||||
selector?: string,
|
||||
hash?: string
|
||||
) => {
|
||||
const anchorTop =
|
||||
document.getElementById("main-app")?.scrollTop ?? undefined
|
||||
|
||||
if (stackedNotes.value.includes(sha)) {
|
||||
scrollToFocusedNote({
|
||||
noteId: selector ?? sha,
|
||||
hash,
|
||||
smoothHash: true
|
||||
smoothHash: true,
|
||||
anchorTop
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -139,7 +146,7 @@ export const useRouteQueryStackedNotes = () => {
|
||||
stackedNotes.value = newStackedNotes
|
||||
}
|
||||
|
||||
scrollToFocusedNote({ noteId: selector ?? sha, hash })
|
||||
scrollToFocusedNote({ noteId: selector ?? sha, hash, anchorTop })
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -121,12 +121,12 @@ export const queryFileContent = async (
|
||||
repo: string,
|
||||
sha: string
|
||||
) => {
|
||||
const octokit = await getOctokit()
|
||||
|
||||
if (!user || !repo) {
|
||||
null
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const octokit = await getOctokit()
|
||||
const file = await octokit.request(
|
||||
"GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
|
||||
{
|
||||
@@ -135,6 +135,9 @@ export const queryFileContent = async (
|
||||
file_sha: sha
|
||||
}
|
||||
)
|
||||
|
||||
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 { toRaw } from "vue"
|
||||
|
||||
import { data, generateId } from "@/data/data"
|
||||
import { DataType } from "@/data/DataType.enum"
|
||||
@@ -166,8 +166,10 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
||||
|
||||
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
||||
if (requestId !== this._requestId) return
|
||||
this.readme = cachedReadme
|
||||
this.readme = await getMainReadme(user, repo)
|
||||
if (cachedReadme) this.readme = cachedReadme
|
||||
const fetched = await getMainReadme(user, repo)
|
||||
if (requestId !== this._requestId) return
|
||||
this.readme = fetched
|
||||
})
|
||||
},
|
||||
addFile(file: RepoFile) {
|
||||
|
||||
@@ -95,8 +95,9 @@ export const router = createRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach(() => {
|
||||
router.beforeEach((to, from) => {
|
||||
if (!("startViewTransition" in document)) return
|
||||
if (to.path === from.path) return
|
||||
return new Promise<void>((resolve) => {
|
||||
;(
|
||||
document as Document & {
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
--light-link: lighten(#445fb9, 45%);
|
||||
--background-color: #ffffff;
|
||||
--note-width: 500px;
|
||||
--note-canvas-bg: color-mix(in oklch, var(--color-base-100) 60%, var(--color-base-200));
|
||||
--note-canvas-bg: color-mix(
|
||||
in oklch,
|
||||
var(--color-base-100) 60%,
|
||||
var(--color-base-200)
|
||||
);
|
||||
--note-sheet-shadow: 1px 0 8px rgb(0 0 0 / 6%);
|
||||
--color-contrast-content: var(--color-success);
|
||||
--notyf-margin: 0.5rem;
|
||||
@@ -27,8 +31,8 @@
|
||||
|
||||
@plugin 'daisyui' {
|
||||
themes:
|
||||
emerald --default,
|
||||
forest --prefersdark;
|
||||
light --default,
|
||||
dracula --prefersdark;
|
||||
}
|
||||
|
||||
@config '../../tailwind.config.js';
|
||||
@@ -91,18 +95,36 @@ a {
|
||||
}
|
||||
}
|
||||
|
||||
a.title-stacked-note-link {
|
||||
.title-stacked-note {
|
||||
color: var(--color-base-content);
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.title-stacked-note-link {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title-stacked-note ul,
|
||||
.title-stacked-note li {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.notyf__toast {
|
||||
border-radius: revert-layer;
|
||||
border: none;
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
// Update these values to change the light and dark themes
|
||||
|
||||
export const themeConfig = {
|
||||
light: "emerald",
|
||||
dark: "forest"
|
||||
light: 'light',
|
||||
dark: 'dracula'
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default defineConfig(({ command }) => {
|
||||
"pwa-512x512.png",
|
||||
"masked-icon.png",
|
||||
"maskable-icon-512x512.png",
|
||||
"monochrome-icon.png",
|
||||
"monochromeicon.png",
|
||||
"assets/*.svg"
|
||||
],
|
||||
manifest: {
|
||||
@@ -54,7 +54,7 @@ export default defineConfig(({ command }) => {
|
||||
purpose: "maskable"
|
||||
},
|
||||
{
|
||||
src: "monochrome-icon.png",
|
||||
src: "monochromeicon.png",
|
||||
sizes: "1024x1024",
|
||||
type: "image/png",
|
||||
purpose: "monochrome"
|
||||
|
||||
Reference in New Issue
Block a user