diff --git a/data/cartero.gresource.xml b/data/cartero.gresource.xml index a67207b0..216bb8ea 100644 --- a/data/cartero.gresource.xml +++ b/data/cartero.gresource.xml @@ -1,11 +1,16 @@ + ui/collection_pane.ui + ui/sidebar.ui + ui/sidebar_row.ui style.css gtk/help_overlay.ui ui/endpoint_pane.ui ui/formdata_payload_pane.ui ui/key_value_pane.ui + ui/new_collection_window.ui + ui/new_request_window.ui ui/key_value_row.ui ui/main_window_no_csd.ui ui/main_window.ui @@ -22,6 +27,7 @@ ui/urlencoded_payload_pane.ui icons/scalable/actions/horizontal-arrows-symbolic.svg icons/scalable/actions/tab-new-symbolic.svg + icons/scalable/actions/tab-new-filled-symbolic.svg icons/scalable/apps/es.danirod.Cartero.Devel.svg icons/scalable/apps/es.danirod.Cartero.svg icons/symbolic/apps/es.danirod.Cartero-symbolic.svg diff --git a/data/es.danirod.Cartero.gschema.xml b/data/es.danirod.Cartero.gschema.xml index a846a3c0..5308bf06 100644 --- a/data/es.danirod.Cartero.gschema.xml +++ b/data/es.danirod.Cartero.gschema.xml @@ -43,6 +43,18 @@ 500 The position of the split between two windows + + [] + The collections that will be visible in the sidebar + + + nothing + The default directory to present when creating a new collection + + + nothing + The default directory to present when opening a new collection + [] The current list of opened files diff --git a/data/icons/scalable/actions/tab-new-filled-symbolic.svg b/data/icons/scalable/actions/tab-new-filled-symbolic.svg new file mode 100644 index 00000000..2b2f1d80 --- /dev/null +++ b/data/icons/scalable/actions/tab-new-filled-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/meson.build b/data/meson.build index 2f2ed7d4..52bd2352 100644 --- a/data/meson.build +++ b/data/meson.build @@ -16,6 +16,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later blueprint_files = [ + 'ui/collection_pane.blp', 'gtk/help_overlay.blp', 'ui/code_export_pane.blp', 'ui/endpoint_pane.blp', @@ -30,6 +31,10 @@ blueprint_files = [ 'ui/raw_payload_pane.blp', 'ui/response_headers.blp', 'ui/response_panel.blp', + 'ui/new_collection_window.blp', + 'ui/new_request_window.blp', + 'ui/sidebar.blp', + 'ui/sidebar_row.blp', 'ui/save_dialog.blp', 'ui/search_box.blp', 'ui/settings_dialog.blp', diff --git a/data/ui/collection_pane.blp b/data/ui/collection_pane.blp new file mode 100644 index 00000000..f2221e42 --- /dev/null +++ b/data/ui/collection_pane.blp @@ -0,0 +1,56 @@ +/* + * Copyright 2024 the Cartero authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// SPDX-License-Identifier: GPL-3.0-or-later +using Gtk 4.0; + +template $CarteroCollectionPane: Gtk.Box { + orientation: vertical; + + Box { + orientation: horizontal; + spacing: 10; + + Entry collection_name { + placeholder-text: _("Collection name"); + hexpand: true; + } + + Button { + label: _("Save"); + clicked => $on_save() swapped; + + styles [ + 'suggested-action', + ] + } + } + + Notebook { + NotebookPage { + tab: Label { + label: _("Variables"); + }; + + child: ScrolledWindow { + hexpand: true; + vexpand: true; + + $CarteroKeyValuePane variables {} + }; + } + } +} diff --git a/data/ui/main_window.blp b/data/ui/main_window.blp index ea30db95..fa5ce3df 100644 --- a/data/ui/main_window.blp +++ b/data/ui/main_window.blp @@ -22,153 +22,163 @@ template $CarteroWindow: Adw.ApplicationWindow { title: "Cartero"; [content] - Adw.ToolbarView { - top-bar-style: raised; - - [top] - Adw.HeaderBar { - title-widget: Adw.WindowTitle window_title { - title: 'Cartero'; - }; - - [start] - Box { - spacing: 5; - - Button { - action-name: "win.new"; - icon-name: 'tab-new-symbolic'; - tooltip-text: _("New"); + Adw.OverlaySplitView split_view { + sidebar: Adw.ToolbarView { + [top] + Adw.HeaderBar { + [start] + MenuButton { + icon-name: "list-add-symbolic"; + menu-model: new_menu; } - Separator {} - - Button open_dialog { - styles [ - "flat", - ] - - action-name: "win.open"; - tooltip-text: _("Open"); - - [child] - Box { - spacing: 5; - - Image { - icon-name: 'document-open-symbolic'; - } - - Label { - label: _("Open"); - } - } - } - - Button save_dialog { - action-name: "win.save"; - icon-name: 'document-save-symbolic'; - tooltip-text: _("Save"); + [end] + MenuButton { + icon-name: "open-menu-symbolic"; + primary: true; + menu-model: main_menu; } } - [end] - MenuButton { - icon-name: "open-menu-symbolic"; - primary: true; - menu-model: main_menu; - } - } - - [top] - Adw.TabBar tabs { - view: tabview; - } - - Adw.ToastOverlay toaster { - Stack stack { - StackPage { - name: "welcome"; - - child: Adw.StatusPage { - vexpand: true; - title: _("Welcome to Cartero"); - description: _("Create or open a request and start testing APIs now."); - icon-name: "es.danirod.Cartero-symbolic"; - - child: Adw.Clamp { - maximum-size: 500; - - Gtk.Box { - orientation: vertical; - - Gtk.Button { - styles [ - "pill", - "suggested-action", - ] + $CarteroSidebar collections {} + }; - action-name: "win.new"; + content: Adw.ToolbarView { + [top] + Adw.HeaderBar { + title-widget: Adw.WindowTitle window_title { + title: 'Cartero'; + }; - child: Adw.ButtonContent { - icon-name: "tab-new-symbolic"; - label: _("New tab"); - }; - } + Label main_label {} + } - Gtk.Button { - styles [ - "pill", - ] + [top] + Gtk.Box { + orientation: vertical; - action-name: "win.open"; + Adw.TabBar tabs { + view: tabview; + } + } - child: Adw.ButtonContent { - icon-name: "document-open-symbolic"; - label: _("Open request…"); - }; + Adw.ToastOverlay toaster { + Stack stack { + StackPage { + name: "welcome"; + + child: Adw.StatusPage { + vexpand: true; + title: _("Welcome to Cartero"); + description: _("Create or open a request and start testing APIs now."); + icon-name: "es.danirod.Cartero-symbolic"; + + child: Adw.Clamp { + maximum-size: 500; + + Gtk.Box { + orientation: vertical; + + Gtk.Button { + styles [ + "pill", + "suggested-action", + ] + + action-name: "win.new-collection"; + + child: Adw.ButtonContent { + icon-name: "folder-new-symbolic"; + label: _("New collection"); + }; + } + + Gtk.Button { + styles [ + "pill", + ] + + action-name: "win.open-collection"; + + child: Adw.ButtonContent { + icon-name: "folder-open-symbolic"; + label: _("Open collection"); + }; + } + + Gtk.Button { + styles [ + "pill", + ] + + action-name: "win.new"; + + child: Adw.ButtonContent { + icon-name: "tab-new-symbolic"; + label: _("New scratchpad"); + }; + } } - } + }; }; - }; - } + } - StackPage { - name: "tabview"; + StackPage { + name: "tabview"; - child: Adw.TabView tabview {}; + child: Adw.TabView tabview {}; + } } } - } + }; } } -menu main_menu { +menu new_menu { section { item { - label: _("New tab"); - action: "win.new"; + label: _("New collection"); + action: "win.new-collection"; } item { - label: _("Open request…"); - action: "win.open"; + label: _("New request"); + action: "win.new"; } + } + section { item { - label: _("Save request"); - action: "win.save"; + label: _("New scratchpad"); + action: "win.new-scratchpad"; } + } +} - item { - label: _("Save request as…"); - action: "win.save-as"; - } +menu main_menu { + item { + label: _("Open collection..."); + action: "win.open-collection"; + } - item { - label: _("Close tab"); - action: "win.close"; - } + item { + label: _("Open request..."); + action: "win.open"; + } + + item { + label: _("Save request"); + action: "win.save"; + } + + item { + label: _("Save request as..."); + action: "win.save-as"; + } + + item { + label: _("Close tab"); + action: "win.close"; } section { diff --git a/data/ui/new_collection_window.blp b/data/ui/new_collection_window.blp new file mode 100644 index 00000000..2bdd90b5 --- /dev/null +++ b/data/ui/new_collection_window.blp @@ -0,0 +1,118 @@ +/* + * Copyright 2024 the Cartero authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// SPDX-License-Identifier: GPL-3.0-or-later +using Gtk 4.0; +using Adw 1; + +template $CarteroNewCollectionWindow: Adw.Dialog { + width-request: 640; + title: _("Create Collection"); + + Adw.ToolbarView { + [top] + Adw.HeaderBar {} + + Adw.Clamp { + margin-top: 30; + margin-bottom: 30; + maximum-size: 500; + + Box { + orientation: vertical; + spacing: 20; + + ListBox { + selection-mode: none; + + styles [ + "boxed-list", + ] + + Adw.EntryRow collection_location { + title: _("Location"); + changed => $on_collection_location_changed() swapped; + + [suffix] + Button { + styles [ + "flat", + ] + + valign: center; + icon-name: "folder-symbolic"; + clicked => $on_location_clicked() swapped; + } + } + + Adw.EntryRow collection_name { + title: _("Collection name"); + changed => $on_collection_name_changed() swapped; + + [suffix] + Image parent_directory_does_not_exist { + visible: false; + margin-start: 9; + margin-end: 9; + icon-name: "dialog-error-symbolic"; + tooltip-text: "The collection location does not exist"; + } + + [suffix] + Image file_taken { + visible: false; + margin-start: 9; + margin-end: 9; + icon-name: "dialog-error-symbolic"; + tooltip-text: "The given collection is not valid"; + } + } + } + + Label { + styles [ + "dim-label", + ] + + wrap: true; + label: _("A folder will be created in the given location with the requested name to store the requests and subfolders you add to this collection."); + } + + Box { + orientation: horizontal; + halign: end; + + Button create_button { + styles [ + "suggested-action", + ] + + label: _("Create"); + halign: end; + hexpand: false; + clicked => $on_create_collection() swapped; + } + } + } + } + } +} + +FileDialog file_dialog { + accept-label: _("Select"); + title: _("Collection location"); + modal: true; +} diff --git a/data/ui/new_request_window.blp b/data/ui/new_request_window.blp new file mode 100644 index 00000000..c4ac62d8 --- /dev/null +++ b/data/ui/new_request_window.blp @@ -0,0 +1,97 @@ +/* + * Copyright 2024 the Cartero authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// SPDX-License-Identifier: GPL-3.0-or-later +using Gtk 4.0; +using Adw 1; + +template $CarteroNewRequestWindow: Adw.Dialog { + width-request: 500; + title: _("Create Request"); + + Adw.ToolbarView { + [top] + Adw.HeaderBar {} + + Adw.Clamp { + margin-top: 30; + margin-bottom: 30; + maximum-size: 400; + + Box { + orientation: vertical; + spacing: 20; + + ListBox { + selection-mode: none; + + styles [ + "boxed-list", + ] + + Adw.EntryRow request_name { + title: _("Name"); + changed => $on_request_name_changed() swapped; + + [suffix] + Image request_name_taken { + visible: false; + margin-start: 9; + margin-end: 9; + icon-name: "dialog-error-symbolic"; + tooltip-text: "A request with this name already exists"; + } + } + + Adw.ComboRow request_collection { + title: _("Collection"); + model: model_collection; + + factory: Gtk.SignalListItemFactory { + setup => $on_collection_factory_setup(); + bind => $on_collection_factory_bind(); + unbind => $on_collection_factory_unbind(); + teardown => $on_collection_factory_teardown(); + }; + + notify::selected => $on_collection_changed() swapped; + } + } + + Box { + orientation: horizontal; + halign: end; + + Button create_button { + styles [ + "suggested-action", + ] + + label: _("Create"); + halign: end; + hexpand: false; + clicked => $on_create_request() swapped; + } + } + } + } + } +} + +Gtk.SingleSelection model_collection { + can-unselect: false; + autoselect: false; +} diff --git a/data/ui/sidebar.blp b/data/ui/sidebar.blp new file mode 100644 index 00000000..dff5f617 --- /dev/null +++ b/data/ui/sidebar.blp @@ -0,0 +1,45 @@ +/* + * Copyright 2024 the Cartero authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// SPDX-License-Identifier: GPL-3.0-or-later +using Gtk 4.0; +using Adw 1; + +template $CarteroSidebar: Adw.Bin { + Gtk.ScrolledWindow { + Gtk.ListView list_view { + styles [ + 'navigation-sidebar', + ] + + model: selection_model; + + factory: Gtk.SignalListItemFactory { + setup => $on_factory_setup(); + bind => $on_factory_bind(); + unbind => $on_factory_unbind(); + teardown => $on_factory_teardown(); + }; + + activate => $on_activate(); + } + } + + Gtk.SingleSelection selection_model { + can-unselect: true; + autoselect: false; + } +} diff --git a/data/ui/sidebar_row.blp b/data/ui/sidebar_row.blp new file mode 100644 index 00000000..66ce270c --- /dev/null +++ b/data/ui/sidebar_row.blp @@ -0,0 +1,45 @@ +/* + * Copyright 2024 the Cartero authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// SPDX-License-Identifier: GPL-3.0-or-later +using Gtk 4.0; +using Adw 1; + +template $CarteroSidebarRow: Adw.Bin { + Gtk.Inscription inscription { + hexpand: true; + text: bind template.title; + tooltip-text: bind template.path; + } + + Gtk.GestureClick { + button: 3; + released => $on_right_click() swapped; + } +} + +PopoverMenu context_menu { + menu-model: context_menu_model; + has-arrow: false; +} + +menu context_menu_model { + item { + label: _("Close collection"); + action: "row.close-collection"; + hidden-when: 'action-disabled'; + } +} diff --git a/src/app.rs b/src/app.rs index 1ec70674..fdee0b11 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,15 +16,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later use adw::prelude::*; +use adw::AboutWindow; use glib::subclass::types::ObjectSubclassIsExt; use glib::Object; use gtk::gdk::Display; use gtk::gio::{self, ActionEntryBuilder, Settings}; use gtk::pango::FontDescription; -use gtk::prelude::ActionMapExtManual; +use gtk::prelude::*; +use gtk::subclass::prelude::*; use gtk::{CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION}; -use crate::config::{APP_ID, BASE_ID, RESOURCE_PATH}; +use crate::config::{self, APP_ID, BASE_ID, RESOURCE_PATH}; use crate::win::CarteroWindow; use crate::windows::SettingsDialog; @@ -44,6 +46,7 @@ mod imp { use adw::prelude::*; use adw::subclass::application::AdwApplicationImpl; + use glib::subclass::{object::ObjectImpl, types::ObjectSubclass}; use gtk::gio::Settings; use gtk::subclass::prelude::*; @@ -92,11 +95,13 @@ mod imp { gtk::Window::set_default_icon_name(APP_ID); let obj = self.obj(); + obj.set_accels_for_action("win.open-collection", &["o"]); obj.set_accels_for_action("win.new", &[accelerator!("t")]); obj.set_accels_for_action("win.open", &[accelerator!("o")]); obj.set_accels_for_action("win.save", &[accelerator!("s")]); obj.set_accels_for_action("win.save-as", &[accelerator!("s")]); obj.set_accels_for_action("win.close", &[accelerator!("w")]); + // obj.set_accels_for_action("win.request", &["Return"]); obj.set_accels_for_action("win.request", &[accelerator!("Return")]); obj.set_accels_for_action("app.preferences", &[accelerator!("comma")]); obj.set_accels_for_action("app.quit", &[accelerator!("q")]); diff --git a/src/error.rs b/src/error.rs index a30e77a4..89fd5981 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,12 @@ pub enum CarteroError { #[error("Internal error on file dialog")] FileDialogError, + #[error("Not a collection")] + NotValidCollection, + + #[error("Collection already opened")] + AlreadyOpened, + #[error("DNS error")] Dns, diff --git a/src/fs/collection.rs b/src/fs/collection.rs new file mode 100644 index 00000000..542e2889 --- /dev/null +++ b/src/fs/collection.rs @@ -0,0 +1,93 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::fs::DirEntry; +use std::path::{Path, PathBuf}; +use std::{fs::File, io::Write}; + +use crate::{error::CarteroError, objects::Collection}; + +use super::metadata::Metadata; + +pub fn save_collection(path: &Path, collection: &Collection) -> Result<(), CarteroError> { + std::fs::create_dir(path)?; + + let metadata: Metadata = collection.into(); + let metadata_toml = toml::to_string(&metadata)?; + let metadata_file = path.join("cartero-metadata"); + + let mut file = File::create(metadata_file)?; + write!(file, "{}", metadata_toml)?; + Ok(()) +} + +pub fn open_collection(path: &Path) -> Result { + let metadata_file = path.join("cartero-metadata"); + + // make sure that this is an actual collection + if !metadata_file.exists() { + return Err(CarteroError::NotValidCollection); + } + let metadata_content = std::fs::read_to_string(metadata_file)?; + let metadata: Metadata = toml::from_str(&metadata_content)?; + Ok(metadata.into()) +} + +fn is_cartero_request(entry: &Result) -> bool { + let Ok(entry) = entry else { + return false; + }; + let path = entry.path(); + if !path.is_file() { + return false; + } + let Some(ext) = path.as_path().extension() else { + return false; + }; + ext.to_str().is_some_and(|s| s == "cartero") +} + +fn is_cartero_folder(entry: &Result) -> bool { + let Ok(entry) = entry else { + return false; + }; + let path = entry.path(); + if !path.is_dir() { + return false; + } + + let metadata = path.join("cartero-metadata"); + metadata.exists() && metadata.is_file() +} + +pub fn list_folders(path: &Path) -> Result, CarteroError> { + let folders = path + .read_dir()? + .filter(is_cartero_folder) + .map(|entry| entry.unwrap().path()) + .collect(); + Ok(folders) +} + +pub fn list_endpoints(path: &Path) -> Result, CarteroError> { + let endpoints = path + .read_dir()? + .filter(is_cartero_request) + .map(|entry| entry.unwrap().path()) + .collect(); + Ok(endpoints) +} diff --git a/src/fs/metadata.rs b/src/fs/metadata.rs new file mode 100644 index 00000000..0050f981 --- /dev/null +++ b/src/fs/metadata.rs @@ -0,0 +1,99 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use std::collections::HashMap; + +use glib::{object::ObjectExt, Object}; +use serde::{Deserialize, Serialize}; + +use crate::objects::{Collection, KeyValueItem}; + +#[derive(Serialize, Deserialize)] +struct Information { + title: String, + description: String, + version: String, +} + +#[derive(Serialize, Deserialize)] +struct Variable { + active: bool, + value: String, + secret: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct Metadata { + info: Information, + variables: Option>, +} + +impl From for Collection { + fn from(val: Metadata) -> Self { + let col: Collection = Collection::new(); + col.set_properties(&[ + ("title", &val.info.title), + ("description", &val.info.description), + ("version", &val.info.version), + ]); + + if let Some(variables) = val.variables { + for (variable_name, variable_info) in variables { + let kv: KeyValueItem = Object::builder() + .property("header-name", variable_name) + .property("header-value", variable_info.value) + .property("active", variable_info.active) + .property("secret", variable_info.secret) + .build(); + col.add_variable(&kv); + } + } + + col + } +} + +impl From<&Collection> for Metadata { + fn from(val: &Collection) -> Self { + let title = val.title(); + let description = val.description(); + let version = val.version(); + + let variables = val + .variables_list() + .iter() + .map(|var| { + let variable_name = var.header_name(); + let variable = Variable { + active: var.active(), + value: var.header_value(), + secret: var.secret(), + }; + (variable_name.to_string(), variable) + }) + .collect(); + + Metadata { + info: Information { + title, + description, + version, + }, + variables: Some(variables), + } + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 00000000..9cf7e94b --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +pub mod collection; +pub mod metadata; diff --git a/src/main.rs b/src/main.rs index a35febf3..fc6f36fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ mod widgets; #[rustfmt::skip] mod config; mod entities; +mod fs; mod objects; mod utils; mod win; @@ -67,7 +68,7 @@ fn init_data_dir() { let mut xdg_final_dirs = vec![datadir]; xdg_final_dirs.extend(xdg_data_dirs); let xdg_data_dir = std::env::join_paths(&xdg_final_dirs).unwrap(); - std::env::set_var("XDG_DATA_DIRS", xdg_data_dir); + unsafe { std::env::set_var("XDG_DATA_DIRS", xdg_data_dir) }; } } diff --git a/src/objects/collection.rs b/src/objects/collection.rs new file mode 100644 index 00000000..6f859f85 --- /dev/null +++ b/src/objects/collection.rs @@ -0,0 +1,142 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use glib::{object::Cast, types::StaticType, Object}; +use gtk::{gio::ListStore, prelude::ListModelExt}; + +use super::KeyValueItem; + +mod imp { + use std::cell::{OnceCell, RefCell}; + + use glib::Properties; + use gtk::gio::ListStore; + use gtk::glib::prelude::*; + use gtk::glib::subclass::prelude::*; + + #[derive(Default, Debug, Properties)] + #[properties(wrapper_type = super::Collection)] + pub struct Collection { + #[property(get, set, default = "")] + title: RefCell, + + #[property(get, set, default = "")] + description: RefCell, + + #[property(get, set, default = "")] + version: RefCell, + + #[property(get, set)] + pub(super) variables: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Collection { + const NAME: &'static str = "CarteroCollection"; + type Type = super::Collection; + } + + #[glib::derived_properties] + impl ObjectImpl for Collection {} +} + +glib::wrapper! { + pub struct Collection(ObjectSubclass); +} + +impl Default for Collection { + fn default() -> Self { + Self::new() + } +} + +impl Collection { + pub fn new() -> Self { + let empty_collection = ListStore::with_type(KeyValueItem::static_type()); + Object::builder() + .property("variables", empty_collection) + .build() + } + + pub fn add_variable(&self, var: &KeyValueItem) { + self.variables().append(var); + } + + pub fn variable_count(&self) -> u32 { + self.variables().n_items() + } + + pub fn variable_get(&self, pos: u32) -> Option { + self.variables() + .item(pos) + .and_then(|obj| obj.downcast::().ok()) + } + + pub fn variable_del(&self, pos: u32) -> Option { + if let Some(obj) = self.variable_get(pos) { + self.variables().remove(pos); + Some(obj) + } else { + None + } + } + + pub fn variables_list(&self) -> Vec { + (0..self.variable_count()) + .map(|i| self.variable_get(i)) + .map(Option::unwrap) + .collect() + } +} + +#[cfg(test)] +mod tests { + use crate::objects::KeyValueItem; + + use super::Collection; + + #[test] + pub fn test_collections_can_have_variables() { + let collection = Collection::new(); + + let variable = { + let v = KeyValueItem::default(); + v.set_header_name("token"); + v.set_header_value("12341234"); + v.set_active(true); + v.set_secret(true); + v + }; + + assert_eq!(0, collection.variable_count()); + assert!(collection.variable_get(0).is_none()); + + collection.add_variable(&variable); + assert_eq!(1, collection.variable_count()); + + let variable = collection.variable_get(0).unwrap(); + assert_eq!("token", variable.header_name()); + assert_eq!("12341234", variable.header_value()); + assert!(variable.active()); + assert!(variable.secret()); + + let var = collection.variable_del(0); + assert!(var.is_some()); + assert_eq!(collection.variable_count(), 0); + assert!(collection.variable_get(0).is_none()); + } +} diff --git a/src/objects/key_value_item.rs b/src/objects/key_value_item.rs index 925d4eaa..80bfd46d 100644 --- a/src/objects/key_value_item.rs +++ b/src/objects/key_value_item.rs @@ -101,6 +101,14 @@ impl KeyValueItem { Self::default() } + pub fn new_with_value(name: &str, value: &str) -> Self { + let header = Self::new(); + header.set_header_name(name); + header.set_header_value(value); + header.set_active(true); + header + } + // For a header to be actually usable, it must be checked, and also it must have a header name // properly set. We could argue that having an empty value is also dumb, but the spec // technically allows this. diff --git a/src/objects/mod.rs b/src/objects/mod.rs index def8da14..2378b132 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -15,6 +15,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +mod collection; mod key_value_item; +mod tree_node; +pub use collection::Collection; pub use key_value_item::KeyValueItem; +pub use tree_node::TreeNode; +pub use tree_node::TreeNodeKind; diff --git a/src/objects/tree_node.rs b/src/objects/tree_node.rs new file mode 100644 index 00000000..92b5a80a --- /dev/null +++ b/src/objects/tree_node.rs @@ -0,0 +1,93 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use glib::{object::CastNone, Object}; +use gtk::{gio, TreeListRow}; +use std::path::PathBuf; + +mod imp { + use std::cell::RefCell; + + use glib::Properties; + use gtk::glib; + use gtk::glib::subclass::prelude::*; + use gtk::prelude::*; + + #[derive(Default, Properties)] + #[properties(wrapper_type = super::TreeNode)] + pub struct TreeNode { + #[property(get, set)] + path: RefCell, + + #[property(get, set)] + title: RefCell, + + #[property(get, set, builder(super::TreeNodeKind::default()))] + node_type: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for TreeNode { + const NAME: &'static str = "CarteroTreeNode"; + type Type = super::TreeNode; + } + + #[glib::derived_properties] + impl ObjectImpl for TreeNode {} +} + +glib::wrapper! { + pub struct TreeNode(ObjectSubclass); +} + +impl Default for TreeNode { + fn default() -> Self { + Self::new() + } +} + +impl TreeNode { + pub fn new() -> Self { + Object::builder().build() + } + + pub fn file(&self) -> gio::File { + gio::File::for_path(self.path()) + } + + pub fn pretty_name(&self) -> String { + let title = self.title(); + if title.is_empty() { + let path = PathBuf::from(self.path()); + match path.file_name() { + Some(name) => name.to_os_string().into_string().unwrap(), + None => self.path(), + } + } else { + title + } + } +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, glib::Enum)] +#[enum_type(name = "CarteroTreeNodeKind")] +pub enum TreeNodeKind { + #[default] + Collection, + Folder, + Endpoint, +} diff --git a/src/widgets/collection_pane.rs b/src/widgets/collection_pane.rs new file mode 100644 index 00000000..ad5c2fd6 --- /dev/null +++ b/src/widgets/collection_pane.rs @@ -0,0 +1,135 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use glib::{subclass::types::ObjectSubclassIsExt, Object}; +use gtk::glib; + +use crate::objects::Collection; + +mod imp { + use std::cell::RefCell; + use std::sync::OnceLock; + + use glib::subclass::{InitializingObject, Signal}; + use glib::Properties; + use gtk::subclass::prelude::*; + use gtk::{prelude::*, CompositeTemplate}; + + use crate::objects::Collection; + use crate::objects::KeyValueItem; + use crate::widgets::KeyValuePane; + + #[derive(CompositeTemplate, Default, Properties)] + #[template(resource = "/es/danirod/Cartero/collection_pane.ui")] + #[properties(wrapper_type = super::CollectionPane)] + pub struct CollectionPane { + #[template_child] + collection_name: TemplateChild, + + #[template_child] + variables: TemplateChild, + + #[property(get, set)] + dirty: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for CollectionPane { + const NAME: &'static str = "CarteroCollectionPane"; + type Type = super::CollectionPane; + type ParentType = gtk::Box; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_callbacks(); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for CollectionPane { + fn constructed(&self) { + self.parent_constructed(); + } + + fn signals() -> &'static [Signal] { + static SIGNALS: OnceLock> = OnceLock::new(); + SIGNALS.get_or_init(|| vec![Signal::builder("save-requested").build()]) + } + } + + impl WidgetImpl for CollectionPane {} + + impl BoxImpl for CollectionPane {} + + #[gtk::template_callbacks] + impl CollectionPane { + #[template_callback] + fn on_save(&self) { + let obj = self.obj(); + + obj.emit_by_name::<()>("save-requested", &[]); + } + + pub(super) fn load_collection(&self, col: &Collection) { + self.collection_name.set_text(&col.title()); + let variables: Vec = col + .variables_list() + .iter() + .map(|v| KeyValueItem::new_with_value(&v.header_name(), &v.header_value())) + .collect(); + self.variables.set_entries(&variables); + } + + pub(super) fn save_collection(&self, col: &Collection) { + let name = self.collection_name.text(); + let variables = self.variables.get_entries(); + + col.set_title(name); + col.variables().remove_all(); + for variable in variables { + col.add_variable(&variable); + } + } + } +} + +glib::wrapper! { + pub struct CollectionPane(ObjectSubclass) + @extends gtk::Widget, gtk::Box; +} + +impl Default for CollectionPane { + fn default() -> Self { + Object::builder().build() + } +} + +impl CollectionPane { + pub fn load_collection(&self, col: &Collection) { + let imp = self.imp(); + imp.load_collection(col); + } + + pub fn save_collection(&self, col: &Collection) { + let imp = self.imp(); + imp.save_collection(col); + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 7d6354bc..32135233 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -16,6 +16,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later mod code_view; +mod collection_pane; mod endpoint_pane; mod export_tab; mod file_dialogs; @@ -23,10 +24,16 @@ mod item_pane; mod key_value_pane; mod key_value_row; mod method_dropdown; +mod new_collection_window; +mod new_request_window; mod request_body; mod response_headers; mod response_panel; mod save_dialog; +mod sidebar; +mod sidebar_row; + +pub use collection_pane::CollectionPane; mod search_box; pub use code_view::CodeView; @@ -37,8 +44,11 @@ pub use item_pane::ItemPane; pub use key_value_pane::KeyValuePane; pub use key_value_row::KeyValueRow; pub use method_dropdown::MethodDropdown; +pub use new_collection_window::NewCollectionWindow; +pub use new_request_window::NewRequestWindow; pub use request_body::*; pub use response_headers::ResponseHeaders; pub use response_panel::ResponsePanel; pub use save_dialog::SaveDialog; pub use search_box::SearchBox; +pub use sidebar::Sidebar; diff --git a/src/widgets/new_collection_window.rs b/src/widgets/new_collection_window.rs new file mode 100644 index 00000000..e5827013 --- /dev/null +++ b/src/widgets/new_collection_window.rs @@ -0,0 +1,205 @@ +// Copyright 2024 the Cartero authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// SPDX-License-Identifier: GPL-3.0-or-later + +use glib::Object; +use gtk::subclass::prelude::*; +use gtk::{gio, prelude::*}; + +mod imp { + use adw::prelude::*; + use adw::subclass::prelude::*; + use glib::subclass::InitializingObject; + use gtk::Button; + use gtk::CompositeTemplate; + use gtk::FileDialog; + use gtk::Image; + + use crate::win::CarteroWindow; + + #[derive(CompositeTemplate, Default)] + #[template(resource = "/es/danirod/Cartero/new_collection_window.ui")] + pub struct NewCollectionWindow { + #[template_child] + collection_name: TemplateChild, + + #[template_child] + pub(super) collection_location: TemplateChild, + + #[template_child] + file_dialog: TemplateChild, + + #[template_child] + file_taken: TemplateChild, + + #[template_child] + parent_directory_does_not_exist: TemplateChild, + + #[template_child] + create_button: TemplateChild