Browse / github.com/storytold/craft-launcher / patch
Added quick launch feature
Added quick launch feature ## Summary Adds a **Quick Launch** section to ArtCraft Launcher, allowing users to favourite their most-used Craft applications for faster access. ## Changes - Add a Quick Launch section at the top of the launcher. - Allow users to favourite or unfavourite applications using a star icon on each app card. - Display favourited applications in a compact, easy-to-access layout. - Allow users to launch their favourite applications directly from Quick Launch. - Persist favourites between sessions. - Automatically update Quick Launch when applications are added to or removed from favourites. - Hide
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- AJMakesStuff (imported from pull request #937, open)
- Base
7aafad661a11594a70ae949471d481f30d8fe64a- Size
- +164 −10 in 15 files
- Review
- pending
- Shared
- 2026-10-28
- Id
210ecb1f77494f48306661e2
How it went
No other operator has reported using it yet.
Diff
raw · apps/artcraft-launcher/src/settings_file.rs crates/ui-egui/src/app.rs crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-BR.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/zh-CN.tsv crates/ui-egui/src/settings.rs crates/ui-egui/src/views.rs crates/ui-egui/src/widgets.rs
@@ -110,7 +110,12 @@ mod tests { #[test] fn settings_saved_with_no_file_read_back() { let dir = Scratch::new("settings-round-trip");- let settings = Settings { last_check: Some(1_791_500_000), notify: false, ..Settings::default() };+ let settings = Settings {+ last_check: Some(1_791_500_000),+ notify: false,+ pinned: ["photocraft".to_string(), "gridcraft".to_string()].into(),+ ..Settings::default()+ }; assert_eq!(save(&dir.file(), "settings", &settings), Ok(())); assert_eq!(load::<Settings>(&dir.file(), "settings"), Some(settings)); }@@ -93,6 +93,7 @@ pub(crate) enum UiAction { Launch(String), /// Re-run the manual update check, clearing the auto-update back-off. CheckNow,+ TogglePin(String), /// A message a UI interaction asked to show. Toast { text: String,@@ -372,6 +373,11 @@ impl LauncherUi { *opened = opened.saturating_add(1); self.backend.send(Command::Launch { app: id.clone() }); }+ UiAction::TogglePin(id) => {+ if !self.settings.pinned.remove(id) {+ self.settings.pinned.insert(id.clone());+ }+ } UiAction::CheckNow => { self.auto_update_attempted.clear(); self.backend.send(Command::CheckForUpdates { force: true });@@ -384,6 +390,10 @@ impl LauncherUi { } } + pub(crate) fn toggle_pin(&mut self, id: &str) {+ self.pending.push(UiAction::TogglePin(id.to_string()));+ }+ /// Queues a message for the next pass that can actually draw. Its age is deliberately not /// part of the queue: a job that finished while the window was hidden is still news when the /// window first opens, so the countdown starts on display (see [`Self::draw_toasts`]).@@ -797,6 +807,7 @@ impl LauncherUi { ViewMode::Table => egui::ScrollArea::both(), }; scroll.auto_shrink(false).show(ui, |ui| {+ self.quick_launch(ui); if ids.is_empty() { ui.add_space(40.0); ui.vertical_centered(|ui| {@@ -349,3 +349,7 @@ filter Installed Nainstalováno Show {n} more changes Zobrazit dalších změn: {n} Last updated: {date} Naposledy aktualizováno: {date} column Version Verze++ Quick Launch Rychlé spuštění+ Add to Quick Launch Přidat do rychlého spuštění+ Remove from Quick Launch Odebrat z rychlého spuštění@@ -381,3 +381,7 @@ filter Installed Installiert Show {n} more changes {n} weitere Änderungen anzeigen Last updated: {date} Zuletzt aktualisiert: {date} column Version Version++ Quick Launch Schnellstart+ Add to Quick Launch Zum Schnellstart hinzufügen+ Remove from Quick Launch Aus Schnellstart entfernen@@ -362,3 +362,7 @@ filter Installed Instaladas Show {n} more changes Mostrar {n} cambios más Last updated: {date} Última actualización: {date} column Version Versión++ Quick Launch Inicio rápido+ Add to Quick Launch Añadir a Inicio rápido+ Remove from Quick Launch Quitar de Inicio rápido@@ -379,3 +379,7 @@ filter Installed Installées Show {n} more changes Afficher {n} modifications de plus Last updated: {date} Dernière mise à jour : {date} column Version Version++ Quick Launch Lancement rapide+ Add to Quick Launch Ajouter au lancement rapide+ Remove from Quick Launch Retirer du lancement rapide@@ -382,3 +382,7 @@ filter Installed Telepítve Show {n} more changes {n} további változás megjelenítése Last updated: {date} Utoljára frissítve: {date} column Version Verzió++ Quick Launch Gyorsindítás+ Add to Quick Launch Hozzáadás a gyorsindításhoz+ Remove from Quick Launch Eltávolítás a gyorsindításból@@ -384,3 +384,7 @@ filter Installed インストール済み Show {n} more changes さらに{n}件の変更を表示 Last updated: {date} 最終更新: {date} column Version バージョン++ Quick Launch クイック起動+ Add to Quick Launch クイック起動に追加+ Remove from Quick Launch クイック起動から削除@@ -383,3 +383,7 @@ filter Installed Zainstalowane Show {n} more changes Pokaż {n} więcej zmian Last updated: {date} Ostatnia aktualizacja: {date} column Version Wersja++ Quick Launch Szybkie uruchamianie+ Add to Quick Launch Dodaj do szybkiego uruchamiania+ Remove from Quick Launch Usuń z szybkiego uruchamiania@@ -369,3 +369,7 @@ filter Installed Instalados Show {n} more changes Mostrar mais {n} alterações Last updated: {date} Última atualização: {date} column Version Versão++ Quick Launch Inicialização rápida+ Add to Quick Launch Adicionar à Inicialização rápida+ Remove from Quick Launch Remover da Inicialização rápida@@ -377,3 +377,7 @@ filter Installed Установлены Show {n} more changes Показать ещё изменений: {n} Last updated: {date} Последнее обновление: {date} column Version Версия++ Quick Launch Быстрый запуск+ Add to Quick Launch Добавить в быстрый запуск+ Remove from Quick Launch Убрать из быстрого запуска@@ -349,3 +349,7 @@ figcraft Interface design and prototyping 界面设计与原型制作 Show {n} more changes 显示另外 {n} 项更改 Last updated: {date} 最后更新:{date} column Version 版本++ Quick Launch 快速启动+ Add to Quick Launch 添加到快速启动+ Remove from Quick Launch 从快速启动移除@@ -122,6 +122,8 @@ pub struct Settings { pub tray_hint_shown: bool, /// How many times each app was opened from the launcher (catalog id → count), for "Frequently used". pub launches: BTreeMap<String, u32>,+ /// Catalog ids pinned to Quick Launch.+ pub pinned: std::collections::BTreeSet<String>, } impl Settings {@@ -204,6 +206,7 @@ impl Default for Settings { hidden: std::collections::BTreeSet::new(), tray_hint_shown: false, launches: BTreeMap::new(),+ pinned: std::collections::BTreeSet::new(), } } }@@ -63,6 +63,62 @@ const CARD_PAD: f32 = 20.0; const TRAFFIC_LIGHTS_W: f32 = 72.0; impl LauncherUi {+ fn pin_button(&mut self, ui: &mut Ui, id: &str) {+ self.pin_button_with_offset(ui, id, egui::Vec2::ZERO);+ }++ fn pin_button_with_offset(&mut self, ui: &mut Ui, id: &str, offset: egui::Vec2) {+ let pinned = self.settings.pinned.contains(id);+ let label = if pinned { tl!("Remove from Quick Launch") } else { tl!("Add to Quick Launch") };+ if widgets::glyph_button_with_offset(ui, widgets::Glyph::Star(pinned), label, offset).clicked() {+ self.toggle_pin(id);+ }+ }++ pub(crate) fn quick_launch(&mut self, ui: &mut Ui) {+ let entries: Vec<_> =+ self.entries.iter().filter(|e| self.settings.pinned.contains(e.app.id) && !self.settings.hidden.contains(e.app.id)).cloned().collect();+ if entries.is_empty() {+ return;+ }+ let t = Tokens::get(ui.ctx());+ widgets::hud(ui, tl!("Quick Launch"), t.muted);+ ui.add_space(8.0);+ ui.push_id("quick-launch", |ui| {+ ui.horizontal_wrapped(|ui| {+ for entry in entries {+ ui.push_id(entry.app.id, |ui| {+ egui::Frame::new().stroke(t.hairline()).inner_margin(8).show(ui, |ui| {+ ui.horizontal(|ui| {+ self.pin_button(ui, entry.app.id);+ let status = entry.status(self.platform, self.caps.manage_apps);+ let installed = status.is_installed();+ if let Some(icon) = crate::icons::app_image(ui.ctx(), entry.app.id, installed) {+ ui.add(icon.fit_to_exact_size(vec2(24.0, 24.0)));+ }+ widgets::wordmark_in(ui, entry.app, 16.0, installed);+ if let Some(phase) = entry.job {+ let label = match phase {+ JobPhase::Downloading { .. } => tl!("Downloading…"),+ JobPhase::Reconnecting { .. } => tl!("Reconnecting…"),+ JobPhase::Verifying => tl!("Verifying checksum…"),+ JobPhase::Waiting => tl!("Waiting for another app…"),+ JobPhase::Installing => tl!("Installing…"),+ JobPhase::Removing => tl!("Removing…"),+ };+ ui.add_enabled_ui(false, |ui| widgets::button(ui, label, Kind::Secondary, 100.0));+ } else {+ self.primary_button(ui, &entry, entry.app.id, &status, 100.0);+ }+ });+ });+ });+ }+ });+ });+ ui.add_space(18.0);+ }+ /// The header bar; with a custom or macOS title bar it is also the window's title bar. pub(crate) fn header(&mut self, ui: &mut Ui, bar: TitleBar) { let t = Tokens::get(ui.ctx());@@ -477,11 +533,12 @@ impl LauncherUi { ); if !version.is_empty() { let github_w = widgets::hud_galley(&strip_ui, &format!("{} ↗", tl!("GitHub")), t.muted).size().x;- let room = (strip_ui.available_width() - github_w - 16.0).max(0.0);+ let room = (strip_ui.available_width() - github_w - theme::CONTROL_H - 32.0).max(0.0); widgets::hud_elided(&mut strip_ui, &version, if in_colour { t.app_ink(app) } else { t.muted }, room); } strip_ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.spacing_mut().item_spacing.x = 16.0;+ self.pin_button_with_offset(ui, id, vec2(4.0, 0.0)); widgets::hud_link(ui, tl!("GitHub"), &app.repo_url()); }); }@@ -574,7 +631,7 @@ impl LauncherUi { .column(Column::exact(176.0)) .column(Column::exact(150.0)) .column(Column::exact(104.0))- .column(Column::exact(206.0))+ .column(Column::exact(220.0)) .header(42.0, |header| self.table_header(header, &mut sort_clicked, &t, span, clip)) .body(|body| self.table_rows(body, ids, &t, (title_h, tagline_h), now, (span, clip))); });@@ -689,7 +746,10 @@ impl LauncherUi { row.col(|ui| self.table_status_cell(ui, &entry, &status, t)); row.col(|ui| self.table_version_cell(ui, &entry, &status, t)); row.col(|ui| self.table_released_cell(ui, &entry, now, t));- row.col(|ui| self.actions(ui, &entry, &status, 204.0));+ row.col(|ui| {+ self.pin_button(ui, id);+ self.actions(ui, &entry, &status, 168.0);+ }); }); } }@@ -503,6 +503,7 @@ pub enum Glyph { Close, Settings, Refresh,+ Star(bool), } /// Paints one of the launcher's symbols with the painter, centred in `rect`.@@ -522,6 +523,21 @@ pub fn paint_glyph(p: &egui::Painter, rect: Rect, glyph: Glyph, color: Color32) fn glyph_shapes(rect: Rect, glyph: Glyph, color: Color32) -> Vec<egui::Shape> { let m = rect.center(); match glyph {+ Glyph::Star(selected) => {+ let points: Vec<_> = (0..10)+ .map(|i| {+ let angle = std::f32::consts::TAU * i as f32 / 10.0 - std::f32::consts::FRAC_PI_2;+ let radius = if i % 2 == 0 { 7.0 } else { 3.2 };+ m + vec2(angle.cos(), angle.sin()) * radius+ })+ .collect();+ vec![egui::Shape::Path(egui::epaint::PathShape {+ points,+ closed: true,+ fill: if selected { color } else { Color32::TRANSPARENT },+ stroke: Stroke::new(1.2, color).into(),+ })]+ } Glyph::Pause => [-3.0, 3.0] .into_iter() .map(|dx| egui::Shape::rect_filled(Rect::from_center_size(m + vec2(dx, 0.0), vec2(2.5, 10.0)), CornerRadius::ZERO, color))@@ -562,19 +578,38 @@ fn glyph_shapes(rect: Rect, glyph: Glyph, color: Color32) -> Vec<egui::Shape> { } } -/// A small outlined square button with a drawn glyph; inverts on hover.+/// A square button with a drawn glyph; stars are borderless and other glyphs invert on hover. pub fn glyph_button(ui: &mut Ui, glyph: Glyph, tip: &str) -> Response {+ glyph_button_with_offset(ui, glyph, tip, egui::Vec2::ZERO)+}++/// A glyph button whose painted control is nudged without moving its click target.+pub fn glyph_button_with_offset(ui: &mut Ui, glyph: Glyph, tip: &str, offset: egui::Vec2) -> Response { let t = Tokens::get(ui.ctx()); let (rect, resp) = ui.allocate_exact_size(vec2(theme::CONTROL_H, theme::CONTROL_H), Sense::click());+ let paint_rect = rect.translate(offset); let hovered = resp.hovered(); let p = ui.painter();- if hovered {- p.rect_filled(rect, CornerRadius::ZERO, t.invert_bg);+ let is_star = matches!(glyph, Glyph::Star(_));+ if is_star {+ // Stars sit beside text links on app cards, so keep their full click target but omit the+ // square control outline. A quiet hover wash still makes the affordance discoverable.+ if hovered {+ p.rect_filled(paint_rect, CornerRadius::ZERO, t.line);+ }+ } else if hovered {+ p.rect_filled(paint_rect, CornerRadius::ZERO, t.invert_bg); } else {- p.rect_stroke(rect, CornerRadius::ZERO, t.hairline(), egui::StrokeKind::Inside);+ p.rect_stroke(paint_rect, CornerRadius::ZERO, t.hairline(), egui::StrokeKind::Inside); }- let c = if hovered { t.invert_fg } else { t.muted };- paint_glyph(p, rect, glyph, c);+ let c = match glyph {+ Glyph::Star(true) => t.accent_ink,+ Glyph::Star(false) if hovered => t.ink_strong,+ Glyph::Star(false) => t.faint,+ _ if hovered => t.invert_fg,+ _ => t.muted,+ };+ paint_glyph(p, paint_rect, glyph, c); resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tip)); resp.on_hover_text(tip).on_hover_cursor(egui::CursorIcon::PointingHand) }