icon.rs
| 1 | use api_types::FileKind; |
| 2 | use leptos::prelude::*; |
| 3 | |
| 4 | #[derive(Clone, Copy, PartialEq, Debug)] |
| 5 | // Several variants are used from later milestones (context menu icons). |
| 6 | #[allow(dead_code)] |
| 7 | pub enum IconName { |
| 8 | Folder, |
| 9 | File, |
| 10 | Image, |
| 11 | Video, |
| 12 | Audio, |
| 13 | Text, |
| 14 | Code, |
| 15 | Archive, |
| 16 | Pdf, |
| 17 | Grid, |
| 18 | List, |
| 19 | Refresh, |
| 20 | Upload, |
| 21 | FolderPlus, |
| 22 | Download, |
| 23 | Pencil, |
| 24 | Move, |
| 25 | Copy, |
| 26 | Share, |
| 27 | Info, |
| 28 | Trash, |
| 29 | } |
| 30 | |
| 31 | /// Extensions that get the "code" icon instead of the plain-text one. Both are |
| 32 | /// [`FileKind::Text`] to the server — code-vs-prose is a naming question, not a |
| 33 | /// content one, so it stays here. |
| 34 | const CODE_EXTS: &[&str] = &[ |
| 35 | "rs", "js", "mjs", "cjs", "ts", "tsx", "jsx", "py", "rb", "go", "c", "cc", "cpp", "h", "hpp", |
| 36 | "java", "kt", "cs", "php", "swift", "lua", "sh", "bash", "fish", "zsh", "ps1", "json", "toml", |
| 37 | "yaml", "yml", "html", "htm", "css", "scss", "sql", "xml", "vue", "svelte", |
| 38 | ]; |
| 39 | |
| 40 | /// Pick the icon for an entry from its server-sniffed [`FileKind`]. |
| 41 | /// |
| 42 | /// `name` is used only to split [`FileKind::Text`] into prose and code. |
| 43 | pub fn icon_for(kind: FileKind, name: &str) -> IconName { |
| 44 | match kind { |
| 45 | FileKind::Dir => IconName::Folder, |
| 46 | FileKind::Image => IconName::Image, |
| 47 | FileKind::Video => IconName::Video, |
| 48 | FileKind::Audio => IconName::Audio, |
| 49 | FileKind::Pdf => IconName::Pdf, |
| 50 | FileKind::Archive => IconName::Archive, |
| 51 | FileKind::Text => { |
| 52 | let ext = name.rsplit('.').next().unwrap_or("").to_lowercase(); |
| 53 | if CODE_EXTS.contains(&ext.as_str()) { |
| 54 | IconName::Code |
| 55 | } else { |
| 56 | IconName::Text |
| 57 | } |
| 58 | } |
| 59 | FileKind::Binary => IconName::File, |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | fn icon_d(name: IconName) -> &'static str { |
| 64 | match name { |
| 65 | IconName::Folder => { |
| 66 | "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" |
| 67 | } |
| 68 | IconName::File => "M6 2h8l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z M13 2v6h6", |
| 69 | IconName::Image => { |
| 70 | "M3 5h18v14H3z M10 10a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0z M21 15l-5-5-9 9" |
| 71 | } |
| 72 | IconName::Video => "M3 5h18v14H3z M10 9l5 3-5 3z", |
| 73 | IconName::Audio => { |
| 74 | "M9 18V6l10-2v10 M9 18a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z M19 15a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z" |
| 75 | } |
| 76 | IconName::Text => "M5 3h14v18H5z M9 8h6 M9 12h6 M9 16h4", |
| 77 | IconName::Code => "M8 8l-4 4 4 4 M16 8l4 4-4 4", |
| 78 | IconName::Archive => "M4 4h16v16H4z M12 4v2 M12 8v2 M12 12v2 M9 16h6v4H9z", |
| 79 | IconName::Pdf => { |
| 80 | "M6 2h8l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z M13 2v6h6 M8 13h8 M8 17h8" |
| 81 | } |
| 82 | IconName::Grid => "M4 4h7v7H4z M13 4h7v7h-7z M4 13h7v7H4z M13 13h7v7h-7z", |
| 83 | IconName::List => "M4 6h2 M9 6h11 M4 12h2 M9 12h11 M4 18h2 M9 18h11", |
| 84 | IconName::Refresh => "M21 12a9 9 0 1 1-3-6.7 M21 3v6h-6", |
| 85 | IconName::Upload => "M12 16V4 M7 9l5-5 5 5 M4 20h16", |
| 86 | IconName::FolderPlus => { |
| 87 | "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M12 11v6 M9 14h6" |
| 88 | } |
| 89 | IconName::Download => "M12 4v12 M7 11l5 5 5-5 M4 20h16", |
| 90 | IconName::Pencil => "M4 20l1-4L16 5l3 3L8 19z M13 8l3 3", |
| 91 | IconName::Move => "M20 7H6 M9 4L6 7l3 3 M4 17h14 M15 14l3 3-3 3", |
| 92 | IconName::Copy => "M8 8h12v12H8z M4 16V4h12", |
| 93 | IconName::Share => "M4 13v6a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-6 M12 3v11 M8 7l4-4 4 4", |
| 94 | IconName::Info => "M12 3a9 9 0 1 0 .01 0z M12 11v5 M12 8h.01", |
| 95 | IconName::Trash => "M4 7h16 M9 7V4h6v3 M6 7l1 13h10l1-13 M10 11v6 M14 11v6", |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | fn icon_class(name: IconName) -> &'static str { |
| 100 | match name { |
| 101 | IconName::Folder | IconName::FolderPlus => "ic-folder", |
| 102 | _ => "ic-muted", |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | #[component] |
| 107 | pub fn Icon(name: IconName, class: Option<String>) -> impl IntoView { |
| 108 | let mut cls = String::from("icon "); |
| 109 | cls.push_str(icon_class(name)); |
| 110 | if let Some(extra) = class { |
| 111 | cls.push(' '); |
| 112 | cls.push_str(&extra); |
| 113 | } |
| 114 | view! { |
| 115 | <svg |
| 116 | class=cls |
| 117 | viewBox="0 0 24 24" |
| 118 | fill="none" |
| 119 | stroke="currentColor" |
| 120 | stroke_width="1.8" |
| 121 | stroke_linecap="round" |
| 122 | stroke_linejoin="round" |
| 123 | aria-hidden="true" |
| 124 | > |
| 125 | <path d=icon_d(name)/> |
| 126 | </svg> |
| 127 | } |
| 128 | } |
| 129 |