From 107a8245e48a638ecfb248b82af61666031c653c Mon Sep 17 00:00:00 2001 From: Artemy Egorov Date: Mon, 29 Jul 2024 22:14:05 +0300 Subject: [PATCH] feat: tabs, state management --- README.md | 13 ++++ src-tauri/src/main.rs | 109 ++++++++++++++++++++++++------ src-tauri/src/types.rs | 100 ++++++++++++++++++++++++--- src-tauri/src/utils.rs | 20 ++++-- src/app.css | 57 ++++++++++++++-- src/lib/components/Sidebar.svelte | 41 +++++++++-- src/lib/components/Tab.svelte | 46 +++++++++++++ src/lib/icons/Add.svelte | 17 +++++ src/lib/stores.ts | 7 +- src/lib/types.ts | 14 ++++ src/lib/utils.ts | 32 +++++++++ src/routes/+page.svelte | 9 ++- 12 files changed, 414 insertions(+), 51 deletions(-) create mode 100644 src/lib/components/Tab.svelte create mode 100644 src/lib/icons/Add.svelte create mode 100644 src/lib/types.ts create mode 100644 src/lib/utils.ts diff --git a/README.md b/README.md index 8beb1a6..edca429 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,21 @@ Browser is WIP. See also: [Dalet](https://github.com/TxtDot/dalet). +Format support: + - [ ] Dalet support - [x] Text support - [ ] Gemtext support - [ ] Proxy support - [ ] Local txtdot engines support + +Browser features: + +- [x] input url processing +- [x] tab window hiding +- [x] tabs +- [x] tab switching +- [x] tab closing +- [x] save and restore tabs +- [ ] save and restore favorites +- [ ] cache tab data diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7c847ec..88acdeb 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,8 +2,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use std::{ - fs::{self, File}, - io::Write, + fs::{self}, sync::Mutex, }; @@ -13,30 +12,106 @@ mod types; mod utils; use tauri::Manager; -use types::{State, VigiError}; -use utils::{read_jsonl_tabs, read_or_create_current_tab_index, read_or_create_jsonl}; +use types::{TabType, VigiError, VigiState}; +use utils::{read_or_create_jsonl, read_or_create_number}; // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] -async fn process_input(input: String) -> Result, VigiError> { +async fn process_input( + input: String, + state: tauri::State<'_, Mutex>, +) -> Result, VigiError> { // TODO: Implement mime type, language, protocol or search detection // TODO: Implement text links parsing - match reqwest::get(input).await { + println!("Processing: {}", input); + + match reqwest::get(input.clone()).await { Ok(res) => match res.text().await { - Ok(res) => Ok(vec![Tag::new(0, Body::Text(res), Argument::Null)]), - Err(_) => Err(VigiError::ParseError), + Ok(res) => { + update_tab(state, TabType::Text, res.clone(), input.clone())?; + Ok(vec![Tag::new(0, Body::Text(res), Argument::Null)]) + } + Err(_) => Err(VigiError::Parse), }, - Err(_) => Err(VigiError::NetworkError), + Err(_) => Err(VigiError::Network), } } +#[tauri::command] +fn get_state(state: tauri::State>) -> VigiState { + (*state.lock().unwrap()).clone() +} + +#[tauri::command] +fn select_tab(state: tauri::State>, index: usize) -> Result<(), VigiError> { + match state.lock() { + Ok(mut state) => { + state.update_current_tab_index(index)?; + Ok(()) + } + Err(_) => Err(VigiError::StateLock), + } +} + +#[tauri::command] +fn add_tab(state: tauri::State>) -> Result<(), VigiError> { + match state.lock() { + Ok(mut state) => { + state.add_tab()?; + Ok(()) + } + Err(_) => Err(VigiError::StateLock), + } +} + +#[tauri::command] +fn remove_tab(state: tauri::State>, index: usize) -> Result<(), VigiError> { + match state.lock() { + Ok(mut state) => { + state.remove_tab(index)?; + Ok(()) + } + Err(_) => Err(VigiError::StateLock), + } +} + +fn update_tab( + state: tauri::State>, + tab_type: TabType, + tab_title: String, + tab_url: String, +) -> Result<(), VigiError> { + match state.lock() { + Ok(mut state) => { + state.update_tab(tab_type, tab_title, tab_url)?; + Ok(()) + } + Err(_) => Err(VigiError::StateLock), + } +} + +fn main() { + tauri::Builder::default() + .manage(Mutex::new(VigiState::null())) + .setup(setup_handler) + .invoke_handler(tauri::generate_handler![ + process_input, + get_state, + select_tab, + add_tab, + remove_tab + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} + fn setup_handler(app: &mut tauri::App) -> Result<(), Box> { println!("---Setup---"); let app_handle = app.handle(); - let state = app.state::>(); + let state = app.state::>(); let mut state = state.lock().unwrap(); let config_dir = app_handle @@ -78,19 +153,13 @@ fn setup_handler(app: &mut tauri::App) -> Result<(), Box, pub favorites_tabs: Vec, } -impl State { +impl VigiState { pub fn null() -> Self { Self { + tabs_id_counter_path: PathBuf::new(), current_tab_index_path: PathBuf::new(), local_tabs_path: PathBuf::new(), favorites_tabs_path: PathBuf::new(), cache_dir: PathBuf::new(), + + tabs_id_counter: 0, current_tab_index: 0, tabs: Vec::new(), favorites_tabs: Vec::new(), } } + + pub fn update_current_tab_index(&mut self, new_index: usize) -> Result<(), VigiError> { + self.current_tab_index = new_index; + + self.write_current_tab_index()?; + Ok(()) + } + + fn write_current_tab_index(&mut self) -> Result<(), VigiError> { + fs::write( + &self.current_tab_index_path, + self.current_tab_index.to_string(), + ) + .map_err(|_| VigiError::StateUpdate) + } + + fn write_id_counter(&mut self) -> Result<(), VigiError> { + fs::write(&self.tabs_id_counter_path, self.tabs_id_counter.to_string()) + .map_err(|_| VigiError::StateUpdate) + } + + pub fn update_tab( + &mut self, + tab_type: TabType, + tab_title: String, + tab_url: String, + ) -> Result<(), VigiError> { + match self.tabs.get_mut(self.current_tab_index) { + Some(tab) => { + *tab = Tab::new(tab_type, tab_title, tab_url, tab.id); + + write_tabs(&self.local_tabs_path, &self.tabs)?; + + Ok(()) + } + None => Err(VigiError::StateUpdate), + } + } + + pub fn add_tab(&mut self) -> Result<(), VigiError> { + self.tabs_id_counter += 1; + self.tabs.push(Tab::new( + TabType::HomePage, + "New tab".to_string(), + "".to_string(), + self.tabs_id_counter, + )); + + self.write_id_counter()?; + write_tabs(&self.local_tabs_path, &self.tabs)?; + Ok(()) + } + + pub fn remove_tab(&mut self, index: usize) -> Result<(), VigiError> { + if self.tabs.len() == 1 { + self.current_tab_index = 0; + } else { + self.current_tab_index = self.current_tab_index + 1; + } + + self.tabs.remove(index); + write_tabs(&self.local_tabs_path, &self.tabs)?; + self.write_current_tab_index()?; + + Ok(()) + } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Tab { ty: TabType, - name: String, + title: String, url: String, + id: usize, } -#[derive(Serialize, Deserialize, Debug)] +impl Tab { + pub fn new(ty: TabType, title: String, url: String, id: usize) -> Self { + Self { ty, title, url, id } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] pub enum TabType { HomePage, Text, diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 312c412..57898e0 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -3,7 +3,7 @@ use std::{ path::PathBuf, }; -use crate::types::Tab; +use crate::types::{Tab, VigiError}; pub fn read_jsonl_tabs(path: &PathBuf) -> Vec { fs::read_to_string(&path) @@ -24,11 +24,8 @@ pub fn read_or_create_jsonl(path: &PathBuf) -> Vec { } } -pub fn read_or_create_current_tab_index(path: &PathBuf) -> usize { - println!( - " Getting current tab index from {}", - path.to_string_lossy() - ); +pub fn read_or_create_number(path: &PathBuf) -> usize { + println!(" Getting number from {}", path.to_string_lossy()); if path.exists() { fs::read_to_string(path) .unwrap() @@ -40,3 +37,14 @@ pub fn read_or_create_current_tab_index(path: &PathBuf) -> usize { 0 } } + +pub fn write_tabs(path: &PathBuf, tabs: &Vec) -> Result<(), VigiError> { + fs::write( + path, + tabs.iter() + .map(|tab| serde_json::to_string(tab).unwrap()) + .collect::>() + .join("\n"), + ) + .map_err(|_| VigiError::StateUpdate) +} diff --git a/src/app.css b/src/app.css index bbd8b8e..9b85c1c 100644 --- a/src/app.css +++ b/src/app.css @@ -43,24 +43,28 @@ body { @apply ease-out duration-150; @apply hover:bg-vigi-90; - @apply hover:px-2 hover:mx-1 hover:scale-125 hover:cursor-pointer; + @apply hover:px-2 hover:mx-1 hover:scale-125 cursor-pointer; @apply active:bg-vigi-100; } +.open-tabs { + @apply flex justify-between mt-2 mx-2; +} + .block { @apply p-2 rounded-xl bg-vigi-60; } .sidebar { - @apply basis-1/5 shrink-0; + @apply shrink-0 grow-0 flex flex-col w-1/5; @apply ease-out duration-100; } .sidebar.collapsed { @apply basis-0; - @apply p-0 m-0; + @apply p-0 m-0 bg-transparent; } .top-bar { @@ -80,13 +84,54 @@ input::placeholder { @apply text-vigi-40 focus:text-vigi-20; } +.tabs { + @apply flex flex-col gap-1 mt-2 grow overflow-auto; +} + +.tab { + @apply p-2 rounded-xl bg-vigi-50; + @apply cursor-pointer; + @apply ease-out duration-100; + + @apply hover:bg-vigi-55; + + @apply hover:px-4; + + @apply flex items-center justify-between gap-2 w-full; +} + +.close-button { + @apply p-1 rounded-lg; + @apply ease-out duration-100; + + @apply hover:bg-vigi-90 active:bg-vigi-100; +} + +/* .tab .close-button { + @apply text-transparent; +} + +.tab:hover .close-button { + @apply text-vigi-0; +} */ + +.tab.active { + @apply bg-vigi-40 text-vigi-0 font-bold; + + @apply hover:bg-vigi-45; +} + +.tab-title { + @apply truncate; +} + ::selection { @apply bg-vigi-60; } /* width */ ::-webkit-scrollbar { - width: 20px; + width: 15px; } /* Track */ @@ -96,12 +141,12 @@ input::placeholder { /* Handle */ ::-webkit-scrollbar-thumb { - @apply rounded-xl bg-vigi-90 bg-clip-content; + @apply rounded-xl bg-vigi-70 bg-clip-content; border: 6px solid transparent; } /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { - @apply bg-vigi-100; + @apply bg-vigi-75; border: 5px solid transparent; } diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index e8356e0..6d66a36 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -1,16 +1,45 @@ - - - {#if sidebarOpen} -
- + + {#if collapsed} + + +
+ Open tabs + +
+ +
+ {#each tabs as tab, i (tab.id)} + + {/each}
{/if}
diff --git a/src/lib/components/Tab.svelte b/src/lib/components/Tab.svelte new file mode 100644 index 0000000..dc0b0da --- /dev/null +++ b/src/lib/components/Tab.svelte @@ -0,0 +1,46 @@ + + +
(hovered = true)} + on:mouseleave={() => (hovered = false)} + role="tab" + tabindex={id} +> + + + {#if hovered} + + {/if} +
diff --git a/src/lib/icons/Add.svelte b/src/lib/icons/Add.svelte new file mode 100644 index 0000000..6b0c4a9 --- /dev/null +++ b/src/lib/icons/Add.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/stores.ts b/src/lib/stores.ts index 460929e..47384e8 100644 --- a/src/lib/stores.ts +++ b/src/lib/stores.ts @@ -1,3 +1,6 @@ -import { writable } from "svelte/store"; +import { writable, type Writable } from "svelte/store"; +import type { VigiState } from "./types"; -export const topBarInput = writable(""); +export const topBarInput: Writable = writable(""); + +export const state: Writable = writable(); diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..43a8eac --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,14 @@ +export interface VigiState { + current_tab_index: number; + tabs: StateTab[]; + favorites_tabs: StateTab[]; +} + +type TabType = "HomePage" | "Text"; + +export interface StateTab { + ty: TabType; + title: string; + url: string; + id: number; +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..2114c5b --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,32 @@ +import { invoke } from "@tauri-apps/api"; +import { state, topBarInput } from "./stores"; +import type { StateTab, VigiState } from "./types"; + +export function updateVigiState() { + invoke("get_state") + .then((r) => { + let st = r as VigiState; + + state.set(st); + + topBarInput.set(st.tabs[st.current_tab_index].url); + }) + .catch((err) => console.log(err)); +} + +export async function addTab() { + await invoke("add_tab"); + + updateVigiState(); +} + +export async function selectTab(index: number) { + await invoke("select_tab", { index }); + + updateVigiState(); +} + +export async function removeTab(index: number) { + await invoke("remove_tab", { index }); + updateVigiState(); +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3b116b0..cba7e5f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,14 +8,16 @@ import { invoke } from "@tauri-apps/api/tauri"; import { topBarInput } from "$lib/stores"; + import { updateVigiState } from "$lib/utils"; let sidebarOpen = true; - let inputValue = ""; let isLoading = false; let data: Root = []; + updateVigiState(); + document.addEventListener("keypress", (e: KeyboardEvent) => { const formElements = ["INPUT", "TEXTAREA", "SELECT", "OPTION"]; if (formElements.includes((e.target as Element).tagName)) { @@ -34,6 +36,9 @@ .catch((err) => { data = [{ id: 0, body: "Error: " + err, argument: null }]; isLoading = false; + }) + .finally(() => { + updateVigiState(); }); }); @@ -42,7 +47,7 @@ class={`common-window${sidebarOpen ? "" : " collapsed"}`} data-tauri-drag-region > - +