Sfoglia il codice sorgente

feat: use stylized icon choices for pinned shortcuts

Krasimir Kazakov 1 settimana fa
parent
commit
035d933fb7

+ 15 - 0
assets/icons/bookmark.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-bookmark"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
+</svg>

+ 18 - 0
assets/icons/calendar.svg

@@ -0,0 +1,18 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-calendar"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M8 2v3" />
+  <path d="M16 2v3" />
+  <rect x="3" y="3" width="18" height="18" rx="2" />
+  <path d="M3 9h18" />
+</svg>

+ 16 - 0
assets/icons/camera.svg

@@ -0,0 +1,16 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-camera"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z" />
+  <circle cx="12" cy="13" r="3" />
+</svg>

+ 18 - 0
assets/icons/car.svg

@@ -0,0 +1,18 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-car"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2" />
+  <circle cx="7" cy="17" r="2" />
+  <path d="M9 17h6" />
+  <circle cx="17" cy="17" r="2" />
+</svg>

+ 16 - 0
assets/icons/clock.svg

@@ -0,0 +1,16 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-clock"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <circle cx="12" cy="12" r="10" />
+  <path d="M12 6v6l4 2" />
+</svg>

+ 15 - 0
assets/icons/cloud.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-cloud"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
+</svg>

+ 18 - 0
assets/icons/coffee.svg

@@ -0,0 +1,18 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-coffee"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M10 2v2" />
+  <path d="M14 2v2" />
+  <path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1" />
+  <path d="M6 2v2" />
+</svg>

+ 15 - 0
assets/icons/folder.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-folder"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
+</svg>

+ 19 - 0
assets/icons/gamepad-2.svg

@@ -0,0 +1,19 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-gamepad-2"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <line x1="6" x2="10" y1="11" y2="11" />
+  <line x1="8" x2="8" y1="9" y2="13" />
+  <line x1="15" x2="15.01" y1="12" y2="12" />
+  <line x1="18" x2="18.01" y1="10" y2="10" />
+  <path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59c-.006.052-.01.101-.017.152C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258-.007-.05-.011-.1-.017-.151A4 4 0 0 0 17.32 5z" />
+</svg>

+ 17 - 0
assets/icons/globe.svg

@@ -0,0 +1,17 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-globe"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <circle cx="12" cy="12" r="10" />
+  <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
+  <path d="M2 12h20" />
+</svg>

+ 15 - 0
assets/icons/headphones.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-headphones"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3" />
+</svg>

+ 15 - 0
assets/icons/heart.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-heart"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5" />
+</svg>

+ 16 - 0
assets/icons/home.svg

@@ -0,0 +1,16 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-home"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
+  <path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
+</svg>

+ 16 - 0
assets/icons/mail.svg

@@ -0,0 +1,16 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-mail"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
+  <rect x="2" y="4" width="20" height="16" rx="2" />
+</svg>

+ 17 - 0
assets/icons/map.svg

@@ -0,0 +1,17 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-map"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z" />
+  <path d="M15 5.764v15" />
+  <path d="M9 3.236v15" />
+</svg>

+ 15 - 0
assets/icons/message-circle.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-message-circle"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
+</svg>

+ 15 - 0
assets/icons/phone.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-phone"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384" />
+</svg>

+ 15 - 0
assets/icons/play.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-play"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" />
+</svg>

+ 17 - 0
assets/icons/shopping-bag.svg

@@ -0,0 +1,17 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-shopping-bag"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M16 10a4 4 0 0 1-8 0" />
+  <path d="M3.103 6.034h17.794" />
+  <path d="M3.4 5.467a2 2 0 0 0-.4 1.2V20a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6.667a2 2 0 0 0-.4-1.2l-2-2.667A2 2 0 0 0 17 2H7a2 2 0 0 0-1.6.8z" />
+</svg>

+ 15 - 0
assets/icons/star.svg

@@ -0,0 +1,15 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-star"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" />
+</svg>

+ 16 - 0
assets/icons/tv.svg

@@ -0,0 +1,16 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-tv"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="m17 2-5 5-5-5" />
+  <rect width="20" height="15" x="2" y="7" rx="2" />
+</svg>

+ 16 - 0
assets/icons/video.svg

@@ -0,0 +1,16 @@
+<!-- @license lucide-static v1.64.0 - ISC -->
+<svg
+  class="lucide lucide-video"
+  xmlns="http://www.w3.org/2000/svg"
+  width="24"
+  height="24"
+  viewBox="0 0 24 24"
+  fill="none"
+  stroke="currentColor"
+  stroke-width="2"
+  stroke-linecap="round"
+  stroke-linejoin="round"
+>
+  <path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" />
+  <rect x="2" y="6" width="14" height="12" rx="2" />
+</svg>

+ 7 - 13
lib/data/models/slot_assignment.dart

@@ -7,30 +7,25 @@ class SlotAssignment {
     required this.packageName,
     required this.activityName,
     required this.label,
-    this.iconPackageName,
-    this.iconActivityName,
+    this.iconAsset,
   });
 
   final String packageName;
   final String activityName;
   final String label;
 
-  /// Optional app whose icon should be used for this Home button. Only pinned
-  /// apps use this; slot assignments leave both fields null.
-  final String? iconPackageName;
-  final String? iconActivityName;
+  /// Optional stylized SVG icon for this Home button. Null means the pinned
+  /// app's own icon (or the generic fallback for fixed slots).
+  final String? iconAsset;
 
-  bool get hasCustomIcon =>
-      (iconPackageName?.isNotEmpty ?? false) &&
-      (iconActivityName?.isNotEmpty ?? false);
+  bool get hasCustomIcon => iconAsset?.isNotEmpty ?? false;
 
   factory SlotAssignment.fromMap(Map<dynamic, dynamic> map) {
     return SlotAssignment(
       packageName: (map['package'] as String?) ?? '',
       activityName: (map['activity'] as String?) ?? '',
       label: (map['label'] as String?) ?? '',
-      iconPackageName: map['iconPackage'] as String?,
-      iconActivityName: map['iconActivity'] as String?,
+      iconAsset: map['iconAsset'] as String?,
     );
   }
 
@@ -39,8 +34,7 @@ class SlotAssignment {
       'package': packageName,
       'activity': activityName,
       'label': label,
-      if (hasCustomIcon) 'iconPackage': iconPackageName,
-      if (hasCustomIcon) 'iconActivity': iconActivityName,
+      if (hasCustomIcon) 'iconAsset': iconAsset,
     };
   }
 

+ 3 - 11
lib/features/home/home_screen.dart

@@ -42,18 +42,13 @@ class HomeScreen extends StatelessWidget {
 
     ShortcutSpec slotSpec(SlotType slot) {
       final assignment = launcher.assignmentFor(slot);
-      final app = assignment == null
-          ? null
-          : launcher.appForAssignment(assignment);
       final playing =
           slot == SlotType.music && system.isMusicPlaying && assignment != null;
-      final nonSystemIcon = app != null && !app.isSystemApp ? app.icon : null;
 
       return ShortcutSpec(
         title: slot.label,
         subtitle: assignment?.label ?? 'Select app',
         iconAsset: slot.iconAsset,
-        iconBytes: playing ? app?.icon : nonSystemIcon,
         showEqualizer: playing,
         assigned: assignment != null,
         onTap: () => _openSlot(context, slot),
@@ -64,15 +59,12 @@ class HomeScreen extends StatelessWidget {
 
     ShortcutSpec pinnedSpec(SlotAssignment pinned) {
       final app = launcher.appForAssignment(pinned);
-      final customIcon = launcher.appForCustomIcon(pinned);
-      final iconSource = customIcon ?? app;
-      final useRealIcon =
-          iconSource != null && (customIcon != null || !iconSource.isSystemApp);
+      final customIcon = pinned.iconAsset;
       return ShortcutSpec(
         title: pinned.label.isEmpty ? pinned.packageName : pinned.label,
         subtitle: null,
-        iconAsset: 'assets/icons/app-window.svg',
-        iconBytes: useRealIcon ? iconSource.icon : null,
+        iconAsset: customIcon ?? 'assets/icons/app-window.svg',
+        iconBytes: customIcon == null ? app?.icon : null,
         assigned: true,
         onTap: () => _openPinned(context, pinned),
         onLongPress: () => showPinnedAppMenu(context, pinned),

+ 3 - 18
lib/features/home/launcher_controller.dart

@@ -65,19 +65,6 @@ class LauncherController extends ChangeNotifier {
     return null;
   }
 
-  AppEntry? appForCustomIcon(SlotAssignment assignment) {
-    if (!assignment.hasCustomIcon) return null;
-    final package = assignment.iconPackageName;
-    final activity = assignment.iconActivityName;
-    if (package == null || activity == null) return null;
-    for (final app in _apps) {
-      if (app.packageName == package && app.activityName == activity) {
-        return app;
-      }
-    }
-    return null;
-  }
-
   /// Loads persisted choices synchronously, then refreshes platform state in
   /// the background so the first frame is not blocked by package enumeration.
   void initialize() {
@@ -232,8 +219,7 @@ class LauncherController extends ChangeNotifier {
       packageName: current.packageName,
       activityName: current.activityName,
       label: trimmed,
-      iconPackageName: current.iconPackageName,
-      iconActivityName: current.iconActivityName,
+      iconAsset: current.iconAsset,
     );
     await _settings.setPinnedApps(_pinnedApps);
     _notify();
@@ -241,7 +227,7 @@ class LauncherController extends ChangeNotifier {
 
   Future<void> setPinnedIcon(
     SlotAssignment assignment,
-    AppEntry? iconSource,
+    String? iconAsset,
   ) async {
     final index = _pinnedApps.indexOf(assignment);
     if (index < 0) return;
@@ -250,8 +236,7 @@ class LauncherController extends ChangeNotifier {
       packageName: current.packageName,
       activityName: current.activityName,
       label: current.label,
-      iconPackageName: iconSource?.packageName,
-      iconActivityName: iconSource?.activityName,
+      iconAsset: iconAsset,
     );
     await _settings.setPinnedApps(_pinnedApps);
     _notify();

+ 88 - 97
lib/features/home/pinned_app_actions.dart

@@ -6,9 +6,9 @@ import '../../core/widgets/app_icon.dart';
 import '../../core/widgets/app_icon_image.dart';
 import '../../core/widgets/pressable.dart';
 import '../../core/widgets/surface_card.dart';
-import '../../data/models/app_entry.dart';
 import '../../data/models/slot_assignment.dart';
 import 'launcher_controller.dart';
+import 'style_icon_choices.dart';
 
 const _sheetBarrier = Color(0x8A000000);
 
@@ -19,7 +19,6 @@ Future<void> showPinnedAppMenu(
 ) async {
   final launcher = context.read<LauncherController>();
   final app = launcher.appForAssignment(assignment);
-  final iconApp = launcher.appForCustomIcon(assignment) ?? app;
 
   await showModalBottomSheet<void>(
     context: context,
@@ -51,7 +50,24 @@ Future<void> showPinnedAppMenu(
                 children: [
                   Row(
                     children: [
-                      AppIconImage(bytes: iconApp?.icon, size: 44),
+                      if (assignment.iconAsset != null)
+                        Container(
+                          width: 44,
+                          height: 44,
+                          decoration: BoxDecoration(
+                            color: colors.surfaceStrong,
+                            borderRadius: BorderRadius.circular(15),
+                            border: Border.all(color: colors.border),
+                          ),
+                          alignment: Alignment.center,
+                          child: AppIcon(
+                            assignment.iconAsset!,
+                            size: 22,
+                            color: colors.ink,
+                          ),
+                        )
+                      else
+                        AppIconImage(bytes: app?.icon, size: 44),
                       const SizedBox(width: 14),
                       Expanded(
                         child: Column(
@@ -202,47 +218,16 @@ Future<void> showPinnedIconPicker(
   );
 }
 
-class _PinnedIconSheet extends StatefulWidget {
+class _PinnedIconSheet extends StatelessWidget {
   const _PinnedIconSheet({required this.assignment});
 
   final SlotAssignment assignment;
 
-  @override
-  State<_PinnedIconSheet> createState() => _PinnedIconSheetState();
-}
-
-class _PinnedIconSheetState extends State<_PinnedIconSheet> {
-  @override
-  void initState() {
-    super.initState();
-    WidgetsBinding.instance.addPostFrameCallback((_) {
-      if (!mounted) return;
-      final launcher = context.read<LauncherController>();
-      if (!launcher.isLoadingApps) launcher.loadApps(force: true);
-    });
-  }
-
   @override
   Widget build(BuildContext context) {
     final colors = context.colors;
     final theme = Theme.of(context);
-    final launcher = context.watch<LauncherController>();
-    final current = launcher.appForAssignment(widget.assignment);
-    final choices = <AppEntry>[];
-    final seen = <String>{};
-
-    if (current != null) {
-      choices.add(current);
-      seen.add(current.componentId);
-    }
-    for (final app in launcher.apps) {
-      if (choices.length >= 25) break;
-      if (seen.add(app.componentId)) choices.add(app);
-    }
-
-    final selectedComponent = widget.assignment.hasCustomIcon
-        ? '${widget.assignment.iconPackageName}/${widget.assignment.iconActivityName}'
-        : '${widget.assignment.packageName}/${widget.assignment.activityName}';
+    final selectedAsset = assignment.iconAsset;
 
     return Container(
       height: MediaQuery.sizeOf(context).height * 0.86,
@@ -272,7 +257,7 @@ class _PinnedIconSheetState extends State<_PinnedIconSheet> {
                       Text('Change icon', style: theme.textTheme.headlineSmall),
                       const SizedBox(height: 3),
                       Text(
-                        'Choose an icon for ${widget.assignment.label}.',
+                        'Choose a stylized icon for ${assignment.label}.',
                         maxLines: 1,
                         overflow: TextOverflow.ellipsis,
                         style: theme.textTheme.bodySmall?.copyWith(
@@ -307,70 +292,76 @@ class _PinnedIconSheetState extends State<_PinnedIconSheet> {
           ),
           const SizedBox(height: 8),
           Expanded(
-            child: choices.isEmpty
-                ? Center(
-                    child: Text(
-                      'No installed apps to choose from.',
-                      style: theme.textTheme.bodyMedium?.copyWith(
-                        color: colors.inkMuted,
-                      ),
-                    ),
-                  )
-                : GridView.builder(
-                    padding: const EdgeInsets.fromLTRB(24, 4, 24, 28),
-                    gridDelegate:
-                        const SliverGridDelegateWithMaxCrossAxisExtent(
-                          maxCrossAxisExtent: 116,
-                          mainAxisExtent: 112,
-                          crossAxisSpacing: 10,
-                          mainAxisSpacing: 10,
+            child: GridView.builder(
+              padding: const EdgeInsets.fromLTRB(24, 4, 24, 28),
+              gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
+                maxCrossAxisExtent: 116,
+                mainAxisExtent: 108,
+                crossAxisSpacing: 10,
+                mainAxisSpacing: 10,
+              ),
+              itemCount: styleIconChoices.length + 1,
+              itemBuilder: (context, index) {
+                final isDefault = index == 0;
+                final choice = isDefault ? null : styleIconChoices[index - 1];
+                final asset = isDefault
+                    ? 'assets/icons/app-window.svg'
+                    : choice!.asset;
+                final label = isDefault ? 'Default' : choice!.label;
+                final selected = isDefault
+                    ? !assignment.hasCustomIcon
+                    : selectedAsset == asset;
+
+                return Material(
+                  color: Colors.transparent,
+                  child: InkWell(
+                    borderRadius: BorderRadius.circular(24),
+                    onTap: () async {
+                      final launcher = context.read<LauncherController>();
+                      await launcher.setPinnedIcon(
+                        assignment,
+                        isDefault ? null : asset,
+                      );
+                      if (context.mounted) Navigator.of(context).pop();
+                    },
+                    child: Container(
+                      padding: const EdgeInsets.fromLTRB(8, 10, 8, 8),
+                      decoration: BoxDecoration(
+                        color: colors.surfaceStrong,
+                        borderRadius: BorderRadius.circular(24),
+                        border: Border.all(
+                          color: selected ? colors.accent : colors.border,
+                          width: selected ? 1.8 : 1,
                         ),
-                    itemCount: choices.length,
-                    itemBuilder: (context, index) {
-                      final app = choices[index];
-                      final selected = app.componentId == selectedComponent;
-                      return Material(
-                        color: Colors.transparent,
-                        child: InkWell(
-                          borderRadius: BorderRadius.circular(24),
-                          onTap: () async {
-                            await launcher.setPinnedIcon(
-                              widget.assignment,
-                              app,
-                            );
-                            if (context.mounted) Navigator.of(context).pop();
-                          },
-                          child: Container(
-                            padding: const EdgeInsets.fromLTRB(8, 10, 8, 8),
-                            decoration: BoxDecoration(
-                              color: colors.surfaceStrong,
-                              borderRadius: BorderRadius.circular(24),
-                              border: Border.all(
-                                color: selected ? colors.accent : colors.border,
-                                width: selected ? 1.8 : 1,
-                              ),
-                            ),
-                            child: Column(
-                              mainAxisAlignment: MainAxisAlignment.center,
-                              children: [
-                                AppIconImage(bytes: app.icon, size: 44),
-                                const SizedBox(height: 6),
-                                Text(
-                                  app.label,
-                                  maxLines: 1,
-                                  overflow: TextOverflow.ellipsis,
-                                  textAlign: TextAlign.center,
-                                  style: theme.textTheme.bodySmall?.copyWith(
-                                    color: colors.ink,
-                                  ),
-                                ),
-                              ],
+                      ),
+                      child: Column(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          AppIcon(
+                            asset,
+                            size: 30,
+                            color: selected ? colors.accent : colors.ink,
+                          ),
+                          const SizedBox(height: 7),
+                          Text(
+                            label,
+                            maxLines: 1,
+                            overflow: TextOverflow.ellipsis,
+                            textAlign: TextAlign.center,
+                            style: theme.textTheme.bodySmall?.copyWith(
+                              color: selected ? colors.accent : colors.ink,
+                              fontWeight: selected
+                                  ? FontWeight.w600
+                                  : FontWeight.w400,
                             ),
                           ),
-                        ),
-                      );
-                    },
+                        ],
+                      ),
+                    ),
                   ),
+                );
+              },
+            ),
           ),
         ],
       ),

+ 40 - 0
lib/features/home/style_icon_choices.dart

@@ -0,0 +1,40 @@
+/// Curated monochrome line icons shown in the Change icon grid. They inherit
+/// the current theme ink color, so they read dark on light and light on dark.
+class StyleIconChoice {
+  const StyleIconChoice(this.asset, this.label);
+
+  final String asset;
+  final String label;
+}
+
+const styleIconChoices = <StyleIconChoice>[
+  StyleIconChoice('assets/icons/navigation.svg', 'Navigation'),
+  StyleIconChoice('assets/icons/music.svg', 'Music'),
+  StyleIconChoice('assets/icons/radio.svg', 'Radio'),
+  StyleIconChoice('assets/icons/settings.svg', 'Settings'),
+  StyleIconChoice('assets/icons/layout-grid.svg', 'Apps'),
+  StyleIconChoice('assets/icons/search.svg', 'Search'),
+  StyleIconChoice('assets/icons/camera.svg', 'Camera'),
+  StyleIconChoice('assets/icons/image.svg', 'Image'),
+  StyleIconChoice('assets/icons/video.svg', 'Video'),
+  StyleIconChoice('assets/icons/play.svg', 'Play'),
+  StyleIconChoice('assets/icons/headphones.svg', 'Headphones'),
+  StyleIconChoice('assets/icons/phone.svg', 'Phone'),
+  StyleIconChoice('assets/icons/message-circle.svg', 'Messages'),
+  StyleIconChoice('assets/icons/mail.svg', 'Mail'),
+  StyleIconChoice('assets/icons/calendar.svg', 'Calendar'),
+  StyleIconChoice('assets/icons/clock.svg', 'Clock'),
+  StyleIconChoice('assets/icons/folder.svg', 'Folder'),
+  StyleIconChoice('assets/icons/map.svg', 'Map'),
+  StyleIconChoice('assets/icons/car.svg', 'Car'),
+  StyleIconChoice('assets/icons/home.svg', 'Home'),
+  StyleIconChoice('assets/icons/star.svg', 'Star'),
+  StyleIconChoice('assets/icons/heart.svg', 'Heart'),
+  StyleIconChoice('assets/icons/bookmark.svg', 'Bookmark'),
+  StyleIconChoice('assets/icons/globe.svg', 'Globe'),
+  StyleIconChoice('assets/icons/cloud.svg', 'Cloud'),
+  StyleIconChoice('assets/icons/gamepad-2.svg', 'Games'),
+  StyleIconChoice('assets/icons/tv.svg', 'TV'),
+  StyleIconChoice('assets/icons/coffee.svg', 'Coffee'),
+  StyleIconChoice('assets/icons/shopping-bag.svg', 'Shopping'),
+];

+ 1 - 1
lib/features/settings/settings_screen.dart

@@ -306,7 +306,7 @@ class SettingsScreen extends StatelessWidget {
                     const SizedBox(height: 28),
                     Center(
                       child: Text(
-                        'CarDash 0.4.0 · Android 10+',
+                        'CarDash 0.5.0 · Android 10+',
                         style: theme.textTheme.bodySmall?.copyWith(
                           color: colors.inkFaint,
                         ),

+ 1 - 1
pubspec.yaml

@@ -1,7 +1,7 @@
 name: cardash_launcher
 description: "CarDash — a calm, configurable Android car dashboard launcher."
 publish_to: "none"
-version: 0.4.0+4
+version: 0.5.0+5
 
 environment:
   sdk: ^3.13.7

+ 2 - 3
test/launcher_controller_test.dart

@@ -159,7 +159,7 @@ void main() {
     expect(await first.addPinnedApp(apps[0]), isNull);
     final pinned = first.pinnedApps.first;
     await first.renamePinnedApp(pinned, 'My shortcut');
-    await first.setPinnedIcon(pinned, apps[1]);
+    await first.setPinnedIcon(pinned, 'assets/icons/star.svg');
 
     final second = LauncherController(platform, SettingsRepository(prefs));
     second.initialize();
@@ -167,8 +167,7 @@ void main() {
 
     expect(second.pinnedApps.length, 1);
     expect(second.pinnedApps.first.label, 'My shortcut');
-    expect(second.pinnedApps.first.iconPackageName, apps[1].packageName);
-    expect(second.pinnedApps.first.iconActivityName, apps[1].activityName);
+    expect(second.pinnedApps.first.iconAsset, 'assets/icons/star.svg');
   });
 
   test('pinned apps and slot visibility survive a new controller', () async {

+ 2 - 3
test/pin_service_test.dart

@@ -89,8 +89,7 @@ void main() {
           packageName: 'com.example.one',
           activityName: 'com.example.one.Main',
           label: 'One',
-          iconPackageName: 'com.example.icon',
-          iconActivityName: 'com.example.icon.Main',
+          iconAsset: 'assets/icons/star.svg',
         ),
         SlotAssignment(
           packageName: 'com.example.two',
@@ -104,7 +103,7 @@ void main() {
       expect(restored.length, 2);
       expect(restored.first.label, 'One');
       expect(restored.first.hasCustomIcon, isTrue);
-      expect(restored.first.iconPackageName, 'com.example.icon');
+      expect(restored.first.iconAsset, 'assets/icons/star.svg');
       expect(restored.last.packageName, 'com.example.two');
       expect(restored.last.hasCustomIcon, isFalse);