feat: allow renaming sidebar favorites to custom names (#1911)
Closes #1474 https://github.com/user-attachments/assets/69fb51ca-225c-43c9-91c9-8f48375e39c4 - [x] I have disclosed use of any AI generated code in my commit messages. - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. - [x] I understand these changes in full and will be able to respond to review comments. - [x] My change is accurately described in the commit message. - [x] My contribution is tested and working as described. - [x] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions.
This commit is contained in:
commit
7cebbef82f
5 changed files with 218 additions and 27 deletions
|
|
@ -118,6 +118,9 @@ rename-file = Rename file
|
||||||
rename-folder = Rename folder
|
rename-folder = Rename folder
|
||||||
rename-confirm = Rename
|
rename-confirm = Rename
|
||||||
|
|
||||||
|
## Change Sidebar Label Dialog
|
||||||
|
sidebar-label = Sidebar label
|
||||||
|
|
||||||
## Replace Dialog
|
## Replace Dialog
|
||||||
replace = Replace
|
replace = Replace
|
||||||
replace-title = "{$filename}" already exists in this location
|
replace-title = "{$filename}" already exists in this location
|
||||||
|
|
@ -341,6 +344,7 @@ move-to = Move to...
|
||||||
move-to-trash = Move to trash
|
move-to-trash = Move to trash
|
||||||
restore-from-trash = Restore from trash
|
restore-from-trash = Restore from trash
|
||||||
remove-from-sidebar = Remove from sidebar
|
remove-from-sidebar = Remove from sidebar
|
||||||
|
change-sidebar-label = Change sidebar label
|
||||||
sort-by-name = Sort by name
|
sort-by-name = Sort by name
|
||||||
sort-by-modified = Sort by modified
|
sort-by-modified = Sort by modified
|
||||||
sort-by-size = Sort by size
|
sort-by-size = Sort by size
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,10 @@ permanently-delete-warning = Deseja realmente excluir permanentemente { $target
|
||||||
rename-file = Renomear arquivo
|
rename-file = Renomear arquivo
|
||||||
rename-folder = Renomear pasta
|
rename-folder = Renomear pasta
|
||||||
|
|
||||||
|
## Change Sidebar Label Dialog
|
||||||
|
|
||||||
|
sidebar-label = Rótulo da barra lateral
|
||||||
|
|
||||||
## Replace Dialog
|
## Replace Dialog
|
||||||
|
|
||||||
replace = Substituir
|
replace = Substituir
|
||||||
|
|
@ -365,6 +369,7 @@ open-in-terminal = Abrir no terminal
|
||||||
move-to-trash = Mover para a lixeira
|
move-to-trash = Mover para a lixeira
|
||||||
restore-from-trash = Restaurar da lixeira
|
restore-from-trash = Restaurar da lixeira
|
||||||
remove-from-sidebar = Remover da barra lateral
|
remove-from-sidebar = Remover da barra lateral
|
||||||
|
change-sidebar-label = Alterar rótulo na barra lateral
|
||||||
sort-by-name = Ordenar por nome
|
sort-by-name = Ordenar por nome
|
||||||
sort-by-modified = Ordenar por data de modificação
|
sort-by-modified = Ordenar por data de modificação
|
||||||
sort-by-size = Ordenar por tamanho
|
sort-by-size = Ordenar por tamanho
|
||||||
|
|
|
||||||
124
src/app.rs
124
src/app.rs
|
|
@ -321,6 +321,7 @@ pub enum NavMenuAction {
|
||||||
Preview(segmented_button::Entity),
|
Preview(segmented_button::Entity),
|
||||||
RunContextAction(segmented_button::Entity, usize),
|
RunContextAction(segmented_button::Entity, usize),
|
||||||
RemoveFromSidebar(segmented_button::Entity),
|
RemoveFromSidebar(segmented_button::Entity),
|
||||||
|
ChangeSidebarLabel(segmented_button::Entity),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MenuAction for NavMenuAction {
|
impl MenuAction for NavMenuAction {
|
||||||
|
|
@ -575,6 +576,10 @@ pub enum DialogPage {
|
||||||
DeleteTrash {
|
DeleteTrash {
|
||||||
items: Vec<TrashItem>,
|
items: Vec<TrashItem>,
|
||||||
},
|
},
|
||||||
|
ChangeSidebarLabel {
|
||||||
|
entity: Entity,
|
||||||
|
label: String,
|
||||||
|
},
|
||||||
RenameItem {
|
RenameItem {
|
||||||
from: PathBuf,
|
from: PathBuf,
|
||||||
parent: PathBuf,
|
parent: PathBuf,
|
||||||
|
|
@ -1769,15 +1774,9 @@ impl App {
|
||||||
|
|
||||||
for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {
|
for (favorite_i, favorite) in self.config.favorites.iter().enumerate() {
|
||||||
if let Some(path) = favorite.path_opt() {
|
if let Some(path) = favorite.path_opt() {
|
||||||
let name = if matches!(favorite, Favorite::Home) {
|
let name = favorite
|
||||||
fl!("home")
|
.display_name()
|
||||||
} else if let Favorite::Network { name, .. } = favorite {
|
.unwrap_or_else(|| fl!("filesystem"));
|
||||||
name.clone()
|
|
||||||
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
|
||||||
file_name.to_string()
|
|
||||||
} else {
|
|
||||||
fl!("filesystem")
|
|
||||||
};
|
|
||||||
nav_model = nav_model.insert(move |b| {
|
nav_model = nav_model.insert(move |b| {
|
||||||
b.text(name.clone())
|
b.text(name.clone())
|
||||||
.icon(
|
.icon(
|
||||||
|
|
@ -2290,15 +2289,33 @@ impl App {
|
||||||
.favorites
|
.favorites
|
||||||
.iter()
|
.iter()
|
||||||
.map(|favorite| {
|
.map(|favorite| {
|
||||||
if let Favorite::Path(path) = favorite {
|
match favorite {
|
||||||
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref())) {
|
Favorite::Path(path) => {
|
||||||
if path.starts_with(from)
|
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
|
||||||
&& let Ok(relative) = path.strip_prefix(from)
|
|
||||||
{
|
{
|
||||||
favorites_changed = true;
|
if path.starts_with(from)
|
||||||
return Favorite::from_path(to.join(relative));
|
&& let Ok(relative) = path.strip_prefix(from)
|
||||||
|
{
|
||||||
|
favorites_changed = true;
|
||||||
|
return Favorite::from_path(to.join(relative));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Favorite::Named { path, name } => {
|
||||||
|
for (from, to) in path_changes.iter().map(|(f, t)| (f.as_ref(), t.as_ref()))
|
||||||
|
{
|
||||||
|
if path.starts_with(from)
|
||||||
|
&& let Ok(relative) = path.strip_prefix(from)
|
||||||
|
{
|
||||||
|
favorites_changed = true;
|
||||||
|
return Favorite::Named {
|
||||||
|
path: to.join(relative),
|
||||||
|
name: name.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
favorite.clone()
|
favorite.clone()
|
||||||
})
|
})
|
||||||
|
|
@ -2613,6 +2630,11 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
items.push(cosmic::widget::menu::Item::Divider);
|
items.push(cosmic::widget::menu::Item::Divider);
|
||||||
if favorite_index_opt.is_some() {
|
if favorite_index_opt.is_some() {
|
||||||
|
items.push(cosmic::widget::menu::Item::Button(
|
||||||
|
fl!("change-sidebar-label"),
|
||||||
|
None,
|
||||||
|
NavMenuAction::ChangeSidebarLabel(entity),
|
||||||
|
));
|
||||||
items.push(cosmic::widget::menu::Item::Button(
|
items.push(cosmic::widget::menu::Item::Button(
|
||||||
fl!("remove-from-sidebar"),
|
fl!("remove-from-sidebar"),
|
||||||
None,
|
None,
|
||||||
|
|
@ -2904,7 +2926,8 @@ impl Application for App {
|
||||||
} else {
|
} else {
|
||||||
Favorite::from_path(path)
|
Favorite::from_path(path)
|
||||||
};
|
};
|
||||||
if !favorites.contains(&favorite) {
|
let favorite_path = favorite.path_opt();
|
||||||
|
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
|
||||||
favorites.push(favorite);
|
favorites.push(favorite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3262,6 +3285,18 @@ impl Application for App {
|
||||||
DialogPage::DeleteTrash { items } => {
|
DialogPage::DeleteTrash { items } => {
|
||||||
tasks.push(self.operation(Operation::DeleteTrash { items }));
|
tasks.push(self.operation(Operation::DeleteTrash { items }));
|
||||||
}
|
}
|
||||||
|
DialogPage::ChangeSidebarLabel { entity, label } => {
|
||||||
|
if let Some(FavoriteIndex(favorite_i)) =
|
||||||
|
self.nav_model.data::<FavoriteIndex>(entity)
|
||||||
|
{
|
||||||
|
let mut favorites = self.config.favorites.clone();
|
||||||
|
if let Some(favorite) = favorites.get_mut(*favorite_i) {
|
||||||
|
*favorite = favorite.with_label(label.trim());
|
||||||
|
config_set!(favorites, favorites);
|
||||||
|
tasks.push(self.update_config());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
DialogPage::RenameItem {
|
DialogPage::RenameItem {
|
||||||
from, parent, name, ..
|
from, parent, name, ..
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -4487,7 +4522,8 @@ impl Application for App {
|
||||||
tab::Command::AddToSidebar(path) => {
|
tab::Command::AddToSidebar(path) => {
|
||||||
let mut favorites = self.config.favorites.clone();
|
let mut favorites = self.config.favorites.clone();
|
||||||
let favorite = Favorite::from_path(path);
|
let favorite = Favorite::from_path(path);
|
||||||
if !favorites.contains(&favorite) {
|
let favorite_path = favorite.path_opt();
|
||||||
|
if !favorites.iter().any(|f| f.path_opt() == favorite_path) {
|
||||||
favorites.push(favorite);
|
favorites.push(favorite);
|
||||||
}
|
}
|
||||||
config_set!(favorites, favorites);
|
config_set!(favorites, favorites);
|
||||||
|
|
@ -5271,6 +5307,26 @@ impl Application for App {
|
||||||
return self.update_config();
|
return self.update_config();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavMenuAction::ChangeSidebarLabel(entity) => {
|
||||||
|
if let Some(favorite) = self
|
||||||
|
.nav_model
|
||||||
|
.data::<FavoriteIndex>(entity)
|
||||||
|
.and_then(|FavoriteIndex(favorite_i)| {
|
||||||
|
self.config.favorites.get(*favorite_i)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
let label = favorite
|
||||||
|
.display_name()
|
||||||
|
.unwrap_or_else(|| fl!("filesystem"));
|
||||||
|
return Task::batch([
|
||||||
|
self.dialog_pages
|
||||||
|
.push_back(DialogPage::ChangeSidebarLabel { entity, label }),
|
||||||
|
widget::text_input::focus(self.dialog_text_input.clone()),
|
||||||
|
widget::text_input::select_all(self.dialog_text_input.clone()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Message::Recents => {
|
Message::Recents => {
|
||||||
if self.config.show_recents {
|
if self.config.show_recents {
|
||||||
|
|
@ -6164,6 +6220,40 @@ impl Application for App {
|
||||||
target = target
|
target = target
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
DialogPage::ChangeSidebarLabel { entity, label } => {
|
||||||
|
let entity = *entity;
|
||||||
|
let complete_maybe = if label.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Message::DialogComplete)
|
||||||
|
};
|
||||||
|
|
||||||
|
widget::dialog()
|
||||||
|
.title(fl!("change-sidebar-label"))
|
||||||
|
.primary_action(
|
||||||
|
widget::button::suggested(fl!("save"))
|
||||||
|
.on_press_maybe(complete_maybe.clone()),
|
||||||
|
)
|
||||||
|
.secondary_action(
|
||||||
|
widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
|
||||||
|
)
|
||||||
|
.control(
|
||||||
|
widget::column::with_children([
|
||||||
|
widget::text::body(fl!("sidebar-label")).into(),
|
||||||
|
widget::text_input("", label.as_str())
|
||||||
|
.id(self.dialog_text_input.clone())
|
||||||
|
.on_input(move |label| {
|
||||||
|
Message::DialogUpdate(DialogPage::ChangeSidebarLabel {
|
||||||
|
entity,
|
||||||
|
label,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.spacing(space_xxs),
|
||||||
|
)
|
||||||
|
}
|
||||||
DialogPage::RenameItem {
|
DialogPage::RenameItem {
|
||||||
from,
|
from,
|
||||||
parent,
|
parent,
|
||||||
|
|
|
||||||
100
src/config.rs
100
src/config.rs
|
|
@ -70,6 +70,11 @@ pub enum Favorite {
|
||||||
name: String,
|
name: String,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
},
|
},
|
||||||
|
/// A path with a custom name chosen by the user
|
||||||
|
Named {
|
||||||
|
path: PathBuf,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Favorite {
|
impl Favorite {
|
||||||
|
|
@ -98,6 +103,38 @@ impl Favorite {
|
||||||
Self::Videos => dirs::video_dir(),
|
Self::Videos => dirs::video_dir(),
|
||||||
Self::Path(path) => Some(path.clone()),
|
Self::Path(path) => Some(path.clone()),
|
||||||
Self::Network { path, .. } => Some(path.clone()),
|
Self::Network { path, .. } => Some(path.clone()),
|
||||||
|
Self::Named { path, .. } => Some(path.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name shown in the sidebar, or `None` if the path has no usable file name
|
||||||
|
pub fn display_name(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
Self::Home => Some(crate::fl!("home")),
|
||||||
|
Self::Named { name, .. } | Self::Network { name, .. } => Some(name.clone()),
|
||||||
|
_ => self
|
||||||
|
.path_opt()?
|
||||||
|
.file_name()
|
||||||
|
.and_then(|x| x.to_str())
|
||||||
|
.map(ToString::to_string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return this favorite with a custom sidebar label chosen by the user
|
||||||
|
pub fn with_label(&self, name: &str) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Network { uri, path, .. } => Self::Network {
|
||||||
|
uri: uri.clone(),
|
||||||
|
name: name.to_string(),
|
||||||
|
path: path.clone(),
|
||||||
|
},
|
||||||
|
other => match other.path_opt() {
|
||||||
|
Some(path) => Self::Named {
|
||||||
|
path,
|
||||||
|
name: name.to_string(),
|
||||||
|
},
|
||||||
|
None => other.clone(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -385,3 +422,66 @@ pub const TIME_CONFIG_ID: &str = "com.system76.CosmicAppletTime";
|
||||||
pub struct TimeConfig {
|
pub struct TimeConfig {
|
||||||
pub military_time: bool,
|
pub military_time: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn favorite_with_label_converts_path_to_named() {
|
||||||
|
let favorite = Favorite::Path(PathBuf::from("/some/dir"));
|
||||||
|
assert_eq!(
|
||||||
|
favorite.with_label("Custom"),
|
||||||
|
Favorite::Named {
|
||||||
|
path: PathBuf::from("/some/dir"),
|
||||||
|
name: "Custom".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn favorite_with_label_updates_network_in_place() {
|
||||||
|
let favorite = Favorite::Network {
|
||||||
|
uri: "sftp://example.com/".to_string(),
|
||||||
|
name: "example.com".to_string(),
|
||||||
|
path: PathBuf::from("/run/mount/example"),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
favorite.with_label("Custom"),
|
||||||
|
Favorite::Network {
|
||||||
|
uri: "sftp://example.com/".to_string(),
|
||||||
|
name: "Custom".to_string(),
|
||||||
|
path: PathBuf::from("/run/mount/example"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn favorite_with_label_converts_special_folder_to_named() {
|
||||||
|
let home = dirs::home_dir().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
Favorite::Home.with_label("Custom"),
|
||||||
|
Favorite::Named {
|
||||||
|
path: home,
|
||||||
|
name: "Custom".to_string(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn favorite_display_name() {
|
||||||
|
assert_eq!(
|
||||||
|
Favorite::Path(PathBuf::from("/some/dir")).display_name(),
|
||||||
|
Some("dir".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(Favorite::Path(PathBuf::from("/")).display_name(), None);
|
||||||
|
assert_eq!(
|
||||||
|
Favorite::Named {
|
||||||
|
path: PathBuf::from("/some/dir"),
|
||||||
|
name: "Custom".to_string(),
|
||||||
|
}
|
||||||
|
.display_name(),
|
||||||
|
Some("Custom".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,7 @@ use std::{env, fmt, fs};
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
|
Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
|
||||||
};
|
};
|
||||||
use crate::config::{
|
use crate::config::{Config, DialogConfig, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch};
|
||||||
Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,
|
|
||||||
};
|
|
||||||
use crate::key_bind::key_binds;
|
use crate::key_bind::key_binds;
|
||||||
use crate::localize::LANGUAGE_SORTER;
|
use crate::localize::LANGUAGE_SORTER;
|
||||||
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
|
use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
|
||||||
|
|
@ -889,13 +887,7 @@ impl App {
|
||||||
|
|
||||||
for favorite in &self.flags.config.favorites {
|
for favorite in &self.flags.config.favorites {
|
||||||
if let Some(path) = favorite.path_opt() {
|
if let Some(path) = favorite.path_opt() {
|
||||||
let name = if matches!(favorite, Favorite::Home) {
|
let Some(name) = favorite.display_name() else {
|
||||||
fl!("home")
|
|
||||||
} else if let Favorite::Network { name, .. } = favorite {
|
|
||||||
name.clone()
|
|
||||||
} else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
|
|
||||||
file_name.to_string()
|
|
||||||
} else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
nav_model = nav_model.insert(move |b| {
|
nav_model = nav_model.insert(move |b| {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue