diff --git a/Cargo.lock b/Cargo.lock index eb99dc7..5a83650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2581,7 +2581,7 @@ dependencies = [ [[package]] name = "rustpad" -version = "0.2.48" +version = "0.2.49" dependencies = [ "argon2", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index 4e3c8f7..15bc8d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.2.48" +version = "0.2.49" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/migrations/mysql/0032_navbar_collapsed.sql b/migrations/mysql/0032_navbar_collapsed.sql new file mode 100644 index 0000000..a6b795d --- /dev/null +++ b/migrations/mysql/0032_navbar_collapsed.sql @@ -0,0 +1 @@ +ALTER TABLE user_editor_preferences ADD COLUMN navbar_collapsed BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/postgres/0032_navbar_collapsed.sql b/migrations/postgres/0032_navbar_collapsed.sql new file mode 100644 index 0000000..a6b795d --- /dev/null +++ b/migrations/postgres/0032_navbar_collapsed.sql @@ -0,0 +1 @@ +ALTER TABLE user_editor_preferences ADD COLUMN navbar_collapsed BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/sqlite/0032_navbar_collapsed.sql b/migrations/sqlite/0032_navbar_collapsed.sql new file mode 100644 index 0000000..b2c45c7 --- /dev/null +++ b/migrations/sqlite/0032_navbar_collapsed.sql @@ -0,0 +1 @@ +ALTER TABLE user_editor_preferences ADD COLUMN navbar_collapsed INTEGER NOT NULL DEFAULT 0; diff --git a/src/api/mod.rs b/src/api/mod.rs index e2729cf..a73bb38 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -454,6 +454,7 @@ pub struct NoteInfo { preview_line_numbers: bool, line_links: bool, toolbar_collapsed: bool, + navbar_collapsed: bool, font_family: String, font_size: i64, personal_editor_settings: bool, @@ -485,6 +486,8 @@ pub struct EditorSettingsRequest { #[serde(default)] toolbar_collapsed: Option, #[serde(default)] + navbar_collapsed: Option, + #[serde(default)] font_family: Option, #[serde(default)] font_size: Option, @@ -527,6 +530,7 @@ async fn save_editor_settings( || payload.preview_line_numbers.is_some() || payload.line_links.is_some() || payload.toolbar_collapsed.is_some() + || payload.navbar_collapsed.is_some() || payload.font_family.is_some() || payload.font_size.is_some(); let wants_global_update = payload.authorship_mode.is_some() || payload.colors_enabled.is_some(); @@ -581,6 +585,9 @@ async fn save_editor_settings( if let Some(value) = payload.toolbar_collapsed { preferences.toolbar_collapsed = value; } + if let Some(value) = payload.navbar_collapsed { + preferences.navbar_collapsed = value; + } if let Some(value) = payload.font_family { preferences.font_family = match value.as_str() { "mono" | "system" | "serif" | "arial" | "georgia" => value, @@ -1148,6 +1155,7 @@ pub async fn note_info( preview_line_numbers: editor_preferences.preview_line_numbers, line_links: editor_preferences.line_links, toolbar_collapsed: editor_preferences.toolbar_collapsed, + navbar_collapsed: editor_preferences.navbar_collapsed, font_family: editor_preferences.font_family, font_size: editor_preferences.font_size, personal_editor_settings, diff --git a/src/api/pads_public.rs b/src/api/pads_public.rs index b3fd577..5c5c610 100644 --- a/src/api/pads_public.rs +++ b/src/api/pads_public.rs @@ -55,6 +55,7 @@ pub struct PadInfo { preview_line_numbers: bool, line_links: bool, toolbar_collapsed: bool, + navbar_collapsed: bool, font_family: String, font_size: i64, personal_editor_settings: bool, @@ -174,6 +175,7 @@ pub async fn pad_info( preview_line_numbers: editor_preferences.preview_line_numbers, line_links: editor_preferences.line_links, toolbar_collapsed: editor_preferences.toolbar_collapsed, + navbar_collapsed: editor_preferences.navbar_collapsed, font_family: editor_preferences.font_family, font_size: editor_preferences.font_size, personal_editor_settings, diff --git a/src/db/editor_preferences.rs b/src/db/editor_preferences.rs index 28ba83c..e187810 100644 --- a/src/db/editor_preferences.rs +++ b/src/db/editor_preferences.rs @@ -16,6 +16,7 @@ pub struct EditorPreferences { pub preview_line_numbers: bool, pub line_links: bool, pub toolbar_collapsed: bool, + pub navbar_collapsed: bool, pub font_family: String, pub font_size: i64, } @@ -28,6 +29,7 @@ impl Default for EditorPreferences { preview_line_numbers: false, line_links: false, toolbar_collapsed: false, + navbar_collapsed: false, font_family: "mono".into(), font_size: 14, } @@ -95,8 +97,9 @@ pub async fn load_editor_preferences( preview_line_numbers: row.try_get::(2)? != 0, line_links: row.try_get::(3)? != 0, toolbar_collapsed: row.try_get::(4)? != 0, - font_family: crate::row_decode::text(&row, 5)?, - font_size: row.try_get(6)?, + navbar_collapsed: row.try_get::(5)? != 0, + font_family: crate::row_decode::text(&row, 6)?, + font_size: row.try_get(7)?, })) } @@ -121,6 +124,7 @@ pub async fn save_editor_configuration( .bind(preferences.preview_line_numbers) .bind(preferences.line_links) .bind(preferences.toolbar_collapsed) + .bind(preferences.navbar_collapsed) .bind(&preferences.font_family) .bind(preferences.font_size) .execute(&mut *tx) diff --git a/src/queries/mysql.rs b/src/queries/mysql.rs index fc800b2..fe58007 100644 --- a/src/queries/mysql.rs +++ b/src/queries/mysql.rs @@ -59,16 +59,16 @@ pub fn get(query: Query) -> &'static str { r#"DELETE FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"# } Query::EDITOR_PREFERENCES_SELECT_PAD => { - r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"# + r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN navbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"# } Query::EDITOR_PREFERENCES_SELECT_NOTE => { - r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"# + r#"SELECT CAST(CASE WHEN compact_view THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN editor_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN preview_line_numbers THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN line_links THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(CASE WHEN navbar_collapsed THEN 1 ELSE 0 END AS SIGNED), CAST(font_family AS CHAR CHARACTER SET utf8mb4), font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"# } Query::EDITOR_PREFERENCES_UPSERT_PAD => { - r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), toolbar_collapsed = VALUES(toolbar_collapsed), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"# + r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, navbar_collapsed, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), toolbar_collapsed = VALUES(toolbar_collapsed), navbar_collapsed = VALUES(navbar_collapsed), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"# } Query::EDITOR_PREFERENCES_UPSERT_NOTE => { - r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), toolbar_collapsed = VALUES(toolbar_collapsed), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"# + r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, navbar_collapsed, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON DUPLICATE KEY UPDATE compact_view = VALUES(compact_view), editor_line_numbers = VALUES(editor_line_numbers), preview_line_numbers = VALUES(preview_line_numbers), line_links = VALUES(line_links), toolbar_collapsed = VALUES(toolbar_collapsed), navbar_collapsed = VALUES(navbar_collapsed), font_family = VALUES(font_family), font_size = VALUES(font_size), updated_at = CURRENT_TIMESTAMP"# } Query::EDITOR_PREFERENCES_DELETE_BY_USER => { r#"DELETE FROM user_editor_preferences WHERE user_id = ?"# diff --git a/src/queries/postgres.rs b/src/queries/postgres.rs index 618c2e3..9a92e43 100644 --- a/src/queries/postgres.rs +++ b/src/queries/postgres.rs @@ -59,16 +59,16 @@ pub fn get(query: Query) -> &'static str { r#"DELETE FROM resource_editor_settings WHERE resource_kind = $1 AND resource_slug = $2"# } Query::EDITOR_PREFERENCES_SELECT_PAD => { - r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND pad_id = $2"# + r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN navbar_collapsed THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND pad_id = $2"# } Query::EDITOR_PREFERENCES_SELECT_NOTE => { - r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND note_id = $2"# + r#"SELECT (CASE WHEN compact_view THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN editor_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN preview_line_numbers THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN line_links THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END)::BIGINT, (CASE WHEN navbar_collapsed THEN 1 ELSE 0 END)::BIGINT, font_family, font_size FROM user_editor_preferences WHERE user_id = $1 AND note_id = $2"# } Query::EDITOR_PREFERENCES_UPSERT_PAD => { - r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES ($1, $2, NULL, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, toolbar_collapsed = EXCLUDED.toolbar_collapsed, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"# + r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, navbar_collapsed, font_family, font_size, updated_at) VALUES ($1, $2, NULL, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, toolbar_collapsed = EXCLUDED.toolbar_collapsed, navbar_collapsed = EXCLUDED.navbar_collapsed, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"# } Query::EDITOR_PREFERENCES_UPSERT_NOTE => { - r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES ($1, NULL, $2, $3, $4, $5, $6, $7, $8, $9, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, toolbar_collapsed = EXCLUDED.toolbar_collapsed, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"# + r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, navbar_collapsed, font_family, font_size, updated_at) VALUES ($1, NULL, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP::text) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = EXCLUDED.compact_view, editor_line_numbers = EXCLUDED.editor_line_numbers, preview_line_numbers = EXCLUDED.preview_line_numbers, line_links = EXCLUDED.line_links, toolbar_collapsed = EXCLUDED.toolbar_collapsed, navbar_collapsed = EXCLUDED.navbar_collapsed, font_family = EXCLUDED.font_family, font_size = EXCLUDED.font_size, updated_at = CURRENT_TIMESTAMP::text"# } Query::EDITOR_PREFERENCES_DELETE_BY_USER => { r#"DELETE FROM user_editor_preferences WHERE user_id = $1"# diff --git a/src/queries/sqlite.rs b/src/queries/sqlite.rs index 131c89c..fd59740 100644 --- a/src/queries/sqlite.rs +++ b/src/queries/sqlite.rs @@ -59,16 +59,16 @@ pub fn get(query: Query) -> &'static str { r#"DELETE FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"# } Query::EDITOR_PREFERENCES_SELECT_PAD => { - r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"# + r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END, CASE WHEN navbar_collapsed THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND pad_id = ?"# } Query::EDITOR_PREFERENCES_SELECT_NOTE => { - r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"# + r#"SELECT CASE WHEN compact_view THEN 1 ELSE 0 END, CASE WHEN editor_line_numbers THEN 1 ELSE 0 END, CASE WHEN preview_line_numbers THEN 1 ELSE 0 END, CASE WHEN line_links THEN 1 ELSE 0 END, CASE WHEN toolbar_collapsed THEN 1 ELSE 0 END, CASE WHEN navbar_collapsed THEN 1 ELSE 0 END, font_family, font_size FROM user_editor_preferences WHERE user_id = ? AND note_id = ?"# } Query::EDITOR_PREFERENCES_UPSERT_PAD => { - r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, toolbar_collapsed = excluded.toolbar_collapsed, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"# + r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, navbar_collapsed, font_family, font_size, updated_at) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, pad_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, toolbar_collapsed = excluded.toolbar_collapsed, navbar_collapsed = excluded.navbar_collapsed, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"# } Query::EDITOR_PREFERENCES_UPSERT_NOTE => { - r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, toolbar_collapsed = excluded.toolbar_collapsed, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"# + r#"INSERT INTO user_editor_preferences (user_id, pad_id, note_id, compact_view, editor_line_numbers, preview_line_numbers, line_links, toolbar_collapsed, navbar_collapsed, font_family, font_size, updated_at) VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT (user_id, note_id) DO UPDATE SET compact_view = excluded.compact_view, editor_line_numbers = excluded.editor_line_numbers, preview_line_numbers = excluded.preview_line_numbers, line_links = excluded.line_links, toolbar_collapsed = excluded.toolbar_collapsed, navbar_collapsed = excluded.navbar_collapsed, font_family = excluded.font_family, font_size = excluded.font_size, updated_at = CURRENT_TIMESTAMP"# } Query::EDITOR_PREFERENCES_DELETE_BY_USER => { r#"DELETE FROM user_editor_preferences WHERE user_id = ?"# diff --git a/static/css/styles.css b/static/css/styles.css index 3479ad3..e92a8c5 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -7344,3 +7344,158 @@ dialog::backdrop { grid-row: 1; } } + +/* Published-note permalinks ------------------------------------------------ */ +.public-content .preview-source-line { + scroll-margin-top: 88px; +} + +.public-content .public-permalink { + position: absolute; + top: .08em; + left: -1.55rem; + z-index: 3; + display: inline-grid; + width: 1.25rem; + height: 1.25rem; + place-items: center; + border-radius: 5px; + color: var(--muted-2); + font: 700 .86rem/1 ui-monospace, SFMono-Regular, Consolas, monospace; + text-decoration: none; + user-select: none; + opacity: 0; + transform: translateX(3px); + transition: opacity .14s ease, transform .14s ease, background-color .14s ease, color .14s ease; +} + +.public-content .list-source-line > .public-permalink { + left: -2.35rem; +} + +.public-content .preview-source-line:hover > .public-permalink, +.public-content .preview-source-line:focus-within > .public-permalink, +.public-content .public-permalink:focus-visible { + opacity: 1; + transform: translateX(0); +} + +.public-content .public-permalink:hover, +.public-content .public-permalink:focus-visible { + background: color-mix(in srgb, var(--accent) 16%, transparent); + color: var(--text); + outline: none; +} + +.public-content .public-permalink.is-copied { + background: color-mix(in srgb, var(--success) 18%, transparent); + color: var(--text); + opacity: 1; +} + +.public-content .is-public-link-target { + border-radius: 5px; + background: color-mix(in srgb, var(--accent) 11%, transparent); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent); +} + +@media (hover: none), (pointer: coarse) { + .public-content .public-permalink { + opacity: .62; + transform: none; + } +} + +/* Per-user, per-note navigation-bar focus mode ---------------------------- */ +.pad-page .navbar-collapse-toggle { + position: fixed; + top: max(3px, env(safe-area-inset-top)); + right: 0; + z-index: 40; + display: grid; + width: 24px; + height: 28px; + padding: 0; + place-items: center; + border: 1px solid color-mix(in srgb, var(--border-strong) 76%, transparent); + border-top: 0; + border-right: 0; + border-radius: 0 0 0 9px; + background: color-mix(in srgb, var(--surface-toolbar) 88%, transparent); + color: var(--muted); + box-shadow: 0 6px 18px var(--shadow-28); + backdrop-filter: blur(10px); + cursor: pointer; + opacity: 0; + transform: translateY(-5px); + transition: width .14s ease, opacity .14s ease, transform .14s ease, color .14s ease, background-color .14s ease; +} + +.pad-page .navbar-collapse-toggle > span { + position: relative; + display: block; + width: 14px; + height: 9px; + transition: transform .18s ease; +} + +.pad-page .navbar-collapse-toggle > span::before, +.pad-page .navbar-collapse-toggle > span::after { + content: ""; + position: absolute; + top: 3px; + width: 8px; + height: 2px; + border-radius: 2px; + background: currentColor; +} + +.pad-page .navbar-collapse-toggle > span::before { + left: 0; + transform: rotate(-36deg); + transform-origin: right center; +} + +.pad-page .navbar-collapse-toggle > span::after { + right: 0; + transform: rotate(36deg); + transform-origin: left center; +} + +.pad-page .app-header:hover + .navbar-collapse-toggle, +.pad-page .navbar-collapse-toggle:hover, +.pad-page .navbar-collapse-toggle:focus-visible, +.pad-page.navbar-collapsed .navbar-collapse-toggle:hover, +.pad-page.navbar-collapsed .navbar-collapse-toggle:focus-visible { + width: 32px; + color: var(--text); + opacity: 1; + transform: translateY(0); + outline: none; +} + +.pad-page.navbar-collapsed .navbar-collapse-toggle > span { + transform: rotate(180deg); +} + +.pad-page.navbar-collapsed { + grid-template-rows: minmax(0, 1fr); +} + +.pad-page.navbar-collapsed .app-header { + display: none !important; +} + +.pad-page.navbar-collapsed .editor-layout { + height: 100%; + flex: 1 1 0; +} + +@media (hover: none), (pointer: coarse) { + .pad-page .navbar-collapse-toggle { + width: 32px; + color: var(--text); + opacity: .78; + transform: none; + } +} diff --git a/static/editor.html b/static/editor.html index 3884e74..e3021c9 100644 --- a/static/editor.html +++ b/static/editor.html @@ -74,6 +74,8 @@ +
diff --git a/static/js/note-editor.js b/static/js/note-editor.js index ed6fa11..b206bbc 100644 --- a/static/js/note-editor.js +++ b/static/js/note-editor.js @@ -32,7 +32,7 @@ import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-lib export function startNoteEditor(adapter) { const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer"); - const modeToggle = document.querySelector("#mode-toggle"), toolbarCollapseToggle = document.querySelector("#toolbar-collapse-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog"); + const modeToggle = document.querySelector("#mode-toggle"), toolbarCollapseToggle = document.querySelector("#toolbar-collapse-toggle"), navbarCollapseToggle = document.querySelector("#navbar-collapse-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog"); const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), mobileConnectionDetails = document.querySelector("#mobile-connection-details"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread"), connectionNotice = document.querySelector("#connection-notice"), connectionNoticeTitle = document.querySelector("#connection-notice-title"), connectionNoticeMessage = document.querySelector("#connection-notice-message"); const saveState = document.querySelector("#save-state"); editor.readOnly = true; @@ -42,6 +42,7 @@ export function startNoteEditor(adapter) { const shareToken = new URLSearchParams(location.search).get("share"); const notePreferenceKey = name => `rustpad:${name}:${location.pathname}`; let toolbarCollapsed = localStorage.getItem(notePreferenceKey("toolbar-collapsed")) === "on"; + let navbarCollapsed = localStorage.getItem(notePreferenceKey("navbar-collapsed")) === "on"; const collaborationClientId = typeof crypto.randomUUID === "function" ? crypto.randomUUID().replaceAll("-", "") : `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`; @@ -234,6 +235,7 @@ export function startNoteEditor(adapter) { previewLineToggle.checked = info.preview_line_numbers === true; lineLinksToggle.checked = info.line_links === true; toolbarCollapsed = info.toolbar_collapsed === true; + navbarCollapsed = info.navbar_collapsed === true; if (["mono", "system", "serif", "arial", "georgia"].includes(info.font_family)) fontFamily.value = info.font_family; if (["14", "16", "18", "20", "22"].includes(String(info.font_size))) fontSize.value = String(info.font_size); } @@ -1172,6 +1174,15 @@ export function startNoteEditor(adapter) { toolbarCollapseToggle.querySelector(".toolbar-collapse-toggle__icon").textContent = toolbarCollapsed ? "⌄" : "⌃"; } + function applyNavbarCollapsed() { + document.body.classList.toggle("navbar-collapsed", navbarCollapsed); + if (!navbarCollapseToggle) return; + const label = navbarCollapsed ? "Show navigation bar" : "Hide navigation bar"; + navbarCollapseToggle.setAttribute("aria-pressed", String(navbarCollapsed)); + navbarCollapseToggle.setAttribute("aria-label", label); + navbarCollapseToggle.title = label; + } + function applyUi({ write = false, replace = false } = {}) { const view = activeView(); renderedView = view; @@ -1180,6 +1191,7 @@ export function startNoteEditor(adapter) { document.body.classList.toggle("compact-editor", compactToggle.checked); document.body.classList.toggle("compact-note-layout", compactLayoutQuery.matches); applyToolbarCollapsed(); + applyNavbarCollapsed(); document.querySelectorAll("[data-view]").forEach(button => { const active = button.dataset.view === view; button.classList.toggle("active", active); @@ -1602,6 +1614,7 @@ export function startNoteEditor(adapter) { }); modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUiPreservingScroll({ write: true }); }); toolbarCollapseToggle?.addEventListener("click", () => { toolbarCollapsed = !toolbarCollapsed; localStorage.setItem(notePreferenceKey("toolbar-collapsed"), toolbarCollapsed ? "on" : "off"); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); }); + navbarCollapseToggle?.addEventListener("click", () => { navbarCollapsed = !navbarCollapsed; localStorage.setItem(notePreferenceKey("navbar-collapsed"), navbarCollapsed ? "on" : "off"); if (navbarCollapsed) setHeaderMenuOpen(false); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); }); lineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("line-numbers"), lineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); scheduleEditorSettingsSave({ personal: true }); }); previewLineToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("preview-line-numbers"), previewLineToggle.checked ? "on" : "off"); syncMobileEditorControls(); renderGutter(); alignPreviewLineNumbers(preview); scheduleEditorSettingsSave({ personal: true }); }); compactToggle.addEventListener("change", () => { localStorage.setItem(notePreferenceKey("compact"), compactToggle.checked ? "on" : "off"); syncMobileEditorControls(); applyUiPreservingScroll(); scheduleEditorSettingsSave({ personal: true }); }); @@ -1800,6 +1813,7 @@ export function startNoteEditor(adapter) { preview_line_numbers: previewLineToggle.checked, line_links: lineLinksToggle.checked, toolbar_collapsed: toolbarCollapsed, + navbar_collapsed: navbarCollapsed, font_family: fontFamily.value, font_size: Number(fontSize.value), }; diff --git a/static/js/public.js b/static/js/public.js index 5c235a1..d678656 100644 --- a/static/js/public.js +++ b/static/js/public.js @@ -12,6 +12,7 @@ installGlobalDiagnostics(); import { api } from "@rustpad/api"; import { copyText } from "@rustpad/clipboard"; +import { lineFromHash, lineLink } from "@rustpad/line-links"; import { alignPreviewLineNumbers, renderMarkdown, setMarkdownFiles } from "@rustpad/markdown"; import { toast } from "@rustpad/toast"; import { getTheme } from "@rustpad/theme"; @@ -52,17 +53,45 @@ function lockPublicContent(allowTaskUpdates) { content.querySelectorAll('.preview-editable').forEach(node => node.classList.remove('preview-editable')); content.querySelectorAll('.task-checkbox').forEach(box => { box.disabled = !allowTaskUpdates; box.title = allowTaskUpdates ? 'Update this task' : 'Task updates are disabled by the owner'; }); } -function scrollToPublicAnchor(hash, behavior = "auto") { +function publicAnchorTarget(hash) { const rawId = String(hash || "").replace(/^#/, ""); - if (!rawId) return false; + if (!rawId) return null; let id; try { id = decodeURIComponent(rawId); } catch { id = rawId; } const target = document.getElementById(id); - if (!target || !content.contains(target)) return false; + if (target && content.contains(target)) return target; + const line = lineFromHash(hash); + return line ? content.querySelector(`.preview-source-line[data-source-line="${line}"]`) : null; +} +function markPublicAnchor(target) { + content.querySelectorAll(".is-public-link-target").forEach(node => node.classList.remove("is-public-link-target")); + target?.classList.add("is-public-link-target"); +} +function scrollToPublicAnchor(hash, behavior = "auto") { + const target = publicAnchorTarget(hash); + if (!target) return false; for (let details = target.closest("details"); details; details = details.parentElement?.closest("details")) details.open = true; + markPublicAnchor(target); target.scrollIntoView({ behavior, block: "start" }); return true; } +function installPublicPermalinks() { + content.querySelectorAll(".preview-source-line[data-source-line]").forEach(target => { + if (target.matches("hr") || target.querySelector(":scope > .public-permalink")) return; + const line = Number(target.dataset.sourceLine); + if (!Number.isSafeInteger(line) || line < 1) return; + const heading = target.matches("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]"); + const anchor = document.createElement("a"); + anchor.className = "public-permalink"; + anchor.href = heading ? `#${encodeURIComponent(target.id)}` : `#L${line}`; + anchor.dataset.line = String(line); + anchor.dataset.linkKind = heading ? "heading" : "line"; + anchor.textContent = heading ? "¶" : "#"; + anchor.setAttribute("aria-label", heading ? `Copy link to heading on line ${line}` : `Copy link to line ${line}`); + anchor.title = heading ? "Copy link to this heading" : `Copy link to line ${line}`; + target.append(anchor); + }); +} function setFullWidth(enabled, { persist = true } = {}) { const active = Boolean(enabled); document.body.classList.toggle("public-full-width", active); @@ -77,14 +106,22 @@ function restoreFullWidth() { try { enabled = localStorage.getItem(fullWidthStorageKey) === "1"; } catch { } setFullWidth(enabled, { persist: false }); } -async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); alignPreviewLineNumbers(content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } } -content.addEventListener("click", event => { +async function initialize() { try { const page = await api(`/api/public/${encodeURIComponent(token)}`, { headers: pageHeaders() }); if (passwordDialog.open) passwordDialog.close(); passwordError.textContent = ""; document.querySelector("#public-title").textContent = page.title; document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")}${page.allow_task_updates ? " · tasks can be updated" : ""}`; document.title = `${page.title} · RustPad`; setMarkdownFiles(page.files || []); content.innerHTML = renderMarkdown(page.content); lockPublicContent(page.allow_task_updates); await Promise.all([renderMermaid(), renderCodeHighlight(), renderMediaPlayers()]); installPublicPermalinks(); alignPreviewLineNumbers(content); requestAnimationFrame(() => scrollToPublicAnchor(location.hash)); } catch (error) { if (error.status === 401 || error.status === 403) { passwordError.textContent = error.status === 403 ? "Sign in with an authorized account or enter the resource password." : "Enter the correct password."; if (!passwordDialog.open) passwordDialog.showModal(); passwordInput.focus(); return; } content.replaceChildren(); const message = document.createElement("p"); message.className = "error"; message.textContent = String(error.message); content.append(message); } } +content.addEventListener("click", async event => { const link = event.target.closest('a[href^="#"]'); if (!link || !content.contains(link)) return; const hash = link.getAttribute("href"); event.preventDefault(); if (!hash || hash === "#" || !scrollToPublicAnchor(hash, "smooth")) return; history.replaceState(null, "", `${location.pathname}${location.search}${hash}`); + if (!link.classList.contains("public-permalink")) return; + try { + const href = link.dataset.linkKind === "heading" ? new URL(hash, location.href).href : lineLink(location.href, Number(link.dataset.line)); + await copyText(href); + link.classList.add("is-copied"); + setTimeout(() => link.classList.remove("is-copied"), 900); + toast(link.dataset.linkKind === "heading" ? "Heading link copied" : `Link to line ${link.dataset.line} copied`); + } catch (error) { toast(error.message); } }); window.addEventListener("hashchange", () => scrollToPublicAnchor(location.hash, "smooth")); content.addEventListener("change", async event => { const box = event.target.closest(".task-checkbox"); if (!box || box.disabled) return; const previous = !box.checked; box.disabled = true; try { const page = await api(`/api/public/${encodeURIComponent(token)}/tasks`, { method: "POST", headers: pageHeaders(), body: JSON.stringify({ source_line: Number(box.dataset.sourceLine), checked: box.checked }) }); document.querySelector("#public-meta").textContent = `Updated: ${new Date(page.updated_at).toLocaleString("en-US")} · tasks can be updated`; toast("Task saved"); } catch (error) { box.checked = previous; toast(error.message); } finally { box.disabled = false; } });