From 8121b1bdbdaacefed48a6e5b397b8d45cfb7039d Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Thu, 30 May 2024 05:08:22 +0200 Subject: [PATCH 01/21] add console --- src-tauri/src/external_command.rs | 14 +++- src-tauri/src/main.rs | 31 +++++++++ src/App.vue | 4 +- src/components/Console.vue | 106 ++++++++++++++++++++++++++++++ src/components/RustDetail.vue | 2 +- 5 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/components/Console.vue diff --git a/src-tauri/src/external_command.rs b/src-tauri/src/external_command.rs index 4133bd4..03ef825 100644 --- a/src-tauri/src/external_command.rs +++ b/src-tauri/src/external_command.rs @@ -16,8 +16,12 @@ fn is_abort_state(app: tauri::AppHandle) -> bool { use tokio::io::AsyncBufReadExt; use tokio::process::Command; +fn log_to_frontend(app: &tauri::AppHandle, message: &str) { + app.emit_all("command-log", message).unwrap(); +} + pub async fn run_external_command_with_progress( - _window: Window, + window: Window, app: tauri::AppHandle, cmd_name: &str, cmd_args: &[&str], @@ -27,6 +31,7 @@ pub async fn run_external_command_with_progress( let cmd_args_owned: Vec = cmd_args.iter().map(|&s| s.to_string()).collect(); info!("Command: {} {}", cmd_name_owned, cmd_args_owned.join(" ")); + log_to_frontend(&app, &format!("Command: {} {}", cmd_name_owned, cmd_args_owned.join(" "))); let child_result = Command::new(&cmd_name_owned) .args(&cmd_args_owned) @@ -38,6 +43,7 @@ pub async fn run_external_command_with_progress( Ok(child) => child, Err(e) => { info!("Failed to launch command: {:?}", e); + log_to_frontend(&app, &format!("Failed to launch command: {:?}", e)); return Err(()); } }; @@ -55,12 +61,14 @@ pub async fn run_external_command_with_progress( _ = stdout.read_line(&mut stdout_buf) => { if !stdout_buf.is_empty() { info!("{}", stdout_buf); + log_to_frontend(&app, &stdout_buf); stdout_buf.clear(); } }, _ = stderr.read_line(&mut stderr_buf) => { if !stderr_buf.is_empty() { info!("{}", stderr_buf); + log_to_frontend(&app, &stderr_buf); stderr_buf.clear(); } }, @@ -68,14 +76,17 @@ pub async fn run_external_command_with_progress( match status { Ok(status) if status.success() => { info!("Done"); + log_to_frontend(&app, "Child process completed successfully"); return Ok("Child process completed successfully".to_string()); }, Ok(_) => { info!("Child process exited with an error"); + log_to_frontend(&app, "Child process exited with an error"); return Err(()); }, Err(err) => { info!("Child process encountered an error: {:?}", err); + log_to_frontend(&app, &format!("Child process encountered an error: {:?}", err)); return Err(()); }, } @@ -83,6 +94,7 @@ pub async fn run_external_command_with_progress( _ = tokio::time::sleep(poll_interval) => { if is_abort_state(app.clone()) { info!("Aborting command due to external signal."); + log_to_frontend(&app, "Aborting command due to external signal."); let _ = child.kill(); return Err(()); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 42ef23c..bf755b7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -368,6 +368,36 @@ async fn get_connected_serial_devices() -> Vec { esp32s } +#[tauri::command] +async fn execute_command(command: String) -> Result { + use std::process::Command; + + let output = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", &command]) + .output() + } else { + Command::new("sh") + .arg("-c") + .arg(command) + .output() + }; + + match output { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + if output.status.success() { + Ok(stdout) + } else { + Err(stderr) + } + } + Err(e) => Err(e.to_string()), + } +} + + fn main() { tauri::Builder::default() .manage(Mutex::new(AppState::default())) @@ -375,6 +405,7 @@ fn main() { compress, decompress, download_esp_idf, + execute_command, get_connected_serial_devices, get_disk_usage, get_user_home, diff --git a/src/App.vue b/src/App.vue index 6c0e0f6..f57a107 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,6 +4,7 @@ import { getVersion } from '@tauri-apps/api/app'; import { appWindow } from '@tauri-apps/api/window'; import HomeIcon from "./components/HomeIcon.vue"; import ErrorMessage from './components/ErrorMessage.vue'; +import Console from './components/Console.vue'; const appVersion = ref(''); const errorMessage = ref(""); @@ -26,8 +27,6 @@ onMounted(() => { fetchVersion(); }); - - diff --git a/src/components/Console.vue b/src/components/Console.vue new file mode 100644 index 0000000..bfe4eed --- /dev/null +++ b/src/components/Console.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/components/RustDetail.vue b/src/components/RustDetail.vue index 0b841bc..df3c93e 100644 --- a/src/components/RustDetail.vue +++ b/src/components/RustDetail.vue @@ -122,7 +122,7 @@ let supportedChips = ref("ESP32, ESP32-S2, ESP-S3"); // Default for Xtensa Installation in progress...
- +
From 8e8b220fd959f41ba02547b0a23872a8e6640b02 Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Thu, 30 May 2024 05:16:12 +0200 Subject: [PATCH 02/21] transparent console --- src/components/Console.vue | 43 ++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/components/Console.vue b/src/components/Console.vue index bfe4eed..630562b 100644 --- a/src/components/Console.vue +++ b/src/components/Console.vue @@ -1,14 +1,17 @@ @@ -82,7 +85,7 @@ onBeforeUnmount(() => { left: 0; width: 100%; max-height: 50%; - background-color: black; + background-color: rgba(0, 0, 0, 0.8); /* Partially transparent background */ color: limegreen; overflow: auto; padding: 10px; @@ -93,14 +96,32 @@ onBeforeUnmount(() => { .console-output { max-height: 80%; overflow-y: auto; + text-align: left; /* Align text to the left */ } input[type="text"] { - width: 100%; + width: calc(100% - 110px); /* Adjust width to avoid overlap with the button */ padding: 5px; margin-top: 10px; background-color: black; color: limegreen; border: 1px solid limegreen; } + +.toggle-button { + position: fixed; + bottom: 10px; + right: 10px; + background-color: rgba(0, 0, 0, 0.7); + border: 1px solid limegreen; + color: limegreen; + cursor: pointer; + font-size: 1em; + padding: 5px 10px; + z-index: 10000; /* Ensure the button is above other elements, including the console */ +} + +.toggle-button:hover { + color: white; +} From 17ba278aaa969aa39c9f6202d5ce4e7a395035d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 08:53:47 +0200 Subject: [PATCH 03/21] improve the console --- src/App.vue | 3 +- src/components/Console.css | 82 +++++++++++++++++++++++++++++++++++ src/components/Console.vue | 87 ++++++++++++-------------------------- 3 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 src/components/Console.css diff --git a/src/App.vue b/src/App.vue index f57a107..6b9c30f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -35,7 +35,6 @@ onMounted(() => { -
@@ -46,7 +45,7 @@ onMounted(() => {
Version: {{ appVersion }}
- +
diff --git a/src/components/Console.css b/src/components/Console.css new file mode 100644 index 0000000..3fcf958 --- /dev/null +++ b/src/components/Console.css @@ -0,0 +1,82 @@ +.console { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + max-height: 50%; + min-height: 100px; /* Ensure the console is at least big enough to fit the command prompt */ + background-color: rgba(0, 0, 0, 0.8); /* Partially transparent background */ + color: limegreen; + overflow: hidden; + padding: 10px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + z-index: 9999; /* Ensure the console is above other elements */ + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Use a more readable monospace font */ + font-weight: 600; /* Slightly heavier font */ + display: flex; + flex-direction: column; +} + +.console-output { + flex-grow: 1; + overflow-y: auto; /* Ensure vertical scrollbar */ + text-align: left; /* Align text to the left */ + scrollbar-width: thin; /* Add a thin scrollbar */ + padding-right: 10px; /* Add some padding to avoid text overlap with scrollbar */ +} + +.console-output::-webkit-scrollbar { + width: 8px; /* Width of vertical scrollbar */ +} + +.console-output::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.console-output::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.input-container { + display: flex; + align-items: center; + width: 100%; +} + +input[type="text"] { + flex-grow: 1; + padding: 5px; + background-color: black; + color: limegreen; + border: 1px solid limegreen; + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Use the same readable monospace font */ + font-weight: 600; /* Slightly heavier font */ + margin-right: 8em; +} + +button { + background-color: rgba(0, 0, 0, 0.7); + border: 1px solid limegreen; + color: limegreen; + cursor: pointer; + font-size: 1em; + padding: 5px 10px; +} + +.toggle-button { + position: fixed; + bottom: 10px; + right: 10px; + background-color: rgba(0, 0, 0, 0.7); + border: 1px solid limegreen; + color: limegreen; + cursor: pointer; + font-size: 1em; + padding: 5px 10px; + z-index: 10000; /* Ensure the button is above other elements, including the console */ +} + +.toggle-button:hover { + color: white; +} diff --git a/src/components/Console.vue b/src/components/Console.vue index 630562b..c9dcec1 100644 --- a/src/components/Console.vue +++ b/src/components/Console.vue @@ -2,16 +2,21 @@
-
{{ log.message }}
+
+
+
+
-
- +
@@ -19,15 +24,27 @@ import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { invoke } from '@tauri-apps/api/tauri'; import { appWindow } from '@tauri-apps/api/window'; +import './Console.css'; + +// Function to escape HTML characters +const escapeHtml = (unsafe: string) => { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +}; const logs = ref<{ id: number, message: string }[]>([]); let logId = 0; const command = ref(""); -const isVisible = ref(true); // Set to true to make console visible by default +const isVisible = ref(false); const consoleOutput = ref(null); const addLog = (message: string) => { - logs.value.push({ id: logId++, message }); + logs.value.push({ id: logId++, message: escapeHtml(message) }); + nextTick(() => { if (consoleOutput.value) { consoleOutput.value.scrollTop = consoleOutput.value.scrollHeight; @@ -37,7 +54,7 @@ const addLog = (message: string) => { const executeCommand = () => { if (command.value.trim()) { - addLog(`> ${command.value}`); + addLog(`> ${escapeHtml(command.value)}`); invoke('execute_command', { command: command.value }) .then((result) => { addLog(result as string); @@ -77,51 +94,3 @@ onBeforeUnmount(() => { console.log("Key listener removed"); }); - - From 973426ee18df60d14b0193b9f01a4c7b8c402b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 09:28:02 +0200 Subject: [PATCH 04/21] add devportal --- src-tauri/src/main.rs | 45 ++++++++++++-- src/components/Dashboard.css | 42 +++++++++++++ src/components/Dashboard.vue | 56 ++--------------- src/components/DeveloperPortalContribute.vue | 63 ++++++++++++++++++++ src/components/DeveloperPortalRead.vue | 23 +++++++ src/components/DeveloperPortalTile.vue | 26 ++++++++ src/router.ts | 12 +++- 7 files changed, 210 insertions(+), 57 deletions(-) create mode 100644 src/components/Dashboard.css create mode 100644 src/components/DeveloperPortalContribute.vue create mode 100644 src/components/DeveloperPortalRead.vue create mode 100644 src/components/DeveloperPortalTile.vue diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bf755b7..d769b9c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -46,7 +46,7 @@ async fn abort_build(state_mutex: State<'_, Mutex>) -> Result Result { Ok(js_versions_file[object_start..object_end].to_string()) } -// Comand to get the current user home +// Command to get the current user home #[tauri::command] async fn get_user_home() -> Result { match dirs::home_dir() { @@ -338,7 +338,7 @@ async fn get_disk_usage() -> Result, ()> { Ok(disk_info) } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, serde::Deserialize)] struct ConnectedPort { port_name: String, product: String, @@ -397,6 +397,39 @@ async fn execute_command(command: String) -> Result { } } +#[tauri::command] +async fn check_devportal() -> Result { + // Implement the logic to check if the devportal directory exists + // Example: Check if ~/.espressif/devportal exists + Ok(std::path::Path::new(&dirs::home_dir().unwrap().join(".espressif/devportal")).exists()) +} + +#[tauri::command] +async fn get_authors() -> Result, String> { + // Implement the logic to read authors from data/authors + // Example implementation: + let authors_path = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); + let mut authors = Vec::new(); + for entry in std::fs::read_dir(authors_path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.is_file() { + let author: Author = serde_json::from_reader(std::fs::File::open(path).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + authors.push(author); + } + } + Ok(authors) +} + +#[derive(serde::Serialize, serde::Deserialize)] +struct Author { + name: String, + image: String, + bio: String, + social: Vec>, +} + fn main() { tauri::Builder::default() @@ -420,7 +453,9 @@ fn main() { stop_monitor, check_rust_support, install_rust_support, - get_platform + get_platform, + check_devportal, + get_authors ]) .setup(|app| { // Initialize the logging system diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css new file mode 100644 index 0000000..c47708d --- /dev/null +++ b/src/components/Dashboard.css @@ -0,0 +1,42 @@ +.add-button { + display: inline-block; + padding: 10px 20px; + color: #fff; + background-color: #007bff; + border-radius: 5px; + text-decoration: none; + transition: background-color 0.2s ease; +} + +.add-button:hover { + background-color: #0056b3; +} + +.grid-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; + padding: 20px; +} + +.tile { + border: 1px solid #ccc; + padding: 20px; + border-radius: 8px; + background-color: #f8f8f8; +} + +.tile :deep(h2)::after { + content: ''; + display: block; + height: 1px; + background-color: lightgray; + margin-top: 10px; + margin-bottom: 10px; +} + +@media (prefers-color-scheme: dark) { + .tile { + background-color: #989898; + } +} \ No newline at end of file diff --git a/src/components/Dashboard.vue b/src/components/Dashboard.vue index 9b99398..0f65eb0 100644 --- a/src/components/Dashboard.vue +++ b/src/components/Dashboard.vue @@ -7,6 +7,8 @@ import EspIdfList from './EspIdfList.vue'; import ConnectedDevicesList from "./ConnectedDevicesList.vue"; import RustDashboardTile from "./RustDashboardTile.vue"; import BooksTile from "./BooksTile.vue"; +import DeveloperPortalTile from "./DeveloperPortalTile.vue"; +import './Dashboard.css'; let versions = ref([]); @@ -29,8 +31,6 @@ onMounted(() => { console.error(error); }); }); - - - - - - \ No newline at end of file diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue new file mode 100644 index 0000000..12637df --- /dev/null +++ b/src/components/DeveloperPortalContribute.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/components/DeveloperPortalRead.vue b/src/components/DeveloperPortalRead.vue new file mode 100644 index 0000000..661756e --- /dev/null +++ b/src/components/DeveloperPortalRead.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/src/components/DeveloperPortalTile.vue b/src/components/DeveloperPortalTile.vue new file mode 100644 index 0000000..bc08c41 --- /dev/null +++ b/src/components/DeveloperPortalTile.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/router.ts b/src/router.ts index fd8dbaf..d8e0003 100644 --- a/src/router.ts +++ b/src/router.ts @@ -32,6 +32,16 @@ const routes: Array = [ path: "/rust/book", name: "RustBook", component: () => import("./components/RustBook.vue"), + }, + { + path: "/developer-portal/read", + name: "DeveloperPortalRead", + component: () => import("./components/DeveloperPortalRead.vue"), + }, + { + path: "/developer-portal/contribute", + name: "DeveloperPortalContribute", + component: () => import("./components/DeveloperPortalContribute.vue"), } ]; @@ -40,4 +50,4 @@ const router = createRouter({ routes, }); -export default router; \ No newline at end of file +export default router; From e9c1b473d9a52aa30e8787718859183681c96e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 09:49:38 +0200 Subject: [PATCH 05/21] add option to update author --- src-tauri/src/commands.rs | 30 +++++++++++ src-tauri/src/main.rs | 38 +++---------- src-tauri/src/models.rs | 6 +++ src/components/DeveloperPortalContribute.vue | 56 +++++++++++++++++--- 4 files changed, 91 insertions(+), 39 deletions(-) create mode 100644 src-tauri/src/commands.rs create mode 100644 src-tauri/src/models.rs diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs new file mode 100644 index 0000000..894aa73 --- /dev/null +++ b/src-tauri/src/commands.rs @@ -0,0 +1,30 @@ +use tauri::{State, Window}; +use std::sync::Mutex; + +use crate::app_state::{AppState, BuilderState}; +use crate::Author; + +#[tauri::command] +pub async fn abort_build(state_mutex: State<'_, Mutex>) -> Result { + let mut state = state_mutex.lock().unwrap(); + state.builder = BuilderState::Abort; + Ok("ok".to_string()) +} + +// Other command functions... + +#[tauri::command] +pub async fn get_authors() -> Result, String> { + let authors_path = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); + let mut authors = Vec::new(); + for entry in std::fs::read_dir(authors_path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.is_file() { + let author: Author = serde_json::from_reader(std::fs::File::open(path).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + authors.push(author); + } + } + Ok(authors) +} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d769b9c..d37734a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -28,6 +28,12 @@ use tauri::{State, Window}; use serialport::available_ports; use sysinfo::{DiskExt, System, SystemExt}; +mod commands; +mod models; + +use commands::*; +use models::*; + // Create a custom Error that we can return in Results #[derive(Debug, thiserror::Error)] enum Error { @@ -39,13 +45,6 @@ enum Error { // PoisonError(String), } -#[tauri::command] -async fn abort_build(state_mutex: State<'_, Mutex>) -> Result { - let mut state = state_mutex.lock().unwrap(); - state.builder = BuilderState::Abort; - Ok("ok".to_string()) -} - // Command to compress directories into an archive file. #[tauri::command] async fn compress( @@ -404,31 +403,6 @@ async fn check_devportal() -> Result { Ok(std::path::Path::new(&dirs::home_dir().unwrap().join(".espressif/devportal")).exists()) } -#[tauri::command] -async fn get_authors() -> Result, String> { - // Implement the logic to read authors from data/authors - // Example implementation: - let authors_path = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); - let mut authors = Vec::new(); - for entry in std::fs::read_dir(authors_path).map_err(|e| e.to_string())? { - let entry = entry.map_err(|e| e.to_string())?; - let path = entry.path(); - if path.is_file() { - let author: Author = serde_json::from_reader(std::fs::File::open(path).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - authors.push(author); - } - } - Ok(authors) -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct Author { - name: String, - image: String, - bio: String, - social: Vec>, -} fn main() { diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs new file mode 100644 index 0000000..29dda61 --- /dev/null +++ b/src-tauri/src/models.rs @@ -0,0 +1,6 @@ +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Author { + pub name: String, + pub bio: String, + pub social: Vec>, +} diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 12637df..4e58ccf 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -2,19 +2,31 @@

Developer Portal

-

Developer Portal directory is present.

+

Authors

-
-

{{ author.name }}

- Author Image -

{{ author.bio }}

-
- {{ Object.keys(social)[0] }} +
+
+ + +
+ +
+ + +
+
+

{{ author.name }}

+

{{ author.bio }}

+ +
+

Developer Portal directory is not present.

@@ -30,6 +42,8 @@ import { appWindow } from '@tauri-apps/api/window'; const isDevPortalPresent = ref(false); const authors = ref([]); +const editIndex = ref(-1); +const isHugoRunning = ref(false); const checkDevPortal = async () => { try { @@ -54,10 +68,38 @@ const cloneRepo = async () => { const launchHugo = async () => { try { await invoke('execute_command', { command: 'hugo server --source ~/.espressif/devportal' }); + isHugoRunning.value = true; } catch (error) { console.error(error); } }; +const saveAuthor = async (index: number) => { + try { + const author = authors.value[index]; + const fileName = `${author.name.toLowerCase().replace(/ /g, '-')}.json`; + await invoke('execute_command', { + command: `echo '${JSON.stringify(author)}' > ~/.espressif/devportal/data/authors/${fileName}` + }); + editIndex.value = -1; + checkDevPortal(); + } catch (error) { + console.error(error); + } +}; + +const addNewAuthor = () => { + authors.value.push({ name: '', bio: '', social: [{ linkedin: '', twitter: '' }] }); + editIndex.value = authors.value.length - 1; +}; + onMounted(checkDevPortal); + + From 54e1c58100c15b732dabfda25e7288498ad5e59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 10:21:09 +0200 Subject: [PATCH 06/21] add Hugo to backend --- src-tauri/src/commands.rs | 65 +++++++++++++++++--- src-tauri/src/main.rs | 6 +- src/components/DeveloperPortalContribute.vue | 10 +-- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 894aa73..883eeb7 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,17 +1,64 @@ +use std::process::Command; +use std::sync::{Arc, Mutex}; use tauri::{State, Window}; -use std::sync::Mutex; - -use crate::app_state::{AppState, BuilderState}; use crate::Author; +pub struct HugoState { + pub is_running: Arc>, +} + #[tauri::command] -pub async fn abort_build(state_mutex: State<'_, Mutex>) -> Result { - let mut state = state_mutex.lock().unwrap(); - state.builder = BuilderState::Abort; - Ok("ok".to_string()) +pub async fn launch_hugo(state: State<'_, HugoState>) -> Result<(), String> { + let mut is_running = state.is_running.lock().unwrap(); + if *is_running { + return Err("Hugo is already running".to_string()); + } + + Command::new("hugo") + .args(&["server", "--source", "~/.espressif/devportal"]) + .spawn() + .map_err(|e| e.to_string())?; + + *is_running = true; + Ok(()) } -// Other command functions... +#[tauri::command] +pub async fn restart_hugo(state: State<'_, HugoState>) -> Result<(), String> { + let mut is_running = state.is_running.lock().unwrap(); + if *is_running { + // Send SIGTERM to Hugo process + Command::new("pkill") + .args(&["-f", "hugo server"]) + .output() + .map_err(|e| e.to_string())?; + + // Relaunch Hugo + Command::new("hugo") + .args(&["server", "--source", "~/.espressif/devportal"]) + .spawn() + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +pub async fn save_author(author: Author, file_name: String) -> Result<(), String> { + let authors_dir = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); + let content_dir = dirs::home_dir().unwrap().join(".espressif/devportal/content/authors"); + let file_path = authors_dir.join(&file_name); + let index_path = content_dir.join(file_name.replace(".json", "")).join("_index.md"); + + // Create author JSON file + std::fs::write(file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + + // Create _index.md file + std::fs::create_dir_all(index_path.parent().unwrap()).map_err(|e| e.to_string())?; + std::fs::write(index_path, &author.name).map_err(|e| e.to_string())?; + + Ok(()) +} #[tauri::command] pub async fn get_authors() -> Result, String> { @@ -27,4 +74,4 @@ pub async fn get_authors() -> Result, String> { } } Ok(authors) -} \ No newline at end of file +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d37734a..4f5b92f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -419,7 +419,6 @@ fn main() { get_esp_idf_list, get_esp_idf_tools_dir, get_available_idf_versions, - abort_build, run_esp_idf_install_script, start_flash, stop_flash, @@ -429,7 +428,10 @@ fn main() { install_rust_support, get_platform, check_devportal, - get_authors + get_authors, + save_author, + launch_hugo, + restart_hugo, ]) .setup(|app| { // Initialize the logging system diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 4e58ccf..284eba4 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -38,7 +38,6 @@ - + diff --git a/src/assets/console-icon.svg b/src/assets/console-icon.svg new file mode 100644 index 0000000..052621f --- /dev/null +++ b/src/assets/console-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/Console.css b/src/components/Console.css index 3fcf958..578f04a 100644 --- a/src/components/Console.css +++ b/src/components/Console.css @@ -10,7 +10,7 @@ overflow: hidden; padding: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); - z-index: 9999; /* Ensure the console is above other elements */ + z-index: 10001; /* Ensure the console is above other elements */ font-family: 'Consolas', 'Courier New', Courier, monospace; /* Use a more readable monospace font */ font-weight: 600; /* Slightly heavier font */ display: flex; @@ -52,7 +52,7 @@ input[type="text"] { border: 1px solid limegreen; font-family: 'Consolas', 'Courier New', Courier, monospace; /* Use the same readable monospace font */ font-weight: 600; /* Slightly heavier font */ - margin-right: 8em; + margin-right: 1.2em; } button { @@ -65,16 +65,12 @@ button { } .toggle-button { - position: fixed; - bottom: 10px; - right: 10px; background-color: rgba(0, 0, 0, 0.7); border: 1px solid limegreen; color: limegreen; cursor: pointer; font-size: 1em; padding: 5px 10px; - z-index: 10000; /* Ensure the button is above other elements, including the console */ } .toggle-button:hover { diff --git a/src/components/Console.vue b/src/components/Console.vue index c9dcec1..399343e 100644 --- a/src/components/Console.vue +++ b/src/components/Console.vue @@ -1,27 +1,24 @@ diff --git a/src/components/DeveloperPortalContribute.css b/src/components/DeveloperPortalContribute.css new file mode 100644 index 0000000..a864ff8 --- /dev/null +++ b/src/components/DeveloperPortalContribute.css @@ -0,0 +1,13 @@ +.developer-portal-contribute { + padding: 20px; +} + +button { + margin: 5px; +} + +.author { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 10px; +} diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 284eba4..27452b3 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -1,48 +1,49 @@ diff --git a/src/styles.css b/src/styles.css index 34a007a..5b56e68 100644 --- a/src/styles.css +++ b/src/styles.css @@ -110,5 +110,46 @@ button { } .path-input { - width: 30em; + width: 30em; +} + +.sidebar { + position: fixed; + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + align-items: center; + background-color: #f0f0f0; /* Make the background lighter */ + width: 60px; + padding: 10px 10px; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); + z-index: 10000; /* Ensure the sidebar is above other elements */ +} + +.nav-icon { + margin-bottom: 20px; +} + +.icon { + width: 30px; + height: 30px; +} + +main { + flex: 1; + padding-left: 20px; +} + +.sidebar .toggle-button { + background: none; + border: none; + cursor: pointer; + padding: 0; +} + +.sidebar .toggle-button img { + width: 30px; + height: 30px; + filter: invert(50%); /* Make the icon visible against the light background */ } From 546ce393099b1869add542234a1de915fdd7ef61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 12:47:59 +0200 Subject: [PATCH 08/21] move version string to the left panel --- src/App.css | 15 +++++++++------ src/App.vue | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/App.css b/src/App.css index b861823..c3cbb1b 100644 --- a/src/App.css +++ b/src/App.css @@ -1,12 +1,6 @@ .router-view { height: 78vh; } -.version { - position: fixed; - bottom: 10px; - right: 10px; - font-size: xx-small; -} .container { display: flex; @@ -48,6 +42,15 @@ filter: invert(50%); } +.version { + margin-top: auto; + font-size: xx-small; + text-align: center; + padding: 10px 0; + width: 100%; + border-top: 1px solid #ccc; +} + main { flex: 1; padding-left: 80px; diff --git a/src/App.vue b/src/App.vue index a949992..075a587 100644 --- a/src/App.vue +++ b/src/App.vue @@ -44,6 +44,7 @@ const toggleConsole = () => { +
Version: {{ appVersion }}
@@ -52,7 +53,6 @@ const toggleConsole = () => {
-
Version: {{ appVersion }}
From bc3ec8f131b582c3cee452e9ade59c9c07661b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 12:55:22 +0200 Subject: [PATCH 09/21] devportal - move css --- src/components/DeveloperPortalContribute.css | 8 ++++++++ src/components/DeveloperPortalContribute.vue | 16 ---------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/DeveloperPortalContribute.css b/src/components/DeveloperPortalContribute.css index a864ff8..cb38ec1 100644 --- a/src/components/DeveloperPortalContribute.css +++ b/src/components/DeveloperPortalContribute.css @@ -1,5 +1,6 @@ .developer-portal-contribute { padding: 20px; + text-align: left; } button { @@ -9,5 +10,12 @@ button { .author { border: 1px solid #ddd; padding: 10px; + margin-bottom: 20px; +} + + +input, textarea { + display: block; margin-bottom: 10px; + width: 100%; } diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 27452b3..056464d 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -118,19 +118,3 @@ const cancelEdit = () => { onMounted(checkDevPortal); - - From f1ae4884b034800218061bb5d8fd0ed4b3c6b537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 13:10:19 +0200 Subject: [PATCH 10/21] move git clone of repo to backend --- src-tauri/src/commands.rs | 17 +++++++++++++++++ src-tauri/src/main.rs | 1 + src/components/DeveloperPortalContribute.vue | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 579165f..035f403 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2,6 +2,7 @@ use async_std::sync::{Arc, Mutex}; use tauri::{State, AppHandle}; use crate::Author; use crate::external_command::run_external_command_with_progress; +use std::path::PathBuf; pub struct HugoState { pub is_running: Arc>, @@ -114,3 +115,19 @@ pub async fn delete_author(file_name: String) -> Result<(), String> { Ok(()) } + +#[tauri::command] +pub async fn clone_devportal_repo(app: AppHandle) -> Result { + // Expand the tilde to the full path + let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?; + let target_dir = home_dir.join(".espressif/devportal"); + + // Execute git clone command and stream output to frontend + let result = run_external_command_with_progress(app.clone(), "git", &["clone", "git@github.com:espressif/developer-portal.git", target_dir.to_str().unwrap()], "git-progress").await; + + if result.is_err() { + return Err("Failed to clone repository".to_string()); + } + + Ok("Repository cloned successfully".into()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 418eeb5..a5adba8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -433,6 +433,7 @@ fn main() { save_author, launch_hugo, restart_hugo, + clone_devportal_repo ]) .setup(|app| { // Initialize the logging system diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 056464d..2d577e8 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -58,7 +58,7 @@ const checkDevPortal = async () => { const cloneRepo = async () => { try { - await invoke('execute_command', { command: 'git clone git@github.com:espressif/developer-portal.git ~/.espressif/devportal' }); + await invoke('clone_devportal_repo'); checkDevPortal(); } catch (error) { console.error(error); From 941542110182fb42ae1fcf99da33753173b8c542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 13:23:19 +0200 Subject: [PATCH 11/21] devportal - use modal form for editing --- src/components/DeveloperPortalContribute.css | 39 +++++++++++++++++++- src/components/DeveloperPortalContribute.vue | 23 +++++++----- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/components/DeveloperPortalContribute.css b/src/components/DeveloperPortalContribute.css index cb38ec1..bf4d718 100644 --- a/src/components/DeveloperPortalContribute.css +++ b/src/components/DeveloperPortalContribute.css @@ -13,9 +13,46 @@ button { margin-bottom: 20px; } - input, textarea { display: block; margin-bottom: 10px; width: 100%; } + +/* Modal styles */ +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fff; + padding: 20px; + border: 1px solid #888; + width: 80%; + max-width: 500px; + text-align: left; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 2d577e8..4c5d516 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -2,8 +2,8 @@

Developer Portal

- +

Authors

@@ -21,15 +21,20 @@

Developer Portal directory is not present.

-
-

{{ editFormTitle }}

- - -
- + + +
From e47f10849246e26b222f3a6a98cdf65b18689f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 13:39:57 +0200 Subject: [PATCH 12/21] edit form populated with data --- src/components/DeveloperPortalContribute.css | 7 ++++++- src/components/DeveloperPortalContribute.vue | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/DeveloperPortalContribute.css b/src/components/DeveloperPortalContribute.css index bf4d718..2c0cdcb 100644 --- a/src/components/DeveloperPortalContribute.css +++ b/src/components/DeveloperPortalContribute.css @@ -16,7 +16,12 @@ button { input, textarea { display: block; margin-bottom: 10px; - width: 100%; + width: calc(100% - 20px); /* Make inputs slightly smaller than the modal */ + box-sizing: border-box; + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Ensure the same font is used */ + font-size: 14px; /* Ensure consistent font size */ + border-radius: 4px; /* Add rounded corners */ + padding: 5px; } /* Modal styles */ diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 4c5d516..d157e33 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -30,7 +30,7 @@
- +
@@ -95,7 +95,11 @@ const deleteAuthor = async (author: any) => { }; const editAuthor = (author: any) => { - editAuthorData.value = { ...author }; + editAuthorData.value = { + name: author.name, + bio: author.bio, + social: author.social.map((s: any) => ({ url: Object.values(s)[0] })) + }; editFormTitle.value = `Edit Author: ${author.name}`; showEditForm.value = true; }; From 377d0ab932692548698608c29016dd0979574b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 14:12:21 +0200 Subject: [PATCH 13/21] fix saving --- src/components/DeveloperPortalContribute.vue | 43 ++++++++++++++++---- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index d157e33..9f0e955 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -12,7 +12,7 @@ - +
@@ -29,9 +29,18 @@

{{ editFormTitle }}

-
+ +
@@ -49,6 +58,11 @@ const authors = ref([]); const showEditForm = ref(false); const editAuthorData = ref({ name: '', bio: '', social: [] }); const editFormTitle = ref(''); +let originalFileName = ''; + +const getFileName = (name: string) => { + return `${name.toLowerCase().replace(/ /g, '-')}.json`; +}; const checkDevPortal = async () => { try { @@ -86,7 +100,7 @@ const confirmDelete = (author: any) => { const deleteAuthor = async (author: any) => { try { - const fileName = `${author.name.toLowerCase().replace(/ /g, '-')}.json`; + const fileName = getFileName(author.name); await invoke('delete_author', { file_name: fileName }); checkDevPortal(); // Refresh the list of authors } catch (error) { @@ -94,26 +108,39 @@ const deleteAuthor = async (author: any) => { } }; -const editAuthor = (author: any) => { +const editAuthor = (author: any, fileName: string) => { editAuthorData.value = { name: author.name, bio: author.bio, - social: author.social.map((s: any) => ({ url: Object.values(s)[0] })) + social: author.social.map((s: any) => ({ key: Object.keys(s)[0], url: Object.values(s)[0] })) }; + originalFileName = fileName; editFormTitle.value = `Edit Author: ${author.name}`; showEditForm.value = true; }; const newAuthor = () => { editAuthorData.value = { name: '', bio: '', social: [] }; + originalFileName = ''; editFormTitle.value = 'New Author'; showEditForm.value = true; }; +const addSocialField = () => { + editAuthorData.value.social.push({ key: '', url: '' }); +}; + const saveAuthor = async () => { try { - const fileName = `${editAuthorData.value.name.toLowerCase().replace(/ /g, '-')}.json`; - await invoke('save_author', { author: editAuthorData.value, file_name: fileName }); + let fileName = originalFileName; + if (!fileName) { + fileName = getFileName(editAuthorData.value.name); + } + const social = editAuthorData.value.social.reduce((acc: any[], s: any) => { + acc.push({ [s.key]: s.url }); + return acc; + }, []); + await invoke('save_author', { author: { ...editAuthorData.value, social }, fileName: fileName }); showEditForm.value = false; checkDevPortal(); // Refresh the list of authors } catch (error) { @@ -127,3 +154,5 @@ const cancelEdit = () => { onMounted(checkDevPortal); + + From 715df4262070b8d9c9eda12b1098b7aacca62fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Mich=C3=A1lek?= Date: Thu, 30 May 2024 14:19:31 +0200 Subject: [PATCH 14/21] edit form allign --- src/components/DeveloperPortalContribute.css | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/components/DeveloperPortalContribute.css b/src/components/DeveloperPortalContribute.css index 2c0cdcb..d770dc6 100644 --- a/src/components/DeveloperPortalContribute.css +++ b/src/components/DeveloperPortalContribute.css @@ -61,3 +61,25 @@ input, textarea { text-decoration: none; cursor: pointer; } + +.social-input { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.social-input select { + margin-right: 10px; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + padding: 5px; + border-radius: 4px; +} + +.social-input input { + flex: 1; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + padding: 5px; + border-radius: 4px; +} From 4bd98a6992343296b43ead1f5ccb165f6ce8e5c5 Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Fri, 31 May 2024 07:22:45 +0200 Subject: [PATCH 15/21] split form into separate component --- src/components/AuthorForm.css | 70 ++++++++++++++++++++ src/components/AuthorForm.vue | 61 +++++++++++++++++ src/components/DeveloperPortalContribute.vue | 44 ++++-------- 3 files changed, 144 insertions(+), 31 deletions(-) create mode 100644 src/components/AuthorForm.css create mode 100644 src/components/AuthorForm.vue diff --git a/src/components/AuthorForm.css b/src/components/AuthorForm.css new file mode 100644 index 0000000..30601cb --- /dev/null +++ b/src/components/AuthorForm.css @@ -0,0 +1,70 @@ +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fff; + padding: 20px; + border: 1px solid #888; + width: 80%; + max-width: 500px; + text-align: left; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +input, +textarea { + display: block; + margin-bottom: 10px; + width: calc(100% - 20px); /* Make inputs slightly smaller than the modal */ + box-sizing: border-box; + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Ensure the same font is used */ + font-size: 14px; /* Ensure consistent font size */ + border-radius: 4px; /* Add rounded corners */ + padding: 5px; +} + +.social-input { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.social-input select { + margin-right: 10px; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + padding: 5px; + border-radius: 4px; +} + +.social-input input { + flex: 1; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + padding: 5px; + border-radius: 4px; +} diff --git a/src/components/AuthorForm.vue b/src/components/AuthorForm.vue new file mode 100644 index 0000000..3fe8733 --- /dev/null +++ b/src/components/AuthorForm.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 9f0e955..6c08ceb 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -22,29 +22,14 @@
- - + +
@@ -52,6 +37,7 @@ import { ref, onMounted } from 'vue'; import { invoke } from '@tauri-apps/api/tauri'; import './DeveloperPortalContribute.css'; +import AuthorForm from './AuthorForm.vue'; const isDevPortalPresent = ref(false); const authors = ref([]); @@ -126,21 +112,17 @@ const newAuthor = () => { showEditForm.value = true; }; -const addSocialField = () => { - editAuthorData.value.social.push({ key: '', url: '' }); -}; - -const saveAuthor = async () => { +const saveAuthor = async (authorData: any) => { try { let fileName = originalFileName; if (!fileName) { - fileName = getFileName(editAuthorData.value.name); + fileName = getFileName(authorData.name); } - const social = editAuthorData.value.social.reduce((acc: any[], s: any) => { + const social = authorData.social.reduce((acc: any[], s: any) => { acc.push({ [s.key]: s.url }); return acc; }, []); - await invoke('save_author', { author: { ...editAuthorData.value, social }, fileName: fileName }); + await invoke('save_author', { author: { ...authorData, social }, fileName: fileName }); showEditForm.value = false; checkDevPortal(); // Refresh the list of authors } catch (error) { From 670a48ae9dc0e4ab012f352851de41f9c750f52d Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Fri, 31 May 2024 11:18:24 +0200 Subject: [PATCH 16/21] developer portal add option to clone fork --- .../src/{commands.rs => developer_portal.rs} | 98 +++++++++++++------ src-tauri/src/main.rs | 19 ++-- src-tauri/src/settings.rs | 34 +++++++ src/App.vue | 10 +- src/assets/console-icon.svg | 6 -- src/assets/home-icon.svg | 4 - src/components/CloneRepository.css | 33 +++++++ src/components/CloneRepository.vue | 80 +++++++++++++++ src/components/DeveloperPortalContribute.vue | 29 ++++-- src/components/DeveloperPortalTile.css | 13 +++ src/components/DeveloperPortalTile.vue | 17 +--- src/components/Settings.css | 28 ++++++ src/components/Settings.vue | 43 ++++++++ src/icons/ConsoleIcon.vue | 7 ++ src/icons/HomeIcon.vue | 6 ++ src/icons/SettingsIcon.vue | 9 ++ src/router.ts | 7 +- 17 files changed, 361 insertions(+), 82 deletions(-) rename src-tauri/src/{commands.rs => developer_portal.rs} (50%) create mode 100644 src-tauri/src/settings.rs delete mode 100644 src/assets/console-icon.svg delete mode 100644 src/assets/home-icon.svg create mode 100644 src/components/CloneRepository.css create mode 100644 src/components/CloneRepository.vue create mode 100644 src/components/DeveloperPortalTile.css create mode 100644 src/components/Settings.css create mode 100644 src/components/Settings.vue create mode 100644 src/icons/ConsoleIcon.vue create mode 100644 src/icons/HomeIcon.vue create mode 100644 src/icons/SettingsIcon.vue diff --git a/src-tauri/src/commands.rs b/src-tauri/src/developer_portal.rs similarity index 50% rename from src-tauri/src/commands.rs rename to src-tauri/src/developer_portal.rs index 035f403..5410aba 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/developer_portal.rs @@ -1,5 +1,5 @@ use async_std::sync::{Arc, Mutex}; -use tauri::{State, AppHandle}; +use tauri::{AppHandle, State}; use crate::Author; use crate::external_command::run_external_command_with_progress; use std::path::PathBuf; @@ -8,20 +8,42 @@ pub struct HugoState { pub is_running: Arc>, } +const REPO_NAME: &str = "developer-portal"; +const BASE_DIR: &str = ".espressif"; +const AUTHORS_DIR: &str = "data/authors"; +const CONTENT_DIR: &str = "content/authors"; +const HUGO_COMMAND: &str = "hugo"; +const HUGO_ARGS: &[&str] = &["server", "--source"]; + +// Helper function to expand ~ to the user's home directory +fn expand_tilde(path: &str) -> PathBuf { + if path.starts_with("~/") { + if let Some(home_dir) = dirs::home_dir() { + return home_dir.join(path.trim_start_matches("~/")); + } + } + PathBuf::from(path) +} + +// Helper function to get default repo directory +fn get_default_repo_dir() -> PathBuf { + dirs::home_dir().unwrap().join(BASE_DIR).join(REPO_NAME) +} + #[tauri::command] -pub async fn launch_hugo(state: State<'_, HugoState>, app: AppHandle) -> Result { +pub async fn launch_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path: Option) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + { let mut is_running = state.is_running.lock().await; if *is_running { return Err("Hugo is already running".to_string()); } - - // Set Hugo state to running *is_running = true; - } // MutexGuard is dropped here + } - // Execute Hugo command and stream output to frontend - let result = run_external_command_with_progress(app.clone(), "hugo", &["server", "--source", "~/.espressif/devportal"], "hugo-progress").await; + let result = run_external_command_with_progress(app.clone(), HUGO_COMMAND, &[HUGO_ARGS[0], expanded_repo_dir.to_str().unwrap()], "hugo-progress").await; if result.is_err() { let mut is_running = state.is_running.lock().await; @@ -33,21 +55,22 @@ pub async fn launch_hugo(state: State<'_, HugoState>, app: AppHandle) -> Result< } #[tauri::command] -pub async fn restart_hugo(state: State<'_, HugoState>, app: AppHandle) -> Result { +pub async fn restart_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path: Option) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let relaunch: bool; { let mut is_running = state.is_running.lock().await; relaunch = *is_running; if relaunch { - // Send SIGTERM to Hugo process run_external_command_with_progress(app.clone(), "pkill", &["-f", "hugo server"], "hugo-progress").await.ok(); *is_running = false; } - } // MutexGuard is dropped here + } if relaunch { - // Relaunch Hugo - let result = run_external_command_with_progress(app.clone(), "hugo", &["server", "--source", "~/.espressif/devportal"], "hugo-progress").await; + let result = run_external_command_with_progress(app.clone(), HUGO_COMMAND, &[HUGO_ARGS[0], expanded_repo_dir.to_str().unwrap()], "hugo-progress").await; if result.is_err() { let mut is_running = state.is_running.lock().await; @@ -63,17 +86,17 @@ pub async fn restart_hugo(state: State<'_, HugoState>, app: AppHandle) -> Result } #[tauri::command] -pub async fn save_author(author: Author, file_name: String) -> Result<(), String> { - let authors_dir = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); - let content_dir = dirs::home_dir().unwrap().join(".espressif/devportal/content/authors"); +pub async fn save_author(author: Author, file_name: String, repo_path: Option) -> Result<(), String> { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); + let content_dir = expanded_repo_dir.join(CONTENT_DIR); let file_path = authors_dir.join(&file_name); let index_path = content_dir.join(file_name.replace(".json", "")).join("_index.md"); - // Create author JSON file std::fs::write(file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) .map_err(|e| e.to_string())?; - // Create _index.md file std::fs::create_dir_all(index_path.parent().unwrap()).map_err(|e| e.to_string())?; std::fs::write(index_path, &author.name).map_err(|e| e.to_string())?; @@ -81,8 +104,10 @@ pub async fn save_author(author: Author, file_name: String) -> Result<(), String } #[tauri::command] -pub async fn get_authors() -> Result, String> { - let authors_path = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); +pub async fn get_authors(repo_path: Option) -> Result, String> { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let authors_path = expanded_repo_dir.join(AUTHORS_DIR); let mut authors = Vec::new(); for entry in std::fs::read_dir(authors_path).map_err(|e| e.to_string())? { let entry = entry.map_err(|e| e.to_string())?; @@ -97,16 +122,16 @@ pub async fn get_authors() -> Result, String> { } #[tauri::command] -pub async fn delete_author(file_name: String) -> Result<(), String> { - let authors_dir = dirs::home_dir().unwrap().join(".espressif/devportal/data/authors"); - let content_dir = dirs::home_dir().unwrap().join(".espressif/devportal/content/authors"); +pub async fn delete_author(file_name: String, repo_path: Option) -> Result<(), String> { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); + let content_dir = expanded_repo_dir.join(CONTENT_DIR); let file_path = authors_dir.join(&file_name); let index_path = content_dir.join(file_name.replace(".json", "")).join("_index.md"); - // Remove author JSON file std::fs::remove_file(file_path).map_err(|e| e.to_string())?; - // Remove _index.md file and the directory if empty std::fs::remove_file(index_path.clone()).map_err(|e| e.to_string())?; let content_dir = index_path.parent().unwrap(); if std::fs::read_dir(content_dir).map_err(|e| e.to_string())?.next().is_none() { @@ -117,13 +142,16 @@ pub async fn delete_author(file_name: String) -> Result<(), String> { } #[tauri::command] -pub async fn clone_devportal_repo(app: AppHandle) -> Result { - // Expand the tilde to the full path - let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?; - let target_dir = home_dir.join(".espressif/devportal"); - - // Execute git clone command and stream output to frontend - let result = run_external_command_with_progress(app.clone(), "git", &["clone", "git@github.com:espressif/developer-portal.git", target_dir.to_str().unwrap()], "git-progress").await; +pub async fn clone_devportal_repo(repo_url: String, repo_path: Option, app: AppHandle) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let result = run_external_command_with_progress( + app, + "git", + &["clone", &repo_url, expanded_repo_dir.to_str().unwrap()], + "clone-progress", + ) + .await; if result.is_err() { return Err("Failed to clone repository".to_string()); @@ -131,3 +159,11 @@ pub async fn clone_devportal_repo(app: AppHandle) -> Result { Ok("Repository cloned successfully".into()) } + +#[tauri::command] +pub async fn check_devportal(repo_path: Option) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + + Ok(expanded_repo_dir.exists()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a5adba8..6dae753 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -28,11 +28,13 @@ use tauri::{State, Window}; use serialport::available_ports; use sysinfo::{DiskExt, System, SystemExt}; -mod commands; +mod developer_portal; mod models; +mod settings; -use commands::*; +use developer_portal::*; use models::*; +use settings::*; // Create a custom Error that we can return in Results #[derive(Debug, thiserror::Error)] @@ -396,15 +398,6 @@ async fn execute_command(command: String) -> Result { } } -#[tauri::command] -async fn check_devportal() -> Result { - // Implement the logic to check if the devportal directory exists - // Example: Check if ~/.espressif/devportal exists - Ok(std::path::Path::new(&dirs::home_dir().unwrap().join(".espressif/devportal")).exists()) -} - - - fn main() { tauri::Builder::default() .manage(Mutex::new(AppState::default())) @@ -433,7 +426,9 @@ fn main() { save_author, launch_hugo, restart_hugo, - clone_devportal_repo + clone_devportal_repo, + load_settings, + save_settings ]) .setup(|app| { // Initialize the logging system diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..c0169fd --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use tauri::{AppHandle}; + +#[derive(Serialize, Deserialize)] +pub struct Settings { + github_username: Option, + developer_portal_repo_path: Option, +} + +const SETTINGS_PATH: &str = ".espressif/esp-workbench.json"; + +#[tauri::command] +pub async fn load_settings(app: AppHandle) -> Result { + let path = dirs::home_dir().unwrap().join(SETTINGS_PATH); + if path.exists() { + let settings = fs::read_to_string(path).map_err(|e| e.to_string())?; + let settings: Settings = serde_json::from_str(&settings).map_err(|e| e.to_string())?; + Ok(settings) + } else { + Ok(Settings { github_username: None, developer_portal_repo_path: None }) + } +} + +#[tauri::command] +pub async fn save_settings(github_username: Option, developer_portal_repo_path: Option, app: AppHandle) -> Result<(), String> { + let path = dirs::home_dir().unwrap().join(SETTINGS_PATH); + let settings = Settings { + github_username, + developer_portal_repo_path, + }; + fs::write(path, serde_json::to_string(&settings).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src/App.vue b/src/App.vue index 075a587..271eb7e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,10 +2,11 @@ import { onMounted, ref } from "vue"; import { getVersion } from '@tauri-apps/api/app'; import { appWindow } from '@tauri-apps/api/window'; -import HomeIcon from "./components/HomeIcon.vue"; +import HomeIcon from "./icons/HomeIcon.vue"; +import ConsoleIcon from './icons/ConsoleIcon.vue'; +import SettingsIcon from './icons/SettingsIcon.vue'; import Console from "./components/Console.vue"; import ErrorMessage from './components/ErrorMessage.vue'; -import ConsoleIcon from './assets/console-icon.svg'; const appVersion = ref(''); const errorMessage = ref(""); @@ -41,8 +42,11 @@ const toggleConsole = () => { + + +
Version: {{ appVersion }}
diff --git a/src/assets/console-icon.svg b/src/assets/console-icon.svg deleted file mode 100644 index 052621f..0000000 --- a/src/assets/console-icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/assets/home-icon.svg b/src/assets/home-icon.svg deleted file mode 100644 index 6524403..0000000 --- a/src/assets/home-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/components/CloneRepository.css b/src/components/CloneRepository.css new file mode 100644 index 0000000..5b4fad8 --- /dev/null +++ b/src/components/CloneRepository.css @@ -0,0 +1,33 @@ +.clone-repository { + margin: 20px 0; +} + +.repo-option { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.repo-option input { + flex: 1; + margin-right: 10px; + padding: 5px; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + border-radius: 4px; + border: 1px solid #ccc; +} + +.repo-option button { + padding: 5px 10px; + font-size: 14px; + border-radius: 4px; + border: none; + cursor: pointer; + background-color: #007bff; + color: white; +} + +.repo-option button:hover { + background-color: #0056b3; +} diff --git a/src/components/CloneRepository.vue b/src/components/CloneRepository.vue new file mode 100644 index 0000000..9613837 --- /dev/null +++ b/src/components/CloneRepository.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 6c08ceb..5f32f61 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -19,7 +19,10 @@

Developer Portal directory is not present.

- +
@@ -38,6 +41,7 @@ import { ref, onMounted } from 'vue'; import { invoke } from '@tauri-apps/api/tauri'; import './DeveloperPortalContribute.css'; import AuthorForm from './AuthorForm.vue'; +import CloneRepository from './CloneRepository.vue'; const isDevPortalPresent = ref(false); const authors = ref([]); @@ -45,6 +49,8 @@ const showEditForm = ref(false); const editAuthorData = ref({ name: '', bio: '', social: [] }); const editFormTitle = ref(''); let originalFileName = ''; +let githubUsername = ref(''); +let repoPath = ref(''); const getFileName = (name: string) => { return `${name.toLowerCase().replace(/ /g, '-')}.json`; @@ -54,17 +60,18 @@ const checkDevPortal = async () => { try { isDevPortalPresent.value = await invoke('check_devportal'); if (isDevPortalPresent.value) { - authors.value = await invoke('get_authors'); + authors.value = await invoke('get_authors', { repoPath: repoPath.value }); } } catch (error) { console.error(error); } }; -const cloneRepo = async () => { +const loadSettings = async () => { try { - await invoke('clone_devportal_repo'); - checkDevPortal(); + const settings = await invoke('load_settings'); + githubUsername.value = settings.github_username || ''; + repoPath.value = settings.developer_portal_repo_path || '~/.espressif/developer-portal'; } catch (error) { console.error(error); } @@ -72,7 +79,7 @@ const cloneRepo = async () => { const launchHugo = async () => { try { - await invoke('launch_hugo'); + await invoke('launch_hugo', { repoPath: repoPath.value }); } catch (error) { console.error(error); } @@ -87,7 +94,7 @@ const confirmDelete = (author: any) => { const deleteAuthor = async (author: any) => { try { const fileName = getFileName(author.name); - await invoke('delete_author', { file_name: fileName }); + await invoke('delete_author', { fileName: fileName, repoPath: repoPath.value }); checkDevPortal(); // Refresh the list of authors } catch (error) { console.error(error); @@ -122,7 +129,7 @@ const saveAuthor = async (authorData: any) => { acc.push({ [s.key]: s.url }); return acc; }, []); - await invoke('save_author', { author: { ...authorData, social }, fileName: fileName }); + await invoke('save_author', { author: { ...authorData, social }, fileName: fileName, repoPath: repoPath.value }); showEditForm.value = false; checkDevPortal(); // Refresh the list of authors } catch (error) { @@ -134,7 +141,11 @@ const cancelEdit = () => { showEditForm.value = false; }; -onMounted(checkDevPortal); +onMounted(async () => { + await loadSettings(); + await checkDevPortal(); +}); + diff --git a/src/components/DeveloperPortalTile.css b/src/components/DeveloperPortalTile.css new file mode 100644 index 0000000..7efde22 --- /dev/null +++ b/src/components/DeveloperPortalTile.css @@ -0,0 +1,13 @@ +.add-button { + display: inline-block; + padding: 10px 20px; + color: #fff; + background-color: #007bff; + border-radius: 5px; + text-decoration: none; + transition: background-color 0.2s ease; +} + +.add-button:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/src/components/DeveloperPortalTile.vue b/src/components/DeveloperPortalTile.vue index bc08c41..dd9f696 100644 --- a/src/components/DeveloperPortalTile.vue +++ b/src/components/DeveloperPortalTile.vue @@ -7,20 +7,5 @@ - - diff --git a/src/components/Settings.css b/src/components/Settings.css new file mode 100644 index 0000000..ff0b74d --- /dev/null +++ b/src/components/Settings.css @@ -0,0 +1,28 @@ +.settings { + padding: 20px; +} + +label { + display: block; + margin-top: 10px; +} + +input { + width: 100%; + padding: 5px; + margin-top: 5px; +} + +button { + margin-top: 20px; + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +button:hover { + background-color: #0056b3; +} diff --git a/src/components/Settings.vue b/src/components/Settings.vue new file mode 100644 index 0000000..33cd2f8 --- /dev/null +++ b/src/components/Settings.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/src/icons/ConsoleIcon.vue b/src/icons/ConsoleIcon.vue new file mode 100644 index 0000000..850f07d --- /dev/null +++ b/src/icons/ConsoleIcon.vue @@ -0,0 +1,7 @@ + diff --git a/src/icons/HomeIcon.vue b/src/icons/HomeIcon.vue new file mode 100644 index 0000000..164c298 --- /dev/null +++ b/src/icons/HomeIcon.vue @@ -0,0 +1,6 @@ + diff --git a/src/icons/SettingsIcon.vue b/src/icons/SettingsIcon.vue new file mode 100644 index 0000000..50f9106 --- /dev/null +++ b/src/icons/SettingsIcon.vue @@ -0,0 +1,9 @@ + diff --git a/src/router.ts b/src/router.ts index d8e0003..6369181 100644 --- a/src/router.ts +++ b/src/router.ts @@ -42,7 +42,12 @@ const routes: Array = [ path: "/developer-portal/contribute", name: "DeveloperPortalContribute", component: () => import("./components/DeveloperPortalContribute.vue"), - } + }, + { + path: "/settings", + name: "Settings", + component: () => import("./components/Settings.vue"), + }, ]; const router = createRouter({ From 272ec421397a46be31d59f88a3433e03a49a8628 Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Fri, 31 May 2024 11:53:47 +0200 Subject: [PATCH 17/21] fix cloning forked repo --- src/components/CloneRepository.vue | 6 ++++-- src/components/DeveloperPortalContribute.vue | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/CloneRepository.vue b/src/components/CloneRepository.vue index 9613837..5403b65 100644 --- a/src/components/CloneRepository.vue +++ b/src/components/CloneRepository.vue @@ -29,7 +29,7 @@ From 3b3c4cd4e98415b93120f68bd6a8024427aff662 Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Fri, 31 May 2024 13:40:29 +0200 Subject: [PATCH 18/21] remove special characters from author's file name --- src-tauri/Cargo.toml | 2 ++ src-tauri/src/developer_portal.rs | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 04088c1..e5e37b3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,6 +37,8 @@ walkdir = "2.3.3" sysinfo = "0.29.7" serialport = { version = "4.2.1" } espflash = "2.0.1" +regex = "1.10.4" +deunicode = "1.6.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/developer_portal.rs b/src-tauri/src/developer_portal.rs index 5410aba..71fe464 100644 --- a/src-tauri/src/developer_portal.rs +++ b/src-tauri/src/developer_portal.rs @@ -3,6 +3,8 @@ use tauri::{AppHandle, State}; use crate::Author; use crate::external_command::run_external_command_with_progress; use std::path::PathBuf; +use regex::Regex; +use deunicode::deunicode; pub struct HugoState { pub is_running: Arc>, @@ -85,14 +87,23 @@ pub async fn restart_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path Ok("Hugo server restarted".to_string()) } +// Helper function to normalize and create file name +fn create_file_name(name: &str) -> String { + let re = Regex::new(r"[^a-zA-Z0-9-]").unwrap(); + let normalized_name = deunicode(name).to_lowercase().replace(" ", "-"); + re.replace_all(&normalized_name, "").to_string() +} + #[tauri::command] -pub async fn save_author(author: Author, file_name: String, repo_path: Option) -> Result<(), String> { +pub async fn save_author(author: Author, original_file_name: Option, repo_path: Option) -> Result<(), String> { let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); let expanded_repo_dir = expand_tilde(&repo_dir); let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); let content_dir = expanded_repo_dir.join(CONTENT_DIR); - let file_path = authors_dir.join(&file_name); - let index_path = content_dir.join(file_name.replace(".json", "")).join("_index.md"); + + let file_name = original_file_name.unwrap_or_else(|| create_file_name(&author.name)); + let file_path = authors_dir.join(format!("{}.json", file_name)); + let index_path = content_dir.join(file_name).join("_index.md"); std::fs::write(file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) .map_err(|e| e.to_string())?; From bad22305ed5951bd1ef4e07cd96f1766fa5d324c Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Mon, 3 Jun 2024 15:34:57 +0200 Subject: [PATCH 19/21] refactor developer_portal into smaller modules --- src-tauri/src/developer_portal.rs | 180 ---------------------- src-tauri/src/developer_portal/authors.rs | 85 ++++++++++ src-tauri/src/developer_portal/hugo.rs | 79 ++++++++++ src-tauri/src/developer_portal/mod.rs | 8 + src-tauri/src/developer_portal/repo.rs | 43 ++++++ 5 files changed, 215 insertions(+), 180 deletions(-) delete mode 100644 src-tauri/src/developer_portal.rs create mode 100644 src-tauri/src/developer_portal/authors.rs create mode 100644 src-tauri/src/developer_portal/hugo.rs create mode 100644 src-tauri/src/developer_portal/mod.rs create mode 100644 src-tauri/src/developer_portal/repo.rs diff --git a/src-tauri/src/developer_portal.rs b/src-tauri/src/developer_portal.rs deleted file mode 100644 index 71fe464..0000000 --- a/src-tauri/src/developer_portal.rs +++ /dev/null @@ -1,180 +0,0 @@ -use async_std::sync::{Arc, Mutex}; -use tauri::{AppHandle, State}; -use crate::Author; -use crate::external_command::run_external_command_with_progress; -use std::path::PathBuf; -use regex::Regex; -use deunicode::deunicode; - -pub struct HugoState { - pub is_running: Arc>, -} - -const REPO_NAME: &str = "developer-portal"; -const BASE_DIR: &str = ".espressif"; -const AUTHORS_DIR: &str = "data/authors"; -const CONTENT_DIR: &str = "content/authors"; -const HUGO_COMMAND: &str = "hugo"; -const HUGO_ARGS: &[&str] = &["server", "--source"]; - -// Helper function to expand ~ to the user's home directory -fn expand_tilde(path: &str) -> PathBuf { - if path.starts_with("~/") { - if let Some(home_dir) = dirs::home_dir() { - return home_dir.join(path.trim_start_matches("~/")); - } - } - PathBuf::from(path) -} - -// Helper function to get default repo directory -fn get_default_repo_dir() -> PathBuf { - dirs::home_dir().unwrap().join(BASE_DIR).join(REPO_NAME) -} - -#[tauri::command] -pub async fn launch_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path: Option) -> Result { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - - { - let mut is_running = state.is_running.lock().await; - if *is_running { - return Err("Hugo is already running".to_string()); - } - *is_running = true; - } - - let result = run_external_command_with_progress(app.clone(), HUGO_COMMAND, &[HUGO_ARGS[0], expanded_repo_dir.to_str().unwrap()], "hugo-progress").await; - - if result.is_err() { - let mut is_running = state.is_running.lock().await; - *is_running = false; - return Err("Failed to start Hugo server".to_string()); - } - - Ok("Hugo server started".into()) -} - -#[tauri::command] -pub async fn restart_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path: Option) -> Result { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - - let relaunch: bool; - { - let mut is_running = state.is_running.lock().await; - relaunch = *is_running; - if relaunch { - run_external_command_with_progress(app.clone(), "pkill", &["-f", "hugo server"], "hugo-progress").await.ok(); - *is_running = false; - } - } - - if relaunch { - let result = run_external_command_with_progress(app.clone(), HUGO_COMMAND, &[HUGO_ARGS[0], expanded_repo_dir.to_str().unwrap()], "hugo-progress").await; - - if result.is_err() { - let mut is_running = state.is_running.lock().await; - *is_running = false; - return Err("Failed to restart Hugo server".to_string()); - } - - let mut is_running = state.is_running.lock().await; - *is_running = true; - } - - Ok("Hugo server restarted".to_string()) -} - -// Helper function to normalize and create file name -fn create_file_name(name: &str) -> String { - let re = Regex::new(r"[^a-zA-Z0-9-]").unwrap(); - let normalized_name = deunicode(name).to_lowercase().replace(" ", "-"); - re.replace_all(&normalized_name, "").to_string() -} - -#[tauri::command] -pub async fn save_author(author: Author, original_file_name: Option, repo_path: Option) -> Result<(), String> { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); - let content_dir = expanded_repo_dir.join(CONTENT_DIR); - - let file_name = original_file_name.unwrap_or_else(|| create_file_name(&author.name)); - let file_path = authors_dir.join(format!("{}.json", file_name)); - let index_path = content_dir.join(file_name).join("_index.md"); - - std::fs::write(file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - - std::fs::create_dir_all(index_path.parent().unwrap()).map_err(|e| e.to_string())?; - std::fs::write(index_path, &author.name).map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn get_authors(repo_path: Option) -> Result, String> { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - let authors_path = expanded_repo_dir.join(AUTHORS_DIR); - let mut authors = Vec::new(); - for entry in std::fs::read_dir(authors_path).map_err(|e| e.to_string())? { - let entry = entry.map_err(|e| e.to_string())?; - let path = entry.path(); - if path.is_file() { - let author: Author = serde_json::from_reader(std::fs::File::open(path).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - authors.push(author); - } - } - Ok(authors) -} - -#[tauri::command] -pub async fn delete_author(file_name: String, repo_path: Option) -> Result<(), String> { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); - let content_dir = expanded_repo_dir.join(CONTENT_DIR); - let file_path = authors_dir.join(&file_name); - let index_path = content_dir.join(file_name.replace(".json", "")).join("_index.md"); - - std::fs::remove_file(file_path).map_err(|e| e.to_string())?; - - std::fs::remove_file(index_path.clone()).map_err(|e| e.to_string())?; - let content_dir = index_path.parent().unwrap(); - if std::fs::read_dir(content_dir).map_err(|e| e.to_string())?.next().is_none() { - std::fs::remove_dir(content_dir).map_err(|e| e.to_string())?; - } - - Ok(()) -} - -#[tauri::command] -pub async fn clone_devportal_repo(repo_url: String, repo_path: Option, app: AppHandle) -> Result { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - let result = run_external_command_with_progress( - app, - "git", - &["clone", &repo_url, expanded_repo_dir.to_str().unwrap()], - "clone-progress", - ) - .await; - - if result.is_err() { - return Err("Failed to clone repository".to_string()); - } - - Ok("Repository cloned successfully".into()) -} - -#[tauri::command] -pub async fn check_devportal(repo_path: Option) -> Result { - let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); - let expanded_repo_dir = expand_tilde(&repo_dir); - - Ok(expanded_repo_dir.exists()) -} diff --git a/src-tauri/src/developer_portal/authors.rs b/src-tauri/src/developer_portal/authors.rs new file mode 100644 index 0000000..89fe2a1 --- /dev/null +++ b/src-tauri/src/developer_portal/authors.rs @@ -0,0 +1,85 @@ +use tauri::AppHandle; +use crate::Author; +use std::path::PathBuf; +use regex::Regex; +use deunicode::deunicode; + +const AUTHORS_DIR: &str = "data/authors"; +const CONTENT_DIR: &str = "content/authors"; + +fn expand_tilde(path: &str) -> PathBuf { + if path.starts_with("~/") { + if let Some(home_dir) = dirs::home_dir() { + return home_dir.join(path.trim_start_matches("~/")); + } + } + PathBuf::from(path) +} + +fn get_default_repo_dir() -> PathBuf { + dirs::home_dir().unwrap().join(".espressif").join("developer-portal") +} + +fn create_file_name(name: &str) -> String { + let re = Regex::new(r"[^a-zA-Z0-9-]").unwrap(); + let normalized_name = deunicode(name).to_lowercase().replace(" ", "-"); + re.replace_all(&normalized_name, "").to_string() +} + +#[tauri::command] +pub async fn save_author(author: Author, original_file_name: Option, repo_path: Option) -> Result<(), String> { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); + let content_dir = expanded_repo_dir.join(CONTENT_DIR); + + let file_name = original_file_name.unwrap_or_else(|| create_file_name(&author.name)); + let file_path = authors_dir.join(format!("{}.json", file_name)); + let index_path = content_dir.join(file_name).join("_index.md"); + + std::fs::write(file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + + std::fs::create_dir_all(index_path.parent().unwrap()).map_err(|e| e.to_string())?; + std::fs::write(index_path, &author.name).map_err(|e| e.to_string())?; + + Ok(()) +} + +#[tauri::command] +pub async fn get_authors(repo_path: Option) -> Result, String> { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let authors_path = expanded_repo_dir.join(AUTHORS_DIR); + let mut authors = Vec::new(); + for entry in std::fs::read_dir(authors_path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.is_file() { + let author: Author = serde_json::from_reader(std::fs::File::open(path).map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?; + authors.push(author); + } + } + Ok(authors) +} + +#[tauri::command] +pub async fn delete_author(file_name: String, repo_path: Option) -> Result<(), String> { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let authors_dir = expanded_repo_dir.join(AUTHORS_DIR); + let content_dir = expanded_repo_dir.join(CONTENT_DIR); + let file_path = authors_dir.join(&file_name); + let index_path = content_dir.join(file_name.replace(".json", "")).join("_index.md"); + + std::fs::remove_file(file_path).map_err(|e| e.to_string())?; + + std::fs::remove_file(index_path.clone()).map_err(|e| e.to_string())?; + let content_dir = index_path.parent().unwrap(); + if std::fs::read_dir(content_dir).map_err(|e| e.to_string())?.next().is_none() { + std::fs::remove_dir(content_dir).map_err(|e| e.to_string())?; + } + + Ok(()) +} diff --git a/src-tauri/src/developer_portal/hugo.rs b/src-tauri/src/developer_portal/hugo.rs new file mode 100644 index 0000000..5900668 --- /dev/null +++ b/src-tauri/src/developer_portal/hugo.rs @@ -0,0 +1,79 @@ +use async_std::sync::{Arc, Mutex}; +use tauri::{AppHandle, State}; +use crate::external_command::run_external_command_with_progress; +use std::path::PathBuf; + +pub struct HugoState { + pub is_running: Arc>, +} + +const HUGO_COMMAND: &str = "hugo"; +const HUGO_ARGS: &[&str] = &["server", "--source"]; + +fn expand_tilde(path: &str) -> PathBuf { + if path.starts_with("~/") { + if let Some(home_dir) = dirs::home_dir() { + return home_dir.join(path.trim_start_matches("~/")); + } + } + PathBuf::from(path) +} + +fn get_default_repo_dir() -> PathBuf { + dirs::home_dir().unwrap().join(".espressif").join("developer-portal") +} + +#[tauri::command] +pub async fn launch_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path: Option) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + + { + let mut is_running = state.is_running.lock().await; + if *is_running { + return Err("Hugo is already running".to_string()); + } + *is_running = true; + } + + let result = run_external_command_with_progress(app.clone(), HUGO_COMMAND, &[HUGO_ARGS[0], expanded_repo_dir.to_str().unwrap()], "hugo-progress").await; + + if result.is_err() { + let mut is_running = state.is_running.lock().await; + *is_running = false; + return Err("Failed to start Hugo server".to_string()); + } + + Ok("Hugo server started".into()) +} + +#[tauri::command] +pub async fn restart_hugo(state: State<'_, HugoState>, app: AppHandle, repo_path: Option) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + + let relaunch: bool; + { + let mut is_running = state.is_running.lock().await; + relaunch = *is_running; + if relaunch { + run_external_command_with_progress(app.clone(), "pkill", &["-f", "hugo server"], "hugo-progress").await.ok(); + *is_running = false; + } + } + + if relaunch { + let result = run_external_command_with_progress(app.clone(), HUGO_COMMAND, &[HUGO_ARGS[0], expanded_repo_dir.to_str().unwrap()], "hugo-progress").await; + + if result.is_err() { + let mut is_running = state.is_running.lock().await; + *is_running = false; + return Err("Failed to restart Hugo server".to_string()); + } + + let mut is_running = state.is_running.lock().await; + *is_running = true; + } + + Ok("Hugo server restarted".to_string()) +} diff --git a/src-tauri/src/developer_portal/mod.rs b/src-tauri/src/developer_portal/mod.rs new file mode 100644 index 0000000..e6ec950 --- /dev/null +++ b/src-tauri/src/developer_portal/mod.rs @@ -0,0 +1,8 @@ +pub mod hugo; +pub mod repo; +pub mod authors; + +// Re-exporting commands for easy access +pub use hugo::*; +pub use repo::*; +pub use authors::*; diff --git a/src-tauri/src/developer_portal/repo.rs b/src-tauri/src/developer_portal/repo.rs new file mode 100644 index 0000000..f0258d6 --- /dev/null +++ b/src-tauri/src/developer_portal/repo.rs @@ -0,0 +1,43 @@ +use tauri::AppHandle; +use crate::external_command::run_external_command_with_progress; +use std::path::PathBuf; + +fn expand_tilde(path: &str) -> PathBuf { + if path.starts_with("~/") { + if let Some(home_dir) = dirs::home_dir() { + return home_dir.join(path.trim_start_matches("~/")); + } + } + PathBuf::from(path) +} + +fn get_default_repo_dir() -> PathBuf { + dirs::home_dir().unwrap().join(".espressif").join("developer-portal") +} + +#[tauri::command] +pub async fn clone_devportal_repo(repo_url: String, repo_path: Option, app: AppHandle) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + let result = run_external_command_with_progress( + app, + "git", + &["clone", &repo_url, expanded_repo_dir.to_str().unwrap()], + "clone-progress", + ) + .await; + + if result.is_err() { + return Err("Failed to clone repository".to_string()); + } + + Ok("Repository cloned successfully".into()) +} + +#[tauri::command] +pub async fn check_devportal(repo_path: Option) -> Result { + let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); + let expanded_repo_dir = expand_tilde(&repo_dir); + + Ok(expanded_repo_dir.exists()) +} From 61256956b17141fa7a1607565f01462e2007ceaf Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Mon, 3 Jun 2024 15:45:37 +0200 Subject: [PATCH 20/21] doc: save author empty page --- src-tauri/src/developer_portal/authors.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/developer_portal/authors.rs b/src-tauri/src/developer_portal/authors.rs index 89fe2a1..d97fbb1 100644 --- a/src-tauri/src/developer_portal/authors.rs +++ b/src-tauri/src/developer_portal/authors.rs @@ -26,6 +26,13 @@ fn create_file_name(name: &str) -> String { re.replace_all(&normalized_name, "").to_string() } +fn create_author_page_content(name: &str) -> String { + format!( + "---\ntitle: \"{}\"\n---\n", + name + ) +} + #[tauri::command] pub async fn save_author(author: Author, original_file_name: Option, repo_path: Option) -> Result<(), String> { let repo_dir = repo_path.unwrap_or_else(|| get_default_repo_dir().to_str().unwrap().to_string()); @@ -35,13 +42,13 @@ pub async fn save_author(author: Author, original_file_name: Option, rep let file_name = original_file_name.unwrap_or_else(|| create_file_name(&author.name)); let file_path = authors_dir.join(format!("{}.json", file_name)); - let index_path = content_dir.join(file_name).join("_index.md"); + let index_path = content_dir.join(&file_name).join("_index.md"); - std::fs::write(file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) + std::fs::write(&file_path, serde_json::to_string_pretty(&author).map_err(|e| e.to_string())?) .map_err(|e| e.to_string())?; std::fs::create_dir_all(index_path.parent().unwrap()).map_err(|e| e.to_string())?; - std::fs::write(index_path, &author.name).map_err(|e| e.to_string())?; + std::fs::write(&index_path, create_author_page_content(&author.name)).map_err(|e| e.to_string())?; Ok(()) } From 8e41941ccc7ea8e2b7500f3879ef841d74cda4e0 Mon Sep 17 00:00:00 2001 From: Juraj Michalek Date: Mon, 3 Jun 2024 16:35:27 +0200 Subject: [PATCH 21/21] add article editor --- src-tauri/src/developer_portal/articles.rs | 92 +++++++++++++++ src-tauri/src/developer_portal/mod.rs | 2 + src-tauri/src/main.rs | 5 +- src/components/ArticleForm.css | 49 ++++++++ src/components/ArticleForm.vue | 52 ++++++++ src/components/ArticleList.css | 54 +++++++++ src/components/ArticleList.vue | 95 +++++++++++++++ src/components/AuthorList.css | 76 ++++++++++++ src/components/AuthorList.vue | 118 +++++++++++++++++++ src/components/DeveloperPortalContribute.css | 77 ------------ src/components/DeveloperPortalContribute.vue | 99 +--------------- 11 files changed, 548 insertions(+), 171 deletions(-) create mode 100644 src-tauri/src/developer_portal/articles.rs create mode 100644 src/components/ArticleForm.css create mode 100644 src/components/ArticleForm.vue create mode 100644 src/components/ArticleList.css create mode 100644 src/components/ArticleList.vue create mode 100644 src/components/AuthorList.css create mode 100644 src/components/AuthorList.vue diff --git a/src-tauri/src/developer_portal/articles.rs b/src-tauri/src/developer_portal/articles.rs new file mode 100644 index 0000000..e821543 --- /dev/null +++ b/src-tauri/src/developer_portal/articles.rs @@ -0,0 +1,92 @@ +use tauri::AppHandle; +use std::path::PathBuf; +use regex::Regex; +use deunicode::deunicode; + +const ARTICLES_DIR: &str = "content/articles"; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Article { + title: String, + content: String, + date: String, + draft: bool, + file_name: String, +} + +#[tauri::command] +pub async fn get_articles(repo_path: String) -> Result, String> { + let path = PathBuf::from(repo_path).join(ARTICLES_DIR); + let mut articles = Vec::new(); + + for entry in std::fs::read_dir(path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + if path.join("_index.md").exists() { + let content = std::fs::read_to_string(path.join("_index.md")).map_err(|e| e.to_string())?; + let article = parse_article(&content, path.file_name().unwrap().to_str().unwrap())?; + articles.push(article); + } + } + Ok(articles) +} + +#[tauri::command] +pub async fn save_article(article: Article, repo_path: String) -> Result<(), String> { + let normalized_file_name = create_file_name(&article.title); + let path = PathBuf::from(repo_path).join(ARTICLES_DIR).join(&normalized_file_name).join("_index.md"); + let content = format!( + r#"+++ +title = '{}' +date = '{}' +draft = {} ++++ +{} +"#, + article.title, article.date, article.draft, article.content + ); + std::fs::create_dir_all(path.parent().unwrap()).map_err(|e| e.to_string())?; + std::fs::write(path, content).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn delete_article(file_name: String, repo_path: String) -> Result<(), String> { + let path = PathBuf::from(repo_path).join(ARTICLES_DIR).join(&file_name); + std::fs::remove_dir_all(path).map_err(|e| e.to_string())?; + Ok(()) +} + +fn parse_article(content: &str, file_name: &str) -> Result { + let re = Regex::new(r"(?s)\+\+\+(?P.+?)\+\+\+(?P.*)").map_err(|e| e.to_string())?; + let caps = re.captures(content).ok_or("Invalid article format")?; + let meta = caps.name("meta").ok_or("Invalid article format")?.as_str(); + let body = caps.name("body").map_or("", |m| m.as_str()).trim().to_string(); + let mut article = Article { + title: String::new(), + content: body, + date: String::new(), + draft: false, + file_name: file_name.to_string(), + }; + + for line in meta.lines() { + if let Some((key, value)) = line.split_once('=') { + let key = key.trim(); + let value = value.trim().trim_matches(|c| c == '"' || c == '\''); + match key { + "title" => article.title = value.to_string(), + "date" => article.date = value.to_string(), + "draft" => article.draft = value.parse::().map_err(|e| e.to_string())?, + _ => {} + } + } + } + Ok(article) +} + +fn create_file_name(name: &str) -> String { + let re = Regex::new(r"[^a-zA-Z0-9-]").unwrap(); + let normalized_name = deunicode(name).to_lowercase().replace(" ", "-"); + re.replace_all(&normalized_name, "").to_string() +} diff --git a/src-tauri/src/developer_portal/mod.rs b/src-tauri/src/developer_portal/mod.rs index e6ec950..17bbe63 100644 --- a/src-tauri/src/developer_portal/mod.rs +++ b/src-tauri/src/developer_portal/mod.rs @@ -1,8 +1,10 @@ pub mod hugo; pub mod repo; +pub mod articles; pub mod authors; // Re-exporting commands for easy access pub use hugo::*; pub use repo::*; +pub use articles::*; pub use authors::*; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6dae753..cd5e2e5 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -428,7 +428,10 @@ fn main() { restart_hugo, clone_devportal_repo, load_settings, - save_settings + save_settings, + get_articles, + save_article, + delete_article ]) .setup(|app| { // Initialize the logging system diff --git a/src/components/ArticleForm.css b/src/components/ArticleForm.css new file mode 100644 index 0000000..5615b1f --- /dev/null +++ b/src/components/ArticleForm.css @@ -0,0 +1,49 @@ +/* Common styles for form components */ + +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fff; + padding: 20px; + border: 1px solid #888; + width: 80%; + max-width: 500px; + text-align: left; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +input, textarea { + display: block; + margin-bottom: 10px; + width: calc(100% - 20px); /* Make inputs slightly smaller than the modal */ + box-sizing: border-box; + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Ensure the same font is used */ + font-size: 14px; /* Ensure consistent font size */ + border-radius: 4px; /* Add rounded corners */ + padding: 5px; +} diff --git a/src/components/ArticleForm.vue b/src/components/ArticleForm.vue new file mode 100644 index 0000000..6d39873 --- /dev/null +++ b/src/components/ArticleForm.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/src/components/ArticleList.css b/src/components/ArticleList.css new file mode 100644 index 0000000..559aff9 --- /dev/null +++ b/src/components/ArticleList.css @@ -0,0 +1,54 @@ +.article { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 20px; +} + +input, textarea { + display: block; + margin-bottom: 10px; + width: calc(100% - 20px); /* Make inputs slightly smaller than the modal */ + box-sizing: border-box; + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Ensure the same font is used */ + font-size: 14px; /* Ensure consistent font size */ + border-radius: 4px; /* Add rounded corners */ + padding: 5px; +} + +/* Modal styles */ +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fff; + padding: 20px; + border: 1px solid #888; + width: 80%; + max-width: 500px; + text-align: left; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} diff --git a/src/components/ArticleList.vue b/src/components/ArticleList.vue new file mode 100644 index 0000000..9c214fa --- /dev/null +++ b/src/components/ArticleList.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/src/components/AuthorList.css b/src/components/AuthorList.css new file mode 100644 index 0000000..e909427 --- /dev/null +++ b/src/components/AuthorList.css @@ -0,0 +1,76 @@ +.author { + border: 1px solid #ddd; + padding: 10px; + margin-bottom: 20px; +} + +input, textarea { + display: block; + margin-bottom: 10px; + width: calc(100% - 20px); /* Make inputs slightly smaller than the modal */ + box-sizing: border-box; + font-family: 'Consolas', 'Courier New', Courier, monospace; /* Ensure the same font is used */ + font-size: 14px; /* Ensure consistent font size */ + border-radius: 4px; /* Add rounded corners */ + padding: 5px; +} + +/* Modal styles */ +.modal { + display: flex; + justify-content: center; + align-items: center; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.4); +} + +.modal-content { + background-color: #fff; + padding: 20px; + border: 1px solid #888; + width: 80%; + max-width: 500px; + text-align: left; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +.social-input { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.social-input select { + margin-right: 10px; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + padding: 5px; + border-radius: 4px; +} + +.social-input input { + flex: 1; + font-family: 'Consolas', 'Courier New', Courier, monospace; + font-size: 14px; + padding: 5px; + border-radius: 4px; +} diff --git a/src/components/AuthorList.vue b/src/components/AuthorList.vue new file mode 100644 index 0000000..0303f26 --- /dev/null +++ b/src/components/AuthorList.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/components/DeveloperPortalContribute.css b/src/components/DeveloperPortalContribute.css index d770dc6..b50bdac 100644 --- a/src/components/DeveloperPortalContribute.css +++ b/src/components/DeveloperPortalContribute.css @@ -6,80 +6,3 @@ button { margin: 5px; } - -.author { - border: 1px solid #ddd; - padding: 10px; - margin-bottom: 20px; -} - -input, textarea { - display: block; - margin-bottom: 10px; - width: calc(100% - 20px); /* Make inputs slightly smaller than the modal */ - box-sizing: border-box; - font-family: 'Consolas', 'Courier New', Courier, monospace; /* Ensure the same font is used */ - font-size: 14px; /* Ensure consistent font size */ - border-radius: 4px; /* Add rounded corners */ - padding: 5px; -} - -/* Modal styles */ -.modal { - display: flex; - justify-content: center; - align-items: center; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgba(0, 0, 0, 0.4); -} - -.modal-content { - background-color: #fff; - padding: 20px; - border: 1px solid #888; - width: 80%; - max-width: 500px; - text-align: left; -} - -.close { - color: #aaa; - float: right; - font-size: 28px; - font-weight: bold; -} - -.close:hover, -.close:focus { - color: black; - text-decoration: none; - cursor: pointer; -} - -.social-input { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.social-input select { - margin-right: 10px; - font-family: 'Consolas', 'Courier New', Courier, monospace; - font-size: 14px; - padding: 5px; - border-radius: 4px; -} - -.social-input input { - flex: 1; - font-family: 'Consolas', 'Courier New', Courier, monospace; - font-size: 14px; - padding: 5px; - border-radius: 4px; -} diff --git a/src/components/DeveloperPortalContribute.vue b/src/components/DeveloperPortalContribute.vue index 4c29772..7977c3e 100644 --- a/src/components/DeveloperPortalContribute.vue +++ b/src/components/DeveloperPortalContribute.vue @@ -2,20 +2,9 @@

Developer Portal

- + + -
-

Authors

-
-

{{ author.name }}

-

{{ author.bio }}

- - - -
-

Developer Portal directory is not present.

@@ -25,15 +14,6 @@ @cloningComplete="checkDevPortal" />
- - -
@@ -41,28 +21,17 @@ import { ref, onMounted } from 'vue'; import { invoke } from '@tauri-apps/api/tauri'; import './DeveloperPortalContribute.css'; -import AuthorForm from './AuthorForm.vue'; +import AuthorList from './AuthorList.vue'; +import ArticleList from './ArticleList.vue'; import CloneRepository from './CloneRepository.vue'; const isDevPortalPresent = ref(false); -const authors = ref([]); -const showEditForm = ref(false); -const editAuthorData = ref({ name: '', bio: '', social: [] }); -const editFormTitle = ref(''); -let originalFileName = ''; -let githubUsername = ref(''); -let repoPath = ref(''); - -const getFileName = (name: string) => { - return `${name.toLowerCase().replace(/ /g, '-')}.json`; -}; +const githubUsername = ref(''); +const repoPath = ref(''); const checkDevPortal = async () => { try { isDevPortalPresent.value = await invoke('check_devportal', { repoPath: repoPath.value }); - if (isDevPortalPresent.value) { - authors.value = await invoke('get_authors', { repoPath: repoPath.value }); - } } catch (error) { console.error(error); } @@ -86,62 +55,6 @@ const launchHugo = async () => { } }; -const confirmDelete = (author: any) => { - if (confirm(`Are you sure you want to delete ${author.name}?`)) { - deleteAuthor(author); - } -}; - -const deleteAuthor = async (author: any) => { - try { - const fileName = getFileName(author.name); - await invoke('delete_author', { fileName: fileName, repoPath: repoPath.value }); - checkDevPortal(); // Refresh the list of authors - } catch (error) { - console.error(error); - } -}; - -const editAuthor = (author: any, fileName: string) => { - editAuthorData.value = { - name: author.name, - bio: author.bio, - social: author.social.map((s: any) => ({ key: Object.keys(s)[0], url: Object.values(s)[0] })) - }; - originalFileName = fileName; - editFormTitle.value = `Edit Author: ${author.name}`; - showEditForm.value = true; -}; - -const newAuthor = () => { - editAuthorData.value = { name: '', bio: '', social: [] }; - originalFileName = ''; - editFormTitle.value = 'New Author'; - showEditForm.value = true; -}; - -const saveAuthor = async (authorData: any) => { - try { - let fileName = originalFileName; - if (!fileName) { - fileName = getFileName(authorData.name); - } - const social = authorData.social.reduce((acc: any[], s: any) => { - acc.push({ [s.key]: s.url }); - return acc; - }, []); - await invoke('save_author', { author: { ...authorData, social }, fileName: fileName, repoPath: repoPath.value }); - showEditForm.value = false; - checkDevPortal(); // Refresh the list of authors - } catch (error) { - console.error(error); - } -}; - -const cancelEdit = () => { - showEditForm.value = false; -}; - onMounted(async () => { await loadSettings(); await checkDevPortal();