add: journal.css special note

This commit is contained in:
trisua 2025-06-19 19:13:07 -04:00
parent f0d1a1e8e4
commit dc50f3a8af
8 changed files with 125 additions and 13 deletions

View file

@ -1250,3 +1250,32 @@ details.accordion .inner {
.CodeMirror-focused .CodeMirror-placeholder {
opacity: 50%;
}
.CodeMirror-gutters {
border-color: var(--color-super-lowered) !important;
background-color: var(--color-lowered) !important;
}
.CodeMirror-hints {
background: var(--color-raised) !important;
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
var(--color-shadow);
border-radius: var(--radius) !important;
padding: var(--pad-1) !important;
border-color: var(--color-super-lowered) !important;
}
.CodeMirror-hints li {
color: var(--color-text-raised) !important;
border-radius: var(--radius) !important;
transition:
background 0.15s,
color 0.15s;
font-size: 10px;
padding: calc(var(--pad-1) / 2) var(--pad-2);
}
.CodeMirror-hints li.CodeMirror-hint-active {
background-color: var(--color-primary) !important;
color: var(--color-text-primary) !important;
}

View file

@ -9,6 +9,11 @@
; redirect to journal homepage
(meta ("http-equiv" "refresh") ("content" "0; url=/@{{ user.username }}/{{ journal.title }}"))
(text "{%- endif %} {%- endif %}")
(text "{% if view_mode and journal -%}")
; add journal css
(link ("rel" "stylesheet") ("data-turbo-temporary" "true") ("href" "/api/v1/journals/{{ journal.id }}/journal.css?v=tetratto-{{ random_cache_breaker }}"))
(text "{%- endif %}")
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"journals\") }}")
(text "{% if not view_mode -%}")
(nav
@ -73,7 +78,7 @@
(b (text "{{ note.title }}"))
(text "{%- endif %}"))
(text "{% if user and user.id == journal.owner -%}")
(text "{% if user and user.id == journal.owner and (not note or note.title != \"journal.css\") -%}")
(div
("class" "pillmenu")
(a
@ -181,10 +186,36 @@
; import codemirror
(script ("src" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/display/placeholder.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js") ("data-turbo-temporary" "true"))
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/lib/codemirror.css") ("data-turbo-temporary" "true"))
(text "{% if note.title == \"journal.css\" -%}")
; css editor
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/edit/closebrackets.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/hint/css-hint.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/hint/show-hint.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/addon/lint/css-lint.js") ("data-turbo-temporary" "true"))
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/css/css.js") ("data-turbo-temporary" "true"))
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/addon/hint/show-hint.css") ("data-turbo-temporary" "true"))
(link ("rel" "stylesheet") ("href" "https://unpkg.com/codemirror@5.39.2/addon/lint/lint.css") ("data-turbo-temporary" "true"))
(style
(text ".CodeMirror {
font-family: monospace !important;
font-size: 16px;
border: solid 1px var(--color-super-lowered);
border-radius: var(--radius);
}
.CodeMirror-line {
padding-left: 5px !important;
}"))
(text "{% else %}")
; markdown editor
(script ("src" "https://unpkg.com/codemirror@5.39.2/mode/markdown/markdown.js") ("data-turbo-temporary" "true"))
(text "{%- endif %}")
; tab bar
(text "{% if note.title != \"journal.css\" -%}")
(div
("class" "pillmenu")
(a
@ -199,6 +230,7 @@
("data-tab-button" "preview")
("data-turbo" "false")
(str (text "journals:label.preview_pane"))))
(text "{%- endif %}")
; tabs
(div
@ -222,10 +254,15 @@
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}"))
(script
(text "setTimeout(() => {
if (!document.getElementById(\"preview_tab\").shadowRoot) {
document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" });
}
globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), {
value: document.getElementById(\"editor_content\").innerHTML,
mode: \"markdown\",
mode: \"{% if note.title == 'journal.css' -%} css {%- else -%} markdown {%- endif %}\",
lineWrapping: true,
lineNumbers: \"{{ note.title }}\" === \"journal.css\",
autoCloseBrackets: true,
autofocus: true,
viewportMargin: Number.POSITIVE_INFINITY,
@ -233,7 +270,8 @@
highlightFormatting: false,
fencedCodeBlockHighlighting: false,
xml: false,
smartIndent: false,
smartIndent: true,
indentUnit: 4,
placeholder: `# {{ note.title }}`,
extraKeys: {
Home: \"goLineLeft\",
@ -244,6 +282,15 @@
},
});
editor.on(\"keydown\", (cm, e) => {
if (e.key.length > 1) {
// ignore all keys that aren't a letter
return;
}
CodeMirror.showHint(cm, CodeMirror.hint.css);
});
document.querySelector(\"[data-tab-button=editor]\").addEventListener(\"click\", async (e) => {
e.preventDefault();
trigger(\"atto::hooks::tabs:switch\", [\"editor\"]);
@ -263,7 +310,10 @@
})
).text();
document.getElementById(\"preview_tab\").innerHTML = res;
const preview_token = window.crypto.randomUUID();
document.getElementById(\"preview_tab\").shadowRoot.innerHTML = `${res}<style>
@import url(\"/api/v1/journals/{{ journal.id }}/journal.css?v=preview-${preview_token}\");
</style>`;
trigger(\"atto::hooks::tabs:switch\", [\"preview\"]);
});
}, 150);"))
@ -360,7 +410,7 @@
},
body: JSON.stringify({
title,
content: `# ${title}`,
content: title === \"journal.css\" ? `/* ${title} */\\n` : `# ${title}`,
journal: \"{{ selected_journal }}\",
}),
})

View file

@ -564,14 +564,14 @@
(li
(text "Use custom CSS on your profile"))
(li
(text "Ability to use community emojis outside of
(text "Use community emojis outside of
their community"))
(li
(text "Ability to upload and use gif emojis"))
(text "Upload and use gif emojis"))
(li
(text "Create infinite stack timelines"))
(li
(text "Ability to upload images to posts"))
(text "Upload images to posts"))
(li
(text "Save infinite post drafts"))
(li
@ -579,7 +579,7 @@
(li
(text "Ability to create forges"))
(li
(text "Ability to create more than 1 app"))
(text "Create more than 1 app"))
(li
(text "Create up to 10 stack blocks"))
(li
@ -587,7 +587,9 @@
(li
(text "Increased proxied image size"))
(li
(text "Create infinite journals")))
(text "Create infinite journals"))
(li
(text "Create infinite notes in each journal")))
(a
("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("class" "button")

View file

@ -49,6 +49,20 @@ pub async fn get_request(
})
}
pub async fn get_css_request(
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let note = match data.get_note_by_journal_title(id, "journal.css").await {
Ok(x) => x,
Err(e) => return ([("Content-Type", "text/plain")], format!("/* {e} */")),
};
([("Content-Type", "text/css")], note.content)
}
pub async fn list_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadJournals) {

View file

@ -551,6 +551,7 @@ pub fn routes() -> Router {
.route("/journals", post(journals::create_request))
.route("/journals/{id}", get(journals::get_request))
.route("/journals/{id}", delete(journals::delete_request))
.route("/journals/{id}/journal.css", get(journals::get_css_request))
.route("/journals/{id}/title", post(journals::update_title_request))
.route(
"/journals/{id}/privacy",

View file

@ -116,7 +116,7 @@ pub async fn view_request(
}
// if we don't have a selected journal, we shouldn't be here probably
if selected_journal.is_empty() {
if selected_journal.is_empty() | (selected_note == "journal.css") {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &user).await,
));

View file

@ -70,7 +70,7 @@ impl DataManager {
Ok(res.unwrap())
}
const MAXIMUM_FREE_JOURNALS: usize = 15;
const MAXIMUM_FREE_JOURNALS: usize = 5;
/// Create a new journal in the database.
///

View file

@ -65,6 +65,8 @@ impl DataManager {
Ok(res.unwrap())
}
const MAXIMUM_FREE_NOTES_PER_JOURNAL: usize = 10;
/// Create a new note in the database.
///
/// # Arguments
@ -85,6 +87,20 @@ impl DataManager {
data.title = data.title.replace(" ", "_").to_lowercase();
// check number of notes
let owner = self.get_user_by_id(data.owner).await?;
if !owner.permissions.check(FinePermission::SUPPORTER) {
let journals = self.get_notes_by_journal(data.owner).await?;
if journals.len() >= Self::MAXIMUM_FREE_NOTES_PER_JOURNAL {
return Err(Error::MiscError(
"You already have the maximum number of notes you can have in this journal"
.to_string(),
));
}
}
// check name
let regex = regex::RegexBuilder::new(NAME_REGEX)
.multi_line(true)