### 長話短說
先去 Code 睇一睇先:
```js
// 取得現在月份的第1日
const startOfMonth = moment().startOf('month').format('YYYY-MM-DD hh:mm:ss');
// 取得現在月份的最後一日
const endOfMonth = moment().endOf('month').format('YYYY-MM-DD hh:mm:ss');
```
### Regexp check timezone string
可以用以下的 regex 來檢查傳入的 string 是否為 timezone 時間字串。
只用於檢查基本的 format,不會細緻檢查內容問題。
```js
// 用來 check 基本時間 + timezone 資料
const regex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(|\\.[0-9]+)(Z|[\\+\\-][0-9]{2}:[0-9]{2})$/;
```
### Router 經常壞
話說現在買的 Router 都經常壞,好多時買返嚟一兩年就開始越嚟越慢,初都仲可以 Reset 一下就快返,過多一排就 Reset 完都好快越嚟越慢,甚至有連唔到線嘅情況。呢個問題都持續咗好幾年,唔知幾時開始已經習慣咗兩年換一隻。為咗長遠解決呢個情況,所以決定買隻 Mini PC 返嚟起一隻自己嘅 Router。
### 預備工作
要達成今次嘅目的,要準備以下工具:
- Mini PC一部
- Ubuntu 24.04LTS Desktop
- 普通家用 Router (用來發放Wi-Fi訊號)
#### Mini PC

今次選用嘅 Mini PC 係 Lenovo 出嘅異能者 (ERAZER) 系列!上網搵咗好耐,就算最細機箱嘅 ITX 都仲係好大,同埋就算用 Desktop CPU 都比較食電,今次選擇嘅 Mini PC 係用 Intel N100 CPU,配埋 16GB RAM 加上 256GB SSD,拎嚟裝個Linux來做Router絕對綽綽有餘,所以除此之外仲會拎佢嚟做埋電視盒子!實行一物多用!最緊要嘅係呢隻機竟然自帶兩個千兆網口,佢嘅設計簡直係天生就係Router材料,加上本身自己仲有Wi-Fi,雖然係Wi-Fi5,不過一般辦公睇片已經非常足夠。Mini PC 的技術配置如下:
- Intel N100 CPU
- 16GB RAM
- 256GB SSD
- HDMI端口×3
- USB 3.0端口×3
- 千兆網口×2
- Audio輸出×1
- 12V電源輸入
- 預裝左 Window 11 (簡體版)
#### Ubuntu 24.04LTS Desktop
今次點解要用 Desktop 版,既然要做埋電視盒子,當然唔可以淨係用 Command Line 介面,而且 Ubuntu Desktop 版都非常易用,裝個瀏覽器就已經可以睇到好多相睇嘅嘢。
既然部機都預裝咗 Windows 11,點解唔用 Windows 11? 因為 Windows 其實好大食,Router 嘅工作好簡單,唔需要裝晒成個 Windows,所以今次裝 Ubuntu 都係裝最 Minimal,務求用最少嘅電力消耗,就可以達到我哋嘅目的。
#### 普通家用 Router
雖然 Mini PC 自帶 Wi-Fi,但係要令 Wi-Fi 發放訊號都要有驅動程式配合,這部份比較麻煩,而且做出嚟效果唔係咁好,所以發放 Wi-Fi 訊號都係留返俾傳統家用 Router 做返。呢度只需要一隻普通能夠發放 Wi-Fi 信號嘅家用 Router 就可以,要注意嘅就只係隻 Router 需要支援 AP 模式(即是有線中繼模式)。
有線中繼模式是指路由器不再需要網路地址轉換(network address translation) 和 DHCP 的功能,只需要發放 Wi-Fi 訊號和做交換器(switch)的功能就可以。
### 開始工作
首先去 Ubuntu 的官方網站 Download Ubuntu 24.04LTS Desktop 版本的 ISO 檔案下來,然後用 Rufus 等等可以將 ISO 檔案寫入到 USB 手指成為可開機 (bootable) 的安裝媒體就可以了。
喺呢度先假設螢光幕前嘅,你已經有一定嘅Linux經驗,所以詳細嘅安裝過程就喺度跳過,直接進入定的部份。
#### 硬件連線
首先我哋要將所有硬件連接好,由網絡供應商的光纖轉譯器輸出的網線插入 Mini PC 的網線口 1,再將 Mini PC 網線口 2 連接到你的家用 Router。連接如以下圖片:

#### 檢查 Network Interfaces
當所有的網線都連接好後,開機進入你的 Mini PC 然後按 `CTRL` + `ALT` + `T` 打開 Terminal,然後輸入以下指令:
```sh
# 用來列出本機的所以 Network Interface (或者都可以叫 Lan Port)
$ ip addr
```
大約會看到以下的圖片:

上面圖片顯示左呢部機入面的三個 Network Interface,其實是有 4 個的,還有一個是 Wifi 的 Interface (wlp2s0)。三個 Network Interface 的解讀如下:
- lo (Loopback Interface,即是 localhost 或者 127.0.0.1,是指本身自己)
- enp1s0 (即是乙太網路端口 1,連接著外網)
- enp3s0 (即是乙太網路端口 2,連接著內網)
#### 設定每一個 Network Interface 的地址
每一個 Network Interface 都需要一個 IP 地址才能運作,所以我們需要先設定每一個 Network Interface 的取得地址方法。我們用以下指令修改一下設定檔:
```sh
# 修改 /etc/netplan/50-cloud-init.yaml
sudo vm /etc/netplan/50-cloud-init.yaml
```
變更為以下的內容:
```yaml
network:
version: 2
ethernets:
enp1s0:
dhcp4: true
enp3s0:
addresses: [192.168.5.1/24]
```
上面的 `enp1s0` 設定為使用 DHCP 取得 IP 地址,而 `enp3s0` 則是手動設定一個 IP 地址 `192.168.5.1` 和 Submask 為 `255.255.255.0` (即是前 24bits)。
完成設定後可以輸入下面指令立即套用設定:
```sh
# 套用 netplan 設定
$ sudo netplan apply
```
#### 啟用 Linux 的 IP Forward 功能
Linux 系統本身已經有IP地址轉換的功能,但是預設的情況下是關閉的,我們可以透過修改配置文件來啟用這個功能,方法如下:
```sh
# 修改 /etc/sysctl.conf
$ sudo vi /etc/sysctl.conf
```
然後找出下面這一句,把最開始的 `#` 號刪除:
```sh
net.ipv4.ip_forward=1
```
完成設定後可以輸入下面指令立即套用設定:
```sh
# 套用設定
$ sudo sysctl -p
```
#### 啟用 iptables 的 NAT 功能
要實現路由器功能,我們還需要使用 Linux 內建的工具 iptables 來幫我們把IP地址進行轉譯工作。可以輸入以下指令來實現:
```sh
# 把網段 192.168.5.0/24 傳出的數據包的 source (IP) address 都偽裝成為 enp1s0 的 IP 地址
sudo iptables -t nat -A POSTROUTING -s 192.168.5.0/24 -o enp1s0 -j MASQUERADE
```
上面的意思是指,修改 `nat` 表格,把所有 IP 來自 `192.168.5.0/24` 的數據包 (Packet) 都轉發到 `enp1s0` 這個 Network Interface 上。這樣內網的地址 (例如內網有一部 PC IP 地址是 `192.168.5.10/24` 時) 就能經由 Mini PC (Default Gateway) 進行數據包轉譯連接上 Internet 了。
### iptables 設定
既然要用 MiniPC 來做 Router 功能,都一定要設定最少限度的 Firewall 來防止有外來的人可以 SSH 入去你的 MiniPC,以下會列出一些基本會到用的指令 :
```sh
# 列出所有現有的 iptables rules
$ sudo iptables -vL --line-number
# 把進入本機的數據包全部掉棄
# 因為先前 (如果中間沒有 match 到任何一條 rules)
$ sudo iptables -P INPUT DROP
# 把推送的數據包全部掉棄 (如果中間沒有 match 到任何一條 rules)
$ sudo iptables -P FORWARD DROP
# 把送出的數據包全部批准 (如果中間沒有 match 到任何一條 rules)
$ sudo iptables -P OUTPUT ACCEPT
# 我們起一條 Chain (MY-FIREWALL) 用來裝住重覆使用的 rules
$ sudo iptables -N MY-FIREWALL
# 加入規則到 MY-FIREWALL, 把來自 enp1s0 (WAN) 介面進入的數據包 (如果是已經由內部連接的話), 就可以通過
$ sudo iptables -A MY-FIREWALL -i enp1s0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# 加入規則到 MY-FIREWALL, 把來自 enp3s0 (LAN) 介面進入的數據包設定為通過
$ sudo iptables -A MY-FIREWALL -i enp3s0 -j ACCEPT
# 加入規則到 MY-FIREWALL, 把來自 lo (Localhost) 介面進入的數據包設定為通過
$ sudo iptables -A MY-FIREWALL -i lo -j ACCEPT
# 把 MY-FIREWALL 套用到進入本機的數據包
$ sudo iptables -A INPUT -j MY-FIREWALL
# 把 MY-FIREWALL 套用到進入推送的數據包
$ sudo iptables -A FORWARD -j MY-FIREWALL
```
另外介紹多幾個關於 `iptables` 除錯 (debug) 相關的 command,可以幫助大家快速脫離新手村。
```sh
# 為 rule 加入 comment
$ sudo iptables -I INPUT -i enp1s0 -j DROP -m comment --comment "drop all incomming connnection from wan"
# 用來 log 低經過的 traffic
# log level 6 是 info 級別
# log prefix 就可以等你用 grep command 來 filter 走你想要看到的 log messages
$ sudo iptables -I INPUT -j LOG --log-level 6 --log-prefix "MY-CUSTOM-LOG: "
# 例如 log 低由 enp1s0 進入 tcp port 80 的 traffic
# 可以幫助你快速知道你所打的規則正確或者錯誤
$ sudo iptables -I INPUT -i enp1s0 -p tcp --dport 80 -j LOG --log-level 6 --log-prefix "MY-CUSTOM-LOG: "
# 另外你需要開多一個 terminal 來查看即時的 log 實況
$ tail -f /var/log/syslog
# 或者其他 version 的 linux 可能要用下面呢個 command
$ journalctl -f
```
#### 如果你的 MiniPC 有 Docker 運行
因為 Docker 句會使用到 `iptables` 來進行路由分隔和攔截,如果要配入 Docker 使用的話就需要進一些修改,以達到可以和 Docker 一起共存。
上面最後 `sudo iptables -A FORWARD -j MY-FIREWALL` 這一句不要運行,如果運行了可以用下面這句去清除 :
```sh
# 把 MY-FIREWALL 從推送中移除
$ sudo iptables -D FORWARD -j MY-FIREWALL
```
然後我們可以依照 Docker 的方法去設定 `iptables` :
```sh
# 建立一個叫 DOCKER-USER 的 Chain
$ sudo iptables -N DOCKER-USER
# 把 MY-FIREWALL 套用到進入 DOCKER-USER 的數據包
$ sudo iptables -A DOCKER-USER -j MY-FIREWALL
# 把 DOCKER-USER 套用到進入推送的數據包
$ sudo iptables -A FORWARD -j DOCKER-USER
# 加入規則到 MY-FIREWALL, 把來自 enp1s0 (WAN) 介面進入的數據包設定為掉棄
# 因為先前 `sudo iptables -A MY-FIREWALL -i enp1s0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT` 已經將已連線的數據包接入了
# 所以如果由 enp1s0 (WAN) 來的數據包到達這裡必需要掉棄, 否則接下來 FORWARD 的 docker rules 會把數據 route 到 docker container 裡面, 外來的 WAN IP 就能直接進入到 docker container 的 port
$ sudo iptables -A MY-FIREWALL -i enp1s0 -j DROP
```
詳細的說明可以參考官網解說 : [https://docs.docker.com/engine/network/packet-filtering-firewalls/](https://docs.docker.com/engine/network/packet-filtering-firewalls/)
這樣便完成了。
### iptables-persistent
經過上面的設定,你的網路已經基本上安全了。
不過 `iptables` 的設定不是永久的,當你 reboot 台電腦時所有 `iptables` 的設定都會清空,如果要永久儲存就必需要用 `iptables-persistent` 來實現。
```sh
# 安裝 iptables-persistent
$ sudo apt-get install iptables-persistent
# 如果要儲存現時的 iptables 規則, 可以這樣輸入
$ sudo netfilter-persistent save
# 如果要載入已儲存的 iptables 規則, 可以這樣輸入 (開機時會自動載入)
$ sudo netfilter-persistent reload
```
這樣就真的設定好 `iptables` 了。
### DHCP
通常家用 Router 都會自帶了 DHCP 的服務,而當你的 Router 使用 AP 模式時這個服務可能會隨之而關閉,所以你需要在你的 MiniPC 上起一個 DHCP 服務來派 IP 給連線到網絡的機器。
#### 安裝 `isc-dhcp-server`
今次我地會用套件 `isc-dhcp-server` 去完成。首先要安裝好先 :
```sh
# 安裝 isc-dhcp-server
sudo apt install isc-dhcp-server
```
#### 設定 `isc-dhcp-server`
裝好之後我地要先去 `/etc/dhcp/dhcpd.conf` 設定好我們想要的環境,例如 ip range, default dns 等等。
```sh
$ vi /etc/dhcp/dhcpd.conf
```
看看有沒有以下的設定:
```conf
# network domain name
option domain-name "myhome-lan";
# default dns ip address (8.8.8.8 & 8.8.4.4 are google dns service)
option domain-name-servers 8.8.8.8 8.8.4.4;
# default lease time
default-lease-time 600;
# max lease time
max-lease-time 7200;
# 如果你的 DHC, 是 network 上主要的 DHCP, 就要加入 authoritative
authoritative;
# 設定每一個 network 的屬性
subnet 192.168.5.0 netmask 255.255.255.0 {
# specify gateway
option routers 192.168.5.1;
# specify subnet mask
option subnet-mask 255.255.255.0;
# specify the range of lease IP address
range dynamic-bootp 192.168.5.50 192.168.5.200;
}
```
#### 設定 `isc-dhcp-server` 的生效 network interface
然後我們需要設定 DHCP 生效的 network interface :
```sh
// 修改 /etc/default/isc-dhcp-server
$ vi /etc/default/isc-dhcp-server
```
找出下面這句,然後修改為你想要生效的 network interface :
```sh
INTERFACESv4="enp3s0"
```
#### 啟動並查看 DHCP
當所有東西都設定好後,就可以啟動 DHCP 服務。
```sh
$ sudo systemctl start isc-dhcp-server
```
然後查看運行情況 :
```sh
$ sudo systemctl status isc-dhcp-server
```
如果想查看現在租用出去的 ip 地址,可以用以下兩個方法 :
```sh
# 精簡版
$ dhcp-lease-list
# 詳細版
$ cat /var/lib/dhcp/dhcpd.leases
```

### 完成
去到這裡你的 mini pc 已經完全設定為了一隻 router。在 intranet 內再加入一個設定為 AP 模設的 wifi router 設可以發放 wifi 訊號了。
### First
Remember the key settings `otherPortsAttributes`
### Setup
Open your vscode and go to File > Preference > Settings.

Then paste the word `otherPortsAttributes` into the filter in settings panel.

Press `Add Item` button, then select `onAutoForward` in item and set the value to `ignore`.

No more `Auto Forwarded` ports.
Finished ~
如果安裝時出現 network settings 但又不可能即時處理得到,其實是可以 bypass 的:
1. 按下 SHIFT + F10 叫出 cmd
2. 輸入 OOBE\BYPASSNRO
之後個安裝就會自已 restart,入返去後就可以 bypass network setting 同埋登入 microsoft account 兩個流程了
### 從 pubspec.yaml 中讀取 version 資料
用呢個 package https://pub.dev/packages/package_info_plus
version 個格式係 1.0.0+1
前面係 semantic versioning (Android 個 versionName),後面個 + 1 就係 versionCode (Android 個 versionCode)
所以個 versionCode 只可以愈來愈大,不可以向後退,如果唔係會變成 downgrade 個 app。
### DropdownMenu 防止使用者可以輸入文字
只要加入呢個 `requestFocusOnTap: false` 參數就可以了。係 window 入面個 cursor 仲會變埋 pointer 添。
```dart
DropdownMenu(
requestFocusOnTap: false,
...,
);
```
如果想要在 Widget 一開始時就執行想要的動作,最好就係 `initState` 時加入你要想的東西。
```dart
@override
void initState() {
super.initState();
// your action here
}
```
但後多時候我們是需要等到 widget tree 整個畫好晒一次先可以做後續動作。例如 setState 等,可以通過使用 `WidgetsBinding.instance.addPostFrameCallback` 來達成。
```dart
@override
void initState() {
super.initState();
// post callback (do after build widget tree)
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
// set state
setState(() {
_foo = 'bar';
});
});
}
```
咁樣就可以了。
### Dart 對於 JS 的 reduce
Dart 同樣有 reduce,不過 reduce 有少少唔同,最相似係用 fold。
```dart
// create list
final list = ['a', 'bb', 'ccc'];
// compute the sum of all length
list.fold(0, (t, e) => t + e.length); // result is 6
```
### Dart double ceil
double force 進位
```dart
print(1.99999.ceil()); // 2
print(2.0.ceil()); // 2
print(2.00001.ceil()); // 3
print((-1.99999).ceil()); // -1
print((-2.0).ceil()); // -2
print((-2.00001).ceil()); // -2
```
### Flutter container add border
幫 Container 加入 border
```dart
Container(
margin: const EdgeInsets.all(15.0),
padding: const EdgeInsets.all(3.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueAccent),
),
child: Text('My Awesome Border'),
)
```
### 設定一個 FocusNode 用來自動畫面中 Focus 的目標
先在 class 入面設定一個 `FocusNode`。
```dart
final _myField = FocusNode();
```
然後在 TextField 入面加入 focusNode 參數。
```dart
TextFormField(
focusNode: _myField,
...,
);
```
之後就可以通過使用 `_myField` 去選定現在的 focus 目標。
```dart
_myField.requestFocus();
```
得記用完之後要 `dispose` 返佢。
```dart
@override
void dispose() {
_myField.dispose();
}
```
### Flutter TextField 輸入完成後 focus 下一個 TextField
最普通情況,可以自動移到下一個 TextField:
`TextInputAction.next` 移到下一個 TextField。
`TextInputAction.done` 輸入完成,關上 Keyboard。
```dart
TextField(
decoration: InputDecoration(hintText: 'TextField A'),
textInputAction: TextInputAction.next, // Moves focus to next.
);
```
也可以通過 `onFieldSubmitted` 自訂你想要 Focus 的目標。
```dart
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
textInputAction: TextInputAction.next,
autofocus: true,
decoration: InputDecoration(labelText: "Input 1"),
onFieldSubmitted: (v){
FocusScope.of(context).requestFocus(focus);
},
),
TextFormField(
focusNode: focus,
decoration: InputDecoration(labelText: "Input 2"),
),
],
);
```
### Flutter Scrollbar 及 SingleChildScrollView 設定 controller
要令到 `Scrollbar` 及 `SingleChildScrollView` 可以運作正常,必需要設定好 controller 才行。設定的方法如下:
設先要係 class 入面整返個 `ScrollController` 先
```dart
final _pageScrollController = ScrollController();
```
之後係 `Scrollbar` 同埋 `SingleChildScrollView` 入面都設定返 controller。
```dart
Scrollbar(
controller: _pageScrollController,
child: SingleChildScrollView(
controller: _pageScrollController,
...,
),
);
```
可以用以下方法直接 scroll 去最頭或者最尾。
```dart
// scroll to top
_pageScrollController.jumpTo(0);
// scroll to end
_pageScrollController.jumpTo(_pageScrollController.position.maxScrollExtent);
```
最後就可以係任何地方使用 `jump()` 方法去 Scroll 去頁面不同地方。
```dart
_pageScrollController.jumpTo(0);
```