Compare commits

...

5 Commits

Author SHA1 Message Date
Julien Calixte
c06253e509 chore: migrate oxlint disable comments 2026-03-28 09:52:25 +01:00
Julien Calixte
767093c008 chore: install migrate oxlint skill 2026-03-28 09:49:50 +01:00
Julien Calixte
3a32cb5948 feat: change place for atproto 2026-03-28 09:45:39 +01:00
Julien Calixte
5f48aa5690 chore: lint and fmt 2026-03-28 09:38:55 +01:00
Julien Calixte
8e8706e258 chore: init oxc 2026-03-28 09:34:04 +01:00
119 changed files with 1417 additions and 1053 deletions

View File

@@ -0,0 +1,8 @@
{
"name": "remanso-skills",
"version": "1.0.0",
"description": "Local skills for the Remanso project",
"author": {
"name": "julien"
}
}

View File

@@ -0,0 +1,196 @@
---
name: migrate-oxlint
description: Guide for migrating a project from ESLint to Oxlint. Use when asked to migrate, convert, or switch a JavaScript/TypeScript project's linter from ESLint to Oxlint.
---
This skill guides you through migrating a JavaScript/TypeScript project from ESLint to [Oxlint](https://oxc.rs/docs/guide/usage/linter/).
## Overview
Oxlint is a high-performance linter that implements many popular ESLint rules natively in Rust. It can be used alongside ESLint or as a full replacement.
An official migration tool is available, and will be used by this skill: [`@oxlint/migrate`](https://github.com/oxc-project/oxlint-migrate)
## Step 1: Run Automated Migration
Run the migration tool in the project root:
```bash
npx @oxlint/migrate
```
This reads your ESLint flat config (`eslint.config.js` for example) and generates a `.oxlintrc.json` file from it. It will find your ESLint config file automatically in most cases.
See options below for more info.
### Key Options
| Option | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `--type-aware` | Include type-aware rules from `@typescript-eslint` (will require the `oxlint-tsgolint` package to be installed after migrating) |
| `--with-nursery` | Include experimental rules still under development, may not be fully stable or consistent with ESLint equivalents |
| `--js-plugins [bool]` | Enable/disable ESLint plugin migration via `jsPlugins` (default: enabled) |
| `--details` | List rules that could not be migrated |
| `--replace-eslint-comments` | Convert all `// eslint-disable` comments to `// oxlint-disable` |
| `--output-file <file>` | Specify a different output path (default: `.oxlintrc.json`) |
If your ESLint config is not at the default location, pass the path explicitly:
```bash
npx @oxlint/migrate ./path/to/eslint.config.js
```
## Step 2: Review Generated Config
After migration, review the generated `.oxlintrc.json`.
### Plugin Mapping
The migration tool automatically maps ESLint plugins to oxlint's built-in equivalents. The following table is for reference when reviewing the generated config:
| ESLint Plugin | Oxlint Plugin Name |
| --------------------------------------------------- | ------------------ |
| `@typescript-eslint/eslint-plugin` | `typescript` |
| `eslint-plugin-react` / `eslint-plugin-react-hooks` | `react` |
| `eslint-plugin-import` / `eslint-plugin-import-x` | `import` |
| `eslint-plugin-unicorn` | `unicorn` |
| `eslint-plugin-jsx-a11y` | `jsx-a11y` |
| `eslint-plugin-react-perf` | `react-perf` |
| `eslint-plugin-promise` | `promise` |
| `eslint-plugin-jest` | `jest` |
| `@vitest/eslint-plugin` | `vitest` |
| `eslint-plugin-jsdoc` | `jsdoc` |
| `eslint-plugin-next` | `nextjs` |
| `eslint-plugin-node` | `node` |
| `eslint-plugin-vue` | `vue` |
Default plugins (enabled when `plugins` field is omitted): `unicorn`, `typescript`, `oxc`.
Setting the `plugins` array explicitly overrides these defaults.
ESLint core rules are usable in oxlint without needing to configure a plugin in the config file.
### Rule Categories
Oxlint groups rules into categories for bulk configuration, though only `correctness` is enabled by default:
```json
{
"categories": {
"correctness": "error",
"suspicious": "warn"
}
}
```
Available categories: `correctness` (default: enabled), `suspicious`, `pedantic`, `perf`, `style`, `restriction`, `nursery`.
Individual rule settings in `rules` override category settings.
`@oxlint/migrate` will turn `correctness` off to avoid enabling additional rules that weren't enabled by your ESLint config. You can choose to enable additional categories after migration if desired.
### Check Unmigrated Rules
Run with `--details` to see which ESLint rules could not be migrated:
```bash
npx @oxlint/migrate --details
```
Review the output and decide whether to keep ESLint for those rules or not. Some rules may be mentioned in the output from `--details` as having equivalents in oxlint that were not automatically mapped by the migration tool. In those cases, consider enabling the equivalent oxlint rule manually after migration.
## Step 3: Install Oxlint
Install the core oxlint package (use `yarn install`, `pnpm install`, `vp install`, `bun install`, etc. depending on your package manager):
```bash
npm install -D oxlint
```
If you want to add the `oxlint-tsgolint` package, if you intend to use type-aware rules that require TypeScript type information:
```bash
npm install -D oxlint-tsgolint
```
No other packages besides the above are needed by default, though you will need to keep/install any additional ESLint plugins that were migrated into `jsPlugins`. Do not add `@oxlint/migrate` to the package.json, it is meant for one-off usage.
## Step 4: Handle Unsupported Features
Some features require manual attention:
- Local plugins (relative path imports): Must be migrated manually to `jsPlugins`
- `eslint-plugin-prettier`: Supported, but very slow. It is recommended to use [oxfmt](https://oxc.rs/docs/guide/usage/formatter) instead, or switch to `prettier --check` as a separate step alongside oxlint.
- `settings` in override configs: Oxlint does not support `settings` inside `overrides` blocks.
- ESLint v9+ plugins: Not all work with oxlint's JS Plugins API, but the majority will.
### Local Plugins
If you have any custom ESLint rules in the project repo itself, you can migrate them manually after running the migration tool by adding them to the `jsPlugins` field in `.oxlintrc.json`:
```json
{
"jsPlugins": ["./path/to/my-plugin.js"],
"rules": {
"local-plugin/rule-name": "error"
}
}
```
### External ESLint Plugins
For ESLint plugins without a built-in oxlint equivalent, use the `jsPlugins` field to load them:
```json
{
"jsPlugins": ["eslint-plugin-custom"],
"rules": {
"custom/my-rule": "warn"
}
}
```
## Step 5: Update CI and Scripts
Replace ESLint commands with oxlint. Path arguments are optional; oxlint defaults to the current working directory.
```bash
# Before
npx eslint src/
npx eslint --fix src/
# After
npx oxlint src/
npx oxlint --fix src/
```
### Common CLI Options
| ESLint | oxlint equivalent |
| ------------------------- | ---------------------------------------------- |
| `eslint .` | `oxlint` (default: lints the cwd) |
| `eslint src/` | `oxlint src/` |
| `eslint --fix` | `oxlint --fix` |
| `eslint --max-warnings 0` | `oxlint --deny-warnings` or `--max-warnings 0` |
| `eslint --format json` | `oxlint --format json` |
Additional oxlint options:
- `--tsconfig <path>`: Specify tsconfig.json path, likely unnecessary unless you have a non-standard name for `tsconfig.json`.
## Tips
- You can run alongside ESLint if necessary: Oxlint is designed to complement ESLint during migration, but with JS Plugins many projects can switch over fully without losing many rules.
- Disable comments work: `// eslint-disable` and `// eslint-disable-next-line` comments are supported by oxlint. Use `--replace-eslint-comments` when running @oxlint/migrate to convert them to `// oxlint-disable` equivalents if desired.
- List available rules: Run `npx oxlint --rules` to see all supported rules, or refer to the [rule documentation](https://oxc.rs/docs/guide/usage/linter/rules.html).
- Schema support: Add `"$schema": "./node_modules/oxlint/configuration_schema.json"` to `.oxlintrc.json` for editor autocompletion if the migration tool didn't do it automatically.
- Output formats: `default`, `stylish`, `json`, `github`, `gitlab`, `junit`, `checkstyle`, `unix`
- Ignore files: `.eslintignore` is supported by oxlint if you have it, but it's recommended to move any ignore patterns into the `ignorePatterns` field in `.oxlintrc.json` for consistency and simplicity. All files and paths ignored via a `.gitignore` file will be ignored by oxlint by default as well.
- If you ran the migration tool multiple times, remove the `.oxlintrc.json.bak` backup file created by the migration tool once you've finished migrating.
- If you are not using any JS Plugins and have replaced your ESLint configuration, you can remove all ESLint packages from your project dependencies.
- Ensure your editor is configured to use oxlint instead of ESLint for linting and error reporting. You may want to install the Oxc extension for your preferred editor. See https://oxc.rs/docs/guide/usage/linter/editors.html for more details.
## References
- [CLI Reference](https://oxc.rs/docs/guide/usage/linter/cli.html)
- [Config File Reference](https://oxc.rs/docs/guide/usage/linter/config-file-reference.html)
- [Complete Oxlint rule list and docs](https://oxc.rs/docs/guide/usage/linter/rules.html)

View File

@@ -0,0 +1,14 @@
{
"name": "remanso-local",
"description": "Local plugins for the Remanso project",
"owner": {
"name": "julien"
},
"plugins": [
{
"name": "remanso-skills",
"description": "Local skills for the Remanso project (migrate-oxlint, etc.)",
"source": "./.agents"
}
]
}

13
.claude/settings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extraKnownMarketplaces": {
"remanso-local": {
"source": {
"source": "directory",
"path": "."
}
}
},
"enabledPlugins": {
"remanso-skills@remanso-local": true
}
}

View File

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

9
.oxfmtrc.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"semi": false,
"singleQuote": false,
"trailingComma": "none",
"printWidth": 80,
"sortPackageJson": false,
"ignorePatterns": []
}

40
.oxlintrc.json Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript"],
"jsPlugins": [
"eslint-plugin-prettier-vue",
"eslint-plugin-simple-import-sort",
"eslint-plugin-unused-imports"
],
"categories": {
"correctness": "off"
},
"env": {
"builtin": true
},
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "vue-demi",
"importNames": ["computed"],
"message": "Please use computed from vue instead."
}
]
}
],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error"
},
"overrides": [
{
"files": ["**/*.vue"],
"rules": {
"unused-imports/no-unused-imports": "off"
}
}
]
}

View File

@@ -1,3 +0,0 @@
{
"semi": false
}

View File

@@ -5,6 +5,7 @@
import { readFileSync, writeFileSync } from "fs" import { readFileSync, writeFileSync } from "fs"
import { join } from "path" import { join } from "path"
import { commitTheme } from "./change-theme" import { commitTheme } from "./change-theme"
// Chemins vers les fichiers // Chemins vers les fichiers
@@ -28,7 +29,7 @@ let themeConfigContent = readFileSync(themeConfigPath, "utf8")
// Remplacer la valeur du thème sombre // Remplacer la valeur du thème sombre
themeConfigContent = themeConfigContent.replace( themeConfigContent = themeConfigContent.replace(
/dark:\s*['"][^'"]*['"],/, /dark:\s*['"][^'"]*['"],/,
`dark: '${newTheme}',`, `dark: '${newTheme}',`
) )
// Écrire le contenu mis à jour dans le fichier // Écrire le contenu mis à jour dans le fichier
@@ -38,7 +39,7 @@ writeFileSync(themeConfigPath, themeConfigContent)
let appCssContent = readFileSync(appCssPath, "utf8") let appCssContent = readFileSync(appCssPath, "utf8")
appCssContent = appCssContent.replace( appCssContent = appCssContent.replace(
/(\s+)([a-zA-Z0-9-]+)(\s+--prefersdark;)/, /(\s+)([a-zA-Z0-9-]+)(\s+--prefersdark;)/,
`$1${newTheme}$3`, `$1${newTheme}$3`
) )
writeFileSync(appCssPath, appCssContent) writeFileSync(appCssPath, appCssContent)

View File

@@ -5,6 +5,7 @@
import { readFileSync, writeFileSync } from "fs" import { readFileSync, writeFileSync } from "fs"
import { join } from "path" import { join } from "path"
import { commitTheme } from "./change-theme" import { commitTheme } from "./change-theme"
// Chemins vers les fichiers // Chemins vers les fichiers
@@ -29,7 +30,7 @@ let themeConfigContent = readFileSync(themeConfigPath, "utf8")
// Remplacer la valeur du thème clair // Remplacer la valeur du thème clair
themeConfigContent = themeConfigContent.replace( themeConfigContent = themeConfigContent.replace(
/light:\s*['"][^'"]*['"],/, /light:\s*['"][^'"]*['"],/,
`light: '${newTheme}',`, `light: '${newTheme}',`
) )
// Écrire le contenu mis à jour dans le fichier // Écrire le contenu mis à jour dans le fichier
@@ -39,7 +40,7 @@ writeFileSync(themeConfigPath, themeConfigContent)
let indexContent = readFileSync(indexPath, "utf8") let indexContent = readFileSync(indexPath, "utf8")
indexContent = indexContent.replace( indexContent = indexContent.replace(
/data-theme="[^"]*"/, /data-theme="[^"]*"/,
`data-theme="${newTheme}"`, `data-theme="${newTheme}"`
) )
writeFileSync(indexPath, indexContent) writeFileSync(indexPath, indexContent)
@@ -47,7 +48,7 @@ writeFileSync(indexPath, indexContent)
let appCssContent = readFileSync(appCssPath, "utf8") let appCssContent = readFileSync(appCssPath, "utf8")
appCssContent = appCssContent.replace( appCssContent = appCssContent.replace(
/(\s+)([a-zA-Z0-9-]+)(\s+--default,)/, /(\s+)([a-zA-Z0-9-]+)(\s+--default,)/,
`$1${newTheme}$3`, `$1${newTheme}$3`
) )
writeFileSync(appCssPath, appCssContent) writeFileSync(appCssPath, appCssContent)

View File

@@ -1,3 +1,3 @@
module.exports = { module.exports = {
presets: ['@vue/cli-plugin-babel/preset'] presets: ["@vue/cli-plugin-babel/preset"]
} }

View File

@@ -26,11 +26,10 @@ Rolldown's minifier drops the `while(`/`for(;` keyword when a `while (x in globa
File: `node_modules/@ark/schema/out/shared/registry.js` File: `node_modules/@ark/schema/out/shared/registry.js`
```js ```js
let _registryName = "$ark"; let _registryName = "$ark"
let suffix = 2; let suffix = 2
while (_registryName in globalThis) while (_registryName in globalThis) _registryName = `$ark${suffix++}`
_registryName = `$ark${suffix++}`; export const registryName = _registryName
export const registryName = _registryName;
``` ```
## Actual minified output ## Actual minified output
@@ -44,7 +43,10 @@ The `while(` keyword is missing. The orphaned `)` is a syntax error.
## Expected output ## Expected output
```js ```js
var dn=`$ark`,fn=2;for(;dn in globalThis;)dn=`$ark${fn++}`;var pn=dn; var dn = `$ark`,
fn = 2
for (; dn in globalThis; ) dn = `$ark${fn++}`
var pn = dn
``` ```
## Impact ## Impact

View File

@@ -9,9 +9,9 @@ status = 200
[[headers]] [[headers]]
for = "/client-metadata.json" for = "/client-metadata.json"
[headers.values] [headers.values]
Access-Control-Allow-Origin = "*" Access-Control-Allow-Origin = "*"
Content-Type = "application/json" Content-Type = "application/json"
[[redirects]] [[redirects]]
from = "/client-metadata.json" from = "/client-metadata.json"

View File

@@ -8,7 +8,10 @@
"serve": "vite preview", "serve": "vite preview",
"test": "vitest", "test": "vitest",
"types": "tsc --noEmit", "types": "tsc --noEmit",
"lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore --fix src", "lint": "oxlint",
"lint:fix": "oxlint --fix",
"fmt": "oxfmt",
"fmt:check": "oxfmt --check",
"prepare": "husky", "prepare": "husky",
"theme:light": "esno _scripts/change-theme-light.ts", "theme:light": "esno _scripts/change-theme-light.ts",
"theme:dark": "esno _scripts/change-theme-dark.ts", "theme:dark": "esno _scripts/change-theme-dark.ts",
@@ -60,31 +63,25 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.28.5", "@babel/core": "^7.28.5",
"@rushstack/eslint-patch": "^1.14.1",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@types/fontfaceobserver": "^2.1.3", "@types/fontfaceobserver": "^2.1.3",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node": "^22.15.24", "@types/node": "^22.15.24",
"@types/pouchdb-browser": "^6.1.5", "@types/pouchdb-browser": "^6.1.5",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.16.0",
"@typescript-eslint/eslint-plugin": "^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.28", "@vue/compiler-sfc": "^3.5.28",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.6.0",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.24",
"daisyui": "^5.5.18", "daisyui": "^5.5.18",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"eslint": "^8.57.1",
"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": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.4.1", "eslint-plugin-unused-imports": "^4.4.1",
"eslint-plugin-vue": "^10.8.0",
"esno": "^4.8.0", "esno": "^4.8.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-vue": "^1.1.2", "prettier-vue": "^1.1.2",
"sass": "^1.98.0", "sass": "^1.98.0",

798
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
module.exports = { module.exports = {
plugins: { "@tailwindcss/postcss": {}, autoprefixer: {} }, plugins: { "@tailwindcss/postcss": {}, autoprefixer: {} }
} }

View File

@@ -2,9 +2,7 @@
"client_id": "https://remanso.space/client-metadata.json", "client_id": "https://remanso.space/client-metadata.json",
"client_name": "Remanso", "client_name": "Remanso",
"client_uri": "https://remanso.space", "client_uri": "https://remanso.space",
"redirect_uris": [ "redirect_uris": ["https://remanso.space/"],
"https://remanso.space/"
],
"scope": "atproto transition:generic", "scope": "atproto transition:generic",
"grant_types": ["authorization_code", "refresh_token"], "grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"], "response_types": ["code"],

View File

@@ -1,9 +1,9 @@
import { import {
defineConfig, defineConfig,
minimal2023Preset as preset, minimal2023Preset as preset
} from "@vite-pwa/assets-generator/config" } from "@vite-pwa/assets-generator/config"
export default defineConfig({ export default defineConfig({
preset, preset,
images: ["public/favicon.png"], images: ["public/favicon.png"]
}) })

10
skills-lock.json Normal file
View File

@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"migrate-oxlint": {
"source": "oxc-project/oxc",
"sourceType": "github",
"computedHash": "80ce5201b1ef52d6cabe553a4cacfd6e1db97bad99618216b9cf9318d11d7e64"
}
}
}

View File

@@ -5,5 +5,5 @@ export const op = new OpenPanel({
clientId: "038a6aac-19bb-4a7f-9aae-2d0201fead5b", clientId: "038a6aac-19bb-4a7f-9aae-2d0201fead5b",
trackScreenViews: true, trackScreenViews: true,
trackOutgoingLinks: true, trackOutgoingLinks: true,
trackAttributes: true, trackAttributes: true
}) })

View File

@@ -1,4 +1,4 @@
import { createEventBus } from 'retrobus' import { createEventBus } from "retrobus"
interface EventBusParams { interface EventBusParams {
fileSha: string fileSha: string

View File

@@ -1,4 +1,4 @@
import { createEventBus } from 'retrobus' import { createEventBus } from "retrobus"
interface EventBusParams { interface EventBusParams {
user: string user: string

View File

@@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onBeforeMount, ref } from 'vue' import { onBeforeMount, ref } from "vue"
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router"
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { signIn } from '@/modules/user/service/signIn' import { signIn } from "@/modules/user/service/signIn"
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -16,14 +16,14 @@ onBeforeMount(async () => {
if (code) { if (code) {
const token = await signIn(code.toString()) const token = await signIn(code.toString())
if ('error' in token) { if ("error" in token) {
hasError.value = true hasError.value = true
} else { } else {
token.access_token token.access_token
saveCredentials(token) saveCredentials(token)
} }
router.replace({ name: 'Home' }) router.replace({ name: "Home" })
} }
}) })
</script> </script>

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter, type RouteLocationRaw } from "vue-router" import { type RouteLocationRaw, useRouter } from "vue-router"
const props = withDefaults( const props = withDefaults(
defineProps<{ fallback?: RouteLocationRaw; preferFallback?: boolean }>(), defineProps<{ fallback?: RouteLocationRaw; preferFallback?: boolean }>(),
{ preferFallback: true }, { preferFallback: true }
) )
const router = useRouter() const router = useRouter()

View File

@@ -6,9 +6,10 @@ import {
onMounted, onMounted,
onUnmounted, onUnmounted,
toRefs, toRefs,
watch, watch
} from "vue" } from "vue"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
import StackedNote from "@/components/StackedNote.vue" 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"
@@ -19,10 +20,9 @@ import { useVisitRepo } from "@/modules/history/hooks/useVisitRepo.hook"
import CacheAllNotes from "@/modules/note/components/CacheAllNote.vue" import CacheAllNotes from "@/modules/note/components/CacheAllNote.vue"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store" import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { useUserSettings } from "@/modules/user/hooks/useUserSettings.hook" import { useUserSettings } from "@/modules/user/hooks/useUserSettings.hook"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
const HeaderNote = defineAsyncComponent( const HeaderNote = defineAsyncComponent(
() => import("@/components/HeaderNote.vue"), () => import("@/components/HeaderNote.vue")
) )
const props = withDefaults( const props = withDefaults(
@@ -38,8 +38,8 @@ const props = withDefaults(
content: null, content: null,
parseContent: true, parseContent: true,
withContent: true, withContent: true,
withHeader: true, withHeader: true
}, }
) )
const user = computed(() => props.user) const user = computed(() => props.user)
@@ -61,7 +61,7 @@ const renderedContent = computed(() =>
? props.parseContent ? props.parseContent
? toHTML(props.content) ? toHTML(props.content)
: props.content : props.content
: store.readme, : store.readme
) )
const isLoading = computed(() => renderedContent.value === undefined) const isLoading = computed(() => renderedContent.value === undefined)
@@ -73,7 +73,7 @@ watch(
await nextTick() await nextTick()
listenToClick() listenToClick()
}, },
{ immediate: true }, { immediate: true }
) )
watch( watch(
@@ -81,7 +81,7 @@ watch(
() => { () => {
store.setUserRepo(props.user, props.repo) store.setUserRepo(props.user, props.repo)
}, },
{ immediate: true }, { immediate: true }
) )
onMounted(() => visitRepo()) onMounted(() => visitRepo())

View File

@@ -9,7 +9,7 @@ const store = useUserRepoStore()
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? []) const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
const sortedFontFamilies = computed(() => const sortedFontFamilies = computed(() =>
[...fontFamilies.value].sort((a, b) => a.localeCompare(b)), [...fontFamilies.value].sort((a, b) => a.localeCompare(b))
) )
const fontSizes = Array.from({ length: 7 }, (_, i) => `${9 + i * 2}pt`) const fontSizes = Array.from({ length: 7 }, (_, i) => `${9 + i * 2}pt`)
</script> </script>

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useRouter } from 'vue-router' import { useRouter } from "vue-router"
const { push } = useRouter() const { push } = useRouter()
const back = () => const back = () =>
push({ push({
name: 'Home' name: "Home"
}) })
</script> </script>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
const url = new URL('https://github.com/login/oauth/authorize') const url = new URL("https://github.com/login/oauth/authorize")
url.searchParams.append('client_id', 'Iv1.87be14adcc912fa0') url.searchParams.append("client_id", "Iv1.87be14adcc912fa0")
url.searchParams.append('redirect_uri', location.href) url.searchParams.append("redirect_uri", location.href)
url.searchParams.append('scope', 'repo') url.searchParams.append("scope", "repo")
</script> </script>
<template> <template>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { toShortDid } from "@/modules/atproto/shortDid"
import { slugify } from "@/utils/slugify"
import { vInfiniteScroll } from "@vueuse/components" import { vInfiniteScroll } from "@vueuse/components"
import { toShortDid } from "@/modules/atproto/shortDid"
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { slugify } from "@/utils/slugify"
defineProps<{ defineProps<{
notes: PublicNoteListItem[] notes: PublicNoteListItem[]
canLoadMore: boolean canLoadMore: boolean
@@ -28,8 +29,8 @@ defineSlots<{
params: { params: {
shortDid: toShortDid(note.did), shortDid: toShortDid(note.did),
rkey: note.rkey, rkey: note.rkey,
slug: slugify(note.title), slug: slugify(note.title)
}, }
}" }"
class="btn btn-link" class="btn btn-link"
>{{ note.title }}</router-link >{{ note.title }}</router-link

View File

@@ -26,8 +26,8 @@ const getStyle = (seed: string) => {
name: 'FluxNoteView', name: 'FluxNoteView',
params: { params: {
user: username, user: username,
repo: favoriteRepo.name, repo: favoriteRepo.name
}, }
}" }"
class="btn" class="btn"
:style="getStyle(`${favoriteRepo.name}-${username}`)" :style="getStyle(`${favoriteRepo.name}-${username}`)"

View File

@@ -3,15 +3,16 @@ import { ref } from "vue"
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook" import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
const { handle, isLoggedIn, isATProtoReady, signIn, signOut } = useATProtoLogin() const { handle, isLoggedIn, isATProtoReady, signIn, signOut } =
useATProtoLogin()
withDefaults( withDefaults(
defineProps<{ defineProps<{
withSignOut?: boolean withSignOut?: boolean
}>(), }>(),
{ {
withSignOut: true, withSignOut: true
}, }
) )
const inputHandle = ref("") const inputHandle = ref("")

View File

@@ -5,7 +5,7 @@ import {
nextTick, nextTick,
onMounted, onMounted,
ref, ref,
watch, watch
} from "vue" } from "vue"
import { useEditionMode } from "@/hooks/useEditionMode" import { useEditionMode } from "@/hooks/useEditionMode"
@@ -13,20 +13,20 @@ import { useFile } from "@/hooks/useFile.hook"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook" import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
import { useImages } from "@/hooks/useImages.hook" import { useImages } from "@/hooks/useImages.hook"
import { useLinks } from "@/hooks/useLinks.hook" import { useLinks } from "@/hooks/useLinks.hook"
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook" import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { useTitleNotes } from "@/hooks/useTitleNotes.hook" import { useTitleNotes } from "@/hooks/useTitleNotes.hook"
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store" import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8" import { encodeUTF8ToBase64 } from "@/utils/decodeBase64ToUTF8"
import { filenameToNoteTitle } from "@/utils/noteTitle" import { filenameToNoteTitle } from "@/utils/noteTitle"
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
const LinkedNotes = defineAsyncComponent( const LinkedNotes = defineAsyncComponent(
() => import("@/components/LinkedNotes.vue"), () => import("@/components/LinkedNotes.vue")
) )
const EditNote = defineAsyncComponent( const EditNote = defineAsyncComponent(
() => import("@/modules/note/components/EditNote.vue"), () => import("@/modules/note/components/EditNote.vue")
) )
const props = defineProps<{ const props = defineProps<{
@@ -50,7 +50,7 @@ const {
rawContent, rawContent,
getRawContent, getRawContent,
saveCacheNote, saveCacheNote,
getEditedSha, getEditedSha
} = useFile(sha) } = useFile(sha)
const initialRawContent = ref<string | null>(null) const initialRawContent = ref<string | null>(null)
const className = computed(() => `stacked-note-${props.index}`) const className = computed(() => `stacked-note-${props.index}`)
@@ -67,7 +67,7 @@ const breadcrumbs = computed(() => displayedTitle.value.split(" / "))
const { updateFile } = useGitHubContent({ const { updateFile } = useGitHubContent({
user: user.value, user: user.value,
repo: repo.value, repo: repo.value
}) })
onMounted(async () => { onMounted(async () => {
@@ -115,7 +115,7 @@ watch(mode, async (newMode) => {
const newSha = await updateFile({ const newSha = await updateFile({
content: rawContent.value, content: rawContent.value,
path: path.value, path: path.value,
sha: editedSha, sha: editedSha
}) })
if (!newSha) { if (!newSha) {
@@ -125,7 +125,7 @@ watch(mode, async (newMode) => {
} }
await saveCacheNote(encodeUTF8ToBase64(rawContent.value), { await saveCacheNote(encodeUTF8ToBase64(rawContent.value), {
editedSha: newSha, editedSha: newSha
}) })
initialRawContent.value = rawContent.value initialRawContent.value = rawContent.value
}) })
@@ -137,7 +137,7 @@ watch(mode, async (newMode) => {
:class="{ :class="{
[className]: true, [className]: true,
overlay: displayNoteOverlay, overlay: displayNoteOverlay,
[`note-${sha}`]: true, [`note-${sha}`]: true
}" }"
> >
<a <a
@@ -198,7 +198,9 @@ watch(mode, async (newMode) => {
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" /> <path
d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"
/>
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /> <path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M14 4l0 4l-6 0l0 -4" /> <path d="M14 4l0 4l-6 0l0 -4" />
</svg> </svg>

View File

@@ -1,18 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computedAsync } from "@vueuse/core"
import { computed, nextTick, ref, watch } from "vue" import { computed, nextTick, ref, watch } from "vue"
import { useRoute } from "vue-router" import { useRoute } from "vue-router"
import { errorMessage } from "@/utils/notif"
import SkeletonLoader from "@/components/SkeletonLoader.vue"
import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook" import { useATProtoLinks } from "@/hooks/useATProtoLinks.hook"
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook" import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.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 { getAuthor } from "@/modules/atproto/getAuthor"
import { fromShortDid } from "@/modules/atproto/shortDid" import { getUrl } from "@/modules/atproto/getUrl"
import { PublicNoteRecord } from "@/modules/atproto/publicNote.types" import { PublicNoteRecord } from "@/modules/atproto/publicNote.types"
import SkeletonLoader from "@/components/SkeletonLoader.vue" import { fromShortDid } from "@/modules/atproto/shortDid"
import { withATProtoImages } from "@/modules/atproto/withATProtoImages"
import { errorMessage } from "@/utils/notif"
const props = defineProps<{ const props = defineProps<{
didrkey: string didrkey: string
@@ -28,7 +29,7 @@ const index = computed(() => props.index)
const author = computedAsync(async () => getAuthor(did.value)) const author = computedAsync(async () => getAuthor(did.value))
const url = computedAsync(async () => const url = computedAsync(async () =>
getUrl({ did: did.value, rkey: rkey.value }), getUrl({ did: did.value, rkey: rkey.value })
) )
const className = computed(() => `stacked-note-${props.index}`) const className = computed(() => `stacked-note-${props.index}`)
@@ -36,13 +37,13 @@ const titleClassName = computed(() => `title-${className.value}`)
const route = useRoute() const route = useRoute()
const mainNoteId = computed( const mainNoteId = computed(
() => `${route.params.shortDid}-${route.params.rkey}`, () => `${route.params.shortDid}-${route.params.rkey}`
) )
const { scrollToFocusedNote } = useRouteQueryStackedNotes() const { scrollToFocusedNote } = useRouteQueryStackedNotes()
const { listenToClick } = useATProtoLinks(className.value, { const { listenToClick } = useATProtoLinks(className.value, {
currentAtUri: didrkey, currentAtUri: didrkey,
mainNoteId, mainNoteId
}) })
const { displayNoteOverlay } = useNoteOverlay(className.value, index) const { displayNoteOverlay } = useNoteOverlay(className.value, index)
@@ -69,10 +70,10 @@ const content = computed(() =>
? toHTML( ? toHTML(
withATProtoImages(noteRecord.value.value.content, { withATProtoImages(noteRecord.value.value.content, {
pds: author.value.pds, pds: author.value.pds,
did: did.value, did: did.value
}), })
) )
: "", : ""
) )
watch( watch(
@@ -81,7 +82,7 @@ watch(
await nextTick() await nextTick()
listenToClick() listenToClick()
}, },
{ immediate: true }, { immediate: true }
) )
</script> </script>
@@ -91,7 +92,7 @@ watch(
:class="{ :class="{
[className]: true, [className]: true,
overlay: displayNoteOverlay, overlay: displayNoteOverlay,
[`note-${classNameId}`]: true, [`note-${classNameId}`]: true
}" }"
> >
<a <a

View File

@@ -17,9 +17,6 @@ const { userInput, repoInput, submit } = useForm()
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" /> <img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
Remanso Remanso
</h1> </h1>
<p>
<sign-in-atproto :with-sign-out="false" />
</p>
<repo-list /> <repo-list />
@@ -74,10 +71,11 @@ const { userInput, repoInput, submit } = useForm()
<a href="https://apoena.dev" target="_blank" rel="noopener noreferrer" <a href="https://apoena.dev" target="_blank" rel="noopener noreferrer"
>apoena</a >apoena</a
> >
<sign-in-atproto :with-sign-out="false" />
<router-link <router-link
:to="{ :to="{
name: 'FluxNoteView', name: 'FluxNoteView',
params: { user: 'remanso-space', repo: 'getting-started' }, params: { user: 'remanso-space', repo: 'getting-started' }
}" }"
class="btn btn-sm" class="btn btn-sm"
>Get started</router-link >Get started</router-link

View File

@@ -4,8 +4,8 @@ export const getNoteWidth = () => {
if (cached === undefined) { if (cached === undefined) {
cached = parseInt( cached = parseInt(
getComputedStyle(document.documentElement).getPropertyValue( getComputedStyle(document.documentElement).getPropertyValue(
"--note-width", "--note-width"
), )
) )
} }
return cached return cached

View File

@@ -1,11 +1,11 @@
export enum DataType { export enum DataType {
GithubAccessToken = 'GithubAccessToken', GithubAccessToken = "GithubAccessToken",
FavoriteRepo = 'FavoriteRepo', FavoriteRepo = "FavoriteRepo",
SavedRepo = 'SavedRepo', SavedRepo = "SavedRepo",
Note = 'Note', Note = "Note",
BacklinkNote = 'BacklinkNote', BacklinkNote = "BacklinkNote",
RepetitionCard = 'RepetitionCard', RepetitionCard = "RepetitionCard",
History = 'History', History = "History",
UserSettings = 'UserSettings', UserSettings = "UserSettings",
AtprotoSession = 'AtprotoSession' AtprotoSession = "AtprotoSession"
} }

View File

@@ -15,13 +15,13 @@ interface GetAllParams {
} }
class Data { class Data {
// eslint-disable-next-line @typescript-eslint/ban-types // oxlint-disable-next-line typescript/ban-types
private readonly locale: PouchDB.Database<{}> | null = null private readonly locale: PouchDB.Database<{}> | null = null
constructor() { constructor() {
try { try {
this.locale = new PouchDb("remanso", { this.locale = new PouchDb("remanso", {
adapter: "indexeddb", adapter: "indexeddb"
}) })
} catch (error) { } catch (error) {
console.warn("data error", error) console.warn("data error", error)
@@ -40,7 +40,7 @@ class Data {
} }
public async update<DT extends DataType, T extends Model<DT>>( public async update<DT extends DataType, T extends Model<DT>>(
model: T, model: T
): Promise<boolean> { ): Promise<boolean> {
try { try {
if (!model._id) { if (!model._id) {
@@ -71,7 +71,7 @@ class Data {
} }
const result = await this.locale?.put({ const result = await this.locale?.put({
...doc, ...doc,
_deleted: true, _deleted: true
}) })
return result?.ok ?? false return result?.ok ?? false
} catch { } catch {
@@ -80,7 +80,7 @@ class Data {
} }
public async get<DT extends DataType, T extends Model<DT>>( public async get<DT extends DataType, T extends Model<DT>>(
id: string, id: string
): Promise<T | null> { ): Promise<T | null> {
try { try {
return ((await this.locale?.get(id)) as T) || null return ((await this.locale?.get(id)) as T) || null
@@ -91,7 +91,7 @@ class Data {
public async getOrCreate<DT extends DataType, T extends Model<DT>>( public async getOrCreate<DT extends DataType, T extends Model<DT>>(
id: string, id: string,
initialValue: T, initialValue: T
): Promise<T> { ): Promise<T> {
const element = await this.get<DT, T>(id) const element = await this.get<DT, T>(id)
@@ -108,7 +108,7 @@ class Data {
prefix, prefix,
includeDocs = true, includeDocs = true,
includeAttachments = false, includeAttachments = false,
keys = [], keys = []
}: GetAllParams): Promise<T[]> { }: GetAllParams): Promise<T[]> {
if (!this.locale) { if (!this.locale) {
return [] return []
@@ -118,7 +118,7 @@ class Data {
const response = await this.locale.allDocs({ const response = await this.locale.allDocs({
include_docs: includeDocs, include_docs: includeDocs,
attachments: includeAttachments, attachments: includeAttachments,
keys: keys.map((key) => this.generateId(prefix, key)), keys: keys.map((key) => this.generateId(prefix, key))
}) })
if (includeDocs) { if (includeDocs) {
@@ -148,7 +148,7 @@ class Data {
include_docs: includeDocs, include_docs: includeDocs,
attachments: includeAttachments, attachments: includeAttachments,
startkey: prefix ? prefix : undefined, startkey: prefix ? prefix : undefined,
endkey: prefix ? `${prefix}\ufff0` : undefined, endkey: prefix ? `${prefix}\ufff0` : undefined
}) })
return response.rows.map((row) => row.doc) as T[] return response.rows.map((row) => row.doc) as T[]

View File

@@ -1,5 +1,5 @@
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 AtprotoSession extends Model<DataType.AtprotoSession> { export interface AtprotoSession extends Model<DataType.AtprotoSession> {
did: string did: string

View File

@@ -1,5 +1,5 @@
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 GithubAccessToken extends Model<DataType.GithubAccessToken> { export interface GithubAccessToken extends Model<DataType.GithubAccessToken> {
username: string username: string

View File

@@ -1,5 +1,5 @@
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 History extends Model<DataType.History> { export interface History extends Model<DataType.History> {
repos: ReadonlyArray<{ repos: ReadonlyArray<{

View File

@@ -1,4 +1,4 @@
import { DataType } from '../DataType.enum' import { DataType } from "../DataType.enum"
export interface Model<DT extends DataType> { export interface Model<DT extends DataType> {
_id?: string _id?: string

View File

@@ -1,17 +1,17 @@
import { ComputedRef, onUnmounted, Ref, toValue } from "vue" import { ComputedRef, onUnmounted, Ref, toValue } from "vue"
import { isExternalLink } from "@/utils/link"
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook" import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { parseAtUri } from "@/modules/atproto/parseAtUri" import { parseAtUri } from "@/modules/atproto/parseAtUri"
import { toShortDid } from "@/modules/atproto/shortDid" import { toShortDid } from "@/modules/atproto/shortDid"
import { router } from "@/router/router" import { router } from "@/router/router"
import { isExternalLink } from "@/utils/link"
export const useATProtoLinks = ( export const useATProtoLinks = (
className: ComputedRef<string> | string, className: ComputedRef<string> | string,
options: { options: {
currentAtUri?: Ref<string> | string | ComputedRef<string> currentAtUri?: Ref<string> | string | ComputedRef<string>
mainNoteId: Ref<string> | string | ComputedRef<string> mainNoteId: Ref<string> | string | ComputedRef<string>
}, }
) => { ) => {
const { addStackedNote, scrollToFocusedNote } = useRouteQueryStackedNotes() const { addStackedNote, scrollToFocusedNote } = useRouteQueryStackedNotes()
const { currentAtUri, mainNoteId } = options const { currentAtUri, mainNoteId } = options
@@ -38,7 +38,7 @@ export const useATProtoLinks = (
if (href.startsWith(window.location.origin)) { if (href.startsWith(window.location.origin)) {
const { params } = router.resolve( const { params } = router.resolve(
href.replace(window.location.origin, ""), href.replace(window.location.origin, "")
) )
if (!params.shortDid || !params.rkey) { if (!params.shortDid || !params.rkey) {
@@ -57,7 +57,7 @@ export const useATProtoLinks = (
addStackedNote( addStackedNote(
toValue(currentAtUri) ?? "", toValue(currentAtUri) ?? "",
noteId, noteId,
`${params.shortDid}-${params.rkey}`, `${params.shortDid}-${params.rkey}`
) )
return return
} }
@@ -111,6 +111,6 @@ export const useATProtoLinks = (
}) })
return { return {
listenToClick, listenToClick
} }
} }

View File

@@ -1,8 +1,16 @@
import { computed, ref } from 'vue' import { computed, ref } from "vue"
import { getAuthor } from '@/modules/atproto/getAuthor' import { getAuthor } from "@/modules/atproto/getAuthor"
import { restoreSession, sdkSignOut, signInWithHandle } from '@/modules/atproto/service/atprotoOAuth' import {
import { clearSession, loadSession, saveSession } from '@/modules/atproto/service/atprotoSession' restoreSession,
sdkSignOut,
signInWithHandle
} from "@/modules/atproto/service/atprotoOAuth"
import {
clearSession,
loadSession,
saveSession
} from "@/modules/atproto/service/atprotoSession"
const did = ref<string | null>(null) const did = ref<string | null>(null)
const handle = ref<string | null>(null) const handle = ref<string | null>(null)
@@ -12,20 +20,24 @@ let init = true
const initializeAuth = async () => { const initializeAuth = async () => {
// Load cached session from IndexedDB first (fast, local) so the UI can render immediately // Load cached session from IndexedDB first (fast, local) so the UI can render immediately
const stored = await loadSession() const stored = await loadSession()
did.value = stored?.did ?? '' did.value = stored?.did ?? ""
handle.value = stored?.handle ?? '' handle.value = stored?.handle ?? ""
// Then restore OAuth session in the background (may involve network) // Then restore OAuth session in the background (may involve network)
const session = await restoreSession() const session = await restoreSession()
if (session) { if (session) {
const author = await getAuthor(session.did) const author = await getAuthor(session.did)
const resolvedHandle = author?.handle ?? '' const resolvedHandle = author?.handle ?? ""
did.value = session.did did.value = session.did
handle.value = resolvedHandle handle.value = resolvedHandle
await saveSession(session.did, resolvedHandle) await saveSession(session.did, resolvedHandle)
window.history.replaceState(null, '', window.location.pathname + window.location.search) window.history.replaceState(
null,
"",
window.location.pathname + window.location.search
)
} }
} }
@@ -47,8 +59,8 @@ export const useATProtoLogin = () => {
await sdkSignOut(did.value) await sdkSignOut(did.value)
} }
await clearSession() await clearSession()
did.value = '' did.value = ""
handle.value = '' handle.value = ""
} }
return { return {
@@ -57,6 +69,6 @@ export const useATProtoLogin = () => {
isLoggedIn, isLoggedIn,
isATProtoReady, isATProtoReady,
signIn, signIn,
signOut, signOut
} }
} }

View File

@@ -1,10 +1,10 @@
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from "@vueuse/core"
import { ComputedRef, onUnmounted, toValue } from 'vue' import { ComputedRef, onUnmounted, toValue } from "vue"
import { backlinkEventBus } from '@/bus/backlinkEventBus' import { backlinkEventBus } from "@/bus/backlinkEventBus"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { BacklinkNote } from '@/modules/note/models/BacklinkNote' import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
export const useBacklinks = (sha: string | ComputedRef<string>) => { export const useBacklinks = (sha: string | ComputedRef<string>) => {
sha = toValue(sha) sha = toValue(sha)

View File

@@ -1,5 +1,6 @@
import { ref, Ref, toValue, onUnmounted } from "vue"
import { useDebounceFn } from "@vueuse/core" import { useDebounceFn } from "@vueuse/core"
import { onUnmounted, Ref, ref, toValue } from "vue"
import { useGitHubContent } from "@/hooks/useGitHubContent.hook" import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
const CHECKBOX_PATTERN = /\[([ xX])\]/g const CHECKBOX_PATTERN = /\[([ xX])\]/g
@@ -7,7 +8,7 @@ const CHECKBOX_PATTERN = /\[([ xX])\]/g
const setCheckboxInMarkdown = ( const setCheckboxInMarkdown = (
markdown: string, markdown: string,
index: number, index: number,
checked: boolean, checked: boolean
): string => { ): string => {
let currentIndex = 0 let currentIndex = 0
@@ -21,7 +22,7 @@ const setCheckboxInMarkdown = (
const findCheckboxIndex = ( const findCheckboxIndex = (
container: Element, container: Element,
checkbox: HTMLInputElement, checkbox: HTMLInputElement
): number => { ): number => {
const allCheckboxes = container.querySelectorAll('input[type="checkbox"]') const allCheckboxes = container.querySelectorAll('input[type="checkbox"]')
return Array.from(allCheckboxes).indexOf(checkbox) return Array.from(allCheckboxes).indexOf(checkbox)
@@ -34,7 +35,7 @@ export const useCheckboxCommit = ({
initialContent, initialContent,
initialSha, initialSha,
containerSelector, containerSelector,
debounceMs = 1000, debounceMs = 1000
}: { }: {
user: string user: string
repo: string repo: string
@@ -76,7 +77,7 @@ export const useCheckboxCommit = ({
const newSha = await updateFile({ const newSha = await updateFile({
content: pendingContent.value, content: pendingContent.value,
path: pathValue, path: pathValue,
sha: currentSha.value, sha: currentSha.value
}) })
if (newSha) { if (newSha) {
@@ -109,7 +110,7 @@ export const useCheckboxCommit = ({
pendingContent.value = setCheckboxInMarkdown( pendingContent.value = setCheckboxInMarkdown(
pendingContent.value, pendingContent.value,
index, index,
target.checked, target.checked
) )
hasPendingChanges.value = true hasPendingChanges.value = true
@@ -142,6 +143,6 @@ export const useCheckboxCommit = ({
isCommitting, isCommitting,
hasPendingChanges, hasPendingChanges,
syncContent, syncContent,
listenToCheckboxes, listenToCheckboxes
} }
} }

View File

@@ -1,18 +1,18 @@
import { watch } from 'vue' import { watch } from "vue"
import { backlinkEventBus } from '@/bus/backlinkEventBus' import { backlinkEventBus } from "@/bus/backlinkEventBus"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { useFile } from '@/hooks/useFile.hook' import { useFile } from "@/hooks/useFile.hook"
import { Backlink } from '@/modules/note/models/Backlink' import { Backlink } from "@/modules/note/models/Backlink"
import { BacklinkNote } from '@/modules/note/models/BacklinkNote' import { BacklinkNote } from "@/modules/note/models/BacklinkNote"
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 { isExternalLink } from '@/utils/link' import { isExternalLink } from "@/utils/link"
import { filenameToNoteTitle } from '@/utils/noteTitle' import { filenameToNoteTitle } from "@/utils/noteTitle"
import { confirmMessage } from '@/utils/notif' import { confirmMessage } from "@/utils/notif"
const isMarkdown = (filename?: string) => filename?.endsWith('.md') ?? false const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
export const useComputeBacklinks = () => { export const useComputeBacklinks = () => {
const store = useUserRepoStore() const store = useUserRepoStore()
@@ -51,18 +51,18 @@ export const useComputeBacklinks = () => {
} }
const parser = new DOMParser() const parser = new DOMParser()
const htmlDoc = parser.parseFromString(note, 'text/html') const htmlDoc = parser.parseFromString(note, "text/html")
const links = htmlDoc.querySelectorAll('a') const links = htmlDoc.querySelectorAll("a")
for (const link of links) { for (const link of links) {
const href = link.getAttribute('href') ?? '' const href = link.getAttribute("href") ?? ""
if (isExternalLink(href) || !isMarkdown(href)) { if (isExternalLink(href) || !isMarkdown(href)) {
continue continue
} }
const path = resolvePath(file.path ?? '', href) const path = resolvePath(file.path ?? "", href)
const backlinkFile = store.files.find((file) => file.path === path) const backlinkFile = store.files.find((file) => file.path === path)
if (!backlinkFile?.sha || !backlinkFile?.path) { if (!backlinkFile?.sha || !backlinkFile?.path) {
@@ -77,14 +77,14 @@ export const useComputeBacklinks = () => {
if (!notifiedForComputation) { if (!notifiedForComputation) {
notifiedForComputation = true notifiedForComputation = true
confirmMessage('Updating backlinks...') confirmMessage("Updating backlinks...")
} }
backlinks.set(backlinkFile.sha, [ backlinks.set(backlinkFile.sha, [
...previousBacklinks, ...previousBacklinks,
{ {
sha: file.sha, sha: file.sha,
title: filenameToNoteTitle(file.path ?? '') title: filenameToNoteTitle(file.path ?? "")
} }
]) ])
} }

View File

@@ -1,16 +1,16 @@
import { useMagicKeys } from '@vueuse/core' import { useMagicKeys } from "@vueuse/core"
import { ref, watch } from 'vue' import { ref, watch } from "vue"
export const useEditionMode = () => { export const useEditionMode = () => {
const mode = ref<'read' | 'edit'>('read') const mode = ref<"read" | "edit">("read")
const toggleMode = () => { const toggleMode = () => {
mode.value = mode.value === 'read' ? 'edit' : 'read' mode.value = mode.value === "read" ? "edit" : "read"
} }
const { escape } = useMagicKeys() const { escape } = useMagicKeys()
watch(escape, () => { watch(escape, () => {
if (mode.value === 'edit') { if (mode.value === "edit") {
toggleMode() toggleMode()
} }
}) })

View File

@@ -17,18 +17,18 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
const { const {
render, render,
renderFromUTF8, renderFromUTF8,
getRawContent: getRawContentFromFile, getRawContent: getRawContentFromFile
} = markdownBuilder(shaValue) } = markdownBuilder(shaValue)
const { getCachedNote, saveCacheNote } = prepareNoteCache( const { getCachedNote, saveCacheNote } = prepareNoteCache(
shaValue, shaValue,
toValue(path), toValue(path)
) )
const fromCache = ref(false) const fromCache = ref(false)
const rawContent = ref("") const rawContent = ref("")
const content = computed(() => const content = computed(() =>
rawContent.value ? renderFromUTF8(rawContent.value) : "", rawContent.value ? renderFromUTF8(rawContent.value) : ""
) )
const getEditedSha = async () => { const getEditedSha = async () => {
@@ -55,7 +55,7 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
} }
saveCacheNote(fileContent) saveCacheNote(fileContent)
rawContent.value = getRawContentFromFile(fileContent) rawContent.value = getRawContentFromFile(fileContent)
}, }
) )
} }
@@ -111,6 +111,6 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
getCachedFileContent, getCachedFileContent,
getEditedSha, getEditedSha,
fromCache, fromCache,
saveCacheNote, saveCacheNote
} }
} }

View File

@@ -1,17 +1,18 @@
import { computedAsync } from "@vueuse/core"
import { computed, Ref, ref, watch } from "vue"
import { Author, getAuthors } from "@/modules/atproto/getAuthor" import { Author, getAuthors } from "@/modules/atproto/getAuthor"
import { PublicNoteListItem } from "@/modules/note/models/Note" import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref, watch } from "vue"
export function useFollowingNoteList( export function useFollowingNoteList(
dids: Ref<Set<string>>, dids: Ref<Set<string>>,
enabled: Ref<boolean>, enabled: Ref<boolean>
) { ) {
const isLoading = ref(false) const isLoading = ref(false)
const notes = ref<PublicNoteListItem[]>([]) const notes = ref<PublicNoteListItem[]>([])
const cursor = ref<string | null | undefined>(null) const cursor = ref<string | null | undefined>(null)
const canLoadMore = computed( const canLoadMore = computed(
() => dids.value.size > 0 && cursor.value !== undefined, () => dids.value.size > 0 && cursor.value !== undefined
) )
const onLoadMore = async () => { const onLoadMore = async () => {
@@ -22,7 +23,7 @@ export function useFollowingNoteList(
const body: { dids: string[]; limit: number; cursor?: string } = { const body: { dids: string[]; limit: number; cursor?: string } = {
dids: [...dids.value], dids: [...dids.value],
limit: 20, limit: 20
} }
if (cursor.value) { if (cursor.value) {
@@ -32,7 +33,7 @@ export function useFollowingNoteList(
const response = await fetch("https://api.remanso.space/notes/feed", { const response = await fetch("https://api.remanso.space/notes/feed", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(body), body: JSON.stringify(body)
}) })
const data: { notes: PublicNoteListItem[]; cursor?: string } = const data: { notes: PublicNoteListItem[]; cursor?: string } =

View File

@@ -1,6 +1,6 @@
import { Ref, ref, watch } from 'vue' import { Ref, ref, watch } from "vue"
import { getFollows } from '@/modules/atproto/service/getFollows' import { getFollows } from "@/modules/atproto/service/getFollows"
export const useFollows = (did: Ref<string | null>) => { export const useFollows = (did: Ref<string | null>) => {
const follows = ref<Set<string>>(new Set()) const follows = ref<Set<string>>(new Set())
@@ -14,7 +14,7 @@ export const useFollows = (did: Ref<string | null>) => {
follows.value = new Set() follows.value = new Set()
} }
}, },
{ immediate: true }, { immediate: true }
) )
return { follows } return { follows }

View File

@@ -1,9 +1,9 @@
import { ref } from 'vue' import { ref } from "vue"
import { useRouter } from 'vue-router' import { useRouter } from "vue-router"
export const useForm = () => { export const useForm = () => {
const userInput = ref('') const userInput = ref("")
const repoInput = ref('') const repoInput = ref("")
const { push } = useRouter() const { push } = useRouter()
const submit = () => { const submit = () => {
@@ -12,7 +12,7 @@ export const useForm = () => {
} }
push({ push({
name: 'FluxNoteView', name: "FluxNoteView",
params: { params: {
user: userInput.value, user: userInput.value,
repo: repoInput.value repo: repoInput.value

View File

@@ -4,7 +4,7 @@ import { confirmMessage, errorMessage } from "@/utils/notif"
export const useGitHubContent = ({ export const useGitHubContent = ({
user, user,
repo, repo
}: { }: {
user: string user: string
repo: string repo: string
@@ -12,7 +12,7 @@ export const useGitHubContent = ({
const putFile = async ({ const putFile = async ({
content, content,
path, path,
sha, sha
}: { }: {
content: string content: string
path: string path: string
@@ -29,8 +29,8 @@ export const useGitHubContent = ({
path, path,
message: `Updating ${path} from Remanso`, message: `Updating ${path} from Remanso`,
content: encodeUTF8ToBase64(content), content: encodeUTF8ToBase64(content),
sha, sha
}, }
) )
confirmMessage("✅ Note saved") confirmMessage("✅ Note saved")
@@ -48,6 +48,6 @@ export const useGitHubContent = ({
updateFile: async (props: { content: string; path: string; sha: string }) => updateFile: async (props: { content: string; path: string; sha: string }) =>
putFile(props), putFile(props),
createFile: async (props: { content: string; path: string }) => createFile: async (props: { content: string; path: string }) =>
putFile(props), putFile(props)
} }
} }

View File

@@ -1,8 +1,8 @@
import { computed, ref } from 'vue' import { computed, ref } from "vue"
import { GithubToken } from '@/modules/user/interfaces/GithubToken' import { GithubToken } from "@/modules/user/interfaces/GithubToken"
import { getAccessToken, saveAccessToken } from '@/modules/user/service/signIn' import { getAccessToken, saveAccessToken } from "@/modules/user/service/signIn"
import { confirmMessage } from '@/utils/notif' import { confirmMessage } from "@/utils/notif"
const username = ref<string | null>(null) const username = ref<string | null>(null)
const accessToken = ref<string | null>(null) const accessToken = ref<string | null>(null)
@@ -11,8 +11,8 @@ let init = true
const saveAccessTokenToLocal = async () => { const saveAccessTokenToLocal = async () => {
const response = await getAccessToken() const response = await getAccessToken()
username.value = response?.username || '' username.value = response?.username || ""
accessToken.value = response?.token || '' accessToken.value = response?.token || ""
} }
const saveCredentials = async (token: GithubToken): Promise<void> => { const saveCredentials = async (token: GithubToken): Promise<void> => {

View File

@@ -1,10 +1,10 @@
import { computed, watch } from 'vue' import { computed, watch } from "vue"
import { useFile } from '@/hooks/useFile.hook' import { useFile } from "@/hooks/useFile.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"
const SRC_PREFIX = 'data:image/jpeg;charset=utf-8;base64,' const SRC_PREFIX = "data:image/jpeg;charset=utf-8;base64,"
export const useImages = (sha: string) => { export const useImages = (sha: string) => {
const store = useUserRepoStore() const store = useUserRepoStore()
@@ -23,14 +23,14 @@ export const useImages = (sha: string) => {
const images = document.querySelectorAll(`.note-${sha} img`) const images = document.querySelectorAll(`.note-${sha} img`)
images.forEach(async (image) => { images.forEach(async (image) => {
const src = image.getAttribute('src') const src = image.getAttribute("src")
if (!src || src.startsWith(SRC_PREFIX)) { if (!src || src.startsWith(SRC_PREFIX)) {
return return
} }
const imageFilePath = resolvePath( const imageFilePath = resolvePath(
filePath, filePath,
image.getAttribute('src') ?? '' image.getAttribute("src") ?? ""
) )
const imageFile = store.files.find( const imageFile = store.files.find(
@@ -43,7 +43,7 @@ export const useImages = (sha: string) => {
const { getCachedFileContent } = useFile(imageFile.sha, false) const { getCachedFileContent } = useFile(imageFile.sha, false)
const fileContent = await getCachedFileContent() const fileContent = await getCachedFileContent()
image.setAttribute('src', `${SRC_PREFIX} ${fileContent}`) image.setAttribute("src", `${SRC_PREFIX} ${fileContent}`)
}) })
}, },
{ immediate: true } { immediate: true }

View File

@@ -6,7 +6,7 @@ import { isExternalLink } from "@/utils/link"
export const useLinks = ( export const useLinks = (
className: ComputedRef<string> | string, className: ComputedRef<string> | string,
sha?: Ref<string> | string, sha?: Ref<string> | string
) => { ) => {
const store = useUserRepoStore() const store = useUserRepoStore()
@@ -34,7 +34,7 @@ export const useLinks = (
path: href, path: href,
currentNoteSHA: toValue(sha), currentNoteSHA: toValue(sha),
user: store.user, user: store.user,
repo: store.repo, repo: store.repo
}) })
} }
@@ -74,6 +74,6 @@ export const useLinks = (
}) })
return { return {
listenToClick, listenToClick
} }
} }

View File

@@ -1,18 +1,18 @@
import markdownItKatex from "@vscode/markdown-it-katex" import markdownItKatex from "@vscode/markdown-it-katex"
import MarkdownIt, { Options } from "markdown-it" import MarkdownIt, { Options } from "markdown-it"
import Renderer, { type RenderRuleRecord } from "markdown-it/lib/renderer.mjs"
import type Token from "markdown-it/lib/token.mjs"
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"
import markdownItIframe from "markdown-it-iframe" import markdownItIframe from "markdown-it-iframe"
import Shikiji from "markdown-it-shikiji" import Shikiji from "markdown-it-shikiji"
import mermaid from "mermaid"
import { Ref, toValue } from "vue" 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 { markdownItTablerIcons } from "@/utils/markdown/markdown-it-tabler-icons" import { markdownItTablerIcons } from "@/utils/markdown/markdown-it-tabler-icons"
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 =
@@ -22,7 +22,7 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
index: number, index: number,
options: Options, options: Options,
_: unknown, _: unknown,
self: Renderer, self: Renderer
) { ) {
return self.renderToken(tokens, index, options) return self.renderToken(tokens, index, options)
} }
@@ -32,7 +32,7 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
index: number, index: number,
options: Options, options: Options,
env: unknown, env: unknown,
self: Renderer, self: Renderer
) { ) {
const token = tokens[index] const token = tokens[index]
@@ -47,20 +47,20 @@ const markdownItMermaidExtractor = (md: MarkdownIt) => {
const md = new MarkdownIt({ const md = new MarkdownIt({
typographer: true, typographer: true,
quotes: ["«\xA0", "\xA0»", "\xA0", "\xA0"], quotes: ["«\xA0", "\xA0»", "\xA0", "\xA0"]
}) })
.use(markdownItMermaidExtractor) .use(markdownItMermaidExtractor)
.use(html5Media) .use(html5Media)
.use(blockEmbedPlugin, { .use(blockEmbedPlugin, {
youtube: { youtube: {
width: "100%", width: "100%",
height: 300, height: 300
}, }
}) })
.use(markdownItCheckbox) .use(markdownItCheckbox)
.use(markdownItKatex) .use(markdownItKatex)
.use(markdownItIframe, { .use(markdownItIframe, {
width: "100%", width: "100%"
}) })
.use(MarkdownItGitHubAlerts) .use(MarkdownItGitHubAlerts)
.use(markdownItTablerIcons) .use(markdownItTablerIcons)
@@ -77,7 +77,7 @@ export const useShikiji = async () => {
await Shikiji({ await Shikiji({
themes: { themes: {
light: "vitesse-light", light: "vitesse-light",
dark: "vitesse-black", dark: "vitesse-black"
}, },
langs: [ langs: [
"bash", "bash",
@@ -87,9 +87,9 @@ export const useShikiji = async () => {
"mermaid", "mermaid",
"html", "html",
"css", "css",
"json", "json"
], ]
}), })
) )
} }
@@ -101,19 +101,19 @@ export const runMermaid = (querySelector: string) => {
mermaid.initialize({ mermaid.initialize({
theme: "dark", theme: "dark",
startOnLoad: false, startOnLoad: false,
flowchart: { curve: "natural" }, flowchart: { curve: "natural" }
}) })
} }
mermaid.run({ mermaid.run({
querySelector, querySelector
}) })
} }
const rules: 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>"
} }
md.renderer.rules = { ...md.renderer.rules, ...rules } md.renderer.rules = { ...md.renderer.rules, ...rules }
@@ -128,7 +128,7 @@ export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
const renderFromUTF8 = (content: string, prefix?: string) => { const renderFromUTF8 = (content: string, prefix?: string) => {
return content return content
? md.render(stripFrontmatter(content), { ? md.render(stripFrontmatter(content), {
docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? ""), docId: defaultPrefix ? toValue(defaultPrefix) : (prefix ?? "")
}) })
: "" : ""
} }
@@ -139,6 +139,6 @@ export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
render: (content: string, prefix?: string) => render: (content: string, prefix?: string) =>
renderFromUTF8(decodeBase64ToUTF8(content), prefix), renderFromUTF8(decodeBase64ToUTF8(content), prefix),
renderFromUTF8, renderFromUTF8,
getRawContent, getRawContent
} }
} }

View File

@@ -1,13 +1,16 @@
import { computed, onMounted, Ref, ref, toValue } from "vue" import { computed, onMounted, Ref, ref, toValue } from "vue"
import { BOOKMARK_WIDTH_REM, getBookmarkWidthPx } from "@/constants/bookmark-width" import {
BOOKMARK_WIDTH_REM,
getBookmarkWidthPx
} from "@/constants/bookmark-width"
import { getNoteWidth } from "@/constants/note-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"
export const useNoteOverlay = ( export const useNoteOverlay = (
className: string, className: string,
index: Ref<number> | number, index: Ref<number> | number
) => { ) => {
const { x, y, isMobile } = useOverlay() const { x, y, isMobile } = useOverlay()
const noteHeight = ref(0) const noteHeight = ref(0)
@@ -18,14 +21,17 @@ 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 * getNoteWidth() - valueIndex * getBookmarkWidthPx() return (
x.value >
valueIndex * getNoteWidth() - valueIndex * getBookmarkWidthPx()
)
} }
}) })
onMounted(() => { onMounted(() => {
const { stackedNotes } = useRouteQueryStackedNotes() const { stackedNotes } = useRouteQueryStackedNotes()
const noteElement = document.querySelector( const noteElement = document.querySelector(
`.${className}`, `.${className}`
) satisfies HTMLElement | null ) satisfies HTMLElement | null
if (!noteElement) { if (!noteElement) {
@@ -40,7 +46,7 @@ export const useNoteOverlay = (
noteElement.style.left = `${(toValue(index) + 1) * BOOKMARK_WIDTH_REM}rem` noteElement.style.left = `${(toValue(index) + 1) * BOOKMARK_WIDTH_REM}rem`
const stackedNoteContainers = document.querySelectorAll( const stackedNoteContainers = document.querySelectorAll(
".stacked-note", ".stacked-note"
) satisfies NodeListOf<HTMLElement> ) satisfies NodeListOf<HTMLElement>
stackedNoteContainers.forEach((stackedNote, ind) => { stackedNoteContainers.forEach((stackedNote, ind) => {
@@ -52,6 +58,6 @@ export const useNoteOverlay = (
}) })
return { return {
displayNoteOverlay, displayNoteOverlay
} }
} }

View File

@@ -21,13 +21,13 @@ export const useNoteView = () => {
obj[note] = pathToNotePathTitle(filePath) obj[note] = pathToNotePathTitle(filePath)
return obj return obj
}, {}), }, {})
) )
const unsubscribeLink = noteEventBus.addEventBusListener( const unsubscribeLink = noteEventBus.addEventBusListener(
({ path, currentNoteSHA }) => { ({ path, currentNoteSHA }) => {
const currentFile = store.files.find( const currentFile = store.files.find(
(file) => file.sha === currentNoteSHA, (file) => file.sha === currentNoteSHA
) )
const absolutePath = resolvePath(currentFile?.path ?? "", path) const absolutePath = resolvePath(currentFile?.path ?? "", path)
@@ -39,7 +39,7 @@ export const useNoteView = () => {
} }
addStackedNote(currentNoteSHA ?? "", file.sha) addStackedNote(currentNoteSHA ?? "", file.sha)
}, }
) )
onUnmounted(() => { onUnmounted(() => {
@@ -47,6 +47,6 @@ export const useNoteView = () => {
}) })
return { return {
titles, titles
} }
} }

View File

@@ -1,12 +1,12 @@
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from "@vueuse/core"
import { computed, ref } from 'vue' import { computed, ref } from "vue"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { prepareNoteCache } from '@/modules/note/cache/prepareNoteCache' import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
import { Note } from '@/modules/note/models/Note' import { Note } from "@/modules/note/models/Note"
import { queryFileContent } from '@/modules/repo/services/repo' import { queryFileContent } from "@/modules/repo/services/repo"
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store' import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
export const useOfflineNotes = () => { export const useOfflineNotes = () => {
const store = useUserRepoStore() const store = useUserRepoStore()

View File

@@ -19,11 +19,11 @@ export const useOverlay = (listen = true) => {
} }
useEventListener(window, "scroll", updateScroll, { useEventListener(window, "scroll", updateScroll, {
passive: true, passive: true,
capture: false, capture: false
}) })
useEventListener(document.body, "scroll", updateScroll, { useEventListener(document.body, "scroll", updateScroll, {
passive: true, passive: true,
capture: false, capture: false
}) })
} }
@@ -47,6 +47,6 @@ export const useOverlay = (listen = true) => {
x, x,
y, y,
isMobile, isMobile,
scrollToNote, scrollToNote
} }
} }

View File

@@ -1,7 +1,8 @@
import { computedAsync } from "@vueuse/core"
import { computed, Ref, ref } from "vue"
import { Author, getAuthors } from "@/modules/atproto/getAuthor" import { Author, getAuthors } from "@/modules/atproto/getAuthor"
import { PublicNoteListItem } from "@/modules/note/models/Note" import { PublicNoteListItem } from "@/modules/note/models/Note"
import { computedAsync } from "@vueuse/core"
import { computed, ref, Ref } from "vue"
interface UsePublicNoteListOptions { interface UsePublicNoteListOptions {
did?: Ref<string | undefined> did?: Ref<string | undefined>
@@ -50,6 +51,6 @@ export function usePublicNoteList(options?: UsePublicNoteListOptions) {
canLoadMore, canLoadMore,
onLoadMore, onLoadMore,
authors, authors,
getAuthor, getAuthor
} }
} }

View File

@@ -1,8 +1,8 @@
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from "@vueuse/core"
import { useGitHubLogin } from '@/hooks/useGitHubLogin.hook' import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
import { RepoBase } from '@/modules/repo/interfaces/RepoBase' import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { getOctokit } from '@/modules/repo/services/octo' import { getOctokit } from "@/modules/repo/services/octo"
export const useRepos = () => { export const useRepos = () => {
const { username, accessToken } = useGitHubLogin() const { username, accessToken } = useGitHubLogin()
@@ -13,7 +13,7 @@ export const useRepos = () => {
const octokit = await getOctokit() const octokit = await getOctokit()
const repoList = await octokit.request('GET /search/repositories', { const repoList = await octokit.request("GET /search/repositories", {
q: `user:${username.value}`, q: `user:${username.value}`,
per_page: 100 per_page: 100
}) })

View File

@@ -1,17 +1,17 @@
import { onMounted, watch, type Ref } from "vue" import { onMounted, type Ref, watch } from "vue"
import { getNoteWidth } from "@/constants/note-width" import { getNoteWidth } from "@/constants/note-width"
import { useOverlay } from "@/hooks/useOverlay.hook" import { useOverlay } from "@/hooks/useOverlay.hook"
export const useResizeContainer = ( export const useResizeContainer = (
containerClass: string, containerClass: string,
stackedNotes: Readonly<Ref<readonly string[]>>, stackedNotes: Readonly<Ref<readonly string[]>>
) => { ) => {
const { isMobile } = useOverlay(false) const { isMobile } = useOverlay(false)
const resizeContainer = () => { const resizeContainer = () => {
const container = document.querySelector( const container = document.querySelector(
`.${containerClass}`, `.${containerClass}`
) as HTMLElement | null ) as HTMLElement | null
if (!container) { if (!container) {
@@ -32,6 +32,6 @@ export const useResizeContainer = (
}) })
watch(stackedNotes, resizeContainer, { watch(stackedNotes, resizeContainer, {
immediate: true, immediate: true
}) })
} }

View File

@@ -14,7 +14,7 @@ export const useRouteQueryStackedNotes = () => {
} }
return Array.isArray(value) ? value : [value] return Array.isArray(value) ? value : [value]
}, }
}) })
const { height } = useWindowSize() const { height } = useWindowSize()
@@ -22,7 +22,7 @@ export const useRouteQueryStackedNotes = () => {
const scrollToFocusedNote = ( const scrollToFocusedNote = (
noteId: string | null = null, noteId: string | null = null,
notes: string[] = stackedNotes.value, notes: string[] = stackedNotes.value
) => { ) => {
nextTick(() => { nextTick(() => {
const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0 const index = noteId ? notes.findIndex((nid) => nid === noteId) : 0
@@ -31,7 +31,7 @@ export const useRouteQueryStackedNotes = () => {
if (noteId) { if (noteId) {
const cleanNoteId = noteId.replaceAll(":", "-") const cleanNoteId = noteId.replaceAll(":", "-")
const element = document.querySelector( const element = document.querySelector(
`.note-${cleanNoteId}`, `.note-${cleanNoteId}`
) as HTMLElement ) as HTMLElement
const top = (index + 1) * (element?.clientHeight ?? height.value) const top = (index + 1) * (element?.clientHeight ?? height.value)
@@ -53,7 +53,7 @@ export const useRouteQueryStackedNotes = () => {
const addStackedNote = ( const addStackedNote = (
currentSha: string, currentSha: string,
sha: string, sha: string,
selector?: string, selector?: string
) => { ) => {
if (stackedNotes.value.includes(sha)) { if (stackedNotes.value.includes(sha)) {
scrollToFocusedNote(selector ?? sha) scrollToFocusedNote(selector ?? sha)
@@ -70,7 +70,7 @@ export const useRouteQueryStackedNotes = () => {
const newStackedNotes = [ const newStackedNotes = [
...splittedStackedNotes.replaceAll(";;", ";").split(";"), ...splittedStackedNotes.replaceAll(";;", ";").split(";"),
currentSha, currentSha,
sha, sha
].filter((sha) => !!sha) ].filter((sha) => !!sha)
stackedNotes.value = newStackedNotes stackedNotes.value = newStackedNotes
@@ -82,6 +82,6 @@ export const useRouteQueryStackedNotes = () => {
return { return {
stackedNotes: readonly(stackedNotes), stackedNotes: readonly(stackedNotes),
addStackedNote, addStackedNote,
scrollToFocusedNote, scrollToFocusedNote
} }
} }

View File

@@ -1,19 +1,19 @@
import { useTitle } from '@vueuse/core' import { useTitle } from "@vueuse/core"
import { computed, Ref, toValue, watch } from 'vue' import { computed, Ref, toValue, watch } from "vue"
import { useRouteQueryStackedNotes } from '@/hooks/useRouteQueryStackedNotes.hook' import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
import { useNotes } from '@/modules/note/hooks/useNotes' import { useNotes } from "@/modules/note/hooks/useNotes"
import { pathToNoteTitle } from '@/utils/noteTitle' import { pathToNoteTitle } from "@/utils/noteTitle"
export const generateTitle = (titles: string[]) => titles.join(' | ') export const generateTitle = (titles: string[]) => titles.join(" | ")
export const useTitleNotes = (prefix: Ref<string> | string) => { export const useTitleNotes = (prefix: Ref<string> | string) => {
const { stackedNotes } = useRouteQueryStackedNotes() const { stackedNotes } = useRouteQueryStackedNotes()
const { notes } = useNotes() const { notes } = useNotes()
const titleNotes = computed(() => const titleNotes = computed(() =>
notes.value notes.value
.filter((note) => stackedNotes.value.includes(note.sha ?? '')) .filter((note) => stackedNotes.value.includes(note.sha ?? ""))
.map((note) => pathToNoteTitle(note.path ?? '')) .map((note) => pathToNoteTitle(note.path ?? ""))
) )
const title = useTitle(generateTitle([toValue(prefix), ...titleNotes.value])) const title = useTitle(generateTitle([toValue(prefix), ...titleNotes.value]))

View File

@@ -1,5 +1,5 @@
import en from './en.json' import en from "./en.json"
import fr from './fr.json' import fr from "./fr.json"
export const messages = { export const messages = {
en, en,

View File

@@ -1,5 +1,6 @@
import "notyf/notyf.min.css" import "notyf/notyf.min.css"
import "./styles/app.css" import "./styles/app.css"
import "@/analytics/openpanel"
import { VueQueryPlugin } from "@tanstack/vue-query" import { VueQueryPlugin } from "@tanstack/vue-query"
import { createPinia } from "pinia" import { createPinia } from "pinia"
@@ -10,11 +11,10 @@ import { messages } from "@/locales/message"
import { router } from "@/router/router" import { router } from "@/router/router"
import App from "./App.vue" import App from "./App.vue"
import "@/analytics/openpanel"
const i18n = createI18n({ const i18n = createI18n({
locale: "en", locale: "en",
messages, messages
}) })
createApp(App) createApp(App)

View File

@@ -1,4 +1,4 @@
import { createSchema, createFetch } from "@better-fetch/fetch" import { createFetch, createSchema } from "@better-fetch/fetch"
import { type } from "arktype" import { type } from "arktype"
export type Author = { handle: string; pds: string } export type Author = { handle: string; pds: string }
@@ -12,20 +12,20 @@ const schema = createSchema(
did: "string", did: "string",
handle: "string", handle: "string",
pds: "string", pds: "string",
signing_key: "string", signing_key: "string"
}), }),
query: type({ query: type({
identifier: "string", identifier: "string"
}), })
}
}, },
}, { strict: true }
{ strict: true },
) )
const microcosmSlingshot = createFetch({ const microcosmSlingshot = createFetch({
baseURL: "https://slingshot.microcosm.blue", baseURL: "https://slingshot.microcosm.blue",
// plugins: [logger()], // plugins: [logger()],
schema, schema
}) })
export const getAuthor = async (did: string): Promise<Author | null> => { export const getAuthor = async (did: string): Promise<Author | null> => {
@@ -36,7 +36,7 @@ export const getAuthor = async (did: string): Promise<Author | null> => {
try { try {
const { data: author } = await microcosmSlingshot( const { data: author } = await microcosmSlingshot(
"/xrpc/blue.microcosm.identity.resolveMiniDoc", "/xrpc/blue.microcosm.identity.resolveMiniDoc",
{ query: { identifier: did } }, { query: { identifier: did } }
) )
if (!author) { if (!author) {
@@ -62,7 +62,7 @@ export const getAuthors = async (dids: Set<string>) => {
const { data: author } = await microcosmSlingshot( const { data: author } = await microcosmSlingshot(
"/xrpc/blue.microcosm.identity.resolveMiniDoc", "/xrpc/blue.microcosm.identity.resolveMiniDoc",
{ query: { identifier: did } }, { query: { identifier: did } }
) )
if (!author) { if (!author) {
@@ -72,7 +72,7 @@ export const getAuthors = async (dids: Set<string>) => {
correspondanceCache.set(did, author) correspondanceCache.set(did, author)
return [did, author] as [string, Author | null] return [did, author] as [string, Author | null]
}), })
) )
return new Map(correspondance) return new Map(correspondance)

View File

@@ -1,6 +1,6 @@
import { import {
BrowserOAuthClient, BrowserOAuthClient,
buildLoopbackClientId, buildLoopbackClientId
} from "@atproto/oauth-client-browser" } from "@atproto/oauth-client-browser"
const getClientId = () => const getClientId = () =>
@@ -14,7 +14,7 @@ export const getOAuthClient = (): Promise<BrowserOAuthClient> => {
if (!clientPromise) { if (!clientPromise) {
clientPromise = BrowserOAuthClient.load({ clientPromise = BrowserOAuthClient.load({
clientId: getClientId(), clientId: getClientId(),
handleResolver: "https://bsky.social", handleResolver: "https://bsky.social"
}) })
} }
return clientPromise return clientPromise

View File

@@ -1,6 +1,6 @@
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { AtprotoSession } from '@/data/models/AtprotoSession' import { AtprotoSession } from "@/data/models/AtprotoSession"
const SESSION_ID = `${DataType.AtprotoSession}-current` const SESSION_ID = `${DataType.AtprotoSession}-current`
@@ -8,12 +8,15 @@ export const loadSession = (): Promise<AtprotoSession | null> => {
return data.get<DataType.AtprotoSession, AtprotoSession>(SESSION_ID) return data.get<DataType.AtprotoSession, AtprotoSession>(SESSION_ID)
} }
export const saveSession = async (did: string, handle: string): Promise<void> => { export const saveSession = async (
did: string,
handle: string
): Promise<void> => {
const session: AtprotoSession = { const session: AtprotoSession = {
_id: SESSION_ID, _id: SESSION_ID,
$type: DataType.AtprotoSession, $type: DataType.AtprotoSession,
did, did,
handle, handle
} }
await data.update<DataType.AtprotoSession, AtprotoSession>(session) await data.update<DataType.AtprotoSession, AtprotoSession>(session)
} }

View File

@@ -3,15 +3,18 @@ export const getFollows = async (did: string): Promise<Set<string>> => {
let cursor: string | undefined let cursor: string | undefined
do { do {
const url = new URL('https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows') const url = new URL(
url.searchParams.set('actor', did) "https://public.api.bsky.app/xrpc/app.bsky.graph.getFollows"
url.searchParams.set('limit', '100') )
url.searchParams.set("actor", did)
url.searchParams.set("limit", "100")
if (cursor) { if (cursor) {
url.searchParams.set('cursor', cursor) url.searchParams.set("cursor", cursor)
} }
const response = await fetch(url) const response = await fetch(url)
const result: { follows: { did: string }[]; cursor?: string } = await response.json() const result: { follows: { did: string }[]; cursor?: string } =
await response.json()
for (const follow of result.follows) { for (const follow of result.follows) {
follows.add(follow.did) follows.add(follow.did)

View File

@@ -1,6 +1,6 @@
export const withATProtoImages = ( export const withATProtoImages = (
markdown: string, markdown: string,
{ pds, did }: { pds: string; did: string }, { pds, did }: { pds: string; did: string }
): string => { ): string => {
const imageLinkPattern = /!\[([^\]]*)\]\((bafkrei[a-z0-9]+)\)/g const imageLinkPattern = /!\[([^\]]*)\]\((bafkrei[a-z0-9]+)\)/g

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue' import { computed, ref } from "vue"
import FlipCard from '@/modules/card/components/FlipCard.vue' import FlipCard from "@/modules/card/components/FlipCard.vue"
import { Repetition } from '@/modules/card/hooks/useSpacedRepetitionCards' import { Repetition } from "@/modules/card/hooks/useSpacedRepetitionCards"
const props = defineProps<{ cards: Repetition[] }>() const props = defineProps<{ cards: Repetition[] }>()
const emits = defineEmits<{ const emits = defineEmits<{
@@ -22,24 +22,24 @@ const sortedCards = ref(
const currentIndex = ref(0) const currentIndex = ref(0)
const goToNextCard = (success: boolean) => { const goToNextCard = (success: boolean) => {
const id = sortedCards.value[currentIndex.value].repetition._id ?? '' const id = sortedCards.value[currentIndex.value].repetition._id ?? ""
if (success) { if (success) {
emits('success', id) emits("success", id)
} else { } else {
const failedCard = sortedCards.value.at(currentIndex.value) const failedCard = sortedCards.value.at(currentIndex.value)
if (failedCard) { if (failedCard) {
sortedCards.value.push(failedCard) sortedCards.value.push(failedCard)
} }
emits('fail', id) emits("fail", id)
} }
currentIndex.value++ currentIndex.value++
} }
const needsReview = () => { const needsReview = () => {
const id = sortedCards.value[currentIndex.value].repetition._id ?? '' const id = sortedCards.value[currentIndex.value].repetition._id ?? ""
emits('needsReview', id) emits("needsReview", id)
currentIndex.value++ currentIndex.value++
} }
</script> </script>

View File

@@ -1,8 +1,8 @@
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from "@vueuse/core"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { RepetitionCard } from '@/modules/card/models/RepetitionCard' import { RepetitionCard } from "@/modules/card/models/RepetitionCard"
export const useNeedReviewCards = () => { export const useNeedReviewCards = () => {
const { state: cardsToReview, isReady } = useAsyncState(async () => { const { state: cardsToReview, isReady } = useAsyncState(async () => {

View File

@@ -31,14 +31,14 @@ export const useSpacedRepetitionCards = () => {
(file) => (file) =>
file.path !== undefined && file.path !== undefined &&
file.path.startsWith("_cards") && file.path.startsWith("_cards") &&
file.path.endsWith(".md"), file.path.endsWith(".md")
), )
) )
const { const {
state: cards, state: cards,
isReady, isReady,
execute, execute
} = useAsyncState( } = useAsyncState(
async () => { async () => {
const cards: Repetition[] = [] const cards: Repetition[] = []
@@ -55,7 +55,7 @@ export const useSpacedRepetitionCards = () => {
$type: DataType.RepetitionCard, $type: DataType.RepetitionCard,
level: 1, level: 1,
repeatDate: new Date(), repeatDate: new Date(),
needsReview: false, needsReview: false
}) })
if ( if (
@@ -77,20 +77,20 @@ export const useSpacedRepetitionCards = () => {
card: { card: {
front: toHTML(front), front: toHTML(front),
back: toHTML(back), back: toHTML(back),
references: toHTML(references), references: toHTML(references)
}, }
}) })
} }
return cards return cards
}, },
[], [],
{ immediate: false }, { immediate: false }
) )
const successRepetition = async (cardId: string) => { const successRepetition = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>( const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId, cardId
) )
if (!repetition) { if (!repetition) {
return return
@@ -100,13 +100,13 @@ export const useSpacedRepetitionCards = () => {
...repetition, ...repetition,
needsReview: false, needsReview: false,
level: Math.min(repetition.level + 1, MAX_LEVEL), level: Math.min(repetition.level + 1, MAX_LEVEL),
repeatDate: addDays(new Date(), 2 ** repetition.level), repeatDate: addDays(new Date(), 2 ** repetition.level)
}) })
} }
const failRepetition = async (cardId: string) => { const failRepetition = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>( const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId, cardId
) )
if (!repetition) { if (!repetition) {
return return
@@ -118,13 +118,13 @@ export const useSpacedRepetitionCards = () => {
...repetition, ...repetition,
level, level,
needsReview: false, needsReview: false,
repeatDate: addDays(new Date(), level), repeatDate: addDays(new Date(), level)
}) })
} }
const needsReview = async (cardId: string) => { const needsReview = async (cardId: string) => {
const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>( const repetition = await data.get<DataType.RepetitionCard, RepetitionCard>(
cardId, cardId
) )
if (!repetition) { if (!repetition) {
return return
@@ -132,7 +132,7 @@ export const useSpacedRepetitionCards = () => {
await data.update<DataType.RepetitionCard, RepetitionCard>({ await data.update<DataType.RepetitionCard, RepetitionCard>({
...repetition, ...repetition,
needsReview: true, needsReview: true
}) })
} }
@@ -142,7 +142,7 @@ export const useSpacedRepetitionCards = () => {
nextTick(() => { nextTick(() => {
listenToClick() listenToClick()
}), }),
{ immediate: true }, { immediate: true }
) )
watch(cardFiles, () => execute()) watch(cardFiles, () => execute())
@@ -152,6 +152,6 @@ export const useSpacedRepetitionCards = () => {
successRepetition, successRepetition,
failRepetition, failRepetition,
needsReview, needsReview,
isLoading: !isReady, isLoading: !isReady
} }
} }

View File

@@ -1,5 +1,5 @@
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 RepetitionCard extends Model<DataType.RepetitionCard> { export interface RepetitionCard extends Model<DataType.RepetitionCard> {
level: number level: number

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLastVisitedRepos } from '@/modules/history/hooks/useLastVisitedRepos.hook' import { useLastVisitedRepos } from "@/modules/history/hooks/useLastVisitedRepos.hook"
const { lastVisitedRepos } = useLastVisitedRepos() const { lastVisitedRepos } = useLastVisitedRepos()
</script> </script>

View File

@@ -1,17 +1,17 @@
import { useAsyncState } from '@vueuse/core' import { useAsyncState } from "@vueuse/core"
import { computed } from 'vue' import { computed } from "vue"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { History } from '@/data/models/History' import { History } from "@/data/models/History"
const HISTORY_ID = data.generateId(DataType.History, 'history') const HISTORY_ID = data.generateId(DataType.History, "history")
export const useLastVisitedRepos = () => { export const useLastVisitedRepos = () => {
const history = useAsyncState( const history = useAsyncState(
() => () =>
data.get<DataType.History, History>( data.get<DataType.History, History>(
data.generateId(DataType.History, 'history') data.generateId(DataType.History, "history")
), ),
null null
) )

View File

@@ -1,10 +1,10 @@
import { Ref, toValue } from 'vue' import { Ref, toValue } from "vue"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { History } from '@/data/models/History' import { History } from "@/data/models/History"
const HISTORY_ID = data.generateId(DataType.History, 'history') const HISTORY_ID = data.generateId(DataType.History, "history")
const MAX_REPO_HISTORY = 10 const MAX_REPO_HISTORY = 10
export const useVisitRepo = (newRepo: { export const useVisitRepo = (newRepo: {

View File

@@ -1,14 +1,14 @@
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { Note } from '@/modules/note/models/Note' import { Note } from "@/modules/note/models/Note"
import { useUserRepoStore } from '@/modules/repo/store/userRepo.store' import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
type NoteCacheResult = type NoteCacheResult =
| { | {
note: Note note: Note
from: 'sha' from: "sha"
} }
| { note: Note; from: 'path' } | { note: Note; from: "path" }
| { note: null; from: null } | { note: null; from: null }
export const prepareNoteCache = (sha: string, path?: string) => { export const prepareNoteCache = (sha: string, path?: string) => {
@@ -20,7 +20,7 @@ export const prepareNoteCache = (sha: string, path?: string) => {
const note = await data.get<DataType.Note, Note>(noteId) const note = await data.get<DataType.Note, Note>(noteId)
if (note) { if (note) {
return { note, from: 'sha' } return { note, from: "sha" }
} }
if (notePath) { if (notePath) {
@@ -33,7 +33,7 @@ export const prepareNoteCache = (sha: string, path?: string) => {
} }
return { return {
note, note,
from: 'path' from: "path"
} }
} }

View File

@@ -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 useFolderNotes = (folders: string[]) => { export const useFolderNotes = (folders: string[]) => {
@@ -8,8 +9,8 @@ export const useFolderNotes = (folders: string[]) => {
store.files.filter( store.files.filter(
(file) => (file) =>
folders.some((folder) => file.path?.startsWith(folder)) && folders.some((folder) => file.path?.startsWith(folder)) &&
file.path?.endsWith(".md"), file.path?.endsWith(".md")
), )
) )
const content = computed(() => const content = computed(() =>
@@ -23,10 +24,10 @@ export const useFolderNotes = (folders: string[]) => {
})` })`
}) })
.join("\n") .join("\n")
: "", : ""
) )
return { return {
content, content
} }
} }

View File

@@ -6,10 +6,10 @@ export const useNotes = () => {
const store = useUserRepoStore() const store = useUserRepoStore()
const notes = computed(() => const notes = computed(() =>
store.files.filter((file) => file.path?.endsWith(".md")), store.files.filter((file) => file.path?.endsWith(".md"))
) )
return { return {
notes, notes
} }
} }

View File

@@ -1,6 +1,6 @@
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { Model } from '@/data/models/Model' import { Model } from "@/data/models/Model"
import { Backlink } from '@/modules/note/models/Backlink' import { Backlink } from "@/modules/note/models/Backlink"
export interface BacklinkNote extends Model<DataType.BacklinkNote> { export interface BacklinkNote extends Model<DataType.BacklinkNote> {
sha: string sha: string

View File

@@ -1,13 +1,13 @@
import { initContract } from "@ts-rest/core" import { initContract } from "@ts-rest/core"
import { type } from "arktype"
import { initQueryClient } from "@ts-rest/vue-query" import { initQueryClient } from "@ts-rest/vue-query"
import { type } from "arktype"
const PublicNoteListItem = type({ const PublicNoteListItem = type({
did: "string", did: "string",
rkey: "string", rkey: "string",
title: "string", title: "string",
publishedAt: "string", publishedAt: "string",
createdAt: "string", createdAt: "string"
}) })
export type PublicNoteListItem = typeof PublicNoteListItem.infer export type PublicNoteListItem = typeof PublicNoteListItem.infer
@@ -18,7 +18,7 @@ const PublicNote = type({
title: "string", title: "string",
content: "string", content: "string",
publishedAt: "string", publishedAt: "string",
createdAt: "string", createdAt: "string"
}) })
export type PublicNote = typeof PublicNote.infer export type PublicNote = typeof PublicNote.infer
@@ -31,34 +31,34 @@ export const noteRouter = contract.router({
path: "/notes", path: "/notes",
query: type({ query: type({
cursor: "string | undefined", cursor: "string | undefined",
limit: "number | undefined", limit: "number | undefined"
}), }),
responses: { responses: {
200: type({ 200: type({
notes: PublicNoteListItem.array(), notes: PublicNoteListItem.array()
}), })
}, },
summary: "List all notes", summary: "List all notes"
}, },
noteListsByDid: { noteListsByDid: {
method: "GET", method: "GET",
path: "/:did/notes", path: "/:did/notes",
pathParams: type({ pathParams: type({
did: "string", did: "string"
}), }),
query: type({ query: type({
cursor: "string | undefined", cursor: "string | undefined",
limit: "number | undefined", limit: "number | undefined"
}), }),
responses: { responses: {
200: type({ 200: type({
notes: PublicNoteListItem.array(), notes: PublicNoteListItem.array()
}), })
},
summary: "List all notes",
}, },
summary: "List all notes"
}
}) })
export const client = initQueryClient(noteRouter, { export const client = initQueryClient(noteRouter, {
baseUrl: "https://api.remanso.space", baseUrl: "https://api.remanso.space"
}) })

View File

@@ -1,10 +1,10 @@
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from "vue"
import { data } from '@/data/data' import { data } from "@/data/data"
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { useRepos } from '@/hooks/useRepos.hook' import { useRepos } from "@/hooks/useRepos.hook"
import { RepoBase } from '@/modules/repo/interfaces/RepoBase' import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
import { FavoriteRepo } from '@/modules/repo/models/FavoriteRepo' import { FavoriteRepo } from "@/modules/repo/models/FavoriteRepo"
export const useFavoriteRepos = () => { export const useFavoriteRepos = () => {
const { repos } = useRepos() const { repos } = useRepos()

View File

@@ -1,8 +1,8 @@
import { computed } from 'vue' import { computed } from "vue"
import { useRepos } from '@/hooks/useRepos.hook' import { useRepos } from "@/hooks/useRepos.hook"
import { useFavoriteRepos } from '@/modules/repo/hooks/useFavoriteRepos.hook' import { useFavoriteRepos } from "@/modules/repo/hooks/useFavoriteRepos.hook"
import { RepoBase } from '@/modules/repo/interfaces/RepoBase' import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
export const useRepoList = () => { export const useRepoList = () => {
const { savedFavoriteRepos, addFavorite, removeFavorite } = useFavoriteRepos() const { savedFavoriteRepos, addFavorite, removeFavorite } = useFavoriteRepos()

View File

@@ -1,5 +1,5 @@
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 FavoriteRepo extends Model<DataType.FavoriteRepo> { export interface FavoriteRepo extends Model<DataType.FavoriteRepo> {
isFavorite: boolean isFavorite: boolean

View File

@@ -1,6 +1,6 @@
import { DataType } from '@/data/DataType.enum' import { DataType } from "@/data/DataType.enum"
import { Model } from '@/data/models/Model' import { Model } from "@/data/models/Model"
import { RepoFile } from '@/modules/repo/interfaces/RepoFile' import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
export interface SavedRepo extends Model<DataType.SavedRepo> { export interface SavedRepo extends Model<DataType.SavedRepo> {
user: string user: string

View File

@@ -1,11 +1,11 @@
import { Octokit } from '@octokit/rest' import { Octokit } from "@octokit/rest"
import { getAccessToken } from '@/modules/user/service/signIn' import { getAccessToken } from "@/modules/user/service/signIn"
export const getOctokit = async (): Promise<Octokit> => { export const getOctokit = async (): Promise<Octokit> => {
const response = await getAccessToken() const response = await getAccessToken()
return new Octokit({ return new Octokit({
auth: response?.token ?? '' auth: response?.token ?? ""
}) })
} }

View File

@@ -6,7 +6,7 @@ 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 []
@@ -15,7 +15,7 @@ export const getFiles = async (
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()
@@ -30,8 +30,8 @@ export const getFiles = async (
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") ?? []
@@ -60,7 +60,7 @@ export const getMainReadme = async (owner: string, repo: string) => {
const { render } = markdownBuilder() const { render } = markdownBuilder()
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,7 +90,7 @@ 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<Omit<UserSettings, "chosenFontFamily"> | null> => { ): Promise<Omit<UserSettings, "chosenFontFamily"> | null> => {
const configFile = files.find((file) => file.path === ".remanso.json") const configFile = files.find((file) => file.path === ".remanso.json")
@@ -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()
@@ -123,8 +123,8 @@ export const queryFileContent = async (
{ {
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,43 +1,43 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from "vitest"
import { resolvePath } from './resolvePath' import { resolvePath } from "./resolvePath"
describe('resolve path service', () => { describe("resolve path service", () => {
it('set the absolute path if path to resolve is empty', () => { it("set the absolute path if path to resolve is empty", () => {
expect(resolvePath('standard/README.md', '')).toEqual('standard/') expect(resolvePath("standard/README.md", "")).toEqual("standard/")
}) })
it('returns the path sanitized if there is no absolute path', () => { it("returns the path sanitized if there is no absolute path", () => {
expect(resolvePath('', './here/note.md')).toEqual('here/note.md') expect(resolvePath("", "./here/note.md")).toEqual("here/note.md")
}) })
it('set the absolute path from the current path', () => { it("set the absolute path from the current path", () => {
expect(resolvePath('standard/README.md', './other-note.md')).toEqual( expect(resolvePath("standard/README.md", "./other-note.md")).toEqual(
'standard/other-note.md' "standard/other-note.md"
) )
}) })
it('set the absolute path from the current path with multiple level', () => { it("set the absolute path from the current path with multiple level", () => {
expect( expect(
resolvePath('standard/you/are/here/README.md', './other-note.md') resolvePath("standard/you/are/here/README.md", "./other-note.md")
).toEqual('standard/you/are/here/other-note.md') ).toEqual("standard/you/are/here/other-note.md")
}) })
it('set the absolute path from the current path with a go back in the relative path', () => { it("set the absolute path from the current path with a go back in the relative path", () => {
expect( expect(
resolvePath('standard/you/are/here/README.md', '../other-note.md') resolvePath("standard/you/are/here/README.md", "../other-note.md")
).toEqual('standard/you/are/other-note.md') ).toEqual("standard/you/are/other-note.md")
expect( expect(
resolvePath('standard/you/are/here/README.md', '../../other-note.md') resolvePath("standard/you/are/here/README.md", "../../other-note.md")
).toEqual('standard/you/other-note.md') ).toEqual("standard/you/other-note.md")
expect( expect(
resolvePath('standard/you/are/here/README.md', './../../other-note.md') resolvePath("standard/you/are/here/README.md", "./../../other-note.md")
).toEqual('standard/you/other-note.md') ).toEqual("standard/you/other-note.md")
expect( expect(
resolvePath('standard/you/are/here/README.md', './../../../other-note.md') resolvePath("standard/you/are/here/README.md", "./../../../other-note.md")
).toEqual('standard/other-note.md') ).toEqual("standard/other-note.md")
}) })
}) })

View File

@@ -1,15 +1,15 @@
const sanitizePath = (path: string) => { const sanitizePath = (path: string) => {
if (path.startsWith('./')) { if (path.startsWith("./")) {
return decodeURIComponent(path.replace('./', '')) return decodeURIComponent(path.replace("./", ""))
} }
return decodeURIComponent(path) return decodeURIComponent(path)
} }
const removeNoteFilename = (pathNote: string) => { const removeNoteFilename = (pathNote: string) => {
const path = pathNote.split('/') const path = pathNote.split("/")
path.pop() path.pop()
return sanitizePath(path.join('/')) return sanitizePath(path.join("/"))
} }
export const resolvePath = ( export const resolvePath = (
@@ -19,11 +19,11 @@ export const resolvePath = (
let currentAbsolutePath = removeNoteFilename(currentAbsolutePathNote) let currentAbsolutePath = removeNoteFilename(currentAbsolutePathNote)
pathToResolve = sanitizePath(pathToResolve) pathToResolve = sanitizePath(pathToResolve)
while (pathToResolve.startsWith('../')) { while (pathToResolve.startsWith("../")) {
const adjustedAbsolutePath = currentAbsolutePath.split('/') const adjustedAbsolutePath = currentAbsolutePath.split("/")
adjustedAbsolutePath.pop() adjustedAbsolutePath.pop()
currentAbsolutePath = adjustedAbsolutePath.join('/') currentAbsolutePath = adjustedAbsolutePath.join("/")
pathToResolve = pathToResolve.replace('../', '') pathToResolve = pathToResolve.replace("../", "")
} }
return currentAbsolutePath return currentAbsolutePath

View File

@@ -9,7 +9,7 @@ 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"
@@ -29,7 +29,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
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) {
@@ -38,7 +38,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
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) {
@@ -68,14 +68,14 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
$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( const chosenFontFamily = userSettings?.fontFamilies?.find(
(font) => font === this.userSettings?.chosenFontFamily, (font) => font === this.userSettings?.chosenFontFamily
) )
? this.userSettings?.chosenFontFamily ? this.userSettings?.chosenFontFamily
: userSettings?.fontFamily : userSettings?.fontFamily
@@ -94,7 +94,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
data.update<DataType.UserSettings, UserSettings>({ data.update<DataType.UserSettings, UserSettings>({
...this.userSettings, ...this.userSettings,
_id: userSettingsId, _id: userSettingsId
}) })
}) })
@@ -116,7 +116,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
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>({
@@ -124,7 +124,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
$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
}, },
@@ -147,7 +147,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
const userSettingsId = `UserSetting-${this.user}-${this.repo}` const userSettingsId = `UserSetting-${this.user}-${this.repo}`
data.update<DataType.UserSettings, UserSettings>({ data.update<DataType.UserSettings, UserSettings>({
...this.userSettings, ...this.userSettings,
_id: userSettingsId, _id: userSettingsId
}) })
}, },
setFontSize(fontSize: string) { setFontSize(fontSize: string) {
@@ -159,8 +159,8 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
const userSettingsId = `UserSetting-${this.user}-${this.repo}` const userSettingsId = `UserSetting-${this.user}-${this.repo}`
data.update<DataType.UserSettings, UserSettings>({ data.update<DataType.UserSettings, UserSettings>({
...this.userSettings, ...this.userSettings,
_id: userSettingsId, _id: userSettingsId
}) })
}, }
}, }
}) })

View File

@@ -4,5 +4,5 @@ export interface GithubToken {
refresh_token: string refresh_token: string
refresh_token_expires_in: number refresh_token_expires_in: number
scope: string scope: string
token_type: 'bearer' token_type: "bearer"
} }

View File

@@ -11,7 +11,7 @@ const AUTHENTICATION_SERVER = "https://api.remanso.space/auth/github"
const personalTokenId = "token" const personalTokenId = "token"
export const signIn = async ( export const signIn = async (
code: string, code: string
): Promise<GithubToken | GithubTokenError> => { ): Promise<GithubToken | GithubTokenError> => {
const authenticationServerURL = new URL(AUTHENTICATION_SERVER) const authenticationServerURL = new URL(AUTHENTICATION_SERVER)
authenticationServerURL.searchParams.set("code", code) authenticationServerURL.searchParams.set("code", code)
@@ -84,12 +84,12 @@ export const saveAccessToken = async (githubToken: GithubToken) => {
const expirationDate = addSeconds( const expirationDate = addSeconds(
new Date(), new Date(),
githubToken.expires_in, githubToken.expires_in
).toISOString() ).toISOString()
const refreshTokenExpirationDate = addSeconds( const refreshTokenExpirationDate = addSeconds(
new Date(), new Date(),
githubToken.refresh_token_expires_in, githubToken.refresh_token_expires_in
).toISOString() ).toISOString()
const accessToken: GithubAccessToken = { const accessToken: GithubAccessToken = {
@@ -102,11 +102,11 @@ export const saveAccessToken = async (githubToken: GithubToken) => {
refreshToken: githubToken.refresh_token, refreshToken: githubToken.refresh_token,
refreshTokenExpiresIn: githubToken.refresh_token_expires_in, refreshTokenExpiresIn: githubToken.refresh_token_expires_in,
refreshTokenExpirationDate, refreshTokenExpirationDate,
username: "", username: ""
} }
const octokit = new Octokit({ const octokit = new Octokit({
auth: accessToken?.token, auth: accessToken?.token
}) })
const user = await octokit.request("GET /user") const user = await octokit.request("GET /user")

View File

@@ -7,98 +7,102 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: "/repo-list", path: "/repo-list",
name: "RepoList", name: "RepoList",
component: () => import("@/views/RepoList.vue"), component: () => import("@/views/RepoList.vue")
}, },
{ {
path: "/:user/:repo", path: "/:user/:repo",
name: "FluxNoteView", name: "FluxNoteView",
props: true, props: true,
component: () => import("@/views/FluxNoteView.vue"), component: () => import("@/views/FluxNoteView.vue")
}, },
{ {
path: "/tiboudenote", path: "/tiboudenote",
name: "PublicNoteListView", name: "PublicNoteListView",
component: () => import("@/views/PublicNoteListView.vue"), component: () => import("@/views/PublicNoteListView.vue")
}, },
{ {
path: "/pub", path: "/pub",
name: "PublicNoteListView", name: "PublicNoteListView",
component: () => import("@/views/PublicNoteListView.vue"), component: () => import("@/views/PublicNoteListView.vue")
}, },
{ {
path: "/pub/:shortDid", path: "/pub/:shortDid",
name: "PublicNoteListByDidView", name: "PublicNoteListByDidView",
props: true, props: true,
component: () => import("@/views/PublicNoteListByDidView.vue"), component: () => import("@/views/PublicNoteListByDidView.vue")
}, },
{ {
path: "/pub/:shortDid/:rkey/:slug?", path: "/pub/:shortDid/:rkey/:slug?",
name: "PublicNoteView", name: "PublicNoteView",
props: true, props: true,
component: () => import("@/views/PublicNoteView.vue"), component: () => import("@/views/PublicNoteView.vue")
}, },
{ {
path: "/:user/:repo/inbox", path: "/:user/:repo/inbox",
name: "FleetingNotes", name: "FleetingNotes",
props: true, props: true,
component: () => import("@/views/FleetingNotes.vue"), component: () => import("@/views/FleetingNotes.vue")
}, },
{ {
path: "/:user/:repo/draft", path: "/:user/:repo/draft",
name: "DraftNotes", name: "DraftNotes",
props: true, props: true,
component: () => import("@/views/DraftNotes.vue"), component: () => import("@/views/DraftNotes.vue")
}, },
{ {
path: "/:user/:repo/todo", path: "/:user/:repo/todo",
name: "TodoNotes", name: "TodoNotes",
props: true, props: true,
component: () => import("@/views/TodoNotes.vue"), component: () => import("@/views/TodoNotes.vue")
}, },
{ {
path: "/:user/:repo/history", path: "/:user/:repo/history",
name: "HistoricNotes", name: "HistoricNotes",
props: true, props: true,
component: () => import("@/views/HistoricNotes.vue"), component: () => import("@/views/HistoricNotes.vue")
}, },
{ {
path: "/:user/:repo/spaced-repetition", path: "/:user/:repo/spaced-repetition",
name: "SpacedRepetitionCard", name: "SpacedRepetitionCard",
props: true, props: true,
component: () => import("@/views/SpacedRepetitionCard.vue"), component: () => import("@/views/SpacedRepetitionCard.vue")
}, },
{ {
path: "/:user/:repo/need-review-cards", path: "/:user/:repo/need-review-cards",
name: "NeedReviewCards", name: "NeedReviewCards",
props: true, props: true,
component: () => import("@/views/NeedReviewCards.vue"), component: () => import("@/views/NeedReviewCards.vue")
}, },
{ {
path: "/about", path: "/about",
name: "About", name: "About",
component: () => import("@/views/AboutApp.vue"), component: () => import("@/views/AboutApp.vue")
}, },
{ {
path: "/", path: "/",
name: "Home", name: "Home",
component: Home, component: Home
}, },
{ {
path: "/:catchAll(.*)", path: "/:catchAll(.*)",
name: "SpaceCowboy", name: "SpaceCowboy",
component: () => import("@/views/SpaceCowboy.vue"), component: () => import("@/views/SpaceCowboy.vue")
}, }
] ]
export const router = createRouter({ export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, routes
}) })
router.beforeEach(() => { router.beforeEach(() => {
if (!("startViewTransition" in document)) return if (!("startViewTransition" in document)) return
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
;(document as Document & { startViewTransition: (cb: () => Promise<void>) => void }).startViewTransition(async () => { ;(
document as Document & {
startViewTransition: (cb: () => Promise<void>) => void
}
).startViewTransition(async () => {
resolve() resolve()
await nextTick() await nextTick()
}) })

View File

@@ -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: 'garden', light: "garden",
dark: 'dim', dark: "dim"
} }

View File

@@ -1,9 +1,9 @@
export const decodeBase64ToUTF8 = (content: string): string => { export const decodeBase64ToUTF8 = (content: string): string => {
return decodeURIComponent( return decodeURIComponent(
atob(content) atob(content)
.split('') .split("")
.map((char) => `%${('00' + char.charCodeAt(0).toString(16)).slice(-2)}`) .map((char) => `%${("00" + char.charCodeAt(0).toString(16)).slice(-2)}`)
.join('') .join("")
) )
} }
export const encodeUTF8ToBase64 = (content: string): string => { export const encodeUTF8ToBase64 = (content: string): string => {

View File

@@ -6,7 +6,7 @@ export const displayLanguage = (langCode?: string): string | null => {
try { try {
const locale = navigator.language ?? langCode const locale = navigator.language ?? langCode
const display = new Intl.DisplayNames([locale], { const display = new Intl.DisplayNames([locale], {
type: "language", type: "language"
}) })
return display.of(langCode) ?? null return display.of(langCode) ?? null

View File

@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* oxlint-disable typescript/no-explicit-any */
// We can only detect video/audio files from the extension in the URL. // We can only detect video/audio files from the extension in the URL.
// We ignore MP1 and MP2 (not in active use) and default to video for ambiguous // We ignore MP1 and MP2 (not in active use) and default to video for ambiguous
import MarkdownIt from 'markdown-it' import MarkdownIt from "markdown-it"
// extensions (MPG, MP4) // extensions (MPG, MP4)
const validAudioExtensions = ['aac', 'm4a', 'mp3', 'oga', 'ogg', 'wav'] const validAudioExtensions = ["aac", "m4a", "mp3", "oga", "ogg", "wav"]
const validVideoExtensions = ['mp4', 'm4v', 'ogv', 'webm', 'mpg', 'mpeg'] const validVideoExtensions = ["mp4", "m4v", "ogv", "webm", "mpg", "mpeg"]
/** /**
* @property {Object} messages * @property {Object} messages
@@ -19,13 +19,13 @@ const validVideoExtensions = ['mp4', 'm4v', 'ogv', 'webm', 'mpg', 'mpeg']
*/ */
let messages: { [key: string]: any } = { let messages: { [key: string]: any } = {
en: { en: {
'html5 video not supported': "html5 video not supported":
'Your browser does not support playing HTML5 video.', "Your browser does not support playing HTML5 video.",
'html5 audio not supported': "html5 audio not supported":
'Your browser does not support playing HTML5 audio.', "Your browser does not support playing HTML5 audio.",
'html5 media fallback link': "html5 media fallback link":
'You can <a href="%s" download>download the file</a> instead.', 'You can <a href="%s" download>download the file</a> instead.',
'html5 media description': 'Here is a description of the content: %s' "html5 media description": "Here is a description of the content: %s"
} }
} }
@@ -37,18 +37,18 @@ let translate = (
// Revert back to English default if no message object, or no translation // Revert back to English default if no message object, or no translation
// for this language // for this language
if (!messages[language] || !messages[language][messageKey]) { if (!messages[language] || !messages[language][messageKey]) {
language = 'en' language = "en"
} }
if (!messages[language]) { if (!messages[language]) {
return '' return ""
} }
let message = messages[language][messageKey] || '' let message = messages[language][messageKey] || ""
if (messageParams) if (messageParams)
for (const param of messageParams) { for (const param of messageParams) {
message = message.replace('%s', param) message = message.replace("%s", param)
} }
return message return message
@@ -96,7 +96,7 @@ function tokenizeImagesAndMedia(
} }
): boolean { ): boolean {
let attrs, code, label, pos, ref, res, title, tokens: never[], start let attrs, code, label, pos, ref, res, title, tokens: never[], start
let href = '' let href = ""
const oldPos = state.pos const oldPos = state.pos
const max = state.posMax const max = state.posMax
@@ -140,7 +140,7 @@ function tokenizeImagesAndMedia(
if (state.md.validateLink(href)) { if (state.md.validateLink(href)) {
pos = res.pos pos = res.pos
} else { } else {
href = '' href = ""
} }
} }
@@ -166,7 +166,7 @@ function tokenizeImagesAndMedia(
if (!md.utils.isSpace(code) && code !== 0x0a) break if (!md.utils.isSpace(code) && code !== 0x0a) break
} }
} else { } else {
title = '' title = ""
} }
if (pos >= max || state.src.charCodeAt(pos) !== 0x29) { if (pos >= max || state.src.charCodeAt(pos) !== 0x29) {
@@ -179,7 +179,7 @@ function tokenizeImagesAndMedia(
// //
// Link reference // Link reference
// //
if (typeof state.env.references === 'undefined') return false if (typeof state.env.references === "undefined") return false
if (pos < max && state.src.charCodeAt(pos) === 0x5b) { if (pos < max && state.src.charCodeAt(pos) === 0x5b) {
// Bracket: [ // Bracket: [
@@ -219,15 +219,15 @@ function tokenizeImagesAndMedia(
state.md.inline.parse(content, state.md, state.env, (tokens = [])) state.md.inline.parse(content, state.md, state.env, (tokens = []))
const mediaType = guessMediaType(href) const mediaType = guessMediaType(href)
const tag = mediaType == 'image' ? 'img' : mediaType const tag = mediaType == "image" ? "img" : mediaType
const token = state.push(mediaType, tag, 0) const token = state.push(mediaType, tag, 0)
token.attrs = attrs = [['src', href]] token.attrs = attrs = [["src", href]]
if (mediaType == 'image') attrs.push(['alt', '']) if (mediaType == "image") attrs.push(["alt", ""])
token.children = tokens token.children = tokens
token.content = content token.content = content
if (title) attrs.push(['title', title]) if (title) attrs.push(["title", title])
state.pos = pos state.pos = pos
state.posMax = max state.posMax = max
@@ -247,13 +247,13 @@ function tokenizeImagesAndMedia(
*/ */
function guessMediaType(url: string): string { function guessMediaType(url: string): string {
const extensionMatch = url.match(/\.([^/.]+)$/) const extensionMatch = url.match(/\.([^/.]+)$/)
if (extensionMatch === null) return 'image' if (extensionMatch === null) return "image"
const extension = extensionMatch[1] const extension = extensionMatch[1]
if (validAudioExtensions.indexOf(extension.toLowerCase()) != -1) if (validAudioExtensions.indexOf(extension.toLowerCase()) != -1)
return 'audio' return "audio"
else if (validVideoExtensions.indexOf(extension.toLowerCase()) != -1) else if (validVideoExtensions.indexOf(extension.toLowerCase()) != -1)
return 'video' return "video"
else return 'image' else return "image"
} }
/** /**
@@ -283,37 +283,37 @@ function renderMedia(
const token = tokens[idx] const token = tokens[idx]
const type = token.type const type = token.type
if (!token.attrs || (type !== 'video' && type !== 'audio')) { if (!token.attrs || (type !== "video" && type !== "audio")) {
return '' return ""
} }
let attrs = options.html5Media[`${type}Attrs`].trim() let attrs = options.html5Media[`${type}Attrs`].trim()
if (attrs) { if (attrs) {
attrs = ' ' + attrs attrs = " " + attrs
} }
// We'll always have a URL for non-image media: they are detected by URL // We'll always have a URL for non-image media: they are detected by URL
const url = token.attrs[token.attrIndex('src')][1] const url = token.attrs[token.attrIndex("src")][1]
// Title is set like this: ![descriptive text](video.mp4 "title") // Title is set like this: ![descriptive text](video.mp4 "title")
const title = const title =
token.attrIndex('title') != -1 token.attrIndex("title") != -1
? ` title="${md.utils.escapeHtml( ? ` title="${md.utils.escapeHtml(
token.attrs[token.attrIndex('title')][1] token.attrs[token.attrIndex("title")][1]
)}"` )}"`
: '' : ""
const fallbackText = const fallbackText =
translate(env.language, `html5 ${type} not supported`) + translate(env.language, `html5 ${type} not supported`) +
'\n' + "\n" +
translate(env.language, 'html5 media fallback link', [url]) translate(env.language, "html5 media fallback link", [url])
const description = token.content const description = token.content
? '\n' + ? "\n" +
translate(env.language, 'html5 media description', [ translate(env.language, "html5 media description", [
md.utils.escapeHtml(token.content) md.utils.escapeHtml(token.content)
]) ])
: '' : ""
return ( return (
`<${type} src="${url}"${title}${attrs}>\n` + `<${type} src="${url}"${title}${attrs}>\n` +
@@ -369,7 +369,7 @@ export const html5Media = (
? options.audioAttrs ? options.audioAttrs
: 'controls class="html5-audio-player"' : 'controls class="html5-audio-player"'
md.inline.ruler.at('image', (tokens: any, silent: any) => md.inline.ruler.at("image", (tokens: any, silent: any) =>
tokenizeImagesAndMedia(tokens, silent, md) tokenizeImagesAndMedia(tokens, silent, md)
) )

Some files were not shown because too many files have changed in this diff Show More