add(atomic): better files page
add(atomic): split view add(atomic): move paste deletion option add(atomic): javascript editor highlighting chore: bump version (v0.12.1 -> v0.12.2)
This commit is contained in:
parent
17066671b6
commit
d407706f3c
|
@ -3,7 +3,7 @@ name = "bundlrs"
|
|||
authors = ["hkau"]
|
||||
license = "MIT"
|
||||
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
edition = "2021"
|
||||
|
||||
rust-version = "1.75"
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"@codemirror/autocomplete": "^6.12.0",
|
||||
"@codemirror/commands": "^6.3.3",
|
||||
"@codemirror/lang-css": "^6.2.1",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-markdown": "^6.2.3",
|
||||
"@codemirror/language": "^6.10.0",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse, Responder};
|
||||
|
||||
use crate::db::{self, AtomicPasteFSFile, DefaultReturn, FullPaste, PasteMetadata};
|
||||
use crate::{markdown, ssm, utility};
|
||||
|
@ -30,13 +30,6 @@ struct EditInfo {
|
|||
new_custom_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EditAtomicInfo {
|
||||
custom_url: String,
|
||||
path: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct DeleteInfo {
|
||||
custom_url: String,
|
||||
|
@ -234,102 +227,6 @@ pub async fn edit_request(
|
|||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/edit-atomic")]
|
||||
/// Edit an atomic paste's "file system"
|
||||
pub async fn edit_atomic_request(
|
||||
req: HttpRequest,
|
||||
body: web::Json<EditAtomicInfo>,
|
||||
data: web::Data<db::AppData>,
|
||||
) -> impl Responder {
|
||||
// this is essentially the same as edit_request but it handles the atomic JSON file system
|
||||
// ...it does NOT accept an edit password! users must be authenticated
|
||||
let custom_url: String = body.custom_url.trim().to_string();
|
||||
let path: String = body.path.trim().to_string();
|
||||
let content: String = body.content.trim().to_string();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// get paste
|
||||
let paste: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_url(custom_url.clone()).await;
|
||||
|
||||
if paste.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&paste).unwrap());
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap();
|
||||
let is_atomic = unwrap.paste.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.paste.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let mut decoded = real_content.unwrap();
|
||||
|
||||
// check for existing file in atomic paste fs
|
||||
let existing = decoded.files.iter().position(|f| f.path == path);
|
||||
|
||||
if existing.is_some() {
|
||||
// remove existing file
|
||||
decoded.files.remove(existing.unwrap());
|
||||
}
|
||||
|
||||
// insert file
|
||||
decoded.files.push(AtomicPasteFSFile {
|
||||
path,
|
||||
content: content.clone(),
|
||||
});
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_paste_by_url(
|
||||
custom_url,
|
||||
serde_json::to_string::<db::AtomicPaste>(&decoded).unwrap(), // encode content
|
||||
String::new(),
|
||||
Option::None,
|
||||
Option::None,
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[post("/api/delete")]
|
||||
/// Delete a paste
|
||||
pub async fn delete_request(
|
||||
|
@ -520,3 +417,255 @@ pub async fn get_from_id_request(req: HttpRequest, data: web::Data<db::AppData>)
|
|||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// atomic "CRUD" operations
|
||||
#[get("/api/atomic/crud/{url:.*}/{path:.*}")]
|
||||
/// Read an atomic paste's "file system"
|
||||
pub async fn read_atomic_request(req: HttpRequest, data: web::Data<db::AppData>) -> impl Responder {
|
||||
let custom_url: String = req.match_info().get("url").unwrap().to_string();
|
||||
let path: String = format!("/{}", req.match_info().get("path").unwrap());
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// get paste
|
||||
let paste: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_url(custom_url.clone()).await;
|
||||
|
||||
if paste.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&paste).unwrap());
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap();
|
||||
let is_atomic = unwrap.paste.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.paste.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let decoded = real_content.unwrap();
|
||||
|
||||
// check for existing file in atomic paste fs
|
||||
let existing = decoded.files.iter().find(|f| f.path == path);
|
||||
|
||||
if existing.is_none() {
|
||||
return HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "text/plain"))
|
||||
.body("Path does not exist");
|
||||
}
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/plain"))
|
||||
.body(existing.unwrap().content.clone());
|
||||
}
|
||||
|
||||
#[post("/api/atomic/crud/{url:.*}/{path:.*}")]
|
||||
/// Update an atomic paste's "file system"
|
||||
pub async fn update_atomic_request(
|
||||
req: HttpRequest,
|
||||
body: String, // text/plain
|
||||
data: web::Data<db::AppData>,
|
||||
) -> impl Responder {
|
||||
// this is essentially the same as edit_request but it handles the atomic JSON file system
|
||||
// ...it does NOT accept an edit password! users must be authenticated
|
||||
let custom_url: String = req.match_info().get("url").unwrap().to_string();
|
||||
let path: String = format!("/{}", req.match_info().get("path").unwrap());
|
||||
let content: String = body.clone();
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// get paste
|
||||
let paste: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_url(custom_url.clone()).await;
|
||||
|
||||
if paste.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&paste).unwrap());
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap();
|
||||
let is_atomic = unwrap.paste.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.paste.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let mut decoded = real_content.unwrap();
|
||||
|
||||
// check for existing file in atomic paste fs
|
||||
let existing = decoded.files.iter().position(|f| f.path == path);
|
||||
|
||||
if existing.is_some() {
|
||||
// remove existing file
|
||||
decoded.files.remove(existing.unwrap());
|
||||
}
|
||||
|
||||
// insert file
|
||||
decoded.files.push(AtomicPasteFSFile {
|
||||
path,
|
||||
content: content.clone(),
|
||||
});
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_paste_by_url(
|
||||
custom_url,
|
||||
serde_json::to_string::<db::AtomicPaste>(&decoded).unwrap(), // encode content
|
||||
String::new(),
|
||||
Option::None,
|
||||
Option::None,
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
||||
#[delete("/api/atomic/crud/{url:.*}/{path:.*}")]
|
||||
/// Delete in an atomic paste's "file system"
|
||||
pub async fn delete_atomic_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<db::AppData>,
|
||||
) -> impl Responder {
|
||||
// this is essentially the same as edit_request but it handles the atomic JSON file system
|
||||
// ...it does NOT accept an edit password! users must be authenticated
|
||||
let custom_url: String = req.match_info().get("url").unwrap().to_string();
|
||||
let path: String = format!("/{}", req.match_info().get("path").unwrap());
|
||||
|
||||
// get token user
|
||||
let token_cookie = req.cookie("__Secure-Token");
|
||||
let token_user = if token_cookie.is_some() {
|
||||
Option::Some(
|
||||
data.db
|
||||
.get_user_by_unhashed(token_cookie.as_ref().unwrap().value().to_string()) // if the user is returned, that means the ID is valid
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
Option::None
|
||||
};
|
||||
|
||||
if token_user.is_some() {
|
||||
// make sure user exists
|
||||
if token_user.as_ref().unwrap().success == false {
|
||||
return HttpResponse::NotFound().body("Invalid token");
|
||||
}
|
||||
}
|
||||
|
||||
// get paste
|
||||
let paste: DefaultReturn<Option<FullPaste<PasteMetadata, String>>> =
|
||||
data.db.get_paste_by_url(custom_url.clone()).await;
|
||||
|
||||
if paste.success == false {
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&paste).unwrap());
|
||||
}
|
||||
|
||||
// make sure paste is an atomic paste
|
||||
let unwrap = paste.payload.unwrap();
|
||||
let is_atomic = unwrap.paste.content.contains("\"_is_atomic\":true");
|
||||
|
||||
if is_atomic == false {
|
||||
return HttpResponse::NotFound().body("Paste is not atomic");
|
||||
}
|
||||
|
||||
// get file from path
|
||||
let real_content = serde_json::from_str::<db::AtomicPaste>(&unwrap.paste.content);
|
||||
|
||||
if real_content.is_err() {
|
||||
return HttpResponse::NotAcceptable().body("Paste failed to deserialize");
|
||||
}
|
||||
|
||||
let mut decoded = real_content.unwrap();
|
||||
|
||||
// check for existing file in atomic paste fs
|
||||
let existing = decoded.files.iter().position(|f| f.path == path);
|
||||
|
||||
if existing.is_some() {
|
||||
// remove existing file
|
||||
decoded.files.remove(existing.unwrap());
|
||||
}
|
||||
|
||||
// ...
|
||||
let res = data
|
||||
.db
|
||||
.edit_paste_by_url(
|
||||
custom_url,
|
||||
serde_json::to_string::<db::AtomicPaste>(&decoded).unwrap(), // encode content
|
||||
String::new(),
|
||||
Option::None,
|
||||
Option::None,
|
||||
if token_user.is_some() {
|
||||
Option::Some(token_user.unwrap().payload.unwrap().user.username)
|
||||
} else {
|
||||
Option::None
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// return
|
||||
return HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "application/json"))
|
||||
.body(serde_json::to_string(&res).unwrap());
|
||||
}
|
||||
|
|
|
@ -112,12 +112,15 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(crate::api::pastes::render_request)
|
||||
.service(crate::api::pastes::create_request)
|
||||
.service(crate::api::pastes::edit_request)
|
||||
.service(crate::api::pastes::edit_atomic_request)
|
||||
.service(crate::api::pastes::delete_request)
|
||||
.service(crate::api::pastes::metadata_request)
|
||||
// POST api::pastes SSM
|
||||
.service(crate::api::pastes::render_ssm_request)
|
||||
.service(crate::api::pastes::render_paste_ssm_request)
|
||||
// atomic api
|
||||
.service(crate::api::pastes::read_atomic_request)
|
||||
.service(crate::api::pastes::update_atomic_request)
|
||||
.service(crate::api::pastes::delete_atomic_request)
|
||||
// GET api
|
||||
.service(crate::api::pastes::get_from_url_request)
|
||||
.service(crate::api::pastes::get_from_id_request)
|
||||
|
|
|
@ -15,6 +15,7 @@ struct EditQueryProps {
|
|||
|
||||
#[derive(Default, Properties, PartialEq, serde::Deserialize)]
|
||||
struct FSProps {
|
||||
pub custom_url: String,
|
||||
pub files: Vec<db::AtomicPasteFSFile>,
|
||||
pub auth_state: Option<bool>,
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ fn Dashboard(props: &Props) -> Html {
|
|||
|
||||
<div class="card round secondary flex g-4 flex-column justify-center" id="pastes_list">
|
||||
{for props.pastes.iter().map(|p| html! {
|
||||
<a class="button secondary round full justify-start" href={format!("/d/atomic/{}?path=/index.html", &p.id)}>
|
||||
<a class="button secondary round full justify-start" href={format!("/d/atomic/{}?", &p.id)}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-archive"><circle cx="15" cy="19" r="2"/><path d="M20.9 19.8A2 2 0 0 0 22 18V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h5.1"/><path d="M15 11v-1"/><path d="M15 17v-2"/></svg>
|
||||
{&p.custom_url}
|
||||
</a>
|
||||
|
@ -261,7 +262,26 @@ You can create an account at: /d/auth/register",
|
|||
fn EditPaste(props: &EditProps) -> Html {
|
||||
return html! {
|
||||
<div class="flex flex-column" style="height: 100dvh;">
|
||||
<div id="_doc" style="height: 100%; overflow: auto;" />
|
||||
<div class="panes flex mobile:flex-column" style="height: 100%; overflow: auto;">
|
||||
<div id="_doc" class="full" style="height: 100%; overflow: auto; display: block;"></div>
|
||||
|
||||
<div id="_preview_browser" class="full" style="height: 100%; overflow: hidden; display: none;">
|
||||
<div class="full flex g-4 bg-0" style="padding: var(--u-04); height: 47.8px;">
|
||||
<button class="round" id="preview" title="Refresh Preview">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<iframe id="_preview_pane" class="full" style="height: calc(100% - 47.8px); overflow: auto;" frameborder="0" src="about:blank"></iframe>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{"#_preview_pane { background: white; }
|
||||
#_preview_browser { border-left: solid 1px var(--background-surface2a); }
|
||||
@media screen and (max-width: 900px) { #_preview_browser { border-left: 0; border-top: solid 1px var(--background-surface2a); } }"}
|
||||
</style>
|
||||
</div>
|
||||
|
||||
<div class="card secondary flex mobile:justify-center justify-space-between align-center" style="
|
||||
overflow: auto hidden;
|
||||
border-top: 1px solid var(--background-surface2a);
|
||||
|
@ -270,11 +290,19 @@ fn EditPaste(props: &EditProps) -> Html {
|
|||
<b style="min-width: max-content;" class="device:desktop">{&props.file.path}</b>
|
||||
|
||||
<div class="flex g-4">
|
||||
<button class="round secondary" id="save">{"Save"}</button>
|
||||
<button class="round red secondary" id="delete">{"Delete"}</button>
|
||||
<a href="?" class="button round secondary" id="save" target="_blank">{"Files"}</a>
|
||||
<button class="round secondary green" id="save" title="Save File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-save"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||
</button>
|
||||
|
||||
<a href="?" class="button round secondary" id="file_explorer" title="Manage Files">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-tree"><path d="M20 10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2.5a1 1 0 0 1-.8-.4l-.9-1.2A1 1 0 0 0 15 3h-2a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"/><path d="M20 21a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-2.9a1 1 0 0 1-.88-.55l-.42-.85a1 1 0 0 0-.92-.6H13a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1Z"/><path d="M3 5a2 2 0 0 0 2 2h3"/><path d="M3 3v13a2 2 0 0 0 2 2h3"/></svg>
|
||||
</a>
|
||||
|
||||
<div class="hr-left" />
|
||||
<button class="round border" id="preview">{"Preview"}</button>
|
||||
|
||||
<button class="round secondary red" id="split_view" title="Split View">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-columns-2"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -299,22 +327,137 @@ fn build_edit_renderer_with_props(props: EditProps) -> ServerRenderer<EditPaste>
|
|||
fn PasteFiles(props: &FSProps) -> Html {
|
||||
return html! {
|
||||
<div class="flex flex-column" style="height: 100dvh;">
|
||||
<main class="small">
|
||||
<div class="card secondary round flex flex-column g-4">
|
||||
{for props.files.iter().map(|p| html! {
|
||||
<a href={format!("?path={}", &p.path)}>{&p.path}</a>
|
||||
})}
|
||||
<GlobalMenu auth_state={props.auth_state} />
|
||||
|
||||
<div class="toolbar flex justify-space-between">
|
||||
// left
|
||||
<div class="flex">
|
||||
<button title="Menu" b_onclick="window.toggle_child_menu(event.target, '#upper\\\\:globalmenu')" style="border-left: 0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||
</button>
|
||||
|
||||
<a class="button" href="/d" style="border-left: 0">
|
||||
{"Dashboard"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-layout-wrapper">
|
||||
<div id="link-header" style="display: flex;" class="flex-column bg-1">
|
||||
<div class="link-header-top"></div>
|
||||
|
||||
<div class="link-header-middle">
|
||||
<h1 class="no-margin">{&props.custom_url}</h1>
|
||||
</div>
|
||||
|
||||
<div class="link-header-bottom">
|
||||
<a href="/d" class="button">{"Home"}</a>
|
||||
<a href="/d/pastes" class="button">{"Pastes"}</a>
|
||||
<a href="/d/atomic" class="button active">{"Atomic"}</a>
|
||||
<a href="::PUFFER_ROOT::/d" class="button">{"Boards"}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="flex flex-column g-4 small">
|
||||
<div id="error" class="mdnote note-error full" style="display: none;" />
|
||||
<div id="success" class="mdnote note-note full" style="display: none;" />
|
||||
|
||||
<div id="custom_url" style="display: none;">{&props.custom_url}</div>
|
||||
|
||||
<form class="flex justify-center align-center g-4">
|
||||
<input type="text" placeholder="/index.(html|css|js)" name="path" class="round full" minlength={4} />
|
||||
<button class="round bundles-primary" style="min-width: max-content;">{"Open"}</button>
|
||||
</form>
|
||||
|
||||
<table class="full stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{"Path"}</th>
|
||||
<th>{"Actions"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{for props.files.iter().map(|p| html! {
|
||||
<tr>
|
||||
<td><a href={format!("?path={}", &p.path)}>{&p.path}</a></td>
|
||||
|
||||
<td class="flex g-4 flex-wrap">
|
||||
<a class="button secondary round" href={format!("?path={}", &p.path)} title="Edit File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-pen-line"><path d="m18 5-3-3H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/><path d="M8 18h1"/><path d="M18.4 9.6a2 2 0 1 1 3 3L17 17l-4 1 1-4Z"/></svg>
|
||||
</a>
|
||||
|
||||
<button class="secondary round action:more-modal" data-suffix={format!("{}{}", &props.custom_url, &p.path)} title="More Options">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wrench"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr />
|
||||
|
||||
<form class="flex justify-center align-center g-4 flex-wrap mobile:flex-column">
|
||||
<input type="text" placeholder="/index.html" name="path" class="round mobile:max" minlength={4} />
|
||||
<button class="round bundles-primary mobile:max">{"Open"}</button>
|
||||
</form>
|
||||
</div>
|
||||
<h6 class="no-margin">{"Paste Options"}</h6>
|
||||
|
||||
<Footer auth_state={props.auth_state} />
|
||||
</main>
|
||||
<table class="full stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{"Name"}</th>
|
||||
<th>{"Use"}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{"View"}</td>
|
||||
<td>
|
||||
<a class="button round secondary" target="_blank" href={format!("/{}", props.custom_url)}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-play"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
|
||||
{"Run"}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{"Delete"}</td>
|
||||
<td>
|
||||
<button class="round secondary" id="delete">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-play"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
|
||||
{"Run"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
// ...
|
||||
<dialog id="more-modal">
|
||||
<div style="width: 25rem; max-width: 100%;">
|
||||
<h2 class="no-margin full text-center">{"More Options"}</h2>
|
||||
|
||||
<hr />
|
||||
<div id="more-modal-actions" class="flex flex-column g-4"></div>
|
||||
<hr />
|
||||
|
||||
<div class="full flex justify-right">
|
||||
<a
|
||||
class="button round red"
|
||||
href="javascript:document.getElementById('more-modal').close();"
|
||||
>
|
||||
{"Close"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script type="module">
|
||||
{"import \"/static/js/AtomicOverview.js\";"}
|
||||
</script>
|
||||
|
||||
<Footer auth_state={props.auth_state} />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
@ -395,6 +538,7 @@ You can create an account at: /d/auth/register",
|
|||
// show file list if path is none
|
||||
if info.path.is_none() {
|
||||
let renderer = build_fs_renderer_with_props(FSProps {
|
||||
custom_url: unwrap.custom_url.clone(),
|
||||
files: decoded.files,
|
||||
auth_state: if req.cookie("__Secure-Token").is_some() {
|
||||
Option::Some(true)
|
||||
|
|
|
@ -384,8 +384,9 @@ pub async fn paste_view_request(
|
|||
));
|
||||
}
|
||||
|
||||
#[get("/h/{url:.*}/{path:.*}")]
|
||||
/// Available at "/h/{custom_url}/{file_path}"
|
||||
// #[get("/h/{url:.*}/{path:.*}")]
|
||||
#[get("/+{url:.*}/{path:.*}")]
|
||||
/// Available at "/+{custom_url}/{file_path}"
|
||||
pub async fn atomic_paste_view_request(
|
||||
req: HttpRequest,
|
||||
data: web::Data<AppData>,
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
|
||||
import { basicSetup } from "codemirror";
|
||||
import { html, htmlCompletionSource } from "@codemirror/lang-html";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { css, cssCompletionSource } from "@codemirror/lang-css";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
|
@ -193,6 +194,7 @@ export function create_editor(
|
|||
path: string
|
||||
) {
|
||||
if (globalThis.Bun) return; // must be run from client
|
||||
const file_type = path.split(".").pop();
|
||||
|
||||
const view = new EditorView({
|
||||
// @ts-ignore
|
||||
|
@ -232,7 +234,11 @@ export function create_editor(
|
|||
indentOnInput(),
|
||||
indentUnit.of(" "),
|
||||
// language
|
||||
path.endsWith("css") ? css() : html({ autoCloseTags: true }),
|
||||
path.endsWith("css")
|
||||
? css()
|
||||
: path.endsWith("js")
|
||||
? javascript()
|
||||
: html({ autoCloseTags: true }),
|
||||
path.endsWith("html") ? HTMLLint : EmptyLint,
|
||||
// default
|
||||
basicSetup,
|
||||
|
@ -289,11 +295,49 @@ export function create_editor(
|
|||
};
|
||||
|
||||
// handle interactions
|
||||
let view_split: boolean = false;
|
||||
|
||||
const preview_button = document.getElementById(
|
||||
"preview"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (preview_button) {
|
||||
const split_button = document.getElementById(
|
||||
"split_view"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
const preview_browser = document.getElementById(
|
||||
"_preview_browser"
|
||||
) as HTMLDivElement | null;
|
||||
|
||||
const preview_pane = document.getElementById(
|
||||
"_preview_pane"
|
||||
) as HTMLIFrameElement | null;
|
||||
|
||||
if (split_button && preview_browser) {
|
||||
if (file_type !== "html") {
|
||||
split_button.remove();
|
||||
}
|
||||
|
||||
// split view on click
|
||||
split_button.addEventListener("click", () => {
|
||||
view_split = !view_split;
|
||||
|
||||
if (view_split) {
|
||||
preview_browser.style.display = "block";
|
||||
|
||||
split_button.classList.remove("red");
|
||||
split_button.classList.add("green");
|
||||
preview_button?.click(); // refresh preview
|
||||
} else {
|
||||
preview_browser.style.display = "none";
|
||||
|
||||
split_button.classList.remove("green");
|
||||
split_button.classList.add("red");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (preview_button && preview_pane) {
|
||||
let url: string = "";
|
||||
preview_button.addEventListener("click", () => {
|
||||
if (url.length > 0) {
|
||||
|
@ -308,8 +352,8 @@ export function create_editor(
|
|||
// get url
|
||||
url = URL.createObjectURL(blob);
|
||||
|
||||
// open
|
||||
window.open(url);
|
||||
// load
|
||||
preview_pane.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -319,15 +363,11 @@ export function create_editor(
|
|||
|
||||
if (save_button) {
|
||||
save_button.addEventListener("click", async () => {
|
||||
const res = await fetch("/api/edit-atomic", {
|
||||
const res = await fetch(`/api/atomic/crud/${custom_url}${path}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
custom_url,
|
||||
path,
|
||||
content: (globalThis as any).AtomicEditor.Content,
|
||||
}),
|
||||
body: (globalThis as any).AtomicEditor.Content,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -341,44 +381,11 @@ export function create_editor(
|
|||
});
|
||||
}
|
||||
|
||||
const delete_button = document.getElementById(
|
||||
"delete"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (delete_button) {
|
||||
delete_button.addEventListener("click", async () => {
|
||||
const _confirm = confirm(
|
||||
"Are you sure you would like to do this? This URL will be available for anybody to claim. **This will delete the paste, not the page!"
|
||||
);
|
||||
|
||||
if (!_confirm) return;
|
||||
|
||||
const edit_password = prompt(
|
||||
"Please enter this paste's custom URL to confirm:"
|
||||
);
|
||||
|
||||
if (!edit_password) return;
|
||||
|
||||
const res = await fetch("/api/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
custom_url,
|
||||
edit_password: edit_password,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
return alert(json.message);
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
}
|
||||
// prevent exit
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
e.preventDefault();
|
||||
e.returnValue = true;
|
||||
});
|
||||
|
||||
// return
|
||||
return view;
|
||||
|
|
114
static/ts/pages/AtomicOverview.ts
Normal file
114
static/ts/pages/AtomicOverview.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
const error: HTMLElement = document.getElementById("error")!;
|
||||
const success: HTMLElement = document.getElementById("success")!;
|
||||
|
||||
function init_delete_buttons() {
|
||||
const delete_buttons: HTMLButtonElement[] = Array.from(
|
||||
document.getElementsByClassName("action:delete-file")
|
||||
) as HTMLButtonElement[];
|
||||
|
||||
if (delete_buttons) {
|
||||
// delete files
|
||||
for (const delete_button of delete_buttons) {
|
||||
delete_button.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await fetch(
|
||||
delete_button.getAttribute("data-endpoint")!,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
error.style.display = "block";
|
||||
error.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
} else {
|
||||
success.style.display = "block";
|
||||
success.innerHTML = `<div class="mdnote-title">${json.message}</div>`;
|
||||
}
|
||||
|
||||
(
|
||||
document.getElementById("more-modal") as HTMLDialogElement
|
||||
).close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const more_buttons: HTMLButtonElement[] = Array.from(
|
||||
document.getElementsByClassName("action:more-modal")
|
||||
) as HTMLButtonElement[];
|
||||
|
||||
const more_modal_actions: HTMLDivElement | null = document.getElementById(
|
||||
"more-modal-actions"
|
||||
) as HTMLDivElement | null;
|
||||
|
||||
if (more_buttons && more_modal_actions) {
|
||||
for (const button of more_buttons) {
|
||||
button.addEventListener("click", () => {
|
||||
const data_suffix = button.getAttribute("data-suffix")!;
|
||||
|
||||
more_modal_actions.innerHTML = `<a class="button full justify-start round" target="_blank" href="/+${data_suffix}" title="View File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
View
|
||||
</a>
|
||||
|
||||
<button class="red round full justify-start action:delete-file" data-endpoint="/api/atomic/crud/${data_suffix}" title="Delete File">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>
|
||||
Delete
|
||||
</button>`;
|
||||
|
||||
(
|
||||
document.getElementById("more-modal") as HTMLDialogElement
|
||||
).showModal();
|
||||
|
||||
init_delete_buttons();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const custom_url = (document.getElementById("custom_url") as HTMLDivElement)
|
||||
.innerText;
|
||||
|
||||
const delete_button = document.getElementById(
|
||||
"delete"
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
if (delete_button) {
|
||||
delete_button.addEventListener("click", async () => {
|
||||
const _confirm = confirm(
|
||||
"Are you sure you would like to do this? This URL will be available for anybody to claim. **This will delete the entire paste and all its files!"
|
||||
);
|
||||
|
||||
if (!_confirm) return;
|
||||
|
||||
const edit_password = prompt(
|
||||
"Please enter this paste's edit password:"
|
||||
);
|
||||
|
||||
if (!edit_password) return;
|
||||
|
||||
const res = await fetch("/api/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
custom_url,
|
||||
edit_password: edit_password,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.success === false) {
|
||||
return alert(json.message);
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// default export
|
||||
export default {};
|
|
@ -17,6 +17,7 @@ const output = await build({
|
|||
"./static/ts/pages/NewAtomic.ts",
|
||||
"./static/ts/pages/ManageBoardPost.ts",
|
||||
"./static/ts/pages/SDManageUser.ts",
|
||||
"./static/ts/pages/AtomicOverview.ts",
|
||||
],
|
||||
minify: {
|
||||
identifiers: true,
|
||||
|
|
Loading…
Reference in a new issue