15 Commits

Author SHA1 Message Date
eb2ec79150 Implement local date via intl
We don't want to display utc
2025-04-24 09:58:57 +02:00
e8b9f0ba49 Enable deleting notes by deleting all their content 2025-04-23 20:56:01 +02:00
6c8340d768 Update 2025-04-23 16:32:24 +02:00
db565f4603 Work out a README 2025-04-23 16:16:03 +02:00
b2bff9e5c5 Implement IPC to show up existing instance when re-running 2025-04-23 15:51:55 +02:00
9d39cb09df Squash merge feature/fts into master 2025-04-23 14:51:24 +02:00
5ea62a1216 Do focus on popup
Because the window APPEARS over our current window
But isn't focuseded
So we canj't see our fucking window
And we can't close the new fucking window
So it's the worst of both worlds
FUCK
2025-04-23 11:23:11 +02:00
c457b5cd5b Add app icons 2025-04-22 19:22:13 +02:00
722aa34fdf Don't focus on popup 2025-04-22 19:21:13 +02:00
68c6dd1a95 Update 2025-04-22 19:11:40 +02:00
5a27ac75c7 Implement scrolling through previous notes 2025-04-22 00:35:03 +02:00
900bcd866c Implement basic settings 2025-04-22 00:29:31 +02:00
963f7271a2 Rename todo to scratch 2025-04-22 00:22:01 +02:00
442403b5b9 DO save empty todo 2025-04-21 23:38:06 +02:00
ad767ac33a Fix backspace delete 2025-04-21 23:33:15 +02:00
36 changed files with 1393 additions and 197 deletions

View File

@@ -1,16 +1,89 @@
# journaler # Journaler
A new Flutter project. ![Journaler Logo](assets/app_icon.ico)
## Getting Started Journaler is a "cross-platform" desktop application designed to encourage regular journaling by providing automated reminders and a clean writing environment.
This project is a starting point for a Flutter application. ## Overview
A few resources to get you started if this is your first Flutter project: Journaler helps you build a consistent journaling habit by periodically reminding you to record your thoughts throughout the day. It runs silently in the system tray and pops up at customizable intervals, making it perfect for:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) - **Daily reflection and mindfulness practice**
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - **Tracking your thoughts and activities**
- **Building a journaling habit**
- **Capturing ideas before they fade**
For help getting started with Flutter development, view the ![Journaler Screenshot](docs/screenshots/journaler_main.png)
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. ## Features
- **Automated Reminders**: Customizable popup intervals to remind you to journal
- **System Tray Integration**: Runs silently in the background
- **Scratch Pad**: Quick notes area for temporary thoughts
- **Journal History**: Browse through past entries
- **Full-Text Search**: Find entries containing specific words or phrases
- **Dark/Light Theme**: Automatically follows your system theme
- **Notification Sounds**: Audio cues when it's time to journal
- **Single Instance**: Only one copy of the app runs at a time
## Usage
### Getting Started
1. **First Launch**: When you first launch Journaler, it will start in the system tray
2. **Writing Entries**: The app will automatically pop up at set intervals (default: 20 minutes)
3. **Manual Access**: Click the system tray icon to show the app manually
4. **"Multiple" instances**: Running another instance will make the existing instance show itself
5. **Saving entries**: Entries are saved automatically when the popup is closed either manually or by hitting the escape key
### Main Interface
1. **Current Entry**: Write your thoughts for the current moment
2. **Previous Entry**: View what you wrote last time
3. **Scratch Pad**: Space for quick notes that persist between sessions
4. **Navigation**: Browse through your past entries
5. **Search**: Find specific content in your journal history (May also be invoked by CTRL-F)
![Journaler Interface](docs/screenshots/journaler_annotated.png)
### Settings
You can adjust:
- **Popup Interval**: Change how frequently Journaler reminds you
- **Notification Sound**: Select your preferred audio reminder
- **Volume**: Adjust the sound level of reminders
## Data privacy
All your journal entries are stored securely on your local machine:
- **Windows**: `C:\Users\YourUsername\.journaler\`
No network requests are made, no data sent or received, your data is yours alone.
Data is stored in an SQLite database with a very simple schema that can be fucked with easily.
## Development
This application is built with Flutter. If you want to build from source:
1. Install Flutter (version 3.7.2 or higher)
2. Clone the repository
3. Run `flutter pub get` to install dependencies
4. Use `flutter run` to run the application in debug mode
## Known issues
I don't know whether or not the sound customization works.<br>
In theory only 2 sound files are bundled with the app so, in theory, you may only choose between those two.<br>
But I don't know how flutter handles assets, currently I am not very motivated to figure this out.
Scrolling through long notes is difficult since we're intercepting the scroll event and using it to scroll through notes themselves.<br>
This, again, is not really an issue for me so I'm not very motivated to fix it.
Showing and focusing the window is really annoying for "gamers" and generally whoever is doing anything at the time.<br>
But I have no better ideas.<br>
If we show the window without focusing it, it will still be on top and, if not focused, will be even more annoying because to close it you have to click on it (to focus it) and THEN close it.<br>
If we only play the sound as a reminder we could miss it or simply get used to the sound and learn to ignore it.<br>
I have no ideas for a better solution.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/screenshots/journaler_annotated.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/screenshots/journaler_main.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -21,13 +21,41 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_date_unique ON notes (date);
-- ie. they're not really a list but instead a string -- ie. they're not really a list but instead a string
-- But we will also keep a history of all todos -- But we will also keep a history of all todos
-- Because we might as well -- Because we might as well
CREATE TABLE IF NOT EXISTS todos ( CREATE TABLE IF NOT EXISTS scratches (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT DEFAULT CURRENT_TIMESTAMP, date TEXT DEFAULT CURRENT_TIMESTAMP,
content TEXT NOT NULL content TEXT NOT NULL
); );
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos (date); CREATE INDEX IF NOT EXISTS idx_scratches_date ON scratches (date);
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date); CREATE UNIQUE INDEX IF NOT EXISTS idx_scratches_date_unique ON scratches (date);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL
);
-- Create virtual FTS5 table for searching notes content
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
content,
date,
content='notes',
content_rowid='id'
);
-- Trigger to keep FTS table in sync with notes table when inserting
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, content, date) VALUES (new.id, new.content, new.date);
END;
-- Trigger to keep FTS table in sync when deleting notes
CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
DELETE FROM notes_fts WHERE rowid = old.id;
END;
-- Trigger to keep FTS table in sync when updating notes
CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
UPDATE notes_fts SET content = new.content, date = new.date WHERE rowid = old.id;
END;
'''; ''';
static Future<String> _getDatabasePath() async { static Future<String> _getDatabasePath() async {
@@ -39,7 +67,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date);
if (home == null) { if (home == null) {
throw Exception('Could not find home directory'); throw Exception('Could not find home directory');
} }
debugPrint('Home directory found: home'); debugPrint('Home directory found: $home');
final dbDir = Directory(path.join(home, settingsDir)); final dbDir = Directory(path.join(home, settingsDir));
if (!await dbDir.exists()) { if (!await dbDir.exists()) {
@@ -53,7 +81,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date);
} else { } else {
// Default path for other platforms // Default path for other platforms
final databasesPath = await databaseFactoryFfi.getDatabasesPath(); final databasesPath = await databaseFactoryFfi.getDatabasesPath();
debugPrint('Using default databases path: databasesPath'); debugPrint('Using default databases path: $databasesPath');
return path.join(databasesPath, dbFileName); return path.join(databasesPath, dbFileName);
} }
} }
@@ -63,7 +91,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date);
sqfliteFfiInit(); sqfliteFfiInit();
final dbPath = await _getDatabasePath(); final dbPath = await _getDatabasePath();
debugPrint('Database path: dbPath'); debugPrint('Database path: $dbPath');
try { try {
db = await databaseFactoryFfi.openDatabase( db = await databaseFactoryFfi.openDatabase(
@@ -79,8 +107,93 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date);
); );
debugPrint('Database opened and initialized'); debugPrint('Database opened and initialized');
} catch (e) { } catch (e) {
debugPrint('Failed to initialize database: e'); debugPrint('Failed to initialize database: $e');
rethrow; rethrow;
} }
} }
// Settings Management
static Future<String?> getSetting(String key) async {
final List<Map<String, dynamic>> maps = await db.query(
'settings',
columns: ['value'],
where: 'key = ?',
whereArgs: [key],
);
if (maps.isNotEmpty) {
return maps.first['value'] as String?;
}
return null;
}
static Future<void> setSetting(String key, String value) async {
await db.insert('settings', {
'key': key,
'value': value,
}, conflictAlgorithm: ConflictAlgorithm.replace);
debugPrint("Setting updated: $key = $value");
}
// Search notes using FTS
static Future<List<Map<String, dynamic>>> searchNotes(String query) async {
try {
if (query.trim().isEmpty) {
return [];
}
// Process the query for partial word matching
// Split into individual terms, filter empty ones
List<String> terms =
query
.trim()
.split(RegExp(r'\s+'))
.where((term) => term.isNotEmpty)
.toList();
if (terms.isEmpty) {
return [];
}
// Add wildcards to each term for prefix matching (e.g., "fuck*" will match "fucked")
// Join terms with AND for all-term matching (results must contain ALL terms)
String ftsQuery = terms
.map((term) {
// Remove any special characters that might break the query
String sanitizedTerm = term.replaceAll(RegExp(r'[^\w]'), '');
if (sanitizedTerm.isEmpty) return '';
// Add wildcard for stemming/prefix matching
return '$sanitizedTerm*';
})
.where((term) => term.isNotEmpty)
.join(' AND ');
if (ftsQuery.isEmpty) {
debugPrint('Query was sanitized to empty string');
return [];
}
debugPrint('FTS query: "$ftsQuery"');
// Execute the FTS query with AND logic
final List<Map<String, dynamic>> results = await db.rawQuery(
'''
SELECT n.id, n.date, n.content, snippet(notes_fts, 0, '<b>', '</b>', '...', 20) as snippet
FROM notes_fts
JOIN notes n ON notes_fts.rowid = n.id
WHERE notes_fts MATCH ?
ORDER BY n.date DESC
LIMIT 100
''',
[ftsQuery],
);
debugPrint('Search query "$ftsQuery" returned ${results.length} results');
return results;
} catch (e) {
debugPrint('Search failed: $e');
// Return empty results rather than crashing on malformed queries
return [];
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,24 @@
import 'package:journaler/db.dart'; import 'package:journaler/db.dart';
import 'package:intl/intl.dart';
class Note { class Note {
final String date; final String date;
late final String displayDate;
String content; String content;
String? snippet;
Note({required this.date, required this.content}); Note({required this.date, required this.content, this.snippet}) {
final dtUtc = DateFormat('yyyy-MM-dd HH:mm:ss').parse(date, true);
final dtLocal = dtUtc.toLocal();
displayDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(dtLocal);
}
} }
class Todo { class Scratch {
final String date; final String date;
final String content; String content;
Todo({required this.date, required this.content}); Scratch({required this.date, required this.content});
} }
Future<Note?> getLatestNote() async { Future<Note?> getLatestNote() async {
@@ -28,37 +35,120 @@ Future<Note?> getLatestNote() async {
} }
Future<void> createNote(String content) async { Future<void> createNote(String content) async {
if (content.isEmpty) { // Trim the content to avoid saving just whitespace
final trimmedContent = content.trim();
if (trimmedContent.isEmpty) {
return; return;
} }
await DB.db.insert('notes', {'content': content}); await DB.db.insert('notes', {'content': trimmedContent});
} }
Future<void> updateNote(Note note) async { Future<void> updateNote(Note note) async {
// Trim the content to avoid saving just whitespace
final trimmedContent = note.content.trim();
if (trimmedContent.isEmpty) {
// Delete the note if content is empty
await DB.db.delete('notes', where: 'date = ?', whereArgs: [note.date]);
return;
}
await DB.db.update( await DB.db.update(
'notes', 'notes',
{'content': note.content}, {'content': trimmedContent},
where: 'date = ?', where: 'date = ?',
whereArgs: [note.date], whereArgs: [note.date],
); );
} }
Future<Todo?> getLatestTodo() async { Future<Scratch?> getLatestScratch() async {
final todo = await DB.db.rawQuery( final scratch = await DB.db.rawQuery(
'SELECT content, date FROM todos ORDER BY date DESC LIMIT 1', 'SELECT content, date FROM scratches ORDER BY date DESC LIMIT 1',
); );
if (todo.isEmpty) {
if (scratch.isEmpty) {
return null; return null;
} else {
return Scratch(
date: scratch[0]['date'] as String,
content: scratch[0]['content'] as String,
);
} }
return Todo(
date: todo[0]['date'] as String,
content: todo[0]['content'] as String,
);
} }
Future<void> createTodo(String content) async { Future<void> createScratch(String content) async {
if (content.isEmpty) { // Trim content but allow empty scratch notes (they might be intentionally cleared)
return; final trimmedContent = content.trim();
} await DB.db.insert('scratches', {'content': trimmedContent});
await DB.db.insert('todos', {'content': content}); }
// Get the note immediately older than the given date
Future<Note?> getPreviousNote(String currentDate) async {
final List<Map<String, dynamic>> notes = await DB.db.query(
'notes',
where: 'date < ?',
whereArgs: [currentDate],
orderBy: 'date DESC',
limit: 1,
);
if (notes.isNotEmpty) {
return Note(
date: notes.first['date'] as String,
content: notes.first['content'] as String,
);
}
return null;
}
// Get the note immediately newer than the given date
Future<Note?> getNextNote(String currentDate) async {
final List<Map<String, dynamic>> notes = await DB.db.query(
'notes',
where: 'date > ?',
whereArgs: [currentDate],
orderBy: 'date ASC',
limit: 1,
);
if (notes.isNotEmpty) {
return Note(
date: notes.first['date'] as String,
content: notes.first['content'] as String,
);
}
// If there's no newer note, it means we might be at the latest
// but let's double-check by explicitly getting the latest again.
// This handles the case where the `currentDate` might not be the absolute latest.
return getLatestNote();
}
/// Delete a note by its date
Future<bool> deleteNote(String date) async {
final result = await DB.db.delete(
'notes',
where: 'date = ?',
whereArgs: [date],
);
return result > 0; // Return true if a note was deleted
}
// Search notes using full-text search
Future<List<Note>> searchNotes(String query) async {
if (query.isEmpty) {
return [];
}
// Call DB search function
final List<Map<String, dynamic>> results = await DB.searchNotes(query);
// Convert results to Note objects
return results
.map(
(result) => Note(
date: result['date'] as String,
content: result['content'] as String,
snippet:
result['snippet'] as String?, // Highlighted snippets from FTS
),
)
.toList();
} }

View File

@@ -1,6 +1,22 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
archive:
dependency: transitive
description:
name: archive
sha256: a7f37ff061d7abc2fcf213554b9dcaca713c5853afa5c065c44888bc9ccaf813
url: "https://pub.dev"
source: hosted
version: "4.0.6"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@@ -81,6 +97,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@@ -142,6 +174,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -176,6 +216,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -241,7 +297,7 @@ packages:
source: hosted source: hosted
version: "1.16.0" version: "1.16.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@@ -296,6 +352,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -312,6 +376,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
url: "https://pub.dev"
source: hosted
version: "6.0.2"
ps_list:
dependency: "direct main"
description:
name: ps_list
sha256: "19d32f6c643313cf4f5101bb144b8978b9ba3dc42c9a01b247e8ed90581bc0ab"
url: "https://pub.dev"
source: hosted
version: "0.0.5"
screen_retriever: screen_retriever:
dependency: transitive dependency: transitive
description: description:
@@ -501,6 +581,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks: sdks:
dart: ">=3.7.2 <4.0.0" dart: ">=3.7.2 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.27.0"

View File

@@ -38,10 +38,14 @@ dependencies:
window_manager: ^0.4.3 window_manager: ^0.4.3
audioplayers: ^6.4.0 audioplayers: ^6.4.0
sqflite_common_ffi: ^2.3.5 sqflite_common_ffi: ^2.3.5
path: ^1.8.0
ps_list: ^0.0.5
intl: ^0.20.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_launcher_icons: ^0.13.1
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@@ -93,3 +97,12 @@ flutter:
# #
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package # see https://flutter.dev/to/font-from-package
flutter_launcher_icons:
android: true
ios: true
image_path: "assets/app_icon.ico"
windows:
generate: true
image_path: "assets/app_icon.ico"
icon_size: 256 # min: 48, max: 256, default: 48

BIN
windows/runner/resources/app_icon.ico (Stored with Git LFS)

Binary file not shown.