feat: add fontFamilies array

This commit is contained in:
Julien Calixte
2025-07-06 11:07:09 +02:00
parent 5341e6dcf0
commit b2250b2b95
8 changed files with 169 additions and 126 deletions

View File

@@ -1,57 +1,53 @@
require('@rushstack/eslint-patch/modern-module-resolution') require("@rushstack/eslint-patch/modern-module-resolution")
const DEV_TOOL_ACTIVATED = const DEV_TOOL_ACTIVATED =
process.env.NODE_ENV === 'production' ? 'warn' : 'off' process.env.NODE_ENV === "production" ? "warn" : "off"
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true, node: true,
es2022: true es2022: true,
}, },
extends: [ extends: ["plugin:vue/vue3-essential", "@vue/eslint-config-typescript"],
'plugin:vue/vue3-essential', plugins: ["simple-import-sort", "unused-imports"],
'eslint:recommended',
'plugin:vue/recommended',
'@vue/typescript/recommended',
'@vue/eslint-config-typescript',
'plugin:prettier-vue/recommended'
],
plugins: ['simple-import-sort', 'unused-imports'],
rules: { rules: {
'no-console': DEV_TOOL_ACTIVATED, "no-console": DEV_TOOL_ACTIVATED,
'no-debugger': DEV_TOOL_ACTIVATED, "no-debugger": DEV_TOOL_ACTIVATED,
'@typescript-eslint/explicit-module-boundary-types': 'off', "@typescript-eslint/explicit-module-boundary-types": "off",
'@typescript-eslint/camelcase': 'off', "@typescript-eslint/camelcase": "off",
'prettier-vue/prettier': [ "prettier-vue/prettier": [
'error', "error",
{ {
semi: false, semi: false,
singleQuote: true, singleQuote: true,
trailingComma: 'none', trailingComma: "none",
arrowParens: 'always' arrowParens: "always",
} },
], ],
'vue/no-v-html': 'off', "vue/no-v-html": "off",
'no-restricted-imports': [ "no-restricted-imports": [
'error', "error",
{ {
paths: [ paths: [
{ {
name: 'vue-demi', name: "vue-demi",
importNames: ['computed'], importNames: ["computed"],
message: 'Please use computed from vue instead.' message: "Please use computed from vue instead.",
} },
] ],
} },
], ],
'simple-import-sort/imports': 'error', "simple-import-sort/imports": "error",
'simple-import-sort/exports': 'error', "simple-import-sort/exports": "error",
'unused-imports/no-unused-imports': 'error' "unused-imports/no-unused-imports": "error",
}, },
overrides: [ overrides: [
{ {
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'] files: [
} "**/__tests__/*.{j,t}s?(x)",
] "**/tests/unit/**/*.spec.{j,t}s?(x)",
],
},
],
} }

View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { computed } from "vue"
import { useUserRepoStore } from "../modules/repo/store/userRepo.store"
const store = useUserRepoStore()
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
</script>
<template>
<select
v-if="fontFamilies.length > 0"
class="select select-sm"
:value="store.userSettings?.chosenFontFamily"
@change="store.setFontFamily(($event.target as HTMLSelectElement).value)"
>
<option v-for="font in fontFamilies" :key="font" :value="font">
{{ font }}
</option>
</select>
</template>

View File

@@ -1,8 +1,10 @@
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { Model } from '@/data/models/Model' import { Model } from "@/data/models/Model"
export interface UserSettings extends Model<DataType.UserSettings> { export interface UserSettings extends Model<DataType.UserSettings> {
fontFamilies?: string[]
fontFamily?: string fontFamily?: string
chosenFontFamily?: string
fontSize?: string fontSize?: string
backlink?: boolean backlink?: boolean
} }

View File

@@ -1,21 +1,21 @@
import { useMarkdown } from '@/hooks/useMarkdown.hook' import { useMarkdown } from "@/hooks/useMarkdown.hook"
import { prepareNoteCache } from '@/modules/note/cache/prepareNoteCache' import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
import { RepoFile } from '@/modules/repo/interfaces/RepoFile' import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
import { UserSettings } from '@/modules/repo/interfaces/UserSettings' import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
import { getOctokit } from '@/modules/repo/services/octo' import { getOctokit } from "@/modules/repo/services/octo"
export const getFiles = async ( export const getFiles = async (
owner: string, owner: string,
repo: string repo: string,
): Promise<RepoFile[]> => { ): Promise<RepoFile[]> => {
if (!owner || !repo) { if (!owner || !repo) {
return [] return []
} }
const octokit = await getOctokit() const octokit = await getOctokit()
const commits = await octokit.request('GET /repos/{owner}/{repo}/commits', { const commits = await octokit.request("GET /repos/{owner}/{repo}/commits", {
owner, owner,
repo repo,
}) })
const lastCommit = commits.data.shift() const lastCommit = commits.data.shift()
@@ -25,16 +25,16 @@ export const getFiles = async (
} }
const treeResponse = await octokit.request( const treeResponse = await octokit.request(
'GET /repos/{owner}/{repo}/git/trees/{tree_sha}', "GET /repos/{owner}/{repo}/git/trees/{tree_sha}",
{ {
owner, owner,
repo, repo,
tree_sha: lastCommit.commit.tree.sha, tree_sha: lastCommit.commit.tree.sha,
recursive: 'true' recursive: "true",
} },
) )
return treeResponse?.data.tree.filter((t) => t.type === 'blob') ?? [] return treeResponse?.data.tree.filter((t) => t.type === "blob") ?? []
} }
export const getCachedMainReadme = async (owner: string, repo: string) => { export const getCachedMainReadme = async (owner: string, repo: string) => {
@@ -60,7 +60,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
const { render } = useMarkdown() const { render } = useMarkdown()
const { getCachedNote, saveCacheNote } = prepareNoteCache( const { getCachedNote, saveCacheNote } = prepareNoteCache(
`${owner}-${repo}-README` `${owner}-${repo}-README`,
) )
try { try {
@@ -68,7 +68,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
const README = await octokit.repos.getReadme({ const README = await octokit.repos.getReadme({
owner, owner,
repo repo,
}) })
if (README) { if (README) {
@@ -90,9 +90,9 @@ export const getMainReadme = async (owner: string, repo: string) => {
export const getUserSettingsContent = async ( export const getUserSettingsContent = async (
user: string, user: string,
repo: string, repo: string,
files: RepoFile[] files: RepoFile[],
): Promise<UserSettings | null> => { ): Promise<Omit<UserSettings, "chosenFontFamily"> | null> => {
const configFile = files.find((file) => file.path === '.litenote.json') const configFile = files.find((file) => file.path === ".litenote.json")
if (!configFile?.sha) { if (!configFile?.sha) {
return null return null
@@ -110,7 +110,7 @@ export const getUserSettingsContent = async (
export const queryFileContent = async ( export const queryFileContent = async (
user: string, user: string,
repo: string, repo: string,
sha: string sha: string,
) => { ) => {
const octokit = await getOctokit() const octokit = await getOctokit()
@@ -119,12 +119,12 @@ export const queryFileContent = async (
} }
const file = await octokit.request( const file = await octokit.request(
'GET /repos/{owner}/{repo}/git/blobs/{file_sha}', "GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
{ {
owner: user, owner: user,
repo: repo, repo: repo,
file_sha: sha file_sha: sha,
} },
) )
return file?.data.content ?? null return file?.data.content ?? null

View File

@@ -1,17 +1,17 @@
import { defineStore } from 'pinia' import { defineStore } from "pinia"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { RepoFile } from '@/modules/repo/interfaces/RepoFile' import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
import { UserSettings } from '@/modules/repo/interfaces/UserSettings' import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
import { SavedRepo } from '@/modules/repo/models/SavedRepo' import { SavedRepo } from "@/modules/repo/models/SavedRepo"
import { import {
getCachedMainReadme, getCachedMainReadme,
getFiles, getFiles,
getMainReadme, getMainReadme,
getUserSettingsContent getUserSettingsContent,
} from '@/modules/repo/services/repo' } from "@/modules/repo/services/repo"
import { refreshToken } from '@/modules/user/service/signIn' import { refreshToken } from "@/modules/user/service/signIn"
interface State { interface State {
user: string user: string
@@ -22,15 +22,14 @@ interface State {
needToLogin: boolean needToLogin: boolean
} }
export const useUserRepoStore = defineStore({ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
id: 'USER_REPO_STATE',
state: (): State => ({ state: (): State => ({
user: '', user: "",
repo: '', repo: "",
files: [], files: [],
readme: undefined, readme: undefined,
userSettings: undefined, userSettings: undefined,
needToLogin: false needToLogin: false,
}), }),
actions: { actions: {
async setUserRepo(user: string, repo: string) { async setUserRepo(user: string, repo: string) {
@@ -39,7 +38,7 @@ export const useUserRepoStore = defineStore({
const savedRepoId = data.generateId(DataType.SavedRepo, `${user}-${repo}`) const savedRepoId = data.generateId(DataType.SavedRepo, `${user}-${repo}`)
const cachedSavedRepo = await data.get<DataType.SavedRepo, SavedRepo>( const cachedSavedRepo = await data.get<DataType.SavedRepo, SavedRepo>(
savedRepoId savedRepoId,
) )
if (cachedSavedRepo) { if (cachedSavedRepo) {
@@ -49,7 +48,7 @@ export const useUserRepoStore = defineStore({
try { try {
await refreshToken() await refreshToken()
} catch (error) { } catch (error) {
console.warn('impossible to refresh token', error) console.warn("impossible to refresh token", error)
} }
const userSettingsId = `UserSetting-${user}-${repo}` const userSettingsId = `UserSetting-${user}-${repo}`
@@ -69,20 +68,30 @@ export const useUserRepoStore = defineStore({
$type: DataType.SavedRepo, $type: DataType.SavedRepo,
repo, repo,
user, user,
files files,
}) })
this.files = files this.files = files
return getUserSettingsContent(user, repo, files) return getUserSettingsContent(user, repo, files)
}) })
.then((userSettings) => { .then((userSettings) => {
const chosenFontFamily = userSettings?.fontFamilies?.find(
(font) => font === this.userSettings?.chosenFontFamily,
)
? this.userSettings?.chosenFontFamily
: userSettings?.fontFamily
this.userSettings = userSettings this.userSettings = userSettings
if (userSettings) { if (!this.userSettings) {
data.update<DataType.UserSettings, UserSettings>({ return
...userSettings,
_id: userSettingsId
})
} }
this.userSettings.chosenFontFamily =
chosenFontFamily ?? this.userSettings.fontFamily
data.update<DataType.UserSettings, UserSettings>({
...this.userSettings,
_id: userSettingsId,
})
}) })
getCachedMainReadme(user, repo).then(async (cachedReadme) => { getCachedMainReadme(user, repo).then(async (cachedReadme) => {
@@ -101,11 +110,11 @@ export const useUserRepoStore = defineStore({
return return
} }
console.log('add file') console.log("add file")
const savedRepoId = data.generateId( const savedRepoId = data.generateId(
DataType.SavedRepo, DataType.SavedRepo,
`${this.user}-${this.repo}` `${this.user}-${this.repo}`,
) )
const newFiles = [...this.files.filter((f) => f.sha !== file.sha), file] const newFiles = [...this.files.filter((f) => f.sha !== file.sha), file]
data.update<DataType.SavedRepo, SavedRepo>({ data.update<DataType.SavedRepo, SavedRepo>({
@@ -113,19 +122,31 @@ export const useUserRepoStore = defineStore({
$type: DataType.SavedRepo, $type: DataType.SavedRepo,
repo: this.repo, repo: this.repo,
user: this.user, user: this.user,
files: newFiles files: newFiles,
}) })
this.files = newFiles this.files = newFiles
}, },
resetUserRepo() { resetUserRepo() {
this.user = '' this.user = ""
this.repo = '' this.repo = ""
this.resetFiles() this.resetFiles()
}, },
resetFiles() { resetFiles() {
this.files = [] this.files = []
this.readme = null this.readme = null
this.userSettings = undefined this.userSettings = undefined
} },
} setFontFamily(fontFamily: string) {
if (!this.userSettings) {
return
}
this.userSettings.chosenFontFamily = fontFamily
const userSettingsId = `UserSetting-${this.user}-${this.repo}`
data.update<DataType.UserSettings, UserSettings>({
...this.userSettings,
_id: userSettingsId,
})
},
},
}) })

View File

@@ -1,10 +1,10 @@
import { watchEffect } from 'vue' import { watchEffect } from "vue"
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store' import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { downloadGoogleFont } from '@/utils/downloadGoogleFont' import { downloadGoogleFont } from "@/utils/downloadGoogleFont"
const DEFAULT_FONT_POLICY = 'Courier Prime, monospace' const DEFAULT_FONT_POLICY = "Courier Prime, monospace"
const DEFAULT_FONT_SIZE = '16px' const DEFAULT_FONT_SIZE = "16px"
export const useUserSettings = () => { export const useUserSettings = () => {
const store = useUserRepoStore() const store = useUserRepoStore()
@@ -16,10 +16,10 @@ export const useUserSettings = () => {
const root = document.documentElement const root = document.documentElement
const fontFamily = store.userSettings?.fontFamily const fontFamily = store.userSettings?.chosenFontFamily
const fontSize = store.userSettings?.fontSize const fontSize = store.userSettings?.fontSize
downloadGoogleFont(fontFamily || DEFAULT_FONT_POLICY) downloadGoogleFont(fontFamily || DEFAULT_FONT_POLICY)
root.style.setProperty('--font-size', fontSize || DEFAULT_FONT_SIZE) root.style.setProperty("--font-size", fontSize || DEFAULT_FONT_SIZE)
}) })
} }

View File

@@ -1,4 +1,11 @@
import FontFaceObserver from 'fontfaceobserver' import FontFaceObserver from "fontfaceobserver"
const assembleFontLink = (font: string) => {
return `https://fonts.googleapis.com/css2?display=swap&family=${font.replaceAll(
" ",
"+",
)}`
}
export const downloadGoogleFont = async (font: string): Promise<void> => { export const downloadGoogleFont = async (font: string): Promise<void> => {
const href = assembleFontLink(font) const href = assembleFontLink(font)
@@ -7,21 +14,14 @@ export const downloadGoogleFont = async (font: string): Promise<void> => {
const existingLink = document.querySelector(`link[href="${href}"]`) const existingLink = document.querySelector(`link[href="${href}"]`)
if (!existingLink) { if (!existingLink) {
const link = document.createElement('link') const link = document.createElement("link")
link.href = href link.href = href
link.rel = 'stylesheet' link.rel = "stylesheet"
document.head.appendChild(link) document.head.appendChild(link)
} }
await new FontFaceObserver(font).load() await new FontFaceObserver(font).load()
document.documentElement.style.setProperty('--font-family', font) document.documentElement.style.setProperty("--font-family", font)
}
const assembleFontLink = (font: string) => {
return `https://fonts.googleapis.com/css2?display=swap&family=${font.replaceAll(
' ',
'+'
)}`
} }

View File

@@ -1,17 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { format } from 'date-fns' import { format } from "date-fns"
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from "vue"
import FluxNote from "@/components/FluxNote.vue"
import { useEditionMode } from "@/hooks/useEditionMode"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
import EditNote from "@/modules/note/components/EditNote.vue"
import { useFolderNotes } from "@/modules/note/hooks/useFolderNotes"
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
import FontChange from "@/components/FontChange.vue"
import FluxNote from '@/components/FluxNote.vue' const FLEETING_NOTES_FOLDER = ["inbox", "_inbox"]
import { useEditionMode } from '@/hooks/useEditionMode'
import { useGitHubContent } from '@/hooks/useGitHubContent.hook'
import { useRouteQueryStackedNotes } from '@/hooks/useRouteQueryStackedNotes.hook'
import { prepareNoteCache } from '@/modules/note/cache/prepareNoteCache'
import EditNote from '@/modules/note/components/EditNote.vue'
import { useFolderNotes } from '@/modules/note/hooks/useFolderNotes'
import { encodeUTF8ToBase64 } from '@/utils/decodeBase64ToUTF8'
const FLEETING_NOTES_FOLDER = ['inbox', '_inbox']
const props = defineProps<{ user: string; repo: string }>() const props = defineProps<{ user: string; repo: string }>()
const user = computed(() => props.user) const user = computed(() => props.user)
@@ -20,7 +20,7 @@ const repo = computed(() => props.repo)
const { addStackedNote } = useRouteQueryStackedNotes() const { addStackedNote } = useRouteQueryStackedNotes()
const { content } = useFolderNotes(FLEETING_NOTES_FOLDER) const { content } = useFolderNotes(FLEETING_NOTES_FOLDER)
const today = format(new Date(), 'yyyy-MM-dd') const today = format(new Date(), "yyyy-MM-dd")
const newContentPath = `_inbox/${today}.md` const newContentPath = `_inbox/${today}.md`
const initialContent = `` const initialContent = ``
const newContent = ref(initialContent) const newContent = ref(initialContent)
@@ -28,20 +28,20 @@ const { mode, toggleMode } = useEditionMode()
const { createFile } = useGitHubContent({ const { createFile } = useGitHubContent({
repo: repo.value, repo: repo.value,
user: user.value user: user.value,
}) })
const hasTodayNote = computed(() => content.value.includes(today)) const hasTodayNote = computed(() => content.value.includes(today))
watch(mode, async (newMode) => { watch(mode, async (newMode) => {
if (newMode === 'read' && newContent.value.trim() !== initialContent) { if (newMode === "read" && newContent.value.trim() !== initialContent) {
const content = `# ${new Date().toLocaleDateString()}\n\n${ const content = `# ${new Date().toLocaleDateString()}\n\n${
newContent.value newContent.value
}` }`
const newSha = await createFile({ const newSha = await createFile({
content, content,
path: newContentPath path: newContentPath,
}) })
if (!newSha) { if (!newSha) {
@@ -52,10 +52,10 @@ watch(mode, async (newMode) => {
const { saveCacheNote } = prepareNoteCache(newSha, newContentPath) const { saveCacheNote } = prepareNoteCache(newSha, newContentPath)
await saveCacheNote(encodeUTF8ToBase64(content), { await saveCacheNote(encodeUTF8ToBase64(content), {
editedSha: newSha, editedSha: newSha,
path: newContentPath path: newContentPath,
}) })
addStackedNote('', newSha) addStackedNote("", newSha)
} }
}) })
</script> </script>
@@ -69,11 +69,14 @@ watch(mode, async (newMode) => {
:content="content" :content="content"
> >
<h3 class="subtitle is-3">Inbox</h3> <h3 class="subtitle is-3">Inbox</h3>
<div v-if="!hasTodayNote" class="columns"> <div v-if="!hasTodayNote">
<div class="column"> <div class="column">
<button class="btn btn-primary" @click="toggleMode"> <button class="btn btn-secondary" @click="toggleMode">
new fleeting note new fleeting note
</button> </button>
<div class="column">
<font-change />
</div>
</div> </div>
</div> </div>
<div v-if="mode === 'edit'"> <div v-if="mode === 'edit'">