collective

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

diff --git a/apps/artcraft-launcher/src/settings_file.rs b/apps/artcraft-launcher/src/settings_file.rs--- a/apps/artcraft-launcher/src/settings_file.rs+++ b/apps/artcraft-launcher/src/settings_file.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));     }diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs--- a/crates/ui-egui/src/app.rs+++ b/crates/ui-egui/src/app.rs@@ -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| {diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv--- a/crates/ui-egui/src/i18n/cs.tsv+++ b/crates/ui-egui/src/i18n/cs.tsv@@ -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ídiff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv--- a/crates/ui-egui/src/i18n/de.tsv+++ b/crates/ui-egui/src/i18n/de.tsv@@ -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 entfernendiff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv--- a/crates/ui-egui/src/i18n/es.tsv+++ b/crates/ui-egui/src/i18n/es.tsv@@ -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ápidodiff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv--- a/crates/ui-egui/src/i18n/fr.tsv+++ b/crates/ui-egui/src/i18n/fr.tsv@@ -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 rapidediff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv--- a/crates/ui-egui/src/i18n/hu.tsv+++ b/crates/ui-egui/src/i18n/hu.tsv@@ -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óldiff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv--- a/crates/ui-egui/src/i18n/ja.tsv+++ b/crates/ui-egui/src/i18n/ja.tsv@@ -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	クイック起動から削除diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv--- a/crates/ui-egui/src/i18n/pl.tsv+++ b/crates/ui-egui/src/i18n/pl.tsv@@ -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 uruchamianiadiff --git a/crates/ui-egui/src/i18n/pt-BR.tsv b/crates/ui-egui/src/i18n/pt-BR.tsv--- a/crates/ui-egui/src/i18n/pt-BR.tsv+++ b/crates/ui-egui/src/i18n/pt-BR.tsv@@ -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ápidadiff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv--- a/crates/ui-egui/src/i18n/ru.tsv+++ b/crates/ui-egui/src/i18n/ru.tsv@@ -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	Убрать из быстрого запускаdiff --git a/crates/ui-egui/src/i18n/zh-CN.tsv b/crates/ui-egui/src/i18n/zh-CN.tsv--- a/crates/ui-egui/src/i18n/zh-CN.tsv+++ b/crates/ui-egui/src/i18n/zh-CN.tsv@@ -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	从快速启动移除diff --git a/crates/ui-egui/src/settings.rs b/crates/ui-egui/src/settings.rs--- a/crates/ui-egui/src/settings.rs+++ b/crates/ui-egui/src/settings.rs@@ -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(),         }     } }diff --git a/crates/ui-egui/src/views.rs b/crates/ui-egui/src/views.rs--- a/crates/ui-egui/src/views.rs+++ b/crates/ui-egui/src/views.rs@@ -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);+                });             });         }     }diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs--- a/crates/ui-egui/src/widgets.rs+++ b/crates/ui-egui/src/widgets.rs@@ -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) }