正在顯示「 2024 年 」的所有結果
11月 03, 2024 Dart Flutter


### 從 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,
  ...,
);
```

11月 03, 2024 Dart Flutter


如果想要在 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';
    });
  });
}
```

咁樣就可以了。
11月 02, 2024 Dart Flutter


### 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);
```
    
    


今日又係要講返 ExtJS 既野,話說 Webpack 已經流行左一大段時間,好多野都變得容易管理左好多。但係如果要將一個 Framework 搬去 Webpack 度用就實在係有啲困難,因為 Framework 通常都會用到好多好底層的功能。而呢啲功能本身既前題係運行個 Scope 有自己本身固定既 Context,運單啲講 Webpack 當 Bundle 完個 package 之後,入面所運行既 JS 係有自己個 Context,係同 Browser 入面 html embed 的 Javascript Context 唔一樣,會有可能引起 Webpack 打包後的 JS 運行唔到。

### 自訂 ExtJS Component

如果你要自訂一個 Component,最起碼都要設定佢最基本既行為,例如當個 Component 建立時需要係 runtime 拎一啲 condition 來決定佢畫出來個樣。或者在建立後修改一下 child component 的數值等等。呢個時候如果睇官方個 Guide 係會教你可以用 `initComponent()` 去改寫 Component 生成時的行為,當中有一個 statement 係 `this.callParent()`。佢係類似 Java 入面 `super()` 的 method,用來叫返 override 左既 super class 個 method。正常用係無問題既,但係如果當你係魔改配合 Webpack 使用的話,就會出 Error。

睇下呢段 Code:

```js
// 定義一個 component
Ext.define('MyApp.view.YourComponent', {
	
	// extend 'Ext.window.Window' 呢個 component
	extend: 'Ext.window.Window',
	
	// 原本個 title
	title: 'foo',
	
	// 自動顯示
	autoShow: true,
	
	// 用 initComponent 來改寫
	initComponent: function(...args) {
		
		// 生成 component
		this.callParent(...args);
		
		// 改下個 title
		this.setTitle('bar');
	},
});

// 生成 class
Ext.create('MyApp.view.YourComponent');
```

之後就會出 Error :

```js
Uncaught TypeError: Cannot read properties of null (reading '$owner') at constructor.callParent
```

### 用 beforerender event 

上網找左好耐好耐都找唔到有效既方法,後來用左 beforerender event 暫時頂住先,後來又改用 added event 來頂住,但係都只係治標不治本既方法。

使用 beforerender 來 workaround:

```js
// 定義一個 component
Ext.define('MyApp.view.YourComponent', {
	
	// extend 'Ext.window.Window' 呢個 component
	extend: 'Ext.window.Window',
	
	// 原本個 title
	title: 'foo',
	
	// 自動顯示
	autoShow: true,
	
	// 設定 listeners
	listeners: {
		
		// beforerender 係生命週期比較早的 event
		beforerender: view => {
			
			// 改下個 title
			view.setTitle('bar')
		},
	},
});

// 生成 class
Ext.create('MyApp.view.YourComponent');
```

### Strict Mode 問題

其實 this.callParent() 係會用到 arguments.callee 同埋 arguments.caller 來配合達成到 call 返 override 的 method。

所以在 Strict Mode 底下會有機會用唔到 this.callParent()...

### Workaround 方法

之後睇到有一篇大神講 ExtJS + Webpack 的使用方法,入面提到如果要用 this.callParent() 的話需要使用到 class prototype 去完成,我再測試一下法現成功了!

實現方法如下:

```js
// 定義一個 component
Ext.define('MyApp.view.YourComponent', {
	
	// extend 'Ext.window.Window' 呢個 component
	extend: 'Ext.window.Window',
	
	// 原本個 title
	title: 'foo',
	
	// 自動顯示
	autoShow: true,
	
	// 用 initComponent 來改寫
	initComponent: function(...args) {

		// 生成 component
		// this.callParent(...args); // 會出 Error

		// workaround 方法, 主要係 call 返 extend component prototype 個 method, 再放返現在 context 個 this 入去
		Ext.window.Window.prototype.initComponent.call(this, ...args);
		
		// 改下個 title
		this.setTitle('bar');
	},
});

// 生成 class
Ext.create('MyApp.view.YourComponent');
```

咁就可以成功啦 !
10月 06, 2024 Docker


Docker 點用就唔係度講了,可以去睇其他文章都得。

今日主要講下點可以係 docker compose 入面淨係 rebuild 一個 container,話說今日要 update docker compose 入面其中一個 image 既 version。雖然 rebuild 晒成個 docker compose 都其實係安全,但係為左安全起見,最好都係可以動最少既野係最安全,所以就好快咁找左一陣 solution。

可以睇下下面呢條:

https://stackoverflow.com/questions/31466428/how-to-restart-a-single-container-with-docker-compose

其實就係用

```sh
$ docker compose up -d --build workername
```

就咁可以解決到了,但係要注意如果有 dependance 的話,佢既 dependance worker 都可能會 rebuild 埋。


其實間唔中中下雷都唔係新鮮事,今次想搞既係 custom component 可以連得返去 parent component 個 viewmodel binding,講多無謂睇 code 最實制。

```js

{ // 整左一個 textfield 叫做 username, bind 左去 viewmodel 個 username 度
	bind: {
		value: '{username}',
	},
	xtype: 'textfield',
	fieldLabel: 'username',
	value: '',
}

```

上面係好正常可以運作,對於個 `textfield` 來講,`value` 係佢呀 `config`。當 viewmodel 個 username 變動時,就會叫埋佢個 `setValue()` 去 update 返 `textfield` 個 `value`。

但係如果你自己整左個 component 出來,咁要做先可以連到呢? 一齊睇下下面呢個。

```js
// 整左個自訂 component 出來
Ext.define('App.MyComponent', {
	extend: 'Ext.panel.Panel',
	alias: 'widget.app-mycomponent',
	config: {
		value: '',
	},
	viewModel: {
		data: {
			value: '',
		},
	},
	layout:{
		type: 'vbox',
	},
	items:[{
		bind: {
			value: '{value}',
		},
		xtype: 'textfield',
		fieldLabel: 'username',
		value: '',
	}]
});

// 然後再 create 一個 panel
Ext.create('Ext.panel.Panel', {
	itemId: 'panel1',
	viewModel: {
		data: {
			value: '',
		},
	},
	layout:{
		type: 'vbox',
	},
	items:[{
		itemId: 'panel2',
		bind: {
			value: '{value}',
		},
		xtype: 'app-mycomponent',
	}]
});
```

現在當 `#panel1` 的 viewModel value 變動時,就會連動埋 bind 左個 component (app-mycomponent) 入面個 value 都會更新。

但係如果我現係係 `#panel2` 度 call 個 `setValue()`,你估會唔會可以反向更新返 `#panel1` 個 viewModel 呢???

答案係唔會 !!!!

除非要加一個 `config` 入去 `#panel2` 個 define 入面,就係 `publishes` 睇下下面:

```js
// 下面係改動左
Ext.define('App.MyComponent', {
	extend: 'Ext.panel.Panel',
	alias: 'widget.app-mycomponent',
	config: {
		value: '',
	},
	publishes: {
		value: true, // 呢度個 value 係指 config 個 value, 咁當呢個 instance call .setValue() 時,如果出面有 viewModel bind 到呢個 value 就會一齊更新
	},
	viewModel: {
		data: {
			value: '',
		},
	},
	layout:{
		type: 'vbox',
	},
	items:[{
		bind: {
			value: '{value}',
		},
		xtype: 'textfield',
		fieldLabel: 'username',
		value: '',
	}]
});
```

呢啲野都係 document 上面寫到唔清唔楚又無好好介紹點用... 搵左勁耐先搵到。

但係仲有樣野解決唔到,就係點樣可以 listen 個 config change? 因為 config 轉左其實我係唔知道佢轉左,所以 update 唔到相應既 component。

雖然可以通過 override setValue() 呢個方法去做,但係唔知點解 ExtJS 7.0.0 個 community edition 係有 bug...

係用唔到 this.callParent(arguments) ...

變左係無得用 override 去解法... 唉... 唔知會唔會整返好...


9月 19, 2024 Blogger


唔洗講今次又仲坑搞左幾個鐘頭,話說之前有個可以自動 Generate 圖片的網站現在用唔到了 (可能要靠 Cookie 先可以比自己睇到),所現現在要為所有 Blog post 都加返個可以設定的 Featured Image 功能。

其實 Featured Image 係 Blogger 入面都係有的 (已前),但係現在就唔知點解收收埋埋無左 !!! 變左每一個 post 入面第一張 image 自動變為作 Featured Image !! 不過最慘係佢個 Blogger Data Tag 個 Document 係無寫到出來點用,搞到又要找又試一大輪先用到。

正常情況下用 post.featuredImage 應該可以拎到個 Image 出來係唔係? 不過 Blogger 就好神奇地唔得既,如果 Blog Widget 入面要拎 Featured Image 係要用 post.firstImageUrl 先拎到 (拎到第一張圖片),但係如果係 Blog Archive Widget 度拎 Featured Image 的話,就要正常使用 post.featuredImage 先得 !! 中晒坑

好彩先前有大神遇過先,如果唔係十世都唔會搵到 :

https://stackoverflow.com/questions/51087706/post-thumbnail-not-showing-on-homepage-with-error-img-src-lt-cant-find-su

> When the images are hosted externally (aka not uploaded via Blogger editor) and you still want to access them via Blogger data tag, then you will need to use `<data:post.firstImageUrl/>` in place of `<data:post.thumbnailUrl/>`.

### 在 Post 內加入 Featured Image

係 Blogger 入面,每一個 post 的第一張插入圖片就會自動成為 Featured Image。首先要 Switch 去 HTML Mode 先,然係插入以下 Code:

```html
<img style='display: none;' src='https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghC-axPogzPnql0blsf4UrF0TGWckylVcJewFVhbEj-t6QDGxRrZuG_qeAfgBqAaeox5OL0G0BEAebiqH5VoQNJVRyHeyIufyz4fhrRkbHdUDekSIpKQA1mYsupMuzULgE2837PV5YKWw6Nw3y6axZA750hM4qHsEiB6yuzPjez1nxH-rPckvaUtYm6Hw/s1600/Blogger-Logo-16x9.png' />
```

將入面圖片的 Url 連結改為你自己的圖片就可以了。設定好之後 Blogger 就可以係 Data Tag 比資料時經由 post.firstImageUrl 或者 post.featuredImage 拎返出來用了。
3月 12, 2024 CSS
在網站上載入圖片是需要消耗時間的,因為圖片的傳遞和文字比起來實在要大太多。因為在載入網站時常常會看到一個個的空窗,然後就突然填充了圖片。如果想要令到整件事件可以順暢一點,我們就需要用到 Placeholder 和 Shimmer 技巧了。

### 什麼是 Image placeholder

Image placeholder 是指為圖片預留空的一個填充物,一般在整網站時為了排版的可預測性 (predictability),我們都會加入 `width` / `height` / `min-widht` / `min-height` / `max-width` / `max-height` 設定圖片的大細,確保在載入後都在可控制的範圍來,使排版不會出現不能預期的效果。

這個時候我們可以用一個 `div` 來作為圖片在載入時的填補。可以看看下面代碼 : 

```css
/* 設定為 relative 及背景色彩 */
div.image {
    position: relative;
    background-color: #DDDDDD;
}
```

```html
<!-- 建立一個 div 作為 placeholder 並設定 width / height -->
<div class='image' style='width: 300px; height: 100px;'></div>
```

這樣就可以得一個灰色的 DIV 作為圖片載入的 placeholder 了。

### 設定 Shimmer 效果

Shimmer 就是在灰色的 div 上加入白色的閃光,令使用者知道這是一個正在載入中的提示。

我們可以通過使用 CSS 的 animation 去達成。

```css
/* 圖片的 placeholder */
div.image {
    position: relative;
	background: linear-gradient(-45deg, #eee 40%, #fafafa 50%, #eee 60%);
	background-size: 300%;
	background-position-x: 100%;
	animation: shimmer 1s infinite linear;
}

/* 圖片的內容 */
div.image .image-src {
    position: absolute;
	inset: 0px;
	display: block;
}

/* 動畫 */
@keyframes shimmer {
	to {
		background-position-x: 0%
	}
}
```

然後就可以在 div 但放入你想要載入的東西了。

```html
<!-- 建立一個 div 作為 placeholder 並設定 width / height -->
<div class='image' style='width: 300px; height: 100px;'>
  
  	<!-- 加入圖片的 element -->
  	<div class='image-src' style='background: url(https://source.unsplash.com/1024x600) center center / cover no-repeat;'></div>
</div>
```
3月 11, 2024 CSS
今日講下點樣可以用 CSS 來選取你想要的 DOM 元件。

### TLDR

直接去下面兩個網站睇用法

:first-child 用法 : https://developer.mozilla.org/en-US/docs/Web/CSS/:first-child

:last-child 用法 : https://developer.mozilla.org/en-US/docs/Web/CSS/:last-child

### CSS Selector

透過 CSS 的特定語法來選取 DOM 元件,名稱叫 Selector。

Selector 是一串文字用來指定某一個 / 一些 DOM 物件,例如我們有以下一個 html :

```html
<div>
	<table>
		<tbody>
			<tr>
				<td>
					Hello world A
				</td>
				<td>
					Hello world B
				</td>
			</tr>
			<tr>
				<td>
					Hello world C
				</td>
				<td>
					Hello world D
				</td>
			</tr>
			<tr>
				<td>
					Hello world F
				</td>
				<td>
					Hello world G
				</td>
			</tr>
		</tbody>
	</table>
</div>
```

如果你想要取出 Hello world A 的 `td`,就可以用以下的 Selector :

```css
// 取出 td
div table tbody tr:first-child td:first-child {

	// 變更背景色彩
	background-color: rgb(0, 0, 0);
}
```

如果你想要取出 Hello world F 的 `td`,就可以用以下的 Selector :

```css
// 取出 td
div table tbody tr:last-child td:first-child {

	// 變更背景色彩
	background-color: rgb(0, 0, 0);
}
```

如果你想要取出 Hello world G 的 `td`,就可以用以下的 Selector :

```css
// 取出 td
div table tbody tr:last-child td:last-child {

	// 變更背景色彩
	background-color: rgb(0, 0, 0);
}
```

很方便的呢 !
3月 10, 2024 Javascript
IIFE 的全寫是 Immediately Invoked Function Expression,這是一個當你建立後就會立即執行的 function。通常都會是一次情而且是 anonymous 的。

### 例子

IIFE 會是下面這樣 :

```js
(() => console.log(‘Hello world’))();
```

這段 code 執行時,會立即畫出 Hello world 到你的 console 上。

### 使用原因

使用 IIFE 的原因是為了保護變數的可用性。IIFE 中定義的變數不能從外部存取。這是編寫方法可以保護程式碼並減少出錯的方法。

### 配入 function 的等性使用

另外你可能需要留位一個變數旳值,用來供日後存取用 (即類似於 factory pattern),可以看看以下例子 : 

```js
// 進行 10 
for( let i=0; i<10; ++i ) {
	
	// 使用 setTimeout 來推遲執行
	setTimeout(() =>{

		// 記錄到 console
		console.log(i);
	}, 1000);
}
```

以上的代碼你可能會想看到 : 0,1,2,3,4,5,6,7,8,9

但是實際上你應該會看到是 : 9,9,9,9,9,9,9,9,9,9

這是因為經過 setTimeout 的 delay 後,`i` 的值已經由 0 數去到 9 了,所以當第一個 `setTimeout` 進行 `console.log(i)` 是,只會提取到 `i` 是 9 的結果。

`setTimeout` 只是一個例子,同樣的情況也會發生到 event handler 上,例如 : 

```js
// 取出所有 .button 的 div
const elements = document.querySelectorAll('div.button');

// 每一個 element 也做一次
for( let i=0; i<elements.length; ++i ) {
	
	// 取出 element
	const element = elements[i];
	
	// 加入 ‵click` 事件
	element.addEventListener('click', function(evt) {
			
		// 記錄到 console
		console.log(i);
	})
}
```

你按下任可的 `div.button` element 時,都只會在 console 出現 9。

#### 解決方法

你可以透過使用 IIFE 和變數傳入 function 的特性來避免這個事情。

```js
// 取出所有 .button 的 div
const elements = document.querySelectorAll('div.button');

// 每一個 element 也做一次
for( let i=0; i<elements.length; ++i ) {
	
	// 取出 element
	const element = elements[i];
	
	// IIFE
	(i2 => {

		// 加入 ‵click` 事件
		element.addEventListener('click', function(evt) {

			// 記錄到 console
			console.log(i2);
		})
	})(i);
}
```

通過把變數 `i` 傳入到 function 內,因為數值及文字是 [pass by value](https://www.google.com/search?q=js+pass+by+value) 的關係,所以在 function 內收到的變數 `i2` 是一個值,而不是 `i` 的參照 (reference),所以在之後應用 `i2` 時也只會得到當時 `i2` 值的結果。
3月 08, 2024 Javascript
在 Web Development 上我們常常會儲存小量資料在 Client Side 上,有可能是使用者的使用習慣,或者對上一次動作遇原資料等等。

雖然 Browser 自帶的 `localstorage` API 已經好方便使用,因為再提升可用性,現在就介紹一個 library 用來管理 `localstorage` 的。

### store2

Github : https://github.com/nbubna/store

佢提供左簡單的方法來存取 `localstorage`,可以另你更加專心你 Business logic 的開法。

#### 簡單應用方法

我們可以通過 `store()` 來進行簡單的基本的應用

```js
// 設定 key value
store(key, data);

// 使用 key 讀取 value
store(key);

// 運用 transaction 來寫入 value 到 key
store(key, fn[, alt]);

// 一次過寫入多個 key value
store({key: data, key2: data2});

// 讀所 store 內的所有 key value
store();

// 一個一個咁讀出 key 及 value, 可以通過 `return false` 來停止
store((key, data) => { });

// 清除所有的資料
store(false);
```

#### 進階應用

如果要進行更多的功能,可以通過以下方式來達成。

```js
// 相等於 store(key, data)
store.set(key, data[, overwrite]);

// 相等於 store({key: data, key2: data})
store.setAll(data[, overwrite]);

// 相等於 store(key)
store.get(key[, alt]);

// 相等於 store()
store.getAll([fillObj]);

// 相等於 store(key, fn[, alt])
store.transact(key, fn[, alt]);

// 相等於 store(false)
store.clear();

// 檢查 store 內有沒有提升的 key 值
store.has(key);

// 清除 key 的值及回傳原有資料,如果沒有資料則回傳 alt
store.remove(key[, alt]);

// 相等於 store(fn)
store.each(fn[, fill]);

// 加入資料到 key
store.add(key, data[, replacer]);

// 回傳所有的 keys
store.keys([fillList]);

// 取得 key 的 lenght
store.size();

// 清除所有的 key
store.clearAll();
```
3月 07, 2024 Javascript
在取得 Dom 時我們常常會用到 `document.querySelector()` 用來取得向下的子元件。有時候我們也會可能需要向上取得母系元件,這時候我們就需要用到 `closest()` 了。

### 用法

在使用 `closest()` 時我們需要由一個 Dom 出發,例如有以下這個 html :

```html

<div id='div1'>

	<div id='div2'>

		<div id='div3'></div>

		<div id='div4'></div>		
	</div>

	<div id='div5'></div>
</div>
```

我們先用 `querySelector()` 取得 `#div3` 這個 Element 先 :

```js
const div3 = document.querySelector('#div3');
```

如果需要由 `#div3` 出發,使用 `closest()` 取得 `#div1` 的話,可以這樣 : 

```js
const div3 = document.querySelector('#div3');

const div1 = div3.closest('#div1');
```

那麼問大家一個問題,如果使用以下的語法會取得那一個 `div` 呢?

```js
// 問題 1
div3.closest('div');

// 問題 2
div3.closest('div div');
```

答案時間,問題 1 的答案是 `#div3` 自已本身,而問題 2 的答案也是 `#div3` 自已本身 !

因為 `closest()` 在搜索時也會包含自己在內,這點一定要注意。

### 使用 `closest()` 但是排除自己

如果我們只想要向上取得 Element 就先需要排除自身 Element 在外,可以通過以下方法達成 : 

```js
// 取得 div3 
const div3 = document.querySelector('#div3');

// 通過使用 parentNode 來向上取得 parent,再在 parent 上使用 closest() 就可以了
const div2 = div3.parentNode.closest('div');
```
以下要講下魔改 BlogArchive Widget,今次應該真係魔改,暫時在網站上找不到任何相關的文章和 API 講有關事情。

話說係 BlogArchive Widget 入面如果用 `HIERARCHY` 模式的話,可以得到一個 `data` 參數來畫出你需要的畫面。

因為是 `HIERARCHY` 的關係,所以 `data` 變數也是樹狀型態的。

依照[官方 Documentation](https://support.google.com/blogger/answer/47270) 所講,你可以得到以下的資料:

- title: The title of the widget.
- style: One of 'MENU', 'FLAT', or 'HIERARCHY'.
- data: A list of each archive unit, each of which contains:
    - name: The name of this archive interval, e.g. "August 2006."
    - url: The link to the page containing posts from this interval.
	- post-count: How many posts there are in this interval.

### 限制

但是如果你玩過呢個 Widget 之後,發覺佢只會提供現時所在位置 Post 的相關月份內容,而不是提全部的內容給你去畫出 UI。

例如你現在是正在看 2023 年 6 月的文章,Widget 只會給你以下資料:

- [2023 年](https://example.blogspot.com/2023/)
	- [7 月](https://example.blogspot.com/2023/07/)
	- [6 月](https://example.blogspot.com/2023/06/)
		- [你的文章 1](https://example.blogspot.com/2023/06/example-post1.html)
		- [你的文章 2](https://example.blogspot.com/2023/06/example-post2.html)
	- [4 月](https://example.blogspot.com/2023/04/)
	- [1 月](https://example.blogspot.com/2023/01/)
- [2022 年](https://example.blogspot.com/2022/)
	- [5 月](https://example.blogspot.com/2022/05/)
	- [4 月](https://example.blogspot.com/2022/04/)

你只能夠按下 2023 年 7 月的連結,是不能夠取得 2023 年 7 月的文章列表。

### 開始 Reverse Engineering

如果大家有玩過 Blogger 預設的 Theme,有可能會發現有一些好舊版的 Theme (千禧年主題)入面個 Blog Archive 是可以做到 Ajax 載入不同月份的文章列表。

呢個時間你可以打開 Chrome development tool 來看看到底叫了一個什麼的 Ajax 出去。你應該會看到類似下以的 Url:

```js
https://example.blogspot.com/?action=getTitles&widgetId=BlogArchive1&widgetType=BlogArchive&responseType=js&path=https://example.blogspot.com/2023/06/&xssi_token=TOKEN
```

然後你可以試下去不同的頁面去開一開個 Blog Archive 上面個箭咀去觸發個 Ajax 載入:

```js
https://example.blogspot.com/2023/02/?action=getTitles&widgetId=BlogArchive1&widgetType=BlogArchive&responseType=js&path=https://example.blogspot.com/2023/06/&xssi_token=TOKEN
```

會發現到無論你在任何的 URL 上 Request 去 server 都好,只要你 Url 上 Query 上以下的參數就可以 Call `text/javascript` 的 Response 回來。

- action
	暫時只發現 'getTitles' 這個值能用
- widgetId
	大約是填返你 BlogArchive 個 id,試過填第二啲野會出 HTTP 400 Error
- widgetType
	暫時只發現 'BlogArchive' 這個值能用
- responseType
	暫時只發現 'js' 這個值能用
- path
	這個必需要填對才能通過,如果你要取得 2023 年 6 月的文章列表,就必需要填入 'https://example.blogspot.com/2023/06/' 這個值,填錯出會 Error
- xssi_token
	可以用 JS 在 runtime 時找到 xssi token

### 如何找到 XSSI Token

你可以在你的 Blogger 網站上用 Browser 按 `檢視源始碼` 來看看由 Blogger Server 最終 Generate 出來的 HTML 到底是怎樣。

我們先 Scroll 到最底,會發現到類似的 JS Code :

```js
window['__wavt'] = 'ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD';
```

就是這個 `window['__wavt']` 變數裝住的,就是你需要的 XSSI Token。你可以在 Event `DOMContentLoaded` 後任何時間取出來使用。

```js
// DOMContentLoaded 事件處理
document.addEventListener('DOMContentLoaded', function() {
	
	// 儲起 XSSI TOKEN 備用
	const XSSI_TOKEN = window['__wavt'];
});
```

### 開始 Request

有齊上面資料後,就可以用 `axios` 出個 GET Request 比伺服器。

```js
// prepare url
const url = 'https://example.blogspot.com/?action=getTitles&widgetId=BlogArchive1&widgetType=BlogArchive&responseType=js&path=https://example.blogspot.com/2023/06/&xssi_token=TOKEN';

// axios get request and extract 'data'
const { data } = axios.get(url);
```

如果成功沒有出 Error 的話,大約會得到以下的 Response :

```js
try {
	_WidgetManager._HandleControllerResult('BlogArchive1', 'getTitles',{'path': 'https://example.blogspot.com/2023/06/', 'posts': [{'title': 'Your post title', 'url': 'https://example.blogspot.com/2023/06/your-post.html'}]});
} catch (e) {
  if (typeof log != 'undefined') {
    log('HandleControllerResult failed: ' + e);
  }
}
```

但是問題來了,因為取得的不是 Json 格式,不能夠直接使用,必需要使用神器 `eval()` 來加工一下。

### JsonP

還記得先前有文章講解過 JsonP 嗎? 如果未睇過可以去睇下。

我們先整個 random function attach 去 window object 度先 :

```js
// 整條 Random String 作為 Function 名
const fncName = 'fnc_' + Math.random().toString(16).slice(2, 12);

// 用上面個 Random String 作為 window 的 Property, 指向一個 Function
window[fncName] = (widgetId, action, data) => {
	
	// log 去 console 度
	console.log(data);
	
	// 用完要 delete 返 (一次性 function)
	delete window[fncName];
};
```

然後然要搞搞個 Response 令到佢 `eval()` 時改為執後你個自訂 function。

```js
// 將 '_WidgetManager._HandleControllerResult' 改為你自訂的 function 名稱, 即係上面個 Random function 名
const jsCode = data.replace(/_WidgetManager\._HandleControllerResult/g, fncName);

// 執行 eval
eval(jsCode);
```

如果成功的話,就可以在 development console 上看到 print 左條 json 出來。

```js
{'path': 'https://example.blogspot.com/2023/06/', 'posts': [{'title': 'Your post title', 'url': 'https://example.blogspot.com/2023/06/your-post.html'}
```

有左呢條 json 的資料,就可以用佢來更新返相應的 UI 位置。

搞掂 ! 食碗麵 !

### 總結

呢個係魔改,不知道某一日會唔會突然用唔到,不過機會都好細,因為已經有成千上萬個 Blog 都用緊,所以不會輕易就會出改動。

同埋可能有更多更多的魔改有待發掘 !! 等大家繼續去發現 !!


3月 07, 2024 Google
到現在先發現原來 Google 係出過呢一種公開的 REST API 公開比人使用過。

### GData API Directory

Documentation : https://developers.google.com/gdata/docs/directory

之前幾個講 Blogger Live Search 的文章其實就是在用 GData API 去完成,所有的 URL Params 都有詳細的說明。

不過現在已經 Fadeout 左一大部份了,因為公開的 API 好難管理,現在大多都搬到 Cloud Platform 最少也要一條 API Key 才能使用。

這樣也比較安全,管理起來也比較容易。只要把 API Key revoke 就不用再回傳 API 了,能有部減少資源運用或者誤殺 IP 的情況。

### Blogger API v3

這個大約是十多年前的產物,是次搬 Blog 主要也是靠它來完成。

Documentation : https://developers.google.com/blogger/docs/3.0/getting_started

當中有大量 Endpoint 可以使用,如果只在 Client Side 使用者只要到 [Cloud Platform](https://cloud.google.com/) 開個 Project 申請一條 API Key 就可以使用了。

如果要是 `Create`, `Delete`, `Put`, `Patch` 操作就需要 OAuth 取得 Token 後,使用 Bearer token 方法來 Call API 才能做用。

好像是要實作 Search 功能的話,可以使用以下呢個 API 就行了:

```js
GET https://www.googleapis.com/blogger/v3/blogs/YOUR-BLOG-ID/posts/search?q=KEYWORD&key=YOUR-API-KEY
```

如果成功的話,就會收到 HTTP 200 及以下的 Response body:

```json
{
  "kind": "blogger#postList",
  "nextPageToken": "CgkIChiAj86CpB8QzJTEAQ",
  "items": [
  {
    "kind": "blogger#post",
    "id": "1387873546480002228",
    "blog": {
      "id": "3213900"
    },
    "published": "2012-03-23T01:58:00-07:00",
    "updated": "2012-03-23T01:58:12-07:00",
    "url": "http://code.blogger.com/2012/03/blogger-documentation-has-moved-to.html",
    "selfLink": "https://www.googleapis.com/blogger/v3/blogs/3213900/posts/1387873546480002228",
    "title": "Blogger Documentation has moved to developers.google.com",
    "content": "content elided for readability",
    "author": {
      "id": "16258312240222542576",
      "displayName": "Brett Morgan",
      "url": "http://www.blogger.com/profile/16258312240222542576",
      "image": {
        "url": "https://resources.blogblog.com/img/b16-rounded.gif"
      }
    },
    "replies": {
      "totalItems": "0",
      "selfLink": "https://www.googleapis.com/blogger/v3/blogs/3213900/posts/1387873546480002228/comments"
    }
  },
  ...
  ]
}
```

### 點樣 Call?

用 Axios 啦!

Git hub : https://github.com/axios/axios

有需要的話再寫其他文章講解。
今時今日 QR Code 的用途很多,而很多時我們都要 Program 去 Generate 一條 Token 比個使用者去通過某啲場境。

現在就講下點樣係 Javascript 度 Generate QR Code。

### 搵 Library

已經有大神整好左呢啲惠及人民既野,咩都唔洗講去一去呢度先。

直接去呢度 : https://github.com/davidshimjs/qrcodejs

佢係可以係 Browser 上面 Generate QR Code 的 Library,原理係通過 `canvas` 畫出 QR Code 的樣子,再取出 data url 放到 `img` 上達成。

### 使用方法

可以去佢個 Git hub clone 落來用又得 : 

Git hub 即係呢個 : https://github.com/davidshimjs/qrcodejs

可以去 CDN 拎來直接用都得 : 

CDN 網站 : https://cdnjs.com/libraries/qrcodejs

#### 簡單使用方法

簡單用法:

```html
<!-- 需要你留返個 DIV 比佢用來畫出個 QR Code -->
<div id='qrcode'></div>

<script type='text/javascript'>
	
	// 傳個 div 的 element 入去就可以了
	new QRCode(document.getElementById('qrcode'), 'https://github.com/davidshimjs/qrcodejs');
</script>
```

#### 更多的設定

簡單用法:

```html
<!-- 需要你留返個 DIV 比佢用來畫出個 QR Code -->
<div id='qrcode'></div>

<script type='text/javascript'>
	
	// 都係 new 一個 QRCode 實體出來
	const qrcode = new QRCode(document.getElementById('qrcode'), {

		// 設定文字
		text: 'https://github.com/davidshimjs/qrcodejs',

		// 寬度
        width: 128,

		// 長度
		height: 128,

		// 深色部份
		colorDark : '#000000',
        
		// 淺色部份
		colorLight : '#ffffff',
        
		// 修正度
		correctLevel : QRCode.CorrectLevel.H,
  });
</script>
```

如果之後要刷新 (refresh) 個 QR Code 時,可以用以下方法就得。

```js
// 清除原本的 Code
qrcode.clear();

// 重新畫另一個文字
qrcode.makeCode('https://cdnjs.com/libraries/qrcodejs');
```
之前個 Post 話有找條 Link 可以用來做 Ajax Search,今日有時間就做好左了。

呢個就係條 Link 個樣 : [/feeds/posts/summary?callback=callcall&max-results=50&q=react&start-index=1&alt=json&orderby=updated](/feeds/posts/summary?callback=callcall&max-results=50&q=react&start-index=1&alt=json&orderby=updated)

原本佢係用來做 RSS 的 (我估),不過佢有支援可以用 Json 輸出,除此之外仲有得 Support JsonP 添。

### 參數 (估估下)

- JsonP
	係上面條 URL 度有個 `callback` 的 params,只要你比個名佢,佢就會以 JsonP 的方適去 Response 返來。

- Json
	係上面條 URL 度有個 `alt` 的 params,填個 'json' 比佢,就會以 json 的方式 Response 返來。

- Query
	係上面條 URL 度有個 `q` 的 params,填你想要搜索的關鍵字比佢,就可以搵返相應的 Result 返來。

### 實作時遇到的事情

記錄一下在實作時遇到的事情

#### Custom Event

可以為 Dom element 加入 / 觸發自訂的事件。

```js
// select a single dom element
const element = document.querySelector('#mydiv');

// your custom event handler
element.addEventListener('yourcustomevent', function(evt) {

	console.log('hi, this is your custom event');
});

// prepare event object
const evt = new Event('yourcustomevent');

// dispatch the event.
element.dispatchEvent(evt);
```

#### 取得 Dom element (static position) 的 position (relative to window)

如果淨係用 `getBoundingClientRect()` 只可以取得未加入 window.scroll 的 (X,Y) 值,所以實際用起來時一定要加返 (window.scrollX, window.scrollY) 的值才能使用。

```js
// select a single dom element
const element = document.querySelector('#mydiv');

// get position
element.getBoundingClientRect().top + window.scrollY;
```

#### text-overflow: ellipsis; 用唔到

解決 : https://stackoverflow.com/questions/17779293/css-text-overflow-ellipsis-not-working

就係要必需設定對上一個 block element 的以下 CSS:

```css
div.text-overflow {
	overflow: hidden;
	white-space: nowrap;
}
```

#### encodeURIComponent

記得由 User input 的資料要放入去 URL 時,都一定要做好 `encodeURIComponent()` 先得。

```js
// 由使用者自己輸入
const userInputValue = 'hello world';

// 必定要做好 encodeURIComponent 才能放心
const url = `https://example.com/api/v1/users?name=${encodeURIComponent(userInputValue)}`;
```

#### 記住用 : DOMContentLoaded

呢度 : https://stackoverflow.com/questions/9899372/vanilla-javascript-equivalent-of-jquerys-ready-how-to-call-a-function-whe

如果想安全啲,記得要係呢個 event 入面先好操作 Dom elements。

以前 IE6 年代件用 `window.onload = function() { ... }`;

到到 jQuery 年度用 `$(function() { ... })`;

到翻 Vanilla Javascript 返璞歸真,用下面呢個例子 :

```html
// fire when dom content loaded
document.addEventListener('DOMContentLoaded', function(evt) {

    // Your code to run since DOM is loaded and ready
});
```

### 總結

唔寫低真係會唔記得,上面操作做過無數次,不過每次都係要 Search 下 Google 有無更好的做法。

而都係必需既,可能過幾篇文章又會再寫過呢啲一樣既野。
JsonP 其實即係用 `script` Tag 來載入第三方 (不同 Domain) 的 javascript 檔案來取得資料,好處就係可以通過 Browser 的 CORS 的封鎖。

原理係通過由載入的 Javascript 檔案內容來直接 Call 你預先設定好的 Function。因為用 `script` Tag 載入的 Javascript 會直接執行。

### 例子

你有一個 Html

```html
<!-- 預備工作 -->
<script type='text/javascript'>
	
	// 預先定義一個 function 來處理收到的資料
	const myfnc = data => console.log(data);
</script>

<!-- 載入第三方 JS -->
<script type='text/javascript' src='https://example.com/json.php?callback=myfnc'></script>
```

而 Server 上有這個 PHP 檔案來 Handle 你個 Request

```php
<?php
	
	// get callback name from url params
	$fncName = $_GET['callback'];
	
	// data
	$data = ["foo" => "bar", "apple" => "pen"];
	
	// set javascript response header
	header('Content-Type: text/javascript; charset=utf-8');
	
	// print result
	echo $fncName . "(" . json_encode($data) . ")";
?>
```

當你去 Request 呢個 PHP 時,大約就會得到以下的 Response body

```js
myfnc({"foo":"bar","apple":"pen"})
```

咁當第三方 JS 載入完成後,就會立即自動 call 埋你預先寫好的 `myfnc` function 了。

### 實際應用

當然實際上應用唔會好似上面咁揼死個 `script` tag 去載入,而一定係用 js 去插入一個 `script` tag 先有意義。

```js
// 設定一個 function
function requestData() {
	
	// 開一個 script tag
	const s = document.createElement('script');
	
	// 設定好路徑
	s.src = 'https://example.com/json.php?callback=myfnc';
	
	// 加入到 body 中
	document.body.appendChild(s);
}
```

這樣就可以了。
一連都整左幾日時間,不過其實整來都係自 high 多,沒有什麼商業價值。

之前其實都有整過幾次,但係每次都覺得好複雜同埋做唔到自己想要的東西,所以之後都係搬左去其他唔同既地方。

不過每次搬定之前,都會來到 Blogger 呢度做中轉站,可能因為唔洗錢的關係掛。

印像中係咁:

Blogger > PHP (Codeigniter) > Blogger > PHP (Laravel) > Blogger > Node (Koa) + React > Blogger (現在)

始終維護少一樣野就已經真係差好遠,人大左無咩時間。同埋認真研究左 Blogger 之後,發覺其實設計得好好,有好多你想要的東西其實都已經可以用現成的 `tag` 去完成到。

如果日後有時間再詳細寫一篇有關 Blogger template 的 customization guide 分享下今次的經驗。

現在就記錄返少少有關 Blogger template 的主要要素。

### Data Tags

就是用來提取資料的標籤,全部都係由 `data:` 開始。例如:

```html
<!-- 用來提取 view.isError 這個變數 -->
<data:view.isError />

<!-- 用來提取 blog.url 這個變數 -->
<data:blog.url />
```

問 : 但係有咁多唔同變數,邊度可以睇到呢啲 Dcoumentation 呢?

答 : 可以去呢度睇到 https://support.google.com/blogger/answer/47270

但係有點一定要注意,就係如果你想要拎到 `data:posts` 既內容的話,你就一定要係 `b:widget type='Blog'` 入面先可以提取得到,如果唔係就只會拎到吉的 variable 或者會有可能出現 Error。

```html
<!-- 定義一個 widget, 而個 type='Blog' -->
<b:widget id='Blog1' locked='true' title='文章' type='Blog' visible='true'>

	<!-- 每一個定義的 widget, 都一定會有個 includable id='main' 作為 widget 的進入點 -->
	<b:includable id='main' var='this'>

		<!-- 去到呢度已經可以用到 data:posts 呢個變數, 呢度係要 loop 下佢, loop 出來個 variable 叫做 post -->
		<b:loop var='post' values='data:posts'>
                      	
			<!-- 要係呢度先可以拎到 post.title 出來用 -->
			<data:post.title />
		</b:loop>
    </b:includable>
</b:widget>
```

大致上就係咁,只要砌啱晒呢個 Structure 就咩 widget 都係可以用呢個方法提取到資料。

### Workflow Tags

就是用來控制流程的標籤,全部都係由 `b:` 開始。例如:

```html
<!-- 用來提取 view.isError 這個變數 -->
<b:if />

<!-- 用來提取 blog.url 這個變數 -->
<b:else />
```

佢地通常係要夾埋 Condition 一齊來決定條路點行。例如:

```html
<!-- 對比一下 data:blog.pageType 的值來決定畫出什麼到畫面 -->
<b:if cond='data:blog.pageType == "index"'>
  
    <span>是 index 類頁面</span>
  
    <b:else />
  
    <span>不是 index 類頁面</span>
</b:if>
```

其實同寫 Program 一樣,只不過係語法有少少唔同。

去呢度可以有更詳細的講解: https://support.google.com/blogger/answer/47270

### 總結

其實 Blogger 好好用,又唔洗錢,無論你係初手 / 中手 / 高手,都一樣好適合使用。

佢係專門用來寫 Blog 的 CMS,而且做得好好。

### 隱藏技能

下面呢條 link 好似幾好玩,可以用來做 Ajax search post,遲啲有時間先研究下。

[https://example.com/feeds/posts/summary?callback=callcall&max-results=50&q=react&start-index=1&alt=json&orderby=updated](/feeds/posts/summary?callback=callcall&max-results=50&q=react&start-index=1&alt=json&orderby=updated)
要令到網站體驗去到唔同 Browser 都差唔多的話,咁用 Web font 就一定係個唔少得的步驟。

咁當中最簡單易用就係用 Google web font,一來唔洗錢,二來大家都用佢的話就可能已經有 Proxy 幫你 Cache 左,咁 Client Side 就唔洗再重新載入了。

### 用法

首先去呢度選返個你鍾意既字形先 : https://fonts.google.com/?noto.query=noto+

而我見到有 `Noto Sans Hong Kong` 用,咁就當然係諗都唔法諗就用呢個啦。

然後 embed 下面個 css 入去你個 template file 度。

```html

<link rel='preconnect' href='https://fonts.googleapis.com' />

<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin='' />

<link href='https://fonts.googleapis.com/css2?family=Berkshire+Swash&family=Noto+Sans+HK:wght@100..900&family=Noto+Serif:ital,wght@0,100..900;1,100..900&display=swap' rel='stylesheet' />
```

再係 `style` 入面設定返字形套用的地方 :

```css
html, body, div, p, table, th, td {
	font-family: 'Noto Sans HK', sans-serif;
	font-optical-sizing: auto;
    font-style: normal;
}
```

咁就設定好了,再 Reload 一下你個網站,當堂唔同晒 !
要顯示圖片就一定唔少得用 Lightbox 之類的東西,所以係 Blogger 呢度都係要加入返個先似返啲樣。

比較個咁多款易用程度同埋功能來講,Fslightbox 都係最方便同埋好用的表表者。

官方網站 : https://fslightbox.com/

### 用法

因為 Blogger 係 Vanilla JS,所以最直接就係去搵人地啲 CDN 返來用就最方便。

CDN 地址 : https://cdnjs.com/libraries/fslightbox/3.4.1

將下面呢個直接貼入去你個 Blogger `</body>` 上面一行就搞掂。

```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/fslightbox/3.4.1/index.min.js" integrity="sha512-BD6B4tou7H7CWpfaXthxgqwk7kHs/rPDEk9GD/Rvbx0sCOUdqtrvlBP/o64EmdP5lnGOS4j4FTcn0iPJx6bVCw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
```

如果係純 Js 用就好簡單,用返 `document.querySelectorAll()` 抽返晒你文中的所有 `<img>` 出來,然後用 `getAttribute('src')` 抽出圖片的來源網址備用。

```js
// 建立 Fslightbox 的實體
const lightbox = new FsLightbox();

// 設定來源的類別
lightbox.props.type = 'image';

// 設定來源
lightbox.props.sources = imagesData.map(image => image.src);

// 開啟 lightbox
lightbox.open();
```

### 如何抽出文中的所有 `<img>` 來源網址

上面講到要抽出文中的所有 `<img>` 來源網址,其實都好簡單,今日今日 (2024年) Vanilla Js 先係皇道,所以就用 Vanilla Js 去做。

```js
// get all target elements
const imageElements = document.querySelectorAll(`img`);

// pluck all src
const sources = imageElements.map(imageElement => imageElement.getAttribute('src'));
```

咁樣你個 `sources` 就已經有晒所文中所有的 `<img>` 來源網址了。
3月 04, 2024 NodeJS
最近個 Project 諗起有野想做,係需要用到 OCR 技術,所以睇了一下關於用 NodeJS 去 Extract 圖片入面的文字出來。

### node-tesseract-ocr

https://github.com/zapolnoch/node-tesseract-ocr#readme

呢個係有大神整好左現成的 nodejs lib,已經可以直接使用。而佢背後其實係 Call 緊 Linux 個 tesseract-ocr (都係要裝)。

要先裝好左 tesseract-ocr

```sh
apt-get install tesseract-ocr
```

然後再裝呢個 npm

```sh
npm install node-tesseract-ocr
```

### 使用法方

好簡單

```js
const tesseract = require("node-tesseract-ocr")

const config = {
	lang: "eng", // default
	oem: 3,
	psm: 3,
}

async function main() {
	try {
		const text = await tesseract.recognize("image.jpg", config)
		console.log("Result:", text)
	} catch (error) {
		console.log(error.message)
	}
}

main()
```

### 效果

實際用起上來比起我要的效果仲係差好遠,可能因為我輸入的圖片太多唔係文字的東西,所以認得唔係咁好。

但係如果只係 Crop 淨返文字的部份,佢可以可認得出 99% 的文字內容出來。

睇來要再試試其他方向先得。
3月 02, 2024 Blogger
其實 Google 對 Blogger 呢個 project 都真係認真唔錯的,到今日今日都仲有新 API 出比人用來搬屋,都算係咁。

如果要手動一個一個咁搬真會係搬到 2046 都未完。

使用方法都係例排野,如果係公開資料的話只需要有條 API Key 就可以 Call 到。

但係要 Create Post 就要有 OAuth 或者 Service Account 個 Token 先可以做到。

### API Doc

下面呢個係 API Overview :

https://developers.google.com/blogger/docs/3.0/using?hl=zh-tw

呢個就係講個 Create Post 點用既 :

https://developers.google.com/blogger/docs/3.0/reference/posts/insert?hl=zh-tw

### File Objects 處理

其實 Blogger 真係好方便,不過都有唔好處,真係佢對 Resource 的處理唔係好好。而我呢度所指的 Resources 係指 Objects (Photos, Videos, Files...)。

不過呢個其實唔關佢事,因為要管理 Objects 其實都係複雜的事情,之前 19Site 都係放係自已 Server 上面 Host。

現在搬機就自然要搬埋啲 Files 走,但係要一個一個 Upload 上去 Blogger 然之後 Extract 返個 URL 出來是何奇痛苦的事...

所以今真係將啲 Files 開左個 Object storage 放晒上去,再開個 cdn subdomain 指返去個 bucket cdn 度,咁就指需要係每篇文章改返少少個 URL 後,所有圖片都可以正常顯示了。

### Rate limit

當然每一個 API 為左保護返 Server 都會有 Rate limit 設定,防止使用者係咁 POST 野去塞滿晒個 Server。

而呢個都成為左搬定的最大阻力,實用體驗係每日大約 80 篇文章左右,如果大家搬的網站係好多文章的話,就可以要搬好多日了。

最後經過幾日的時間,終於都搬好晒了 !
3月 01, 2024 Blogger
講真,要搞到 Blogger 可以好好睇睇真係要花好多時間去睇 DOC。

而下面呢兩條 link 係好緊要,無左佢真係咩都搞唔掂。

Widget Tags for Layouts

https://support.google.com/blogger/answer/46995

Layouts Data Tags

https://support.google.com/blogger/answer/47270

仲有呢篇教人可以整到啲內容用 Markdown 出都好正,唔洗再用 html editor 搞到啲內容走晒位 !!

https://github.com/cs905s/md-in-blogger

呢度有個 `data:view` 的 cheatsheet

https://www.share-collections.com/2020/09/blogger-cheatsheet-dataview.html

### 隱藏的 TAG

用用下其實會發覺到有好多(隱藏的 TAG),而係 Documentation 中係搵唔到的,例如:

```html
<div>
	<b:class name='mt-3' cond='data.blog.pageType == "item"' />
</div>
```

上面呢個 Tag 的意思是,如果 `data.blog.pageType == "item"` 時,就會將 `mt-3` 呢個 class 加入去 parent element (div) 度。

3月 01, 2024 Facebook
記錄返呢幾日玩既野先,有關 Facebook Graph API

如果想要拎到 adv accounts,可以用呢個 api

```js
/me/adaccounts
```

但係要在 oauth 時設定 ads_read 同埋 ads_management 呢兩個 scopes

```js
{
	data : [{
		account_id: [numeric string],
		id: [string];
	}]
}
```

之後就可以用呢個 id 去 call

```js
/[id]/adcreatives?fields=id,account_id,actor_id,title,name,instagram_permalink_url,effective_instagram_story_id,instagram_actor_id,status,thumbnail_url&thumbnail_width=300&thumbnail_height=300&limit=50

``` 咁就可以返到廣告了 另外 effective_instagram_story_id 即係 ad creative link 去 ig post 既內容 可以用呢個 api 去拎返啲 comments 出來 ```js /[effective_instagram_story_id]/comments?fields=id,username,text,timestamp ```

3月 01, 2024 19 Things
最後都係搬返去 Blogger,因為無咩時間去維護個 Blog。

之前 React 寫個陣係用 Class Component 去寫,要轉返晒去做 Function Component 都要花唔少時間。所以都係唔改了,直接用返最原始的 Blogger 就算。

搬來搬去最後都係搬返去 Blogger haha
過去文章
2025 (9)
4 (5)
3 (1)
2 (3)
2024 (25)
11 (3)
10 (3)
9 (1)
3 (18)
2022 (6)
10 (1)
6 (2)
5 (1)
3 (1)
1 (1)
2021 (21)
11 (7)
7 (1)
6 (2)
5 (2)
4 (6)
3 (2)
2 (1)
2020 (92)
12 (1)
11 (2)
10 (4)
9 (10)
8 (5)
7 (1)
6 (3)
5 (1)
4 (4)
3 (25)
2 (7)
1 (29)
2019 (57)
12 (25)
11 (7)
9 (25)