<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: amekusa03</title>
    <description>The latest articles on DEV Community by amekusa03 (@amekusa03).</description>
    <link>https://dev.to/amekusa03</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3852389%2F7d4c5fa3-cfce-4adb-9724-a607f41438aa.png</url>
      <title>DEV Community: amekusa03</title>
      <link>https://dev.to/amekusa03</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amekusa03"/>
    <language>en</language>
    <item>
      <title>Building a 5.1ch Surround Upmixer GUI with Python &amp; FFmpeg</title>
      <dc:creator>amekusa03</dc:creator>
      <pubDate>Tue, 01 Sep 2026 07:03:29 +0000</pubDate>
      <link>https://dev.to/amekusa03/building-a-51ch-surround-upmixer-gui-with-python-ffmpeg-1e0g</link>
      <guid>https://dev.to/amekusa03/building-a-51ch-surround-upmixer-gui-with-python-ffmpeg-1e0g</guid>
      <description>&lt;h1&gt;
  
  
  Building a 5.1ch Surround Upmixer GUI with Python &amp;amp; FFmpeg
&lt;/h1&gt;

&lt;p&gt;Ever wanted to watch a movie or listen to live music with true surround sound, but only had a stereo MP3 file? I built a desktop GUI application that upmixes 2-channel stereo audio into &lt;strong&gt;6-channel AC-3 (Dolby Digital 5.1)&lt;/strong&gt; — fully customizable with slider controls and one-click presets.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/amekusa03/mp3-to-ac3-converter" rel="noopener noreferrer"&gt;https://github.com/amekusa03/mp3-to-ac3-converter&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fully adjustable surround parameters&lt;/strong&gt; via sliders:

&lt;ul&gt;
&lt;li&gt;Center Mix Level (0× – 2×) — controls vocal/dialogue presence&lt;/li&gt;
&lt;li&gt;Rear/Surround Level (0× – 2×) — controls ambient depth&lt;/li&gt;
&lt;li&gt;Surround Delay (0–100 ms) — enhances spatial feel&lt;/li&gt;
&lt;li&gt;LFE Subwoofer Cutoff (50–200 Hz) &amp;amp; Gain — deep bass extraction&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3 one-click presets&lt;/strong&gt;: 🎥 Cinema, 🎵 Music/Live, 🗣️ Voice/Dialogue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch conversion&lt;/strong&gt; — drop a whole folder of files&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dark / Light theme toggle&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live FFmpeg log monitor&lt;/strong&gt; with progress bar&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with Python, CustomTkinter (modern Tk GUI), and FFmpeg under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the upmix works
&lt;/h2&gt;

&lt;p&gt;The core idea is converting stereo (L, R) into 5.1 (FL, FR, FC, LFE, SL, SR) using FFmpeg's &lt;code&gt;filter_complex&lt;/code&gt;. Here's the math:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FL / FR&lt;/td&gt;
&lt;td&gt;L, R directly&lt;/td&gt;
&lt;td&gt;Front left/right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FC (Center)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(L + R) × 0.707 × center_gain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Vocals, dialogue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LFE (Sub)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lowpass((L + R) × 0.5, cutoff_hz)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deep bass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SL (Surround L)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(L − R) × 0.707 × rear_gain + delay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ambient / effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SR (Surround R)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(R − L) × 0.707 × rear_gain + delay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ambient / effects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The phase difference (L − R) is what makes surround sound "come from behind" — it extracts stereo-only information that would be mixed out in mono, and sends it to the rear speakers.&lt;/p&gt;

&lt;p&gt;Here's what the generated FFmpeg filter looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0:a]asplit=5[in_flfr][in_c][in_lfe][in_sl][in_sr];
[in_flfr]pan=stereo|c0=c0|c1=c1[flfr];
[in_c]pan=1c|c0=0.707*c0+0.707*c1,volume=1.000[c];
[in_lfe]pan=1c|c0=0.5*c0+0.5*c1,lowpass=f=120,volume=1.000[lfe];
[in_sl]pan=1c|c0=0.707*c0-0.707*c1,adelay=20|20,volume=0.700[sl];
[in_sr]pan=1c|c0=-0.707*c0+0.707*c1,adelay=20|20,volume=0.700[sr];
[flfr][c][lfe][sl][sr]amerge=inputs=5,pan=5.1|FL=c0|FR=c1|FC=c2|LFE=c3|BL=c4|BR=c5[out]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Code highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Thread-safe UI updates with a queue
&lt;/h3&gt;

&lt;p&gt;Tkinter is single-threaded. Calling UI methods from a background FFmpeg worker thread will crash the app. The solution is a &lt;code&gt;queue.Queue&lt;/code&gt; polled by &lt;code&gt;after()&lt;/code&gt; on the main thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_process_queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Called every 50ms on main thread — safely drains worker messages.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;msg_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;msg_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nowait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;msg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;progress_bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;msg_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;batch_complete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# ... handle completion
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_process_queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# reschedule
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The background worker puts messages into the queue; the main thread processes them. No locks, no crashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing Python's late-binding closure trap
&lt;/h3&gt;

&lt;p&gt;A subtle bug lurks when defining closures inside a &lt;code&gt;for&lt;/code&gt; loop — the variable &lt;code&gt;idx&lt;/code&gt; gets bound &lt;em&gt;by reference&lt;/em&gt;, so all closures end up using the &lt;em&gt;last&lt;/em&gt; loop value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Bug: idx is always the final loop value
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;overall_p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total_files&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ Fix: capture current value via default argument
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_progress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;total_files&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;overall_p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_idx&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;_total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default argument values are evaluated &lt;em&gt;at definition time&lt;/em&gt;, so each closure gets the correct snapshot of &lt;code&gt;idx&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean preset system with dataclasses
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AudioPreset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;center_gain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;   &lt;span class="c1"&gt;# 0.0 to 2.0
&lt;/span&gt;    &lt;span class="n"&gt;rear_gain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;rear_delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;      &lt;span class="c1"&gt;# ms
&lt;/span&gt;    &lt;span class="n"&gt;lfe_cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;      &lt;span class="c1"&gt;# Hz
&lt;/span&gt;    &lt;span class="n"&gt;lfe_gain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;bitrate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;         &lt;span class="c1"&gt;# '448k', '640k', etc.
&lt;/span&gt;    &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;     &lt;span class="c1"&gt;# 48000 / 44100
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Presets are just dictionaries of &lt;code&gt;AudioPreset&lt;/code&gt; objects — trivial to add new ones, and the UI automatically reflects the values when clicked.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Requirements: Python 3.8+, FFmpeg in PATH&lt;/span&gt;

git clone https://github.com/[YourUsername]/mp3-to-ac3-converter
&lt;span class="nb"&gt;cd &lt;/span&gt;mp3-to-ac3-converter
./run.sh   &lt;span class="c"&gt;# auto-creates venv, installs deps, launches app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works on Linux and macOS. Windows users can run &lt;code&gt;venv\Scripts\activate&lt;/code&gt; + &lt;code&gt;python main.py&lt;/code&gt; manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg's &lt;code&gt;filter_complex&lt;/code&gt; is incredibly powerful&lt;/strong&gt; — you can split, transform, recombine audio streams in one pipeline with no intermediate files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CustomTkinter makes modern-looking Tk GUIs much easier&lt;/strong&gt; — dark mode, rounded corners, and consistent theming out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;queue.Queue&lt;/code&gt; + &lt;code&gt;after()&lt;/code&gt; pattern&lt;/strong&gt; is the right way to drive Tkinter updates from worker threads — cleaner than &lt;code&gt;after&lt;/code&gt; lambdas scattered everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python closure late binding is a real gotcha&lt;/strong&gt; — always use default argument capture when building callbacks inside loops.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;Feel free to open issues, submit PRs, or fork it for your own surround mix experiments. Contributions welcome!&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/amekusa03/mp3-to-ac3-converter" rel="noopener noreferrer"&gt;https://github.com/amekusa03/mp3-to-ac3-converter&lt;/a&gt;&lt;br&gt;
📘 &lt;strong&gt;日本語版記事 (Qiita)&lt;/strong&gt;: &lt;a href="https://qiita.com/amekusa03/items/c425d0870f462bfb7645" rel="noopener noreferrer"&gt;https://qiita.com/amekusa03/items/c425d0870f462bfb7645&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ffmpeg</category>
      <category>audio</category>
      <category>open</category>
    </item>
    <item>
      <title>Building FocusCycle: A Non-Intrusive Qt5/C++ Pomodoro Timer for Linux</title>
      <dc:creator>amekusa03</dc:creator>
      <pubDate>Fri, 21 Aug 2026 05:01:14 +0000</pubDate>
      <link>https://dev.to/amekusa03/building-focuscycle-a-non-intrusive-qt5c-pomodoro-timer-for-linux-56ob</link>
      <guid>https://dev.to/amekusa03/building-focuscycle-a-non-intrusive-qt5c-pomodoro-timer-for-linux-56ob</guid>
      <description>&lt;p&gt;I have a bad habit with traditional Pomodoro apps: when the 25-minute timer finishes, a loud bell rings or a massive popup interrupts me mid-thought. That's the opposite of focus.&lt;/p&gt;

&lt;p&gt;I also work in an office with fixed break times (lunch at 12:00, afternoon break at 15:00).&lt;/p&gt;

&lt;p&gt;That’s why I developed &lt;strong&gt;FocusCycle&lt;/strong&gt;—a Qt5/C++ Pomodoro timer for Linux.&lt;/p&gt;




&lt;h2&gt;
  
  
  What FocusCycle Does Differently
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 🖱 Smart Idle Detection (Wayland &amp;amp; X11)
&lt;/h3&gt;

&lt;p&gt;The most important feature: &lt;strong&gt;FocusCycle won't interrupt you until you stop typing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When the work timer reaches &lt;code&gt;0:00&lt;/code&gt;, instead of immediately popping up a break dialog, it keeps counting negative into overtime (&lt;code&gt;-00:01&lt;/code&gt;, &lt;code&gt;-00:02&lt;/code&gt;...) while monitoring your keyboard/mouse activity in the background. Only once you've been idle for a configurable number of seconds (0–30) does the break prompt appear.&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;Wayland&lt;/strong&gt; (GNOME), this uses the &lt;code&gt;org.gnome.Mutter.IdleMonitor&lt;/code&gt; D-Bus API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IdleMonitor.cpp — cached D-Bus interface, queried every second&lt;/span&gt;
&lt;span class="n"&gt;IdleMonitor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;IdleMonitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QObject&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;QObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;QString&lt;/span&gt; &lt;span class="n"&gt;waylandDisplay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;QProcessEnvironment&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;systemEnvironment&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WAYLAND_DISPLAY"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;m_waylandMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;waylandDisplay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_waylandMode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m_dbusIface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;QDBusInterface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"org.gnome.Mutter.IdleMonitor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"/org/gnome/Mutter/IdleMonitor/Core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"org.gnome.Mutter.IdleMonitor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;QDBusConnection&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;sessionBus&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;m_timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;m_timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;QTimer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;IdleMonitor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;qint64&lt;/span&gt; &lt;span class="n"&gt;IdleMonitor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;getIdleTimeWayland&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;m_dbusIface&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;m_dbusIface&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;QDBusReply&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;qulonglong&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m_dbusIface&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GetIdletime"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;qint64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// milliseconds&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On &lt;strong&gt;X11&lt;/strong&gt;, it falls back to the &lt;code&gt;XScreenSaver&lt;/code&gt; extension (&lt;code&gt;libxscrnsaver&lt;/code&gt;). The &lt;code&gt;.pro&lt;/code&gt; file detects it automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unix:!macx {
    packagesExist(xscrnsaver) {
        DEFINES += HAVE_X11SS
        CONFIG  += link_pkgconfig
        PKGCONFIG += xscrnsaver
        QT += x11extras
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: The D-Bus interface is created &lt;strong&gt;once&lt;/strong&gt; in the constructor and cached — not reconstructed every polling tick. This was a bug I caught in code review and fixed before shipping.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2. 🔕 Zero-Distraction UI &amp;amp; System Color Palette
&lt;/h3&gt;

&lt;p&gt;Focus is about eliminating distractions. When you click &lt;strong&gt;"Start Work"&lt;/strong&gt;, the window &lt;strong&gt;completely closes and hides&lt;/strong&gt; (&lt;code&gt;hide()&lt;/code&gt;). There are no floating timers, badges, or widgets on your screen to pull your eyes away from your work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System Tray Tooltip&lt;/strong&gt;: Status and time (&lt;code&gt;FocusCycle | Working 24:59&lt;/code&gt;) are accessible via hover tooltip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monochrome System Colors&lt;/strong&gt;: The tray icon uses standard system palette colors (&lt;code&gt;QPalette::WindowText&lt;/code&gt;) during work so it blends in with OS panel icons (Wi-Fi, sound) without bright distracting colors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Red Accent Colors&lt;/strong&gt;: All red font and button highlights have been removed in favor of white (&lt;code&gt;#FFFFFF&lt;/code&gt;) and system blue (&lt;code&gt;#4a90e2&lt;/code&gt;), creating a calm, focused environment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. 🏢 Scheduled Company Breaks
&lt;/h3&gt;

&lt;p&gt;You can configure up to 10 daily break slots with both a &lt;strong&gt;start time and an optional end time&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Break 1  [12:00]  ☑ End  [13:00]
Break 2  [15:00]  □ End
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;When the clock hits 12:00, the company break dialog appears and work stops.&lt;/li&gt;
&lt;li&gt;At 13:00, the app automatically resumes the previous state — no manual action needed.&lt;/li&gt;
&lt;li&gt;Break 2 has no end time, so it stays up until you dismiss it manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data is stored in INI format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[CompanyBreaks]&lt;/span&gt;
&lt;span class="py"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;Break0&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;12:00-13:00&lt;/span&gt;
&lt;span class="py"&gt;Break1&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;15:00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading it back handles both old (start-only) and new (start-end) formats for backward compatibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;QStringList&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QTime&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"HH:mm"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endTime&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QTime&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"HH:mm"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;QTime&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"HH:mm"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. 🎯 The State Machine
&lt;/h3&gt;

&lt;p&gt;The core of FocusCycle is &lt;code&gt;CycleController&lt;/code&gt; — a clean state machine with 5 explicit states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WorkWait → Working (Counts 25:00 → 0:00 → -00:01...) → BreakWait → Break → WorkWait (loop)
   ↓                                                      ↑
   └────────────────── CompanyBreak ──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WorkWait&lt;/code&gt;&lt;/strong&gt;: Waiting for user to start work (&lt;code&gt;WorkStartWindow&lt;/code&gt; popped up).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Working&lt;/code&gt;&lt;/strong&gt;: Timer counting down, continuing into negative seconds during idle detection (&lt;code&gt;-00:01&lt;/code&gt;, &lt;code&gt;-00:02&lt;/code&gt;...).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BreakWait&lt;/code&gt;&lt;/strong&gt;: Idle threshold reached; &lt;code&gt;BreakDialog&lt;/code&gt; popped up with options to &lt;strong&gt;Continue (reset timer)&lt;/strong&gt; or &lt;strong&gt;Complete Work (take break)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Break&lt;/code&gt;&lt;/strong&gt;: Break timer counting down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CompanyBreak&lt;/code&gt;&lt;/strong&gt;: Scheduled company break active.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each state transition emits a &lt;code&gt;stateChanged(AppState)&lt;/code&gt; signal. The UI layer (&lt;code&gt;Application&lt;/code&gt;) connects to these to show/hide windows and send desktop notifications. The state machine itself has zero UI dependencies — it only knows about timers and signals.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CycleController.h&lt;/span&gt;
&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;stateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;workTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;remainingSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;breakTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;remainingSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;workTimeFinished&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;breakTimeFinished&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;showBreakDialog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;showCompanyBreakDialog&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;showWorkStartWindow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. 📊 CSV Session Logging &amp;amp; Quick Task Dropdown
&lt;/h3&gt;

&lt;p&gt;Every session gets logged automatically to &lt;code&gt;~/.local/share/FocusCycle/history.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start Time,End Time,Task Name,Status
2025-08-21T09:00:00,2025-08-21T09:25:00,Write blog post,Completed
2025-08-21T09:30:00,2025-08-21T09:45:00,Code review,Incomplete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task Dropdown&lt;/strong&gt;: &lt;code&gt;WorkStartWindow&lt;/code&gt; reads the top 5 recent unique task names from &lt;code&gt;SessionLogger&lt;/code&gt; and populates the task QComboBox, defaulting to your last worked task for 1-click startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commas in Task Names&lt;/strong&gt;: Task names containing commas are safely parsed by reading from both ends of each line:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Middle section between second and last comma = task name (may contain commas)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;taskName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  6. 🌐 English / Japanese UI
&lt;/h3&gt;

&lt;p&gt;The app ships with English as default and Japanese as an option, switchable at runtime without restarting. Every widget implements &lt;code&gt;retranslateUi()&lt;/code&gt; which &lt;code&gt;Application&lt;/code&gt; calls whenever the language setting changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;updateAllUiTranslations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;m_workStartWindow&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;m_breakDialog&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;m_companyBreakDialog&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;m_logWindow&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;m_settingsDialog&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;m_tray&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── app/
│   └── Application       # Orchestrator: connects signals, owns all windows
├── core/
│   ├── Settings          # Singleton, INI persistence
│   ├── SessionLogger     # Singleton, CSV read/write
│   ├── IdleMonitor       # Wayland D-Bus / X11 XScreenSaver
│   └── AppState.h        # State enum (WorkWait, Working, BreakWait, Break, CompanyBreak)
├── timer/
│   └── CycleController   # State machine, QTimer management, stopAll() cleanup
└── ui/
    ├── WorkStartWindow   # Start window with 5-task dropdown
    ├── BreakDialog       # Break prompt (Continue / Complete Work)
    ├── CompanyBreakDialog
    ├── SettingsDialog
    ├── LogWindow
    └── TrayIcon          # Monochrome system tray icon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separation is intentional: &lt;code&gt;CycleController&lt;/code&gt; never touches Qt UI classes. &lt;code&gt;Application&lt;/code&gt; bridges the core and UI layers through signals and slots.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;qtbase5-dev libqt5dbus5 libnotify-bin build-essential

git clone https://github.com/YOUR_USERNAME/FocusCycle.git
&lt;span class="nb"&gt;cd &lt;/span&gt;FocusCycle
qmake FocusCycle.pro
make &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
./FocusCycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build outputs (&lt;code&gt;*.o&lt;/code&gt;, &lt;code&gt;moc_*.cpp&lt;/code&gt;, etc.) go to &lt;code&gt;build/obj&lt;/code&gt;, &lt;code&gt;build/moc&lt;/code&gt;, and &lt;code&gt;build/rcc&lt;/code&gt; to keep the source tree clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wayland is tricky for idle detection.&lt;/strong&gt; There's no direct equivalent of &lt;code&gt;XScreenSaverQueryInfo&lt;/code&gt; on Wayland. The &lt;code&gt;org.gnome.Mutter.IdleMonitor&lt;/code&gt; D-Bus API works well on GNOME/Mutter, but it's GNOME-specific. KDE Plasma has &lt;code&gt;org.kde.KIdleTime&lt;/code&gt;, which would need a separate implementation. For now, this app targets Ubuntu with GNOME.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frameless windows need careful event filter design.&lt;/strong&gt; Making a draggable frameless window in Qt requires handling &lt;code&gt;QEvent::MouseButtonPress&lt;/code&gt; and &lt;code&gt;QEvent::MouseMove&lt;/code&gt; — either via event filters or by overriding &lt;code&gt;mousePressEvent&lt;/code&gt;/&lt;code&gt;mouseMoveEvent&lt;/code&gt;. I ended up doing both (event filter on child widgets + override on the dialog itself) to catch drags initiated anywhere on the card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean Application Quit &amp;amp; Timer Cleanup.&lt;/strong&gt; Qt singletons created with bare &lt;code&gt;new&lt;/code&gt; and background &lt;code&gt;QTimer&lt;/code&gt; instances won't stop automatically on exit if not handled. Connecting &lt;code&gt;QCoreApplication::aboutToQuit&lt;/code&gt; to &lt;code&gt;stopAll()&lt;/code&gt; in &lt;code&gt;CycleController&lt;/code&gt; ensures all timers (&lt;code&gt;m_workTimer&lt;/code&gt;, &lt;code&gt;m_breakTimer&lt;/code&gt;, &lt;code&gt;m_companyCheckTimer&lt;/code&gt;, &lt;code&gt;IdleMonitor&lt;/code&gt;) stop cleanly and the process exits without thread leaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;qAddPostRoutine&lt;/code&gt; for singleton cleanup.&lt;/strong&gt; Adding &lt;code&gt;qAddPostRoutine([]{ delete m_instance; m_instance = nullptr; })&lt;/code&gt; on first creation ensures proper singleton cleanup on exit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔗 GitHub: &lt;em&gt;[&lt;a href="https://github.com/amekusa03/FocusCycle" rel="noopener noreferrer"&gt;repository link&lt;/a&gt;]&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;📄 License: MIT&lt;/li&gt;
&lt;li&gt;📋 Language docs: README.md (English) / README.ja.md (日本語)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you're building Linux desktop apps with Qt5 and have wrestled with Wayland compatibility or idle detection, I'd love to hear how you approached it — drop a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>qt5</category>
    </item>
    <item>
      <title>Building GXBackup: A Smart, Load-Aware Linux Backup Utility in C++17 and Qt 6</title>
      <dc:creator>amekusa03</dc:creator>
      <pubDate>Tue, 18 Aug 2026 23:39:56 +0000</pubDate>
      <link>https://dev.to/amekusa03/building-gxbackup-a-smart-load-aware-linux-backup-utility-in-c17-and-qt-6-57e0</link>
      <guid>https://dev.to/amekusa03/building-gxbackup-a-smart-load-aware-linux-backup-utility-in-c17-and-qt-6-57e0</guid>
      <description>&lt;h1&gt;
  
  
  🚀 Building GXBackup: A Smart, Load-Aware Linux Backup Utility in C++17 and Qt 6
&lt;/h1&gt;

&lt;p&gt;When working on Linux desktops or development workstations, backups are essential. However, many existing GUI backup utilities suffer from common drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They are &lt;strong&gt;heavy Electron wrappers&lt;/strong&gt; that consume hundreds of megabytes of RAM.&lt;/li&gt;
&lt;li&gt;They &lt;strong&gt;hog system resources&lt;/strong&gt; during background backup tasks, causing stuttering while compiling code or rendering assets.&lt;/li&gt;
&lt;li&gt;They lack granular control over resource usage and desktop integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To solve this, I created &lt;strong&gt;GXBackup&lt;/strong&gt; — a fast, lightweight, native C++17 and Qt 6/Qt 5 application for Linux. It combines the bulletproof reliability of &lt;code&gt;rsync&lt;/code&gt; with a modern UI, system tray integration, and &lt;strong&gt;smart CPU/load-aware auto-pausing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is an overview of how GXBackup works, its key features, and some of the technical design decisions behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌟 Key Features at a Glance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;📸 Time Machine Style Snapshots&lt;/strong&gt;: Uses &lt;code&gt;rsync&lt;/code&gt;'s &lt;code&gt;--link-dest&lt;/code&gt; option to create incremental point-in-time snapshots using hardlinks, saving significant disk space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Smart Load Throttling&lt;/strong&gt;: Monitors &lt;code&gt;/proc/stat&lt;/code&gt; and &lt;code&gt;/proc/loadavg&lt;/code&gt; in real time. If CPU usage or load average exceeds configured thresholds, GXBackup automatically pauses the backup job and resumes it once the system cools down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⏰ Automated Scheduling &amp;amp; Catch-Up&lt;/strong&gt;: Set daily or weekly backup schedules. If your machine was powered off during a scheduled run, GXBackup detects the missed run at boot and executes a "catch-up" backup automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔔 System Tray &amp;amp; Desktop Integration&lt;/strong&gt;: Operates quietly in the background with minimal memory footprint and native notification support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🌐 Dynamic Runtime i18n&lt;/strong&gt;: Ships in English by default with dynamic runtime language switching (e.g., Japanese/English) without requiring an app restart.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛠 Tech Stack &amp;amp; Architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: C++17&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: Qt 6 / Qt 5 (&lt;code&gt;Core&lt;/code&gt;, &lt;code&gt;Gui&lt;/code&gt;, &lt;code&gt;Widgets&lt;/code&gt;, &lt;code&gt;Concurrent&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend Engine&lt;/strong&gt;: &lt;code&gt;rsync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build System&lt;/strong&gt;: CMake (3.16+)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GXBackup/
├── CMakeLists.txt         # Build configuration
├── resources/             # Icons &amp;amp; embedded Qt resources (.qrc)
├── translations/          # Translation files (.ts / .qm)
└── src/
    ├── main.cpp           # Entry point &amp;amp; CLI argument parsing
    ├── core/              # Core logic &amp;amp; backend
    │   ├── AutostartManager.cpp  # XDG desktop autostart manager
    │   ├── BackupController.cpp  # Backup workflow coordinator
    │   ├── HistoryManager.cpp   # Execution history persistence
    │   ├── LanguageManager.cpp  # Dynamic i18n &amp;amp; QTranslator manager
    │   ├── ProfileManager.cpp   # Profile configuration (JSON)
    │   ├── RsyncProcess.cpp     # rsync process lifecycle &amp;amp; progress parser
    │   ├── ScheduleManager.cpp  # Cron-like timer &amp;amp; missed run detector
    │   └── SystemMonitor.cpp    # Real-time /proc resource reader
    └── ui/                # Qt GUI &amp;amp; dialogs
        ├── HistoryDialog.cpp    # History table view
        ├── LogViewer.cpp        # Console log output window
        ├── MainWindow.cpp       # Main dashboard &amp;amp; tray icon
        └── ProfileDialog.cpp    # Profile configuration editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Deep-Dive: What Makes GXBackup Unique?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Smart Resource Throttling (&lt;code&gt;SystemMonitor&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;One of the biggest pain points of background backups on developer machines is resource contention. If &lt;code&gt;rsync&lt;/code&gt; starts indexing thousands of files while you are running &lt;code&gt;cmake --build&lt;/code&gt; or &lt;code&gt;docker build&lt;/code&gt;, your PC can lock up.&lt;/p&gt;

&lt;p&gt;GXBackup addresses this by inspecting system metrics directly from &lt;code&gt;/proc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reading CPU &amp;amp; Load Average from Linux pseudo-filesystems&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;SystemMonitor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;checkMetrics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;cpuUsage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readCpuUsagePercent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;loadAvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readLoadAverage1Min&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;emit&lt;/span&gt; &lt;span class="n"&gt;metricsUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpuUsage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loadAvg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;highLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cpuUsage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;m_cpuThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loadAvg&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;m_loadAvgThreshold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;highLoad&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;m_isHighLoadState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m_isHighLoadState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;highLoad&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;emit&lt;/span&gt; &lt;span class="n"&gt;loadStateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_isHighLoadState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When high load is detected, &lt;code&gt;BackupController&lt;/code&gt; intercepts the event and signals &lt;code&gt;RsyncProcess&lt;/code&gt; to pause execution, ensuring your desktop stays responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Native rsync Execution &amp;amp; Real-Time Progress Parsing
&lt;/h3&gt;

&lt;p&gt;Rather than wrapping &lt;code&gt;rsync&lt;/code&gt; blindly, GXBackup launches &lt;code&gt;rsync&lt;/code&gt; asynchronously via &lt;code&gt;QProcess&lt;/code&gt; and parses its output stream (&lt;code&gt;--progress&lt;/code&gt; format) in real time to calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transfer percentage&lt;/li&gt;
&lt;li&gt;Current transfer speed (&lt;code&gt;MB/s&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Transferred data size&lt;/li&gt;
&lt;li&gt;Estimated time remaining (&lt;code&gt;ETA&lt;/code&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;RsyncProcess&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;parseProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;QString&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Regular expression parsing of rsync --progress output lines:&lt;/span&gt;
    &lt;span class="c1"&gt;// "  1,234,567  45%  12.34MB/s  0:00:15"&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;QRegularExpression&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;R"(\s*([\d,]+)\s+(\d+)%\s+([\d\.]+\w+/s)\s+([\d:]+))"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;QRegularExpressionMatch&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hasMatch&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;toInt&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;QString&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;QString&lt;/span&gt; &lt;span class="n"&gt;transferred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;formatBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;','&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;toULongLong&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;QString&lt;/span&gt; &lt;span class="n"&gt;eta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;emit&lt;/span&gt; &lt;span class="n"&gt;progressUpdated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transferred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Dynamic i18n &amp;amp; Instant Language Switcher
&lt;/h3&gt;

&lt;p&gt;In Qt applications, switching languages usually requires restarting the app. GXBackup uses a custom &lt;code&gt;LanguageManager&lt;/code&gt; singleton that loads &lt;code&gt;.qm&lt;/code&gt; binary translation files from Qt's resource system (&lt;code&gt;:/translations/gxbackup_ja.qm&lt;/code&gt;) and broadcasts dynamic &lt;code&gt;QEvent::LanguageChange&lt;/code&gt; events.&lt;/p&gt;

&lt;p&gt;All UI windows override &lt;code&gt;changeEvent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;MainWindow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;changeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QEvent&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;QEvent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;LanguageChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;retranslateUi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;QMainWindow&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;changeEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows users to switch between English and Japanese on the fly from the &lt;strong&gt;Settings -&amp;gt; Language&lt;/strong&gt; menu with zero restart downtime!&lt;/p&gt;




&lt;h2&gt;
  
  
  💻 Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;For Ubuntu / Debian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;build-essential cmake qt6-base-dev rsync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build &amp;amp; Run
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone repository&lt;/span&gt;
git clone https://github.com/amekusa03/GXBackup.git
&lt;span class="nb"&gt;cd &lt;/span&gt;GXBackup

&lt;span class="c"&gt;# Build project&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;build
cmake ..
make &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Run GUI&lt;/span&gt;
./GXBackup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  👥 Open Source &amp;amp; Feedback
&lt;/h2&gt;

&lt;p&gt;GXBackup is open source under the &lt;strong&gt;MIT License&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/amekusa03/GXBackup" rel="noopener noreferrer"&gt;github.com/amekusa03/GXBackup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐞 &lt;strong&gt;Issues &amp;amp; Feature Requests&lt;/strong&gt;: Contributions and feedback are welcome!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are looking for a fast, responsive backup tool for your Linux desktop or home server, give GXBackup a try!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: &lt;code&gt;#linux&lt;/code&gt; &lt;code&gt;#cpp&lt;/code&gt; &lt;code&gt;#qt&lt;/code&gt; &lt;code&gt;#opensource&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>cpp</category>
      <category>qt</category>
      <category>openso</category>
    </item>
    <item>
      <title>Never Forget What You Ordered: Building 'Lunch Memo' with Jetpack Compose, Room, and AlarmManager</title>
      <dc:creator>amekusa03</dc:creator>
      <pubDate>Wed, 01 Jul 2026 09:50:02 +0000</pubDate>
      <link>https://dev.to/amekusa03/never-forget-what-you-ordered-building-lunch-memo-with-jetpack-compose-room-and-alarmmanager-1j4j</link>
      <guid>https://dev.to/amekusa03/never-forget-what-you-ordered-building-lunch-memo-with-jetpack-compose-room-and-alarmmanager-1j4j</guid>
      <description>&lt;h1&gt;
  
  
  Never Forget What You Ordered: Building "Lunch Memo" with Jetpack Compose, Room, and AlarmManager
&lt;/h1&gt;

&lt;p&gt;Have you ever stood in line at a cafeteria or ticket machine, thinking: &lt;em&gt;"Did I order the Lunch Special A or B today?"&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;For many developers working at companies with pre-ordered lunch systems, this is a daily micro-friction. You reserve a lunch days in advance, only to stand in front of the cashier trying to recall what you picked.&lt;/p&gt;

&lt;p&gt;To solve this, I built &lt;strong&gt;Lunch Memo&lt;/strong&gt;—a sleek, lightweight Android application designed to keep track of daily lunch plans and send a notification right before lunch. &lt;/p&gt;

&lt;p&gt;In this article, I’ll walk through the implementation highlights, showcasing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interactive UI&lt;/strong&gt;: Creating a fluid calendar card swiping animation with Jetpack Compose &lt;code&gt;HorizontalPager&lt;/code&gt; and &lt;code&gt;graphicsLayer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular Architecture&lt;/strong&gt;: A customizable 3-component input form (Selection, Numeric, Text) backed by Room and serialized JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact Alarms&lt;/strong&gt;: Scheduling reliable daily notifications using &lt;code&gt;AlarmManager&lt;/code&gt;, &lt;code&gt;BroadcastReceiver.goAsync()&lt;/code&gt;, and Kotlin Coroutines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Stewardship&lt;/strong&gt;: Auto-cleaning database records to keep the app's footprint tiny.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🛠️ The Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: Kotlin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI&lt;/strong&gt;: Jetpack Compose (Material 3)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Room (SQLite)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt;: AlarmManager + BroadcastReceiver&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous&lt;/strong&gt;: Coroutines &amp;amp; Flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: MVVM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency Management&lt;/strong&gt;: Gradle Version Catalog&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🎨 1. Fluid Card Swipe Animation with Jetpack Compose
&lt;/h2&gt;

&lt;p&gt;To make navigating dates feel premium, the app displays lunch memos as cards in a horizontal calendar. Swiping left or right changes the day.&lt;/p&gt;

&lt;p&gt;Instead of a static list, I used &lt;code&gt;HorizontalPager&lt;/code&gt; and manipulated the cards dynamically via &lt;code&gt;graphicsLayer&lt;/code&gt;. The active card stands out, while adjacent cards are scaled down and made translucent based on their scroll offset.&lt;/p&gt;

&lt;p&gt;Here is the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;LunchMemoContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;memos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppSettingsEntity&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="n"&gt;onMemoChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pageCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;initialPage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pageCount&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pagerState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberPagerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pageCount&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;initialPage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;initialPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;today&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nc"&gt;HorizontalPager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pagerState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;contentPadding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PaddingValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;pageSpacing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;verticalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterVertically&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plusDays&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;initialPage&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLong&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isSelected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pagerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentPage&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;

        &lt;span class="c1"&gt;// Calculate scroll offset fraction&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pageOffset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pagerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentPage&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pagerState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;currentPageOffsetFraction&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;absoluteValue&lt;/span&gt;

        &lt;span class="nc"&gt;LunchMemoCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;isEditable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;isSelected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;onMemoChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;onMemoChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;graphicsLayer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Scale and fade cards based on offset fraction&lt;/span&gt;
                &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;fraction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1f&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pageOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;coerceIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;alpha&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;scaleX&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;scaleY&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.85f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Why this works:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;graphicsLayer&lt;/code&gt; applies changes (like scale and alpha) on the GPU level without triggering recomposition. This keeps animations running at a locked 60/120 FPS.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;lerp&lt;/code&gt; (linear interpolation) function smoothly maps scroll offsets to visual properties.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  ⚙️ 2. Dynamic 3-Component Input Form
&lt;/h2&gt;

&lt;p&gt;Users need different types of memos. For example, you might want to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select a pre-configured lunch package (e.g., "A Lunch", "B Lunch").&lt;/li&gt;
&lt;li&gt;Type in a table or reservation number (Numeric).&lt;/li&gt;
&lt;li&gt;Write custom notes (Text).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To support this without over-complicating the schema, the app represents the memo as a single string composed of three space-separated parts (&lt;code&gt;"Part1 Part2 Part3"&lt;/code&gt;). In the UI, these are rendered as three configurable components:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;LunchMemoCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;isEditable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppSettingsEntity&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="n"&gt;onMemoChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Parse the space-separated parts&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;parts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;mutableStateListOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;updateMemo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onMemoChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ... Layout and Header ...&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isEditable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;ComponentInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;updateMemo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;ComponentInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;updateMemo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;Spacer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nc"&gt;ComponentInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nf"&gt;updateMemo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Display consolidated text view&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Each &lt;code&gt;ComponentInput&lt;/code&gt; displays a different UI depending on its active configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SELECTION&lt;/code&gt; renders custom tags as a flow of &lt;code&gt;FilterChip&lt;/code&gt;s.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NUMERIC&lt;/code&gt; restricts input to a specified number of digits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TEXT&lt;/code&gt; displays a simple text input.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;ComponentInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ComponentConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onValueChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Component $index"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;labelSmall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SELECTION&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;FlowRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;horizontalArrangement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arrangement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spacedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                        &lt;span class="nc"&gt;FilterChip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;selected&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;onValueChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                            &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NUMERIC&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;OutlinedTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digitLimit&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="nf"&gt;onValueChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;keyboardOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyboardType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KeyboardType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;placeholder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${config.digitLimit}-digit number"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nc"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TEXT&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;OutlinedTextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onValueChange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onValueChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⏰ 3. Precision Local Notifications via AlarmManager &amp;amp; Coroutines
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;WorkManager&lt;/code&gt; for a daily alarm is standard practice, but WorkManager is built for &lt;em&gt;deferrable&lt;/em&gt; background work. It operates on a sliding window and cannot guarantee execution at an exact minute (like exactly 12:00 PM).&lt;/p&gt;

&lt;p&gt;To wake the device and post the notification at the exact minute, Lunch Memo uses &lt;strong&gt;&lt;code&gt;AlarmManager&lt;/code&gt;&lt;/strong&gt; combined with a &lt;code&gt;BroadcastReceiver&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Triggering the Alarm
&lt;/h3&gt;

&lt;p&gt;We calculate the delay until the user's configured notification time (adjusting to the next day if that time has already passed) and schedule the alarm using &lt;code&gt;setExactAndAllowWhileIdle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;NotificationScheduler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;REQUEST_CODE&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;scheduleDailyNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;alarmManager&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSystemService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ALARM_SERVICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;AlarmManager&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;intent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NotificationReceiver&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"com.kusa.lunchmemo.NOTIFICATION_ALARM"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pendingIntent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PendingIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;REQUEST_CODE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;PendingIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_UPDATE_CURRENT&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="nc"&gt;PendingIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FLAG_IMMUTABLE&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;calendar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HOUR_OF_DAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MINUTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SECOND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MILLISECOND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeInMillis&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SDK_INT&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nc"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_CODES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canScheduleExactAlarms&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;alarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExactAndAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AlarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RTC_WAKEUP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeInMillis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendingIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;alarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAndAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AlarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RTC_WAKEUP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeInMillis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendingIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;alarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExactAndAllowWhileIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AlarmManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RTC_WAKEUP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeInMillis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendingIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Performing Async Tasks in a BroadcastReceiver with &lt;code&gt;goAsync()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Typically, a &lt;code&gt;BroadcastReceiver&lt;/code&gt; lifecycle is extremely short; performing operations like database queries inside &lt;code&gt;onReceive&lt;/code&gt; can cause an Application Not Responding (ANR) error. &lt;/p&gt;

&lt;p&gt;However, by calling &lt;strong&gt;&lt;code&gt;goAsync()&lt;/code&gt;&lt;/strong&gt;, we can safely launch a Coroutine Scope to fetch today's memo asynchronously on a dispatcher thread and keep the receiver alive until our database lookup completes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationReceiver&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BroadcastReceiver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onReceive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ACTION_BOOT_COMPLETED&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"com.kusa.lunchmemo.NOTIFICATION_ALARM"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Keep BroadcastReceiver active during asynchronous execution&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;pendingResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;goAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="nc"&gt;CoroutineScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;database&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LunchMemoDatabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;dao&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lunchMemoDao&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSettings&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hour&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;notificationHour&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
                    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;minute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;notificationMinute&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

                    &lt;span class="c1"&gt;// Reschedule for tomorrow&lt;/span&gt;
                    &lt;span class="nc"&gt;NotificationScheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheduleDailyNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="c1"&gt;// Show notification if it's the alarm action&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"com.kusa.lunchmemo.NOTIFICATION_ALARM"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;today&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ISO_LOCAL_DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;memos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAllMemos&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;todayMemo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;memo&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;todayMemo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNullOrBlank&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="nf"&gt;showNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;todayMemo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Inform the OS we're done&lt;/span&gt;
                    &lt;span class="n"&gt;pendingResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🧹 4. Lightweight Storage &amp;amp; Auto-Cleanup
&lt;/h2&gt;

&lt;p&gt;A good utility app shouldn't hoard local storage. Since past lunch memos are no longer relevant, Lunch Memo cleans them up automatically.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;LunchMemoDao&lt;/code&gt;, I defined a simple query to delete memos older than a specified date:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DELETE FROM lunch_memos WHERE date &amp;lt; :date"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;deleteMemosOlderThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Inside the &lt;code&gt;LunchMemoViewModel&lt;/code&gt;, this is invoked asynchronously on app launch:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;cleanOldMemos&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;yesterday&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;minusDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dao&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMemosOlderThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yesterday&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This keeps the local SQLite database extremely compact, containing only upcoming and today's memos.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔄 5. Room Database Migrations
&lt;/h2&gt;

&lt;p&gt;As the app grew, its schema evolved to accommodate customization settings. Handling migrations seamlessly is crucial so users do not lose their data.&lt;/p&gt;

&lt;p&gt;Here is the migration trajectory of the app's database from Version 1 to 4:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;v1&lt;/strong&gt;: Basic daily notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v1 -&amp;gt; v2&lt;/strong&gt;: Introduced &lt;code&gt;app_settings&lt;/code&gt; for customized notification times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v2 -&amp;gt; v3&lt;/strong&gt;: Added &lt;code&gt;alphanumericOnly&lt;/code&gt; setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3 -&amp;gt; v4&lt;/strong&gt;: Introduced serialization via GSON to store JSON strings representing custom layouts for the 3 input components.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;MIGRATION_3_4&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SupportSQLiteDatabase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ALTER TABLE `app_settings` ADD COLUMN `component1ConfigJson` TEXT NOT NULL DEFAULT ''"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ALTER TABLE `app_settings` ADD COLUMN `component2ConfigJson` TEXT NOT NULL DEFAULT ''"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ALTER TABLE `app_settings` ADD COLUMN `component3ConfigJson` TEXT NOT NULL DEFAULT ''"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Define default settings layouts&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;defaultConfig1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ComponentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SELECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A Lunch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"B Lunch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"C Lunch"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;defaultConfig2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ComponentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digitLimit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;defaultConfig3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ComponentConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ComponentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE `app_settings` SET `component1ConfigJson` = '$defaultConfig1'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE `app_settings` SET `component2ConfigJson` = '$defaultConfig2'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execSQL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE `app_settings` SET `component3ConfigJson` = '$defaultConfig3'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By adding these migration blocks to &lt;code&gt;Room.databaseBuilder(...).addMigrations(...)&lt;/code&gt;, updates are seamless and never crash the app for existing users.&lt;/p&gt;


&lt;h2&gt;
  
  
  🏆 Key Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unidirectional Data Flow&lt;/strong&gt;: Implementing the Room (Flow) ➔ ViewModel (StateFlow) ➔ Jetpack Compose state cycle made state updates incredibly clean and bug-free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choosing the Right Notification Tool&lt;/strong&gt;: Using &lt;code&gt;WorkManager&lt;/code&gt; is great for background syncing or analytics, but &lt;code&gt;AlarmManager&lt;/code&gt; is the correct engineering choice when notifications must arrive at a precise local time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight Serialized Configurations&lt;/strong&gt;: Storing custom configurations inside the database as serialized JSON allowed adding flexible setting patterns without having to constantly spin up new Room tables.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  📝 Conclusion
&lt;/h2&gt;

&lt;p&gt;Building &lt;strong&gt;Lunch Memo&lt;/strong&gt; was a fun project that solved a real everyday problem. It demonstrates how standard modern Android practices like Jetpack Compose, Room, and AlarmManager can create a tool that is clean, responsive, and incredibly useful.&lt;/p&gt;

&lt;p&gt;If you struggle with remembering your cafeteria bookings, try building your own customized version! &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks for reading, and happy coding!&lt;/em&gt; 🍱&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/amekusa03" rel="noopener noreferrer"&gt;
        amekusa03
      &lt;/a&gt; / &lt;a href="https://github.com/amekusa03/androidLunchMemo" rel="noopener noreferrer"&gt;
        androidLunchMemo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      ランチ予定をシンプルに管理する Android アプリ
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Lunch Memo 🍱&lt;/h1&gt;

&lt;/div&gt;

&lt;p&gt;Lunch Memo は、日々のランチの予定や記録をシンプルかつスタイリッシュに管理できる Android アプリです。
モダンな Material 3 デザインを採用し、スムーズな操作感でランチタイムを楽しく彩ります。&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;主な機能 🌟&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;スマートな記録&lt;/strong&gt;: カレンダー形式のカードをスワイプして、ランチの内容を素早くメモ。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;お昼の通知&lt;/strong&gt;: 毎日12:00に、その日のランチメモを通知。今日何食べるんだっけ？を解消します。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;モダンなデザイン&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Material 3 フル対応。&lt;/li&gt;
&lt;li&gt;スムーズなカードアニメーションとグラデーション背景。&lt;/li&gt;
&lt;li&gt;Android 13 以上の通知権限に完全対応。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;クリーンなデータ保持&lt;/strong&gt;: 過去のメモは自動的に整理され、アプリを常に軽量に保ちます。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;技術スタック 🛠️&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: Kotlin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI&lt;/strong&gt;: Jetpack Compose (Material 3)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database&lt;/strong&gt;: Room (SQLite)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Tasks&lt;/strong&gt;: WorkManager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous&lt;/strong&gt;: Coroutines &amp;amp; Flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: MVVM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency Management&lt;/strong&gt;: Gradle Version Catalog (libs.versions.toml)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotation Processing&lt;/strong&gt;: KSP (Kotlin Symbol Processing)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;セットアップ 🚀&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Android Studio (Ladybug 以降推奨) でプロジェクトを開きます。&lt;/li&gt;
&lt;li&gt;プロジェクトを Gradle Sync します。&lt;/li&gt;
&lt;li&gt;Android デバイスまたはエミュレータで実行します。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;更新履歴 📅&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;[Lunch Memo 2.0] - 2026.07.01&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;選択式入力の導入&lt;/strong&gt;: 自由入力に加え、あらかじめ登録したアイテムをタップして選択できる機能を追加。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3コンポーネント構成&lt;/strong&gt;: メモを「選択」「数字」「文字」の3つの要素に分割。用途に合わせて自由にカスタマイズ可能に。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;設定画面の強化&lt;/strong&gt;: コンポーネントごとの入力モード切替や選択肢の編集機能を実装。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;内部改善&lt;/strong&gt;: データベースのマイグレーション(v4)と、設定保存のためのシリアライズ処理(GSON)を導入。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;ライセンス 📝&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;このプロジェクトは MIT ライセンスの下で公開されています。&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/amekusa03/androidLunchMemo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>android</category>
      <category>kotlin</category>
      <category>jetpackcompose</category>
      <category>room</category>
    </item>
    <item>
      <title>Rescuing 216 Pages from the GeoCities Era: How I Built an HTML-to-Blogger Tool</title>
      <dc:creator>amekusa03</dc:creator>
      <pubDate>Fri, 03 Apr 2026 15:23:57 +0000</pubDate>
      <link>https://dev.to/amekusa03/rescuing-216-pages-from-the-geocities-era-how-i-built-an-html-to-blogger-tool-2ajk</link>
      <guid>https://dev.to/amekusa03/rescuing-216-pages-from-the-geocities-era-how-i-built-an-html-to-blogger-tool-2ajk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wecoded-2026"&gt;2026 WeCoded Challenge&lt;/a&gt;: Echoes of Experience&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I want to transfer the HTML format to Google Blogger.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The outdoor club's website (which was called a homepage back then) was archived when GeoCities shut down. The last update was in April 2014.&lt;br&gt;
If it had become completely inaccessible, I could have accepted it, but it's just too sad that it's become an archive.&lt;/p&gt;

&lt;p&gt;Okay, I'll do this!&lt;br&gt;
I declared to the old site administrator (a friend) "It's okay, let me do it."&lt;br&gt;
For now, I'll just go with Vercel's Hobby for the new site.&lt;br&gt;
Then I checked &lt;strong&gt;the old site's HTML&lt;/strong&gt;&lt;br&gt;
. What? &lt;strong&gt;216 pages.&lt;/strong&gt;&lt;br&gt;
Doing this by hand is impossible!&lt;br&gt;
The old site was beautifully made by my friend while he was learning HTML.&lt;br&gt;
I tried cleaning it with AI, but my instructions were bad and it just didn't work.&lt;br&gt;
With the help of another friend, I aimed to create a stylish new site using React and other things in VS Code.&lt;br&gt;
While doing all that, Windows started to become unstable and eventually crashed.&lt;br&gt;
The data was still there, so I reinstalled Windows and DevStudio. I lost the product ID. It 's completely over .&lt;br&gt;
So I installed Ubuntu, installed VS Code, and decided to develop with Python. (I've never used it before)&lt;br&gt;
While the installation was in progress, I also changed the new site's operational policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Members can post&lt;/li&gt;
&lt;li&gt;You can also upload videos.
With two additional requirements, the site has also been changed to Google Blogger ♫&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Getting Started with Python
&lt;/h2&gt;

&lt;p&gt;I started with 'Hello python' and created a CLI for tag cleaning of HTML files.&lt;br&gt;
Next, I tried posting to Blogger using the API, and I'm starting to get the hang of it.&lt;/p&gt;
&lt;h1&gt;
  
  
  Software Requirements Definition
&lt;/h1&gt;

&lt;p&gt;Ruthless decision&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML history management&lt;/li&gt;
&lt;li&gt;Cleaning HTML

&lt;ul&gt;
&lt;li&gt;I want to manage the design entirely in Blogger, so I'm removing font and color settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Keyword extraction and registration

&lt;ul&gt;
&lt;li&gt;Blogger allows you to set keywords for each post, so I want to use words that match the text in the HTML as keywords
(e.g., kayaking, Bouldering).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Location extraction and registration

&lt;ul&gt;
&lt;li&gt;This is also a feature that can be added to each Blogger post
(Akihabara, Mt. Fuji, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Remove Exif data from images and add watermarks.&lt;/li&gt;
&lt;li&gt;Upload articles and images&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GUI&lt;/p&gt;
&lt;h1&gt;
  
  
  implementation
&lt;/h1&gt;

&lt;p&gt;Just as you all expected, I'm totally hooked.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I was struggling to get used to Ubuntu's IME, so I installed Fcitx5 and managed to work around the problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I discovered that the Blogger API doesn't have image-related features → I established and standardized a somewhat easier manual + automatic linking procedure for images.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I couldn't get used to Python nesting → I ended up checking the nesting with my finger.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Library version issue occurred → Misunderstood Python virtual environment (venv) → Fumbled around trying to create an environment with virtualBox → Realized it was in the terminal and almost went blank.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I accidentally sent about 200 Blogger API requests without using a timer → I received an account lock warning email → I sent an apology email to Google and my account was restored the next day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I installed Wine and Excel (MS-Office) to aggregate regional data → got stuck in cell input mode and switched to LibreOffice → discovered nominatim geocoding and solved the problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trying to run a virtual Windows using insurance → Too slow. Insufficient HDD space. Ultimately, the license is too expensive, so I gave up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I asked the AI ​​to do various things, but it didn't understand the language specifications and approved them haphazardly → I ended up having to rewrite almost everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To create a GUI with Tkinter, you write the code → The base is written by AI, and then you just stare at the source code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tracing Python arguments → Why isn't &lt;code&gt;copy&lt;/code&gt; running? → Wait, even &lt;code&gt;queue&lt;/code&gt; instances are referenced when you put them?! And so on, intense debugging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I can't do it well, so I end up pressing the keys too hard → My family tells me it's too loud, and I almost cry.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Reaffirming established theories
&lt;/h1&gt;

&lt;p&gt;When it comes to technology, you either invest time, money, or wisdom.&lt;br&gt;
For me, someone who lacks wisdom, it seems it was time.&lt;br&gt;
However, I think I gained a lot.&lt;br&gt;
You can't know the taste of pudding until you try it.&lt;br&gt;
Ubuntu is wonderful. I dabbled in it a little for work, but I didn't know about the community at that time. Ultimately, what's important is human connection.&lt;/p&gt;
&lt;h1&gt;
  
  
  I would appreciate your help with debugging.
&lt;/h1&gt;

&lt;p&gt;I've tried to code it so that it should work on Windows and Mac as long as you're using a Python environment.&lt;br&gt;
It requires Google API authentication credentials, which is a bit of a hassle, but I'd appreciate it if you could debug it.&lt;br&gt;
Even just a review would be encouraging.&lt;br&gt;
Please feel free to point out any issues with the Python coding!&lt;/p&gt;

&lt;p&gt;Introduction:&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://amekusa.vercel.app/en/essays#essay=2026-01-30-htmltoblogger" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Famekusa.vercel.app%2Fogp.png" height="800" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://amekusa.vercel.app/en/essays#essay=2026-01-30-htmltoblogger" rel="noopener noreferrer" class="c-link"&gt;
            Notes Archive | Raingrass Garden
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Rainweed Garden — Full article archive. Technical essays, daily memos, cars, etc.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Famekusa.vercel.app%2FFavicon.png" width="800" height="790"&gt;
          amekusa.vercel.app
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Video:   &lt;iframe src="https://www.youtube.com/embed/gFgYCVHIfW0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;code:&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/amekusa03" rel="noopener noreferrer"&gt;
        amekusa03
      &lt;/a&gt; / &lt;a href="https://github.com/amekusa03/html-blogger" rel="noopener noreferrer"&gt;
        html-blogger
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Supports conversion and upload from HTML to Google Blogger
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;HTML to Blogger Ver0.98&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A desktop application that automatically processes local HTML files and images and posts them to Blogger.&lt;br&gt;
It performs HTML cleaning, adds watermarks to images, assigns keywords and location information, and uploads to Blogger.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Qiita Article&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://qiita.com/amekusa03/items/b8ac77cd3dd6e6cc65aa" rel="nofollow noopener noreferrer"&gt;Explanation article for this tool (Qiita)&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Introduction Video&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://youtu.be/gFgYCVHIfW0?si=fby7oARRbfOy2K4K" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e8e366f73ef5f9fd89c8979d944157babf0421cafa86c1678d4749f85080b1c3/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f67466759435648496657302f6d617872657364656661756c742e6a7067" alt="Introduction Video"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Main Features&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML Cleaning&lt;/strong&gt;: Removes unnecessary tags and normalizes formatting for posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Processing&lt;/strong&gt;: Removes EXIF data and adds watermarks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata Assignment&lt;/strong&gt;: Automatically adds keywords (&lt;code&gt;search&lt;/code&gt; tags) and location information (&lt;code&gt;georss&lt;/code&gt; tags) by analyzing the article content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blogger Upload&lt;/strong&gt;: Uploads image links and articles as drafts using the Blogger API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GUI Operation&lt;/strong&gt;: User-friendly GUI with progress visualization and error recovery features.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Processing Flow&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Processing is executed in the following order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;import_file.py&lt;/code&gt;&lt;/strong&gt;: Imports files from the source folder to the working folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;serial_file.py&lt;/code&gt;&lt;/strong&gt;: Converts file names into sequential numbering format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;clean_html.py&lt;/code&gt;&lt;/strong&gt;: Cleans up HTML…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/amekusa03/html-blogger" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>devchallenge</category>
      <category>wecoded</category>
      <category>productivity</category>
      <category>blogger</category>
    </item>
  </channel>
</rss>
