diff --git a/package-lock.json b/package-lock.json index f50f791..28c3a0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "@tailwindcss/vite": "^4.1.18", "astro": "^5.16.10", "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19" } }, "node_modules/@astrojs/compiler": { @@ -1748,6 +1751,19 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, "node_modules/@tailwindcss/vite": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", @@ -3038,7 +3054,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3069,7 +3084,6 @@ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -4410,6 +4424,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -4927,7 +4955,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -5313,6 +5342,13 @@ } } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index 9eedd60..d124c02 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,8 @@ "@tailwindcss/vite": "^4.1.18", "astro": "^5.16.10", "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19" } } diff --git a/public/images/choose-sdk.webp b/public/images/choose-sdk.webp new file mode 100644 index 0000000..1382ce3 Binary files /dev/null and b/public/images/choose-sdk.webp differ diff --git a/public/images/flutter_dash.png b/public/images/flutter_dash.png new file mode 100644 index 0000000..6e952fe Binary files /dev/null and b/public/images/flutter_dash.png differ diff --git a/public/images/hdajackretask-remap-node.png b/public/images/hdajackretask-remap-node.png new file mode 100644 index 0000000..d5e5498 Binary files /dev/null and b/public/images/hdajackretask-remap-node.png differ diff --git a/public/images/no-devices.webp b/public/images/no-devices.webp new file mode 100644 index 0000000..1f3aa30 Binary files /dev/null and b/public/images/no-devices.webp differ diff --git a/public/images/og-flutter-clean-arch.png b/public/images/og-flutter-clean-arch.png new file mode 100644 index 0000000..dfe5742 Binary files /dev/null and b/public/images/og-flutter-clean-arch.png differ diff --git a/public/images/piko-1.webp b/public/images/piko-1.webp new file mode 100644 index 0000000..4acb83d Binary files /dev/null and b/public/images/piko-1.webp differ diff --git a/public/images/piko-2.webp b/public/images/piko-2.webp new file mode 100644 index 0000000..6728d64 Binary files /dev/null and b/public/images/piko-2.webp differ diff --git a/public/images/piko-3.webp b/public/images/piko-3.webp new file mode 100644 index 0000000..9a62ff8 Binary files /dev/null and b/public/images/piko-3.webp differ diff --git a/public/images/profile.jpg b/public/images/profile.jpg new file mode 100644 index 0000000..8a7f992 Binary files /dev/null and b/public/images/profile.jpg differ diff --git a/public/images/project-structure.webp b/public/images/project-structure.webp new file mode 100644 index 0000000..b691b25 Binary files /dev/null and b/public/images/project-structure.webp differ diff --git a/public/images/social-linkedin.svg b/public/images/social-linkedin.svg new file mode 100644 index 0000000..300c816 --- /dev/null +++ b/public/images/social-linkedin.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/astro.svg b/src/assets/astro.svg deleted file mode 100644 index 8cf8fb0..0000000 --- a/src/assets/astro.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/assets/background.svg b/src/assets/background.svg deleted file mode 100644 index 4b2be0a..0000000 --- a/src/assets/background.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/assets/social-email.svg b/src/assets/social-email.svg new file mode 100644 index 0000000..17569b5 --- /dev/null +++ b/src/assets/social-email.svg @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/src/assets/social-facebook.svg b/src/assets/social-facebook.svg new file mode 100644 index 0000000..28a9555 --- /dev/null +++ b/src/assets/social-facebook.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/src/assets/social-github.svg b/src/assets/social-github.svg new file mode 100644 index 0000000..a23a6cf --- /dev/null +++ b/src/assets/social-github.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/src/assets/social-linkedin.svg b/src/assets/social-linkedin.svg new file mode 100644 index 0000000..a2641bf --- /dev/null +++ b/src/assets/social-linkedin.svg @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/social-x.svg b/src/assets/social-x.svg new file mode 100644 index 0000000..e68cb8a --- /dev/null +++ b/src/assets/social-x.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/components/BlogPost.astro b/src/components/BlogPost.astro new file mode 100644 index 0000000..a0b839c --- /dev/null +++ b/src/components/BlogPost.astro @@ -0,0 +1,51 @@ +--- +interface Props { + title: string; + description: string; + date: Date; + tags?: string[]; +} + +const { title, description, date, tags } = Astro.props; +--- + +
+
+

{title}

+

{description}

+
+ +
+
+ +
+ +
+ +
+ { + tags?.map((tag) => ( + + + #{tag} + + + )) + } +
+
diff --git a/src/components/BlogPostCard.astro b/src/components/BlogPostCard.astro new file mode 100644 index 0000000..1cedbc5 --- /dev/null +++ b/src/components/BlogPostCard.astro @@ -0,0 +1,34 @@ +--- +interface Props { + id: string; + date: Date; + title: string; + description: string; + tags?: string[]; +} + +const { id, date, title, description, tags } = Astro.props; +--- + + +
+
+ { + date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }) + } +
+

{title}

+

{description}

+
+ { + tags?.map((tag) => ( + #{tag} + )) + } +
+
+
diff --git a/src/components/Footer.astro b/src/components/Footer.astro new file mode 100644 index 0000000..608479b --- /dev/null +++ b/src/components/Footer.astro @@ -0,0 +1,5 @@ + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 0000000..0b7452f --- /dev/null +++ b/src/components/Header.astro @@ -0,0 +1,19 @@ +--- +import NavLink from "./NavLink.astro"; +--- + +
+ +
diff --git a/src/components/NavLink.astro b/src/components/NavLink.astro new file mode 100644 index 0000000..d5d41da --- /dev/null +++ b/src/components/NavLink.astro @@ -0,0 +1,16 @@ +--- +interface Props { + href: string; + ariaLabel?: string; +} + +const { href, ariaLabel } = Astro.props; +--- + + + + diff --git a/src/components/Welcome.astro b/src/components/Welcome.astro index 52e0333..3d8a242 100644 --- a/src/components/Welcome.astro +++ b/src/components/Welcome.astro @@ -1,210 +1,38 @@ --- -import astroLogo from '../assets/astro.svg'; -import background from '../assets/background.svg'; +import socialEmail from "../assets/social-email.svg"; +import socialFacebook from "../assets/social-facebook.svg"; +import socialGithub from "../assets/social-github.svg"; +import socialLinkedIn from "../assets/social-linkedin.svg"; +import socialX from "../assets/social-x.svg"; --- -
- -
-
- Astro Homepage -

- To get started, open the
src/pages
directory in your project. -

- -
-
- - - -

What's New in Astro 5.0?

-

- From content layers to server islands, click to learn more about the new features and - improvements in Astro 5.0 -

-
+
+ profile picture of Dhemas Nurjaya +

Dhemas Nurjaya

+

Passionate Software Engineer

+ +
- - 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. + +![hdajackretask remap nodes](/images/hdajackretask-remap-node.png) + +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. + +![no devices showing](/images/no-devices.webp) + +All I need to do is open `File > Project Structure...` or `Ctrl + Alt + Shift + S`. + +![project structure window](/images/project-structure.webp) + +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. + +![choose android sdk](/images/choose-sdk.webp) + +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 +--- + +![flutter-clean-architecture](/images/flutter_dash.png) + +# 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 get props => [message, cause]; +} + +class ServerFailure extends Failure { + const ServerFailure({ + required super.message, + super.cause, + }); +} + +// lib/core/error/unknown_failure.dart +class UnknownFailure extends Failure { + const UnknownFailure({ + required super.message, + super.cause, + }); +} +``` + +Then we'll add some custom exceptions to handle different exceptions that might happen in our application. + +```dart +// lib/core/error/exceptions.dart + +/// Exception class for server error +/// Generally, this exception is thrown when the server returns an error response +class ServerException implements Exception { + const ServerException(this.message); + + final String message; +} + +/// Exception class for unauthorized client error +/// this exception is thrown when the client is not authorized +/// to access the resource (server returns 401) +class UnauthorizedException implements Exception { + const UnauthorizedException(this.message); + + final String message; +} +``` +### 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. + +```dart +// lib/core/network/network.dart + +import 'dart:convert'; +import 'dart:io'; +import 'package:clean_architecture/core/error/exceptions.dart'; +import 'package:http/http.dart' as http; + +/// Network interface +abstract class Network { + /// Get data from uri + Future get( + Uri uri, { + Map? headers, + }); +} + +/// Network implementation +class NetworkImpl implements Network { + NetworkImpl(http.Client httpClient) : _httpClient = httpClient; + + final http.Client _httpClient; + + @override + Future get( + Uri uri, { + Map? headers, + }) async { + final response = await _httpClient.get( + uri, + headers: headers, + ); final stringResponse = utf8.decode(response.bodyBytes); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UnauthorizedException(stringResponse); + } + if (response.statusCode != HttpStatus.ok) { + throw ServerException(stringResponse); + } + return stringResponse; + } +} +``` + +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` 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 +// lib/core/presentation/theme/app_theme.dart + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +/// App light theme +ThemeData lightTheme = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF6F43C0), + ), useMaterial3: true, + fontFamily: GoogleFonts.dmSans().fontFamily, +); + +/// App dark theme +ThemeData darkTheme = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF6F43C0), + brightness: Brightness.dark, + ), useMaterial3: true, + fontFamily: GoogleFonts.dmSans().fontFamily, +); +``` + +```dart +// lib/core/presentation/theme/theme_mode_cubit.dart + +import 'package:clean_architecture/core/data/local/config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// Theme mode cubit for theme mode management +class ThemeModeCubit extends Cubit { + /// Default [ThemeMode] is [ThemeMode.system] + ThemeModeCubit({ + required this.themeModeConfig, + required this.initialThemeMode, + }) : super(initialThemeMode); + + /// Theme mode config + final Config themeModeConfig; + + /// Initial theme mode + final ThemeMode initialThemeMode; + + /// Set theme mode + void setThemeMode(ThemeMode themeMode) { + themeModeConfig.set(themeMode); + emit(themeMode); + }} +``` +### 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. +### 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). + +```dart +// lib/core/env.dart + +abstract class Env { + String get weatherApiHost; + String get weatherApiKey; +} + +class EnvImpl implements Env { + @override + String get weatherApiHost => const String.fromEnvironment('WEATHER_API_HOST'); + + @override + String get weatherApiKey => const String.fromEnvironment('WEATHER_API_KEY'); +} +``` + +--- +### 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`. +### 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. + +Let's create a model for [WeatherAPI](https://www.weatherapi.com/) current weather response. + +```dart +// lib/features/weather/data/models/current_weather_model.dart + +import 'package:clean_architecture/core/data/remote/models/weather_api_response_model.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'current_weather_model.g.dart'; + +@JsonSerializable() +class CurrentWeatherModel extends WeatherApiResponseModel { + @JsonKey(name: 'current') + final WeatherApiDataModel? data; + + CurrentWeatherModel({ + required this.data, + required super.location, + required super.error, + }); + + factory CurrentWeatherModel.fromJson(Map json) => + _$CurrentWeatherModelFromJson(json); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class WeatherApiDataModel { + final DateTime lastUpdated; + final double tempC; + final double feelslikeC; + final WeatherApiConditionModel condition; + final double windKph; + final String windDir; + final double precipMm; + final int humidity; + final int cloud; + final double visKm; + final double uv; + + const WeatherApiDataModel({ + required this.lastUpdated, + required this.tempC, + required this.feelslikeC, + required this.condition, + required this.windKph, + required this.windDir, + required this.precipMm, + required this.humidity, + required this.cloud, + required this.visKm, + required this.uv, + }); + + factory WeatherApiDataModel.fromJson(Map json) => + _$WeatherApiDataModelFromJson(json); +} + +@JsonSerializable() +class WeatherApiConditionModel { + final String text; + final String icon; + + const WeatherApiConditionModel({ + required this.text, + required this.icon, + }); + + factory WeatherApiConditionModel.fromJson(Map json) => + _$WeatherApiConditionModelFromJson(json); +} +``` + +The next part is create the `data_source`, which will be responsible to access data from both local and remote sources. For accessing [WeatherAPI](https://www.weatherapi.com/), `network` will be used. + +```dart +// lib/features/weather/data/data_sources/remote/weather_api_remote_data_source.dart + +import 'dart:convert'; + +import 'package:clean_architecture/core/env.dart'; +import 'package:clean_architecture/core/network/network.dart'; +import 'package:clean_architecture/features/weather/data/models/current_weather_model.dart'; + +abstract class WeatherApiRemoteDataSource { + Future getCurrentWeather(String city); +} + +class WeatherApiRemoteDataSourceImpl implements WeatherApiRemoteDataSource { + final Env env; + final Network network; + + WeatherApiRemoteDataSourceImpl({ + required this.env, + required this.network, + }); + @override + Future getCurrentWeather(String city) async { + final uri = Uri( + scheme: 'https', + host: env.weatherApiHost, + path: 'v1/current.json', + queryParameters: { + 'key': env.weatherApiKey, + 'q': city, + }, + ); + final response = await network.get(uri); + final jsonResponse = jsonDecode(response) as Map; + return CurrentWeatherModel.fromJson(jsonResponse); + } +} +``` +### Feature - Domain +`domain` stores `entities`, `use cases` and `abstract repository` classes, as they are the ‘domain’ or ‘subject’ area of an application. If you aren’t 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. + +```dart +// lib/features/weather/domain/entities/current_weather.dart + +import 'package:clean_architecture/features/weather/data/models/current_weather_model.dart'; + +class CurrentWeather { + final DateTime? lastUpdated; + final double? tempC; + final double? feelslikeC; + final double? windKph; + final String? windDir; + final double? precipMm; + final int? humidity; + final int? cloud; + final double? visKm; + final double? uv; + final String? conditionText; + final String? conditionIcon; + final String? locationName; + final String? locationRegion; + final String? locationCountry; + + const CurrentWeather({ + this.lastUpdated, + this.tempC, + this.feelslikeC, + this.windKph, + this.windDir, + this.precipMm, + this.humidity, + this.cloud, + this.visKm, + this.uv, + this.conditionText, + this.conditionIcon, + this.locationName, + this.locationRegion, + this.locationCountry, + }); + + factory CurrentWeather.fromModel(CurrentWeatherModel model) => CurrentWeather( + lastUpdated: model.data?.lastUpdated, + tempC: model.data?.tempC, + feelslikeC: model.data?.feelslikeC, + windKph: model.data?.windKph, + windDir: model.data?.windDir, + precipMm: model.data?.precipMm, + humidity: model.data?.humidity, + cloud: model.data?.cloud, + visKm: model.data?.visKm, + uv: model.data?.uv, + conditionText: model.data?.condition.text, + conditionIcon: model.data?.condition.icon, + locationName: model.location?.name, + locationRegion: model.location?.region, + locationCountry: model.location?.country, + ); +} +``` + +Then create an abstract class for [WeatherAPI](https://www.weatherapi.com) repository. + +```dart +// lib/features/weather/domain/repositories/weather_api_repository.dart + +import 'package:clean_architecture/core/error/failures.dart'; +import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart'; +import 'package:fpdart/fpdart.dart'; + +abstract class WeatherApiRepository { + Future> getCurrentWeather(String city); +} +``` + +Things to keep in mind: `data source` returns `model`, `repository` uses one or more `data source` and gathers data from them, process it and returns `entity`. With this pattern, you can create an `entity` that contains data from several sources. + +Next we will create a `use case` for getting current weather data using the repository above. + +```dart +// lib/features/weather/domain/use_cases/get_current_weather.dart + +import 'package:clean_architecture/core/domain/use_case.dart'; +import 'package:clean_architecture/core/error/failures.dart'; +import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart'; +import 'package:clean_architecture/features/weather/domain/repositories/weather_api_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; + +class GetCurrentWeather + extends UseCase { + final WeatherApiRepository weatherApiRepository; + + GetCurrentWeather({required this.weatherApiRepository}); + + @override + Future> call( + GetCurrentWeatherParams params, + ) async { + return weatherApiRepository.getCurrentWeather(params.city); + } +} + +class GetCurrentWeatherParams extends Equatable { + final String city; + + const GetCurrentWeatherParams({required this.city}); + + @override + List get props => [city]; +} +``` + +We almost finished the `data` and `domain` for weather feature. Last thing is to create an implementation of `weather_api_repository` in the `data` layer. + +```dart +// lib/features/weather/data/repositories/weather_api_repository_impl.dart + +import 'package:clean_architecture/core/error/failures.dart'; +import 'package:clean_architecture/features/weather/data/data_sources/remote/weather_api_remote_data_source.dart'; +import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart'; +import 'package:clean_architecture/features/weather/domain/repositories/weather_api_repository.dart'; +import 'package:fpdart/fpdart.dart'; + +class WeatherApiRepositoryImpl implements WeatherApiRepository { + final WeatherApiRemoteDataSource weatherApiRemoteSource; + + WeatherApiRepositoryImpl({required this.weatherApiRemoteSource}); + + @override + Future> getCurrentWeather(String city) async { + try { + final result = await weatherApiRemoteSource.getCurrentWeather(city); + + if (result.error != null) { + return left(ServerFailure(message: result.error!.message)); + } + + final entity = CurrentWeather.fromModel(result); + return right(entity); + } on Exception catch (e) { + return left(ServerFailure(message: e.toString(), cause: e)); + } + } +} +``` +### 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. + +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. + +When creating a page/UI, keep in mind that **it should be dumb**. Means that it should not contain any logic. The logic should be handled in the `bloc`. Generally, a bloc is composed of **state**, **event**, and the **bloc** itself. The **state** will be used to manage the state of the page and the **event** will be used to communicate with the bloc to update the state. Let's create the `bloc` for the `current_weather` page. + +```dart +// lib/features/weather/presentation/bloc/current_weather_states.dart + +part of 'current_weather_bloc.dart'; + +abstract class CurrentWeatherState extends Equatable { + const CurrentWeatherState(); +} + +class CurrentWeatherInitialState extends CurrentWeatherState { + const CurrentWeatherInitialState(); + + @override + List get props => []; +} + +class CurrentWeatherLoadingState extends CurrentWeatherState { + const CurrentWeatherLoadingState(); + + @override + List get props => []; +} + +class CurrentWeatherLoadedState extends CurrentWeatherState { + final CurrentWeather currentWeather; + + const CurrentWeatherLoadedState({required this.currentWeather}); + + @override + List get props => [currentWeather]; +} + +class CurrentWeatherErrorState extends CurrentWeatherState + implements ErrorState { + @override + final String message; + + @override + final Exception? cause; + + const CurrentWeatherErrorState({required this.message, this.cause}); + + @override + List get props => [message, cause]; +} +``` + +```dart +// lib/features/weather/presentation/bloc/current_weather_events.dart + +part of 'current_weather_bloc.dart'; + +abstract class CurrentWeatherEvent extends Equatable { + const CurrentWeatherEvent(); +} + +class GetCurrentWeatherEvent extends CurrentWeatherEvent { + final String city; + + const GetCurrentWeatherEvent({required this.city}); + + @override + List get props => [city]; +} +``` + +```dart +// lib/features/weather/presentation/bloc/current_weather_bloc.dart + +import 'package:clean_architecture/core/presentation/bloc/error_state.dart'; +import 'package:clean_architecture/features/weather/domain/entities/current_weather.dart'; +import 'package:clean_architecture/features/weather/domain/use_cases/get_current_weather.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'current_weather_events.dart'; +part 'current_weather_states.dart'; + +class CurrentWeatherBloc + extends Bloc { + final GetCurrentWeather getCurrentWeather; + + CurrentWeatherBloc({ + required this.getCurrentWeather, + }) : super(const CurrentWeatherInitialState()) { + on(_onGetCurrentWeatherEvent); + } + + Future _onGetCurrentWeatherEvent( + GetCurrentWeatherEvent event, + Emitter emit, + ) async { + emit(const CurrentWeatherLoadingState()); + + final result = await getCurrentWeather( + GetCurrentWeatherParams(city: event.city), + ); + + result.fold( + (l) => emit(CurrentWeatherErrorState(message: l.message)), + (r) => emit(CurrentWeatherLoadedState(currentWeather: r)), + ); + } +} +``` + +We are missing the `ErrorState` class, let’s create it in the core so all error states can be inherited from it and makes all error uniform across the application. + +```dart +// lib/core/presentation/bloc/error_state.dart + +abstract class ErrorState { + final String message; + final Exception? cause; + + const ErrorState({ + required this.message, + this.cause, + }); +} +``` + +That's it. Now we have the `bloc` for the `current_weather` page. The code itself is quite self-explanatory. When the `CurrentWeatherBloc` receives a `GetCurrentWeatherEvent` event, it will emit a `CurrentWeatherLoadingState` and then a `CurrentWeatherLoadedState` or `CurrentWeatherErrorState` depending on the result of the `GetCurrentWeather` use case. + +The next part is to create weather page in `presentation` directory. + +```dart +// lib/features/weather/presentation/current_weather_page.dart + +import 'package:auto_route/auto_route.dart'; +import 'package:clean_architecture/core/router/app_router.gr.dart'; +import 'package:clean_architecture/features/weather/presentation/bloc/current_weather_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +@RoutePage() +class CurrentWeatherPage extends StatefulWidget { + const CurrentWeatherPage({super.key}); + + @override + State createState() => _CurrentWeatherPageState(); +} + +class _CurrentWeatherPageState extends State { + final _cityTextCtl = TextEditingController(); + final _cityTextFocus = FocusNode(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Current Weather'), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + context.router.push(const AppSettingsRoute()); + }, + ), + ], + ), + body: BlocBuilder( + builder: (context, state) { + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + TextField( + controller: _cityTextCtl, + focusNode: _cityTextFocus, + decoration: const InputDecoration( + hintText: 'City', + ), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: () { + context.read().add( + GetCurrentWeatherEvent( + city: _cityTextCtl.text, + ), + ); + }, + child: const Text('Get Weather'), + ), + const SizedBox(height: 16), + _buildWeather(state), + ], + ); + }, + ), + ); + } + + Widget _buildWeather(CurrentWeatherState state) { + if (state is CurrentWeatherLoadedState) { + _cityTextFocus.unfocus(); + + final weatherIconUrl = + 'https:${state.currentWeather.conditionIcon ?? '//placehold.co/64x64/png'}'; + + return Column( + children: [ + Image.network(weatherIconUrl), + Text( + state.currentWeather.conditionText ?? '-', + style: Theme.of(context).textTheme.headlineSmall, + ), + Text( + '${state.currentWeather.locationName}, ${state.currentWeather.locationRegion}'), + Text('${state.currentWeather.locationCountry}'), + const SizedBox(height: 16), + GridView.count( + crossAxisCount: 3, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + _buildDataCard( + 'Temp (C)', + '${state.currentWeather.tempC ?? '-'}', + ), + _buildDataCard( + 'Feels Like (C)', + '${state.currentWeather.feelslikeC ?? '-'}', + ), + _buildDataCard( + 'Wind (km/h)', + '${state.currentWeather.windKph ?? '-'}', + ), + _buildDataCard( + 'Wind Dir', + state.currentWeather.windDir, + ), + _buildDataCard( + 'Precip (mm)', + '${state.currentWeather.precipMm ?? '-'}', + ), + _buildDataCard( + 'Humidity (%)', + '${state.currentWeather.humidity ?? '-'}', + ), + _buildDataCard( + 'Cloud (%)', + '${state.currentWeather.cloud ?? '-'}', + ), + _buildDataCard( + 'Vis (km)', + '${state.currentWeather.visKm ?? '-'}', + ), + _buildDataCard( + 'UV', + '${state.currentWeather.uv ?? '-'}', + ), + ], + ), + const SizedBox(height: 16), + Text( + 'Last Updated: ${state.currentWeather.lastUpdated}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ); + } + if (state is CurrentWeatherLoadingState) { + return const Center(child: CircularProgressIndicator()); + } + if (state is CurrentWeatherErrorState) { + return Text(state.message); + } + return const SizedBox(); + } + Widget _buildDataCard(String header, String? content) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(header, textAlign: TextAlign.center), + Text( + content ?? '-', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineLarge, + ), + ], + ), + ); + } +} +``` + +and we also need a page to change application configurations (for now we only have a theme mode config). + +```dart +// lib/features/app_settings/presentation/app_settings_page.dart + +import 'package:auto_route/auto_route.dart'; +import 'package:clean_architecture/core/presentation/theme/theme_mode_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +@RoutePage() +class AppSettingsPage extends StatefulWidget { + const AppSettingsPage({super.key}); + + @override + State createState() => _AppSettingsPageState(); +} + +class _AppSettingsPageState extends State { + @override + Widget build(BuildContext context) { + final themeSetting = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('App Theme'), + DropdownButton( + items: const [ + DropdownMenuItem( + value: ThemeMode.system, + child: Text('System'), + ), DropdownMenuItem( + value: ThemeMode.light, + child: Text('Light'), + ), DropdownMenuItem( + value: ThemeMode.dark, + child: Text('Dark'), + ), + ], + value: context.watch().state, + onChanged: (value) { + context.read().setThemeMode(value!); + }, + ), + ], + ); + return Scaffold( + appBar: AppBar( + title: const Text('App Settings'), + ), + body: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16), + children: [ + themeSetting, + ], + ), + ); + } +} +``` + +As for the routing I mentioned in the previously, we will create an `app_router.dart` file in the `core/router` directory. + +```dart +// lib/core/router/app_router.dart + +import 'package:auto_route/auto_route.dart'; +import 'package:clean_architecture/core/router/app_router.gr.dart'; + +@AutoRouterConfig() +class AppRouter extends RootStackRouter { + @override + List get routes => [ + AutoRoute( + page: CurrentWeatherRoute.page, + initial: true, + ), + AutoRoute(page: AppSettingsRoute.page), + ]; + } +``` + +--- +## 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. + +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. + +```dart +// lib/injection_container.dart + +import 'package:clean_architecture/core/data/local/config.dart'; +import 'package:clean_architecture/core/data/local/theme_mode_config.dart'; +import 'package:clean_architecture/core/env.dart'; +import 'package:clean_architecture/core/network/network.dart'; +import 'package:clean_architecture/core/presentation/theme/theme_mode_cubit.dart'; +import 'package:clean_architecture/features/weather/data/data_sources/remote/weather_api_remote_data_source.dart'; +import 'package:clean_architecture/features/weather/data/repositories/weather_api_repository_impl.dart'; +import 'package:clean_architecture/features/weather/domain/repositories/weather_api_repository.dart'; +import 'package:clean_architecture/features/weather/domain/use_cases/get_current_weather.dart'; +import 'package:clean_architecture/features/weather/presentation/bloc/current_weather_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'package:http/http.dart' as http; + +final getIt = GetIt.instance; + +void setup() { + // env + getIt.registerSingleton(EnvImpl()); + + // network + getIt.registerLazySingleton(() => http.Client()); + getIt.registerLazySingleton(() => NetworkImpl(getIt())); + + // shared preferences + getIt.registerSingletonAsync( + () async { + final prefs = await SharedPreferences.getInstance(); + return prefs; + }, + ); + + // configs + getIt.registerSingletonWithDependencies>( + () => ThemeModeConfig(sharedPreferences: getIt()), + dependsOn: [SharedPreferences], + ); + + // data sources + getIt.registerLazySingleton( + () => WeatherApiRemoteDataSourceImpl( + env: getIt(), + network: getIt(), + ), + ); + + // repositories + getIt.registerLazySingleton( + () => WeatherApiRepositoryImpl( + weatherApiRemoteSource: getIt(), + ), + ); + + // use cases + getIt.registerLazySingleton( + () => GetCurrentWeather( + weatherApiRepository: getIt(), + ), + ); + + // blocs + getIt.registerSingletonAsync( + () async { + final initialThemeMode = await getIt>().get(); + return ThemeModeCubit( + themeModeConfig: getIt(), + initialThemeMode: initialThemeMode, + ); + }, + dependsOn: [SharedPreferences, Config], + ); + getIt.registerFactory( + () => CurrentWeatherBloc( + getCurrentWeather: getIt(), + ), + ); + + // others +} +``` + +To monitor events and states in our bloc, also add a `bloc observer` class. + +```dart +// lib/core/presentation/bloc/app_bloc_observer.dart + +import 'dart:developer' as dev; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AppBlocObserver extends BlocObserver { + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + dev.log("[bloc_error] $bloc\nerror: $error\nstacktrace: $stackTrace"); + super.onError(bloc, error, stackTrace); + } + @override + void onChange(BlocBase bloc, Change change) { + dev.log( + "[${bloc.runtimeType}] ${DateTime.now().toIso8601String()}\nFrom: ${change.currentState}\nNext: ${change.nextState}"); + super.onChange(bloc, change); + } +} +``` + +Now we have all the required components for our app, lets look at the `main.dart` file. + +```dart +// lib/main.dart + +import 'package:clean_architecture/core/presentation/bloc/app_bloc_observer.dart'; +import 'package:clean_architecture/core/presentation/theme/app_theme.dart'; +import 'package:clean_architecture/core/presentation/theme/theme_mode_cubit.dart'; +import 'package:clean_architecture/core/router/app_router.dart'; +import 'package:clean_architecture/features/weather/presentation/bloc/current_weather_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'injection_container.dart' as ic; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // dependency injection setup + ic.setup(); + await ic.getIt.allReady(); + + // register bloc observer + Bloc.observer = AppBlocObserver(); + + runApp(WeatherApp()); +} + +class WeatherApp extends StatelessWidget { + WeatherApp({super.key}); + + final _appRouter = AppRouter(); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => ic.getIt(), + ), + BlocProvider( + create: (context) => ic.getIt(), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return MaterialApp.router( + debugShowCheckedModeBanner: false, + title: 'Weather App', + theme: lightTheme, + darkTheme: darkTheme, + themeMode: state, + routerConfig: _appRouter.config(), + ); + }, + ), + ); + } +} +``` + +--- +## Testing +. + +--- + +All the codes in this set of articles are available on [GitHub](https://github.com/dhemasnurjaya/flutter-clean-architecture), and will be updated regularly because I use them too as my project starter. \ No newline at end of file diff --git a/src/content/blog/kuwot-flutter-daily-quote-app.md b/src/content/blog/kuwot-flutter-daily-quote-app.md new file mode 100644 index 0000000..96f4537 --- /dev/null +++ b/src/content/blog/kuwot-flutter-daily-quote-app.md @@ -0,0 +1,46 @@ +--- +title: 'Kuwot' +description: 'Flutter Daily Quote App' +date: 2025-02-20T15:26:04+07:00 +draft: true +tags: + - programming + - flutter +--- + +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 +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 +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) +- Scraping quotes website +- Find dataset that already compiled somewhere in [Github](https://github.com) + +After many consideration, I'd like to use this [Quotes 500k](https://github.com/ShivaliGoel/Quotes-500K) as my quote data source. Originally, that quote data was used for a research paper and made by scraping several quote websites. + +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 +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. + +# Reshaping dataset +The data I got from [Quotes 500k](https://github.com/ShivaliGoel/Quotes-500K) is looking like this: + +| Quote | Author | Tags | +| ----- | ------ | ---- | +| 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. + +```python + +``` + +# UI Design +I want to make it as simple as possible while focusing on the functionality. \ No newline at end of file diff --git a/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md b/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md new file mode 100644 index 0000000..ed721cb --- /dev/null +++ b/src/content/blog/remap-copilot-key-infinix-air-pro-plus.md @@ -0,0 +1,44 @@ +--- +title: 'Remap Infinix Air Pro+ Copilot Key in Linux' +description: 'Re-using Copilot key for something else more useful' +date: 2025-03-01T22:41:32+07:00 +draft: false +tags: + - linux + - laptop + - infinix + - tweak +--- + +> Since I'm moving to Linux, my Copilot key becomes useless. + +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 +- A laptop (I'm using Infinix Air Pro+) with a working keyboard. +- [keyd](https://github.com/rvaiya/keyd) + +# 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. +- Press the Copilot key and read the output. In my laptop, it print out this: + +```bash +AT Translated Set 2 keyboard 0001:0001:70533846 leftmeta down +AT Translated Set 2 keyboard 0001:0001:70533846 leftshift down +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`. + +# `keyd` for the rescue +Edit `/etc/keyd/default.conf` file and I added these lines: + +```plaintext +[ids] +0001:0001:70533846 + +[main] +f23 = f13 +``` + +`ids` is my keyboad ID, and the last line tells to remap `f23` key to `f13` (which is doesn't exist physically). Then reload `keyd` with `sudo keyd reload`. Now I can bind my Copilot key to something else. I'm using it for [yakuake](https://github.com/KDE/yakuake) show/hide toggle. diff --git a/src/content/blog/welcome.md b/src/content/blog/welcome.md new file mode 100644 index 0000000..38dcc9f --- /dev/null +++ b/src/content/blog/welcome.md @@ -0,0 +1,23 @@ +--- +title: 'Welcome' +description: 'Initial commit!' +date: 2024-03-18T14:16:19+07:00 +draft: false +tags: + - misc +--- + +# Introduction + +Hi there! I'm Dhemas, an Indonesian software engineer. I grew up in a small town called Salatiga, Central Java, Indonesia. I really like to tinker with computer since I was a child. Playing games (SkiFree to Age of Empires II) in my dad's office computer, then learn some office softwares, buying a floppy disk to save my diary (encrypted with Webdings font!), buying PC magazines, leads to learn how to assemble my own computer and do an OS install. That was a good time! + +Long story short, I decide to take an Information Technology major in my local university then got accepted in my first company right after graduated, working as a mobile application developer. Since then, it is already 7 years I work as a software developer and now I'm a part of a software house startup company. + +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 + +This is a cat from my work, maybe someday she can be a good software tester to find ~~mice~~ bugs from my code. + +![Piko 1](/images/piko-1.webp) ![Piko 2](/images/piko-2.webp) ![Piko 3](/images/piko-3.webp) \ No newline at end of file diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index e455c61..0f00213 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,22 +1,32 @@ - - - - - - - - Astro Basics - - - - - +--- +import "../styles/global.css"; +import Footer from "../components/Footer.astro"; +import Header from "../components/Header.astro"; - +interface Props { + title: string; + description: string; + contentClass?: string; +} + +const { title, description, contentClass = "" } = Astro.props; +--- + + + + + + + + {title} + + + + +
+
+ +
+