-
-
diff --git a/src/content.config.ts b/src/content.config.ts
new file mode 100644
index 0000000..d1bbd81
--- /dev/null
+++ b/src/content.config.ts
@@ -0,0 +1,18 @@
+import { glob } from "astro/loaders";
+import { z } from "astro/zod";
+import { defineCollection } from "astro:content";
+
+const BlogPostSchema = z.object({
+ title: z.string(),
+ description: z.string(),
+ date: z.date(),
+ draft: z.boolean().optional(),
+ tags: z.array(z.string()).optional(),
+});
+
+const blog = defineCollection({
+ loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
+ schema: BlogPostSchema,
+});
+
+export const collections = { blog };
diff --git a/src/content/blog/crdt-conflict-free-replicated-data-types.md b/src/content/blog/crdt-conflict-free-replicated-data-types.md
new file mode 100644
index 0000000..16a1c7b
--- /dev/null
+++ b/src/content/blog/crdt-conflict-free-replicated-data-types.md
@@ -0,0 +1,130 @@
+---
+title: 'CRDT - Conflict Free Replicated Data Types'
+description: 'A brief intro to Conflict Free Replicated Data Types'
+date: 2025-02-24T09:58:08+07:00
+draft: false
+tags:
+ - programming
+ - dart
+ - data
+---
+
+# Background
+When searching techniques for syncing data between peers, I stumbled upon CRDT (Conflict-free Replicated Data Types). It's basically a algorithm for syncing for distributed systems. CRDT ensures all data changes between peer will be synced with correct order and no data loss.
+
+Since I working with Dart (for Flutter project), I use a [CRDT library for Dart](https://github.com/cachapa/crdt). This library implements core concept of CRDT and it's pretty basic. Here some types of CRDT that often used:
+
+1. **G-Counter (Grow-only Counter)**: A counter that can only be incremented.
+2. **P-Counter (Decrement Counter)**: A counter that can be both incremented and decremented.
+3. **G-Set (Grow-only Set)**: A set that only allows elements to be added.
+4. **2P-Set (Two-Phase Set)**: A set that allows elements to be added and removed, maintaining two sets (one for additions and one for removals).
+5. **OR-Set (Observed Remove Set)**: A set that allows elements to be added and removed, using unique identifiers to track additions and removals.
+6. **LWW-Register (Last Write Wins Register)**: A register that stores the last written value, using a timestamp to determine the most recent update.
+7. **MV-Register (Multi-Value Register)**: A register that stores all values that have been written, using unique identifiers to track writes.
+# How it works -- basic version
+Main components:
+- HLC (Hardware Logical Clock). Combines _wall clock/local time_, a counter that increments, and an optional _node ID_ for uniqueness sake.
+- The data itself (usually contains a key, value, and the HLC object).
+## **Scenario: Two Devices Synchronizing Data**
+We have two devices, **Device A** and **Device B**, which both maintain their own local datasets. Each device can modify data independently. When they synchronize, their CRDT implementations will merge their changes and resolve conflicts.
+## Initial State
+- Both devices start with the same data:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice | false | HLC: A1 |
+| 2 | Bob | false | HLC: A2 |
+
+- Device A's last modified HLC: `A2`.
+- Device B's last modified HLC: `A2`.
+
+---
+## Changes Made on Each Device
+1. **Device A deletes Bob's record (key 2):**
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 2 | null | true | HLC: A3 |
+
+2. **Device B updates Alice's name to Alice Smith (key 1):**
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice Smith | false | HLC: B3 |
+
+---
+## Synchronization and Merge
+- Device A sends its **changeset** to Device B:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 2 | null | true | HLC: A3 |
+
+- Device B sends its **changeset** to Device A:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice Smith | false | HLC: B3 |
+
+---
+## Step-by-Step Conflict Resolution
+
+The `merge` method processes these changes:
+1. **Validate Changeset**:
+ Each incoming record is validated to ensure it matches the expected schema and contains valid HLC timestamps.
+
+2. **Compare Records for Key 1 (`Alice`)**:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice | false | HLC: A1 |
+
+Incoming record from Device B:
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice Smith | false | HLC: B3 |
+
+**Conflict Resolution Rule**: The record with the higher `Last Modified` HLC wins. HLC `B3 > A1,` so Device A updates Alice's record to:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice Smith | false | HLC: B3 |
+
+3. **Compare Records for Key 2 (`Bob`)**:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 2 | Bob | false | HLC: A2 |
+
+Incoming record from Device A:
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 2 | null | true | HLC: A3 |
+
+**Conflict Resolution Rule**: The record with the higher `Last Modified` HLC wins. HLC `A3 > A2`, so Device B updates Bob's record to:
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 2 | null | true | HLC: A3 |
+
+4. **Propagate Changes**:
+ Both devices now have identical datasets after merging.
+
+---
+## Final Merged Dataset on Both Devices
+
+| Key | Value | isDeleted | Last Modified |
+| --- | ----- | --------- | ------------- |
+| 1 | Alice Smith | false | HLC: B3 |
+| 2 | null | true | HLC: A3 |
+
+---
+## Summary of Conflict Resolution Rules
+
+1. **Higher HLC Wins**
+ Records with higher HLCs (later timestamps) overwrite those with lower HLCs.
+2. **Soft Deletes**
+ A `null` value with `isDeleted: true` is treated as a soft delete. It wins if its HLC is higher.
+3. **Deterministic Behavior**
+ All nodes independently apply the same conflict resolution logic, ensuring eventual consistency.
+
diff --git a/src/content/blog/fix-adb-unsufficient-permission-linux.md b/src/content/blog/fix-adb-unsufficient-permission-linux.md
new file mode 100644
index 0000000..307ab6b
--- /dev/null
+++ b/src/content/blog/fix-adb-unsufficient-permission-linux.md
@@ -0,0 +1,54 @@
+---
+title: 'Fix ADB Insufficient Permission'
+description: 'udev rules for fixing ADB Insufficient permission in Linux'
+date: 2025-02-24T11:29:30+07:00
+draft: false
+tags:
+ - android
+ - adb
+ - linux
+---
+
+# Why need this?
+Connecting an Android device to a Linux computer could has problem with permission. Listing devices with `adb devices` might show `unauthorized` status or `insufficient permission` error message.
+
+```bash
+$ adb devices
+
+List of devices attached
+15241JEC211677 device
+RR2M9002AHY unauthorized
+```
+
+This happens because by default, in Linux, when you connect an Android device via USB, the system assigns it to the `root` user and a restrictive permission mode (often 0600), meaning that regular users cannot access it.
+
+# How to fix
+
+1. Check `vendor id` and `device id` from connected Android device with `lsusb`.
+
+```bash
+$ lsusb
+
+...some other devices
+Bus 001 Device 005: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device
+```
+
+`18d1` is *vendor id* and `4ee7` is the *product id*.
+
+2. Create an `udev` rule for this device, I create mine in `/etc/udev/rules.d/51-android.rules`.
+
+```bash
+SUBSYSTEM=="usb", ATTRS{idVendor}=="18d1", ATTRS{idProduct}=="4ee7", MODE="0666", GROUP="plugdev", SYMLINK+="google_pixel_4a_%n"
+```
+
+- `SUBSYSTEM=="usb"` ensures the rule applies only to USB devices.
+- `ATTRS{idVendor}=="18d1" and ATTRS{idProduct}=="4ee7"` match the Google Pixel USB devices by their vendor and product IDs.
+- `MODE="0666"` sets the device's permission mode to 0666, meaning read/write access for all users.
+- `GROUP="plugdev"` assigns the device to the plugdev group, which allows users in that group to access it.
+- `SYMLINK+="google_pixel_4a_%n"` creates a symlink (shortcut) under /dev/ with a readable name for easier identification.
+
+3. Reconnect the device to make sure the rule is correct. If not, try to reload `udev` rules and restart it.
+
+```bash
+$ sudo udevadm control --reload-rules
+```
diff --git a/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md b/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md
new file mode 100644
index 0000000..c791825
--- /dev/null
+++ b/src/content/blog/fix-infinix-air-pro-plus-quad-speakers-linux.md
@@ -0,0 +1,91 @@
+---
+title: 'Fix Infinix Air Pro+ Quad Speakers in Linux'
+description: 'A fix for Infinix Air Pro+ -- only 2 of 4 speakers working in Linux'
+date: 2025-02-23T23:27:49+07:00
+draft: false
+tags:
+ - laptop
+ - infinix
+ - tweak
+ - linux
+---
+
+I installed Linux ([EndeavourOS](https://endeavouros.com/)) in my Infinix Air Pro+ last week and noticed that the sound coming from the speakers was bad. This laptops has 4 speakers, hence only 2 of them are working. This is how I fix this issue.
+
+# Check ALSA for Hidden Speakers
+
+```bash
+$ cat /proc/asound/card0/codec* | grep -i "node"
+
+State of AFG node 0x01:
+Node 0x02 [Audio Output] wcaps 0x41d: Stereo Amp-Out
+Node 0x03 [Audio Output] wcaps 0x41d: Stereo Amp-Out
+Node 0x04 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x05 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x06 [Audio Output] wcaps 0x611: Stereo Digital
+Node 0x07 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x08 [Audio Input] wcaps 0x10051b: Stereo Amp-In
+Node 0x09 [Audio Input] wcaps 0x10051b: Stereo Amp-In
+Node 0x0a [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x0b [Audio Mixer] wcaps 0x20010b: Stereo Amp-In
+Node 0x0c [Audio Mixer] wcaps 0x20010b: Stereo Amp-In
+Node 0x0d [Audio Mixer] wcaps 0x20010b: Stereo Amp-In
+Node 0x0e [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x0f [Audio Mixer] wcaps 0x20010a: Mono Amp-In
+Node 0x10 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x11 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x12 [Pin Complex] wcaps 0x40040b: Stereo Amp-In
+Node 0x13 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x14 [Pin Complex] wcaps 0x40058d: Stereo Amp-Out
+Node 0x15 [Pin Complex] wcaps 0x40058d: Stereo Amp-Out
+Node 0x16 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x17 [Pin Complex] wcaps 0x40050c: Mono Amp-Out
+Node 0x18 [Pin Complex] wcaps 0x40058f: Stereo Amp-In Amp-Out
+Node 0x19 [Pin Complex] wcaps 0x40058f: Stereo Amp-In Amp-Out
+Node 0x1a [Pin Complex] wcaps 0x40058f: Stereo Amp-In Amp-Out
+Node 0x1b [Pin Complex] wcaps 0x40058f: Stereo Amp-In Amp-Out
+Node 0x1c [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x1d [Pin Complex] wcaps 0x400400: Mono
+Node 0x1e [Pin Complex] wcaps 0x400781: Stereo Digital
+Node 0x1f [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x20 [Vendor Defined Widget] wcaps 0xf00040: Mono
+Node 0x21 [Vendor Defined Widget] wcaps 0xf00000: Mono
+Node 0x22 [Audio Mixer] wcaps 0x20010b: Stereo Amp-In
+Node 0x23 [Audio Mixer] wcaps 0x20010b: Stereo Amp-In
+State of AFG node 0x01:
+Node 0x03 [Audio Output] wcaps 0x6611: 8-Channels Digital
+Node 0x04 [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x05 [Audio Output] wcaps 0x6611: 8-Channels Digital
+Node 0x06 [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x07 [Audio Output] wcaps 0x6611: 8-Channels Digital
+Node 0x08 [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x09 [Audio Output] wcaps 0x6611: 8-Channels Digital
+Node 0x0a [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x0b [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x0c [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x0d [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x0e [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+Node 0x0f [Pin Complex] wcaps 0x40778d: 8-Channels Digital Amp-Out CP
+```
+
+Looking at that output, it seems that this laptop has several nodes that could be an audio output. They are nodes with `Stereo Amp-In Amp-Out` in it's description. Filtering the result with that, I got:
+
+- 0x14 - Stereo Amp-Out (I suspect this is the front speakers that are working)
+- 0x15 - Stereo Amp-Out (likely another speakers?)
+- 0x18, 0x19, 0x1a, 0x1b - Stereo Amp-In Amp-Out (might be extra speaker outputs)
+
+# Enable Additional Speakers with `hdajackretask`
+
+`hdajackretask` is a tool from ALSA that allow us to remap/retask those nodes/jack into different purposes.
+
+```bash
+$ sudo pacman -S alsa-tools
+$ sudo hdajackretask
+```
+
+After opening the tool, check the `Show unconnected pins` and there will be list of nodes that can be retasked.
+I need to experiment with this remapping. After trials and errors, I found that `0x1a` and `0x1b` is the responsible nodes for my extra speakers. Overriding them and changing their role as `Internal Speaker` solve my issue.
+
+
+
+Now all my speakers is working! I hope this will help someone in the future.
diff --git a/src/content/blog/fix-infinix-air-pro-plus-screen-color.md b/src/content/blog/fix-infinix-air-pro-plus-screen-color.md
new file mode 100644
index 0000000..c52b93d
--- /dev/null
+++ b/src/content/blog/fix-infinix-air-pro-plus-screen-color.md
@@ -0,0 +1,180 @@
+---
+title: 'Fix Infinix Air Pro+ Screen Color'
+description: 'Fixing Infinix Air Pro+ washed out screen color in Windows and Linux'
+date: 2025-02-21T18:09:54+07:00
+draft: false
+tags:
+ - laptop
+ - infinix
+ - tweak
+ - script
+---
+
+I have Infinix Air Pro+ and I use it for my work. I can say it is a good laptop coding mainly because it has 2.5k OLED 16:10 screen. But I found a problem with its screen color. When the screen brightness is below about 50% and the screen turned off (to save power, not necessarily going system sleep/suspend) and turns back on, the color looks washed out.
+
+First time I noticed this issue is because I was using a pitch black wallpaper image (so I can flex my OLED display). After my screen turns back on, my wallpaper's black color becomes grainy, washed out, as its doesn't have pitch black color anymore. Then I noticed, the color will be fixed after I crank the brightness to above 50%. Turning the brightness down again after this still gives me correct black level.
+
+So, I was wondering if I create a script that will turn the brightness to above 50% and restore it to where it was every time my screen is waking up from a sleep. With a help from Google and ChatGPT, I create these scripts as a workaround for this annoying issue.
+
+# Windows
+Before continuing, I'm sorry I can't give any screenshot for this Windows section because I already switched to Linux, but I hope I can write it clearly.
+
+## Get screen wake up event
+I need to listen to an event that tells me "Hey, the screen is turning on". Fortunately, Windows has [Event Viewer](https://learn.microsoft.com/en-us/shows/inside/event-viewer) that I can use for this. I found that an event from *Kernel-Power* with event ID *507* is the correct event that means the screen in turned back on.
+
+## Script
+Next thing to do is create the script to control screen brightness. After trial and error, I found [NirCmd](https://www.nirsoft.net/utils/nircmd.html) can help me to change my screen brightness. Then I create this Powershell script.
+
+```powershell
+# Infinix Air Pro Plus suffers from washed out colors
+# after the display goes off and back on if the brightness is below 50%.
+# This script will increase the brightness to 60% when initial brightness
+# is below 50% else it will increase 10% from current brightness and turn
+# back to initial brightness value.
+
+# Path to NirCmd executable
+$nircmd = "C:\nircmd-x64\nircmd.exe"
+
+# Function to temporarily adjust brightness
+function Adjust-Brightness {
+ param (
+ [int]$InitialBrightness,
+ [int]$TargetBrightness
+ )
+
+ # Set brightness to target
+ & $nircmd "setbrightness" $TargetBrightness
+ Start-Sleep -Seconds 2
+
+ # Restore to initial brightness
+ & $nircmd "setbrightness" $InitialBrightness
+}
+
+# Dummy current brightness (replace this with actual detection logic if available)
+$currentBrightness = (Get-CimInstance -Namespace root/WMI -ClassName WmiMonitorBrightness).CurrentBrightness
+
+if ($currentBrightness -lt 50) {
+ # If brightness is below 50%, temporarily set to 60
+ Adjust-Brightness -InitialBrightness $currentBrightness -TargetBrightness 60
+} else {
+ # Otherwise, increase brightness by 10%
+ $newBrightness = [Math]::Min($currentBrightness + 10, 100)
+ Adjust-Brightness -InitialBrightness $currentBrightness -TargetBrightness $newBrightness
+}
+```
+
+## Make a schedule
+I use Windows' [Task Scheduler](https://www.windowscentral.com/how-create-automated-tasks-windows-11) to run the script each time *Kernel-Power* with event ID *507* occurs. I can't show the step-by-step guide because I'm on Linux now, but I have a backup file for this task. All you need is just to import [this task](/misc/Restore%20OLED%20Colors.xml) in Task Scheduler.
+
+> Note: You have to change the command it executes to where you save the Powershell script. Also change the author into `YOUR_PC_NAME\YOUR_USERNAME`.
+
+# Linux
+I'm using [EndeavourOS](https://endeavouros.com/) which use `systemd`. So this guide is applicable to `systemd` init system only. If your linux use something else, you need to adjust it with your init system.
+
+## Get screen wake up event
+I already tried several ways to listen the screen wake up events. But I can't find any using `acpi` and `udev`. So I tried different approach. I check `dpms` property from screen device in `/sys/class/drm/card1-eDP-1/dpms`. It has `On` and `Off` value that I can use for triggering a script to fix the color.
+
+## Script
+I have 2 scripts for this approach. One for checking `/sys/class/drm/card1-eDP-1/dpms` value and another one for fixing the color.
+
+```bash
+#!/bin/bash
+# monitor /sys/class/drm/card1-eDP-1/dpms value
+# place it to /usr/local/bin/monitor_screen_power.sh
+
+prev_state=""
+
+while true; do
+ state=$(cat /sys/class/drm/card1-eDP-1/dpms)
+
+ if [[ "$state" != "$prev_state" && "$state" == "On" ]]; then
+ echo "Screen turned on! Running script..."
+ /usr/local/bin/brightness_fix.sh
+ fi
+
+ prev_state=$state
+ sleep 1 # Adjust polling interval as needed
+done
+```
+
+```bash
+#!/bin/bash
+# adjust the brightness
+# place it to /usr/local/bin/brightness_fix.sh
+
+# Path to brightness control (may vary based on hardware, check /sys/class/backlight/)
+BRIGHTNESS_PATH="/sys/class/backlight/intel_backlight/brightness"
+MAX_BRIGHTNESS_PATH="/sys/class/backlight/intel_backlight/max_brightness"
+
+# Read current brightness
+CURRENT_BRIGHTNESS=$(cat "$BRIGHTNESS_PATH")
+MAX_BRIGHTNESS=$(cat "$MAX_BRIGHTNESS_PATH")
+
+# Convert brightness levels to percentage
+CURRENT_PERCENT=$(( CURRENT_BRIGHTNESS * 100 / MAX_BRIGHTNESS ))
+
+# Function to set brightness based on percentage
+set_brightness() {
+ local TARGET_PERCENT=$1
+ local TARGET_BRIGHTNESS=$(( TARGET_PERCENT * MAX_BRIGHTNESS / 100 ))
+ echo $TARGET_BRIGHTNESS | sudo tee "$BRIGHTNESS_PATH" > /dev/null
+}
+
+# Adjust brightness logic
+if [ "$CURRENT_PERCENT" -lt 50 ]; then
+ set_brightness 60
+ sleep 0.5
+ set_brightness "$CURRENT_PERCENT"
+else
+ TARGET_PERCENT=$(( CURRENT_PERCENT + 10 ))
+ if [ "$TARGET_PERCENT" -gt 100 ]; then
+ TARGET_PERCENT=100
+ fi
+ set_brightness "$TARGET_PERCENT"
+ sleep 0.5
+ set_brightness "$CURRENT_PERCENT"
+fi
+```
+
+## Make a systemd service
+Make a `systemd` service in `/etc/systemd/system/brightness-fix.service` to run the first script.
+
+```plaintext
+[Unit]
+Description=Fix screen brightness on wake
+After=multi-user.target
+
+[Service]
+ExecStart=/usr/local/bin/monitor_screen_power.sh
+Restart=always
+User=dhemas
+
+[Install]
+WantedBy=multi-user.target
+```
+
+and another one to run `brightness-fix.sh` after waking up from suspend/sleep, I put it in `/etc/systemd/system/brightness-fix-wakeup.service`.
+
+```plaintext
+[Unit]
+Description=Fix screen brightness after wakeup
+After=suspend.target
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/brightness_fix.sh
+
+[Install]
+WantedBy=suspend.target
+```
+
+Then register, enable, and start it.
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable brightness-fix.service
+sudo systemctl enable brightness-fix-wakeup.service
+sudo systemctl start brightness-fix.service
+```
+
+One more thing, you can add also `/usr/local/bin/brightness_fix.sh` to autostart (I'm using KDE) so it will run each time you login.
\ No newline at end of file
diff --git a/src/content/blog/flutter-android-emulator-not-showing.md b/src/content/blog/flutter-android-emulator-not-showing.md
new file mode 100644
index 0000000..d77f3ef
--- /dev/null
+++ b/src/content/blog/flutter-android-emulator-not-showing.md
@@ -0,0 +1,25 @@
+---
+title: 'Android Devices Not Showing on Flutter Project'
+description: "A fix for new Flutter project that doesn't have Android devices showing up"
+date: 2025-02-22T09:21:22+07:00
+draft: false
+tags:
+ - programming
+ - flutter
+ - android
+ - emulator
+---
+
+When I create a new Flutter project targeting Android device, I can't choose which Android device to run it. Either it a real connected devices or AVDs, even the devices is available and listed in `Device Manager` tab.
+
+
+
+All I need to do is open `File > Project Structure...` or `Ctrl + Alt + Shift + S`.
+
+
+
+As you can see, I have no Android SDK selected for my Flutter project. So, go ahead and select one of SDK listed there and click `OK`. That's it! Now you can see all the devices available to run my Flutter project.
+
+
+
+Hope this can help someone who has similar issues with Flutter project and Android devices.
\ No newline at end of file
diff --git a/src/content/blog/flutter-clean-architecture.md b/src/content/blog/flutter-clean-architecture.md
new file mode 100644
index 0000000..8769b2a
--- /dev/null
+++ b/src/content/blog/flutter-clean-architecture.md
@@ -0,0 +1,1216 @@
+---
+title: 'Flutter Clean Architecture'
+description: Dive into Clean Architecture for Flutter or Dart projects
+images:
+- /images/opengraph.png
+date: 2025-01-12T00:12:04+07:00
+draft: false
+tags:
+ - programming
+ - flutter
+---
+
+
+
+# What is Clean Architecture?
+Do you ever wondering how to manage your Flutter code? How to make it neat, modular, easy to maintain and test? Here where *clean architecture* comes in.
+
+Basically, clean architecture is a way to organize your code into separated pieces that will make your project cleaner. It may looks complicated at first and a lot of boiler code for some reasons. But trust me, it will be a lot easier if you apply the clean architecture in your code, especially in medium to bigger projects.
+
+In this set of Clean Architecture articles, we will create a basic mobile app that uses [WeatherAPI](https://www.weatherapi.com/) to get current weather. Let's get started!
+
+> Please note that this guide requires basic knowledge of Dart and Flutter. So I don't recommend going through this guide if you are completely new to the topic.
+# Directory Structure
+I use this directory structure to organize my code into clean architecture. Once you got the idea, you may modify the structure to match your needs.
+
+```
+your-flutter-project-dir
+├── pubspec.yaml
+├── lib
+│ ├── core
+│ │ ├── data
+│ │ │ ├── local
+│ │ │ ├── remote
+│ │ ├── domain
+│ │ ├── error
+│ │ ├── network
+│ │ ├── presentation
+│ │ ├── routes
+│ │
+│ ├── features
+│ │ ├── feature_name
+│ │ │ ├── data
+│ │ │ │ ├── data_sources
+│ │ │ │ │ ├── local
+│ │ │ │ │ ├── remote
+│ │ │ │ ├── models
+│ │ │ │ ├── repositories
+│ │ │ ├── domain
+│ │ │ │ ├── repositories
+│ │ │ │ ├── use_cases
+│ │ │ ├── presentation
+│ │
+│ ├── injection_container.dart
+│ ├── main.dart
+│
+├── ... other files
+```
+
+---
+## Core
+You'll stores all reusable code inside `core`. Things like abstract classes (maybe a model base, error base, etc), or maybe a base widgets, snackbars, dialogs, also your app router, anything that you need to access across your app are best to keep inside `core` directory.
+### Core - Data
+`core/data` stores base classes related to your data. Divided into `local` for locally-stored data (ex: configs, persistence, cache), and `remote` for data from external sources (ex: web API).
+
+Let's create a local `config.dart` base class to store app configuration using [shared_preferences](https://pub.dev/packages/shared_preferences) .
+
+```dart
+// lib/core/data/local/config.dart
+
+/// Config base class
+abstract class Config {
+ /// Get config value
+ Future get();
+
+ /// Set config value
+ Future set(T value);
+}
+```
+
+For an example, we want to have a config for storing our app theme mode. So the app can restore theme mode data (light mode and dark mode) each time we open it.
+
+```dart
+// lib/core/data/local/theme_mode_config.dart
+
+import 'package:clean_architecture/core/data/local/config.dart';
+import 'package:flutter/material.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// Theme mode shared preferences key
+const themeModeConfigKey = 'themeMode';
+
+/// Theme mode configuration
+class ThemeModeConfig extends Config {
+ /// Default constructor
+ ThemeModeConfig({required this.sharedPreferences});
+
+ /// Shared preferences instance
+ final SharedPreferences sharedPreferences;
+
+ @override
+ Future get() async {
+ final mode = sharedPreferences.getString(themeModeConfigKey);
+ switch (mode) {
+ case 'dark':
+ return ThemeMode.dark;
+ case 'light':
+ return ThemeMode.light;
+ case 'system':
+ return ThemeMode.system;
+ default:
+ return ThemeMode.system;
+ }
+ }
+
+ @override
+ Future set(ThemeMode value) async {
+ switch (value) {
+ case ThemeMode.dark:
+ await sharedPreferences.setString(themeModeConfigKey, 'dark');
+ case ThemeMode.light:
+ await sharedPreferences.setString(themeModeConfigKey, 'light');
+ case ThemeMode.system:
+ await sharedPreferences.setString(themeModeConfigKey, 'system');
+ }
+ }
+}
+```
+
+Then we also need to add `weather_api_response.dart` model class for the [WeatherAPI](https://www.weatherapi.com/) response using [json_serializable](https://pub.dev/packages/json_serializable) package.
+
+```dart
+// lib/core/data/remote/models/weather_api_response_model.dart
+
+import 'package:json_annotation/json_annotation.dart';
+
+part 'weather_api_response_model.g.dart';
+
+@JsonSerializable()
+class WeatherApiResponseModel {
+ final WeatherApiLocationModel? location;
+ final WeatherApiErrorModel? error;
+
+ WeatherApiResponseModel({
+ required this.location,
+ required this.error,
+ });
+
+ factory WeatherApiResponseModel.fromJson(Map json) =>
+ _$WeatherApiResponseModelFromJson(json);
+}
+
+@JsonSerializable()
+class WeatherApiLocationModel {
+ final String name;
+ final String region;
+ final String country;
+
+ const WeatherApiLocationModel({
+ required this.name,
+ required this.region,
+ required this.country,
+ });
+
+ factory WeatherApiLocationModel.fromJson(Map json) =>
+ _$WeatherApiLocationModelFromJson(json);
+}
+
+@JsonSerializable()
+class WeatherApiErrorModel {
+ final int code;
+ final String message;
+
+ const WeatherApiErrorModel({
+ required this.code,
+ required this.message,
+ });
+
+ factory WeatherApiErrorModel.fromJson(Map json) =>
+ _$WeatherApiErrorModelFromJson(json);
+}
+```
+### Core - Domain
+`core/domain` contains *use case* base class. If you unfamiliar with a *use case* (also called *unit-of-work*), it's a **single-purpose** class that has a method `execute/call` to do particular function in your app. We'll find out how it works in several sections ahead.
+
+In this class we use [fpdart](https://pub.dev/packages/fpdart)'s `Either` class. In [Functional Programming](), `Either` means a function that will return a `Right` value for positive/success scenario, or `Left` when it fails. You can read about it in the previous links.
+
+I'll try to explain briefly, `use_case.dart` below has 2 generics. `Type` is a return type when the *use case* is succesfully executed, and `Params` contains parameters that are required to execute the *use case*. Then in `call` method it has return type of `Either`. It means this method will returns `Type` if success, and `Failure` when things got ugly.
+
+```dart
+// lib/core/domain/use_case.dart
+
+import 'package:clean_architecture/core/error/failures.dart';
+import 'package:fpdart/fpdart.dart';
+
+/// [Type] is the return type of a successful use case call.
+/// [Params] are the parameters that are required to call the use case.
+abstract class UseCase {
+ /// Execute the use case
+ Future> call(Params params);
+}
+```
+### Core - Error
+We'll use `core/error` dir to stores `Failure` classes. `Failure` used when the app throws errors and exceptions. It's like having a custom exception class.
+
+```dart
+// lib/core/error/failures.dart
+
+import 'package:equatable/equatable.dart';
+
+/// Base class for all failures
+abstract class Failure extends Equatable {
+ const Failure({
+ required this.message,
+ this.cause,
+ });
+
+ /// Message of the failure
+ final String message;
+
+ /// Cause of the failure
+ final Exception? cause;
+
+ @override
+ List