v0.2.78
This commit is contained in:
1 parent
a6efa52f78
commit
0a269cf6fb
11 files changed
+437
-22
No files matched your search
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.77"
|
version = "0.2.78"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.77"
|
version = "0.2.78"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ Optional databases in Docker Compose:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# PostgreSQL
|
# PostgreSQL
|
||||||
docker compose --profile postgres up -d postgres
|
docker compose --profile postgres up -d pgsql
|
||||||
DATABASE_URL=postgres://rustpad:rustpad@postgres:5432/rustpad docker compose up -d rustpad
|
DATABASE_URL=postgres://rustpad:rustpad@postgres:5432/rustpad docker compose up -d rustpad
|
||||||
|
|
||||||
# MySQL
|
# MySQL
|
||||||
|
|||||||
Executable
+350
@@ -0,0 +1,350 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'USAGE'
|
||||||
|
Usage:
|
||||||
|
bash scripts/load-speed.sh 'https://rustpad.example.com/p/test-note-features'
|
||||||
|
bash scripts/load-speed.sh 'https://rustpad.example.com/w/team'
|
||||||
|
bash scripts/load-speed.sh 'https://rustpad.example.com/w/team/n/note'
|
||||||
|
|
||||||
|
The script will prompt for the RustPad account login and password.
|
||||||
|
If the pad/workspace also has resource password protection, it will ask for that password separately.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
for cmd in curl jq wc mktemp; do
|
||||||
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: required command not found: $cmd" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INPUT_URL="$1"
|
||||||
|
|
||||||
|
if [[ ! "$INPUT_URL" =~ ^(https?://[^/]+)(/.*)?$ ]]; then
|
||||||
|
echo "ERROR: pass a full URL starting with http:// or https://" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
BASE="${BASH_REMATCH[1]}"
|
||||||
|
PATH_WITH_QUERY="${BASH_REMATCH[2]:-/}"
|
||||||
|
PATH_ONLY="${PATH_WITH_QUERY%%\#*}"
|
||||||
|
PATH_ONLY="${PATH_ONLY%%\?*}"
|
||||||
|
[[ "$PATH_ONLY" != "/" ]] && PATH_ONLY="${PATH_ONLY%/}"
|
||||||
|
|
||||||
|
TYPE=""
|
||||||
|
RESOURCE_KIND=""
|
||||||
|
RESOURCE_SLUG=""
|
||||||
|
API_BASE=""
|
||||||
|
WORKSPACE_SLUG=""
|
||||||
|
NOTE_SLUG=""
|
||||||
|
PAD_SLUG=""
|
||||||
|
|
||||||
|
if [[ "$PATH_ONLY" =~ ^/p/([^/]+)$ ]]; then
|
||||||
|
TYPE="pad"
|
||||||
|
RESOURCE_KIND="pad"
|
||||||
|
PAD_SLUG="${BASH_REMATCH[1]}"
|
||||||
|
RESOURCE_SLUG="$PAD_SLUG"
|
||||||
|
API_BASE="$BASE/api/pads/$PAD_SLUG"
|
||||||
|
elif [[ "$PATH_ONLY" =~ ^/w/([^/]+)/n/([^/]+)$ ]]; then
|
||||||
|
TYPE="workspace-note"
|
||||||
|
RESOURCE_KIND="workspace"
|
||||||
|
WORKSPACE_SLUG="${BASH_REMATCH[1]}"
|
||||||
|
NOTE_SLUG="${BASH_REMATCH[2]}"
|
||||||
|
RESOURCE_SLUG="$WORKSPACE_SLUG"
|
||||||
|
API_BASE="$BASE/api/workspaces/$WORKSPACE_SLUG/notes/$NOTE_SLUG"
|
||||||
|
elif [[ "$PATH_ONLY" =~ ^/w/([^/]+)$ ]]; then
|
||||||
|
TYPE="workspace"
|
||||||
|
RESOURCE_KIND="workspace"
|
||||||
|
WORKSPACE_SLUG="${BASH_REMATCH[1]}"
|
||||||
|
RESOURCE_SLUG="$WORKSPACE_SLUG"
|
||||||
|
API_BASE="$BASE/api/workspaces/$WORKSPACE_SLUG"
|
||||||
|
else
|
||||||
|
echo "ERROR: unsupported RustPad URL: $PATH_ONLY" >&2
|
||||||
|
echo "Supported: /p/<pad>, /w/<workspace>, /w/<workspace>/n/<note>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
COOKIE_JAR="$TMP_DIR/cookies.txt"
|
||||||
|
CSRF_BODY="$TMP_DIR/csrf.json"
|
||||||
|
LOGIN_BODY="$TMP_DIR/login.json"
|
||||||
|
INFO_BODY="$TMP_DIR/info.json"
|
||||||
|
UNLOCK_BODY="$TMP_DIR/unlock.json"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
touch "$COOKIE_JAR"
|
||||||
|
chmod 600 "$COOKIE_JAR"
|
||||||
|
|
||||||
|
CURL_COMMON=(
|
||||||
|
--silent
|
||||||
|
--show-error
|
||||||
|
--compressed
|
||||||
|
--connect-timeout 10
|
||||||
|
--max-time 120
|
||||||
|
-b "$COOKIE_JAR"
|
||||||
|
-c "$COOKIE_JAR"
|
||||||
|
)
|
||||||
|
|
||||||
|
human_bytes() {
|
||||||
|
local bytes="$1"
|
||||||
|
if (( bytes >= 1048576 )); then
|
||||||
|
awk -v b="$bytes" 'BEGIN { printf "%.2f MiB", b / 1048576 }'
|
||||||
|
elif (( bytes >= 1024 )); then
|
||||||
|
awk -v b="$bytes" 'BEGIN { printf "%.1f KiB", b / 1024 }'
|
||||||
|
else
|
||||||
|
printf '%s B' "$bytes"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
measure_to_file() {
|
||||||
|
local name="$1"
|
||||||
|
local outfile="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local stats code transfer ttfb total body_bytes
|
||||||
|
stats="$(curl "${CURL_COMMON[@]}" "$@" \
|
||||||
|
-o "$outfile" \
|
||||||
|
-w '%{http_code}|%{size_download}|%{time_starttransfer}|%{time_total}')"
|
||||||
|
|
||||||
|
IFS='|' read -r code transfer ttfb total <<< "$stats"
|
||||||
|
body_bytes="$(wc -c < "$outfile" | tr -d '[:space:]')"
|
||||||
|
|
||||||
|
printf '%-20s HTTP=%-3s transfer=%10s body=%10s TTFB=%8ss total=%8ss\n' \
|
||||||
|
"$name" "$code" "$(human_bytes "${transfer%.*}")" "$(human_bytes "$body_bytes")" "$ttfb" "$total"
|
||||||
|
|
||||||
|
LAST_HTTP="$code"
|
||||||
|
}
|
||||||
|
|
||||||
|
measure() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
local outfile="$TMP_DIR/measure-${name//[^a-zA-Z0-9_-]/_}.body"
|
||||||
|
measure_to_file "$name" "$outfile" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
api_post_json() {
|
||||||
|
local url="$1"
|
||||||
|
local json="$2"
|
||||||
|
local outfile="$3"
|
||||||
|
local code
|
||||||
|
|
||||||
|
code="$(curl "${CURL_COMMON[@]}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data "$json" \
|
||||||
|
-o "$outfile" \
|
||||||
|
-w '%{http_code}' \
|
||||||
|
"$url")"
|
||||||
|
LAST_HTTP="$code"
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_csrf() {
|
||||||
|
local code
|
||||||
|
code="$(curl "${CURL_COMMON[@]}" \
|
||||||
|
-o "$CSRF_BODY" \
|
||||||
|
-w '%{http_code}' \
|
||||||
|
"$BASE/api/security/csrf")"
|
||||||
|
|
||||||
|
if [[ "$code" != "200" ]]; then
|
||||||
|
echo "ERROR: CSRF endpoint returned HTTP $code" >&2
|
||||||
|
cat "$CSRF_BODY" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CSRF="$(jq -r '.token // empty' "$CSRF_BODY")"
|
||||||
|
if [[ -z "$CSRF" ]]; then
|
||||||
|
echo "ERROR: server did not return a CSRF token" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
login_account() {
|
||||||
|
local login password payload code message nickname
|
||||||
|
|
||||||
|
printf 'Login/e-mail: '
|
||||||
|
IFS= read -r login
|
||||||
|
if [[ -z "$login" ]]; then
|
||||||
|
echo "ERROR: login cannot be empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Password: '
|
||||||
|
IFS= read -rs password
|
||||||
|
printf '\n'
|
||||||
|
if [[ -z "$password" ]]; then
|
||||||
|
echo "ERROR: password cannot be empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload="$(jq -nc --arg email "$login" --arg password "$password" '{email:$email,password:$password}')"
|
||||||
|
unset password
|
||||||
|
|
||||||
|
code="$(curl "${CURL_COMMON[@]}" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data "$payload" \
|
||||||
|
-o "$LOGIN_BODY" \
|
||||||
|
-w '%{http_code}' \
|
||||||
|
"$BASE/api/auth/login")"
|
||||||
|
unset payload
|
||||||
|
|
||||||
|
if [[ "$code" != "200" ]]; then
|
||||||
|
message="$(jq -r '.error // empty' "$LOGIN_BODY" 2>/dev/null || true)"
|
||||||
|
echo "ERROR: login failed (HTTP $code)${message:+: $message}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
nickname="$(jq -r '.nickname // empty' "$LOGIN_BODY")"
|
||||||
|
echo "Logged in${nickname:+ as $nickname}."
|
||||||
|
}
|
||||||
|
|
||||||
|
unlock_resource_if_needed() {
|
||||||
|
local info_file="$1"
|
||||||
|
local protected access_level resource_password payload code message
|
||||||
|
|
||||||
|
protected="$(jq -r '.protected // false' "$info_file" 2>/dev/null || echo false)"
|
||||||
|
access_level="$(jq -r '.access_level // empty' "$info_file" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "$protected" != "true" || "$access_level" != "none" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Resource password (%s): ' "$RESOURCE_KIND"
|
||||||
|
IFS= read -rs resource_password
|
||||||
|
printf '\n'
|
||||||
|
if [[ -z "$resource_password" ]]; then
|
||||||
|
echo "ERROR: this resource requires a password" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload="$(jq -nc \
|
||||||
|
--arg kind "$RESOURCE_KIND" \
|
||||||
|
--arg slug "$RESOURCE_SLUG" \
|
||||||
|
--arg password "$resource_password" \
|
||||||
|
'{kind:$kind,slug:$slug,password:$password}')"
|
||||||
|
unset resource_password
|
||||||
|
|
||||||
|
api_post_json "$BASE/api/access-token" "$payload" "$UNLOCK_BODY"
|
||||||
|
code="$LAST_HTTP"
|
||||||
|
unset payload
|
||||||
|
|
||||||
|
if [[ "$code" != "200" ]]; then
|
||||||
|
message="$(jq -r '.error // empty' "$UNLOCK_BODY" 2>/dev/null || true)"
|
||||||
|
echo "ERROR: resource unlock failed (HTTP $code)${message:+: $message}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resource unlocked."
|
||||||
|
}
|
||||||
|
|
||||||
|
print_header() {
|
||||||
|
echo
|
||||||
|
echo "Target: $INPUT_URL"
|
||||||
|
echo "Type: $TYPE"
|
||||||
|
echo "Base: $BASE"
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch_csrf
|
||||||
|
login_account
|
||||||
|
print_header
|
||||||
|
|
||||||
|
echo '=== PAGE / SESSION ==='
|
||||||
|
measure "page_html" "$BASE$PATH_WITH_QUERY"
|
||||||
|
measure "auth_me" "$BASE/api/auth/me"
|
||||||
|
|
||||||
|
echo
|
||||||
|
case "$TYPE" in
|
||||||
|
pad)
|
||||||
|
echo '=== INITIAL NOTE LOAD ==='
|
||||||
|
measure_to_file "note_info" "$INFO_BODY" "$API_BASE"
|
||||||
|
if [[ "$LAST_HTTP" != "200" ]]; then
|
||||||
|
echo "ERROR: note_info failed; cannot continue reliably." >&2
|
||||||
|
jq -r '.error // empty' "$INFO_BODY" 2>/dev/null >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unlock_resource_if_needed "$INFO_BODY"
|
||||||
|
|
||||||
|
measure "editor_color" "$API_BASE/editor-color"
|
||||||
|
measure "favorite_status" "$BASE/api/auth/favorites/status?kind=pad&slug=$PAD_SLUG"
|
||||||
|
measure "files" \
|
||||||
|
-X PUT \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data '{"access_token":null}' \
|
||||||
|
"$API_BASE/files"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo '=== ON DEMAND ==='
|
||||||
|
measure "history" \
|
||||||
|
-X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data '{"access_token":null}' \
|
||||||
|
"$API_BASE/history"
|
||||||
|
;;
|
||||||
|
|
||||||
|
workspace-note)
|
||||||
|
echo '=== INITIAL NOTE LOAD ==='
|
||||||
|
measure_to_file "note_info" "$INFO_BODY" "$API_BASE"
|
||||||
|
if [[ "$LAST_HTTP" != "200" ]]; then
|
||||||
|
echo "ERROR: note_info failed; cannot continue reliably." >&2
|
||||||
|
jq -r '.error // empty' "$INFO_BODY" 2>/dev/null >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unlock_resource_if_needed "$INFO_BODY"
|
||||||
|
|
||||||
|
measure "editor_color" "$API_BASE/editor-color"
|
||||||
|
measure "favorite_status" "$BASE/api/auth/favorites/status?kind=note&slug=$NOTE_SLUG&workspace_slug=$WORKSPACE_SLUG"
|
||||||
|
measure "files" \
|
||||||
|
-X PUT \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data '{"access_token":null}' \
|
||||||
|
"$API_BASE/files"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo '=== ON DEMAND ==='
|
||||||
|
measure "history" \
|
||||||
|
-X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data '{"access_token":null}' \
|
||||||
|
"$API_BASE/history"
|
||||||
|
;;
|
||||||
|
|
||||||
|
workspace)
|
||||||
|
echo '=== INITIAL WORKSPACE LOAD ==='
|
||||||
|
measure_to_file "workspace_info" "$INFO_BODY" "$API_BASE"
|
||||||
|
if [[ "$LAST_HTTP" != "200" ]]; then
|
||||||
|
echo "ERROR: workspace_info failed; cannot continue reliably." >&2
|
||||||
|
jq -r '.error // empty' "$INFO_BODY" 2>/dev/null >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unlock_resource_if_needed "$INFO_BODY"
|
||||||
|
|
||||||
|
measure "workspace_open" \
|
||||||
|
-X POST \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "x-rustpad-csrf: $CSRF" \
|
||||||
|
--data '{"access_token":null}' \
|
||||||
|
"$API_BASE/open?q=&page=1&per_page=25"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo 'Note: WebSocket payloads are not included in these curl measurements.'
|
||||||
@@ -22,7 +22,9 @@ const MODULES: &[&str] = &[
|
|||||||
"connection-state",
|
"connection-state",
|
||||||
"editor-format",
|
"editor-format",
|
||||||
"emoji-data",
|
"emoji-data",
|
||||||
|
"emoji-groups",
|
||||||
"emoji-picker",
|
"emoji-picker",
|
||||||
|
"emoji-shortcodes",
|
||||||
"image-alias",
|
"image-alias",
|
||||||
"image-upload",
|
"image-upload",
|
||||||
"i18n",
|
"i18n",
|
||||||
|
|||||||
@@ -9,6 +9,5 @@
|
|||||||
|
|
||||||
// Generated from Unicode Emoji 17.0 emoji-test.txt.
|
// Generated from Unicode Emoji 17.0 emoji-test.txt.
|
||||||
// Native glyphs use the platform emoji font (Apple style on Apple devices).
|
// Native glyphs use the platform emoji font (Apple style on Apple devices).
|
||||||
export const EMOJI_GROUPS = [{ "name": "Smileys & Emotion", "items": [{ "emoji": "😀", "name": "grinning face", "keywords": "face smiling" }, { "emoji": "😃", "name": "grinning face with big eyes", "keywords": "face smiling" }, { "emoji": "😄", "name": "grinning face with smiling eyes", "keywords": "face smiling" }, { "emoji": "😁", "name": "beaming face with smiling eyes", "keywords": "face smiling" }, { "emoji": "😆", "name": "grinning squinting face", "keywords": "face smiling" }, { "emoji": "😅", "name": "grinning face with sweat", "keywords": "face smiling" }, { "emoji": "🤣", "name": "rolling on the floor laughing", "keywords": "face smiling" }, { "emoji": "😂", "name": "face with tears of joy", "keywords": "face smiling" }, { "emoji": "🙂", "name": "slightly smiling face", "keywords": "face smiling" }, { "emoji": "🙃", "name": "upside-down face", "keywords": "face smiling" }, { "emoji": "🫠", "name": "melting face", "keywords": "face smiling" }, { "emoji": "😉", "name": "winking face", "keywords": "face smiling" }, { "emoji": "😊", "name": "smiling face with smiling eyes", "keywords": "face smiling" }, { "emoji": "😇", "name": "smiling face with halo", "keywords": "face smiling" }, { "emoji": "🥰", "name": "smiling face with hearts", "keywords": "face affection" }, { "emoji": "😍", "name": "smiling face with heart-eyes", "keywords": "face affection" }, { "emoji": "🤩", "name": "star-struck", "keywords": "face affection" }, { "emoji": "😘", "name": "face blowing a kiss", "keywords": "face affection" }, { "emoji": "😗", "name": "kissing face", "keywords": "face affection" }, { "emoji": "☺️", "name": "smiling face", "keywords": "face affection" }, { "emoji": "😚", "name": "kissing face with closed eyes", "keywords": "face affection" }, { "emoji": "😙", "name": "kissing face with smiling eyes", "keywords": "face affection" }, { "emoji": "🥲", "name": "smiling face with tear", "keywords": "face affection" }, { "emoji": "😋", "name": "face savoring food", "keywords": "face tongue" }, { "emoji": "😛", "name": "face with tongue", "keywords": "face tongue" }, { "emoji": "😜", "name": "winking face with tongue", "keywords": "face tongue" }, { "emoji": "🤪", "name": "zany face", "keywords": "face tongue" }, { "emoji": "😝", "name": "squinting face with tongue", "keywords": "face tongue" }, { "emoji": "🤑", "name": "money-mouth face", "keywords": "face tongue" }, { "emoji": "🤗", "name": "smiling face with open hands", "keywords": "face hand" }, { "emoji": "🤭", "name": "face with hand over mouth", "keywords": "face hand" }, { "emoji": "🫢", "name": "face with open eyes and hand over mouth", "keywords": "face hand" }, { "emoji": "🫣", "name": "face with peeking eye", "keywords": "face hand" }, { "emoji": "🤫", "name": "shushing face", "keywords": "face hand" }, { "emoji": "🤔", "name": "thinking face", "keywords": "face hand" }, { "emoji": "🫡", "name": "saluting face", "keywords": "face hand" }, { "emoji": "🤐", "name": "zipper-mouth face", "keywords": "face neutral skeptical" }, { "emoji": "🤨", "name": "face with raised eyebrow", "keywords": "face neutral skeptical" }, { "emoji": "😐", "name": "neutral face", "keywords": "face neutral skeptical" }, { "emoji": "😑", "name": "expressionless face", "keywords": "face neutral skeptical" }, { "emoji": "😶", "name": "face without mouth", "keywords": "face neutral skeptical" }, { "emoji": "🫥", "name": "dotted line face", "keywords": "face neutral skeptical" }, { "emoji": "😶🌫️", "name": "face in clouds", "keywords": "face neutral skeptical" }, { "emoji": "😏", "name": "smirking face", "keywords": "face neutral skeptical" }, { "emoji": "😒", "name": "unamused face", "keywords": "face neutral skeptical" }, { "emoji": "🙄", "name": "face with rolling eyes", "keywords": "face neutral skeptical" }, { "emoji": "😬", "name": "grimacing face", "keywords": "face neutral skeptical" }, { "emoji": "😮💨", "name": "face exhaling", "keywords": "face neutral skeptical" }, { "emoji": "🤥", "name": "lying face", "keywords": "face neutral skeptical" }, { "emoji": "🫨", "name": "shaking face", "keywords": "face neutral skeptical" }, { "emoji": "🙂↔️", "name": "head shaking horizontally", "keywords": "face neutral skeptical" }, { "emoji": "🙂↕️", "name": "head shaking vertically", "keywords": "face neutral skeptical" }, { "emoji": "😌", "name": "relieved face", "keywords": "face sleepy" }, { "emoji": "😔", "name": "pensive face", "keywords": "face sleepy" }, { "emoji": "😪", "name": "sleepy face", "keywords": "face sleepy" }, { "emoji": "🤤", "name": "drooling face", "keywords": "face sleepy" }, { "emoji": "😴", "name": "sleeping face", "keywords": "face sleepy" }, { "emoji": "", "name": "face with bags under eyes", "keywords": "face sleepy" }, { "emoji": "😷", "name": "face with medical mask", "keywords": "face unwell" }, { "emoji": "Line truncated
|
export { EMOJI_GROUPS } from "@rustpad/emoji-groups";
|
||||||
|
export { EMOJI_SHORTCODES } from "@rustpad/emoji-shortcodes";
|
||||||
export const EMOJI_SHORTCODES = { "+1": "👍", "-1": "👎", "100": "💯", "1st_place_medal": "🥇", "2nd_place_medal": "🥈", "3rd_place_medal": "🥉", "a_button_blood_type": "🅰️", "ab_button_blood_type": "🆎", "abacus": "🧮", "accordion": "🪗", "adhesive_bandage": "🩹", "admission_tickets": "🎟️", "aerial_tramway": "🚡", "airplane": "✈️", "airplane_arrival": "🛬", "airplane_departure": "🛫", "alarm_clock": "⏰", "alembic": "⚗️", "alien": "👽", "alien_monster": "👾", "ambulance": "🚑", "american_football": "🏈", "amphora": "🏺", "anatomical_heart": "🫀", "anchor": "⚓", "anger_symbol": "💢", "angry": "😠", "angry_face": "😠", "angry_face_with_horns": "👿", "anguished_face": "😧", "ant": "🐜", "antenna_bars": "📶", "anxious_face_with_sweat": "😰", "aquarius": "♒", "aries": "♈", "articulated_lorry": "🚛", "artist": "🧑🎨", "artist_dark_skin_tone": "🧑🏿🎨", "artist_light_skin_tone": "🧑🏻🎨", "artist_medium_dark_skin_tone": "🧑🏾🎨", "artist_medium_light_skin_tone": "🧑🏼🎨", "artist_medium_skin_tone": "🧑🏽🎨", "artist_palette": "🎨", "astonished_face": "😲", "astronaut": "🧑🚀", "astronaut_dark_skin_tone": "🧑🏿🚀", "astronaut_light_skin_tone": "🧑🏻🚀", "astronaut_medium_dark_skin_tone": "🧑🏾🚀", "astronaut_medium_light_skin_tone": "🧑🏼🚀", "astronaut_medium_skin_tone": "🧑🏽🚀", "atm_sign": "🏧", "atom_symbol": "⚛️", "auto_rickshaw": "🛺", "automobile": "🚗", "avocado": "🥑", "axe": "🪓", "b_button_blood_type": "🅱️", "baby": "👶", "baby_angel": "👼", "baby_angel_dark_skin_tone": "👼🏿", "baby_angel_light_skin_tone": "👼🏻", "baby_angel_medium_dark_skin_tone": "👼🏾", "baby_angel_medium_light_skin_tone": "👼🏼", "baby_angel_medium_skin_tone": "👼🏽", "baby_bottle": "🍼", "baby_chick": "🐤", "baby_dark_skin_tone": "👶🏿", "baby_light_skin_tone": "👶🏻", "baby_medium_dark_skin_tone": "👶🏾", "baby_medium_light_skin_tone": "👶🏼", "baby_medium_skin_tone": "👶🏽", "baby_symbol": "🚼", "back_arrow": "🔙", "backhand_index_pointing_down": "👇", "backhand_index_pointing_down_dark_skin_tone": "👇🏿", "backhand_index_pointing_down_light_skin_tone": "👇🏻", "backhand_index_pointing_down_medium_dark_skin_tone": "👇🏾", "backhand_index_pointing_down_medium_light_skin_tone": "👇🏼", "backhand_index_pointing_down_medium_skin_tone": "👇🏽", "backhand_index_pointing_left": "👈", "backhand_index_pointing_left_dark_skin_tone": "👈🏿", "backhand_index_pointing_left_light_skin_tone": "👈🏻", "backhand_index_pointing_left_medium_dark_skin_tone": "👈🏾", "backhand_index_pointing_left_medium_light_skin_tone": "👈🏼", "backhand_index_pointing_left_medium_skin_tone": "👈🏽", "backhand_index_pointing_right": "👉", "backhand_index_pointing_right_dark_skin_tone": "👉🏿", "backhand_index_pointing_right_light_skin_tone": "👉🏻", "backhand_index_pointing_right_medium_dark_skin_tone": "👉🏾", "backhand_index_pointing_right_medium_light_skin_tone": "👉🏼", "backhand_index_pointing_right_medium_skin_tone": "👉🏽", "backhand_index_pointing_up": "👆", "backhand_index_pointing_up_dark_skin_tone": "👆🏿", "backhand_index_pointing_up_light_skin_tone": "👆🏻", "backhand_index_pointing_up_medium_dark_skin_tone": "👆🏾", "backhand_index_pointing_up_medium_light_skin_tone": "👆🏼", "backhand_index_pointing_up_medium_skin_tone": "👆🏽", "backpack": "🎒", "bacon": "🥓", "badger": "🦡", "badminton": "🏸", "bagel": "🥯", "baggage_claim": "🛄", "baguette_bread": "🥖", "balance_scale": "⚖️", "ballet_dancer": "🧑🩰", "ballet_dancer_dark_skin_tone": "🧑🏿🩰", "ballet_dancer_light_skin_tone": "🧑🏻🩰", "ballet_dancer_medium_dark_skin_tone": "🧑🏾🩰", "ballet_dancer_medium_light_skin_tone": "🧑🏼🩰", "ballet_dancer_medium_skin_tone": "🧑🏽🩰", "ballet_shoes": "🩰", "balloon": "🎈", "ballot_box_with_ballot": "🗳️", "banana": "🍌", "banjo": "🪕", "bank": "🏦", "bar_chart": "📊", "barber_pole": "💈", "baseball": "⚾", "basket": "🧺", "basketball": "🏀", "bat": "🦇", "bathtub": "🛁", "battery": "🔋", "beach_with_umbrella": "🏖️", "beaming_face_with_smiling_eyes": "😁", "beans": "🫘", "bear": "🐻", "beating_heart": "💓", "beaver": "🦫", "bed": "🛏️", "beer_mug": "🍺", "beetle": "🪲", "bell": "🔔", "bell_pepper": "🫑", "bell_with_slash": "🔕", "bellhop_bell": "🛎️", "bento_box": "🍱", "beverage_box": "🧃", "bicycle": "🚲", "bikini": "👙", "billed_cap": "🧢", "biohazard": "☣️", "bird": "🐦", "birthday_cake": "🎂", "bison": "🦬", "biting_lip": "🫦", "black_bird": "🐦⬛", "black_cat": "🐈⬛", "black_circle": "⚫", "black_flag": "🏴", "black_heart": "🖤", "black_larLine truncated
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||||||
|
* Source-Available Code / Dual-Licensed.
|
||||||
|
*
|
||||||
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||||||
|
* Commercial or production use requires a valid paid license.
|
||||||
|
* See LICENSE file in repository root for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Generated from Unicode Emoji 17.0 emoji-test.txt.
|
||||||
|
// Native glyphs use the platform emoji font (Apple style on Apple devices).
|
||||||
|
export const EMOJI_GROUPS = [{ "name": "Smileys & Emotion", "items": [{ "emoji": "😀", "name": "grinning face", "keywords": "face smiling" }, { "emoji": "😃", "name": "grinning face with big eyes", "keywords": "face smiling" }, { "emoji": "😄", "name": "grinning face with smiling eyes", "keywords": "face smiling" }, { "emoji": "😁", "name": "beaming face with smiling eyes", "keywords": "face smiling" }, { "emoji": "😆", "name": "grinning squinting face", "keywords": "face smiling" }, { "emoji": "😅", "name": "grinning face with sweat", "keywords": "face smiling" }, { "emoji": "🤣", "name": "rolling on the floor laughing", "keywords": "face smiling" }, { "emoji": "😂", "name": "face with tears of joy", "keywords": "face smiling" }, { "emoji": "🙂", "name": "slightly smiling face", "keywords": "face smiling" }, { "emoji": "🙃", "name": "upside-down face", "keywords": "face smiling" }, { "emoji": "🫠", "name": "melting face", "keywords": "face smiling" }, { "emoji": "😉", "name": "winking face", "keywords": "face smiling" }, { "emoji": "😊", "name": "smiling face with smiling eyes", "keywords": "face smiling" }, { "emoji": "😇", "name": "smiling face with halo", "keywords": "face smiling" }, { "emoji": "🥰", "name": "smiling face with hearts", "keywords": "face affection" }, { "emoji": "😍", "name": "smiling face with heart-eyes", "keywords": "face affection" }, { "emoji": "🤩", "name": "star-struck", "keywords": "face affection" }, { "emoji": "😘", "name": "face blowing a kiss", "keywords": "face affection" }, { "emoji": "😗", "name": "kissing face", "keywords": "face affection" }, { "emoji": "☺️", "name": "smiling face", "keywords": "face affection" }, { "emoji": "😚", "name": "kissing face with closed eyes", "keywords": "face affection" }, { "emoji": "😙", "name": "kissing face with smiling eyes", "keywords": "face affection" }, { "emoji": "🥲", "name": "smiling face with tear", "keywords": "face affection" }, { "emoji": "😋", "name": "face savoring food", "keywords": "face tongue" }, { "emoji": "😛", "name": "face with tongue", "keywords": "face tongue" }, { "emoji": "😜", "name": "winking face with tongue", "keywords": "face tongue" }, { "emoji": "🤪", "name": "zany face", "keywords": "face tongue" }, { "emoji": "😝", "name": "squinting face with tongue", "keywords": "face tongue" }, { "emoji": "🤑", "name": "money-mouth face", "keywords": "face tongue" }, { "emoji": "🤗", "name": "smiling face with open hands", "keywords": "face hand" }, { "emoji": "🤭", "name": "face with hand over mouth", "keywords": "face hand" }, { "emoji": "🫢", "name": "face with open eyes and hand over mouth", "keywords": "face hand" }, { "emoji": "🫣", "name": "face with peeking eye", "keywords": "face hand" }, { "emoji": "🤫", "name": "shushing face", "keywords": "face hand" }, { "emoji": "🤔", "name": "thinking face", "keywords": "face hand" }, { "emoji": "🫡", "name": "saluting face", "keywords": "face hand" }, { "emoji": "🤐", "name": "zipper-mouth face", "keywords": "face neutral skeptical" }, { "emoji": "🤨", "name": "face with raised eyebrow", "keywords": "face neutral skeptical" }, { "emoji": "😐", "name": "neutral face", "keywords": "face neutral skeptical" }, { "emoji": "😑", "name": "expressionless face", "keywords": "face neutral skeptical" }, { "emoji": "😶", "name": "face without mouth", "keywords": "face neutral skeptical" }, { "emoji": "🫥", "name": "dotted line face", "keywords": "face neutral skeptical" }, { "emoji": "😶🌫️", "name": "face in clouds", "keywords": "face neutral skeptical" }, { "emoji": "😏", "name": "smirking face", "keywords": "face neutral skeptical" }, { "emoji": "😒", "name": "unamused face", "keywords": "face neutral skeptical" }, { "emoji": "🙄", "name": "face with rolling eyes", "keywords": "face neutral skeptical" }, { "emoji": "😬", "name": "grimacing face", "keywords": "face neutral skeptical" }, { "emoji": "😮💨", "name": "face exhaling", "keywords": "face neutral skeptical" }, { "emoji": "🤥", "name": "lying face", "keywords": "face neutral skeptical" }, { "emoji": "🫨", "name": "shaking face", "keywords": "face neutral skeptical" }, { "emoji": "🙂↔️", "name": "head shaking horizontally", "keywords": "face neutral skeptical" }, { "emoji": "🙂↕️", "name": "head shaking vertically", "keywords": "face neutral skeptical" }, { "emoji": "😌", "name": "relieved face", "keywords": "face sleepy" }, { "emoji": "😔", "name": "pensive face", "keywords": "face sleepy" }, { "emoji": "😪", "name": "sleepy face", "keywords": "face sleepy" }, { "emoji": "🤤", "name": "drooling face", "keywords": "face sleepy" }, { "emoji": "😴", "name": "sleeping face", "keywords": "face sleepy" }, { "emoji": "", "name": "face with bags under eyes", "keywords": "face sleepy" }, { "emoji": "😷", "name": "face with medical mask", "keywords": "face unwell" }, { "emoji": "Line truncated
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* See LICENSE file in repository root for details.
|
* See LICENSE file in repository root for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EMOJI_GROUPS } from "@rustpad/emoji-data";
|
import { EMOJI_GROUPS } from "@rustpad/emoji-groups";
|
||||||
import { t } from "@rustpad/i18n";
|
import { t } from "@rustpad/i18n";
|
||||||
|
|
||||||
const RECENTS_KEY = "rustpad:recent-emojis";
|
const RECENTS_KEY = "rustpad:recent-emojis";
|
||||||
@@ -153,4 +153,9 @@ export function bindEmojiPicker({ editor, details, search, categories, grid, emp
|
|||||||
document.addEventListener("rustpad:languagechange", () => {
|
document.addEventListener("rustpad:languagechange", () => {
|
||||||
if (details.open) render();
|
if (details.open) render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (details.open) {
|
||||||
|
render();
|
||||||
|
requestAnimationFrame(() => search.focus());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
||||||
|
* Source-Available Code / Dual-Licensed.
|
||||||
|
*
|
||||||
|
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
|
||||||
|
* Commercial or production use requires a valid paid license.
|
||||||
|
* See LICENSE file in repository root for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Generated from Unicode Emoji 17.0 emoji-test.txt.
|
||||||
|
// Native glyphs use the platform emoji font (Apple style on Apple devices).
|
||||||
|
export const EMOJI_SHORTCODES = { "+1": "👍", "-1": "👎", "100": "💯", "1st_place_medal": "🥇", "2nd_place_medal": "🥈", "3rd_place_medal": "🥉", "a_button_blood_type": "🅰️", "ab_button_blood_type": "🆎", "abacus": "🧮", "accordion": "🪗", "adhesive_bandage": "🩹", "admission_tickets": "🎟️", "aerial_tramway": "🚡", "airplane": "✈️", "airplane_arrival": "🛬", "airplane_departure": "🛫", "alarm_clock": "⏰", "alembic": "⚗️", "alien": "👽", "alien_monster": "👾", "ambulance": "🚑", "american_football": "🏈", "amphora": "🏺", "anatomical_heart": "🫀", "anchor": "⚓", "anger_symbol": "💢", "angry": "😠", "angry_face": "😠", "angry_face_with_horns": "👿", "anguished_face": "😧", "ant": "🐜", "antenna_bars": "📶", "anxious_face_with_sweat": "😰", "aquarius": "♒", "aries": "♈", "articulated_lorry": "🚛", "artist": "🧑🎨", "artist_dark_skin_tone": "🧑🏿🎨", "artist_light_skin_tone": "🧑🏻🎨", "artist_medium_dark_skin_tone": "🧑🏾🎨", "artist_medium_light_skin_tone": "🧑🏼🎨", "artist_medium_skin_tone": "🧑🏽🎨", "artist_palette": "🎨", "astonished_face": "😲", "astronaut": "🧑🚀", "astronaut_dark_skin_tone": "🧑🏿🚀", "astronaut_light_skin_tone": "🧑🏻🚀", "astronaut_medium_dark_skin_tone": "🧑🏾🚀", "astronaut_medium_light_skin_tone": "🧑🏼🚀", "astronaut_medium_skin_tone": "🧑🏽🚀", "atm_sign": "🏧", "atom_symbol": "⚛️", "auto_rickshaw": "🛺", "automobile": "🚗", "avocado": "🥑", "axe": "🪓", "b_button_blood_type": "🅱️", "baby": "👶", "baby_angel": "👼", "baby_angel_dark_skin_tone": "👼🏿", "baby_angel_light_skin_tone": "👼🏻", "baby_angel_medium_dark_skin_tone": "👼🏾", "baby_angel_medium_light_skin_tone": "👼🏼", "baby_angel_medium_skin_tone": "👼🏽", "baby_bottle": "🍼", "baby_chick": "🐤", "baby_dark_skin_tone": "👶🏿", "baby_light_skin_tone": "👶🏻", "baby_medium_dark_skin_tone": "👶🏾", "baby_medium_light_skin_tone": "👶🏼", "baby_medium_skin_tone": "👶🏽", "baby_symbol": "🚼", "back_arrow": "🔙", "backhand_index_pointing_down": "👇", "backhand_index_pointing_down_dark_skin_tone": "👇🏿", "backhand_index_pointing_down_light_skin_tone": "👇🏻", "backhand_index_pointing_down_medium_dark_skin_tone": "👇🏾", "backhand_index_pointing_down_medium_light_skin_tone": "👇🏼", "backhand_index_pointing_down_medium_skin_tone": "👇🏽", "backhand_index_pointing_left": "👈", "backhand_index_pointing_left_dark_skin_tone": "👈🏿", "backhand_index_pointing_left_light_skin_tone": "👈🏻", "backhand_index_pointing_left_medium_dark_skin_tone": "👈🏾", "backhand_index_pointing_left_medium_light_skin_tone": "👈🏼", "backhand_index_pointing_left_medium_skin_tone": "👈🏽", "backhand_index_pointing_right": "👉", "backhand_index_pointing_right_dark_skin_tone": "👉🏿", "backhand_index_pointing_right_light_skin_tone": "👉🏻", "backhand_index_pointing_right_medium_dark_skin_tone": "👉🏾", "backhand_index_pointing_right_medium_light_skin_tone": "👉🏼", "backhand_index_pointing_right_medium_skin_tone": "👉🏽", "backhand_index_pointing_up": "👆", "backhand_index_pointing_up_dark_skin_tone": "👆🏿", "backhand_index_pointing_up_light_skin_tone": "👆🏻", "backhand_index_pointing_up_medium_dark_skin_tone": "👆🏾", "backhand_index_pointing_up_medium_light_skin_tone": "👆🏼", "backhand_index_pointing_up_medium_skin_tone": "👆🏽", "backpack": "🎒", "bacon": "🥓", "badger": "🦡", "badminton": "🏸", "bagel": "🥯", "baggage_claim": "🛄", "baguette_bread": "🥖", "balance_scale": "⚖️", "ballet_dancer": "🧑🩰", "ballet_dancer_dark_skin_tone": "🧑🏿🩰", "ballet_dancer_light_skin_tone": "🧑🏻🩰", "ballet_dancer_medium_dark_skin_tone": "🧑🏾🩰", "ballet_dancer_medium_light_skin_tone": "🧑🏼🩰", "ballet_dancer_medium_skin_tone": "🧑🏽🩰", "ballet_shoes": "🩰", "balloon": "🎈", "ballot_box_with_ballot": "🗳️", "banana": "🍌", "banjo": "🪕", "bank": "🏦", "bar_chart": "📊", "barber_pole": "💈", "baseball": "⚾", "basket": "🧺", "basketball": "🏀", "bat": "🦇", "bathtub": "🛁", "battery": "🔋", "beach_with_umbrella": "🏖️", "beaming_face_with_smiling_eyes": "😁", "beans": "🫘", "bear": "🐻", "beating_heart": "💓", "beaver": "🦫", "bed": "🛏️", "beer_mug": "🍺", "beetle": "🪲", "bell": "🔔", "bell_pepper": "🫑", "bell_with_slash": "🔕", "bellhop_bell": "🛎️", "bento_box": "🍱", "beverage_box": "🧃", "bicycle": "🚲", "bikini": "👙", "billed_cap": "🧢", "biohazard": "☣️", "bird": "🐦", "birthday_cake": "🎂", "bison": "🦬", "biting_lip": "🫦", "black_bird": "🐦⬛", "black_cat": "🐈⬛", "black_circle": "⚫", "black_flag": "🏴", "black_heart": "🖤", "black_larLine truncated
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* See LICENSE file in repository root for details.
|
* See LICENSE file in repository root for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EMOJI_SHORTCODES } from "@rustpad/emoji-data";
|
import { EMOJI_SHORTCODES } from "@rustpad/emoji-shortcodes";
|
||||||
import { t } from "@rustpad/i18n";
|
import { t } from "@rustpad/i18n";
|
||||||
import { parseImageAlias } from "@rustpad/image-alias";
|
import { parseImageAlias } from "@rustpad/image-alias";
|
||||||
|
|
||||||
|
|||||||
+49
-14
@@ -17,10 +17,8 @@ import { CollaborationRevisionGapError, CollaborationSession } from "@rustpad/co
|
|||||||
import { copyText } from "@rustpad/clipboard";
|
import { copyText } from "@rustpad/clipboard";
|
||||||
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
|
import { lineFromHash, lineLink, lineStartOffset } from "@rustpad/line-links";
|
||||||
import { applyFormat, bindFormatShortcuts, bindIndentationShortcuts } from "@rustpad/editor-format";
|
import { applyFormat, bindFormatShortcuts, bindIndentationShortcuts } from "@rustpad/editor-format";
|
||||||
import { bindEmojiPicker } from "@rustpad/emoji-picker";
|
|
||||||
import { updateImageAliasInLineBySource } from "@rustpad/image-alias";
|
import { updateImageAliasInLineBySource } from "@rustpad/image-alias";
|
||||||
import { previewEditingHost } from "@rustpad/preview-edit";
|
import { previewEditingHost } from "@rustpad/preview-edit";
|
||||||
import { bindPdfExport } from "@rustpad/pdf-export";
|
|
||||||
import { createRenderQueue } from "@rustpad/render-queue";
|
import { createRenderQueue } from "@rustpad/render-queue";
|
||||||
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
|
import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles, unresolvedMarkdownFileAliases } from "@rustpad/markdown";
|
||||||
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
|
import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken } from "@rustpad/session";
|
||||||
@@ -270,12 +268,7 @@ export function startNoteEditor(adapter) {
|
|||||||
documentTitle.textContent = info.title;
|
documentTitle.textContent = info.title;
|
||||||
}
|
}
|
||||||
globalColor = info.global_color || ""; noteColor = info.note_color || "";
|
globalColor = info.global_color || ""; noteColor = info.note_color || "";
|
||||||
if (getAuthToken()) {
|
if (!getAuthToken()) noteColor = readGuestColor();
|
||||||
const colors = await adapter.loadColor(accountHeaders());
|
|
||||||
globalColor = colors.global_color || ""; noteColor = colors.note_color || "";
|
|
||||||
} else {
|
|
||||||
noteColor = readGuestColor();
|
|
||||||
}
|
|
||||||
updateMarkdownFiles(info.files || []);
|
updateMarkdownFiles(info.files || []);
|
||||||
if (info.personal_editor_settings) {
|
if (info.personal_editor_settings) {
|
||||||
compactToggle.checked = info.compact_view !== false;
|
compactToggle.checked = info.compact_view !== false;
|
||||||
@@ -1633,7 +1626,7 @@ export function startNoteEditor(adapter) {
|
|||||||
});
|
});
|
||||||
socket.connect();
|
socket.connect();
|
||||||
}
|
}
|
||||||
bindIdentityDialog({ dialog: identityDialog, onIdentity: async (value, session) => { nickname = value; accountSession = session || await validateCurrentSession(); accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); await syncFavoriteButton(); if (info.protected && info.access_level === "none") passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
bindIdentityDialog({ dialog: identityDialog, onIdentity: async (value, session) => { nickname = value; accountSession = session || await validateCurrentSession(); accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key); identityDialog.close(); updateCurrentUser(); await loadNoteInfo(); if (info.protected && info.access_level === "none") passwordDialog.showModal(); else { loadFiles(); connect(); } } });
|
||||||
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
|
identityDialog.addEventListener("close", () => { if (!nickname) queueMicrotask(() => { if (!identityDialog.open) identityDialog.showModal(); }); });
|
||||||
async function showSystemNotFound() {
|
async function showSystemNotFound() {
|
||||||
try {
|
try {
|
||||||
@@ -1667,7 +1660,6 @@ export function startNoteEditor(adapter) {
|
|||||||
adapter.configureView?.(info);
|
adapter.configureView?.(info);
|
||||||
applyUi({ write: true, replace: true });
|
applyUi({ write: true, replace: true });
|
||||||
updateCurrentUser();
|
updateCurrentUser();
|
||||||
await syncFavoriteButton();
|
|
||||||
if (info.protected && info.access_level === "none") passwordDialog.showModal();
|
if (info.protected && info.access_level === "none") passwordDialog.showModal();
|
||||||
else { loadFiles(); connect(); }
|
else { loadFiles(); connect(); }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -2074,14 +2066,57 @@ export function startNoteEditor(adapter) {
|
|||||||
});
|
});
|
||||||
bindFormatShortcuts(editor);
|
bindFormatShortcuts(editor);
|
||||||
bindIndentationShortcuts(editor, { size: 2 });
|
bindIndentationShortcuts(editor, { size: 2 });
|
||||||
bindPdfExport({
|
const pdfExportArgs = {
|
||||||
button: document.querySelector("#export-pdf"),
|
button: document.querySelector("#export-pdf"),
|
||||||
dialog: document.querySelector("#pdf-export-dialog"),
|
dialog: document.querySelector("#pdf-export-dialog"),
|
||||||
form: document.querySelector("#pdf-export-form"),
|
form: document.querySelector("#pdf-export-form"),
|
||||||
preview,
|
preview,
|
||||||
title: () => document.querySelector("#document-title")?.textContent?.trim() || document.title,
|
title: () => document.querySelector("#document-title")?.textContent?.trim() || document.title,
|
||||||
|
};
|
||||||
|
let pdfExportBound = false;
|
||||||
|
let pdfExportLoad = null;
|
||||||
|
pdfExportArgs.button?.addEventListener("click", async event => {
|
||||||
|
if (pdfExportBound) return;
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
pdfExportLoad ||= import("@rustpad/pdf-export").then(({ bindPdfExport }) => {
|
||||||
|
bindPdfExport(pdfExportArgs);
|
||||||
|
pdfExportBound = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await pdfExportLoad;
|
||||||
|
if (!pdfExportArgs.dialog.open) pdfExportArgs.dialog.showModal();
|
||||||
|
} catch (error) {
|
||||||
|
pdfExportLoad = null;
|
||||||
|
console.error("Failed to load PDF export", error);
|
||||||
|
toast.danger(t("editor.pdfExportFailed", {}, "Could not prepare the PDF."), {
|
||||||
|
title: t("editor.exportPdf", {}, "Export PDF"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojiPickerArgs = {
|
||||||
|
editor,
|
||||||
|
details: document.querySelector("#emoji-picker"),
|
||||||
|
search: document.querySelector("#emoji-search"),
|
||||||
|
categories: document.querySelector("#emoji-categories"),
|
||||||
|
grid: document.querySelector("#emoji-grid"),
|
||||||
|
empty: document.querySelector("#emoji-empty"),
|
||||||
|
};
|
||||||
|
let emojiPickerLoad = null;
|
||||||
|
const loadEmojiPicker = async () => {
|
||||||
|
if (emojiPickerLoad) return emojiPickerLoad;
|
||||||
|
emojiPickerLoad = import("@rustpad/emoji-picker").then(({ bindEmojiPicker }) => {
|
||||||
|
bindEmojiPicker(emojiPickerArgs);
|
||||||
|
}).catch(error => {
|
||||||
|
emojiPickerLoad = null;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
return emojiPickerLoad;
|
||||||
|
};
|
||||||
|
emojiPickerArgs.details?.addEventListener("toggle", () => {
|
||||||
|
if (!emojiPickerArgs.details.open) return;
|
||||||
|
void loadEmojiPicker().catch(error => console.error("Failed to load emoji picker", error));
|
||||||
});
|
});
|
||||||
bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") });
|
|
||||||
document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal());
|
document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal());
|
||||||
document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close());
|
document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close());
|
||||||
|
|
||||||
@@ -2259,7 +2294,7 @@ export function startNoteEditor(adapter) {
|
|||||||
password = "";
|
password = "";
|
||||||
setPagePasswordInput.value = "";
|
setPagePasswordInput.value = "";
|
||||||
await loadNoteInfo();
|
await loadNoteInfo();
|
||||||
await syncFavoriteButton();
|
void syncFavoriteButton();
|
||||||
resourceUnlocked = false;
|
resourceUnlocked = false;
|
||||||
socket?.stop();
|
socket?.stop();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
@@ -2357,7 +2392,7 @@ export function startNoteEditor(adapter) {
|
|||||||
document.querySelector("#open-password")?.focus();
|
document.querySelector("#open-password")?.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await adapter.requestAccess(password); setAccessToken(adapter.access.kind, adapter.access.key, result.granted); accessToken = getAccessToken(adapter.access.kind, adapter.access.key); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; await loadNoteInfo(); await syncFavoriteButton(); loadFiles(); connect(); toast.success("Editing access has been unlocked.", { title: "Note unlocked" }); } catch (error) { document.querySelector("#password-error").textContent = error.message; toast.danger(error.message, { title: "Could not unlock note" }); } });
|
document.querySelector("#password-form").addEventListener("submit", async e => { e.preventDefault(); try { password = document.querySelector("#open-password").value; const result = await adapter.requestAccess(password); setAccessToken(adapter.access.kind, adapter.access.key, result.granted); accessToken = getAccessToken(adapter.access.kind, adapter.access.key); password = ""; document.querySelector("#open-password").value = ""; document.querySelector("#password-error").textContent = ""; await loadNoteInfo(); void syncFavoriteButton(); loadFiles(); connect(); toast.success("Editing access has been unlocked.", { title: "Note unlocked" }); } catch (error) { document.querySelector("#password-error").textContent = error.message; toast.danger(error.message, { title: "Could not unlock note" }); } });
|
||||||
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { if (info?.protected && !resourceUnlocked) { if (!passwordDialog.open) passwordDialog.showModal(); document.querySelector("#open-password")?.focus(); return; } historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Loading…</p>'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`; }).join("") : '<p class="empty">No history yet.</p>'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast.success("The selected revision is now the current version.", { title: "Version restored" }); }); } } catch (e) { list.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`; toast.danger(e.message, { title: "Could not load version history" }); } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
|
const historyPanel = document.querySelector("#history-panel"); document.querySelector("#history-button").addEventListener("click", async () => { if (info?.protected && !resourceUnlocked) { if (!passwordDialog.open) passwordDialog.showModal(); document.querySelector("#open-password")?.focus(); return; } historyPanel.classList.add("open"); historyPanel.setAttribute("aria-hidden", "false"); document.body.classList.add("history-open"); const list = document.querySelector("#history-list"); list.innerHTML = '<p class="empty">Loading…</p>'; try { const revisions = await adapter.loadHistory(accessToken); list.innerHTML = revisions.length ? revisions.map((r, i) => { const snippet = escapeHtml(r.content.trim().split("\n").slice(0, 3).join(" · ").slice(0, 150) || "Empty note"); const author = r.author || "Unknown author"; return `<article class="revision"><span class="revision__marker" style="--owner:${colorFor(author)}"></span><div><div class="revision__meta"><strong>${escapeHtml(author)}</strong><time>${formatDate(r.created_at)}</time></div><p class="revision__snippet">${snippet}</p><button data-preview="${r.id}">Preview</button><button data-revision="${r.id}">Restore</button><div class="revision__preview" id="preview-${r.id}" hidden></div></div></article>`; }).join("") : '<p class="empty">No history yet.</p>'; for (const r of revisions) { list.querySelector(`[data-preview="${r.id}"]`)?.addEventListener("click", () => { const el = list.querySelector(`#preview-${r.id}`); el.hidden = !el.hidden; el.textContent = r.content; }); list.querySelector(`[data-revision="${r.id}"]`)?.addEventListener("click", async () => { await adapter.restoreRevision(r.id, accessToken); toast.success("The selected revision is now the current version.", { title: "Version restored" }); }); } } catch (e) { list.innerHTML = `<p class="error">${escapeHtml(e.message)}</p>`; toast.danger(e.message, { title: "Could not load version history" }); } }); document.querySelector("#close-history").addEventListener("click", () => { historyPanel.classList.remove("open"); historyPanel.setAttribute("aria-hidden", "true"); document.body.classList.remove("history-open"); });
|
||||||
|
|
||||||
const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast.danger(error.message, { title: "Could not delete note" }); } });
|
const deleteNoteButton = document.querySelector("#delete-note"); if (deleteNoteButton && adapter.deleteNote) deleteNoteButton.addEventListener("click", async () => { try { await adapter.deleteNote(info, accessToken); } catch (error) { toast.danger(error.message, { title: "Could not delete note" }); } });
|
||||||
|
|||||||
Reference in new issue
Block a user