refactor: update titles and descriptions to use double quotes for consistency

fix: add newlines and formatting improvements in blog posts

refactor: update import paths to use alias for better readability

style: update global CSS to use double quotes for consistency

chore: update tsconfig to include path aliasing for cleaner imports
This commit is contained in:
fiatcode 2026-01-22 17:20:59 +07:00
parent 05dfaed605
commit 33083907fb
22 changed files with 618 additions and 490 deletions

View file

@ -1,11 +1,11 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from "@tailwindcss/vite";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
vite: { vite: {
plugins: [tailwindcss()] plugins: [tailwindcss()],
} },
}); });

62
package-lock.json generated
View file

@ -13,7 +13,9 @@
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19" "@tailwindcss/typography": "^0.5.19",
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1"
} }
}, },
"node_modules/@astrojs/compiler": { "node_modules/@astrojs/compiler": {
@ -4438,6 +4440,37 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-plugin-astro": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-0.14.1.tgz",
"integrity": "sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@astrojs/compiler": "^2.9.1",
"prettier": "^3.0.0",
"sass-formatter": "^0.7.6"
},
"engines": {
"node": "^14.15.0 || >=16.0.0"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.30.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@ -4761,6 +4794,23 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/s.color": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz",
"integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==",
"dev": true,
"license": "MIT"
},
"node_modules/sass-formatter": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.7.9.tgz",
"integrity": "sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"suf-log": "^2.5.3"
}
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
@ -4926,6 +4976,16 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/suf-log": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
"integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==",
"dev": true,
"license": "MIT",
"dependencies": {
"s.color": "0.0.15"
}
},
"node_modules/svgo": { "node_modules/svgo": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",

View file

@ -6,7 +6,8 @@
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
@ -14,6 +15,8 @@
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19" "@tailwindcss/typography": "^0.5.19",
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1"
} }
} }

View file

@ -1,5 +1,5 @@
--- ---
import NavLink from "./NavLink.astro"; import NavLink from "@/components/NavLink.astro";
--- ---
<header class="py-4 text-black dark:text-white w-full"> <header class="py-4 text-black dark:text-white w-full">

View file

@ -1,11 +1,11 @@
--- ---
import { Image } from 'astro:assets'; import { Image } from 'astro:assets';
import profileImage from "../assets/images/profile.jpg"; import profileImage from "@/assets/images/profile.jpg";
import socialEmail from "../assets/images/social-email.svg"; import socialEmail from "@/assets/images/social-email.svg";
import socialFacebook from "../assets/images/social-facebook.svg"; import socialFacebook from "@/assets/images/social-facebook.svg";
import socialGithub from "../assets/images/social-github.svg"; import socialGithub from "@/assets/images/social-github.svg";
import socialLinkedIn from "../assets/images/social-linkedin.svg"; import socialLinkedIn from "@/assets/images/social-linkedin.svg";
import socialX from "../assets/images/social-x.svg"; import socialX from "@/assets/images/social-x.svg";
--- ---
<div class="flex flex-col min-h-full items-center justify-center"> <div class="flex flex-col min-h-full items-center justify-center">

View file

@ -1,6 +1,6 @@
--- ---
title: 'CRDT - Conflict Free Replicated Data Types' title: "CRDT - Conflict Free Replicated Data Types"
description: 'A brief intro to Conflict Free Replicated Data Types' description: "A brief intro to Conflict Free Replicated Data Types"
date: 2025-02-24T09:58:08+07:00 date: 2025-02-24T09:58:08+07:00
draft: false draft: false
tags: tags:
@ -10,6 +10,7 @@ tags:
--- ---
# Background # 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. 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: 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:
@ -21,63 +22,75 @@ Since I working with Dart (for Flutter project), I use a [CRDT library for Dart]
5. **OR-Set (Observed Remove Set)**: A set that allows elements to be added and removed, using unique identifiers to track additions and 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. 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. 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 # How it works -- basic version
Main components: Main components:
- HLC (Hardware Logical Clock). Combines _wall clock/local time_, a counter that increments, and an optional _node ID_ for uniqueness sake. - 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). - The data itself (usually contains a key, value, and the HLC object).
## **Scenario: Two Devices Synchronizing Data** ## **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. 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 ## Initial State
- Both devices start with the same data: - Both devices start with the same data:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----- | --------- | ------------- |
| 1 | Alice | false | HLC: A1 | | 1 | Alice | false | HLC: A1 |
| 2 | Bob | false | HLC: A2 | | 2 | Bob | false | HLC: A2 |
- Device A's last modified HLC: `A2`. - Device A's last modified HLC: `A2`.
- Device B's last modified HLC: `A2`. - Device B's last modified HLC: `A2`.
--- ---
## Changes Made on Each Device ## Changes Made on Each Device
1. **Device A deletes Bob's record (key 2):** 1. **Device A deletes Bob's record (key 2):**
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----- | --------- | ------------- |
| 2 | null | true | HLC: A3 | | 2 | null | true | HLC: A3 |
2. **Device B updates Alice's name to Alice Smith (key 1):** 2. **Device B updates Alice's name to Alice Smith (key 1):**
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----------- | --------- | ------------- |
| 1 | Alice Smith | false | HLC: B3 | | 1 | Alice Smith | false | HLC: B3 |
--- ---
## Synchronization and Merge ## Synchronization and Merge
- Device A sends its **changeset** to Device B: - Device A sends its **changeset** to Device B:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----- | --------- | ------------- |
| 2 | null | true | HLC: A3 | | 2 | null | true | HLC: A3 |
- Device B sends its **changeset** to Device A: - Device B sends its **changeset** to Device A:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----------- | --------- | ------------- |
| 1 | Alice Smith | false | HLC: B3 | | 1 | Alice Smith | false | HLC: B3 |
--- ---
## Step-by-Step Conflict Resolution ## Step-by-Step Conflict Resolution
The `merge` method processes these changes: 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.
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`)**: 2. **Compare Records for Key 1 (`Alice`)**:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----- | --------- | ------------- |
| 1 | Alice | false | HLC: A1 | | 1 | Alice | false | HLC: A1 |
Incoming record from Device B: Incoming record from Device B:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
@ -86,15 +99,15 @@ Incoming record from Device B:
**Conflict Resolution Rule**: The record with the higher `Last Modified` HLC wins. HLC `B3 > A1,` so Device A updates Alice's record to: **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 | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----------- | --------- | ------------- |
| 1 | Alice Smith | false | HLC: B3 | | 1 | Alice Smith | false | HLC: B3 |
3. **Compare Records for Key 2 (`Bob`)**: 3. **Compare Records for Key 2 (`Bob`)**:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----- | --------- | ------------- |
| 2 | Bob | false | HLC: A2 | | 2 | Bob | false | HLC: A2 |
Incoming record from Device A: Incoming record from Device A:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
@ -105,20 +118,22 @@ Incoming record from Device A:
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----- | --------- | ------------- |
| 2 | null | true | HLC: A3 | | 2 | null | true | HLC: A3 |
4. **Propagate Changes**: 4. **Propagate Changes**:
Both devices now have identical datasets after merging. Both devices now have identical datasets after merging.
--- ---
## Final Merged Dataset on Both Devices ## Final Merged Dataset on Both Devices
| Key | Value | isDeleted | Last Modified | | Key | Value | isDeleted | Last Modified |
| --- | ----- | --------- | ------------- | | --- | ----------- | --------- | ------------- |
| 1 | Alice Smith | false | HLC: B3 | | 1 | Alice Smith | false | HLC: B3 |
| 2 | null | true | HLC: A3 | | 2 | null | true | HLC: A3 |
--- ---
## Summary of Conflict Resolution Rules ## Summary of Conflict Resolution Rules
1. **Higher HLC Wins** 1. **Higher HLC Wins**
@ -127,4 +142,3 @@ Incoming record from Device A:
A `null` value with `isDeleted: true` is treated as a soft delete. It wins if its HLC is higher. A `null` value with `isDeleted: true` is treated as a soft delete. It wins if its HLC is higher.
3. **Deterministic Behavior** 3. **Deterministic Behavior**
All nodes independently apply the same conflict resolution logic, ensuring eventual consistency. All nodes independently apply the same conflict resolution logic, ensuring eventual consistency.

View file

@ -1,6 +1,6 @@
--- ---
title: 'Fix ADB Insufficient Permission' title: "Fix ADB Insufficient Permission"
description: 'udev rules for fixing ADB Insufficient permission in Linux' description: "udev rules for fixing ADB Insufficient permission in Linux"
date: 2025-02-24T11:29:30+07:00 date: 2025-02-24T11:29:30+07:00
draft: false draft: false
tags: tags:
@ -10,6 +10,7 @@ tags:
--- ---
# Why need this? # 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. 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 ```bash
@ -33,7 +34,7 @@ $ lsusb
Bus 001 Device 005: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device Bus 001 Device 005: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device
``` ```
`18d1` is *vendor id* and `4ee7` is the *product id*. `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`. 2. Create an `udev` rule for this device, I create mine in `/etc/udev/rules.d/51-android.rules`.

View file

@ -1,6 +1,6 @@
--- ---
title: 'Fix Infinix Air Pro+ Quad Speakers in Linux' title: "Fix Infinix Air Pro+ Quad Speakers in Linux"
description: 'A fix for Infinix Air Pro+ -- only 2 of 4 speakers working 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 date: 2025-02-23T23:27:49+07:00
draft: false draft: false
tags: tags:

View file

@ -1,6 +1,6 @@
--- ---
title: 'Fix Infinix Air Pro+ Screen Color' title: "Fix Infinix Air Pro+ Screen Color"
description: 'Fixing Infinix Air Pro+ washed out screen color in Windows and Linux' description: "Fixing Infinix Air Pro+ washed out screen color in Windows and Linux"
date: 2025-02-21T18:09:54+07:00 date: 2025-02-21T18:09:54+07:00
draft: false draft: false
tags: tags:
@ -17,12 +17,15 @@ First time I noticed this issue is because I was using a pitch black wallpaper i
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. 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 # 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. 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 ## 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.
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 ## 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. 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 ```powershell
@ -64,17 +67,21 @@ if ($currentBrightness -lt 50) {
``` ```
## Make a schedule ## 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.
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`. > 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 # 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. 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 ## 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. 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 ## 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. 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 ```bash
@ -137,6 +144,7 @@ fi
``` ```
## Make a systemd service ## Make a systemd service
Make a `systemd` service in `/etc/systemd/system/brightness-fix.service` to run the first script. Make a `systemd` service in `/etc/systemd/system/brightness-fix.service` to run the first script.
```plaintext ```plaintext

View file

@ -1,5 +1,5 @@
--- ---
title: 'Android Devices Not Showing on Flutter Project' title: "Android Devices Not Showing on Flutter Project"
description: "A fix for new Flutter project that doesn't have Android devices showing up" description: "A fix for new Flutter project that doesn't have Android devices showing up"
date: 2025-02-22T09:21:22+07:00 date: 2025-02-22T09:21:22+07:00
draft: false draft: false

View file

@ -1,8 +1,8 @@
--- ---
title: 'Flutter Clean Architecture' title: "Flutter Clean Architecture"
description: Dive into Clean Architecture for Flutter or Dart projects description: Dive into Clean Architecture for Flutter or Dart projects
images: images:
- /images/opengraph.png - /images/opengraph.png
date: 2025-01-12T00:12:04+07:00 date: 2025-01-12T00:12:04+07:00
draft: false draft: false
tags: tags:
@ -13,14 +13,17 @@ tags:
![flutter-clean-architecture](/images/flutter_dash.png) ![flutter-clean-architecture](/images/flutter_dash.png)
# What is Clean Architecture? # 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.
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. 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! 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. > 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 # 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. 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.
``` ```
@ -57,9 +60,13 @@ your-flutter-project-dir
``` ```
--- ---
## Core ## 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. 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
`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). `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) . Let's create a local `config.dart` base class to store app configuration using [shared_preferences](https://pub.dev/packages/shared_preferences) .
@ -179,12 +186,14 @@ class WeatherApiErrorModel {
_$WeatherApiErrorModelFromJson(json); _$WeatherApiErrorModelFromJson(json);
} }
``` ```
### Core - Domain ### 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.
`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. 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<Failure, Type>`. It means this method will returns `Type` if success, and `Failure` when things got ugly. 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<Failure, Type>`. It means this method will returns `Type` if success, and `Failure` when things got ugly.
```dart ```dart
// lib/core/domain/use_case.dart // lib/core/domain/use_case.dart
@ -199,7 +208,9 @@ abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params); Future<Either<Failure, Type>> call(Params params);
} }
``` ```
### Core - Error ### 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. 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 ```dart
@ -262,7 +273,9 @@ class UnauthorizedException implements Exception {
final String message; final String message;
} }
``` ```
### Core - Network ### Core - Network
We will need a HTTP client to get data from [WeatherAPI](https://www.weatherapi.com/). I'll use [http](https://pub.dev/packages/http) package, but you can also use [dio](https://pub.dev/packages/dio) or another similar packages. We will need a HTTP client to get data from [WeatherAPI](https://www.weatherapi.com/). I'll use [http](https://pub.dev/packages/http) package, but you can also use [dio](https://pub.dev/packages/dio) or another similar packages.
```dart ```dart
@ -309,8 +322,10 @@ class NetworkImpl implements Network {
} }
``` ```
If you are still new in programming, you may wonder: *Why I should create an abstract class here? It will be okay with a concrete Network class without inheritance*. I'll explain it later, but for now is enough for you to know that this abstract class will be used as a *mock* in testing. If you are still new in programming, you may wonder: _Why I should create an abstract class here? It will be okay with a concrete Network class without inheritance_. I'll explain it later, but for now is enough for you to know that this abstract class will be used as a _mock_ in testing.
### Core - Presentation ### Core - Presentation
`core/presentation` contains UI widgets and other presentation related classes that will be used across your app. We can also have a UI-related business logic that will be used across the app. Since our app will have theme mode switching feature, we will add a `cubit` to do the theme mode switch here. `core/presentation` contains UI widgets and other presentation related classes that will be used across your app. We can also have a UI-related business logic that will be used across the app. Since our app will have theme mode switching feature, we will add a `cubit` to do the theme mode switch here.
```dart ```dart
@ -364,9 +379,13 @@ class ThemeModeCubit extends Cubit<ThemeMode> {
emit(themeMode); emit(themeMode);
}} }}
``` ```
### Core - Routes ### Core - Routes
There is a package called [auto_route](https://pub.dev/packages/auto_route) that will ease you to manage routes in your app yet keep your code clean. Using the guide from their package page, we'll have `app_router.dart` inside `core/routes` directory. Since we don't have any page to route to yet, just leave it empty. There is a package called [auto_route](https://pub.dev/packages/auto_route) that will ease you to manage routes in your app yet keep your code clean. Using the guide from their package page, we'll have `app_router.dart` inside `core/routes` directory. Since we don't have any page to route to yet, just leave it empty.
### Env File ### Env File
Storing secret directly in the code is a bad practice and we shouldn't do that. There are many ways to hardcode it into the code and one of them is to have an `env` file. You can read more about it [here](https://dart.dev/libraries/core/environment-declarations). Storing secret directly in the code is a bad practice and we shouldn't do that. There are many ways to hardcode it into the code and one of them is to have an `env` file. You can read more about it [here](https://dart.dev/libraries/core/environment-declarations).
```dart ```dart
@ -387,9 +406,13 @@ class EnvImpl implements Env {
``` ```
--- ---
### Feature ### Feature
In clean architecture, we divide our application into **features**. For example, in this project will have a **weather** feature. Each feature will have its own (but not always neccessarily) `domain`, `data` and `presentation`. In clean architecture, we divide our application into **features**. For example, in this project will have a **weather** feature. Each feature will have its own (but not always neccessarily) `domain`, `data` and `presentation`.
### Feature - Data ### Feature - Data
`data` as it's namesake, will deals with all data needed by the app. It contains (not limited to) `model`, `repository` implementation, and `data sources`. `Model` and `repository` classes are self-explanatory, and `data sources` will be used to access data from both local and remote sources. `data` as it's namesake, will deals with all data needed by the app. It contains (not limited to) `model`, `repository` implementation, and `data sources`. `Model` and `repository` classes are self-explanatory, and `data sources` will be used to access data from both local and remote sources.
Let's create a model for [WeatherAPI](https://www.weatherapi.com/) current weather response. Let's create a model for [WeatherAPI](https://www.weatherapi.com/) current weather response.
@ -504,7 +527,9 @@ class WeatherApiRemoteDataSourceImpl implements WeatherApiRemoteDataSource {
} }
} }
``` ```
### Feature - Domain ### Feature - Domain
`domain` stores `entities`, `use cases` and `abstract repository` classes, as they are the domain or subject area of an application. If you arent familiar with the term, you can think that this domain is the base requirement of an application. `domain` stores `entities`, `use cases` and `abstract repository` classes, as they are the domain or subject area of an application. If you arent familiar with the term, you can think that this domain is the base requirement of an application.
First thing, we need to create an `entity` for current weather data. This entity will represent what kind of data we want to show to the user. First thing, we need to create an `entity` for current weather data. This entity will represent what kind of data we want to show to the user.
@ -654,7 +679,9 @@ class WeatherApiRepositoryImpl implements WeatherApiRepository {
} }
} }
``` ```
### Feature - Presentation ### Feature - Presentation
`presentation` stores **pages** and **widgets**. These are the 'presentation' or 'view' area of the application. If you aren't familiar with the term, you can think that this 'presentation' is the actual view of an application. `presentation` stores **pages** and **widgets**. These are the 'presentation' or 'view' area of the application. If you aren't familiar with the term, you can think that this 'presentation' is the actual view of an application.
In this `presentation` layer, we use [auto_route](https://pub.dev/packages/auto_route) package to manage our pages routing. Then [flutter_bloc](https://pub.dev/packages/flutter_bloc) package will help us to manage state management hence keeping our code clean because we will separate the logic from the UI. In this `presentation` layer, we use [auto_route](https://pub.dev/packages/auto_route) package to manage our pages routing. Then [flutter_bloc](https://pub.dev/packages/flutter_bloc) package will help us to manage state management hence keeping our code clean because we will separate the logic from the UI.
@ -1032,7 +1059,9 @@ class AppRouter extends RootStackRouter {
``` ```
--- ---
## Dependency Injection ## Dependency Injection
In clean architecture, we use **dependency injection** (or DI in short) to make our project cleaner. In a traditional way of creating an instance, we need to use **contructor injection** as we pass the required parameters to the constructor. It will make a mess if we are creating many instances throughout the project, because it will scattered anywhere. In clean architecture, we use **dependency injection** (or DI in short) to make our project cleaner. In a traditional way of creating an instance, we need to use **contructor injection** as we pass the required parameters to the constructor. It will make a mess if we are creating many instances throughout the project, because it will scattered anywhere.
There are many ways to achieve dependency injection in Flutter, for this project I will use [GetIt](https://pub.dev/packages/get_it). Let's create our `injection_container`, this class is responsible for creating all the instances that we need in our project. There are many ways to achieve dependency injection in Flutter, for this project I will use [GetIt](https://pub.dev/packages/get_it). Let's create our `injection_container`, this class is responsible for creating all the instances that we need in our project.
@ -1208,7 +1237,9 @@ class WeatherApp extends StatelessWidget {
``` ```
--- ---
## Testing ## Testing
. .
--- ---

View file

@ -1,6 +1,6 @@
--- ---
title: 'Kuwot' title: "Kuwot"
description: 'Flutter Daily Quote App' description: "Flutter Daily Quote App"
date: 2025-02-20T15:26:04+07:00 date: 2025-02-20T15:26:04+07:00
draft: true draft: true
tags: tags:
@ -11,9 +11,11 @@ tags:
I made [Kuwot](https://play.google.com/store/apps/details?id=com.dhemasnurjaya.kuwot) last year in my spare time. I want to share it, you can use it as an inspiration, practice app, or something else is up to you since I open-sourced the code (links below). I made [Kuwot](https://play.google.com/store/apps/details?id=com.dhemasnurjaya.kuwot) last year in my spare time. I want to share it, you can use it as an inspiration, practice app, or something else is up to you since I open-sourced the code (links below).
# Introduction # Introduction
The Flutter app itself was built using [Clean Architecture]({{< ref "/posts/flutter-clean-architecture/index.md" >}}). It has all basic features from a daily quote application. At first, my idea is to make a simple daily quote application. Showing random quote everytime the app is opened, and give it an image background to make it more appealing. The Flutter app itself was built using [Clean Architecture]({{< ref "/posts/flutter-clean-architecture/index.md" >}}). It has all basic features from a daily quote application. At first, my idea is to make a simple daily quote application. Showing random quote everytime the app is opened, and give it an image background to make it more appealing.
# Hunting for quote data # Hunting for quote data
First thing I did was searching some kind of quote data that available for free. I stumbled across several choices: First thing I did was searching some kind of quote data that available for free. I stumbled across several choices:
- Use available quote API, eg: [Zen Quotes](https://zenquotes.io) - Use available quote API, eg: [Zen Quotes](https://zenquotes.io)
@ -25,15 +27,17 @@ After many consideration, I'd like to use this [Quotes 500k](https://github.com/
First problem solved, since I have the quote dataset I was thinking whether I embed this dataset as SQLite database into the app or do something else. But then I remember that I still need to get the background image for the app and it will make no sense to embed the background images into the app as well. So I decided to create a simple REST API for Kuwot. First problem solved, since I have the quote dataset I was thinking whether I embed this dataset as SQLite database into the app or do something else. But then I remember that I still need to get the background image for the app and it will make no sense to embed the background images into the app as well. So I decided to create a simple REST API for Kuwot.
# Building the API # Building the API
Now I need a simple, easy to make REST API. I was thinking to use Python's [FastAPI](https://fastapi.tiangolo.com/) but after some onboarding tutorial, I just don't like it. Then I found [dart_frog](https://dartfrog.vgv.dev/)! It's a minimalist backend framework written in Dart. There are no reason to not use this since it use same language as my Flutter app, that was I thought. Now I need a simple, easy to make REST API. I was thinking to use Python's [FastAPI](https://fastapi.tiangolo.com/) but after some onboarding tutorial, I just don't like it. Then I found [dart_frog](https://dartfrog.vgv.dev/)! It's a minimalist backend framework written in Dart. There are no reason to not use this since it use same language as my Flutter app, that was I thought.
Building the API is straight-forward, [dart_frog](https://dartfrog.vgv.dev/) has everything I need to build the API. Building the API is straight-forward, [dart_frog](https://dartfrog.vgv.dev/) has everything I need to build the API.
# Reshaping dataset # Reshaping dataset
The data I got from [Quotes 500k](https://github.com/ShivaliGoel/Quotes-500K) is looking like this: The data I got from [Quotes 500k](https://github.com/ShivaliGoel/Quotes-500K) is looking like this:
| Quote | Author | Tags | | Quote | Author | Tags |
| ----- | ------ | ---- | | ---------------------------------------------------------------- | -------------- | ----------------------------------- |
| A friend is someone who knows all about you and still loves you. | Elbert Hubbard | friend, friendship, knowledge, love | | A friend is someone who knows all about you and still loves you. | Elbert Hubbard | friend, friendship, knowledge, love |
Now, I didn't want the tags and I don't want a long quote. So I make a Python script to filter those quotes into a new dataset I need. Now, I didn't want the tags and I don't want a long quote. So I make a Python script to filter those quotes into a new dataset I need.
@ -43,4 +47,5 @@ Now, I didn't want the tags and I don't want a long quote. So I make a Python sc
``` ```
# UI Design # UI Design
I want to make it as simple as possible while focusing on the functionality. I want to make it as simple as possible while focusing on the functionality.

View file

@ -1,6 +1,6 @@
--- ---
title: 'Remap Infinix Air Pro+ Copilot Key in Linux' title: "Remap Infinix Air Pro+ Copilot Key in Linux"
description: 'Re-using Copilot key for something else more useful' description: "Re-using Copilot key for something else more useful"
date: 2025-03-01T22:41:32+07:00 date: 2025-03-01T22:41:32+07:00
draft: false draft: false
tags: tags:
@ -15,10 +15,12 @@ tags:
Let me rephrase that! Even when I was using Windows, I never using this Copilot shortcut key in my keyboard 😬. Fortunately, using Linux I can remap this key for something else ~~more useful~~. Let me rephrase that! Even when I was using Windows, I never using this Copilot shortcut key in my keyboard 😬. Fortunately, using Linux I can remap this key for something else ~~more useful~~.
# Requirements # Requirements
- A laptop (I'm using Infinix Air Pro+) with a working keyboard. - A laptop (I'm using Infinix Air Pro+) with a working keyboard.
- [keyd](https://github.com/rvaiya/keyd) - [keyd](https://github.com/rvaiya/keyd)
# Finding what this Copilot key do # Finding what this Copilot key do
- Open your favorite terminal and execute `sudo keyd monitor`. This command will print what events are triggered when a particular key is pressed. - Open your favorite terminal and execute `sudo keyd monitor`. This command will print what events are triggered when a particular key is pressed.
- Press the Copilot key and read the output. In my laptop, it print out this: - Press the Copilot key and read the output. In my laptop, it print out this:
@ -31,6 +33,7 @@ AT Translated Set 2 keyboard 0001:0001:70533846 f23 down
- Now I know that my copilot key triggers `leftmeta`, `leftshift`, and `f23`. It's seem legit combination of modifier keys and a function key. But unfortunately when I tried to use it in my desktop environment (I use KDE) to bind a shortcut, it only detect the modifier `meta` and `shift`. - Now I know that my copilot key triggers `leftmeta`, `leftshift`, and `f23`. It's seem legit combination of modifier keys and a function key. But unfortunately when I tried to use it in my desktop environment (I use KDE) to bind a shortcut, it only detect the modifier `meta` and `shift`.
# `keyd` for the rescue # `keyd` for the rescue
Edit `/etc/keyd/default.conf` file and I added these lines: Edit `/etc/keyd/default.conf` file and I added these lines:
```plaintext ```plaintext

View file

@ -1,6 +1,6 @@
--- ---
title: 'Welcome' title: "Welcome"
description: 'Initial commit!' description: "Initial commit!"
date: 2024-03-18T14:16:19+07:00 date: 2024-03-18T14:16:19+07:00
draft: false draft: false
tags: tags:
@ -15,7 +15,6 @@ Long story short, I decide to take an Information Technology major in my local u
I never really write anything in my 7 years journey and I feel kinda regretted it. So I decide to create this blog, where I can write anything I found interesting. Maybe it's a coding-related thing, technology heads up, or maybe just my random thought. Feel free read and share! I never really write anything in my 7 years journey and I feel kinda regretted it. So I decide to create this blog, where I can write anything I found interesting. Maybe it's a coding-related thing, technology heads up, or maybe just my random thought. Feel free read and share!
# Bonus # Bonus
This is a cat from my work, maybe someday she can be a good software tester to find ~~mice~~ bugs from my code. This is a cat from my work, maybe someday she can be a good software tester to find ~~mice~~ bugs from my code.

View file

@ -1,7 +1,7 @@
--- ---
import "../styles/global.css"; import "@/styles/global.css";
import Footer from "../components/Footer.astro"; import Footer from "@/components/Footer.astro";
import Header from "../components/Header.astro"; import Header from "@/components/Header.astro";
interface Props { interface Props {
title: string; title: string;

View file

@ -1,6 +1,6 @@
--- ---
import Welcome from "../components/Welcome.astro"; import Welcome from "@/components/Welcome.astro";
import Layout from "../layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build // Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. // Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.

View file

@ -1,7 +1,7 @@
--- ---
import { getCollection, render, type CollectionEntry, type RenderResult } from "astro:content"; import { getCollection, render, type CollectionEntry, type RenderResult } from "astro:content";
import Layout from "../../layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import BlogPost from "../../components/BlogPost.astro"; import BlogPost from "@/components/BlogPost.astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection("blog"); const posts = await getCollection("blog");

View file

@ -1,7 +1,7 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import Layout from "../../layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
import BlogPostCard from "../../components/BlogPostCard.astro"; import BlogPostCard from "@/components/BlogPostCard.astro";
const posts = await getCollection("blog"); const posts = await getCollection("blog");
posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); posts.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());

View file

@ -1,7 +1,7 @@
--- ---
import { getCollection, type CollectionEntry } from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
import BlogPostCard from "../../components/BlogPostCard.astro"; import BlogPostCard from "@/components/BlogPostCard.astro";
import Layout from "../../layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection("blog"); const posts = await getCollection("blog");

View file

@ -1,6 +1,6 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import Layout from "../../layouts/Layout.astro"; import Layout from "@/layouts/Layout.astro";
const posts = await getCollection("blog"); const posts = await getCollection("blog");
const tagsMap = new Map<string, number>(); const tagsMap = new Map<string, number>();

View file

@ -1,11 +1,11 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap'); @import url("https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap");
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap'); @import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap");
@import "tailwindcss"; @import "tailwindcss";
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@theme { @theme {
--font-sans: 'Noto Sans', sans-serif; --font-sans: "Noto Sans", sans-serif;
--font-mono: 'JetBrains Mono', monospace; --font-mono: "JetBrains Mono", monospace;
} }
@layer base { @layer base {

View file

@ -3,6 +3,10 @@
"include": [".astro/types.d.ts", "**/*"], "include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"], "exclude": ["dist"],
"compilerOptions": { "compilerOptions": {
"allowJs": true "allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
} }
} }