diff --git a/data/ui/person_editor.blp b/data/ui/person_editor.blp
new file mode 100644
index 0000000..6d69e37
--- /dev/null
+++ b/data/ui/person_editor.blp
@@ -0,0 +1,15 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $MusicusPersonEditor : Adw.NavigationPage {
+ title: _("Person");
+
+ Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar header_bar {}
+
+ Adw.Clamp {
+ $MusicusTranslationSection name_section {}
+ }
+ }
+}
diff --git a/data/ui/translation_entry.blp b/data/ui/translation_entry.blp
new file mode 100644
index 0000000..3161f8e
--- /dev/null
+++ b/data/ui/translation_entry.blp
@@ -0,0 +1,59 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $MusicusTranslationEntry : Adw.EntryRow {
+ title: _("Translated name");
+
+ Gtk.Button {
+ icon-name: "edit-delete-symbolic";
+ valign: center;
+ clicked => $remove() swapped;
+ styles ["flat"]
+ }
+
+ Gtk.Button {
+ valign: center;
+ clicked => $open_lang_popover() swapped;
+
+ Gtk.Box {
+ spacing: 6;
+
+ Gtk.Label {
+ label: bind lang_entry.text;
+ }
+
+ Gtk.Image {
+ icon-name: "pan-down-symbolic";
+ }
+
+ Gtk.Popover lang_popover {
+ Gtk.Box {
+ orientation: vertical;
+ spacing: 6;
+ margin-start: 6;
+ margin-end: 6;
+ margin-top: 6;
+ margin-bottom: 6;
+
+ Gtk.Label {
+ label: _("Language code");
+ halign: start;
+ styles ["heading"]
+ }
+
+ Gtk.Label {
+ width-request: 200;
+ label: _("Enter an ISO 639 two-letter language code identifying the language this translation uses.");
+ use-markup: true;
+ wrap: true;
+ max-width-chars: 40;
+ halign: start;
+ styles ["dim-label"]
+ }
+
+ Gtk.Entry lang_entry {}
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/ui/translation_section.blp b/data/ui/translation_section.blp
new file mode 100644
index 0000000..22c4644
--- /dev/null
+++ b/data/ui/translation_section.blp
@@ -0,0 +1,19 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $MusicusTranslationSection : Adw.PreferencesGroup {
+ title: _("Name");
+ header-suffix: Gtk.Button add_button {
+ styles ["flat"]
+ clicked => $add_translation() swapped;
+
+ Adw.ButtonContent {
+ icon-name: "list-add-symbolic";
+ label: _("Translate");
+ }
+ };
+
+ Adw.EntryRow entry_row {
+ title: _("Name");
+ }
+}
\ No newline at end of file
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 5de4e74..b2fd98e 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -39,9 +39,9 @@ pub fn connect(file_name: &str) -> Result {
}
/// A single translated string value.
-#[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Debug)]
+#[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Default, Debug)]
#[diesel(sql_type = Text)]
-pub struct TranslatedString(HashMap);
+pub struct TranslatedString(pub HashMap);
impl TranslatedString {
/// Get the best translation for the user's current locale.
diff --git a/src/editor/mod.rs b/src/editor/mod.rs
new file mode 100644
index 0000000..92cb362
--- /dev/null
+++ b/src/editor/mod.rs
@@ -0,0 +1,3 @@
+pub mod person_editor;
+pub mod translation_entry;
+pub mod translation_section;
\ No newline at end of file
diff --git a/src/editor/person_editor.rs b/src/editor/person_editor.rs
new file mode 100644
index 0000000..4d764a4
--- /dev/null
+++ b/src/editor/person_editor.rs
@@ -0,0 +1,45 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::glib;
+
+use crate::editor::translation_section::MusicusTranslationSection;
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, gtk::CompositeTemplate)]
+ #[template(file = "data/ui/person_editor.blp")]
+ pub struct MusicusPersonEditor {}
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MusicusPersonEditor {
+ const NAME: &'static str = "MusicusPersonEditor";
+ type Type = super::MusicusPersonEditor;
+ type ParentType = adw::NavigationPage;
+
+ fn class_init(klass: &mut Self::Class) {
+ MusicusTranslationSection::static_type();
+ klass.bind_template();
+ klass.bind_template_instance_callbacks();
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MusicusPersonEditor {}
+ impl WidgetImpl for MusicusPersonEditor {}
+ impl NavigationPageImpl for MusicusPersonEditor {}
+}
+
+glib::wrapper! {
+ pub struct MusicusPersonEditor(ObjectSubclass)
+ @extends gtk::Widget, adw::NavigationPage;
+}
+
+#[gtk::template_callbacks]
+impl MusicusPersonEditor {
+ pub fn new() -> Self {
+ glib::Object::new()
+ }
+}
diff --git a/src/editor/translation_entry.rs b/src/editor/translation_entry.rs
new file mode 100644
index 0000000..df50956
--- /dev/null
+++ b/src/editor/translation_entry.rs
@@ -0,0 +1,94 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::glib::{self, subclass::Signal};
+use once_cell::sync::Lazy;
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, gtk::CompositeTemplate)]
+ #[template(file = "data/ui/translation_entry.blp")]
+ pub struct MusicusTranslationEntry {
+ #[template_child]
+ pub lang_popover: TemplateChild,
+ #[template_child]
+ pub lang_entry: TemplateChild,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MusicusTranslationEntry {
+ const NAME: &'static str = "MusicusTranslationEntry";
+ type Type = super::MusicusTranslationEntry;
+ type ParentType = adw::EntryRow;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+ klass.bind_template_instance_callbacks();
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MusicusTranslationEntry {
+ fn signals() -> &'static [Signal] {
+ static SIGNALS: Lazy> =
+ Lazy::new(|| vec![Signal::builder("remove").build()]);
+
+ SIGNALS.as_ref()
+ }
+
+ fn constructed(&self) {
+ self.parent_constructed();
+ }
+ }
+
+ impl WidgetImpl for MusicusTranslationEntry {}
+ impl ListBoxRowImpl for MusicusTranslationEntry {}
+ impl PreferencesRowImpl for MusicusTranslationEntry {}
+ impl EntryRowImpl for MusicusTranslationEntry {}
+ impl EditableImpl for MusicusTranslationEntry {}
+}
+
+glib::wrapper! {
+ pub struct MusicusTranslationEntry(ObjectSubclass)
+ @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::EntryRow,
+ @implements gtk::Editable;
+}
+
+#[gtk::template_callbacks]
+impl MusicusTranslationEntry {
+ pub fn new(lang: &str, translation: &str) -> Self {
+ let obj: Self = glib::Object::new();
+ obj.set_text(translation);
+ obj.imp().lang_entry.set_text(lang);
+ obj
+ }
+
+ pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId {
+ self.connect_local("remove", true, move |values| {
+ let obj = values[0].get::().unwrap();
+ f(&obj);
+ None
+ })
+ }
+
+ pub fn lang(&self) -> String {
+ self.imp().lang_entry.text().into()
+ }
+
+ pub fn translation(&self) -> String {
+ self.imp().text().into()
+ }
+
+ #[template_callback]
+ fn open_lang_popover(&self, _: >k::Button) {
+ self.imp().lang_popover.popup();
+ self.imp().lang_entry.grab_focus();
+ }
+
+ #[template_callback]
+ fn remove(&self, _: >k::Button) {
+ self.emit_by_name::<()>("remove", &[]);
+ }
+}
diff --git a/src/editor/translation_section.rs b/src/editor/translation_section.rs
new file mode 100644
index 0000000..5e1c795
--- /dev/null
+++ b/src/editor/translation_section.rs
@@ -0,0 +1,103 @@
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::glib;
+
+use crate::db::TranslatedString;
+use crate::editor::translation_entry::MusicusTranslationEntry;
+use crate::util;
+
+mod imp {
+ use super::*;
+
+ #[derive(Debug, Default, gtk::CompositeTemplate)]
+ #[template(file = "data/ui/translation_section.blp")]
+ pub struct MusicusTranslationSection {
+ #[template_child]
+ pub entry_row: TemplateChild,
+
+ pub translation_entries: RefCell>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MusicusTranslationSection {
+ const NAME: &'static str = "MusicusTranslationSection";
+ type Type = super::MusicusTranslationSection;
+ type ParentType = adw::PreferencesGroup;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.bind_template();
+ klass.bind_template_instance_callbacks();
+ }
+
+ fn instance_init(obj: &glib::subclass::InitializingObject) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MusicusTranslationSection {
+ fn constructed(&self) {
+ self.parent_constructed();
+ }
+ }
+
+ impl WidgetImpl for MusicusTranslationSection {}
+ impl PreferencesGroupImpl for MusicusTranslationSection {}
+}
+
+glib::wrapper! {
+ pub struct MusicusTranslationSection(ObjectSubclass)
+ @extends gtk::Widget, adw::PreferencesGroup;
+}
+
+#[gtk::template_callbacks]
+impl MusicusTranslationSection {
+ pub fn new(translation: TranslatedString) -> Self {
+ let obj: Self = glib::Object::new();
+ let mut translation = translation.0;
+
+ obj.imp()
+ .entry_row
+ .set_text(&translation.remove("generic").unwrap_or_default());
+
+ for (lang, translation) in translation {
+ obj.add_entry(&lang, &translation);
+ }
+
+ obj
+ }
+
+ #[template_callback]
+ fn add_translation(&self, _: >k::Button) {
+ self.add_entry(&util::LANG, &self.imp().entry_row.text());
+ }
+
+ fn translation(&self) -> TranslatedString {
+ let imp = self.imp();
+ let mut translation = HashMap::::new();
+
+ translation.insert(String::from("generic"), imp.entry_row.text().into());
+ for entry in &*imp.translation_entries.borrow() {
+ translation.insert(entry.lang(), entry.translation());
+ }
+
+ TranslatedString(translation)
+ }
+
+ fn add_entry(&self, lang: &str, translation: &str) {
+ let entry = MusicusTranslationEntry::new(lang, translation);
+
+ let obj = self.clone();
+ entry.connect_remove(move |entry| {
+ let mut entries = obj.imp().translation_entries.borrow_mut();
+ if let Some(index) = entries.iter().position(|e| e == entry) {
+ entries.remove(index);
+ }
+ obj.remove(entry);
+ });
+
+ self.add(&entry);
+ self.imp().translation_entries.borrow_mut().push(entry);
+ }
+}
diff --git a/src/library_manager.rs b/src/library_manager.rs
index cb07b49..9ff95e4 100644
--- a/src/library_manager.rs
+++ b/src/library_manager.rs
@@ -1,4 +1,3 @@
-use crate::library::MusicusLibrary;
use adw::{
prelude::*,
subclass::{navigation_page::NavigationPageImpl, prelude::*},
@@ -6,6 +5,9 @@ use adw::{
use gtk::glib::{self, Properties};
use std::cell::OnceCell;
+use crate::editor::person_editor::MusicusPersonEditor;
+use crate::library::MusicusLibrary;
+
mod imp {
use super::*;
@@ -34,7 +36,12 @@ mod imp {
}
#[glib::derived_properties]
- impl ObjectImpl for LibraryManager {}
+ impl ObjectImpl for LibraryManager {
+ fn constructed(&self) {
+ self.parent_constructed();
+ self.obj().set_child(Some(&MusicusPersonEditor::new()));
+ }
+ }
impl WidgetImpl for LibraryManager {}
impl NavigationPageImpl for LibraryManager {}
diff --git a/src/main.rs b/src/main.rs
index 906be97..f336ae4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
mod application;
mod config;
mod db;
+mod editor;
mod home_page;
mod library_manager;
mod library;