12 Commits

Author SHA1 Message Date
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
1051805463 Fix the double escape bug 2025-04-21 23:04:40 +02:00
173c4fbee2 Auto focus new entry 2025-04-21 22:36:54 +02:00
d06a8d6698 Implement commit sudoku button 2025-04-21 22:35:53 +02:00
d788c110a7 TODO 2025-04-21 22:33:02 +02:00
33 changed files with 527 additions and 125 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -427,7 +427,7 @@
isa = XCBuildConfiguration;
buildSettings = {
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
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,18 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_date_unique ON notes (date);
-- ie. they're not really a list but instead a string
-- But we will also keep a history of all todos
-- Because we might as well
CREATE TABLE IF NOT EXISTS todos (
CREATE TABLE IF NOT EXISTS scratches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT DEFAULT CURRENT_TIMESTAMP,
content TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_todos_date ON todos (date);
CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date);
CREATE INDEX IF NOT EXISTS idx_scratches_date ON scratches (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
);
''';
static Future<String> _getDatabasePath() async {
@@ -83,4 +88,27 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_todos_date_unique ON todos (date);
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");
}
}

View File

@@ -6,9 +6,13 @@ import 'package:journaler/notes.dart';
import 'package:system_tray/system_tray.dart';
import 'package:window_manager/window_manager.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/gestures.dart';
const Duration popupInterval = Duration(minutes: 20);
const String notificationSound = 'MeetTheSniper.mp3';
// TODO: Sound does not play when ran from a different workdir? Weird
// Default values - will be replaced by DB values if they exist
const Duration _defaultPopupInterval = Duration(minutes: 20);
const String _defaultNotificationSound = 'MeetTheSniper.mp3';
void main() async {
await DB.init();
@@ -116,19 +120,29 @@ class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
State<MainPage> createState() => MainPageState();
}
class _MainPageState extends State<MainPage> with WindowListener {
class MainPageState extends State<MainPage> with WindowListener {
final SystemTray _systemTray = SystemTray();
final Menu _menu = Menu();
final AudioPlayer _audioPlayer = AudioPlayer();
final TextEditingController _previousEntryController =
TextEditingController();
final TextEditingController _currentEntryController = TextEditingController();
final TextEditingController _todoController = TextEditingController();
final FocusNode _currentEntryFocusNode = FocusNode();
final TextEditingController _scratchController = TextEditingController();
final TextEditingController _intervalController = TextEditingController();
final TextEditingController _soundController = TextEditingController();
Note? previousNote;
Note? _currentlyDisplayedNote;
Duration _currentPopupInterval = _defaultPopupInterval;
String _currentNotificationSound = _defaultNotificationSound;
bool _canGoPrevious = false;
bool _canGoNext = false;
Timer? _popupTimer;
Timer? _debounceTimer;
@@ -139,7 +153,6 @@ class _MainPageState extends State<MainPage> with WindowListener {
windowManager.addListener(this);
_initSystemTray();
_loadData();
_startPopupTimer();
windowManager.setPreventClose(true);
_setWindowConfig();
}
@@ -151,7 +164,10 @@ class _MainPageState extends State<MainPage> with WindowListener {
_debounceTimer?.cancel();
_previousEntryController.dispose();
_currentEntryController.dispose();
_todoController.dispose();
_currentEntryFocusNode.dispose();
_scratchController.dispose();
_intervalController.dispose();
_soundController.dispose();
_audioPlayer.dispose();
super.dispose();
}
@@ -172,16 +188,31 @@ class _MainPageState extends State<MainPage> with WindowListener {
await _systemTray.initSystemTray(iconPath: iconPath, toolTip: "Journaler");
await _menu.buildFrom([
MenuItemLabel(
label: 'Commit Sudoku',
onClicked: (menuItem) => windowManager.destroy(),
),
]);
await _systemTray.setContextMenu(_menu);
_systemTray.registerSystemTrayEventHandler((eventName) {
debugPrint("System Tray Event: $eventName");
if (eventName == kSystemTrayEventClick) {
_showWindow();
} else if (eventName == kSystemTrayEventRightClick) {
_systemTray.popUpContextMenu();
}
});
}
void _startPopupTimer() {
_popupTimer = Timer.periodic(popupInterval, (timer) {
_popupTimer?.cancel();
_popupTimer = Timer.periodic(_currentPopupInterval, (timer) {
_showWindow();
});
debugPrint("Popup timer started with interval: ${_currentPopupInterval.inMinutes} minutes");
}
Future<void> _showWindow() async {
@@ -191,43 +222,127 @@ class _MainPageState extends State<MainPage> with WindowListener {
await windowManager.setSize(const Size(1600, 900));
await windowManager.center();
await windowManager.show();
await windowManager.focus();
await _playSound();
} else {
await windowManager.focus();
_currentEntryFocusNode.requestFocus();
}
}
Future<void> _playSound() async {
await _audioPlayer.stop();
await _audioPlayer.play(AssetSource('sounds/$notificationSound'));
debugPrint("Played sound: $notificationSound");
try {
await _audioPlayer.play(AssetSource('sounds/$_currentNotificationSound'));
debugPrint("Played sound: $_currentNotificationSound");
} catch (e) {
debugPrint("Error playing sound $_currentNotificationSound: $e");
}
}
Future<void> _checkNavigation() async {
if (_currentlyDisplayedNote == null) {
setState(() {
_canGoPrevious = false;
_canGoNext = false;
});
return;
}
final prev = await getPreviousNote(_currentlyDisplayedNote!.date);
final bool isLatest = _currentlyDisplayedNote!.date == previousNote?.date;
setState(() {
_canGoPrevious = prev != null;
_canGoNext = !isLatest;
});
}
Future<void> _goToPreviousNote() async {
if (!_canGoPrevious || _currentlyDisplayedNote == null) return;
final prevNote = await getPreviousNote(_currentlyDisplayedNote!.date);
if (prevNote != null) {
setState(() {
_currentlyDisplayedNote = prevNote;
_previousEntryController.text = prevNote.content;
});
await _checkNavigation();
}
}
Future<void> _goToNextNote() async {
if (!_canGoNext || _currentlyDisplayedNote == null) return;
final nextNote = await getNextNote(_currentlyDisplayedNote!.date);
if (nextNote != null) {
setState(() {
_currentlyDisplayedNote = nextNote;
_previousEntryController.text = nextNote.content;
});
await _checkNavigation();
}
}
void _loadData() async {
String? intervalMinutesStr = await DB.getSetting('popupIntervalMinutes');
String? soundFileStr = await DB.getSetting('notificationSound');
int intervalMinutes = int.tryParse(intervalMinutesStr ?? '') ?? _defaultPopupInterval.inMinutes;
_currentPopupInterval = Duration(minutes: intervalMinutes);
_currentNotificationSound = soundFileStr ?? _defaultNotificationSound;
_intervalController.text = intervalMinutes.toString();
_soundController.text = _currentNotificationSound;
_startPopupTimer();
final note = await getLatestNote();
previousNote = note;
_previousEntryController.text = note?.content ?? "";
_currentlyDisplayedNote = note;
_previousEntryController.text = _currentlyDisplayedNote?.content ?? "";
final todo = await getLatestTodo();
_todoController.text = todo?.content ?? "";
final scratch = await getLatestScratch();
_scratchController.text = scratch?.content ?? "";
_currentEntryController.text = "";
debugPrint("Data loaded (placeholder).");
await _checkNavigation();
debugPrint("Data loaded.");
}
void _saveData() async {
String previousEntry = _previousEntryController.text;
String currentEntry = _currentEntryController.text;
String todoList = _todoController.text;
String scratchContent = _scratchController.text;
String intervalStr = _intervalController.text;
String soundStr = _soundController.text;
await createNote(currentEntry);
await createTodo(todoList);
await createScratch(scratchContent);
if (previousNote != null) {
previousNote!.content = previousEntry;
await updateNote(previousNote!);
}
int newIntervalMinutes = int.tryParse(intervalStr) ?? _currentPopupInterval.inMinutes;
Duration newInterval = Duration(minutes: newIntervalMinutes);
if (newInterval != _currentPopupInterval) {
_currentPopupInterval = newInterval;
DB.setSetting('popupIntervalMinutes', newIntervalMinutes.toString());
_startPopupTimer();
} else {
DB.setSetting('popupIntervalMinutes', newIntervalMinutes.toString());
}
if (soundStr != _currentNotificationSound) {
_currentNotificationSound = soundStr;
DB.setSetting('notificationSound', soundStr);
} else {
DB.setSetting('notificationSound', soundStr);
}
debugPrint(
"Saving data (placeholder)... Current Entry: [${currentEntry.length} chars], Todo: [${todoList.length} chars]",
"Saving data... Current Entry: [${currentEntry.length} chars], Scratch: [${scratchContent.length} chars]",
);
}
@@ -237,18 +352,87 @@ class _MainPageState extends State<MainPage> with WindowListener {
@override
Widget build(BuildContext context) {
return KeyboardListener(
focusNode: FocusNode(),
autofocus: true,
onKeyEvent: (event) {
if (event is KeyDownEvent &&
// Wrap Scaffold with RawKeyboardListener as workaround for Escape key
return RawKeyboardListener(
focusNode: FocusNode(), // Needs its own node
onKey: (RawKeyEvent event) {
if (event is RawKeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
debugPrint("Escape key pressed - hiding window.");
debugPrint(
"Escape pressed inside MainPage (RawKeyboardListener - Workaround)",
);
// Call method directly since we are in the state
FocusManager.instance.primaryFocus?.unfocus(); // Keep unfocus attempt
onWindowClose();
}
},
child: Scaffold(
appBar: AppBar(title: const Text('Journaler'), actions: const []),
appBar: AppBar(
title: const Text('Journaler'),
actions: <Widget>[
// Group Label and Input for Interval
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0).copyWith(left: 8.0), // Add padding
child: Row(
mainAxisSize: MainAxisSize.min, // Use minimum space
children: [
const Text("Interval (m):"),
const SizedBox(width: 4), // Space between label and input
SizedBox(
width: 60, // Constrain width
child: TextField(
controller: _intervalController,
// textAlignVertical: TextAlignVertical.center, // Let default alignment handle it
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
),
),
],
),
),
// Group Label and Input for Sound
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Sound:"),
const SizedBox(width: 4),
SizedBox(
width: 150, // Constrain width
child: TextField(
controller: _soundController,
// textAlignVertical: TextAlignVertical.center,
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
hintText: 'sound.mp3',
),
),
),
],
),
),
// Test Sound Button
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: IconButton(
icon: const Icon(Icons.volume_up),
tooltip: 'Test Sound',
onPressed: _playSound,
),
),
const SizedBox(width: 10),
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
@@ -256,17 +440,63 @@ class _MainPageState extends State<MainPage> with WindowListener {
children: [
Expanded(
flex: 9,
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerSignal: (pointerSignal) {
if (pointerSignal is PointerScrollEvent) {
if (pointerSignal.scrollDelta.dy < 0) {
if (_canGoPrevious) {
_goToPreviousNote();
}
} else if (pointerSignal.scrollDelta.dy > 0) {
if (_canGoNext) {
_goToNextNote();
}
}
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Combine Label, Buttons, and TextField for Previous Entry
Row(
children: [
Expanded(
child: Text(
_currentlyDisplayedNote?.date == previousNote?.date
? 'Previous Entry (Latest)'
: 'Entry: ${_currentlyDisplayedNote?.date ?? 'N/A'}',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
),
IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Previous Note',
onPressed: _canGoPrevious ? _goToPreviousNote : null,
),
IconButton(
icon: const Icon(Icons.arrow_forward),
tooltip: 'Next Note',
onPressed: _canGoNext ? _goToNextNote : null,
),
],
),
Expanded(
child: TextField(
controller: _previousEntryController,
readOnly: _currentlyDisplayedNote?.date != previousNote?.date,
maxLines: null,
expands: true,
style: Theme.of(context).textTheme.bodyMedium,
decoration: const InputDecoration(
labelText: 'Previous Entry',
decoration: InputDecoration(
hintText: _currentlyDisplayedNote?.date != previousNote?.date
? 'Viewing note from ${_currentlyDisplayedNote?.date} (Read-Only)'
: 'Latest Note',
border: const OutlineInputBorder(),
filled: _currentlyDisplayedNote?.date != previousNote?.date,
fillColor: _currentlyDisplayedNote?.date != previousNote?.date
? Colors.grey.withOpacity(0.1)
: null,
),
),
),
@@ -274,6 +504,7 @@ class _MainPageState extends State<MainPage> with WindowListener {
Expanded(
child: TextField(
controller: _currentEntryController,
focusNode: _currentEntryFocusNode,
maxLines: null,
expands: true,
autofocus: true,
@@ -281,27 +512,22 @@ class _MainPageState extends State<MainPage> with WindowListener {
decoration: const InputDecoration(
labelText: 'Current Entry (What\'s on your mind?)',
),
onChanged: (text) {},
),
),
],
),
),
),
const SizedBox(width: 8),
Expanded(
flex: 3,
flex: 4, // Adjust flex factor as needed
child: TextField(
controller: _todoController,
controller: _scratchController,
maxLines: null,
expands: true,
style:
Theme.of(
context,
).textTheme.bodyMedium, // Apply theme text style
style: Theme.of(context).textTheme.bodyMedium, // Apply theme text style
decoration: const InputDecoration(
labelText: 'Todo',
// border: OutlineInputBorder(), // Handled by theme
// contentPadding: EdgeInsets.all(8.0), // Handled by theme or default
labelText: 'Scratch',
),
),
),
@@ -312,3 +538,5 @@ class _MainPageState extends State<MainPage> with WindowListener {
);
}
}
// --- End Actions and Shortcuts ---

View File

@@ -7,11 +7,11 @@ class Note {
Note({required this.date, required this.content});
}
class Todo {
class Scratch {
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 {
@@ -43,22 +43,60 @@ Future<void> updateNote(Note note) async {
);
}
Future<Todo?> getLatestTodo() async {
final todo = await DB.db.rawQuery(
'SELECT content, date FROM todos ORDER BY date DESC LIMIT 1',
Future<Scratch?> getLatestScratch() async {
final scratch = await DB.db.rawQuery(
'SELECT content, date FROM scratches ORDER BY date DESC LIMIT 1',
);
if (todo.isEmpty) {
if (scratch.isEmpty) {
return null;
}
return Todo(
date: todo[0]['date'] as String,
content: todo[0]['content'] as String,
} else {
return Scratch(
date: scratch[0]['date'] as String,
content: scratch[0]['content'] as String,
);
}
}
Future<void> createTodo(String content) async {
if (content.isEmpty) {
return;
}
await DB.db.insert('todos', {'content': content});
Future<void> createScratch(String content) async {
await DB.db.insert('scratches', {'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();
}

View File

@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
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:
dependency: transitive
description:
@@ -81,6 +97,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -142,6 +174,14 @@ packages:
description: flutter
source: sdk
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:
dependency: "direct dev"
description:
@@ -176,6 +216,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
image:
dependency: transitive
description:
name: image
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev"
source: hosted
version: "4.5.4"
json_annotation:
dependency: transitive
description:
@@ -296,6 +344,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
@@ -312,6 +368,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
posix:
dependency: transitive
description:
name: posix
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
url: "https://pub.dev"
source: hosted
version: "6.0.2"
screen_retriever:
dependency: transitive
description:
@@ -501,6 +565,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dart: ">=3.7.2 <4.0.0"
flutter: ">=3.27.0"

View File

@@ -42,6 +42,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.13.1
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
@@ -93,3 +94,12 @@ flutter:
#
# For details regarding fonts from package dependencies,
# 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.