Replace the stock Flutter launcher icon, add a Linux desktop icon

Adopt the shared icon family (python_pkg/app_icons in testsAndMisc): a
charcoal #1B1D21 field carrying a deep-purple checklist glyph, deep purple
being this app's own theme seedColor. Two ticked-off rows rather than a lone
checkmark, so it reads as a notes app and not as a generic "done" badge.

Android gains the adaptive-icon layers the project never had, including an
Android 13 monochrome layer.

Linux needs separate handling: Flutter's GTK runner never sets a window icon,
so my_application.cc now calls gtk_window_set_icon_name and
install_desktop_entry.sh populates the hicolor theme from the pre-rendered
PNGs in linux/icons (committed, so the installer needs no image tooling).
StartupWMClass is the APPLICATION_ID rather than the binary name - verified
with xprop against a running window, where WM_CLASS is the GApplication id.

No Dart code changes, so test coverage is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-07-19 17:01:32 +02:00
parent ebb610e1f1
commit 0be07060de
35 changed files with 253 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
<monochrome>
<inset
android:drawable="@drawable/ic_launcher_monochrome"
android:inset="16%" />
</monochrome>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1B1D21</color>
</resources>

BIN
assets/icon/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

9
assets/icon/icon.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<rect width="1024" height="1024" fill="#1B1D21"/>
<g fill="none" stroke="#7E57C2" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,8)">
<path d="M 272 372 L 346 446 L 478 314"/>
<path d="M 594 400 L 752 400"/>
<path d="M 272 620 L 346 694 L 478 562"/>
<path d="M 594 648 L 752 648"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<g fill="none" stroke="#7E57C2" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,8)">
<path d="M 272 372 L 346 446 L 478 314"/>
<path d="M 594 400 L 752 400"/>
<path d="M 272 620 L 346 694 L 478 562"/>
<path d="M 594 648 L 752 648"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<g fill="none" stroke="#FFFFFF" stroke-width="72" stroke-linecap="round" stroke-linejoin="round" transform="translate(0,8)">
<path d="M 272 372 L 346 446 L 478 314"/>
<path d="M 594 400 L 752 400"/>
<path d="M 272 620 L 346 694 L 478 562"/>
<path d="M 594 648 L 752 648"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 397 B

BIN
linux/icons/128/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
linux/icons/16/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

BIN
linux/icons/24/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

BIN
linux/icons/256/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
linux/icons/32/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

BIN
linux/icons/48/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

BIN
linux/icons/512/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
linux/icons/64/todo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

118
linux/install_desktop_entry.sh Executable file
View File

@ -0,0 +1,118 @@
#!/bin/bash
# ============================================================================
# Install the todo launcher icon and .desktop entry for the Linux desktop.
#
# Flutter's GTK runner has no icon of its own: my_application.cc calls
# gtk_window_set_icon_name("todo"), which resolves through the hicolor icon
# theme. This script is what puts the icon into that theme, plus a .desktop
# entry so the app appears in application menus.
#
# Icon PNGs are pre-rendered and committed under linux/icons/, so this script
# needs no image tooling at run time. Regenerate them with:
# PYTHONPATH=~/testsAndMisc python3 -m python_pkg.app_icons \
# generate --app todo --linux-out linux/icons
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
LINUX_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(dirname "$LINUX_DIR")"
readonly SCRIPT_NAME LINUX_DIR REPO_DIR
readonly ICON_NAME="todo"
readonly BINARY_NAME="todo"
# GTK derives WM_CLASS from the GApplication id in linux/CMakeLists.txt,
# not from the binary name; verified with xprop on the running window.
readonly WM_CLASS="dev.kuhy.todo"
readonly ICON_SRC_DIR="$LINUX_DIR/icons"
readonly ICON_THEME_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor"
readonly DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
readonly BUNDLE_BIN="$REPO_DIR/build/linux/x64/release/bundle/$BINARY_NAME"
usage() {
echo "Usage: $SCRIPT_NAME"
echo "Installs the todo icon into the hicolor theme and writes a .desktop entry."
exit 0
}
validate_requirements() {
if [[ ! -d "$ICON_SRC_DIR" ]]; then
echo "Error: $ICON_SRC_DIR is missing; regenerate the icons first" >&2
exit 1
fi
}
ensure_theme_index() {
# A user-local hicolor tree created from scratch has no index.theme, and
# without it gtk-update-icon-cache refuses to build a cache ("No theme
# index file"). Seed it from the system theme when one is available.
local system_index="/usr/share/icons/hicolor/index.theme"
if [[ ! -f "$ICON_THEME_DIR/index.theme" && -f "$system_index" ]]; then
install -Dm644 "$system_index" "$ICON_THEME_DIR/index.theme"
fi
}
install_icons() {
local size_dir size icon
for size_dir in "$ICON_SRC_DIR"/*/; do
size="$(basename "$size_dir")"
icon="$size_dir$ICON_NAME.png"
[[ -f "$icon" ]] || continue
install -Dm644 "$icon" \
"$ICON_THEME_DIR/${size}x${size}/apps/$ICON_NAME.png"
done
# Refresh the theme cache so GTK picks the icon up without a re-login.
# Harmless if the tool is absent: the icon still resolves, just slower.
if command -v gtk-update-icon-cache >/dev/null; then
gtk-update-icon-cache --quiet --force "$ICON_THEME_DIR" || true
fi
}
install_desktop_entry() {
mkdir -p "$DESKTOP_DIR"
# StartupWMClass must match the window's actual WM class, or the taskbar
# shows a generic icon for the running window even though the launcher
# entry itself looks right.
cat > "$DESKTOP_DIR/$ICON_NAME.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=todo
Comment=Offline-first, CRDT-synced notes
Exec=$BUNDLE_BIN
Icon=$ICON_NAME
Terminal=false
Categories=Utility;TextEditor;
StartupWMClass=$WM_CLASS
EOF
if command -v update-desktop-database >/dev/null; then
update-desktop-database "$DESKTOP_DIR" || true
fi
}
main() {
validate_requirements
ensure_theme_index
install_icons
install_desktop_entry
echo "Installed $ICON_NAME icon into $ICON_THEME_DIR"
echo "Installed $DESKTOP_DIR/$ICON_NAME.desktop"
if [[ ! -x "$BUNDLE_BIN" ]]; then
echo "Note: $BUNDLE_BIN does not exist yet."
echo " Run 'flutter build linux --release' to make the entry launchable."
fi
}
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
main "$@"

View File

@ -54,6 +54,11 @@ static void my_application_activate(GApplication* application) {
gtk_window_set_default_size(window, 1280, 720);
// Resolved from the hicolor icon theme, which linux/install_desktop_entry.sh
// populates from linux/icons/. Without this the GTK runner shows a generic
// placeholder in the taskbar, since Flutter never sets a window icon itself.
gtk_window_set_icon_name(window, "todo");
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(
project, self->dart_entrypoint_arguments);

View File

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
@ -41,6 +49,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
@ -223,6 +247,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7"
url: "https://pub.dev"
source: hosted
version: "0.14.4"
flutter_lints:
dependency: "direct dev"
description:
@ -321,6 +353,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
url: "https://pub.dev"
source: hosted
version: "4.9.1"
jni:
dependency: transitive
description:
@ -337,6 +377,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
@ -489,6 +537,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
platform:
dependency: transitive
description:
@ -513,6 +569,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
url: "https://pub.dev"
source: hosted
version: "6.5.0"
process_run:
dependency: transitive
description:
@ -846,6 +910,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "7.0.1"
yaml:
dependency: transitive
description:

View File

@ -57,6 +57,9 @@ dependencies:
# libsecret) instead of plaintext SharedPreferences.
flutter_secure_storage: ^10.3.1
dev_dependencies:
# Generates the Android launcher icons from assets/icon/ (see
# python_pkg/app_icons in testsAndMisc for the shared style).
flutter_launcher_icons: ^0.14.4
flutter_test:
sdk: flutter
@ -117,3 +120,15 @@ flutter:
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
# Launcher icon generation. Source art is produced by the shared generator:
# PYTHONPATH=~/testsAndMisc python3 -m python_pkg.app_icons generate --app <key>
# then run: dart run flutter_launcher_icons
flutter_launcher_icons:
android: true
ios: false
image_path: "assets/icon/icon.png"
adaptive_icon_background: "#1B1D21"
adaptive_icon_foreground: "assets/icon/icon_foreground.png"
adaptive_icon_monochrome: "assets/icon/icon_monochrome.png"
min_sdk_android: 21