Compare commits
54 Commits
chore/migr
...
70b679b204
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70b679b204 | ||
|
|
36dc1293f9 | ||
|
|
801b7cb94a | ||
|
|
1fa66d8594 | ||
|
|
b827f31cf0 | ||
|
|
cf02569c75 | ||
|
|
0a4f8dbf41 | ||
|
|
b6f6759af5 | ||
|
|
c42c26a407 | ||
|
|
cfe5ef8fcd | ||
|
|
4c5116bc89 | ||
|
|
8581baafb7 | ||
|
|
29c092e0a0 | ||
|
|
410c0cec7c | ||
|
|
66a1bcbaa9 | ||
|
|
541e058d12 | ||
|
|
a05ff9f238 | ||
|
|
6558de8df5 | ||
|
|
b48c1bd0d5 | ||
|
|
e369541dc0 | ||
|
|
73a6014750 | ||
|
|
c197b80095 | ||
|
|
f3e74aed34 | ||
|
|
8d9134a062 | ||
|
|
006cd63388 | ||
|
|
3de9eb35f6 | ||
|
|
99c349f6df | ||
|
|
64b29bcdef | ||
|
|
9e26e231cb | ||
|
|
b003a3e008 | ||
|
|
1b5e23e3d4 | ||
|
|
52d7c84bd0 | ||
|
|
d76182b2c2 | ||
|
|
ed1a6b7fba | ||
|
|
d5b251c4a0 | ||
|
|
19b77810ec | ||
|
|
c8b0a78973 | ||
|
|
087d1a355e | ||
|
|
5d90da8ab5 | ||
|
|
72d065975d | ||
|
|
8b3df48791 | ||
|
|
cd8e173e05 | ||
|
|
8767f7c430 | ||
|
|
369a200a42 | ||
|
|
06eaa3c9a7 | ||
|
|
4cbcf42e3d | ||
|
|
a0be25c0dd | ||
|
|
dcee26100f | ||
|
|
ac68c68f8a | ||
|
|
982f3070a1 | ||
|
|
20e9538983 | ||
|
|
10c3e1ca60 | ||
| 6dc98c80ca | |||
|
|
1aef212a36 |
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "remanso-skills",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Local skills for the Remanso project",
|
|
||||||
"author": {
|
|
||||||
"name": "julien"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
---
|
|
||||||
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)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,7 +2,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ RUN pnpm run build
|
|||||||
FROM nginx:alpine AS runner
|
FROM nginx:alpine AS runner
|
||||||
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s \
|
|
||||||
CMD wget -qO- http://localhost:80/ || exit 1
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ 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}'$1`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Écrire le contenu mis à jour dans le fichier
|
// Écrire le contenu mis à jour dans le fichier
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
# Remanso React Native Migration — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-04-20
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Migrate Remanso from a Vue 3 web app to a fully native iOS + Android app built with Expo (React Native). The primary motivation is native feel: fluid stack animations, swipe-back gestures, and native navigation chrome. The scope is a full replacement of the web app.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Concern | Current (Vue) | React Native |
|
||||||
|
|---|---|---|
|
||||||
|
| Framework | Vue 3 + Vite | Expo SDK (managed workflow) |
|
||||||
|
| Routing | Vue Router | Expo Router (file-system routing over React Navigation v7) |
|
||||||
|
| State | Pinia | Zustand |
|
||||||
|
| Server state | TanStack Vue Query | TanStack Query (same library) |
|
||||||
|
| Styling | DaisyUI + Tailwind CSS | NativeWind v4 |
|
||||||
|
| Local DB | PouchDB (IndexedDB) | Expo SQLite |
|
||||||
|
| Simple KV store | localStorage | MMKV |
|
||||||
|
| Auth | OAuth redirects | expo-auth-session |
|
||||||
|
| GitHub API | @octokit/rest | @octokit/rest (unchanged) |
|
||||||
|
| i18n | vue-i18n | react-i18next |
|
||||||
|
| Date utils | date-fns | date-fns (unchanged) |
|
||||||
|
| Markdown content | markdown-it (DOM) | react-native-webview |
|
||||||
|
| Fonts | CSS custom properties | expo-font |
|
||||||
|
|
||||||
|
## Navigation Structure
|
||||||
|
|
||||||
|
Expo Router generates a React Navigation v7 tree from the file system. The stacked note pattern — the core native feel win — maps to a nested Stack navigator where each backlink push adds a note with a native slide animation and swipe-left pops it.
|
||||||
|
|
||||||
|
```
|
||||||
|
Root Stack
|
||||||
|
├── Auth screens (unauthenticated, no tab bar)
|
||||||
|
│ ├── Home / Welcome
|
||||||
|
│ ├── GitHub OAuth callback
|
||||||
|
│ └── ATProto OAuth callback
|
||||||
|
│
|
||||||
|
└── Authenticated App — Bottom Tabs
|
||||||
|
├── Tab: Feed (Stack)
|
||||||
|
│ ├── Repo picker
|
||||||
|
│ └── Note Stack (nested Stack)
|
||||||
|
│ ├── Note (root)
|
||||||
|
│ ├── Note (pushed via backlink) ← swipe-back to pop
|
||||||
|
│ └── Note (pushed via backlink) ← swipe-back to pop
|
||||||
|
│
|
||||||
|
├── Tab: Inbox / Drafts / Todos (Stack)
|
||||||
|
│ └── Note list → Note detail
|
||||||
|
│
|
||||||
|
├── Tab: Public Notes (Stack)
|
||||||
|
│ ├── Public note list
|
||||||
|
│ └── Public note detail
|
||||||
|
│
|
||||||
|
└── Tab: Settings (Stack)
|
||||||
|
├── Settings root
|
||||||
|
└── Font picker (presented as modal)
|
||||||
|
```
|
||||||
|
|
||||||
|
History and Spaced Repetition are accessible as sections within the Feed tab or as additional tabs — to be decided during implementation.
|
||||||
|
|
||||||
|
## Data Layer
|
||||||
|
|
||||||
|
PouchDB is replaced by **Expo SQLite** for structured data and **MMKV** for simple key-value preferences.
|
||||||
|
|
||||||
|
### Expo SQLite schema
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS github_tokens (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
access_token TEXT NOT NULL,
|
||||||
|
refresh_token TEXT,
|
||||||
|
expires_at INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS atproto_sessions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
did TEXT NOT NULL
|
||||||
|
-- full session_json stored in Expo SecureStore keyed by id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS saved_repos (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user TEXT NOT NULL,
|
||||||
|
repo TEXT NOT NULL,
|
||||||
|
files_json TEXT NOT NULL,
|
||||||
|
cached_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS history (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user TEXT NOT NULL,
|
||||||
|
repo TEXT NOT NULL,
|
||||||
|
note_path TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing `DataApi` interface (`add`, `update`, `remove`, `get`, `getAll`) is re-implemented as a thin wrapper over Expo SQLite. No ORM — plain async SQL with `CREATE TABLE IF NOT EXISTS` on first open handles schema initialization at this scale.
|
||||||
|
|
||||||
|
Sensitive data (access tokens, refresh tokens, ATProto sessions) is stored in **Expo SecureStore** rather than plain SQLite. SQLite holds only metadata and content cache.
|
||||||
|
|
||||||
|
### MMKV
|
||||||
|
|
||||||
|
Replaces localStorage for user settings: font family, font size, theme preference. Synchronous reads, no async overhead.
|
||||||
|
|
||||||
|
### No Web Worker
|
||||||
|
|
||||||
|
Database calls move to the main thread via Expo SQLite's async API. The performance concern that justified the Web Worker on web does not apply on mobile.
|
||||||
|
|
||||||
|
## Auth Flows
|
||||||
|
|
||||||
|
### GitHub OAuth
|
||||||
|
|
||||||
|
The existing auth server (`api.remanso.space/auth/github`) handles code exchange — the client flow is unchanged:
|
||||||
|
|
||||||
|
1. `expo-auth-session` opens an in-app browser tab with the GitHub authorize URL
|
||||||
|
2. OAuth redirect captured by Expo's deep link handler
|
||||||
|
3. Code sent to `api.remanso.space/auth/github?code=...` for token exchange
|
||||||
|
4. Access token + refresh token stored in Expo SecureStore
|
||||||
|
5. Token refresh logic (15-minute pre-expiry check) stays the same; HTTP calls use `fetch`
|
||||||
|
|
||||||
|
### ATProto / Bluesky OAuth
|
||||||
|
|
||||||
|
`@atproto/oauth-client-browser` is browser-only (IndexedDB, `window.crypto`, browser redirects). There is no official React Native client. A custom client (~200–300 lines) is implemented using:
|
||||||
|
|
||||||
|
- `expo-auth-session` for the OAuth redirect flow (PKCE)
|
||||||
|
- `expo-crypto` for PKCE code verifier/challenge generation
|
||||||
|
- Expo SecureStore for session persistence
|
||||||
|
- The same Bluesky API endpoints as the browser client
|
||||||
|
|
||||||
|
This keeps ATProto auth fully client-side, consistent with the app's current architecture.
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
Zustand replaces Pinia. The store shape is identical to `userRepo.store.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const useRepoStore = create<RepoState>((set, get) => ({
|
||||||
|
user: '',
|
||||||
|
repo: '',
|
||||||
|
files: [],
|
||||||
|
userSettings: null,
|
||||||
|
needToLogin: false,
|
||||||
|
setRepo: (user, repo) => set({ user, repo }),
|
||||||
|
loadFiles: async () => { /* Octokit call */ },
|
||||||
|
loadSettings: async () => { /* MMKV read */ },
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
TanStack Query handles all GitHub API server state (file fetching, README, repo listing) — same library, same patterns as today.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
NativeWind v4 provides Tailwind utility classes in React Native. DaisyUI is web-only and has no React Native equivalent — all component styling (buttons, cards, modals) is hand-written using NativeWind utilities.
|
||||||
|
|
||||||
|
The two DaisyUI themes (`retro` light, `coffee` dark) are translated into a custom NativeWind theme in `tailwind.config.ts` with the same color tokens. System appearance (`useColorScheme`) drives theme selection.
|
||||||
|
|
||||||
|
Font customization uses `expo-font` for loading custom fonts and React Native's `fontFamily` style prop, replacing the CSS custom property approach.
|
||||||
|
|
||||||
|
## Markdown Rendering
|
||||||
|
|
||||||
|
The markdown-it pipeline (KaTeX, Mermaid, shiki, tabler icons, html5-media, GitHub alerts, checkboxes) runs in the React Native JS context unchanged — same code, same output. The resulting HTML string is passed to a `NoteWebView` component built on `react-native-webview`.
|
||||||
|
|
||||||
|
`NoteWebView` is a native UIView/View wrapper around a WebView engine. It is a React Native component — not a web app. The surrounding app (navigation chrome, tab bar, headers, settings, auth screens) is 100% native. Only the note content pane renders HTML. This is the standard pattern for rich content in React Native (used by GitHub Mobile, Linear, and others).
|
||||||
|
|
||||||
|
The WebView communicates back to the native layer via `postMessage` for:
|
||||||
|
- Internal note link taps (trigger React Navigation push)
|
||||||
|
- External URL taps (open in system browser)
|
||||||
|
- Backlink detection events
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # Expo Router — file-system screens
|
||||||
|
│ ├── _layout.tsx # Root Stack navigator
|
||||||
|
│ ├── index.tsx # Home / Welcome
|
||||||
|
│ └── (tabs)/ # Authenticated tab navigator
|
||||||
|
│ ├── _layout.tsx
|
||||||
|
│ ├── feed/ # Feed + Note Stack screens
|
||||||
|
│ ├── inbox/
|
||||||
|
│ ├── public/
|
||||||
|
│ └── settings/
|
||||||
|
│
|
||||||
|
├── modules/ # Feature domains (mirrors current structure)
|
||||||
|
│ ├── note/ # Note models, hooks, caching
|
||||||
|
│ ├── repo/ # Zustand store, Octokit service
|
||||||
|
│ ├── user/
|
||||||
|
│ │ ├── auth/ # GitHub + ATProto OAuth hooks
|
||||||
|
│ │ └── fonts.ts # Font downloading (was utils/downloadFont.ts)
|
||||||
|
│ ├── card/ # Spaced repetition
|
||||||
|
│ ├── history/ # Edit history
|
||||||
|
│ ├── atproto/ # Custom ATProto OAuth client, DID resolution
|
||||||
|
│ └── post/ # ts-rest API client (unchanged)
|
||||||
|
│
|
||||||
|
├── components/ # Shared UI components
|
||||||
|
│
|
||||||
|
├── rendering/ # Markdown pipeline — first-class module
|
||||||
|
│ ├── pipeline.ts # markdown-it setup + plugin registration
|
||||||
|
│ ├── plugins/ # Custom plugins
|
||||||
|
│ │ ├── html5-media.ts
|
||||||
|
│ │ ├── regexp.ts
|
||||||
|
│ │ └── tabler-icons.ts
|
||||||
|
│ └── NoteWebView.tsx # react-native-webview wrapper
|
||||||
|
│
|
||||||
|
├── lib/ # Shared low-level, stateless helpers
|
||||||
|
│ ├── text.ts # slugify, noteTitle, displayLanguage
|
||||||
|
│ ├── links.ts # link.ts + youtube.ts
|
||||||
|
│ ├── encoding.ts # decodeBase64ToUTF8
|
||||||
|
│ └── notifications.ts # notif.ts
|
||||||
|
│
|
||||||
|
├── hooks/ # React hooks
|
||||||
|
├── data/ # Expo SQLite wrapper (same DataApi interface)
|
||||||
|
├── locales/ # i18n strings (unchanged)
|
||||||
|
└── constants/ # Theme tokens, note width constants
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Risks
|
||||||
|
|
||||||
|
1. **ATProto custom OAuth client** — no official SDK; requires careful PKCE implementation and session lifecycle management.
|
||||||
|
2. **DaisyUI → NativeWind component styling** — no 1:1 mapping; all themed components need to be rebuilt. Most labor-intensive non-feature work.
|
||||||
|
3. **NoteWebView ↔ native bridge** — link tap handling and scroll coordination between the WebView and the native Stack navigator require careful implementation to feel seamless.
|
||||||
|
4. **Mermaid in WebView** — Mermaid is JS-heavy; initial render may be slow on lower-end Android. May need lazy rendering or a timeout fallback.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- PWA / service worker (not applicable to native)
|
||||||
|
- Web Worker (replaced by async SQLite)
|
||||||
|
- Comlink (not applicable)
|
||||||
|
- Server-side rendering
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" data-theme="garden">
|
<html lang="en" data-theme="emerald">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
|||||||
9
nginx.conf
Normal file
9
nginx.conf
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,11 +28,14 @@
|
|||||||
"@tailwindcss/postcss": "^4.1.16",
|
"@tailwindcss/postcss": "^4.1.16",
|
||||||
"@tanstack/vue-query": "^5.92.9",
|
"@tanstack/vue-query": "^5.92.9",
|
||||||
"@toycode/markdown-it-class": "^1.2.4",
|
"@toycode/markdown-it-class": "^1.2.4",
|
||||||
|
"@ts-rest/core": "^3.52.1",
|
||||||
|
"@ts-rest/vue-query": "^3.52.1",
|
||||||
"@vscode/markdown-it-katex": "^1.1.2",
|
"@vscode/markdown-it-katex": "^1.1.2",
|
||||||
"@vueuse/components": "^14.2.1",
|
"@vueuse/components": "^14.2.1",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^13.6.0",
|
||||||
"@vueuse/router": "^13.6.0",
|
"@vueuse/router": "^13.6.0",
|
||||||
"arktype": "^2.1.29",
|
"arktype": "^2.1.29",
|
||||||
|
"comlink": "^4.4.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"font-color-contrast": "^11.1.0",
|
"font-color-contrast": "^11.1.0",
|
||||||
|
|||||||
105
pnpm-lock.yaml
generated
105
pnpm-lock.yaml
generated
@@ -38,6 +38,12 @@ importers:
|
|||||||
'@toycode/markdown-it-class':
|
'@toycode/markdown-it-class':
|
||||||
specifier: ^1.2.4
|
specifier: ^1.2.4
|
||||||
version: 1.2.4
|
version: 1.2.4
|
||||||
|
'@ts-rest/core':
|
||||||
|
specifier: ^3.52.1
|
||||||
|
version: 3.52.1(@types/node@22.15.24)(zod@3.25.76)
|
||||||
|
'@ts-rest/vue-query':
|
||||||
|
specifier: ^3.52.1
|
||||||
|
version: 3.52.1(@tanstack/vue-query@5.92.9(vue@3.5.18(typescript@5.9.3)))(@ts-rest/core@3.52.1(@types/node@22.15.24)(zod@3.25.76))(zod@3.25.76)
|
||||||
'@vscode/markdown-it-katex':
|
'@vscode/markdown-it-katex':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
@@ -53,6 +59,9 @@ importers:
|
|||||||
arktype:
|
arktype:
|
||||||
specifier: ^2.1.29
|
specifier: ^2.1.29
|
||||||
version: 2.1.29
|
version: 2.1.29
|
||||||
|
comlink:
|
||||||
|
specifier: ^4.4.2
|
||||||
|
version: 4.4.2
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -1053,12 +1062,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.4.0':
|
|
||||||
resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==}
|
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
|
||||||
peerDependencies:
|
|
||||||
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
|
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.7.0':
|
'@eslint-community/eslint-utils@4.7.0':
|
||||||
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@@ -1069,10 +1072,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.6.2':
|
|
||||||
resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==}
|
|
||||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
|
||||||
|
|
||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
@@ -2119,6 +2118,27 @@ packages:
|
|||||||
'@toycode/markdown-it-class@1.2.4':
|
'@toycode/markdown-it-class@1.2.4':
|
||||||
resolution: {integrity: sha512-hA4gHBK8moObkOYdWTjhy1wYcYy0MJeM3JjSKbsXHRpRMvIKhk6Jm+t3bXsSScTdz/byWqQbs8YIwVYjHp+SlQ==}
|
resolution: {integrity: sha512-hA4gHBK8moObkOYdWTjhy1wYcYy0MJeM3JjSKbsXHRpRMvIKhk6Jm+t3bXsSScTdz/byWqQbs8YIwVYjHp+SlQ==}
|
||||||
|
|
||||||
|
'@ts-rest/core@3.52.1':
|
||||||
|
resolution: {integrity: sha512-tAjz7Kxq/grJodcTA1Anop4AVRDlD40fkksEV5Mmal88VoZeRKAG8oMHsDwdwPZz+B/zgnz0q2sF+cm5M7Bc7g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': ^18.18.7 || >=20.8.4
|
||||||
|
zod: ^3.22.3
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@ts-rest/vue-query@3.52.1':
|
||||||
|
resolution: {integrity: sha512-89u7aS9LGDC7uNUC5CagWX1EB7vTwyXohYcizLi1D9v7MD/Cnu5OTQNf8SY3PuAK62RcFJXB2XZGsMAPC0svNw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tanstack/vue-query': ^4.0.0
|
||||||
|
'@ts-rest/core': ~3.52.0
|
||||||
|
zod: ^3.22.3
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
@@ -2985,6 +3005,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
comlink@4.4.2:
|
||||||
|
resolution: {integrity: sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==}
|
||||||
|
|
||||||
commander@2.20.3:
|
commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
@@ -3275,15 +3298,6 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
|
||||||
engines: {node: '>=6.0'}
|
|
||||||
peerDependencies:
|
|
||||||
supports-color: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
supports-color:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -4959,9 +4973,6 @@ packages:
|
|||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
ms@2.1.2:
|
|
||||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -7642,25 +7653,17 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.25.5':
|
'@esbuild/win32-x64@0.25.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)':
|
|
||||||
dependencies:
|
|
||||||
eslint: 8.57.1
|
|
||||||
eslint-visitor-keys: 3.4.3
|
|
||||||
|
|
||||||
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
|
'@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-visitor-keys: 3.4.3
|
eslint-visitor-keys: 3.4.3
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.12.1':
|
'@eslint-community/regexpp@4.12.1': {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@eslint-community/regexpp@4.6.2': {}
|
|
||||||
|
|
||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.3.4
|
debug: 4.4.3
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.20.0
|
globals: 13.20.0
|
||||||
ignore: 5.2.4
|
ignore: 5.2.4
|
||||||
@@ -7676,7 +7679,7 @@ snapshots:
|
|||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
debug: 4.3.4
|
debug: 4.4.3
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -8424,6 +8427,18 @@ snapshots:
|
|||||||
|
|
||||||
'@toycode/markdown-it-class@1.2.4': {}
|
'@toycode/markdown-it-class@1.2.4': {}
|
||||||
|
|
||||||
|
'@ts-rest/core@3.52.1(@types/node@22.15.24)(zod@3.25.76)':
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 22.15.24
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
|
'@ts-rest/vue-query@3.52.1(@tanstack/vue-query@5.92.9(vue@3.5.18(typescript@5.9.3)))(@ts-rest/core@3.52.1(@types/node@22.15.24)(zod@3.25.76))(zod@3.25.76)':
|
||||||
|
dependencies:
|
||||||
|
'@tanstack/vue-query': 5.92.9(vue@3.5.18(typescript@5.9.3))
|
||||||
|
'@ts-rest/core': 3.52.1(@types/node@22.15.24)(zod@3.25.76)
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -8656,7 +8671,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.46.2
|
'@typescript-eslint/visitor-keys': 8.46.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -8676,7 +8691,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -8707,7 +8722,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.46.2(eslint@8.57.1)(typescript@5.9.3)
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -8742,7 +8757,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.46.2
|
'@typescript-eslint/types': 8.46.2
|
||||||
'@typescript-eslint/visitor-keys': 8.46.2
|
'@typescript-eslint/visitor-keys': 8.46.2
|
||||||
debug: 4.4.1
|
debug: 4.4.3
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
@@ -9542,6 +9557,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
|
comlink@4.4.2: {}
|
||||||
|
|
||||||
commander@2.20.3: {}
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@7.2.0: {}
|
commander@7.2.0: {}
|
||||||
@@ -9836,10 +9853,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
debug@4.3.4:
|
|
||||||
dependencies:
|
|
||||||
ms: 2.1.2
|
|
||||||
|
|
||||||
debug@4.4.1:
|
debug@4.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -10155,8 +10168,8 @@ snapshots:
|
|||||||
|
|
||||||
eslint@8.57.1:
|
eslint@8.57.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1)
|
||||||
'@eslint-community/regexpp': 4.6.2
|
'@eslint-community/regexpp': 4.12.1
|
||||||
'@eslint/eslintrc': 2.1.4
|
'@eslint/eslintrc': 2.1.4
|
||||||
'@eslint/js': 8.57.1
|
'@eslint/js': 8.57.1
|
||||||
'@humanwhocodes/config-array': 0.13.0
|
'@humanwhocodes/config-array': 0.13.0
|
||||||
@@ -10165,8 +10178,8 @@ snapshots:
|
|||||||
'@ungap/structured-clone': 1.2.0
|
'@ungap/structured-clone': 1.2.0
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.6
|
||||||
debug: 4.3.4
|
debug: 4.4.3
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@@ -11745,8 +11758,6 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.0.0: {}
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.2: {}
|
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
multiformats@9.9.0: {}
|
multiformats@9.9.0: {}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const { isATProtoReady } = useATProtoLogin()
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#main-app {
|
#main-app {
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const goBack = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a class="btn btn-sm back-button" @click="goBack">
|
<button class="btn btn-sm back-button text-base-content" @click="goBack">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icon-tabler-arrow-narrow-left"
|
class="icon icon-tabler icon-tabler-arrow-narrow-left"
|
||||||
@@ -41,5 +41,5 @@ const goBack = () => {
|
|||||||
<line x1="5" y1="12" x2="9" y2="16" />
|
<line x1="5" y1="12" x2="9" y2="16" />
|
||||||
<line x1="5" y1="12" x2="9" y2="8" />
|
<line x1="5" y1="12" x2="9" y2="8" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
defineAsyncComponent,
|
|
||||||
nextTick,
|
nextTick,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
watch
|
watch
|
||||||
} from "vue"
|
} from "vue"
|
||||||
|
|
||||||
|
import HeaderNote from "@/components/HeaderNote.vue"
|
||||||
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
import StackedNote from "@/components/StackedNote.vue"
|
import StackedNote from "@/components/StackedNote.vue"
|
||||||
import { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
@@ -21,10 +21,6 @@ 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"
|
||||||
|
|
||||||
const HeaderNote = defineAsyncComponent(
|
|
||||||
() => import("@/components/HeaderNote.vue")
|
|
||||||
)
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
user: string
|
user: string
|
||||||
|
|||||||
@@ -7,50 +7,106 @@ import { useUserRepoStore } from "../modules/repo/store/userRepo.store"
|
|||||||
|
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
|
|
||||||
const fontFamilies = computed(() => store.userSettings?.fontFamilies ?? [])
|
const DEFAULT_FONT_FAMILIES = [
|
||||||
const sortedFontFamilies = computed(() =>
|
"EB Garamond",
|
||||||
[...fontFamilies.value].sort((a, b) => a.localeCompare(b))
|
"Inter",
|
||||||
|
"Lato",
|
||||||
|
"Libertinus Serif",
|
||||||
|
"Lora",
|
||||||
|
"Merriweather",
|
||||||
|
"Playfair Display",
|
||||||
|
"Roboto",
|
||||||
|
"Source Serif 4"
|
||||||
|
]
|
||||||
|
|
||||||
|
const fontFamilies = computed(
|
||||||
|
() => store.userSettings?.fontFamilies ?? DEFAULT_FONT_FAMILIES
|
||||||
)
|
)
|
||||||
|
const sortedFontFamilies = computed(() => {
|
||||||
|
const base = fontFamilies.value
|
||||||
|
const extras = [
|
||||||
|
store.userSettings?.chosenTitleFont,
|
||||||
|
store.userSettings?.chosenBodyFont
|
||||||
|
].filter((f): f is string => !!f && !base.includes(f))
|
||||||
|
return [...base, ...extras].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`)
|
||||||
|
|
||||||
|
const titleFont = computed({
|
||||||
|
get: () => store.userSettings?.chosenTitleFont,
|
||||||
|
set: (value) => store.setTitleFont(value!)
|
||||||
|
})
|
||||||
|
const bodyFont = computed({
|
||||||
|
get: () => store.userSettings?.chosenBodyFont,
|
||||||
|
set: (value) => store.setBodyFont(value!)
|
||||||
|
})
|
||||||
|
const fontSize = computed({
|
||||||
|
get: () => store.userSettings?.chosenFontSize,
|
||||||
|
set: (value) => store.setFontSize(value!)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="font-change" v-if="sortedFontFamilies.length > 0">
|
<div class="font-change">
|
||||||
<theme-swap />
|
<div>
|
||||||
|
<label for="title-font" class="font-label">t</label>
|
||||||
|
<select
|
||||||
|
id="title-font"
|
||||||
|
class="select"
|
||||||
|
v-model="titleFont"
|
||||||
|
>
|
||||||
|
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
||||||
|
{{ font }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<select
|
<label for="body-font" class="font-label">p</label>
|
||||||
class="select"
|
<select
|
||||||
:value="store.userSettings?.chosenFontFamily"
|
id="body-font"
|
||||||
@change="store.setFontFamily(($event.target as HTMLSelectElement).value)"
|
class="select"
|
||||||
>
|
v-model="bodyFont"
|
||||||
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
>
|
||||||
{{ font }}
|
<option v-for="font in sortedFontFamilies" :key="font" :value="font">
|
||||||
</option>
|
{{ font }}
|
||||||
</select>
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<theme-swap />
|
||||||
|
|
||||||
<select
|
<label for="font-size" class="font-label">s</label>
|
||||||
class="select"
|
<select
|
||||||
:value="store.userSettings?.chosenFontSize"
|
id="font-size"
|
||||||
@change="store.setFontSize(($event.target as HTMLSelectElement).value)"
|
class="select"
|
||||||
>
|
v-model="fontSize"
|
||||||
<option v-for="size in fontSizes" :key="size" :value="size">
|
>
|
||||||
{{ size }}
|
<option v-for="size in fontSizes" :key="size" :value="size">
|
||||||
</option>
|
{{ size }}
|
||||||
</select>
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.font-change {
|
.font-change {
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-label {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,32 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import FontChange from "@/components/FontChange.vue"
|
import FontChange from "@/components/FontChange.vue"
|
||||||
|
import HomeButton from "@/components/HomeButton.vue"
|
||||||
|
|
||||||
defineProps<{ user: string; repo: string }>()
|
defineProps<{ user: string; repo: string }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="header-note">
|
<header class="header-note">
|
||||||
<router-link
|
<home-button />
|
||||||
:to="{ name: 'Home' }"
|
|
||||||
class="button is-small is-white back-button"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="icon icon-tabler icon-tabler-arrow-narrow-left"
|
|
||||||
width="28"
|
|
||||||
height="28"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12" />
|
|
||||||
<line x1="5" y1="12" x2="9" y2="16" />
|
|
||||||
<line x1="5" y1="12" x2="9" y2="8" />
|
|
||||||
</svg>
|
|
||||||
</router-link>
|
|
||||||
<!-- <router-link
|
<!-- <router-link
|
||||||
:to="{ name: 'SpacedRepetitionCard', params: { user, repo } }"
|
:to="{ name: 'SpacedRepetitionCard', params: { user, repo } }"
|
||||||
>
|
>
|
||||||
@@ -51,12 +32,15 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
</svg>
|
</svg>
|
||||||
</router-link> -->
|
</router-link> -->
|
||||||
|
|
||||||
<button onclick="font_modal.showModal()">
|
<button
|
||||||
|
class="btn btn-ghost btn-circle text-base-content"
|
||||||
|
onclick="font_modal.showModal()"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icons-tabler-outline icon-tabler-typography"
|
class="icon icon-tabler icons-tabler-outline icon-tabler-typography"
|
||||||
width="36"
|
width="30"
|
||||||
height="36"
|
height="30"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -71,11 +55,14 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
<path d="M5 20l6 -16l2 0l7 16" />
|
<path d="M5 20l6 -16l2 0l7 16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<router-link :to="{ name: 'FluxNoteView', params: { user, repo } }">
|
<router-link
|
||||||
|
class="btn btn-ghost btn-circle"
|
||||||
|
:to="{ name: 'FluxNoteView', params: { user, repo } }"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="36"
|
width="30"
|
||||||
height="36"
|
height="30"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -88,12 +75,15 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
|
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
|
||||||
</svg>
|
</svg>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link :to="{ name: 'DraftNotes', params: { user, repo } }">
|
<router-link
|
||||||
|
class="btn btn-ghost btn-circle"
|
||||||
|
:to="{ name: 'DraftNotes', params: { user, repo } }"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icon-tabler-notes"
|
class="icon icon-tabler icon-tabler-notes"
|
||||||
width="36"
|
width="30"
|
||||||
height="36"
|
height="30"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -107,11 +97,14 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
<line x1="9" y1="15" x2="13" y2="15" />
|
<line x1="9" y1="15" x2="13" y2="15" />
|
||||||
</svg>
|
</svg>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link :to="{ name: 'TodoNotes', params: { user, repo } }">
|
<router-link
|
||||||
|
class="btn btn-ghost btn-circle"
|
||||||
|
:to="{ name: 'TodoNotes', params: { user, repo } }"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="36"
|
width="30"
|
||||||
height="36"
|
height="30"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -129,12 +122,15 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
<path d="M11 18l9 0" />
|
<path d="M11 18l9 0" />
|
||||||
</svg>
|
</svg>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link :to="{ name: 'FleetingNotes', params: { user, repo } }">
|
<router-link
|
||||||
|
class="btn btn-ghost btn-circle"
|
||||||
|
:to="{ name: 'FleetingNotes', params: { user, repo } }"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icon-tabler-mailbox"
|
class="icon icon-tabler icon-tabler-mailbox"
|
||||||
width="36"
|
width="30"
|
||||||
height="36"
|
height="30"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -150,7 +146,7 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
</svg>
|
</svg>
|
||||||
</router-link>
|
</router-link>
|
||||||
<dialog id="font_modal" class="modal">
|
<dialog id="font_modal" class="modal">
|
||||||
<div class="modal-box">
|
<div class="modal-box w-10/12 max-w-2xl">
|
||||||
<h3 class="text-lg font-bold">Style settings</h3>
|
<h3 class="text-lg font-bold">Style settings</h3>
|
||||||
<font-change />
|
<font-change />
|
||||||
</div>
|
</div>
|
||||||
@@ -168,12 +164,6 @@ defineProps<{ user: string; repo: string }>()
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
img {
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ const goHome = () => router.push({ name: "Home" })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a class="btn btn-ghost btn-circle btn-lg" @click="goHome">
|
<button class="btn btn-ghost btn-circle btn-lg text-base-content" @click="goHome">
|
||||||
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
|
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
|
||||||
</a>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ const emitNote = (sha: string) => {
|
|||||||
<h5 class="subtitle is-5">🔗</h5>
|
<h5 class="subtitle is-5">🔗</h5>
|
||||||
<ul class="links">
|
<ul class="links">
|
||||||
<li v-for="link in backlink?.links" :key="link.sha">
|
<li v-for="link in backlink?.links" :key="link.sha">
|
||||||
<a @click.prevent="emitNote(link.sha)">
|
<button class="link" @click="emitNote(link.sha)">
|
||||||
{{ link.title }}
|
{{ link.title }}
|
||||||
</a>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const getStyle = (seed: string) => {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.repo-list {
|
.repo-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
section {
|
section {
|
||||||
padding: 1rem 0 2rem;
|
padding: 1rem 0 2rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-content {
|
.note-content {
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
section {
|
section {
|
||||||
padding: 1rem 0 2rem;
|
padding: 1rem 0 2rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-content {
|
.note-content {
|
||||||
|
|||||||
@@ -3,53 +3,52 @@ import RepoList from "@/components/RepoList.vue"
|
|||||||
import SignInAtproto from "@/components/SignInAtproto.vue"
|
import SignInAtproto from "@/components/SignInAtproto.vue"
|
||||||
import SignInGithub from "@/components/SignInGithub.vue"
|
import SignInGithub from "@/components/SignInGithub.vue"
|
||||||
import ThemeSwap from "@/components/ThemeSwap.vue"
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||||
|
import { useATProtoLogin } from "@/hooks/useATProtoLogin.hook"
|
||||||
import { useForm } from "@/hooks/useForm.hook"
|
import { useForm } from "@/hooks/useForm.hook"
|
||||||
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||||
import LastVisited from "@/modules/history/components/LastVisited.vue"
|
import LastVisited from "@/modules/history/components/LastVisited.vue"
|
||||||
|
|
||||||
const { isLogged } = useGitHubLogin()
|
const { isLogged } = useGitHubLogin()
|
||||||
|
const { isLoggedIn: isATProtoLoggedIn, avatarUrl } = useATProtoLogin()
|
||||||
const { userInput, repoInput, submit } = useForm()
|
const { userInput, repoInput, submit } = useForm()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="welcome-world">
|
<div class="welcome-world">
|
||||||
<h1 class="title is-1">
|
<div class="welcome-content">
|
||||||
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
|
<h1 class="title is-1">
|
||||||
Remanso
|
<img src="/favicon.png" alt="Remanso icon" class="remanso-logo" />
|
||||||
</h1>
|
Remanso
|
||||||
|
</h1>
|
||||||
|
|
||||||
<repo-list />
|
<repo-list />
|
||||||
|
|
||||||
<last-visited />
|
<last-visited />
|
||||||
|
|
||||||
<div class="get-started">
|
<form class="github-form" @submit.prevent>
|
||||||
<sign-in-github />
|
<div>github/</div>
|
||||||
<router-link v-if="isLogged" :to="{ name: 'RepoList' }" class="btn btn-sm"
|
<input
|
||||||
>Manage your repos</router-link
|
v-model="userInput"
|
||||||
>
|
class="input input-ghost"
|
||||||
|
type="text"
|
||||||
|
placeholder="user"
|
||||||
|
/>
|
||||||
|
/
|
||||||
|
<input
|
||||||
|
v-model="repoInput"
|
||||||
|
class="input input-ghost"
|
||||||
|
type="text"
|
||||||
|
placeholder="repo"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary" @click="submit">
|
||||||
|
go
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="github-form" @submit.prevent>
|
|
||||||
<div>github/</div>
|
|
||||||
<input
|
|
||||||
v-model="userInput"
|
|
||||||
class="input input-ghost"
|
|
||||||
type="text"
|
|
||||||
placeholder="user"
|
|
||||||
/>
|
|
||||||
/
|
|
||||||
<input
|
|
||||||
v-model="repoInput"
|
|
||||||
class="input input-ghost"
|
|
||||||
type="text"
|
|
||||||
placeholder="repo"
|
|
||||||
/>
|
|
||||||
<button type="submit" class="btn btn-primary" @click="submit">go</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<theme-swap />
|
<theme-swap />
|
||||||
Made with
|
made with
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon icon-tabler icon-tabler-heart"
|
class="icon icon-tabler icon-tabler-heart"
|
||||||
@@ -71,7 +70,32 @@ 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" />
|
<button
|
||||||
|
class="btn btn-ghost btn-circle btn-sm profile-btn"
|
||||||
|
onclick="profile_modal.showModal()"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="isATProtoLoggedIn && avatarUrl"
|
||||||
|
:src="avatarUrl"
|
||||||
|
class="profile-avatar"
|
||||||
|
alt="Profile"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
|
||||||
|
<path d="M6 20c0 -2.21 2.686 -4 6 -4s6 1.79 6 4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'FluxNoteView',
|
name: 'FluxNoteView',
|
||||||
@@ -81,6 +105,28 @@ const { userInput, repoInput, submit } = useForm()
|
|||||||
>Get started</router-link
|
>Get started</router-link
|
||||||
>
|
>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<dialog id="profile_modal" class="modal">
|
||||||
|
<div class="modal-box profile-modal-box">
|
||||||
|
<h3 class="text-lg font-bold">Profile</h3>
|
||||||
|
<div class="profile-section">
|
||||||
|
<sign-in-atproto :with-sign-out="true" />
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="profile-section">
|
||||||
|
<sign-in-github />
|
||||||
|
<router-link
|
||||||
|
v-if="isLogged"
|
||||||
|
:to="{ name: 'RepoList' }"
|
||||||
|
class="btn btn-sm"
|
||||||
|
>Manage your repos</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button></button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -98,26 +144,26 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-world {
|
.welcome-world {
|
||||||
padding: 1rem;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
align-self: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
gap: 1rem;
|
||||||
|
|
||||||
.get-started {
|
|
||||||
margin: center;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.github-form {
|
.github-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -127,15 +173,33 @@ h1 {
|
|||||||
max-width: 140px;
|
max-width: 140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 0.2rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
.profile-avatar {
|
||||||
vertical-align: middle;
|
max-width: 100%;
|
||||||
margin-top: 0;
|
border-radius: 50%;
|
||||||
}
|
object-fit: cover;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-modal-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.to-user-repo {
|
.to-user-repo {
|
||||||
|
|||||||
179
src/data/data.ts
179
src/data/data.ts
@@ -1,166 +1,31 @@
|
|||||||
|
import { wrap } from "comlink"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import indexedDb from "pouchdb-adapter-indexeddb"
|
|
||||||
import PouchDb from "pouchdb-browser"
|
|
||||||
|
|
||||||
import { DataType } from "./DataType.enum"
|
import { DataType } from "./DataType.enum"
|
||||||
import { Model } from "./models/Model"
|
import { Model } from "./models/Model"
|
||||||
|
|
||||||
PouchDb.plugin(indexedDb)
|
export interface DataApi {
|
||||||
|
add<DT extends DataType>(model: Model<DT>): Promise<boolean>
|
||||||
interface GetAllParams {
|
update<DT extends DataType, T extends Model<DT>>(model: T): Promise<boolean>
|
||||||
prefix?: string
|
remove(id: string): Promise<boolean>
|
||||||
includeDocs?: boolean
|
get<DT extends DataType, T extends Model<DT>>(id: string): Promise<T | null>
|
||||||
includeAttachments?: boolean
|
getOrCreate<DT extends DataType, T extends Model<DT>>(
|
||||||
keys?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
class Data {
|
|
||||||
// oxlint-disable-next-line typescript/ban-types
|
|
||||||
private readonly locale: PouchDB.Database<{}> | null = null
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
try {
|
|
||||||
this.locale = new PouchDb("remanso", {
|
|
||||||
adapter: "indexeddb"
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("data error", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const result = await this.locale?.put(model)
|
|
||||||
return result?.ok ?? false
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async update<DT extends DataType, T extends Model<DT>>(
|
|
||||||
model: T
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
if (!model._id) {
|
|
||||||
const result = await this.locale?.put(model)
|
|
||||||
return result?.ok ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldModel = await this.get(model._id)
|
|
||||||
if (oldModel) {
|
|
||||||
const result = await this.locale?.put({ ...oldModel, ...model })
|
|
||||||
return result?.ok ?? false
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.locale?.put(model)
|
|
||||||
return result?.ok ?? false
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(error)
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(id: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const doc = await this.get(id)
|
|
||||||
if (!doc) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const result = await this.locale?.put({
|
|
||||||
...doc,
|
|
||||||
_deleted: true
|
|
||||||
})
|
|
||||||
return result?.ok ?? false
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get<DT extends DataType, T extends Model<DT>>(
|
|
||||||
id: string
|
|
||||||
): Promise<T | null> {
|
|
||||||
try {
|
|
||||||
return ((await this.locale?.get(id)) as T) || null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
getAll<DT extends DataType, T extends Model<DT>>(params: {
|
||||||
|
prefix?: string
|
||||||
if (element) {
|
includeDocs?: boolean
|
||||||
return element
|
includeAttachments?: boolean
|
||||||
}
|
keys?: string[]
|
||||||
|
}): Promise<T[]>
|
||||||
await data.add<DT>({ ...initialValue, _id: id })
|
|
||||||
|
|
||||||
return this.getOrCreate(id, initialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAll<DT extends DataType, T extends Model<DT>>({
|
|
||||||
prefix,
|
|
||||||
includeDocs = true,
|
|
||||||
includeAttachments = false,
|
|
||||||
keys = []
|
|
||||||
}: GetAllParams): Promise<T[]> {
|
|
||||||
if (!this.locale) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keys.length) {
|
|
||||||
const response = await this.locale.allDocs({
|
|
||||||
include_docs: includeDocs,
|
|
||||||
attachments: includeAttachments,
|
|
||||||
keys: keys.map((key) => this.generateId(prefix, key))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (includeDocs) {
|
|
||||||
return response.rows
|
|
||||||
.map((row) => {
|
|
||||||
if ("error" in row) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.doc
|
|
||||||
})
|
|
||||||
.filter(Boolean) as T[]
|
|
||||||
} else {
|
|
||||||
return response.rows
|
|
||||||
.map((row) => {
|
|
||||||
if ("error" in row) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return { _id: row.id }
|
|
||||||
})
|
|
||||||
.filter(Boolean) as T[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.locale.allDocs({
|
|
||||||
include_docs: includeDocs,
|
|
||||||
attachments: includeAttachments,
|
|
||||||
startkey: prefix ? prefix : undefined,
|
|
||||||
endkey: prefix ? `${prefix}\ufff0` : undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.rows.map((row) => row.doc) as T[]
|
|
||||||
}
|
|
||||||
|
|
||||||
public generateId(type?: DataType | string, id?: string) {
|
|
||||||
if (!type) {
|
|
||||||
return id || nanoid()
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${type}-${id || nanoid()}`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const data = new Data()
|
export const generateId = (type?: DataType | string, id?: string): string => {
|
||||||
|
if (!type) return id || nanoid()
|
||||||
|
return `${type}-${id || nanoid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
import DataWorker from "./data.worker?worker"
|
||||||
|
|
||||||
|
export const data = wrap(new DataWorker()) as unknown as DataApi
|
||||||
|
|||||||
156
src/data/data.worker.ts
Normal file
156
src/data/data.worker.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { expose } from "comlink"
|
||||||
|
import { nanoid } from "nanoid"
|
||||||
|
import indexedDb from "pouchdb-adapter-indexeddb"
|
||||||
|
import PouchDb from "pouchdb-browser"
|
||||||
|
|
||||||
|
import { DataType } from "./DataType.enum"
|
||||||
|
import { Model } from "./models/Model"
|
||||||
|
|
||||||
|
PouchDb.plugin(indexedDb)
|
||||||
|
|
||||||
|
interface GetAllParams {
|
||||||
|
prefix?: string
|
||||||
|
includeDocs?: boolean
|
||||||
|
includeAttachments?: boolean
|
||||||
|
keys?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class Data {
|
||||||
|
// oxlint-disable-next-line typescript/ban-types
|
||||||
|
private readonly locale: PouchDB.Database<{}> | null = null
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
try {
|
||||||
|
this.locale = new PouchDb("remanso", {
|
||||||
|
adapter: "indexeddb"
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("data error", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildId(type?: DataType | string, id?: string): string {
|
||||||
|
if (!type) return id || nanoid()
|
||||||
|
return `${type}-${id || nanoid()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public async add<DT extends DataType>(model: Model<DT>): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.locale?.put(model)
|
||||||
|
return result?.ok ?? false
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update<DT extends DataType, T extends Model<DT>>(
|
||||||
|
model: T
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!model._id) {
|
||||||
|
const result = await this.locale?.put(model)
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldModel = await this.get(model._id)
|
||||||
|
if (oldModel) {
|
||||||
|
const result = await this.locale?.put({ ...oldModel, ...model })
|
||||||
|
return result?.ok ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.locale?.put(model)
|
||||||
|
return result?.ok ?? false
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const doc = await this.get(id)
|
||||||
|
if (!doc) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const result = await this.locale?.put({
|
||||||
|
...doc,
|
||||||
|
_deleted: true
|
||||||
|
})
|
||||||
|
return result?.ok ?? false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get<DT extends DataType, T extends Model<DT>>(
|
||||||
|
id: string
|
||||||
|
): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
return ((await this.locale?.get(id)) as T) || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getOrCreate<DT extends DataType, T extends Model<DT>>(
|
||||||
|
id: string,
|
||||||
|
initialValue: T
|
||||||
|
): Promise<T> {
|
||||||
|
const element = await this.get<DT, T>(id)
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.add<DT>({ ...initialValue, _id: id })
|
||||||
|
|
||||||
|
return this.getOrCreate(id, initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAll<DT extends DataType, T extends Model<DT>>({
|
||||||
|
prefix,
|
||||||
|
includeDocs = true,
|
||||||
|
includeAttachments = false,
|
||||||
|
keys = []
|
||||||
|
}: GetAllParams): Promise<T[]> {
|
||||||
|
if (!this.locale) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
const response = await this.locale.allDocs({
|
||||||
|
include_docs: includeDocs,
|
||||||
|
attachments: includeAttachments,
|
||||||
|
keys: keys.map((key) => this.buildId(prefix, key))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (includeDocs) {
|
||||||
|
return response.rows
|
||||||
|
.map((row) => {
|
||||||
|
if ("error" in row) return null
|
||||||
|
return row.doc
|
||||||
|
})
|
||||||
|
.filter(Boolean) as T[]
|
||||||
|
} else {
|
||||||
|
return response.rows
|
||||||
|
.map((row) => {
|
||||||
|
if ("error" in row) return null
|
||||||
|
return { _id: row.id }
|
||||||
|
})
|
||||||
|
.filter(Boolean) as T[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.locale.allDocs({
|
||||||
|
include_docs: includeDocs,
|
||||||
|
attachments: includeAttachments,
|
||||||
|
startkey: prefix ? prefix : undefined,
|
||||||
|
endkey: prefix ? `${prefix}\ufff0` : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.rows.map((row) => row.doc) as T[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expose(new Data())
|
||||||
@@ -14,14 +14,32 @@ import {
|
|||||||
|
|
||||||
const did = ref<string | null>(null)
|
const did = ref<string | null>(null)
|
||||||
const handle = ref<string | null>(null)
|
const handle = ref<string | null>(null)
|
||||||
|
const avatarUrl = ref<string | null>(null)
|
||||||
|
|
||||||
let init = true
|
let init = true
|
||||||
|
|
||||||
|
const fetchAvatar = async (actorDid: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(actorDid)}`
|
||||||
|
)
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json()
|
||||||
|
avatarUrl.value = data.avatar ?? null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
avatarUrl.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 ?? ""
|
||||||
|
if (stored?.did) {
|
||||||
|
fetchAvatar(stored.did)
|
||||||
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
@@ -32,6 +50,7 @@ const initializeAuth = async () => {
|
|||||||
did.value = session.did
|
did.value = session.did
|
||||||
handle.value = resolvedHandle
|
handle.value = resolvedHandle
|
||||||
await saveSession(session.did, resolvedHandle)
|
await saveSession(session.did, resolvedHandle)
|
||||||
|
fetchAvatar(session.did)
|
||||||
|
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
null,
|
null,
|
||||||
@@ -61,11 +80,13 @@ export const useATProtoLogin = () => {
|
|||||||
await clearSession()
|
await clearSession()
|
||||||
did.value = ""
|
did.value = ""
|
||||||
handle.value = ""
|
handle.value = ""
|
||||||
|
avatarUrl.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
did,
|
did,
|
||||||
handle,
|
handle,
|
||||||
|
avatarUrl,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
isATProtoReady,
|
isATProtoReady,
|
||||||
signIn,
|
signIn,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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, generateId } 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"
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export const useBacklinks = (sha: string | ComputedRef<string>) => {
|
|||||||
|
|
||||||
const { state: backlink, execute } = useAsyncState(
|
const { state: backlink, execute } = useAsyncState(
|
||||||
data.get<DataType.BacklinkNote, BacklinkNote>(
|
data.get<DataType.BacklinkNote, BacklinkNote>(
|
||||||
data.generateId(DataType.BacklinkNote, sha)
|
generateId(DataType.BacklinkNote, sha)
|
||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, generateId } 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"
|
||||||
@@ -14,93 +14,105 @@ import { confirmMessage } from "@/utils/notif"
|
|||||||
|
|
||||||
const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
|
const isMarkdown = (filename?: string) => filename?.endsWith(".md") ?? false
|
||||||
|
|
||||||
|
const yieldToMain = () =>
|
||||||
|
"scheduler" in globalThis
|
||||||
|
? (globalThis as unknown as { scheduler: { yield: () => Promise<void> } }).scheduler.yield()
|
||||||
|
: new Promise<void>((r) => setTimeout(r, 0))
|
||||||
|
|
||||||
export const useComputeBacklinks = () => {
|
export const useComputeBacklinks = () => {
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
|
|
||||||
watch(store, async () => {
|
watch(
|
||||||
if (!store.userSettings?.backlink) {
|
() => store.files,
|
||||||
return
|
async () => {
|
||||||
}
|
await new Promise<void>((r) => setTimeout(r, 300))
|
||||||
|
|
||||||
let notifiedForComputation = false
|
if (!store.userSettings?.backlink) {
|
||||||
|
|
||||||
const backlinks: Map<string, Backlink[]> = new Map()
|
|
||||||
|
|
||||||
for (const file of store.files) {
|
|
||||||
if (!isMarkdown(file.path) || !file.sha) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileBacklinkId = data.generateId(DataType.BacklinkNote, file.sha)
|
|
||||||
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
|
|
||||||
fileBacklinkId
|
|
||||||
)
|
|
||||||
if (fileBacklink) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!backlinks.has(file.sha)) {
|
|
||||||
backlinks.set(file.sha, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getContent } = useFile(file.sha, false)
|
|
||||||
const note = await getContent()
|
|
||||||
|
|
||||||
if (!note) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const parser = new DOMParser()
|
let notifiedForComputation = false
|
||||||
const htmlDoc = parser.parseFromString(note, "text/html")
|
|
||||||
|
|
||||||
const links = htmlDoc.querySelectorAll("a")
|
const backlinks: Map<string, Backlink[]> = new Map()
|
||||||
|
|
||||||
for (const link of links) {
|
for (const file of store.files) {
|
||||||
const href = link.getAttribute("href") ?? ""
|
await yieldToMain()
|
||||||
|
|
||||||
if (isExternalLink(href) || !isMarkdown(href)) {
|
if (!isMarkdown(file.path) || !file.sha) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = resolvePath(file.path ?? "", href)
|
const fileBacklinkId = generateId(DataType.BacklinkNote, file.sha)
|
||||||
const backlinkFile = store.files.find((file) => file.path === path)
|
const fileBacklink = await data.get<DataType.BacklinkNote, BacklinkNote>(
|
||||||
|
fileBacklinkId
|
||||||
if (!backlinkFile?.sha || !backlinkFile?.path) {
|
)
|
||||||
|
if (fileBacklink) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousBacklinks = backlinks.get(backlinkFile.sha) ?? []
|
if (!backlinks.has(file.sha)) {
|
||||||
|
backlinks.set(file.sha, [])
|
||||||
if (previousBacklinks.find((bl) => bl.sha === file.sha)) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!notifiedForComputation) {
|
const { getContent } = useFile(file.sha, false)
|
||||||
notifiedForComputation = true
|
const note = await getContent()
|
||||||
confirmMessage("Updating backlinks...")
|
|
||||||
|
if (!note) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
backlinks.set(backlinkFile.sha, [
|
const parser = new DOMParser()
|
||||||
...previousBacklinks,
|
const htmlDoc = parser.parseFromString(note, "text/html")
|
||||||
{
|
|
||||||
sha: file.sha,
|
const links = htmlDoc.querySelectorAll("a")
|
||||||
title: filenameToNoteTitle(file.path ?? "")
|
|
||||||
|
for (const link of links) {
|
||||||
|
const href = link.getAttribute("href") ?? ""
|
||||||
|
|
||||||
|
if (isExternalLink(href) || !isMarkdown(href)) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [sha, fileBacklinks] of backlinks) {
|
const path = resolvePath(file.path ?? "", href)
|
||||||
const fileBacklinkId = data.generateId(DataType.BacklinkNote, sha)
|
const backlinkFile = store.files.find((file) => file.path === path)
|
||||||
const backlinkNote: BacklinkNote = {
|
|
||||||
_id: fileBacklinkId,
|
if (!backlinkFile?.sha || !backlinkFile?.path) {
|
||||||
$type: DataType.BacklinkNote,
|
continue
|
||||||
sha: sha,
|
}
|
||||||
links: fileBacklinks
|
|
||||||
|
const previousBacklinks = backlinks.get(backlinkFile.sha) ?? []
|
||||||
|
|
||||||
|
if (previousBacklinks.find((bl) => bl.sha === file.sha)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notifiedForComputation) {
|
||||||
|
notifiedForComputation = true
|
||||||
|
confirmMessage("Updating backlinks...")
|
||||||
|
}
|
||||||
|
|
||||||
|
backlinks.set(backlinkFile.sha, [
|
||||||
|
...previousBacklinks,
|
||||||
|
{
|
||||||
|
sha: file.sha,
|
||||||
|
title: filenameToNoteTitle(file.path ?? "")
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await data.update(backlinkNote)
|
for (const [sha, fileBacklinks] of backlinks) {
|
||||||
backlinkEventBus.emit({ fileSha: sha })
|
const fileBacklinkId = generateId(DataType.BacklinkNote, sha)
|
||||||
|
const backlinkNote: BacklinkNote = {
|
||||||
|
_id: fileBacklinkId,
|
||||||
|
$type: DataType.BacklinkNote,
|
||||||
|
sha: sha,
|
||||||
|
links: fileBacklinks
|
||||||
|
}
|
||||||
|
|
||||||
|
await data.update(backlinkNote)
|
||||||
|
backlinkEventBus.emit({ fileSha: sha })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, generateId } 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"
|
||||||
@@ -36,7 +36,7 @@ export const useOfflineNotes = () => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!file.sha ||
|
!file.sha ||
|
||||||
cachedNotesSet.has(data.generateId(DataType.Note, file.sha))
|
cachedNotesSet.has(generateId(DataType.Note, file.sha))
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,59 @@
|
|||||||
import { useAsyncState } from "@vueuse/core"
|
import { computed, ref } from "vue"
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
const PER_PAGE = 30
|
||||||
|
const STALE_TIME_MS = 20 * 60 * 1000
|
||||||
|
|
||||||
|
const repos = ref<RepoBase[]>([])
|
||||||
|
const isReady = ref(false)
|
||||||
|
const currentPage = ref(0)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
let lastFetchedAt = 0
|
||||||
|
|
||||||
export const useRepos = () => {
|
export const useRepos = () => {
|
||||||
const { username, accessToken } = useGitHubLogin()
|
const { username, accessToken } = useGitHubLogin()
|
||||||
const repos = useAsyncState<RepoBase[]>(async () => {
|
|
||||||
|
const loadMore = async () => {
|
||||||
if (!accessToken.value || !username.value) {
|
if (!accessToken.value || !username.value) {
|
||||||
return []
|
isReady.value = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const octokit = await getOctokit()
|
const octokit = await getOctokit()
|
||||||
|
const nextPage = currentPage.value + 1
|
||||||
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: PER_PAGE,
|
||||||
|
page: nextPage
|
||||||
})
|
})
|
||||||
|
currentPage.value = nextPage
|
||||||
return repoList.data.items
|
totalCount.value = repoList.data.total_count
|
||||||
.map((item) => ({
|
const newItems = repoList.data.items.map((item) => ({
|
||||||
id: `${item.id}`,
|
id: `${item.id}`,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
isPrivate: item.private
|
isPrivate: item.private
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
repos.value = [...repos.value, ...newItems].sort((a, b) =>
|
||||||
}, [])
|
a.name < b.name ? -1 : 1
|
||||||
|
)
|
||||||
return {
|
isReady.value = true
|
||||||
repos: repos.state,
|
|
||||||
isReady: repos.isReady
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canLoadMore = computed(() => repos.value.length < totalCount.value)
|
||||||
|
|
||||||
|
const isStale = Date.now() - lastFetchedAt > STALE_TIME_MS
|
||||||
|
if (!isReady.value || isStale) {
|
||||||
|
if (isStale && isReady.value) {
|
||||||
|
repos.value = []
|
||||||
|
currentPage.value = 0
|
||||||
|
totalCount.value = 0
|
||||||
|
isReady.value = false
|
||||||
|
}
|
||||||
|
lastFetchedAt = Date.now()
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { repos, isReady, canLoadMore, loadMore }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useAsyncState } from "@vueuse/core"
|
|||||||
import { addDays, isAfter } from "date-fns"
|
import { addDays, isAfter } from "date-fns"
|
||||||
import { computed, nextTick, watch } from "vue"
|
import { computed, nextTick, watch } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } 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 { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
@@ -51,7 +51,7 @@ export const useSpacedRepetitionCards = () => {
|
|||||||
const repetition = await data.getOrCreate<
|
const repetition = await data.getOrCreate<
|
||||||
DataType.RepetitionCard,
|
DataType.RepetitionCard,
|
||||||
RepetitionCard
|
RepetitionCard
|
||||||
>(data.generateId(DataType.RepetitionCard, cardFile.path), {
|
>(generateId(DataType.RepetitionCard, cardFile.path), {
|
||||||
$type: DataType.RepetitionCard,
|
$type: DataType.RepetitionCard,
|
||||||
level: 1,
|
level: 1,
|
||||||
repeatDate: new Date(),
|
repeatDate: new Date(),
|
||||||
|
|||||||
@@ -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, generateId } 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 = 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")
|
generateId(DataType.History, "history")
|
||||||
),
|
),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Ref, toValue } from "vue"
|
import { Ref, toValue } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } 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 = generateId(DataType.History, "history")
|
||||||
const MAX_REPO_HISTORY = 10
|
const MAX_REPO_HISTORY = 10
|
||||||
|
|
||||||
export const useVisitRepo = (newRepo: {
|
export const useVisitRepo = (newRepo: {
|
||||||
|
|||||||
6
src/modules/note/cache/prepareNoteCache.ts
vendored
6
src/modules/note/cache/prepareNoteCache.ts
vendored
@@ -1,4 +1,4 @@
|
|||||||
import { data } from "@/data/data"
|
import { data, generateId } 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"
|
||||||
@@ -14,8 +14,8 @@ type NoteCacheResult =
|
|||||||
export const prepareNoteCache = (sha: string, path?: string) => {
|
export const prepareNoteCache = (sha: string, path?: string) => {
|
||||||
const store = useUserRepoStore()
|
const store = useUserRepoStore()
|
||||||
|
|
||||||
const noteId = data.generateId(DataType.Note, sha)
|
const noteId = generateId(DataType.Note, sha)
|
||||||
const notePath = path ? data.generateId(DataType.Note, path) : null
|
const notePath = path ? generateId(DataType.Note, path) : null
|
||||||
const getCachedNote = async (): Promise<NoteCacheResult> => {
|
const getCachedNote = async (): Promise<NoteCacheResult> => {
|
||||||
const note = await data.get<DataType.Note, Note>(noteId)
|
const note = await data.get<DataType.Note, Note>(noteId)
|
||||||
|
|
||||||
|
|||||||
@@ -29,31 +29,19 @@ export const noteRouter = contract.router({
|
|||||||
noteLists: {
|
noteLists: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/notes",
|
path: "/notes",
|
||||||
query: type({
|
query: contract.type<{ cursor?: string; limit?: number }>(),
|
||||||
cursor: "string | undefined",
|
|
||||||
limit: "number | undefined"
|
|
||||||
}),
|
|
||||||
responses: {
|
responses: {
|
||||||
200: type({
|
200: contract.type<{ notes: PublicNoteListItem[] }>()
|
||||||
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: contract.type<{ did: string }>(),
|
||||||
did: "string"
|
query: contract.type<{ cursor?: string; limit?: number }>(),
|
||||||
}),
|
|
||||||
query: type({
|
|
||||||
cursor: "string | undefined",
|
|
||||||
limit: "number | undefined"
|
|
||||||
}),
|
|
||||||
responses: {
|
responses: {
|
||||||
200: type({
|
200: contract.type<{ notes: PublicNoteListItem[] }>()
|
||||||
notes: PublicNoteListItem.array()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
summary: "List all notes"
|
summary: "List all notes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { computed, onMounted, ref } from "vue"
|
import { computed, onMounted, ref } from "vue"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } 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"
|
||||||
@@ -27,7 +27,7 @@ export const useFavoriteRepos = () => {
|
|||||||
|
|
||||||
const toggleFavorite = async (repo: RepoBase, isFavorite: boolean) => {
|
const toggleFavorite = async (repo: RepoBase, isFavorite: boolean) => {
|
||||||
const favorite: FavoriteRepo = {
|
const favorite: FavoriteRepo = {
|
||||||
_id: data.generateId(DataType.FavoriteRepo, repo.id),
|
_id: generateId(DataType.FavoriteRepo, repo.id),
|
||||||
$type: DataType.FavoriteRepo,
|
$type: DataType.FavoriteRepo,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
name: repo.name,
|
name: repo.name,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { RepoBase } from "@/modules/repo/interfaces/RepoBase"
|
|||||||
|
|
||||||
export const useRepoList = () => {
|
export const useRepoList = () => {
|
||||||
const { savedFavoriteRepos, addFavorite, removeFavorite } = useFavoriteRepos()
|
const { savedFavoriteRepos, addFavorite, removeFavorite } = useFavoriteRepos()
|
||||||
const { repos } = useRepos()
|
const { repos, canLoadMore, loadMore } = useRepos()
|
||||||
|
|
||||||
const favoriteRepos = computed(() => {
|
const favoriteRepos = computed(() => {
|
||||||
return repos.value.filter((repo) =>
|
return repos.value.filter((repo) =>
|
||||||
@@ -38,6 +38,8 @@ export const useRepoList = () => {
|
|||||||
favoriteRepos,
|
favoriteRepos,
|
||||||
otherRepos,
|
otherRepos,
|
||||||
favoriteCheckboxes,
|
favoriteCheckboxes,
|
||||||
toggleCheckbox
|
toggleCheckbox,
|
||||||
|
canLoadMore,
|
||||||
|
loadMore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ export interface UserSettings extends Model<DataType.UserSettings> {
|
|||||||
fontSize?: string
|
fontSize?: string
|
||||||
chosenFontSize?: string
|
chosenFontSize?: string
|
||||||
backlink?: boolean
|
backlink?: boolean
|
||||||
|
chosenTitleFont?: string
|
||||||
|
chosenBodyFont?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,16 @@ export const getUserSettingsContent = async (
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(atob(content)) as UserSettings
|
const raw = JSON.parse(atob(content)) as UserSettings & {
|
||||||
|
t?: string
|
||||||
|
p?: string
|
||||||
|
}
|
||||||
|
const { t, p, ...rest } = raw
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
chosenTitleFont: t,
|
||||||
|
chosenBodyFont: p
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryFileContent = async (
|
export const queryFileContent = async (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
|
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
|
||||||
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
|
||||||
@@ -20,6 +20,7 @@ interface State {
|
|||||||
readme?: string | null
|
readme?: string | null
|
||||||
userSettings?: UserSettings | null
|
userSettings?: UserSettings | null
|
||||||
needToLogin: boolean
|
needToLogin: boolean
|
||||||
|
_requestId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
||||||
@@ -29,40 +30,71 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
files: [],
|
files: [],
|
||||||
readme: undefined,
|
readme: undefined,
|
||||||
userSettings: undefined,
|
userSettings: undefined,
|
||||||
needToLogin: false
|
needToLogin: false,
|
||||||
|
_requestId: 0
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
_persistFonts() {
|
||||||
|
if (!this.userSettings) return
|
||||||
|
try {
|
||||||
|
const { chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily } =
|
||||||
|
this.userSettings
|
||||||
|
localStorage.setItem(
|
||||||
|
`remanso:fonts:${this.user}:${this.repo}`,
|
||||||
|
JSON.stringify({ chosenTitleFont, chosenBodyFont, chosenFontSize, chosenFontFamily })
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
async setUserRepo(user: string, repo: string) {
|
async setUserRepo(user: string, repo: string) {
|
||||||
|
const requestId = ++this._requestId
|
||||||
this.user = user
|
this.user = user
|
||||||
this.repo = repo
|
this.repo = repo
|
||||||
|
|
||||||
const savedRepoId = data.generateId(DataType.SavedRepo, `${user}-${repo}`)
|
let lsFonts: Partial<UserSettings> = {}
|
||||||
const cachedSavedRepo = await data.get<DataType.SavedRepo, SavedRepo>(
|
try {
|
||||||
savedRepoId
|
const lsRaw = localStorage.getItem(`remanso:fonts:${user}:${repo}`)
|
||||||
)
|
if (lsRaw) lsFonts = JSON.parse(lsRaw)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(lsFonts).length) {
|
||||||
|
if (!this.userSettings) this.userSettings = { $type: DataType.UserSettings }
|
||||||
|
Object.assign(this.userSettings, lsFonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedRepoId = generateId(DataType.SavedRepo, `${user}-${repo}`)
|
||||||
|
const userSettingsId = `UserSetting-${user}-${repo}`
|
||||||
|
|
||||||
|
const [cachedSavedRepo, cachedUserSettings] = await Promise.all([
|
||||||
|
data.get<DataType.SavedRepo, SavedRepo>(savedRepoId),
|
||||||
|
data.get<DataType.UserSettings, UserSettings>(userSettingsId)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
|
|
||||||
if (cachedSavedRepo) {
|
if (cachedSavedRepo) {
|
||||||
this.files = cachedSavedRepo.files
|
this.files = cachedSavedRepo.files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cachedUserSettings) {
|
||||||
|
// localStorage font choices take priority over PouchDB cache
|
||||||
|
this.userSettings = { ...cachedUserSettings, ...lsFonts }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await refreshToken()
|
await refreshToken()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("impossible to refresh token", error)
|
console.warn("impossible to refresh token", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSettingsId = `UserSetting-${user}-${repo}`
|
if (requestId !== this._requestId) return
|
||||||
const cachedUserSettings = await data.get<
|
|
||||||
DataType.UserSettings,
|
|
||||||
UserSettings
|
|
||||||
>(userSettingsId)
|
|
||||||
|
|
||||||
if (cachedUserSettings) {
|
|
||||||
this.userSettings = cachedUserSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
getFiles(user, repo)
|
getFiles(user, repo)
|
||||||
.then(async (files) => {
|
.then(async (files) => {
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
data.update<DataType.SavedRepo, SavedRepo>({
|
data.update<DataType.SavedRepo, SavedRepo>({
|
||||||
_id: savedRepoId,
|
_id: savedRepoId,
|
||||||
$type: DataType.SavedRepo,
|
$type: DataType.SavedRepo,
|
||||||
@@ -74,6 +106,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
return getUserSettingsContent(user, repo, files)
|
return getUserSettingsContent(user, repo, files)
|
||||||
})
|
})
|
||||||
.then((userSettings) => {
|
.then((userSettings) => {
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
const chosenFontFamily = userSettings?.fontFamilies?.find(
|
const chosenFontFamily = userSettings?.fontFamilies?.find(
|
||||||
(font) => font === this.userSettings?.chosenFontFamily
|
(font) => font === this.userSettings?.chosenFontFamily
|
||||||
)
|
)
|
||||||
@@ -81,24 +114,43 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
: userSettings?.fontFamily
|
: userSettings?.fontFamily
|
||||||
const chosenFontSize =
|
const chosenFontSize =
|
||||||
this.userSettings?.chosenFontSize ?? userSettings?.fontSize
|
this.userSettings?.chosenFontSize ?? userSettings?.fontSize
|
||||||
this.userSettings = userSettings
|
const chosenTitleFont =
|
||||||
|
this.userSettings?.chosenTitleFont ??
|
||||||
if (!this.userSettings) {
|
userSettings?.chosenTitleFont ??
|
||||||
return
|
chosenFontFamily
|
||||||
|
const chosenBodyFont =
|
||||||
|
this.userSettings?.chosenBodyFont ??
|
||||||
|
userSettings?.chosenBodyFont ??
|
||||||
|
chosenFontFamily
|
||||||
|
if (userSettings) {
|
||||||
|
this.userSettings = userSettings
|
||||||
|
} else if (!this.userSettings) {
|
||||||
|
this.userSettings = { $type: DataType.UserSettings }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userSettings.chosenFontFamily =
|
this.userSettings.chosenFontFamily =
|
||||||
chosenFontFamily ?? this.userSettings.fontFamily
|
chosenFontFamily ?? this.userSettings.fontFamily
|
||||||
this.userSettings.chosenFontSize =
|
this.userSettings.chosenFontSize =
|
||||||
chosenFontSize ?? this.userSettings.fontSize
|
chosenFontSize ?? this.userSettings.fontSize
|
||||||
|
this.userSettings.chosenTitleFont = chosenTitleFont
|
||||||
|
this.userSettings.chosenBodyFont = chosenBodyFont
|
||||||
|
|
||||||
|
// Persist only repo config fields — chosen* are localStorage-only
|
||||||
|
const {
|
||||||
|
chosenTitleFont: _t,
|
||||||
|
chosenBodyFont: _b,
|
||||||
|
chosenFontSize: _s,
|
||||||
|
chosenFontFamily: _f,
|
||||||
|
...repoConfig
|
||||||
|
} = this.userSettings
|
||||||
data.update<DataType.UserSettings, UserSettings>({
|
data.update<DataType.UserSettings, UserSettings>({
|
||||||
...this.userSettings,
|
...repoConfig,
|
||||||
_id: userSettingsId
|
_id: userSettingsId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
getCachedMainReadme(user, repo).then(async (cachedReadme) => {
|
||||||
|
if (requestId !== this._requestId) return
|
||||||
this.readme = cachedReadme
|
this.readme = cachedReadme
|
||||||
this.readme = await getMainReadme(user, repo)
|
this.readme = await getMainReadme(user, repo)
|
||||||
})
|
})
|
||||||
@@ -114,7 +166,7 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedRepoId = data.generateId(
|
const savedRepoId = generateId(
|
||||||
DataType.SavedRepo,
|
DataType.SavedRepo,
|
||||||
`${this.user}-${this.repo}`
|
`${this.user}-${this.repo}`
|
||||||
)
|
)
|
||||||
@@ -132,35 +184,39 @@ export const useUserRepoStore = defineStore("USER_REPO_STATE", {
|
|||||||
this.user = ""
|
this.user = ""
|
||||||
this.repo = ""
|
this.repo = ""
|
||||||
this.resetFiles()
|
this.resetFiles()
|
||||||
|
this.userSettings = undefined
|
||||||
},
|
},
|
||||||
resetFiles() {
|
resetFiles() {
|
||||||
this.files = []
|
this.files = []
|
||||||
this.readme = null
|
this.readme = null
|
||||||
this.userSettings = undefined
|
|
||||||
},
|
},
|
||||||
setFontFamily(fontFamily: string) {
|
setFontFamily(fontFamily: string) {
|
||||||
if (!this.userSettings) {
|
if (!this.userSettings) {
|
||||||
return
|
this.userSettings = { $type: DataType.UserSettings }
|
||||||
}
|
}
|
||||||
this.userSettings.chosenFontFamily = fontFamily
|
this.userSettings.chosenFontFamily = fontFamily
|
||||||
|
this._persistFonts()
|
||||||
const userSettingsId = `UserSetting-${this.user}-${this.repo}`
|
|
||||||
data.update<DataType.UserSettings, UserSettings>({
|
|
||||||
...this.userSettings,
|
|
||||||
_id: userSettingsId
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
setFontSize(fontSize: string) {
|
setFontSize(fontSize: string) {
|
||||||
if (!this.userSettings) {
|
if (!this.userSettings) {
|
||||||
return
|
this.userSettings = { $type: DataType.UserSettings }
|
||||||
}
|
}
|
||||||
this.userSettings.chosenFontSize = fontSize
|
this.userSettings.chosenFontSize = fontSize
|
||||||
|
this._persistFonts()
|
||||||
const userSettingsId = `UserSetting-${this.user}-${this.repo}`
|
},
|
||||||
data.update<DataType.UserSettings, UserSettings>({
|
setTitleFont(font: string) {
|
||||||
...this.userSettings,
|
if (!this.userSettings) {
|
||||||
_id: userSettingsId
|
this.userSettings = { $type: DataType.UserSettings }
|
||||||
})
|
}
|
||||||
|
this.userSettings.chosenTitleFont = font
|
||||||
|
this._persistFonts()
|
||||||
|
},
|
||||||
|
setBodyFont(font: string) {
|
||||||
|
if (!this.userSettings) {
|
||||||
|
this.userSettings = { $type: DataType.UserSettings }
|
||||||
|
}
|
||||||
|
this.userSettings.chosenBodyFont = font
|
||||||
|
this._persistFonts()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ export const useUserSettings = () => {
|
|||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
|
|
||||||
const fontFamily = store.userSettings?.chosenFontFamily
|
|
||||||
const fontSize = store.userSettings?.chosenFontSize
|
const fontSize = store.userSettings?.chosenFontSize
|
||||||
|
const bodyFont = store.userSettings?.chosenBodyFont
|
||||||
|
const titleFont = store.userSettings?.chosenTitleFont
|
||||||
|
|
||||||
downloadFont(fontFamily || DEFAULT_FONT_POLICY)
|
downloadFont(bodyFont || DEFAULT_FONT_POLICY, "--font-family")
|
||||||
|
downloadFont(
|
||||||
|
titleFont || bodyFont || DEFAULT_FONT_POLICY,
|
||||||
|
"--title-font-family"
|
||||||
|
)
|
||||||
root.style.setProperty("--font-size", fontSize || DEFAULT_FONT_SIZE)
|
root.style.setProperty("--font-size", fontSize || DEFAULT_FONT_SIZE)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Octokit } from "@octokit/rest"
|
import { Octokit } from "@octokit/rest"
|
||||||
import { addMinutes, addSeconds, isBefore } from "date-fns"
|
import { addMinutes, addSeconds, isBefore } from "date-fns"
|
||||||
|
|
||||||
import { data } from "@/data/data"
|
import { data, generateId } from "@/data/data"
|
||||||
import { DataType } from "@/data/DataType.enum"
|
import { DataType } from "@/data/DataType.enum"
|
||||||
import { GithubAccessToken } from "@/data/models/GithubAccessToken"
|
import { GithubAccessToken } from "@/data/models/GithubAccessToken"
|
||||||
import { GithubToken } from "@/modules/user/interfaces/GithubToken"
|
import { GithubToken } from "@/modules/user/interfaces/GithubToken"
|
||||||
@@ -26,7 +26,7 @@ export const needToRefreshToken = async () => {
|
|||||||
const accessToken = await data.get<
|
const accessToken = await data.get<
|
||||||
DataType.GithubAccessToken,
|
DataType.GithubAccessToken,
|
||||||
GithubAccessToken
|
GithubAccessToken
|
||||||
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
|
>(generateId(DataType.GithubAccessToken, personalTokenId))
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return false
|
return false
|
||||||
@@ -42,7 +42,7 @@ export const refreshToken = async () => {
|
|||||||
const accessToken = await data.get<
|
const accessToken = await data.get<
|
||||||
DataType.GithubAccessToken,
|
DataType.GithubAccessToken,
|
||||||
GithubAccessToken
|
GithubAccessToken
|
||||||
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
|
>(generateId(DataType.GithubAccessToken, personalTokenId))
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return null
|
return null
|
||||||
@@ -74,7 +74,7 @@ export const getAccessToken = async () => {
|
|||||||
const response = await data.get<
|
const response = await data.get<
|
||||||
DataType.GithubAccessToken,
|
DataType.GithubAccessToken,
|
||||||
GithubAccessToken
|
GithubAccessToken
|
||||||
>(data.generateId(DataType.GithubAccessToken, personalTokenId))
|
>(generateId(DataType.GithubAccessToken, personalTokenId))
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export const saveAccessToken = async (githubToken: GithubToken) => {
|
|||||||
|
|
||||||
const accessToken: GithubAccessToken = {
|
const accessToken: GithubAccessToken = {
|
||||||
...actualPAT,
|
...actualPAT,
|
||||||
_id: data.generateId(DataType.GithubAccessToken, personalTokenId),
|
_id: generateId(DataType.GithubAccessToken, personalTokenId),
|
||||||
$type: DataType.GithubAccessToken,
|
$type: DataType.GithubAccessToken,
|
||||||
token: githubToken.access_token,
|
token: githubToken.access_token,
|
||||||
expiresIn: githubToken.expires_in,
|
expiresIn: githubToken.expires_in,
|
||||||
|
|||||||
@@ -104,7 +104,12 @@ router.beforeEach(() => {
|
|||||||
}
|
}
|
||||||
).startViewTransition(async () => {
|
).startViewTransition(async () => {
|
||||||
resolve()
|
resolve()
|
||||||
await nextTick()
|
await new Promise<void>((r) => {
|
||||||
|
const unwatch = router.afterEach(() => {
|
||||||
|
unwatch()
|
||||||
|
nextTick().then(r)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
6
src/shims-vue.d.ts
vendored
6
src/shims-vue.d.ts
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
||||||
declare module "pouchdb-adapter-indexeddb"
|
declare module "pouchdb-adapter-indexeddb"
|
||||||
declare module "@toycode/markdown-it-class"
|
declare module "@toycode/markdown-it-class"
|
||||||
declare module "markdown-it-block-embed"
|
declare module "markdown-it-block-embed"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #ffa4c0;
|
--primary-color: #ffa4c0;
|
||||||
--font-family: "Libertinus Serif", serif;
|
--font-family: "Libertinus Serif", serif;
|
||||||
|
--title-font-family: "Libertinus Serif", serif;
|
||||||
--font-size: 13pt;
|
--font-size: 13pt;
|
||||||
--font-color: #4a4a4a;
|
--font-color: #4a4a4a;
|
||||||
--link: #445fb9;
|
--link: #445fb9;
|
||||||
@@ -24,8 +25,8 @@
|
|||||||
|
|
||||||
@plugin 'daisyui' {
|
@plugin 'daisyui' {
|
||||||
themes:
|
themes:
|
||||||
garden --default,
|
emerald --default,
|
||||||
dim --prefersdark;
|
forest --prefersdark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@config '../../tailwind.config.js';
|
@config '../../tailwind.config.js';
|
||||||
@@ -48,22 +49,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html,
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 769px) {
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.columns {
|
.columns {
|
||||||
@@ -77,7 +66,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: 'emerald',
|
||||||
dark: "dim"
|
dark: 'forest'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ const assembleFontLink = (font: string) => {
|
|||||||
.replaceAll(" ", "+")}`
|
.replaceAll(" ", "+")}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadFont = async (font: string): Promise<void> => {
|
export const downloadFont = async (
|
||||||
|
font: string,
|
||||||
|
cssVar = "--font-family"
|
||||||
|
): Promise<void> => {
|
||||||
const href = assembleFontLink(font)
|
const href = assembleFontLink(font)
|
||||||
|
|
||||||
// check if the href already exists
|
// check if the href already exists
|
||||||
@@ -23,7 +26,7 @@ export const downloadFont = async (font: string): Promise<void> => {
|
|||||||
try {
|
try {
|
||||||
await new FontFaceObserver(font).load()
|
await new FontFaceObserver(font).load()
|
||||||
|
|
||||||
document.documentElement.style.setProperty("--font-family", font)
|
document.documentElement.style.setProperty(cssVar, font)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("error when loading font")
|
console.warn("error when loading font")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import WelcomeWorld from "@/components/WelcomeWorld.vue"
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorize {
|
.authorize {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useTitle } from "@vueuse/core"
|
|||||||
import { computed, nextTick, ref, watch } from "vue"
|
import { computed, nextTick, ref, watch } from "vue"
|
||||||
import { useRouter } from "vue-router"
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
import BackButton from "@/components/BackButton.vue"
|
import HomeButton from "@/components/HomeButton.vue"
|
||||||
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
import SkeletonLoader from "@/components/SkeletonLoader.vue"
|
||||||
import StackedPublicNote from "@/components/StackedPublicNote.vue"
|
import StackedPublicNote from "@/components/StackedPublicNote.vue"
|
||||||
import ThemeSwap from "@/components/ThemeSwap.vue"
|
import ThemeSwap from "@/components/ThemeSwap.vue"
|
||||||
@@ -129,13 +129,7 @@ watch(
|
|||||||
<main class="public-note-view repo-note note-container">
|
<main class="public-note-view repo-note note-container">
|
||||||
<div class="note article">
|
<div class="note article">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<back-button
|
<home-button />
|
||||||
:fallback="{ name: 'PublicNoteListByDidView', params: { shortDid } }"
|
|
||||||
:prefer-fallback="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img src="/favicon.png" alt="Remanso" class="remanso-logo" />
|
|
||||||
|
|
||||||
<theme-swap />
|
<theme-swap />
|
||||||
</div>
|
</div>
|
||||||
<div class="subheader">
|
<div class="subheader">
|
||||||
@@ -197,13 +191,6 @@ watch(
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remanso-logo {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
box-shadow: none;
|
|
||||||
view-transition-name: remanso-logo;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subheader {
|
.subheader {
|
||||||
margin: 1rem auto 0;
|
margin: 1rem auto 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { vInfiniteScroll } from "@vueuse/components"
|
||||||
|
|
||||||
import GoBack from "@/components/GoBack.vue"
|
import GoBack from "@/components/GoBack.vue"
|
||||||
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
import { useGitHubLogin } from "@/hooks/useGitHubLogin.hook"
|
||||||
import { useRepos } from "@/hooks/useRepos.hook"
|
import { useRepos } from "@/hooks/useRepos.hook"
|
||||||
@@ -6,12 +8,15 @@ import { useRepoList } from "@/modules/repo/hooks/useRepoList.hook"
|
|||||||
|
|
||||||
const { username } = useGitHubLogin()
|
const { username } = useGitHubLogin()
|
||||||
const { isReady } = useRepos()
|
const { isReady } = useRepos()
|
||||||
const { favoriteRepos, otherRepos, favoriteCheckboxes, toggleCheckbox } =
|
const { favoriteRepos, otherRepos, favoriteCheckboxes, toggleCheckbox, canLoadMore, loadMore } =
|
||||||
useRepoList()
|
useRepoList()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="repo-list">
|
<div
|
||||||
|
class="repo-list"
|
||||||
|
v-infinite-scroll="[loadMore, { canLoadMore: () => canLoadMore }]"
|
||||||
|
>
|
||||||
<h1 class="title is-1">Repositories</h1>
|
<h1 class="title is-1">Repositories</h1>
|
||||||
<go-back />
|
<go-back />
|
||||||
<div v-if="!isReady">loading...</div>
|
<div v-if="!isReady">loading...</div>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const defaultTitleStyles = Array.from(
|
|||||||
...acc,
|
...acc,
|
||||||
[heading]: {
|
[heading]: {
|
||||||
"margin-top": "0",
|
"margin-top": "0",
|
||||||
"margin-bottom": "0.5em"
|
"margin-bottom": "0.5em",
|
||||||
|
"font-family": "var(--title-font-family)"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -47,11 +47,6 @@ export default defineConfig(({ command }) => {
|
|||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
type: "image/png"
|
type: "image/png"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
src: "favicon.png",
|
|
||||||
sizes: "1024x1024",
|
|
||||||
type: "image/png"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
src: "maskable-icon-512x512.png",
|
src: "maskable-icon-512x512.png",
|
||||||
sizes: "512x512",
|
sizes: "512x512",
|
||||||
|
|||||||
Reference in New Issue
Block a user