[{"content":"自從推出 EinkBro 後，一直有人在敲碗的功能就是希望可以針對每個網站，能夠自訂 CSS 和塞入 Javascript。很多時候，使用者會希望隱藏網頁上的某些元件，或是修改一些行為，讓閱讀時能更流暢。但是，因為之前並沒有為每個網站建立獨自的資料儲存，而且，在手機上如果想要手動輸入 CSS 和 Javascript 也不太實際，所以並沒有想過將這功能實作出來。\n實作 但，這一切在 AI coding 幾乎完全取代我手工寫程式之後，就變得是舉手之勞的一件事。首先，之前就已經實作了在網頁載入後，啟動客製化 CSS 和 Javascript 的流程，包含字型變粗體，更換字體，以及各種翻譯的操作，都是透過這套實作機制來達成的。現在要額外增加的只是：在網頁載入後，先判斷該網站使用者是不是有自訂了 CSS 和 Javascript，有的話就再執行一下。\n而資料輸入很麻煩的這個問題，也在整合了 AI 之後，有了更好的解法：讓 AI 幫忙看一下要怎麼產生所需要的 CSS 和 Javascript 程式碼，由它來幫忙在 EinkBro 中加上所需的程式片段。使用者甚至可以不用去了解它是怎麼達成的，只要在它完成後，測試看看有沒有達到自己想要的效果就好。\n例子說明 舉個例子來說，平常偶爾會看一下公視的新聞網站。每次進入時，都會跳出蓋板的對話框建議登入。如果能直接將這個對話框隱藏，體驗會更好。這時，操作的流程會是：\n點擊 Page AI Actions 點擊 Tasks 點擊 Custom task\u0026hellip; 然後跟它說：想要將這個網站上的建議註冊畫面給隱藏，把它寫到 domain config 中 由於現在 EinkBro 已經支援類似 functions 的功能給整合進來的 AI (OpenAI, Gemini, Alternative LLM) 使用，其中包括取得網頁原始內容，撈出網頁中所有連結，讀取和寫入目前網站的 personal config (CSS, Javascript snippets)。上述的第 3 步驟，在跟 AI 講完需求後，它會先試著從網頁原始碼中找到需要隱藏的元件，寫成 javascript snippet，再寫入 domain config 中。\n這時，使用者只要再重新載入網頁，就知道是不是有達到需求。如果沒有的話，可以在聊天介面中多給 AI 一點資訊，看它是不是能順利修改好。整個過程使用者不需要自己輸入任何程式碼，相當直覺。\n實作完這功能後，我自己也針對好幾個網站做了調整，包含把 reddit 的 use app banner 也都隱藏了，整個的使用體驗大幅提升！\n相關連結 https://github.com/plateaukao/ADR/blob/main/einkbro-per-site-configuration.md https://github.com/plateaukao/ADR/blob/main/einkbro-agent-in-chat-with-web.md https://github.com/plateaukao/ADR/blob/main/einkbro-agent-domain-patch-tools.md ","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-einkbro-%E4%B8%AD%E8%87%AA%E8%A8%82-css-%E5%92%8C-javascript/","summary":"\u003cp\u003e自從推出 EinkBro 後，一直有人在敲碗的功能就是希望可以針對每個網站，能夠自訂 CSS 和塞入 Javascript。很多時候，使用者會希望隱藏網頁上的某些元件，或是修改一些行為，讓閱讀時能更流暢。但是，因為之前並沒有為每個網站建立獨自的資料儲存，而且，在手機上如果想要手動輸入 CSS 和 Javascript 也不太實際，所以並沒有想過將這功能實作出來。\u003c/p\u003e","title":"在 EinkBro 中自訂 CSS 和 Javascript"},{"content":"\n從前年開始，就陸續開始用 copilot 和其他 AI 工具來開發 EinkBro。這幾個月來，就幾乎都是用 CLI 的工具在進行開發，不太使用 IDE 來自己寫 code，甚至連 code review 也沒有什麼在做了。\n目前 CLI 的 AI 工具愈來愈多，雖然大家還是說最強的是 claude code，但因為沒有訂閱，所以我使用 claude code 的經驗是最少的。目前有輪流在使用的是： Google 的 Antigravity，OpenAI 的 codex，和 Github copilot。\nAntigravity Google Antigravity 底子裡是 VS Code，但因為它有個 Agent Manager，勉強算得上是類 CLI 的介面吧？這是這一陣子用比較多的工具；免費的 token 對我來說也還算夠用，因為想開發的功能沒有很多。\n前一陣子利用它開發了個 Chrome extension — Ask Web，可以用來針對網頁內容設定一大堆動作，並且給予每個動作不同的快捷鍵；也可以進入對話模式，直接跟網頁內容對話。雖然這樣的 extension 應該有超多的，但是總沒有找到合用的，或是有開放原始碼，可以讓我比較安心使用的，所以還是自己叫 AI 寫了一個。功能完全針對我自己的需求量身打造，相當順手。如果還缺什麼功能，或是 UI 不夠簡潔，隨時可以再叫 AI 調整一下。\nGitHub - plateaukao/ask_web: A chrome extension to use openai API to query web content\n為了要隨時知道 Antigravity 各 model 的使用情況，有裝了一個用來看使用量的 plugin — Antigravity Quota Monitor。有了它，就比較可以知道何時應該再換下個 model，甚至是工具來繼續開發。\nCodex OpenAI Codex 是最近才開始在嘗試的。因為沒有訂閱 Pro 或是 Plus ，照理來說，是不行使用的。但因為我有買 API 點數，而且有加入 Data Sharing 的計畫，所以有一定的 quota 可以用來使用 OpenAI 的各種 LLM models。\n上週利用它來改善 KOReader plugin — AskGPT，讓它能夠提供摘要章節內容的功能。只給了一個 prompt，它就先去找文件來看，然後完成了開發。這點讓我蠻訝異的。一來 KOReader 只是個 github 上的專案，並不是什麼大案子，有很完整的 API 文件，二來，這只是個 plugin 的開發，相關的參考文件應該更少才對。但，它還是一次開發就順利能使用；只是…它用了超多的 token。雖然我的 OpenAI free token for gpt-5.1-codex 有 1m，想說很夠用的，但它硬是用了兩三百萬的 token 數，花了點我自己的儲值 (一兩美元左右)。\nfeat: support chapter summary · plateaukao/AskGPT@ac0951f\nCopilot Github Copilot 的 pro 帳號，是 Github 給 open source developer 的福利。以前還在自己寫 code 時，會拿來做 super powerful autocomplete 的工具，用得還蠻開心的。但當各種 CLI 工具出現時，Copilot 就被我冷落了感覺至少有一年吧。\n這兩天其他的 CLI 工具 quota 都用得差不多了，想說再回過頭來看一下 Copilot 是不是有點進步，發現它其實也有了 CLI 型式可以使用，於是馬上裝了起來。看來各家提供的功能都大同小異。\n雖然用的是 Copilot CLI，但是模型的選擇上，它也提供了 Claude 在用的 Sonnet / Opus 系列，Google 的 Gemini 3 pro。所以如果不喜歡 OpenAI 自家的模型表現，大可使用其他家的模型。不同模型會扣的點數也有些不同。\n現在 EinkBro 對於文字選取後的 AI 應用，已經提供了還算完整的設定彈性，讓使用者能夠自訂各種提示詞，以及使用不同的 AI 方案。不過，對於整份網頁內容的操作，現在只有提供 chat with web，得要進到另一個對話型式的分頁後，跟網頁內容對話才行。這樣子的操作，雖然很有彈性；但是對於常用的功能就很麻煩：比方說，我可能會需要請 AI 列出網頁中的重點，請它摘要；請它生成表格；或是做為一個外文老師，把網頁內容用教學的方式整理出來。\n發現 Copilot 有 CLI 模式後，我請它幫忙為 EinkBro 加上這個功能。為了達到這需求，它得要去修改 GptActionInfo，讓 genAI 行為能夠先指定該行為是文字選取的操作，還是是要針對網頁全文；另外，還得要去修改翻譯的結果視窗，以及整合既有的 chat with web 畫面。\n來回幾個 prompt 後，它就把這功能開發出來了。雖然過程中有幾個 import 該加沒加，我是手動自己加的，但除此之外，它已經完全能按照平常我開發的方式(參考之前的開發模式和寫法，找到需要調整或擴充的地方)把這功能加進來了。原來自己來要兩三個小時，或是一整個晚上的工夫，現在加上 UI 的微調，只需要十幾分鐘就可以完成。\nfeat: support page ai toolbar action · plateaukao/einkbro@cdc9f42\n後記 現在的開發漸漸變成，要自己先在腦海裡對於想要的需求有相對明確的想法，然後再給 AI 足夠明確的指示，讓它能逐步開發出來。實作的細節，如果能在一開始給它多點提示的話，它會比較快找到要著手的地方，省點 token 量，或是少花點 planning 的時間。\nAI 可以完成你要它做的功能，但功能要怎麼表現出來，怎樣用起來才順手，現階段我覺得還是要人為地去給意見，請 AI 修改。但我相信很快地，會變成 AI 思考後，給出幾個後續加強方案來讓我選吧。\n","permalink":"https://plateaukao.github.io/blog/posts/%E7%94%A8-ai-cli-%E9%96%8B%E7%99%BC%E7%9A%84%E4%B8%80%E4%BA%9B%E8%A8%98%E9%8C%84/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/933152a04882/1_vK2smFo5jOY-iHIGx_IM5g.png\"\u003e\u003c/p\u003e\n\u003cp\u003e從前年開始，就陸續開始用 copilot 和其他 AI 工具來開發 EinkBro。這幾個月來，就幾乎都是用 CLI 的工具在進行開發，不太使用 IDE 來自己寫 code，甚至連 code review 也沒有什麼在做了。\u003c/p\u003e","title":"用 AI CLI 開發的一些記錄"},{"content":"\n平常都是從陽光運動公園上彩虹橋，跑到碧潭，或是前往自來水園區的路線。今天，選擇了另一個方向：往永和中和的方向前進。中永和的河濱也是很多人跑的路段，不過今天人不多，可能因為是過年假期，大家都去南部或出國了吧。\n以前雖然有跑過這個路線，但大多是跑來回的路線，所以一次能逛的距離就 10k 左右。今天想說，懶得只跑 10k 就回頭，還得要遇到好幾個紅綠燈；既然進了中永和的河濱，應該可以一路跑到板橋去吧，只要半途不要爆掉就好。\n出門的時候已經太陽快下山了，也因此不用再曬太陽，但溫度又不會過低。大約有一個半小時是在天黑的情況下跑的，沿路都有很明亮的路燈，即使我沒有戴上眼鏡，也能夠很清楚的看到路況和路線；基本上就是沿著路燈跑，看是要跑靠馬路一點，或是沿著河岸邊跑。如果是選擇河岸邊的話，有時路線會比較繞一點。\n最終，在經過江子翠後，來到板橋的湳仔溝抽水站。到此，剛好完成一個半馬。一路上我吃了三個電解質錠，和一包迪卡儂的能量膠，確保我不會跑到一半就腳抽筋。\n下次應該可以嘗試再跑慢一點，然後把距離延長為 30k，這樣子應該有機會可以跑到樹林和土城。\n回程時，走了 2k 的馬路，抵達板橋捷運站，搭環狀線先到十四張站，再轉安坑輕軌回家。除了走這 2k 時，腳已經有點不聽使喚外，轉一趟捷運就能回到家，還算是方便，省下了要先開車出門停在某個停車場的困擾。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%B0%E5%BA%97%E5%88%B0%E6%9D%BF%E6%A9%8B%E7%9A%84%E5%8D%8A%E9%A6%AC/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/9b4cc32eb6a3/1_7mEPzlg_lZxDSMi04VIDnQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e平常都是從陽光運動公園上彩虹橋，跑到碧潭，或是前往自來水園區的路線。今天，選擇了另一個方向：往永和中和的方向前進。中永和的河濱也是很多人跑的路段，不過今天人不多，可能因為是過年假期，大家都去南部或出國了吧。\u003c/p\u003e","title":"新店到板橋的半馬"},{"content":"適當的跑步除了可以讓身體更健康外，還可以讓自己能夠探索更多地方。每年通常會有一到兩次到韓國出差的機會，雖然不是到首爾市區，但盆唐也還算是離首爾不太遠的衛星城市，尤其是從機場搭巴士的話，約一個小時多就可以抵達。在交通上算是很便利的。\n以前去出差時，都會沿著飯店旁的小溪(炭川 탄천) 跑跑步，或是到附近的盆唐中央公園逛一下。但因為當時的體力不足以及時間上的考量，所以僅能點到為止。最遠一次是有繞到四五公里外的盆唐貯水池，看起來很像一個人工湖。早期還有 bumgi jumping 的設施。\n這一趟有備而來，帶了平常在跑山的迪卡儂越野跑鞋。原先也是有想過要直接帶碳板鞋來的，但想到可能路面還有積雪，以及如果偶爾想要跑個山的話，應該還是帶越野跑鞋的適用度比較高。\n白天的盆唐貯水池路線 (上方) 抵達飯店後，天氣還很晴朗和明亮，所以先去跑了一趟。由於現在 10k 的距離是可以很輕鬆跑的範圍，所以一開始打定主意要再去一趟之前挑戰過的貯水池。這次省去了先繞一圈盆唐中央公園的距離，直接走比較直的路線前往。沿路除了遇到紅綠燈要等久一點之外，都還算輕鬆。微上坡對現在的我來說，不是什麼問題。\n半小時後抵達貯水池，發現整個水面有三分之二都結冰了，只剩下中間一小圈還是有流動的水。比較令我驚訝的是：貯水池週邊環境比上次來的狀態好太多了。以前比較像是很一般，沒什麼人在走的環湖道；現在則是完全公園化，除了池邊一整圈都是很平坦寬廣的散步道外，每一區延伸出去還有很多類似公園的設施和步道。\n原先打算繞一圈後再往回跑的，但看到貯水池左側也是山坡，而且有小溪再繞出去，就臨時變換路線，朝另一個方向跑。雖然沒繞水池一圈，但是看到了小溪裡特別的景色：溪水表面結了厚厚一層冰，但是下方的水流還是在流動的，所以可以同時看到流動中的水，以及上方厚度不一的冰層。這是在日本未曾見過的。\n晚上的盆唐貯水池路線 (下方) 有天晚上想要沿著飯店旁的溪邊跑步，朝南方的幾站地鐵方向前進。結果走偏了路，接到市區內的溪邊小路。跑了一陣子後，看地圖才發現再往前跑並不會接到任何地鐵站；不過，塞翁失馬焉知非福，從地圖裡也看出這條路竟然可以一路接到貯水池。相較於前幾天跑的大馬路得要不斷地等紅綠燈，這條路線完全不用停下來。中間還會經過盆唐中央公園，所以如果是白天的話，完全可以先切入中央公園繞一下，再繼續前進。\n晚上的河濱路線，除了會點上常見的路燈外，路面上也有會發光的小燈。這對於市區內的小溪來說，既可以照亮路線，又可以避免過亮造成週邊的光害。加上莫名的霧氣，整個跑步的氣氛很夢幻。\n但，夜晚的盆唐貯水池就沒有什麼可看性，一片烏漆嘛黑。由於進到貯水池和離開時都有點小坡，所以要很小心不要踩到地上的標線，不然很有可能會滑倒。\n靈長山 (영장산) 靈長山，海拔四百多公尺，從飯店出發只要五公里左右，是個適合上班前造訪一下的小山。但我還是高估了自己爬升的能力。原以為可以在一小時半左右來回的，結果發了快兩個小時才完成這 11.5 k 的路程。或許，零度上下的氣溫還是對心肺有很大的影響。\n除了一開始的一兩公里是在繞過 AK Plaza ，走過一小段的針樹林步道外，接下來的路線都是山坡路。\n山徑整理地很好，除了少數比較陡一點的路段會用木頭造出階梯外，其餘全是比較自然的泥土步道；落葉多一點的地方，也都會覆蓋著針葉或是大片落葉。落葉上有霜，陽光出現時，整個落葉會呈現白亮亮的感覺，很漂亮。\n山路上的路線標示也還算蠻明顯地，雖然我還是都開著 Garmin Explore，確認自己有走在它幫我建立好的路線上，避免自己走偏了，到時候回不了飯店，或是要花超出預期的精力回頭。\n出發前看網路上的照片，應該是可以在登頂後，看到盆唐市區的街景；但是因為我出發的時間很早，早上七點半約日出後就出門了，所以早上的霧氣(或雲？)一直還沒散去。當我到達山頂時，還是滿滿地雲層蓋在四週的山坡上。所以街景沒看到，倒是看到了雲海。能在海拔四百多公尺高的地方看到雲海，感覺這趟很值得。\n到達山頂時，時間剛好是一小時，約八點半左右。下山通常會比較快，能夠在一小時抵達飯店。心情上比較輕鬆。下山時，一邊注意著腳下的路線，一邊調整自己呼吸，讓自己下降的速度和步頻能盡量保持在穩定的狀態。上山途中有先吃了顆電解質錠，到山頂時開了一包能量膠，沒有一口氣吃完；在下山時邊跑邊吃，讓它可以為身體提供源源不絕的能量。\n到山腳時，太陽整個露臉，針葉林步道也比出發時還要更閃亮，有不少人牽著狗出來散步。\n能在出差地點不用搭車就能越野跑，真是太幸福了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9F%93%E5%9C%8B%E7%9B%86%E5%94%90%E5%8D%80%E6%9B%B8%E5%B3%B4%E7%AB%99%E9%80%B1%E9%82%8A%E8%B7%91%E6%AD%A5%E8%B7%AF%E7%B7%9A/","summary":"\u003cp\u003e適當的跑步除了可以讓身體更健康外，還可以讓自己能夠探索更多地方。每年通常會有一到兩次到韓國出差的機會，雖然不是到首爾市區，但盆唐也還算是離首爾不太遠的衛星城市，尤其是從機場搭巴士的話，約一個小時多就可以抵達。在交通上算是很便利的。\u003c/p\u003e","title":"韓國盆唐區書峴站週邊跑步路線"},{"content":"\n已經七八年沒有跑全馬了。從去年三月滑雪回來後，因為體重增加了快 5 公斤，所以又開始比較規律地跑步。除了週間會一到兩次跑後山，一趟 8 公里到 10 公里左右，爬升 300 ~ 350 公尺；再來就是週末常會和同學去跑唭哩岸站到陽明山 7–11 的路線，約 18 到 22 公里。\n本來跑步就是為了健康而已，所以沒有特別想要再去報名馬拉松。一來交通很麻煩，從新店安坑要到哪場馬拉松集合地點，都只能叫車過去，不然就是要提早去找車位，跟繳貴貴的市區內停車費，然後跑完後再用快跛腳的雙腿開車回家。\n不過，跑了快一年，還是想要知道一下自己目前的實力到底在哪，所以還是報了一場來看看。原先是想報台北馬的，可以跟同事一起，比較有伴；但是剛好 2025 台北馬的日期我已經有其他安排了，只好找接下來最近的賽事，也就是 2026 的渣打馬。聽說這場是大家在台北馬沒有抽到的情形下會來報名的賽事，相對上沒那麼熱門。(我也忘了是不是有抽籤，還是我報名就成功了)。\n全馬起跑時間是五點半(好早)，從總統府前出發，四點開始寄物，所以我三點四十就叫車從家裡出發。結果四點左右就到了，感覺下次四點十分再出門就行。因為一早天氣還很涼，大家都在衣保收集車附近休息等待，不想要先寄物，因為一要寄物就得把身上禦寒衣物脫掉，吹冷風。也因此，工作人員一直催大家趕快寄物，不然待會兒寄物得排隊，而且寄完衣保袋還要去上洗手間，再走到出發的拱門，可能要抓個 15 到 20 分鐘比較保險。\n起跑點等候區不大，看不出來可以塞到一兩萬人的參賽者。前面一區是全馬區，有人舉著完賽時間 4:00, 4:30 的牌子，但其實如果想的話，可以直接走到最前面，因為中間沒有圍起來，也沒有事前按照之前的比賽成績做區隔。全馬區的後方就是半馬的等候區，也是有人舉牌不同完賽時間；再更後面則是 11k。看來所有人的出發路線都是一樣的，起跑時間間隔 15 分鐘，老實說沒有很多。半馬菁英跑者應該很快就會趕上全馬尾部的跑者。\n果不其然，起跑後沒多久，就開始有摩托車從跑道左邊騎過去，要全馬跑者讓路，因為半馬跑者來了。好在大多數的跑道都還夠寬，大家往右邊站一點，空間不至於過擠。\n對於這次的路線，個人並沒有很喜歡。總統府出發後繞了一圈中正紀念堂後，再來是跑中山北路，在圓山附近，轉入河濱。再來…就全是河濱了！可以理解路權不好取得，但是，大部分都是在跑河濱的話，我自己跑就好了啊，何必付錢來跑平常就能跑的路線呢？好在最近有寒流來，天氣比較冷，又剛好今天有出太陽；以跑馬拉松的天氣來說，再好不過了，即使是跑河濱也很舒服。\n起跑後，自以為有了很充足的練習，想要嘗試 4 小時列車。剛開始很順利，一路跟了 16 公里。然後…就覺得大腿有點緊，似乎再以 5:40 分速跑下去的話，很快就會抽筋。無奈之下，當機立斷，自行減速，變成輕鬆跑。畢竟，沒有抽筋順利完賽比什麼都還來得重要。一開始先以 6 分速跑，但到 20k 時，發現還是太快。應該是前面的 16k 太快了，雙腿累積疲勞過多。只好再降速，一直到 7 分半左右，腳才比較能持續地運作。\n20 幾公里時，由於能量補給沒有到位，左腳有點沒力，右腳腳底板又很不舒服。這次穿的是前一陣子在迪卡儂買的碳板鞋。在家附近練習時，感受到它的 Q 軟和威力，直接讓我平常訓練的均速可以提升 30 秒到 1 分鐘。但是，用它跑長距離的次數太少了，拿來跑全馬時，才發現我跟它還不夠熟。過了 20k 那些不合的地方不斷被放大，造成大腿內側肌肉不適，以及右腿腳底板不舒服。這時腦中已經在想像，啊，這次難不成要上回收車了嗎？還有 20k 呢。\n過了七八年，現在跑馬有各式各樣的濃縮補給可以吃，有的防抽筋，有的及時補充能量，每樣的包裝上都寫得洋洋灑灑。昨天我特地去全家買了一點能量膠和電解質錠保平安。這趟就依里程吃好吃滿。20幾k 時的不適，減速下來跑一陣子後，竟然就緩和了許多。看來是補給發揮效用了。\n從 21 k 之後就幾乎都是以 7 分半速到 8 分速左右在跑。偶爾覺得累了，或是進了水站，會吃一小段香蕉，喝點運動飲料，然後走一小段，再起跑。就這麼慢慢地把後半馬給跑完了。\n這次的渣打馬讓我很訝異的一點是：一直到前半馬跑完，幾乎沒有步兵。即使是後半馬，用走的人也是很少數。大部分的跑者都是用跑的，或快或慢，朝著終點前進。只有到最後兩三公里才看到比較多在用走的。感覺實力比很久以前我的印象好很多。\n另外，不知道是不是因為路線大多在河濱，這次幾乎沒有看到什麼 cosplay 的跑者。太久沒參加全馬了，不曉得其他國內馬拉松是不是也這樣。想當初在大阪馬和京都馬，超多各種造型的跑者，光是欣賞和拍攝這些跑者的裝扮就可以分心很長一段時間，不用一直專注在里程上。\n河濱很 relax，太陽很溫暖，又不會熱。雖然雙腿已經快要跑不動了，最後半馬花了三小時，但過程都很舒適。補給也很恰當，不會有後段就幾乎沒有東西吃沒有水喝的情況發生。\n倒是有一點要提一下：進終點拱門時，全馬的計時器竟然是暗的。也就是說，如果想看官方當下的時間，是看不到的；拍拱門照也看不到自己的時間；而一旁的半馬計時器是正常的，所以會看到一個慢了 15 分鐘的時間。(因為半馬晚 15 分才出發)。一開始我只看到那時間，還以為我跑得還可以，結果再加上 15 分鐘就差別很大。看了一下網上別人發的照片，好像也都有注意到。對於一個幾千人參加的全馬來說，就算當下有問題，應該也要儘快修正吧，怎麼會放給它一路壞到底。\n總的來說，拜好天氣之賜，這次全馬的體驗還不錯，知道自己長距離還有很多要努力的地方。知道剛起步時不要硬跟 pacer，因為一旦中途發現自己跟不上，想要再慢下來也太晚了。\n這是 2026 年第一馬。應該…不會再報下一馬了吧，希望。\n","permalink":"https://plateaukao.github.io/blog/posts/2026-%E5%8F%B0%E5%8C%97%E6%B8%A3%E6%89%93%E9%A6%AC%E5%85%A8%E9%A6%AC%E5%AE%8C%E8%B3%BD%E6%84%9F%E6%83%B3/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/cda70cf01062/1_GMzjIefT8PvNeZfzmSGkmA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e已經七八年沒有跑全馬了。從去年三月滑雪回來後，因為體重增加了快 5 公斤，所以又開始比較規律地跑步。除了週間會一到兩次跑後山，一趟 8 公里到 10 公里左右，爬升 300 ~ 350 公尺；再來就是週末常會和同學去跑唭哩岸站到陽明山 7–11 的路線，約 18 到 22 公里。\u003c/p\u003e","title":"2026 台北渣打馬全馬完賽感想"},{"content":" 撒哈啦沙漠\n運動 今年三月滑雪回來後，體重直線上升到 58 公斤，嚇了我一大跳。這是長這麼大以來，第一次體重超過當兵時的 56 公斤。想說應該要好好地來運動減肥一下了，所以開始比較規律地跑步。週末也常和同學去跑淡水河濱、北投，或是陽明山。\n15 公里以上的距離，跑了 21 次；爬升 300 公尺以上的則有 41 次；總距離來到了前所未有的 1210 公里，總爬升高度 30,015 公尺。\n目前有報了 2026 年一月初的渣打馬，不期待可以破 PB，只求能順順地完賽。目前覺得上山跑越野還蠻有趣的。希望能長長久久地跑下去就好。\n打網球的頻率因為週末跑步，以及很久沒去公司的社團活動，而少了許多；但還是有在練習。\n四月時有跟同事參加國際龍舟賽，差一點就冠軍了 (其實差很多)。\n生活 一月北海道滑雪，過年去清邁，二三月青森滑雪，六月去函館跑山，七月韓國首爾出差，八月去胡志明市逛逛，十月去摩洛哥十幾天，十二月又再回到北海道。不知不覺中，莫名地買了許多機票；然後，又回復了單身。\n北海道\n清邁\n青森\n函館\n首爾\n胡志明市\n摩洛哥\n十月從摩洛哥回來後，有認真的打掃家裡，貫徹用不到的物品就送走或丟掉，很久沒穿的衣服也都洗一洗送去回收。目前家裡又回復十年前剛拿回來住的樣子了，很空曠，但也很乾淨。\n工作 今年的工作量大增，去年底新接下來的案子，一路忙到今年九月十月才輕鬆一點；四月後因為工作性質的小調整，除了帶的人變多之外，還多了另外兩個案子。一次同時在這麼多案子間來回，就算再會 context switching 還是覺得很累。\n除了正常工作外，從去年十月起，參與了一個培訓課程，每週都得上課，而且還時不時有作業，跟不同國家的同事組成小隊，經常得討論作業。整個課程一直到今年七月才結束，拿了個書卷獎?(Academic Exellence Award)。重溫當學生的感覺，跟同學培養革命情感。跟學生時代最大的差別是：白天還是得上班啊！七月結束時，有種鬆了一口氣的感覺。現在學習也很幸福，只要有問題，隨時都可以請教 AI，所以就算學習時聽不懂，請 AI 多解釋幾次就可以懂。\nEinkBro \u0026amp; Github \u0026amp; Side projects 自從 ChatGPT 橫空出世後，寫文章的頻率就愈來愈低。到了 2025 年，已經幾乎都沒有在寫技術類文章，因為…寫出來的程式大多是問 gpt 或是 claude 就得到的答案，既不是我想出來的，也不是我自己實際寫出來的。如果真的忘了當初是怎麼實作的，可能再問一下 AI 又可以得到類似的答案；即使我花時間記錄下來，可能也不會有人來看。因為，同樣的問題別人也一樣可以在 AI 那兒快速地得到答案。\n現在，能寫的文章應該只剩下不太專業的跑步登山記綠，以及整年度的回顧了吧。明天，就是今年最後一天上班，是時候開始來整理一下今年的 achievement 了。\n2025 年的 EinkBro release 次數來到歷史新低，只有 13 次；而 commit 數則是 122 次，只算得上是去年的 413 次的 1/4 強。一方面大部分電子書上需要的功能都開發得差不多了；二來，是我真的也想不到還有什麼功能可以做了。\n雖然 release 的次數很少，但今年還是有幾個不錯功能發布，包括叫 AI 寫出來的 chat with web，長按連結滑動至功能選單，以及支援 Instapaper 儲存文章功能。\nSide project 數目也是少得可憐，只有無意中發現了對於無蝦米支援超好的 fcitx5-android 專案，把它 clone 下來硬改成適合在 Eink 設備上顯示的介面。\nAdd option to draw key border stroke instead of shadow by plateaukao · Pull Request #799 ·…\nGithub 星星數只比去年多了快 1000 顆，來到 2107。以今年這麼少付出，還持續有在成長，算是很不錯的了。\n閱讀 竟然有看超過 50 本書。沒看完的還都沒算進來。\n從年初開始算的話，以下是讀過的清單：\n重啟人生\n太平島16小時\n黑色行動 台北 16 小時\n暴雪圈\n不當世界的局外人\n阿拉斯加韓醫院\n輝達之道\n師父\n神鬼當舖 2\n日本雜學圖鑑\n世界上最透明的故事\n贖罪奏鳴曲\n追憶夜想曲\n恩仇鎮魂曲\n殺戮狂詩曲\n我的印度歲月\nOut of the Silence\nChop Fry Watch Learn\nST 警視廳科學特搜班 青\nST 警視廳科學特搜班 紅\nST 警視廳科學特搜班 黃\n無限之網 草間彌生自傳\nST 警視廳科學特搜班 綠\nHumble Pi When Math Goes Wrong in the Real World\n海海人生\n不正常人生超展開\nLa Boulangerie de la plage\n巫醫動物與我\nApple in China\n眾神之島\n眾神之島 2\n沒有媽媽的超市\nのび太の海底鬼岩城\n直接證懼\n哈利波特 神秘的魔法石\nThe Boba Book\n我獨自升級 1~5\n摩洛哥古城沙漠聖羅蘭\n馬拉喀什漫步\n日落之邦摩洛哥\n撒哈拉的故事\n跑步聖經\n台灣漫遊錄\n不懂權謀的人無法做大事\n在巴黎的晃遊貓\n我獨自升級 6~8\n小梅好想詛咒\n一步一腳印 邁向永續路\n玫瑰與龜殼琴\n開運巡禮 東京社寺\n小家的幸福好品味\n借來的 100 天\n中東的美麗與哀愁 以色列 約旦 巴勒斯垣\n","permalink":"https://plateaukao.github.io/blog/posts/2025-%E5%B9%B4%E5%9B%9E%E9%A1%A7/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/28a68748a1ff/1_DMhZrB0gCtZnL1GuZVqbDQ.png\"\u003e\n\u003cem\u003e撒哈啦沙漠\u003c/em\u003e\u003c/p\u003e\n\u003ch4 id=\"運動\"\u003e運動\u003c/h4\u003e\n\u003cp\u003e今年三月滑雪回來後，體重直線上升到 58 公斤，嚇了我一大跳。這是長這麼大以來，第一次體重超過當兵時的 56 公斤。想說應該要好好地來運動減肥一下了，所以開始比較規律地跑步。週末也常和同學去跑淡水河濱、北投，或是陽明山。\u003c/p\u003e","title":"2025 年回顧"},{"content":"\n從黎明路出發，經過車子路來到祥和路，從祥和路 2 號的路牌走進去。再來，要記得走沒幾步路的叉路要左轉，不要像我一樣往右邊走去，走了好一陣子才遇到住家跟我說前面沒路啦。\n明明裝了 garmin 的路線，但看起來似乎偏離路線時，看起來沒有很明顯，也沒有收到很明確的警示，倒是後來回到路線上時，才有看到提示。\n叉路左轉後，繞著一棟鐵皮屋右側走，就開始會有上山的路徑。出發時間大約是六點多接近七點，太陽是一小時前日出的，這段路線至少我在走的一兩個小時內，幾乎是都沒有日曬，很舒服。\n上山時，還是有一兩條小叉路要稍微注意一下，不然還是有機會走偏的。大概海拔到 100 多公尺後，才陸續有路標出現，之後幾乎就可以看著路標來行走。可能因為從祥和路上去並不是一條主要的路徑吧。一旦快到觀山亭和光明頂，路上的指標就愈來愈清楚，每個路口都會說明是前往哪裡和需要多少路程。\n觀山亭的風景很好，可以看到新店的山巒起伏，太陽還沒升到高空時，陽光會從每座山的後面努力跑出來，山的輪廓也因此變得很曚曨。在這兒有觀景台，也有休憩的亭子，是個可以先駐足稍作休息的好地方。\n光明頂是最高點，到這兒以後，後續的路程就沒有什麼太多的上升路段。這裡的視野沒有觀山亭好，所以拍拍到此一遊的照片後，就可以再往外挖子山前進。\n接下來的路線就很舒服，有蠻多土路可以跑的，雖然都是在山稜跑，但沿路都有樹蔭遮陽，即使是正中午跑也不會太熱。一路往下跑到馬路時，過個馬路就能再上外挖子山。外挖子山的海拔比較低，階梯也少了許多，能輕鬆上下一趟。\n整趟路線大約 5 k，中等速度跑的話，應該一小時到一個半小時能完成，算是一條很親民的越野跑路線。下山後附近有新和國小輕軌站，如果懶得再跑太遠的話，捷運也很方便。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%AE%89%E5%9D%91-%E6%96%B0%E5%BA%97%E5%85%89%E6%98%8E%E9%A0%82-%E5%A4%96%E6%8C%96%E5%AD%90%E5%B1%B1/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/932f66e7e192/1_y3d0BRaIOszdbla6Fh2vFQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e從黎明路出發，經過車子路來到祥和路，從祥和路 2 號的路牌走進去。再來，要記得走沒幾步路的叉路要左轉，不要像我一樣往右邊走去，走了好一陣子才遇到住家跟我說前面沒路啦。\u003c/p\u003e","title":"安坑 -\u003e 新店光明頂 -\u003e 外挖子山"},{"content":" https://www.strava.com/activities/14951925853\n舊登山道路線 + 下山車用道路\n函館山不高，主要的”後殿山”只有 334 公尺高；但是登山路線卻規劃有從A到K，11條之多。這次在函館待了好幾天，為的就是把所有路線跑過一輪(其實是臨時決定要這麼做的)。\n經過三四天閒逛後，只差F路線 (Ezodate) 沒有跑到；因為E路線(宮之森) 實在是太讚了，連兩天跑，中途捨不得叉開去跑 F路線。\n這麼多路線中，A舊登山路線應該是最受歡迎的，走(跑)起來也最輕鬆，步道很大條，而且彎來彎去地上山，坡度相對上比較平緩。上山後能夠來到纜車的終點展望台。在展望台拍夜景雖然很好，但…下山的柏油道路竟然是沒有路燈的！還好我有遠見，上山時有帶了頭燈，不然真的無法摸黑下山。\n搭新幹線北斗8號到達函館市時，已經是下午的事，因為剛好是舉辦函館馬拉松的日子，路上還有許多剛剛跑完的跑者，很多路標也都還在。在房間先休息到f四點，才出發去函館山。今天也是少數天氣還不錯的日子，稍微看得到藍天。直接從飯店出發，直接跑到函館山上山的入口。走的是舊登山道，路蠻寬的，短短的兩公里，只花了不到三十分鐘登頂，不論是用走的還是跑的都很b舒服。路上遇到的登山客並不多，幾乎快一個人獨享整條登山道。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8C%97%E6%B5%B7%E9%81%93%E5%87%BD%E9%A4%A8%E5%B1%B1-%E7%B3%BB%E5%88%97%E4%B8%80/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/66d23cf5df36/1_g1iimATIdamtZHSXZwQp8Q.png\"\u003e\n\u003cem\u003e\u003ca href=\"https://www.strava.com/activities/14951925853\"\u003ehttps://www.strava.com/activities/14951925853\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e舊登山道路線 + 下山車用道路\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e函館山不高，主要的”後殿山”只有 334 公尺高；但是登山路線卻規劃有從A到K，11條之多。這次在函館待了好幾天，為的就是把所有路線跑過一輪(其實是臨時決定要這麼做的)。\u003c/p\u003e","title":"北海道函館山 系列一"},{"content":"\n從唭哩岸捷運站出發，經過榮總後，左轉，開始上山。約五公里後，可以到達敦敘高工。再來，有兩條路線選擇：一條是接著直走，先下泉源路，轉接到坡度較陡的東昇路；另一條是右轉，緩緩上升到文化大學。\n一開始嘗試的都是走文化大學那一條路。雖然相對上比較好走，但對於還沒跑過多少次山路的我來說，還是花了點時間在適應。從以前一上到文化大學就快要用光所有力氣，到現在走這條路線上坡到文化大學都還老神在在的差別，可以看得出來自己這陣子以來的進步。\n東昇路是後來新開發出來的路線。它的坡度陡，相對地，可以以更短的距離到達山頂。這裡的山頂指的是陽明山花鐘，或是溫泉旁的 Seven。在進到東昇路前的泉源路，路旁有水量很大的水溝(或小溪)，而且這條路線有兩三個瀑布或小溪流可以觀賞，在跑步時比較有趣，可以視情況轉換一下心情。\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%99%BD%E6%98%8E%E5%B1%B1%E8%B7%91%E6%AD%A5%E8%B7%AF%E7%B7%9A/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8650967e492e/1_q_PCvk9MMZXAnGo2GPxv7w.png\"\u003e\u003c/p\u003e\n\u003cp\u003e從唭哩岸捷運站出發，經過榮總後，左轉，開始上山。約五公里後，可以到達敦敘高工。再來，有兩條路線選擇：一條是接著直走，先下泉源路，轉接到坡度較陡的東昇路；另一條是右轉，緩緩上升到文化大學。\u003c/p\u003e","title":"陽明山跑步路線"},{"content":"買來這台漢王的Clear 7 Turbo 已經幾週了。到目前為止，還算是滿意。比類似的掌閱產品線來說，漢王帶給我的可用性更高。以下就是我很個人的使用心得分享。\n硬體 重量跟掌閱的 Ocean 系列差不多，也是主打輕薄；而且，一樣有人體工學設計，閱讀器側重於機身的一邊，並且帶有硬體的按鍵。除了有兩顆用來翻頁的按鈕外，還多了一顆可以用來自定義的按鍵。預設是用來回到前一個畫面，但可以自行更改成其他用途。另外，長按時的功能也是可以自定的。硬要挑點毛病的話…它的機身比較方正，邊角沒有做圓狐處理，當單手手持時，手掌心會沒有那麼舒服。單純只是爽度沒那麼高而已，但算不上是缺點。\n再來要特別稱讚的是：終於有這類輕薄的閱讀器換上了比較高規格的CPU了。雖然跟手機用的還是天差地遠，但至少漢王的用料比掌閱好，不能是在開啟App或是畫面切換上，都流暢許多。這種操作直覺上的差別很重要，這也是主要為什麼我對它評價比較好的主要原因之一。\n軟體 系統下拉選單很中規中矩，跟別家都差不多；比較不一樣的是：它多了一個滑動翻頁的功能，而且是真正的翻頁，而不是像文石那種只是呼叫系統scroll的半殘翻頁。雖然它看起來只是把中間的動畫效果停掉，還是需要等那麼一下下，但至少畫面不會不斷地更新。這功能在一些沒有為eink設備最佳化的App中，還蠻好用的。\n系統的設定畫面蠻簡潔的，在畫面上方有類似 tab 的設計，裡面的調整也都很清楚。\n內建的書庫 漢王家的閱讀器，買來的時候就附了1300本書。很多是經典的書，但也有不少是比較近期市面上才在賣的書。如果對於這些書有些興趣的話，那其實整台設備扣掉書的錢，所花費的成本是更低的。\n比掌閱好上很多截的 WebView 版本 這點也是在比較後才感受到原來差別這麼大。之前在用掌閱閱讀器時，因為當時是心頭好，最常用的方式就是拿它來看 Hyread App 中線上圖書館借來的書。但不知道是因為系統的 WebView 版本太舊還是掌閱胡亂優化做過了頭，在選取文字時，常常會造成系統沒有反應；不然就是選取完文字後，老是點不到想要的標記按鈕。我一直以為是 Hyread App 的問題，還寫了篇 post 在抱怨。但是，在 Clear 7 Turbo 中使用 Hyread App 卻出乎意料，什麼問題也沒有！而且不論是在下載書或是畫面切換上，都迅速很多。真的是不用不知道，用了嚇一跳。好在有買了漢王的閱讀器，不然我還真不知要一直錯怪 Hyread 到什麼時候。\n適度的刷新模式調整選項\n電力 普通。有開 Wifi 的情況下，感覺是一小時會掉 10 %。但看在它的操作速度那麼流暢，吃電一點是 ok 的。關掉 Wifi 只拿它來閱讀的話，應該是不需要擔心電力會下降太多。但我很少這麼使用就是了，常常會想要再開個網頁來東逛西逛的。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%BC%A2%E7%8E%8B-clear-7-turbo/","summary":"\u003cp\u003e買來這台漢王的Clear 7 Turbo 已經幾週了。到目前為止，還算是滿意。比類似的掌閱產品線來說，漢王帶給我的可用性更高。以下就是我很個人的使用心得分享。\u003c/p\u003e","title":"漢王 Clear 7 Turbo"},{"content":"其實應該也沒有什麼心得可以寫的，因為在購買 BOOX Go 6 白色版之前，我已經有同樣尺寸的 Poke 4s，以及也是前一陣子推出的 BOOX Go 7 Color 白色版。\n硬體 相較於 Poke4s 來說，這版是很大幅度的升級。螢幕同樣是 6 吋大小，但 Go 6 的整體體積又小了一圈：長度由原先的 153mm 縮短到 148mm，厚度也由原先的 7.10mm 再薄了一些，來到 6.8mm。其實都已經是很輕薄的機型了。重量也再輕了個幾克，由原本的 150g 縮減到 146g。能夠拿著一台不到 150 g 的閱讀器，就算是手機也已經很少有這麼輕的機種了。\n儘管在體積上做了縮減，但電池一樣是 1500mAh，然後 CPU 升級了！由原本爛爛的 2.0GHz Qualcomm ARM Cortex-A53 4-Core 換成了8 核 662。應該效能要更好才對，我還在體會中。\n畫面升級到 300ppi，比 poke4s 的 212ppi 要細緻很多。小字也可以看得很清楚了，雖然有了老花之後，都會習慣性地把字體放大一點來看。\n皮套 附的皮套款式跟 BOOX Go 7 Color 一樣，把閱讀器前後夾起來的磁吸設計，很容易搞混哪面是有裝磁鐵的。而且沒有做成可折疊的書架型式，只能在需要的時候，拿其他的小架子來用。目前最滿意的還是掌閱 iReader 的皮套，除了後端部分透過磁吸黏住，正面是可折疊式的設計。重點是還做得很輕薄，不會因此增加太多重量。Go 6 的皮套只能算得上是中規中矩的。\n軟體 因為使用的 CPU 的關係，這台不需要再經過什麼複雜的 Google 帳號認證，拿到設備快速開機進到 Home 之後，就可以開啟 Google Play Store 開始安裝自己常用的軟體。\n我的安裝順序通常是：\n用內建的瀏覽器把 EinkBro 裝好 利用 EinkBro 安裝無蝦米輸入法需要的 sweetlime 利用 EinkBro 安裝 KOReader 利用 EinkBro 把我別台閱讀器上的 EinkBro 備份檔傳過來，還原到 EinkBro 中 利用 EinkBro 把我別台閱讀器上的 sweetlime 備份檔傳過來，還原到萊姆輸入法中。 再來就是安裝其他家書城的 app，像是 Kindle, Kobo, Hyread 等。 最後，再連上電腦，把 KOReader 需要的 plugin 和手勢設定也都傳進來，省下還要全部重新設定一次的麻煩。 ","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3-boox-go-6-%E7%99%BD%E8%89%B2%E7%89%88%E9%96%8B%E7%AE%B1%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e其實應該也沒有什麼心得可以寫的，因為在購買 BOOX Go 6 白色版之前，我已經有同樣尺寸的 Poke 4s，以及也是前一陣子推出的 BOOX Go 7 Color 白色版。\u003c/p\u003e\n\u003ch4 id=\"硬體\"\u003e硬體\u003c/h4\u003e\n\u003cp\u003e相較於 Poke4s 來說，這版是很大幅度的升級。螢幕同樣是 6 吋大小，但 Go 6 的整體體積又小了一圈：長度由原先的 153mm 縮短到 148mm，厚度也由原先的 7.10mm 再薄了一些，來到 6.8mm。其實都已經是很輕薄的機型了。重量也再輕了個幾克，由原本的 150g 縮減到 146g。能夠拿著一台不到 150 g 的閱讀器，就算是手機也已經很少有這麼輕的機種了。\u003c/p\u003e","title":"文石 BOOX Go 6 白色版開箱心得"},{"content":" 這篇文章將分享在 KOReader 中怎麼修改 epub 中的 css style，讓閱讀體驗可以有更多樣的應用場景。\n其實 KOReader 一次都很強大，只是我又再次感受到它的可調整性，想要來跟大家分享一下。這次是認真的研究了一下在文件的設定中，可以指定使用者自行加入的 css 樣式表，用來調整 epub 電子書的呈現方式。\n會研究這個功能的原因是：之前自己曾寫了一個小 script，可以把 iTHome 的鐵人賽參賽系列文章轉換成電子書；而這些電子書因為原本網頁的設計，背景並不是全白的，而是帶點淡淡顏色的灰色。\n這背景色在電腦上看，不會有什麼不舒服的地方；但在電子書閱讀器上就會很明顯地讓畫面背景和文字的對比不夠高。在 KOReader 又因為是整頁背景色，在切換頁面時，都會重繪整個畫面。\n為了解決這問題，一個最根治的方式是：把我已經轉換好的每本 iTHome 鐵人賽主題電子書檔案中的每個 html 的背景，再強制設定為透明的。這方式曠日費時。\n另一個方式則是亡羊補牢，只需要在 KOReader 中針對這些電子書將其背景設定為透明即可。稍微研究了一下，要讓畫面背景變為透明的話，可以用下面的 css 設定\nbody { background-color: transparent !important; } 將它寫入一個 transparent_background.css，再放到 koreader 目錄下的 styletweaks 目錄即可。它會出現在 KOReader 文件設定 \u0026gt; 樣式表調整 \u0026gt; 使用者樣式表調整中。\n—\n同樣地，想到之前曾經開發了製作雙語字幕電子書的 calibre plugin，製作了許多影集的字幕書，如果想要將其中的中文(第二個語言)翻譯部分給隱藏的話，也可以為它建立一個 css 檔案，並放入以下的 css:\ndiv[class=\u0026#34;sub\u0026#34;] {display: none !important;} 隱藏的話，會造成整份文件的位置改變，效果其實不是很好。另一個嘗試是：把第二個字幕語言的文字顏色改為透明的。如此一來，保留了第二字幕所佔的空間，只是字都看不到了。這麼一來，整份文件的相對位置就不會因此而改變。\nbody { background-color: transparent !important; } 加入到使用者樣式表後，還可以長按這些項目，點擊加入功能列表中。這麼一來，在設定手勢時，就可以將部分手勢指定為這些 css style change 的開關，省下需要層層進入選單的麻煩。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%AE%93-koreader-%E5%86%8D%E6%AC%A1%E5%81%89%E5%A4%A7/","summary":"\u003cblockquote\u003e\n\u003cp\u003e這篇文章將分享在 KOReader 中怎麼修改 epub 中的 css style，讓閱讀體驗可以有更多樣的應用場景。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e其實 KOReader 一次都很強大，只是我又再次感受到它的可調整性，想要來跟大家分享一下。這次是認真的研究了一下在文件的設定中，可以指定使用者自行加入的 css 樣式表，用來調整 epub 電子書的呈現方式。\u003c/p\u003e","title":"讓 KOReader 再次偉大"},{"content":"工作 2024 年是比較特別的一年。工作上，多帶領了一個團隊，雖然這一年我還是沒有好好地把相關的技術學習起來；也由於團隊有部分同事被調派去支援其他的專案，預見未來會有愈來愈多類似的調度，在今年度開始讓大家比較有系統地學習其他領域的知識。\n不知為何，下半年度又突然冒出很多新的專案，讓我開始感覺到時間管理的重要性。\nEinkBro \u0026amp; Coding 自從去年出書後，今年一度感覺沒有什麼新功能可以加了，也就忙著其他的事情。但從八月後，又找到了可以開發的功能，瘋狂新增程式碼。\n一些開發的數據：\nRelease 次數： 26 次。 11.2.0 ~ 11.17.0 ~ 14.1.0 ，跟去年持平。 commit 數： 413 次，竟然比去年還要多。果然少了寫書這件苦差事，寫程式的次數就增加了。 git rev-list --count HEAD --since=\u0026#34;Jan 1 2024\u0026#34; 星星數：根據我的觀察，當 Github 星星超過 1000 後，可能權重有變得比較高，很有感地發現星星增加的速率變快了。從去年的 777，到今年年中破千，一直現在 (2024/12/16) 已經漲到 1267 ，幾乎要是前兩三年的總和了。 技術演講：還是掛零。 技術文章分享：19篇！今年那麼忙碌，竟然還寫了 19 篇文章。雖說是分享，而且水分很高，但真正有在看的人應該很少，主要還是自己留個記錄而已。很多文章內容充其量也只是寫個自己看。其他人遇到類似的問題，應該也跟我一樣，直接問一下 ChatGPT 就解決了。 * 改善 EinkBro 中的直排閱讀體驗 * 升級 EinkBro 的廣告阻擋機制 * 改善 EinkBro 的工具列設定畫面 * 整合 Edge TTS 文字轉語音 * EinkBro 的更新小記: v11.16.0 \u0026amp; v11.17.0 * EinkBro 六月以來的一些更新 * EinkBro: 支援 Gemini API * SONY DPT-CP1: 加上 KOReader 啟動圖示及安裝 AssistiveTouch, 萊姆輸入法 * root SONY DPT-CP1 並且安裝 KOReader App * 擴充 AskGPT KOReader plugin: 新增一鍵加入筆記 * 擴充 AskGPT KOReader plugin * JB Kindle Voyage 和 AskGPT plugin for KOReader * 自製偽直排字型 * Koreader 的直排閱讀 patch * Koreader 中快速切換成直排閱讀模式 * 華為 Matepad Paper 快刷 App * 自製 iThome 鐵人賽參賽主題電子書 * 在 MacOS 上跑 llama.cpp server 並使用 Mixtral-8x7B LLM Model * EinkBro App 被 Google Play Store suspend 新增的 Side Projects AskGPT plugin for KOReader (link)\n延續原先作者的架構，加入了許多符合我自己需求的功能：支援 Gemini API，支援根據前後文翻譯選取的文字，支援將 LLM 回覆的內容以及選取文字存到筆記中。\nvertical_read patch for KOReader (link)\n將 KOReader 作者之一的小作品 patch 做更多的衍伸，讓直排閱讀在 KOReader 中更加便利：能夠在選單中快速啟動直排，也能透過手勢指定直排；還可以快速地切換選取文字直接標記或跳出動作對話框。\ntategakifont (link)\n配合上述的直排功能，此 repo 是可以將字型轉成旋轉 90 度的字體。因為要處理一些中日文的標點符號顯示方式，所以我有 fork 這個 repo 回來做調整。\nBoox Rapid Draw (link)\n有神人利用文石提供的 sdk 開發了能在文石閱讀器上，任何 app 中提筆就寫的小工具，讓許多原先手寫會很遲緩的軟體，能夠有接近原生 app 一樣的書寫體驗。\n我有協助幫它瘦身，並且改了點小邏輯。\nAdblockAndroid (link)\n這是三年多前別人針對 Android WebView，參考 Brave Browser 開源的 adblock repo 寫的防廣告函式庫。因為已事隔三年，當初使用的函式庫都已經過時，所以我花了點時間讓它可以在現在的各種開發版本上運行。為了取得最佳的執行速度，它底層用的是 C++。一方面是程式碼比較不好閱讀，二來，編譯後會產生各個不同 CPU model 的 binary 函式庫檔案；跟一般寫 Android app 的功能有些不同。\n當能成功編譯和運作後，我花了一兩個月的時間，把它整合進了 EinkBro。程式因此大了 1MB多，但是對於廣告阻擋的效能和效果都比以前好非常多。\nAssistiveTouch (link)\n在 Pubook 上為了可以有個比系統內建的 navigation top bar 還要方便使用的導航方式，在網路上找到了這個小工具，把這個浮動按鈕改成了我想要的樣子。現在除了在 Pubook SE 上有安裝外，在 SONY DPT-CP1 中也扮演了在不同 app 中切換的重要功能。\nithome_ironman_crawler (link)\n這兩年雖然不再參加 iTHome 的鐵人賽，但還是會找每年有得獎的優秀作品來拜讀。為了不想要一直連到網路上閱讀，以及得要很不方便地去按下一篇的按鈕，我寫了個爬蟲可以把指定的鐵人賽作品一直下載下來。光是下載下來，在閱讀上還是很不方便；所以有再利用 Calibre 提供的相關 python module，把這些下載下來的文章(含圖片) 轉成了一本電子書。\n經過不斷改良，現在可以一鍵產生指定鐵人賽作品的電子書。自己用它建了不少的電子書。\nLight Android Launcher (link)\n也是個年久失修的小程式。我把它相關的函式庫升級，讓它可以正常地在我的 Hisense A7 中運作。\nFluxAnchor\n為了解決每天通勤，沒有適合自己聽的 podcast 內容，利用 Miniflux 專案提供的 python library，搭配免費的 Google Gemini API，以及 MS 的 edge-tts 模組，可以每天定時產生我想要的語音檔，讓我在開車時能聽一些自己感興趣的新聞或文章內容。\n運動 除了持續在打網球外，也比較有在跑步了。後來似乎買了新的跑鞋後，跑起步來就沒有什麼太大的問題。也不再有之前偶爾腳底板會很痛的問題發生。九月去日本出差時，還順便花了四個早上，撿了個馬拉松的距離回來。\n網球則是今年下半年有開始認真的學習發球。明明知道自己的動作哪裡有缺點，但還是要透過不斷地練，糾正動作才行。現在的發球不再那麼軟趴趴了，但整體的攻擊能力還是有待加強。\n生活 為了讓家人能更輕鬆的進出家門，在年初時跟家人表示，可以考慮買間有電梯的房子；這麼一來，就不用在辛苦的上下樓梯。從找房看房，一直到透過許多貴人買了間雖然不大，但交通還算方便的房子。從八九月到十一二月都在忙著辦理貸款，交屋，裝潢，買傢俱，和搬東西。原來要搬個家是這麼累人的，即使要搬的不是我自己。\n閱讀 今年的閱讀量應該沒有比去年少，但是做記錄的心態比去年差，所以有記錄下來的書並不多。很多書看了，就過了，沒特別再寫點心得。到後面一兩個月則是改成將書的封面貼到 threads 上留個記錄而已，沒有寫什麼感想。\n年初應該是有把今野敏的隱蔽搜查系列全看完了。除了前五六集是看中文的，後面的四五集就全是看原文小說。很少有花這麼多時間在看原文小說的。\n在年初開發的 AskEinkBro 和 AskGPT 的協助下，就算遇到看不懂的日文用法或句子，95 成以上都能夠藉由 ChatGPT 或是 Gemini 得到很滿意的答案。\n明年應該還是要老老實實地記錄一下才行。\n其他有看過的書可以在我的 threads 上看到\nhttps://www.threads.net/@d4ni3lk4o\n","permalink":"https://plateaukao.github.io/blog/posts/2024-%E5%B9%B4%E5%9B%9E%E9%A1%A7/","summary":"\u003ch4 id=\"工作\"\u003e工作\u003c/h4\u003e\n\u003cp\u003e2024 年是比較特別的一年。工作上，多帶領了一個團隊，雖然這一年我還是沒有好好地把相關的技術學習起來；也由於團隊有部分同事被調派去支援其他的專案，預見未來會有愈來愈多類似的調度，在今年度開始讓大家比較有系統地學習其他領域的知識。\u003c/p\u003e","title":"2024 年回顧"},{"content":"\n兩三年前，透過很簡單的 css style 設定，讓 EinkBro 可以呈現最基本的網頁內容直排效果。這次，要來總結一下怎麼把直排體驗的小地方也都顧到，其中包含下面的各項調整：\n有單一數字時，讓數字保持是直立的 有連續 2 個數字時，讓數字保持是直立的，而且並排在一起 有 1. 2. 3. 之類的項目標題時，讓數字保持是直立的 有單一英文字母時，讓英文字母是直立的 有連續 2 個英文字母時，讓英文維持直接，而且並排在一起 有 a. b. c. 之類的項目標題時，讓它保持是直立的 微調直立的數字，確保它看起來是跟上下文有對齊的 在直排閱讀模式中，將 minute 這個閱讀時間單位做成多語化 單一數字與多個數字的處理 為了讓數字不要在直排閱讀時總是躺平，在顯示前必須掃過一次全部文字，把這些數字找出來，一一處理。\n在 EinkBro 中，寫了一段 javascript 來處理這件事：先找出網頁中的所有 TEXT_NODE，利用正規式表示法把最少一個數字，最多連續 4 個數字挑出來。之所以要撈到 4 個數字，是因為我想同時處理年份的情況。以中文新聞和文章來說，常常會出現西元年份；如果能把連續 4 個數字也轉成正的會提高可讀性。\n利用第 4 行的字串表示，在第 9 行逐步找出 TEXT_NODE 中符合的情況，再為這些數字加上 span，補個 vertical 的 class name。針對 vertical class，會配上以下的 css style：\n單一數字或是單一英文字母後後面有 . 接下來是處理條列式內容時常會用到的標號，像是：\n或是 a. b. c. 等 這時，我們得把上面的正規表示式改成下面的樣子(這寫法還是不完美，後面會再做修改)，除了撈1 到 4 個數字，跟後面有 . 的情況外，也對英文做同樣的處理：\nconst regex = /(\\d{1,4}\\.?|[a-zA-Z]{1,50}\\.?)/g; 這樣子撈到的符合字串會比較多，所以在處理時，要多點 if-else 來濾掉一些不要的情況：\n第 11 行會將長度超過 1 的符合情況，如果都是英文的話，跳過不處理。如果是較長的數字，但在長度 4 以內的話，就幫它們加上 verticalSingleChr 的 class name。這是為了將這一個個數字，獨立的轉成站起來。\n大致的作法就如同上面所述，能涵蓋到八九成以上的數字。\nol 元件中的 li 標號轉正 接再來，介紹如何處理網頁中 ol (order list) 中的 li 元素。以下的程式碼是參考 taketori.js 來的。它的直排閱讀做得很完整，但太大包了，而且不大符合我的使用情景，所以只能抽取部分我需要的邏輯出來用。\n為了讓 li 元件可以站起來，有幾個步驟：\n找出這些需要旋轉的 li 元件，判斷它是哪種型式 (1. 2. 3., or i, ii, ii, or I, II, III, or 一些日文的編號) 將這些 li 塞入 data-marker 的屬性，填入想要顯示的標號 將 li 上層的 ol 元件加上 cjk class name。 cjk class 的 css style 如下。要先把 ol.cjk 的 list-style-type 清除，讓它不會有預設的標號文字；再來是在 li:before 中，將剛剛塞的 data-marker 放進 content，然後針對它做旋轉。\n收尾：避免重覆旋轉同個字串兩次 在使用時，常常會遇到有些連續的 2 位數會轉過頭：原本是 90 度躺平，調整後變成 180 度躺平。一樣是躺平，只是方向不同。\n後來發現可能在某些流程中，會造成部分符合的數字字串被處理了兩次，包了兩層的 vertical span 。為了避免重覆處理，在新增 vertical span 前，可以先看看 parent node 是不是已經是 vertical class 了：\n以上，就是大部分直排閱讀模式的改善邏輯。經過這番改造後，直排閱讀的體驗又更上一層樓了！\n相關連結 相關的調整 commit EinkBro 的直排閱讀模式技術分享 直排書中的數字，該怎麼處理才好？ ","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%B9%E5%96%84-einkbro-%E4%B8%AD%E7%9A%84%E7%9B%B4%E6%8E%92%E9%96%B1%E8%AE%80%E9%AB%94%E9%A9%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/c437b39f68e4/1_NmMNQIxxgNlaIUpfJgbXuw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e兩三年前，透過很簡單的 css style 設定，讓 EinkBro 可以呈現最基本的網頁內容直排效果。這次，要來總結一下怎麼把直排體驗的小地方也都顧到，其中包含下面的各項調整：\u003c/p\u003e","title":"改善 EinkBro 中的直排閱讀體驗"},{"content":"\n新版的 EinkBro 在經過一番努力後，即將支援 EasyList 格式的廣告清單，除了阻擋一般廣告外，還可以隱藏其區塊，甚至是去除一些常見的 tracking 機制。\n原先 EinkBro 的廣告阻擋機制比較陽春，採用的方式是定期抓取 \u0026lt;https://github.com/StevenBlack/hosts\u0026gt; 上的 adblock 清單，在載入網頁時，會檢查每個 WebResourceRequest 的連結是不是來自於這份清單中的項目；是的話就傳送一個 dummy response 給 WebView，讓它既不會去浪費頻寬抓廣告，也不會在畫面上呈現出來。\n一直以來這方式都還做得不錯，能擋掉大部分我原先看得到的廣告。不過，總還是有些小缺憾，像是廣告雖然看不見了，但通常它還是會在畫面上留下一塊空白，造成需要多捲動一些才能跳過廣告版面。\n為了讓廣告阻擋的效果能更好，勢必得要導入更好的機制，讓這個機制能夠適度地去調整網頁的內容，把不必要的元素移除或隱藏。\n找尋合適的方案(現成的作法) 在找了一陣子後，覺得 https://github.com/Edsuns/AdblockAndroid 這個 repository 還蠻完整的，實作了 EasyList 和 Adguard 的過濾方式；程式碼大部分採用 c++ 開發的，理論上在執行的速度會比 Java/Kotlin 的版本還有效率；而且原始的實作是從開源的 brave 程式碼來的，應該在穩定度上也會不錯(吧？)。\n這專案最後的 commit 是在 2021 年 8 月，已經三年多沒更新了。不過，我還是試著把它調整一下，讓它再度能在現在的開發環境以及各種最新的函式庫下運作。相關的調整可以看這支 commit:\nmake it buildable on newer libraries · plateaukao/AdblockAndroid@2d819ae\n與 EinkBro 整合 能將別人的 example app 成功編譯和執行是一回事，要能整合到自己的專案中，又是另一回事啦。\n底下這第一支整合的 PR，一次塞進來了 19225 行的程式碼(主要為兩個函式庫 ad-filter 和 adblock-client。後者大多是 c++ 實作)，程式碼大部分是我已經看不大懂的 c++ filter implementation。雖然它看似能動，能正常運作，但還是抖抖的。\n整合進來後，發現 app size 從原先的 4.2 MB 一下子跳到了 5.4 MB。再怎麼調整 makefile 也無事於補，怎樣就是要多出 1.2 MB 。\nfeat: make sure new adblock works · plateaukao/einkbro@93fcc7b\n重構重構再重構 ad-filter 和 adblock-client 中還在使用早期的 LiveData，這必須換成 StateFlow 才行；為了要可以增修 adblock 列表，原先範例中的 Setting 介面，也得再重刻一個 Compose 版本的。\n為了重寫這塊，花了我不少時間，才成功地把 LiveData 整個拔掉。\nrefactor: remove livedata variables · plateaukao/einkbro@77972bc\n光拿掉 LiveData 還不夠。原先有些資料 class 的 member variables 是可以變動的。這在 StatFlow 中會造成是否需要更新時，會有誤判的情況發生。以致於有的時候 adblock filter 下載後下載時間不會更新，或是刪除時畫面上的項目還留著。\n最終，在將 Filter class 改成 Immutable 後才解決了 Compose UI 更新的問題。(請看下面 commit)\nrefactor: make Filter immutable, whenever it\u0026rsquo;s changed, create new one · plateaukao/einkbro@6725cb7\n多個 binary 版本的 release 只要 Android app 一使用上 jni，就得考慮到要怎麼編譯最終 apk 版本：一個比較簡單但吃容量的方式是，把所有 CPU model 會用到的 native library 都塞進 apk，這麼一來不管什麼裝置都能安裝並正常運作；另一個方式則是追求較小的 apk size，針對不同 CPU 架構編譯出不同的 binary。需要哪個版本就自己找到對應的版本安裝。\n由於這項由一個 apk 變成多個 apk 的改變，延伸出來的是：原先在設定畫面中的版本更新按鈕都失效了。在程式碼裡我把它改成較多人會用的 cpu 版本 app-arm64-v8a-release.apk 。\n後續收尾 目前 adblock 的白名單是失效的，這得等之後有空再補做。再來是還得花更多時間了解一下這包函式庫大概的架構長怎樣，之後才會有能力去做點修正或改善。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8D%87%E7%B4%9A-einkbro-%E7%9A%84%E5%BB%A3%E5%91%8A%E9%98%BB%E6%93%8B%E6%A9%9F%E5%88%B6/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/5f3756c4b3af/1_X6JIDNNT32UJOH8Hn2mfdA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e新版的 EinkBro 在經過一番努力後，即將支援 EasyList 格式的廣告清單，除了阻擋一般廣告外，還可以隱藏其區塊，甚至是去除一些常見的 tracking 機制。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e原先 EinkBro 的廣告阻擋機制比較陽春，採用的方式是定期抓取 \u003ccode\u003e\u0026lt;https://github.com/StevenBlack/hosts\u0026gt;\u003c/code\u003e 上的 adblock 清單，在載入網頁時，會檢查每個 WebResourceRequest 的連結是不是來自於這份清單中的項目；是的話就傳送一個 dummy response 給 WebView，讓它既不會去浪費頻寬抓廣告，也不會在畫面上呈現出來。\u003c/p\u003e","title":"升級 EinkBro 的廣告阻擋機制"},{"content":"為了要可以塞入更多的功能按鈕，而又同時可以輕鬆的設定工具列，我改寫了工具列設定畫面。\n修改前和修改後的差異如下。\n修改前 vs 修改後\n在導入 Jetpack Compose 後，做這樣子的調整就容易多了，不需要再寫一堆無謂的 layout xml 檔案。右邊新的設定畫面，不再使用 Dialog 的方式來呈現，而是新建了一個 ToolbarConfigActivity，全畫面顯示，一方面是為了日後再加入更多的功能，以及設定；另一方面，在下方可以完整地顯示一個工具列預覽區，想調整成怎麼樣，在下面可以即時移動按鈕，不再需要望著一個直式的列表，在腦中想像成果會長成怎樣。\n下面的預覽還包含了選擇空白空間按鈕後的效果(虛線區域)，會盡量吃掉工具列上剩餘的空間，讓按鈕往兩側靠的排列成為可能。\n實作上沒有什麼特別的地方：下方的工具列預覽是參照目前 Compose 寫成的 Toolbar.kt，新增一個 ReorderableToolbar ，讓使用者可以長按按鈕以調整位置；以及特別將空間按鈕畫上虛線，方便標示。\n而上方可供選擇的功能按鈕 GridView，就只是把原先的 listview 換成 GridView 而已，沒有什麼特別需要調解的。\n目前在新的畫面下，已經陸續加入了”反轉顏色”，”分享連結”，”儲存epub”等功能。之後，應該會再把其他的功能也都加進來吧。到時候懶得再多按一下把功能選單叫起來的人有福啦~\n相關連結 主要的實作在這支 commit ，和後續的幾支修改 commit 中。\nui: re-write toolbar configuration UI · plateaukao/einkbro@77f78f0\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%B9%E5%96%84-einkbro-%E7%9A%84%E5%B7%A5%E5%85%B7%E5%88%97%E8%A8%AD%E5%AE%9A%E7%95%AB%E9%9D%A2/","summary":"\u003cp\u003e為了要可以塞入更多的功能按鈕，而又同時可以輕鬆的設定工具列，我改寫了工具列設定畫面。\u003c/p\u003e\n\u003cp\u003e修改前和修改後的差異如下。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2b70a3a25e57/0_R0ivP7OPKxP5M7zJ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2b70a3a25e57/1_MNwdpCSYeMnkyEdHY0T7eQ.png\"\u003e\n\u003cem\u003e修改前 vs 修改後\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e在導入 Jetpack Compose 後，做這樣子的調整就容易多了，不需要再寫一堆無謂的 layout xml 檔案。右邊新的設定畫面，不再使用 Dialog 的方式來呈現，而是新建了一個 ToolbarConfigActivity，全畫面顯示，一方面是為了日後再加入更多的功能，以及設定；另一方面，在下方可以完整地顯示一個工具列預覽區，想調整成怎麼樣，在下面可以即時移動按鈕，不再需要望著一個直式的列表，在腦中想像成果會長成怎樣。\u003c/p\u003e","title":"改善 EinkBro 的工具列設定畫面"},{"content":"這篇文章會整理這一個月來整合 Edge TTS 功能到 EinkBro 中的過程。經過一連串的開發後，總算是把自己想要的功能都開發出來了。\n為什麼要整合？ 目前 Android 手機上的 Accessibility Service 常常在更新，它的文字轉語音效果也比前幾年有明顯的進步。但是，聽起來還是感覺得出來比較機器人一點。目前能通吃所有語言的 OpenAI tts 文字轉語音，雖然什麼語言的文字丟給它它都可以唸得出來，但是 ABC 的腔調還是聽起來不是很舒服。所以，開始著手找看看是不是有機會將其他的語音轉文字整合到 EinkBro 中。\n找方案 其他比較大間可以選擇的，再來應該就是微軟的文字轉語音方案，它的語音效果相當好，朗讀文字內容時很自然。目前微軟推出的 Edge 瀏覽器中可以免費使用此服務；如果有商用需求，或是超出其免費使用量的話，可以也採用微軟推出的 Azure AI Speech 來提供服務。\n和 Android 內建的語音轉文字功能不同的是：它無法下載資料檔離線使用。在使用時，必須透過打 API 的方式，取得語音的音檔，再利用設備上的多媒體播放函式庫進行播放。\n在 Android 上比較有名的整合實作是 tts-server-android。下面是它在 github 上的介紹：\n这是一个Android系统TTS应用，内置微软演示接口，可自定义HTTP请求，可导入其他本地TTS引擎，以及根据中文双引号的简单旁白/对话识别朗读 ，还有自动重试，备用配置，文本替换等更多功能。 内置微软接口(Edge大声朗读、Azure演示API(已猝) )，可自定义HTTP请求，可导入其他本地TTS引擎，以及根据中文双引号的简单旁白/对话识别朗读 ，还有自动重试，备用配置，文本替换等更多功能。 其設定介面如下 (有點複雜…)\nsource: https://github.com/jing332/tts-server-android?tab=readme-ov-file\n我曾嘗試著安裝並設定來用，但…一直搞不定。再加上它還提供了更多微軟方案中強大的 script 功能設定，要搞懂它比想像中的花時間。另外，在看程式碼時，發現它真正跟 Edge ReadAloud 整合的實作，是用 go 寫的，再利用 jni 的方式把 go 的功能引入。如此一來，如果我想改什麼的話，難度也就更高了些。\n如果是在電腦上要執行的話，有一套 Python 的模組 edge-tts 可以使用。只要利用 pip install edge-tts 把它裝起來，就可以從終端機中呼叫。\n最終，我決定要採用的是 Edge-TTS-Lib 。這是個去年 (2023) 出現在 Github 上的專案，沒半顆星，但是它除了 library 外，還提供了個範例程式，讓我可以直接測試是不是真的還可以用。\n它的範例中，使用方式也很簡潔：\nclass MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding= ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.speak.setOnClickListener { // 就這一行而已 TTS.getInstance().findHeadHook().speak(\u0026#34;你好啊\u0026#34;) } } } 初步整合 這個函式庫很小，只有幾個程式檔，其中好幾個還是單純的資料格式宣告。所以，與其在 build.gradle 中引入這函式庫，不如直接在 EinkBro 中建立一個 sub package，把這些檔案都搬進來。\nCode Hierarchy for Edge-TTS-Lib\n後來也證明這是個明智的決定，因為我把相關的程式碼都做了縮減，改寫和重構，最終主要的程式碼只剩下一個檔案而已。\ntts related codes in EinkBro\n調整、縮減和重構 既然將它納入 EinkBro，它就不再是個 generic 的函式庫，要考慮到各種設定或客製化的可能性，我可以把它改造成 EinkBro 需要的形狀。\n首先，TTS-Lib 的原始實作是在收到來自 MS Edge ReadAloud API 來的語音資料後，會先將內容寫到暫存檔中，再把暫存檔傳給 MediaPlayer 播放。以其他應用來說，這種直接把播放邏輯也處理掉的方式應該是比較討喜的。使用的人就不用再自己實作播放的細節。\n不過，我想像中 EinkBro 提供的功能是：可以讓使用者中斷或暫停目前播放的內容；而且我也希望可以做點 pre-fetch，而不是播完一段文字後，才再去呼叫一次 API，等它回傳、寫檔後才又由 MediaPlayer 播放。\n為此，最先做的改造是：\n把程式碼中，語音文件播放的功能拿掉。這會留待 EinkBro 的 ViewModel 中，針對使用場景去處理。 不再將收到的語音內容寫入暫存檔。這件事有點多此一舉。MediaPlayer 是可以從 InputStream 中讀取資料，直接播放的，沒有必要進行多餘的 IO 讀寫。上圖中的 ByteArrayMediaSource 就是為了做這件事而撰寫的。 將原先 TTS.kt 的一堆 getter / setter，初始工作拔掉，只留下 EinkBro 會使用到的設定。 拿掉 TTSUtil.java, TTSVoice.kt, TTSWebSocketListener.java, UrlConstant.kt 這些文件的內容在經過縮減後都很小，其實可以直接宣告或實作在 TTS.kt 中就行。 調整完的 TTS.kt 中，原先用來呼叫 MS API 並播放語音的函式，變成了這個樣子：可以根據帶入的語音角色、速度、和文字，輸出從 API 端取得的 ByteArray。\n整合到既有的 ViewModel 中 播放邏輯 原先的 EinkBro 已經支援系統內建的語音功能，也可以使用 OpenAI 提供的 tts API。相關的設定有的在設定畫面裡，有的在朗讀按鈕點擊後的對話框裡。\n為了再加入新成員，先重新調整了一下對話框的功能如下，讓使用者可以在這裡就決定想用哪個方式朗讀網頁內容。根據不同的方式，會呈現不同的設定讓使用者調整。\n目前支援的三種語音模式：MS ReadAloud, OpenAI, Built-in\n有了播放設定後，再來是播放狀態的呈現和控制。為了要讓這強大的語音效果能一次多唸幾篇文章，在 ViewModel 中加了一個 articlesToBeRead 的列表，存放著使用者塞了多少文章內容進來，準備在目前文章唸完後，接著往下唸。畫面左下角的文字進度顯示是：\n目前朗讀中的區塊 / 目前文章總共有多少區塊 (有多少文章待讀)\n,\n為什麼要用區塊而不是用句子數量或是語音長度來當進度呢？\n如果想用語音長度來當進度的話，前提是所有內容都已經轉成了文字才會知道總長。這在 EinkBro 中是不符合使用體驗的。一篇文章要全都轉成語音，勢必要花不少時間，所以，目前的作法是把兩三句句子，湊成一定長度後，就送去打 API，期待 OpenAI 或 MS ReadAloud 可以快速地反回語音內容，先開始朗讀；接著，趁使用者在聽當前區塊時，趕快再在背景多抓一兩個區塊的語音回來。\n只能偷抓一兩個區塊備用，多了也不行。為什麼呢？因為打 OpenAI tts API 是要錢的。總不行使用者只聽了一句，按了停止，結果 EinkBro 在背後偷偷把整篇文章的語音都抓了回來吧。\n下面的邏輯是用來處理 OpenAI tts API 和 MS ReadAloud 的作法。在切好區塊後，利用 Channel 的特性和一個 Semaphore 來控制有多少區塊被送去 API 取回語音。\n第 19 行的 send() 會被第 34 行的 playAudioByteArray() 給阻擋住。唯有語音播放完成後，語音發送端才能再把資料餵進 Channel 中。\n上圖中的 processedTextToChunks () 實作如下：在分區塊大小時，必須先考慮到語言的差異。如果是歐美語系的話，希望能用 word 為單位，計算內容的多寡。而中文、日文、韓文的話，則希望是以字為單位，計算內容多寡。\n中文和日文大多會用。為句子結尾，所以不難處理。而韓文，因為使用的句子結尾符號跟英文一樣是 . ，所以只能另外處理。這邊使用了另一個 getWordCount() 的實作來處理這細節：\n播放狀態 原先 ViewModel 中的播放狀態很陽春，只有一個 Boolean 值在記錄目前是否有在朗讀中，用來顯示工具列和功能選單上按鈕的狀態。\n現在，為了要能呈現準備中，播放中，暫停，以及停止後的各種狀態，建立了 enum class 來更精準地記錄各種狀態；然後，再讓對話框的 compose 依照當前狀態呈現對應的按鈕和功能。\nenum class TtsReadingState { PREPARING, PLAYING, PAUSED, IDLE } 呈現朗讀的內容，及其翻譯結果 最終，我最想要的功能加了上來。MS Edge 瀏覽器上，如果讓它朗讀的話，它會進入 Reader Mode，並且隨著朗讀的進度，將文字內容標注起來。這樣子的作法很適合時時想看目前唸到哪的使用者；但是，對於一旦開始閱讀，就想一邊聽一邊看其他網頁的人來說，就被朗讀的畫面卡住了。另一個方式是從功能選單裡點擊大聲朗讀。雖然這樣子能將朗讀功能縮小成一個側邊的按鈕，卻無法加入其他想朗讀的文章。\n為了良好的解決這問題，我在既有的 TTS 對話框中多了個功能：\n只要點擊當前的進度資訊，就能將其他的設定元件隱藏，改成顯示目前在朗讀中的區塊。這麼一來，就不用執著於得要開啟原先的網頁才能知道在讀什麼內容。這對於聆聽母話的人來說，不是很重要的功能，因為不論看不看文字，都聽得懂在說什麼。\n但是這對於語言學習的使用者來說，就至關重要。加上文字的呈現通常可以更容易理解朗讀的內容。\n那…如果文字還是看不懂時怎麼辦呢？關於這一點也考慮到了：只要點擊文字內容，它就會顯示翻譯後的結果。這麼一來，即使聽力不是很好的使用者也可以透過文字和翻譯來學習聽到的內容。\n相關連結 目前這些實作大多在 v12.0.0 和後續的 30, 40 支 commit 中。因為更新的方式有點片段和局部性，所以無法直接提供完整的程式碼連結。\nEinkBro v12.0.0\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%95%B4%E5%90%88-edge-tts-%E6%96%87%E5%AD%97%E8%BD%89%E8%AA%9E%E9%9F%B3/","summary":"\u003cp\u003e這篇文章會整理這一個月來整合 Edge TTS 功能到 EinkBro 中的過程。經過一連串的開發後，總算是把自己想要的功能都開發出來了。\u003c/p\u003e\n\u003ch3 id=\"為什麼要整合\"\u003e為什麼要整合？\u003c/h3\u003e\n\u003cp\u003e目前 Android 手機上的 Accessibility Service 常常在更新，它的文字轉語音效果也比前幾年有明顯的進步。但是，聽起來還是感覺得出來比較機器人一點。目前能通吃所有語言的 OpenAI tts 文字轉語音，雖然什麼語言的文字丟給它它都可以唸得出來，但是 ABC 的腔調還是聽起來不是很舒服。所以，開始著手找看看是不是有機會將其他的語音轉文字整合到 EinkBro 中。\u003c/p\u003e","title":"整合 Edge TTS 文字轉語音"},{"content":"果然，沒有良好的測試偶爾就是會出大包。還好，不用寫 outage report，只要趕快再推出新版，有人來問時，就說新版已經解決了。\nv11.16.0 支援書籤重排！ 重磅功能，不曉得為什麼我拖了那麼久沒開發，寧可用 prefix 加上數字的方式，撐了兩三年。我想，主要原因應該是：得要調整資料庫格式，將 Bookmark 加上 order 的欄位吧。有了這個欄位，才能夠將使用者移動後的新排序記錄起來。\n顯示當前時間 目前在設定中可以把系統狀態欄隱藏，增加網頁可視面積。但隱藏後又常常需要看一下現在的時間。所以，在工具列功能中，新增了時間的顯示。\n在歷史記錄中顯示連結 目前的瀏覽記錄只有顯示網頁的標題。偏偏有很多網頁不論你怎麼瀏覽，他永遠只會顯示同樣的標題。為了解決使用者看不清這些名稱都一樣的標題，但實際上各自指到不同的連結，在呈現時，也把連結也都顯示了出來。\n可關閉觸控區域長按 有使用者反應，觸控區域長按的功能(跳到網頁最前端/最底部)太過靈敏，常會不小心誤觸。如果能夠關閉的話，讓使用者自行選擇，會更加合適。所以，我在設定\u0026gt;行為中，新增了可關閉長按的功能。\n移除設定畫面中的小圖示 將設定第二層以後的畫面中圖示移除。太多的圖示會讓畫面太過花俏。\n修正 預設的 APP 語言，在沒有設定的情況下，會使用繁體中文。目前修正為會使用系統當前的語言設定 增加書籤可被點擊的區域 (原先只可以點有字的地方) v11.17.0 調整設定畫面 在設定畫面中使用較為正常的功能開關按鈕。\n工具列中新增兩個空白按鈕 以後使用者可以自行決定要怎麼排放按鈕，讓工具列可以分群。\n下面是我的設定方式\n出來的效果如下：\n可開啟其他的 Epub 檔案 在功能選單中的 “開啟 Epub”，可用來開啟設備中其他的 epub 文件。\n長按朗讀可調整速度 長按功能選單中的朗讀按鈕，可調整朗讀的速度。\n預設使用網頁快取 開啟使用網頁快取。當按下返回鍵時，大部分情況下可以不用重新載入網頁，加快操作的速度。如果覺得有異常，可到設定\u0026gt;行為中關閉它。\n修正 編輯書籤後，會跳出同步的對話框 第一次進入 EinkBro 時，網址列沒有被 focus，需要點擊它才可以使用 修正朗讀功能的邏輯，確保只有當前的句子讀完後，才會再往下去跟 OpenAI API 要新的語音內容 相關連結 Releases · plateaukao/einkbro\n","permalink":"https://plateaukao.github.io/blog/posts/einkbro-%E7%9A%84%E6%9B%B4%E6%96%B0%E5%B0%8F%E8%A8%98-v11160-v11170/","summary":"\u003cp\u003e果然，沒有良好的測試偶爾就是會出大包。還好，不用寫 outage report，只要趕快再推出新版，有人來問時，就說新版已經解決了。\u003c/p\u003e\n\u003ch3 id=\"v11160\"\u003ev11.16.0\u003c/h3\u003e\n\u003ch4 id=\"支援書籤重排\"\u003e支援書籤重排！\u003c/h4\u003e\n\u003cp\u003e重磅功能，不曉得為什麼我拖了那麼久沒開發，寧可用 prefix 加上數字的方式，撐了兩三年。我想，主要原因應該是：得要調整資料庫格式，將 Bookmark 加上 order 的欄位吧。有了這個欄位，才能夠將使用者移動後的新排序記錄起來。\u003c/p\u003e","title":"EinkBro 的更新小記: v11.16.0 \u0026 v11.17.0"},{"content":"這篇除了用來記錄這四天在東京市區跑步的路線外，也分享這些路線跑起來的心得和沿路拍的照片。\n這次在東京待了六天，第一天(週一)下午到，第六天(週六)一大早搭機返台，所以真正能跑步的日期只有週二到週五。再加上通常晚上會跟同事一起去吃吃喝喝，買買東西，逛逛街，所以能挪出來的時間也就只剩下早上而已。\n出發前雖然心中已經有了打算，想要在每天早上跑跑不同的路線，但最終能湊成一個全馬距離，這也是後來覺得身體狀況還可以，才慢慢有了這個念頭。\n第一天：皇居御苑 這次住的飯店在赤坂，離公司走路只需要 10 分鐘左右，路上是很熱鬧的各式各樣餐廳。而赤坂旁其實就是皇居御苑，距離約 1.7 公里左右。繞皇居御苑一圈是 5 公里；這樣子來回差不多是 10 公里，符合我最近週末慢跑的長度。\n第一趟，週二跑的是皇居御苑，天氣陰陰的，跑起來很舒服。從飯店出發後得先有點上坡，跑經過國會議堂和一堆政府機關，然後才來到櫻田門，開始繞皇居。繞皇居時就不太用花大腦，因為路上不會有紅綠燈；只需要稍微注意一下後面是不是有跑者速度比較快，正打算超車。\n這 5 公里都很平緩，沒有什麼起伏，適合對坡道沒轍的跑者，而且沿路有很多 metro 站可以適時撤退，不會有身體不適但無法快速返回的問題。\n回程時，還貪心地想一併收編東京鐵塔，所以朝南跑。但跑了一段後，想想還是算了，不要第一天就跑過頭。除了後面還有三天外，跑完還得要去上班，跑太累是不行的。\n國會議事堂\n從櫻田門接入皇居御苑慢跑路線\n沿路的歐式建築\n很大片的(不可進入)草坪\n路燈還沒關的東京車站\n一旁的高樓大廈\n第二天：神宮外苑 + 國立競技場 + 東京體育館 網路上查到，神宮外苑也很適合跑步，而且，還有銀杏林可以拍照。可惜來的時間是 9 月，都還沒有變黃。感覺再一兩個月就能看到滿滿的黃色銀杏葉。回程時，想說可以順便經過去年有造訪的國立新美術館，所以一路朝南多跑了一段。不過，由於時間還很早，其實也拍不到什麼，抵達後就一路跑回飯店。\n日枝神社\na la bonne heure\n第三天：東京鐵塔 + 濱離宮恩賜庭園外圍 這一天的路線安排得不是很好，除了繞東京鐵塔可以狂拍照外，濱離宮恩賜庭園要到 9 點才有開門，一大早只能沿著外圍跑，除了滿滿的高樓外，沒有什麼景色。\n東京鐵塔旁有個小小的芝公園，也適合輕度的慢跑。公園內還有介紹紅葉種類的牌子。看來在秋天時也是個賞楓勝地。\n第四天：皇居御苑 again 赤坂週邊的東西南方能跑的公園、景點都跑過了；北方沒看到值得跑步前往的地點，所以再跑了一次皇居御苑，希望天氣能好一些，拍拍晴天時的景色。\n雖然重點一樣是要跑皇居御苑，但出發路線調整了一下，多看了點沿路的風景。這一趟很明顯地，照片就少拍了一些，速度也慢了許多，因為已經是連續跑步的第四天了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E7%94%A8%E5%9B%9B%E5%A4%A9%E5%9C%A8%E6%9D%B1%E4%BA%AC%E8%B7%91%E4%B8%80%E5%80%8B%E9%A6%AC%E6%8B%89%E6%9D%BE%E8%B7%9D%E9%9B%A2/","summary":"\u003cp\u003e這篇除了用來記錄這四天在東京市區跑步的路線外，也分享這些路線跑起來的心得和沿路拍的照片。\u003c/p\u003e\n\u003cp\u003e這次在東京待了六天，第一天(週一)下午到，第六天(週六)一大早搭機返台，所以真正能跑步的日期只有週二到週五。再加上通常晚上會跟同事一起去吃吃喝喝，買買東西，逛逛街，所以能挪出來的時間也就只剩下早上而已。\u003c/p\u003e","title":"用四天在東京跑一個馬拉松距離"},{"content":"\n好久沒有寫開箱文了，雖然最近買了不少閱讀器，但因為都是舊的款式，所以也懶得寫，只顧著開箱使用，也懶得再寫點什麼。但文石這台白色的 Go Color 7，倒是等了很久才等到它開始販售，所以可以來寫一下使用心得。\n硬體 以 7 吋閱讀器來說，Go Color 7 的重量算是中等。雖然比不上輕很多的掌閱 Ocean 2 的 170 克，也比不上金屬機身的 Kindle Oasis 的 188 克；但是 195 克的重量也還算是在可以接受，能夠長時間手持閱讀的範圍內。\n就外觀來說，它沒有 Kindle Oasis 和 掌閱 Ocean 系列的人體工學設計，偏重於一邊；但手感倒也比想像中的好很多，背後是類磨砂材質，既止滑，又不沾指紋。\n在右側有實體翻頁鍵，可以自行定義按鍵行為，很方便。右側下方有喇叭，只是音質沒有很好，而且用右手手持時，會擋到出音孔，影響到聲音的品質。\n再來是這台的最大特色了 — 白色機身！白色真的比較討喜。它不是純白的，所以在跟螢幕對比時，不至於覺得螢幕很暗。不過，因為採用的 Kaleido 3 彩色電子紙天生的特性，它的對比度怎麼比也比不了黑白的電子紙螢幕。\n軟體 這一塊似乎就不用特別再寫什麼了。文石對於電子紙的客製化，是目前所有廠商裡面屬一屬二好的。各種針對每個 App 的優化，能給的都給了，設定多到一般的使用者要花很多時間才能弄懂怎麼設定。\n機子剛拿到的時候，有個比較嚴重的問題是：如果將閱讀器放在手機架上，或是平放在桌上，這時對於畫面的觸控會變得非常不靈敏。很多使用者在一收到後就發現了這個問題。\n好在，文石很快地就在幾天內推出了更新。只要更新系統後，就不再有這個問題。這點還算不錯，至少不用等上十天半個月。\n相關連結 ICAT | 7吋彩色BOOX Go Color 7\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3-boox-go-color-7-%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/50b1f23a053c/1_mI3hoW5aAE1GHQ2mhrgOLw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/50b1f23a053c/1_MNY7bS5FyZRQ7vsFdq9VLw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e好久沒有寫開箱文了，雖然最近買了不少閱讀器，但因為都是舊的款式，所以也懶得寫，只顧著開箱使用，也懶得再寫點什麼。但文石這台白色的 Go Color 7，倒是等了很久才等到它開始販售，所以可以來寫一下使用心得。\u003c/p\u003e","title":"文石 BOOX Go Color 7 使用心得"},{"content":"\n自從開始更大量地使用 ChatGPT 協助寫 code 之後，就漸漸失去了動力寫文章，因為大部分的功能只需要請 ChatGPT 幫忙產生所需程式碼，再稍微重構一下，將其套用到 EinkBro 中就行。如此一來，少了很多需要自己探索，或是值得記錄下來的內容。不過，一直這樣下去也不是辦法，所以，還是來寫寫最近有加入的新功能吧。\nv11.10.0 改善翻頁的作法。大部分的網頁會乖乖地在 Android WebView 呼叫scrollTo 函式時，將網頁內容向下位移指令的距離。但是，有很多網頁偏偏想要用一些特別的實作來捲動畫面，導致這樣的作法會失效。\n為了解決這種情況發生，一個方式是找到目前畫面中具有 overflow-y: auto 的網頁元件，然後在按下翻頁按鈕或是翻頁按鍵時，針對那個元件進行捲動。\n詳細的實作是利用 Javascript 來完成的，可以在下面的 commit 中看到。\nfix: #366 add javascript handling to fix scrolling issue on websites. · plateaukao/einkbro@23c3fc5\n然而，這個作法很陽春，也只能解決到一小部分的特例而已。不過，至少有了一點點的進步。\nv11.11.0 OpenAI ChatGPT 面世已經一年多了，在 EinkBro 中對於 GPT 的支援也愈來愈豐富。在這個版本中，支援了 OpenAI 的模型設定；並且，能夠指定其他跟 OpenAI API 相容的 API 服務；甚至，還加上了 Google Gemini 的支援。\n這一版沒有什麼特別的技術需要多做解釋的；就是把原先的 API 呼叫做得有彈性一點，既能換掉 url entry point，也可以指定 model 罷了。\n比較可以提的是：原來 Reader Mode 的字型設定壞了好一陣子。在網站上看到有使用者提出這個問題，才順手在這一版將它修復。\nv11.12.0 又再次試圖改善部分網頁的翻頁行為。\n另一點是調整了儲存網頁列表的邏輯。正常來說，應該是要儲存每個分頁最終所在的網頁才對，而不是該分頁一開始被建立的網址。\n這也是有網友建議這麼做的時候，我才想到：對啊，這樣才合理吧。\nv11.13.0 終於，開始又有想要實作的功能了！\n早些時候開發的在 Youtube 中顯示雙語字幕。這功能在我使用 EinkBro 看 Youtube 時幾乎都會使用。不過，一直盯著影片看也是會累的。是不是可以直接將這些影片的雙語字幕儲存下來呢。\n在不想看影片時，就可以看看字幕學習外語。如果儲存成 epub 格式，還可以再用 EinkBro 打開來，做後續的查單字，以及叫 ChatGPT 做很詳細的解釋。\n之前雙語字幕的作法，簡單來說，是在 WebView 需要從網路下載 resource request 時攔截這需求，一次抓下來兩個語言的字幕，將它們合併成一個，再傳給 WebView。\n這次，則是在這過程中，將要用來呈現的雙語字幕 resource 存在 WebView 中。當使用者想要對這個 Youtube 網頁儲存 epub 時，只要發現有雙語字幕的 resource，就改成儲存雙語字幕。\n細節可以在下面這支 PR 看到。\nhttps://github.com/plateaukao/einkbro/commit/27caeff9530decb4ebd888583d08da642511a548\nv11.14.0 依網域做不同的設定 自以為的重磅功能來了！v11.14.0 中加入了一個之前很提不起勁加入的功能：根據網域的不同，提供不一樣的各種設定 (翻譯，背景色變白，等等)。\n在這個版本裡，對於 per-site 的設定，還是很用土炮的方式實作：將每一個 configuration 從原先的 Boolean 值，改成 Set ，能夠將開啟的網域都加入一個 Set 裡。每次新開網頁時，檢查當前的網址是不是有在這個 Set 中；有的話就當它是有啟用這項功能的。\n詳細的實作可以在下面的 commit 看到：\nfeat: support by site enabling translation feature now · plateaukao/einkbro@b2b424e\n只要有想要 per-site 設定的項目，就都寫一段類似下面的程式碼。\n這方法雖然很遜，但有用。\n改善選取文字的方式：選取句子 像這個功能就是問 ChatGPT 而來的，所以雖然看起來很酷炫，不過功勞其實都是 ChatGPT 的。\n作法是寫段 Javascript，讓它先取得目前 Web 中的 selection ，從這個 selection 去找到被選取的 container 。有了 container 後就可以取得裡頭的 textContent，再決定要從現有的選取位置往前移動或往後移動。\n以選取句子來說，在選取的起始位置是要試著往回走，走到前一個句子的結束 (可能會是 . 。！!？?等字元)，而選取的結束位置則是要繼續往後走，走到這個句子的結尾 (另一個. 。！!？?)。\n作法可以在這個 commit 中看到。\nfeat: select context as a whole sentence · plateaukao/einkbro@e3132ea\n輸出 GPT 結果 不論是 gpt-4o 或是 Gemini-1.5-pro，都可以產生不錯的解釋內容。這些產出的結果，在閱讀之後如果能夠儲存下來，就能夠事後再拿來復習，溫故知新。\n總是翻譯 某些特定的網站，每次進入就是要開啟翻譯的功能。按久了，偷懶的心漸漸冒出來。難道就不行讓它能自動判斷網址，來決定要不要自動翻譯嗎？\n這版支援了！\n在設定方面，就是剛剛提到的 per-site 設定作法。設定好之後，在 WebView 載入網頁時，當載入完成會進入 WebContentPostProcessor 的 postProcess 。\n只要目前的網頁有在需要被翻譯的清單，翻譯的 Javascript 會將網頁翻譯成指定好的語言。\n這點真是太好用了！我應該早點開發的。\nv11.15.0 選取文字的段落 延伸 v11.14.0 的選取句子功能，這一版加入了選取段落的功能。這對於常常需要翻譯的使用者來說，應該會很使用。\n將輸出 GPT 結果轉為 html 文件 gpt-4o 或是 Gemini-1.5-pro，常會在回覆中使用 markdown 的標記方式。雖然它是純文字，即使沒有特別的 rendering engine ，依然具有很高的可讀性。\n但為了原先的 markdown 格式能更清楚易讀，這一版實作了 markdownToHtml ，利用外部的 javascript library 協助，讓產生的 html 能顯示漂亮的效果。\n支援 App 內切換語系 早知道程式內換語系這麼容易實作，應該早點就開發的。這功能也是透過 GPT 得到的答案。\nAndroid 有提供了 Configuration.setLocale 的函式，讓開發者可以在 Activity 或是 Fragment 中即時去更換語言。不過，我不用那麼即時，只要重開 App 能解決就行；所以目前是在設定 \u0026gt; 介面 中新增選擇介面語言的設定，等設定好後，回到網頁時，會問使用者是不是要重啟 App，讓語言能夠生效。\n實作內容都在這支 commit:\nfeat: support in-app language setting · plateaukao/einkbro@bc900a2\n有了這功能後，以往一些大陸買的閱讀器，系統中沒有繁中語言可選的，現在都可以用這方式單獨改變 App 的介面語言。\n也因為這樣，我發現韓文的介面好像全部都是英文。所以，我把韓文的 strings.ml 丟給 ChatGPT，讓它幫我生出對應 ko/strings.xml。希望這樣能為我帶來更多韓國的使用者。\n","permalink":"https://plateaukao.github.io/blog/posts/einkbro-%E5%85%AD%E6%9C%88%E4%BB%A5%E4%BE%86%E7%9A%84%E4%B8%80%E4%BA%9B%E6%9B%B4%E6%96%B0/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1a091f99ab5f/1_U7_A7KAID90_lJW75td99g.png\"\u003e\u003c/p\u003e\n\u003cp\u003e自從開始更大量地使用 ChatGPT 協助寫 code 之後，就漸漸失去了動力寫文章，因為大部分的功能只需要請 ChatGPT 幫忙產生所需程式碼，再稍微重構一下，將其套用到 EinkBro 中就行。如此一來，少了很多需要自己探索，或是值得記錄下來的內容。不過，一直這樣下去也不是辦法，所以，還是來寫寫最近有加入的新功能吧。\u003c/p\u003e","title":"EinkBro 六月以來的一些更新"},{"content":"韓文漢字學習表 (未完) 從這篇文章整理而來：\n【一撃で暗記】漢字で覚える韓国語単語【完全講義】\n사 기 수 ","permalink":"https://plateaukao.github.io/blog/posts/%E9%9F%93%E6%96%87%E6%BC%A2%E5%AD%97%E5%AD%B8%E7%BF%92%E8%A1%A8-%E6%9C%AAnrr/","summary":"\u003ch3 id=\"韓文漢字學習表-未完\"\u003e韓文漢字學習表 (未完)\u003c/h3\u003e\n\u003cp\u003e從這篇文章整理而來：\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://trilingual.jp/jako/20230524-18758/\"\u003e【一撃で暗記】漢字で覚える韓国語単語【完全講義】\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"사\"\u003e사\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8ccbd5d1f4f6/1_LSI8R6eDNkc7rER48lqNPw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8ccbd5d1f4f6/1_7277OGPDYaWItjTyAxTvQw.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"기\"\u003e기\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8ccbd5d1f4f6/1_q5Mwac_YewV0LSj_uvn6Ig.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8ccbd5d1f4f6/1_dsCd48cyG6CdpPIJrh8NYw.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"수\"\u003e수\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8ccbd5d1f4f6/1__INGriluia6UYf-DW3c3kg.png\"\u003e\u003c/p\u003e","title":"韓文漢字學習表 (未nrr"},{"content":"\nGoogle 在 Google IO 2024 公布了 Gemini 1.5 Flash model，並且幾乎免費地讓開發者可以來試用。為了能夠試試它的效果，我把它也整合進了 EinkBro。本篇文章會說明整合的方式。\nGemini API 的呼叫大致上可以分為兩種方式：一種是透過 Google 在各平台或語言推出的 Gemini SDK，直接利用其中的 api client 呼叫其函式，代入參數，就可以收到結果，而且內容都已經幫忙解析好，為完整的 class 物件；另一種方式則是比較原始的方式，單純利用 http request 去跟 Gemini API server 溝通，等拿到 json 字串結果後，再自行解析成 response object。\n各有各的好處，以 EinkBro 的情況來說，我並不想因為要整合 Gemini API，就把整套 SDK 都導入進來 (實測大約會多 150 KB)；所以，選擇的方式是直接打 API，等拿到 json 字串後，再從裡面取出所需要的值。雖然要寫的程式碼會多一點點，但省下來許多寶貴的 app size。\n實作 HTTP REQUEST 最重要的是，Gemini API 的網址長得怎樣。下面可以看到，我已經把 model name 寫死，是 gemini-1.5-flash 。雖然 Gemini 也有開放免費試用的 1.5-pro，但因為一分鐘內只能呼叫兩次，個人覺得應該不大夠用，所以在 EinkBro 中就沒有開放彈性讓使用者指定 model name。\n在網址的最後面必須代入 api key。相比於 OpenAI 的作法，Google Gemini 在申請 API Key 的流程上就很簡單，直接到 AI Studio 按個鈕就可以拿到一把 key。\nhttps://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=$apiKey 知道網址後，再來要看 request 的 body 中需要代入什麼內容。這邊主要塞了兩種資訊：\n詢問的內容 安全性設定 關於 1. 詢問的內容，在 OpenAI 的場景下，是代入一個 message List，其中會包含使用者設定的 system prompt，以及一則 user prompt。而從網頁中的選取文字則是會被附加在 user prompt 的最後面。\n但在 Gemini API 的情況下，目前我是先只塞了這個 message List 的最後一則資料，也就是 user prompt 的內容。\n再來， 2. 安全性設定，則是要跟 Gemini 講，不要一遇有好像 sensitive 的內容，就拒絕回答。雖然它還是會做一些審查，但至少可以讓這情況不要太常發生。\nval data = RequestData( contents = listOf( Content(parts = listOf(ContentPart(text = contextMessage))) ), safety_settings = listOf( SafetySetting( category = \u0026#34;HARM_CATEGORY_SEXUALLY_EXPLICIT\u0026#34;, threshold = \u0026#34;BLOCK_ONLY_HIGH\u0026#34; ), SafetySetting( category = \u0026#34;HARM_CATEGORY_HATE_SPEECH\u0026#34;, threshold = \u0026#34;BLOCK_ONLY_HIGH\u0026#34; ), SafetySetting(category = \u0026#34;HARM_CATEGORY_HARASSMENT\u0026#34;, threshold = \u0026#34;BLOCK_ONLY_HIGH\u0026#34;), SafetySetting( category = \u0026#34;HARM_CATEGORY_DANGEROUS_CONTENT\u0026#34;, threshold = \u0026#34;BLOCK_ONLY_HIGH\u0026#34; ) ) ) val requestBody = json.encodeToString(data).toRequestBody(\u0026#34;application/json\u0026#34;.toMediaTypeOrNull()) HTTP RESPONSE 完成 request 後，利用 OkHttpClient 送到 Gemini API server 去，收到結果後，再包裝成 EinkBro 想要的格式。\nval request = Request.Builder() .url(apiUrl) .post(requestBody) .apply { headers.forEach { (key, value) -\u0026gt; addHeader(key, value) } } .build() return withContext(Dispatchers.IO) { try { val response: Response = client.newCall(request).execute() if (!response.isSuccessful) { return@withContext \u0026#34;Error querying Gemini API: ${response.code}\u0026#34; } val responseBody = response.body?.string() ?: return@withContext \u0026#34;Empty response from Gemini API\u0026#34; val responseData = json.decodeFromString\u0026lt;ResponseData\u0026gt;(responseBody) responseData.candidates.firstOrNull()?.content?.parts?.firstOrNull()?.text ?: \u0026#34;No content available\u0026#34; } catch (exception: Exception) { \u0026#34;something wrong\u0026#34; } } 從 ResponseData 中，一路拆解，最終可以拿到 Gemini model 產生回來的文字。ResponseData → List → Content → ContentPart → text\n@Serializable data class ContentPart(val text: String) @Serializable data class Content(val parts: List\u0026lt;ContentPart\u0026gt;) @Serializable data class SafetySetting(val category: String, val threshold: String) @Serializable data class RequestData( val contents: List\u0026lt;Content\u0026gt;, val safety_settings: List\u0026lt;SafetySetting\u0026gt; ) @Serializable data class ResponseData(val candidates: List\u0026lt;Candidate\u0026gt;) @Serializable data class Candidate(val content: Content) 主要的實作就這樣而已。不過，這樣子的作法還缺了點什麼：streaming。\n支援 Streaming Gemini-1.5-Flash 有點話多。回答的內容常常會一大串。因此，應該要讓它也可以用 stream 的型式回傳結果，再隨時更新在畫面上。\n但是，Gemini-1.5-Flash 的 streaming 實作使用的不是 OpenAI 在用的 SSE ，所以，只好再依照它的方式再實作一次。\n首先，要將 request url 改掉。原先的最後一段 path segement 是 :generateContent , 要裝它改為 :streamGenerateContent。再來是 query 時，必須要從 api call 拿回 stream 的 buffer 來處理。\n這裡的實作方式比較粗糙，先去從 stream 中拿到一行一行的內容，assign 給 chunk。如果 chunk 中的文字有發現到 text: 的字樣，我就認定這一行包含了產生的文字。利用 substringAfter() 和 substringBeforeLast() 夾擊，把文字抓出來。\n透過這種陽春的解析法，一樣能達到 streaming 的結果，但不需要引入肥大的 Gemini SDK。\nval request = createGeminiRequest(messages, true) client.newCall(request).execute().use { response -\u0026gt; if (!response.isSuccessful) { failureAction() return } val inputStream = response.body?.byteStream() ?: return inputStream.source().buffer().use { source -\u0026gt; var outputString = \u0026#34;\u0026#34; while (!source.exhausted()) { val chunk = source.readUtf8Line() if (chunk == null) { failureAction() return } try { //Log.d(\u0026#34;OpenAiRepository\u0026#34;, \u0026#34;chunk: $chunk\u0026#34;) val textField = \u0026#34;\\\u0026#34;text\\\u0026#34;: \\\u0026#34;\u0026#34; if (chunk.contains(textField)) { appendResponseAction( chunk.substringAfter(textField).substringBeforeLast(\u0026#34;\\\u0026#34;\u0026#34;).unescape() ) } } catch (e: Exception) { failureAction() return } } } 支援簡易 Markdown 格式 Gemini API 還有另一個特色是：特別愛使用 markdown 的格式回傳內容，尤其是標題文字，粗體字，或是 bullet points。在畫面上看到 ###, **文字** ，或是 * a, * b 等，雖然不致於到看不懂，但總覺得不是那麼美觀。所以，EinkBro 還加入了基本的 Markdown 格式支援。\nJetpack Compose 的 Text Widget 有支援類似傳統 TextView 的 Spannable，叫做 AnnotatedString。實作方式便是解析文字內容，在遇到應該要呈現不同效果的標記時，把該個段落抽出來，包裝成一個 AnnotatedString。細節可以看這個檔案，這裡就不多做解釋了。\n相關連結 commit for 支援 Gemini: https://github.com/plateaukao/einkbro/commit/4bc8bebe74fc36e6d33adca9cc89fe645c134828 commit for 支援 Gemini Streaming: https://github.com/plateaukao/einkbro/commit/8fa995744d7495c9467ca28a95ecb1dbb602cee6 ","permalink":"https://plateaukao.github.io/blog/posts/einkbro-%E6%94%AF%E6%8F%B4-gemini-api/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/10d5daf135e3/1_mf0rksDlW76GwWX8qbwMtg.png\"\u003e\u003c/p\u003e\n\u003cp\u003eGoogle 在 Google IO 2024 公布了 Gemini 1.5 Flash model，並且幾乎免費地讓開發者可以來試用。為了能夠試試它的效果，我把它也整合進了 EinkBro。本篇文章會說明整合的方式。\u003c/p\u003e","title":"EinkBro: 支援 Gemini API"},{"content":"\n這篇文章講說明如何在系統選單中加入 KOReader 圖案，並且，在設備上顯示懸浮球，方便返回上一步和開啟系統選單。\n在系統 Launcher 加上 KOReader App 圖案 本小節的作法主要是參考這個連結。如果對於 Android 開發有點概念的話，操作上很容易。但，就算沒有概念的話，一步步照著作也是可以的。\n/etc/dp_extensions 是系統用來放選單中每一個按鈕的設定。我們先將 Launcher 中的隨便一個設定目錄複製到電腦中。這邊舉的例子是 NoteCreator。 adb pull /etc/dp_extensions/NoteCreator 再來是把這份目錄以及下面的每個檔案根據我們想要新增的應用作調整。首先，是把目錄名稱和檔案名稱都改掉。這邊以 KOReader 當例子的話，要進行以下的操作。 cp -R NoteCreator KOReader cd KOReader mv NoteCreator_extension.xml KOReader_extension.xml mv NoteCreator_strings-en.xml KOReader_strings-en.xml mv NoteCreator_strings-ja.xml KOReader_strings-ja.xml mv NoteCreator_strings-zh_CN.xml KOReader_strings-zh_CN.xml mv ic_homemenu_createnote.png ic_homemenu_koreader.png 改好檔名後，開啟 KOReader_extension.xml，這是指定按鈕被點擊時要怎麼啟動 App。這需要你先知道你想呼叫的 App 的 main activity 名稱才行。以 KOReader app 為例，這可以在其 github repo 中的 AndroidManifest.xml 中找到。找到後，把內容中的 name 和 component, icon 換掉。修改如下： \u0026lt;LauncherEntry name=\u0026#34;NoteCreator\u0026#34; category=\u0026#34;Launcher\u0026#34; uri=\u0026#34;intent:#Intent;launchFlags=0x10000000;component=com.sony.apps.notecreator/.activities.MainActivity;end\u0026#34; string=\u0026#34;STR_ICONMENU_1005\u0026#34; icon=\u0026#34;ic_homemenu_createnote.png\u0026#34; order=\u0026#34;5\u0026#34;/\u0026gt; =\u0026gt; \u0026lt;LauncherEntry name=\u0026#34;KOReader\u0026#34; category=\u0026#34;Launcher\u0026#34; uri=\u0026#34;intent:#Intent;launchFlags=0x10000000;component=org.koreader.launcher/.MainActivity;end\u0026#34; string=\u0026#34;STR_ICONMENU_1005\u0026#34; icon=\u0026#34;ic_homemenu_koreader.png\u0026#34; order=\u0026#34;5\u0026#34;/\u0026gt; 內容中還有 order 參數可以修改，我並沒有特別做調整。如果希望它的位置前面一點或後面一點的話，可以自行再調整。\n再來是修改一下字串檔的內容。進到每個 KOReader_strings-*.xml 中，將內容中的 Create New Note 置換掉。以 KOReader 為例，則是全換成 KOReader 就行，因為它沒有其他語言的翻譯。 \u0026lt;string name=\u0026#34;STR_ICONMENU_1005\u0026#34;\u0026gt;Create\u0026amp;#010;New Note\u0026lt;/string\u0026gt; =\u0026gt; \u0026lt;string name=\u0026#34;STR_ICONMENU_1005\u0026#34;\u0026gt;KOReader\u0026lt;/string\u0026gt; 需要把圖檔 (ic_homemenu_createnote.png) 換成一個你想要的圖。我的作法是先打開原檔，將原先的圖檔 clear 掉，再從想要的 App 中，拿它的圖來縮小後，塞到原檔中，儲存。這麼一來，圖檔的寬高 (220x120) 就會跟原先的一樣，不需要擔心新的圖檔有相容性的問題。\n完成上述所有調整後，再來是把這個目錄放到設備中，讓系統在重開機時能重新處理，找到這個新的目錄。DPT-CP1 除了會在 /etc/dp_extensions/ 下找資訊外，也會到 /data/dp_extensions 下找。而 /data 下是不需要什麼特別權限的。所以我們要將目錄複製到這兒。首先，先建立 /data/dp_extensions 這個目錄，再將調整完的目錄 KOReader 複製過去。\nadb shell mkdir /data/dp_extensions # 要先跳出 KOReader 這個目錄 cd .. adb push KOReader/ /data/dp_extensions/ 7.做完後，要將暫存的相關資料清掉，以便設備在重開機時，會重新建立選單的內容。這麼一來就大功告成啦！\nadb shell cd /data/system mv ExtMgr.db ExtMgr.db_bak mv ExtMgr.db-journal ExtMgr.db-journal_bak # 先離開 adb shell exit # 重開機 adb reboot 上述步驟都沒出錯的話，重開機後應該就可以在系統選單中看到 KOReader 圖案出現在 Create New Note 旁邊。\n安裝 AssistiveTouch 原以為修改來給 Pubu Pubook SE 的 AssistiveTouch app 可以直接安裝到 SONY DPT-CP1 上，但事情總沒有想像中的順利。DPT-CP1 是 Android OS 5.1 的老機種了，而且有可能 SONY 也有做過一些系統調整，所以在取得系統權限上遇到了一些問題。把之前 AssistiveTouch 裝上後，執行會因為檢查 overlay 權限 API 不存在而馬上 crash。詳細的各種錯誤可以在這個與 ChatGPT 的請教中看到。\n後續做了一堆修改，包括\noverlay on top of other windows 的權限修改； 加上版本的判斷，避免 Android 6 以後的程式碼被執行； 將懸浮球的尺寸變大； 將原先送出 Accessibility key code 的方式換為從程式裡執行 adb shell input keyevent key_code_name 。(反正已經 root 了，目前就只有這方式能測試成功) 主要的實作如下：\nView gist\n在 commandline 呼叫下面的給予權限方式 (不確定有沒有效，但我都有執行) adb shell pm grant com.android.mirror.assisttouch android.permission.SYSTEM_ALERT_WINDOW 經過這麼胡亂的操作後，終於可以把 AssistiveTouch 懸浮球安裝上去，並且正常執行。\n通常在電子書閱讀器上我是不大想用這類軟體的；但安裝懸浮球的好處是：\n(單擊) 在 SONY 設備上提供方便的返回方式 (雙擊) 減少需要一直點實體按鍵開啟系統功能選單，降低這台二手機壞掉的可能性 (長按) 之前在 Pubook 上的長按功能是用來刷新整個畫面。不過，這在 SONY 設備上因為都沒殘影，好像不用提供這功能。可以留著以後如果有更常用的功能時，再實作上去。 安裝萊娒輸入法 之前自己在維護的萊娒輸入法，經過下面修改後，也可以安裝到這台設備上：\n將 minSDK 降到 21 升級 gradle 的版本，並且處理一下新版的需求 (像是要加上 name space) 相關連結 AssistiveTouch 的 commit 萊姆輸入法的修改 ","permalink":"https://plateaukao.github.io/blog/posts/sony-dpt-cp1-%E5%8A%A0%E4%B8%8A-koreader-%E5%95%9F%E5%8B%95%E5%9C%96%E7%A4%BA%E5%8F%8A%E5%AE%89%E8%A3%9D-assistivetouch-%E8%90%8A%E5%A7%86%E8%BC%B8%E5%85%A5%E6%B3%95/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/3fea512ed890/1_eggxpN7QAmHDunKlGw3iSQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e這篇文章講說明如何在系統選單中加入 KOReader 圖案，並且，在設備上顯示懸浮球，方便返回上一步和開啟系統選單。\u003c/p\u003e\n\u003ch3 id=\"在系統-launcher-加上-koreader-app圖案\"\u003e在系統 Launcher 加上 KOReader App 圖案\u003c/h3\u003e\n\u003cp\u003e本小節的作法主要是參考\u003ca href=\"https://github.com/HappyZ/dpt-tools/wiki/ADB-Tricks,-DIY-Launcher-App#launcher-app\"\u003e這個連結\u003c/a\u003e。如果對於 Android 開發有點概念的話，操作上很容易。但，就算沒有概念的話，一步步照著作也是可以的。\u003c/p\u003e","title":"SONY DPT-CP1: 加上 KOReader 啟動圖示及安裝 AssistiveTouch, 萊姆輸入法"},{"content":"\n上週在網上逛 Amazon 時，看到國際運費免費，失心瘋地買了二手的閱讀器 SONY DPT-CP1，一台 2018 年推出的超輕 10 吋閱讀器。這篇要來說說怎麼在 SONY DPT-CP1 上安裝 KOReader 的方式。\n前言 目前我在買閱讀器時，在意的因素有兩點: 要嘛就是 CPU 性能要很強，因為要拿來上網，流暢度很重要；另外一點是要輕要好拿，能有多輕就多輕，因為看書時習慣拿在手上看，所以如果螢幕能很大，又能兼顧到重量的話，是最好的選擇。\n以第一點 “CPU 性能” 要強來看，目前文石去年到今年推出的閱讀器都有很好的性能，很適合拿來上網用。所以目前瀏覽網頁的話，我使用的主力是 BOOX Tab Ultra C。\n以第二點 “愈輕愈好” 來選擇的話，這一兩年陸續買了掌閱 Ocean2 (7\u0026quot;), Ocean3 Plus (8\u0026quot;) 和 Ocean 3 Turo (7\u0026quot;) 以及 Amazon 的 Kindle Oasis 3 (7\u0026quot;)；重量都是在 170 克到 200 克左右，而且都有將閱讀器重量偏重一邊的設計，在手持上更加舒適。\n但，總覺得還是少了一台 10 吋的輕型設備。目前市面上的10吋新機種，大都是在400450克之間。好一點的，標榜很輕的，也是要 370 380 克。跟 8 吋只要 200 克的 Ocean3 plus 比的話，還是差了一大截，甚至要它兩倍的重量。\n找著找著，就只好往舊的機種找去。剛好看到網路上有人寫到，其實 SONY 2018年出的 DPT-CP1 (10\u0026quot;) 也是能 root，然後安裝 KOReader App 的。看到 KOReader app 我眼睛就亮了。只要能安裝 KOReader app，就表示能夠自己動手調整的可能性變得無限大；而且，10吋大的 DPT-CP1 只有 240 克！這才是真正的輕啊~\n看了看，它似乎沒有再出新款的，但舊款的也沒有因此而降價多少。剛好最近美亞和日亞都有在促銷國際運費免費，所以在日亞上逛一逛，看到有二手的 DPT-CP1 只要半價。雖然知道應該已經被使用很長一段時間了，但，可以買來試試裝上 KOReader app，即使有些小缺點，也是可以忍受的。\n於是，我入手了照片中左邊的 SONY DPT-CP1 (右邊是拿來對比用的文石 6 吋白色 Poke4s)。照著網路的教學 (大概 10 分鐘？)就 root 成功了，也裝上了 2024.04 版的 KOReader app。不得不說，KOReader 真的很神，連 6 年前推出的機種，Android OS 還在 5.1 ，都可以順利安裝。更重要的是！我修改的轉直排 patch 也可以套用上去。\nroot 方式 root 的方式我主要是依照下面這篇文章操作的。在這邊先假設使用的環境是 MacOS，在電腦上也已經裝好了 python3 。\nSony DPT-RP1 电子纸破解\n安裝 python 需要的一些套件 pip install httpsig pyserial urllib3 requests 把下面 github repo clone 一份下來 https://github.com/HappyZ/dpt-tools\n開啟 DPT-CP1 的 wifi 連線，找到其 wifi IP，並且在終端機裡進到 dpt-tools 的目錄下，執行下面指令 python dpt-tools.py -ip your_device_wifi_ip 執行後，如果在 Terminal 中看到 DPT Tools 的字串，就表示完成了第一步。\n在 Terminal 中輸入 fw，按 Enter，然後填入以下的路徑： fw_updater_packer_by_shankerzhiwu/pkg_example/hack_basics/fw.pkg 畫面出現提示 yes/no 時，打入 yes，再按 Enter\n這時系統會重開機，並跑一些安裝程式，等它完成。如果畫面上有 update failure 或 uneable to update，都不要理它。(因為實際上已經裝了)\n依步驟 4，再安裝下面兩個路徑： fw_updater_packer_unpacker/pkg_example/flashable_mod_boot_img_1.6.50.14130/FwUpdater.pkg fw_updater_packer_unpacker/pkg_example/flashable_supersu/FwUpdater.pkg 試試 adb devices，看看有沒有抓到 DPT-CP1。有的話，可以到下面的 github 抓一個小小的 App Launcher 來使用。 Releases · Modificator/E-Ink-Launcher\n下載後執行\nadb install the_apk_you_downloaded.apk 安裝好之後，為了讓它會出現在設備上方的選單中，要再進行下面的操作。第一行的 launcher_mod.tar.gz 可以在 dpt-tools 中找到。都執行好之後，重開機，點擊設備上方的按鈕後，可以看到多了一個 Apps。 adb push /path_to/launcher_mod.tar.gz /sdcard/launcher_mod.tar.gz adb shell mount -o rw,remount /system adb shell rm -rf /etc/dp_extensions 其他 上述流程算是完成了整個 root 的過程，而且還順便裝了個 App Launcher。以下則是一些可能會需要調整的設定；可以根據自己的需求，決定要不要做。\n打開系統預設的設定 App adb shell am start -a android.settings.SETTINGS 切換系統語系 (只有中英日三種語言) adb shell am start -a android.settings.LOCALE_SETTINGS 切換輸入法 adb shell am start -a android.settings.INPUT_METHOD_SETTINGS 安裝 KOReader App 在 root 完之後，安裝 app 的方式其實就跟一般的 Android 設備差不多，能夠利用 adb install 的指令將 app sideload 進 DPT-CP1 中。\nKOReader App 可以到 https://github.com/koreader/koreader/releases 下載比較新的版本。然後執行下面的指令：\nadb install name_of_koreader_app.apk 細步調整 在 root 後的 DPT-CP1 上，所有安裝的 app 的字體都會變得很小。在 KOReader 中能夠對此做一些設定：\n設定 \u0026gt; 螢幕 \u0026gt; 螢幕 DPI \u0026gt; 自訂 : 200\n因為 DPT-CP1 是黑白的，所以可以把用彩色繪製畫面的選項關掉\n設定 \u0026gt; 螢幕 \u0026gt; 彩色顯示: 關閉它\n如果在使用時，畫面會自動關掉的話，可以開啟。或是有時畫面重繪有問題的話，也可以把它打開來。\n設定 \u0026gt; 螢幕 \u0026gt; 螢幕逾時 \u0026gt; 保持螢幕開啟\n相關連結 除了上面參考的那篇中文文章外，最齊全的 Rooting 教學都在下面這個連結：\nThe Ultimate Rooting Guide\n裡面有教到怎麼利用重新打包 framework-res.apk，來更換待機畫面。但我一直試不成功。因為這過程可能會不小心讓你的設備變磚，所以建議不要嘗試。\n","permalink":"https://plateaukao.github.io/blog/posts/root-sony-dpt-cp1-%E4%B8%A6%E4%B8%94%E5%AE%89%E8%A3%9D-koreader-app/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/49a0755df67e/1_2AIHUr3Cu5T-reEKVFElRg.png\"\u003e\u003c/p\u003e\n\u003cp\u003e上週在網上逛 Amazon 時，看到國際運費免費，失心瘋地買了二手的閱讀器 SONY DPT-CP1，一台 2018 年推出的超輕 10 吋閱讀器。這篇要來說說怎麼在 SONY DPT-CP1 上安裝 KOReader 的方式。\u003c/p\u003e","title":"root SONY DPT-CP1 並且安裝 KOReader App"},{"content":"擴充 AskGPT KOReader plugin: 新增一鍵加入筆記 這篇文章將說明在 AskGPT plugin 中 怎麼將 gpt 查詢結果一鍵存入筆記，並且說明開發 KOReader plugin 時如何避免踩雷。\n在前一篇文章中已經實作了點擊按鈕後，喚起編輯筆記的對話框。雖然也是能達到效果，但畢竟多了一步。大多數儲存 gpt 結果的時候，是不需要編輯內容的，所以我又繼續研究怎麼將中間這一步省略掉。\n研究既有實作 在 readerhighlight.lua 中，既有的加筆記函式實作內容如下：\nfunction ReaderHighlight:addNote(text) local index = self:saveHighlight(true) if text then -- called from Translator to save translation to note self:clear() end self:editHighlight(index, true, text) UIManager:close(self.edit_highlight_dialog) self.edit_highlight_dialog = nil end function ReaderHighlight:editHighlight(index, is_new_note, text) self.ui.bookmark:setBookmarkNote(index, is_new_note, text) end 如程式碼所示：\n它會先把標記存下來 (self.saveHighlight)； 並且開啟編輯的對話框 (self.editHighlight)。 再往下看的話，可以看到 self.editHighlight 其實是去呼叫另一個 bookmark 模組的函式。\n那麼，我們再來看看下面 bookmark.setBookmarkNote() 是怎麼實作的。以下內容是去掉不相干邏輯後的函式實作。InputDialog 是編輯筆記時的對話框主體。由於我想要的行為是完全不出現這個 UI 介面，直接將結果儲存，所以需要參考的實作是它的 Save Button 點擊邏輯。\n從 Save 的實作中可以看到，它主要做了兩件事：\n把 value 塞入 annotation 利用 self.ui.handleEvent 將這個 annotation 的變化傳給需要知道的人。 而 annotation 則是在函式的第一行取得的。\nfunction ReaderBookmark:setBookmarkNote(item_or_index, is_new_note, new_note) local annotation = self.ui.annotation.annotations[index] local type_before = item and item.type or self.getBookmarkType(annotation) input_text = new_note self.input = InputDialog:new{ title = _(\u0026#34;Edit note\u0026#34;), description = \u0026#34; \u0026#34; .. self:_getDialogHeader(annotation), input = input_text, allow_newline = true, add_scroll_buttons = true, use_available_height = true, buttons = { { ... { text = _(\u0026#34;Save\u0026#34;), is_enter_default = true, callback = function() local value = self.input:getInputText() if value == \u0026#34;\u0026#34; then -- blank input deletes note value = nil end annotation.note = value local type_after = self.getBookmarkType(annotation) if type_before ~= type_after then if type_before == \u0026#34;highlight\u0026#34; then self.ui:handleEvent(Event:new(\u0026#34;AnnotationsModified\u0026#34;, { annotation, nb_highlights_added = -1, nb_notes_added = 1 })) else self.ui:handleEvent(Event:new(\u0026#34;AnnotationsModified\u0026#34;, { annotation, nb_highlights_added = 1, nb_notes_added = -1 })) end end if annotation.drawer then self.ui.highlight:writePdfAnnotation(\u0026#34;content\u0026#34;, annotation, value) end UIManager:close(self.input) if from_highlight then if self.view.highlight.note_mark then UIManager:setDirty(self.dialog, \u0026#34;ui\u0026#34;) -- refresh note marker end else item.note = value item.type = type_after item.text = self:getBookmarkItemText(item) self.refresh() end end, }, } }, } UIManager:show(self.input) self.input:onShowKeyboard() end 改寫現有機制 了解了既有的實作後，試著將上面的作法實作到 AskGPT plugin 中。把原先的 addNote() 函式換成上述的內容，省略了一大堆關於對話框的操作。\n看似完全照抄的內容，卻在執行時一直 crash。系統總是找不到 ui.annotatoin 這個元件；但明明看程式碼，其他的元件也都是這麼呼叫的啊。為什麼就我的 plugin 無法取得。\n系統總會跳出以下的錯誤訊息：\nFailed to run script: …ulated/0/koreader/plugins//askgpt.koplugin/askdialog.lua:113: attempt to index field ‘annotation’ (a nil value)\n查找為什麼會出錯 針對這個出錯點，花了兩三天在查問題出在哪，一直沒有什麼頭緒。過程中還讓我多學了一點怎麼在 android 上 debug KOReader 的開發。如果只是安裝 KOReader app，沒做什麼設定的話，當 App crash 時，會在畫面上出現一顆炸彈圖案，偶爾會附上出錯的 call stack。但大部分情況下，就只有顯示一顆炸彈，讓人一頭霧水，不知從何查起。\n後來，再認真研讀文件後，找到在 KOReader 的文件瀏覽模式下，可以從工具列的 more tools 找到打開 debugging information 的選項。一旦這個選項打開了，就不用苦苦等炸彈畫面的 call stack；在常用的 android logcat 中就可以看到許多關於 KOReader 的 debugging information。而且，也包含了 crash 時最重要的 call stack。\n雖然會了這個技巧，但依然無法讓我找到為什麼 ui.annotation 總是 nil 的原因。\n求援 無奈之下，只好在 KOReader github 上開了一條 issue，問問眾開發大神。\n於是，我洋洋灑灑解釋了一大篇：\nFR: A function to save notes directly without showing Note Edit Dialog · Issue #11948 ·…\n裡面包含了我想做的事，參考的程式碼，以及 crash 時的 error logs。\n然後…在十幾二十分鐘內就得到了答案，也解了這個我追了好幾天的難題！\n原因原來出在我在參考的程式碼是 github 上最新的程式碼，而我在測試設備上裝的版本是 stable version。兩者間還是有一定的程式碼差別。就那麼剛好，我需要使用的 ui.annotation 是在最新程式碼中才有的元件； stable release version 中其實還無法參考到這元件。\n解決這個 crash 的方式是：把我測試的版本升級到 development channel 的 nightly build 版本就可以了！\n心得 搞清楚自己在開發的 App 版本和參考實作的程式碼版本是一致的，這點很重要，很重要，很重要。\n下次不要再犯這種錯誤了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%93%B4%E5%85%85-askgpt-koreader-plugin-%E6%96%B0%E5%A2%9E%E4%B8%80%E9%8D%B5%E5%8A%A0%E5%85%A5%E7%AD%86%E8%A8%98/","summary":"\u003ch3 id=\"擴充-askgpt-koreader-plugin新增一鍵加入筆記\"\u003e\u003ca href=\"https://medium.com/me/stats/post/d53aa5f5a38b?source=stats_homepage-------------------------------------\"\u003e擴充 AskGPT KOReader plugin\u003c/a\u003e: 新增一鍵加入筆記\u003c/h3\u003e\n\u003cp\u003e這篇文章將說明在 AskGPT plugin 中 怎麼將 gpt 查詢結果一鍵存入筆記，並且說明開發 KOReader plugin 時如何避免踩雷。\u003c/p\u003e\n\u003cp\u003e在前一篇文章中已經實作了點擊按鈕後，喚起編輯筆記的對話框。雖然也是能達到效果，但畢竟多了一步。大多數儲存 gpt 結果的時候，是不需要編輯內容的，所以我又繼續研究怎麼將中間這一步省略掉。\u003c/p\u003e","title":"擴充 AskGPT KOReader plugin: 新增一鍵加入筆記"},{"content":"KOReader 的 AskGPT plugin 沒有任何預設的 prompt；再加上 KOReader 中又只能用它內建很陽春的軟體鍵盤，在使用上並不是很方便。因此，這幾天我一邊看書，一邊改良了一版，加入了最常使用的幾個 template，讓它們直接以按鈕的型式出現在選單中，也加入了 Google Gemini Model 的支援。\n雖然 Google Gemini 1.5 Flash 的表現跟 OpenAI Gpt-4o 還有段差距 (比較慢，產出結果也比較不優)，但是畢竟它現在是近乎免費的，還是蠻香的。\nKOReader 選擇文字後的功能選單，新增 GPT Dictionar, GPT Translate 和 Gemini Dictionary\n下面記錄一下我做的改良 修正 loading 對話框太慢出現 新增 Dictionary, Translate 按鈕 實作 context-aware 字典解釋 新增 Gemini Dictionary 按鈕 將結果存入 Notes 修正 loading 對話框 如果在同個函式中呼叫的話，它會一直等到 http request 回來後，才會跟著結果內容一併顯示。這麼一來就失去了顯示它的用意。為了解決類似的問題，KOReader 實作裡的 UIManager 有提供函式可以讓部分操作在下個 UI cycle 中再執行。因此，可以先顯示 loading 對話框，然後讓 http request 在下個 UI cycle 中再執行。\nUIManager:scheduleIn(0.1, function() showChatGPTDialog(self.ui, _reader_highlight_instance.selected_text.text) _reader_highlight_instance:onClose() end) 完整修改如下：\n新增 GPT Dictionary, Translate 按鈕 這一部分沒啥技術難度，就跳過原先會呈現的輸入框，直接將寫好的 prompt 餵給 http request 去執行。\n主要的修改在這支 commit。關於加 dictionary button 的程式碼如下：\n在 dictdialog.lua 中，有塞入比較合適的 prompt，讓它可以做為字典。下面的程式碼有怪怪的兩個值：prev_context 和 next_context。它們的用途會在後面做說明。\nlocal context_message = { role = \u0026#34;user\u0026#34;, content = \u0026#34;完整句子: \u0026#34; .. prev_context .. \u0026#34;\u0026lt;\u0026lt;\u0026#34; .. highlightedText .. \u0026#34;\u0026gt;\u0026gt;\u0026#34; .. next_context .. \u0026#34;\\n\u0026#34; .. \u0026#34;將上述句子中 \u0026lt;\u0026lt;\u0026gt;\u0026gt; 中的內容 1. 翻譯成 zh-TW\\n\u0026#34; .. \u0026#34;2. 顯示單字原型;如果是日文單字，則顯示漢字拼法 (原本語言)\\n\u0026#34; .. \u0026#34;3. 舉一個新的例句 (原本語言與 zh-TW 對照，各佔一行)\\n\u0026#34; .. \u0026#34;只回答，不要重覆提示\\n\\n\u0026#34; .. \u0026#34;\u0026lt;\u0026lt;\u0026#34; .. highlightedText .. \u0026#34;\u0026gt;\u0026gt;\u0026#34;, } GPT Translate 的話，跟做為字典差不多，就是 prompt 有點差別而已。\n實作 context-aware 字典解釋 在使用字典時，常會遇到一個問題：有些常用字的解釋少說也有十幾二十個。在查詢時往往得要將所有解釋一個個看過一次，再來研究文章中的用途究竟是哪一個。如果剛好遇到一句話裡有兩三個字看不懂，那排列組合下來，要看懂那句話可能得花上不少時間。等搞懂後，應該也沒有心情再往下讀了。\n如果有個字典能夠根據文章的前後文，直接給出當下最合適的解釋的話，不就可以省下很多時間，而且可以讓使用者快速地再回到書籍上，不用在字典停留太久。\n以往的字典無法做到這一點是因為它都是以單字或是片話為搜尋的 key，然後解釋為其 value。如果一個 key 可以對應到超過一個 value 的話，它就頂多只能依常用度來排序，無法再依照其他方式排序或是提供額外的查詢方式。\n有幸生在 ChatGPT 出現的年代，這個限制被打破了。ChatGPT 就是個可以了解整段文字，再來回答你問題的好工具。拿它來當 context-aware 字典真是再適合不過。\nKOReader 的實作中，除了可以拿到 selected text 外，它還很貼心的提供了 context string，可以取得 selected text range 的往前和往後字串。這可以透過 ui.highlight:getSelectedWordContext(context_length) 。context_length 可以指定要往前和往後抓多少文字進來。這個函式的回傳值有兩個，用法如下：\nprev_context, next_context = ui.highlight:getSelectedWordContext(15) 拿到前後 context，就能在 prompt 中請 chatgpt 根據前後文來給出 selected text 的意思。作法可以看上方提供的 prompt。\n新增 Gemini Dictionary 按鈕 今年五月多的 Google IO 宣布了 Gemini 1.5 Flash 的新 model，能夠更快更便宜地提供服務。如果每分鐘呼叫次數在 15 次以內的話，是不需要收費的。拿來個人使用的話，很少會需要有這麼大呼叫次數。因此，很適合拿來做一些不需要太高品質的呼叫。\nGoogle Gemini 取得 API key 的方式比 OpenAI 還容易，也不容事前先儲值，所以我很快地也整合了 Gemini 進 AskGPT plugin。\n主要的修改在這個 commit。\n參考原先的 gpt_query.lua，複製成 gemini_query.lua。把 api_url 換成了 Google 的 “https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=\u0026quot; .. API_KEY.key\n在 prompt 方面，則是代入跟 ChatGPT 一模一樣的內容，方便兩者在使用上作效果的比較。\n將結果存入 Note 好不容易搜尋到的結果，如果過一會兒忘了怎麼辦？是不是能將它記錄下來，方便事後再閱讀呢？\nKOReader 原先的實作就已經有將標記的文字儲存下來的功能，也支援針對選定的文字內容輸入一些文字記錄到 Notes。利用後者，就能將 AskGPT 搜尋的結果偷塞到 Notes 中。\n所以，我在搜尋結果對話框下方，多了一個加入筆記的按鍵。\n實作方式如下：\n在 chatgptviewr.lua 中加入按鈕 在 dictdialog.lua 中生成結果對話框時，代入 handleAddToNote 函式。 而 handleAddToNote 的實作就是去呼叫原先 KOReader 就有的功能 主要的修改在下面這一支 commit：\nfeat: support adding results to note. 存入 Notes 的內容，可以在點擊標記時看到：\n將選取文字在結果對話框裡標成粗體字 最後對於 UI 上的一點小改善，把被選取的文字用粗體表示。這樣做會比顯示醜醜的 \u0026laquo; 和 \u0026raquo; 來的美觀一些。\n主要的修改在這支 commit。\nKOReader 的 Text Widget 支援利用某些 tag 來標示哪些文字片段要以粗體顯示，實際作法如下：在字串的最開始加上 TextBoxWidget.PTF_HEADER，告知字串裡會有其他的 tag。然後利用 TextBoxWidget.PTF_BOLD_START 和 TextBoxWidget.PTF_BOLD_END 將粗體字包住。這樣子就可以啦\n顯示的效果如下：\n相關連結 GitHub - plateaukao/AskGPT\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%93%B4%E5%85%85-askgpt-koreader-plugin/","summary":"\u003cp\u003eKOReader 的 AskGPT plugin 沒有任何預設的 prompt；再加上 KOReader 中又只能用它內建很陽春的軟體鍵盤，在使用上並不是很方便。因此，這幾天我一邊看書，一邊改良了一版，加入了最常使用的幾個 template，讓它們直接以按鈕的型式出現在選單中，也加入了 Google Gemini Model 的支援。\u003c/p\u003e","title":"擴充 AskGPT KOReader plugin"},{"content":"kindle voyage 買來有快十年了吧，隨著最近又買了 kindle scribe 和 kindle oasis3，以及對於 koreader 的熟悉程度愈來愈高，終於下定決心來越獄一下，為它安裝 koreader app。\njb 的過程有點煩瑣，不過，照著”書伴”網站上的教學一步步來，應該十幾二十分鐘可以搞定。裝好 koreader app 後，自然是先裝上前幾天研究好的直排 patch，再換上自己轉好的偽直排字型，從此，可以開心地用著它直排地看 epub 電子書了。\nKindle 通用越狱教程：适用固件版本 5.12.2.2~5.14.2\n另外，基於 koreader 強大的擴充性，有強者為它寫了 askGPT plugin。只要你有 OpenAI API key，就可以直接在 koreader 中跟 chatgpt 互動！我稍微調整了一下 UI，把 plugin 連結放在留言處。有興趣的人可以試試。\n另外，因為 OpenAI API 是要錢的，如果大家會寫程式的話，可以考慮把 gpt_query.lua 中的 api entrypoint 改成 local 自建的 ollama api server 或是其他跟 openAI API 相容的 API server，就可以少一筆支出。(雖然目前 openai API 的收費已經便宜到不行)\n對了，另外跟大家分享一下小發現：\n如果在 kindle 上執行 koreader 的話，可以在 koreader 中自定休眠的圖案喔！可以用目前閱讀中的書籍封面、或是某個特定資料夾下的一堆圖片，或是以目前的畫面做為修眠畫面。還可以自訂在休眠時，畫面是不是要有個提醒的 message，像是 書中自有黃金屋 之類的。 koreader 對於許多系統(像是文石和 kindle)的畫面刷新都有支援，所以可以設定當點擊畫面某個區域，或是做了某個手勢之後，可以讓畫面全刷。對於不愛用懸浮球的我來說，這個功能在 kindle 和 文石機器 上很實用！(kindle 不太需要重刷畫面就是了) 相關連結 GitHub - plateaukao/AskGPT\n","permalink":"https://plateaukao.github.io/blog/posts/jb-kindle-voyage-%E5%92%8C-askgpt-plugin-for-koreader/","summary":"\u003cp\u003ekindle voyage 買來有快十年了吧，隨著最近又買了 kindle scribe 和 kindle oasis3，以及對於 koreader 的熟悉程度愈來愈高，終於下定決心來越獄一下，為它安裝 koreader app。\u003c/p\u003e\n\u003cp\u003ejb 的過程有點煩瑣，不過，照著”書伴”網站上的教學一步步來，應該十幾二十分鐘可以搞定。裝好 koreader app 後，自然是先裝上前幾天研究好的直排 patch，再換上自己轉好的偽直排字型，從此，可以開心地用著它直排地看 epub 電子書了。\u003c/p\u003e","title":"JB Kindle Voyage 和 AskGPT plugin for KOReader"},{"content":"這篇文章主要是介紹如何比較快速地把一個字型轉換成偽直排的字型。\n之前已經有很多文章在介紹了，但大都是在討論怎麼利用 fontforge 這套軟體，一個個手動去改字型的轉向。目前這邊收集到的文章有：\n偽直排：直書控的救星\nKobo/Kindle E-Reader 也能一鍵橫排轉直排? 還不用等待轉檔?\nepub3guide/tdpf-epub3guide.md at master · dpublishing/epub3guide\n但要一個個選字元再來做轉換太累人了。前一陣子看到有人提供了一個 script，可以利用 fontforge 程式中的 python library 直接做轉換。雖然目前還有一點小問題(有些標點符號沒有正常旋轉成功)，但是已經可以省下九成以上的時間。只要跑個 script 後，再進 fontforge 手動調整兩三個標點符號就行。如果覺得兩三個標點符號無傷大雅的話，直接拿來用也是可以的。\n轉換的 script 在：\nGitHub - MyK00L/tategakifont\n裡面有一個 VerticalOrientation.txt 的文件，定義了在旋轉字元時，有哪些字元要旋轉；是要以中間為圓心旋轉，或是要轉完靠右上，左下，等不同的差別。有些中文的標點符號會不大正確，可以在這邊試著去做一些調整。\n把上面的 repo 抓下來，改一改 VerticalOrientation.txt 後，在 MacOS 上先裝好 FontForge，然後執行下面的指令：\n/Applications/FontForge.app/Contents/MacOS/FFPython main.py ./input.ttf ./output.ttf 應該就可以產生一個轉了 90 度的字體，馬上就可以拿來試試效果，再做細步調整。\n如果有些標點符號沒有轉換成功的話，可以再進到 fontforge，依照上面分站的第一篇 偽直排：直書控的救星 裡頭的作法轉換。通常只需要再手動轉兩三個符號，不用兩三分鐘就可以完成。下面有比較常會出錯的字元列表。\nfrom: https://github.com/dpublishing/epub3guide/blob/master/tdpf-epub3guide.md\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%87%AA%E8%A3%BD%E5%81%BD%E7%9B%B4%E6%8E%92%E5%AD%97%E5%9E%8B/","summary":"\u003cp\u003e這篇文章主要是介紹如何比較快速地把一個字型轉換成偽直排的字型。\u003c/p\u003e\n\u003cp\u003e之前已經有很多文章在介紹了，但大都是在討論怎麼利用 fontforge 這套軟體，一個個手動去改字型的轉向。目前這邊收集到的文章有：\u003c/p\u003e","title":"自製偽直排字型"},{"content":"從 Koreader 的開發網站上，有另一位神人寫了個小修正(lua script)，可以在當書籍的 typography 設為日文時，將畫面(文字的部分)轉成 90 度。\n這樣子有什麼好處呢？\n前幾週我有分享一個方式可以讓使用者點擊畫面某個區域時，可以同時把字型換成 轉成 90 度的特殊字型，再順便將系統畫面也轉 90 度，藉此達成一個很克難的直排閱讀。雖然這樣子就可以直排閱讀了，但是當你想要做些其他調整時，就會發現 menu 變成在畫面的左右邊，很難操作(見圖二)。\n而這個 script 則是可以讓只有套了轉 90 度的文字的顯示區再轉 90 度而已 (有點繞口)，其他的 app 介面都還是正常的(選單，對話框，進度列都還是在上下方，見圖一)！\n套用方式 神人的說明在: https://github.com/koreader/koreader/issues/11469#issuecomment-1969338188\n簡單的操作方式是 1. 先下載這個連結到手機上 https://github.com/koreader/koreader/files/14436601/2-cre-rotate-japanese-book.lua.txt\n把它放到 /koreader/patches 下 (可能要把 .txt 的副檔名拿掉)\n然後開啟書籍後，自己先將字型換成 偽直排字型，再將書籍的 typography 設成日文 (目前 script 裡是寫死 ja，如果你主要看的是中文書的話，可能要換成中文 locale)\n同場加映 另一個神人在同個討論串裡，提到他寫了個 python script 可以快速地將字型轉成 偽直排字型，不用再開啟 fontforge 去操作那難用的 UI 了 (但不確定是不是它只針對日文有作用而已，我還沒拿來試中文)。有興趣的人可以研究一下他的 script 寫法 (要使用到 fontforge 的 python extension) 。\n在 MacOS 上的話，要先安裝 fontforge，然後執行下面的指令。(直接用 python 跑會找不到 fontforge module，所以，必須用 fontforge 中提供的 FFPython 來執行)\n/Applications/FontForge.app/Contents/MacOS/FFPython main.py input.ttf ouput.ttf 轉換偽直排字型的小工具: https://github.com/MyK00L/tategakifont?tab=readme-ov-file\n","permalink":"https://plateaukao.github.io/blog/posts/koreader-%E7%9A%84%E7%9B%B4%E6%8E%92%E9%96%B1%E8%AE%80-patch/","summary":"\u003cp\u003e從 Koreader 的開發網站上，有另一位神人寫了個小修正(lua script)，可以在當書籍的 typography 設為日文時，將畫面(文字的部分)轉成 90 度。\u003c/p\u003e\n\u003cp\u003e這樣子有什麼好處呢？\u003c/p\u003e","title":"Koreader 的直排閱讀 patch"},{"content":"KOReader 是個很強大的 open source閱讀軟體，也因為它的功能太多太彈性了，造成剛接觸的使用者，一看到各式各樣的選單，就會落荒而逃。(我之前也是，所以跟它擦身而過好幾年)\n今天要分享一個最近使用 KOReader的小心得。它雖然功能多得驚人，但是沒有直接支援橫排轉直排，對於喜歡看中文小說的我來說，還是覺得有點遺憾。但是，這兩天找到了一個快速設定的方式，可以巧妙地一鍵達到直排的效果。\n其中的奧秘在於： KOReader的畫面區域點擊動作可以設定成 很多個動作的組合(這太神了)。如果說大家覺得 EinkBro 的點擊畫面很好用的話，Koreader的設定完全是另一個高深世界。\n下面就是簡單的設定說明(也可以直接看我附的影片比較快)\n設定概念 點擊畫面右下角，變成直排畫面 長按畫面右下角，切回橫排閱讀模式 設定細節 前往 設定 \u0026gt; 輕觸與手勢 \u0026gt; 手勢管理員 \u0026gt; 角落 \u0026gt; 右下 (裝置 \u0026gt; 旋轉 90 度) + (可重排文件 \u0026gt; Set Font: 原俠正楷90 度) 前往 設定 \u0026gt; 輕觸與手勢 \u0026gt; 手勢管理員 \u0026gt; 長按角落 \u0026gt; 右下： (裝置 \u0026gt; 旋轉 0 度) + (可重排文件 \u0026gt; Font:正常原俠正楷體) 有在使用 KOReader 的使用者，也可以試試看喔。\n","permalink":"https://plateaukao.github.io/blog/posts/koreader-%E4%B8%AD%E5%BF%AB%E9%80%9F%E5%88%87%E6%8F%9B%E6%88%90%E7%9B%B4%E6%8E%92%E9%96%B1%E8%AE%80%E6%A8%A1%E5%BC%8F/","summary":"\u003cp\u003eKOReader 是個很強大的 open source閱讀軟體，也因為它的功能太多太彈性了，造成剛接觸的使用者，一看到各式各樣的選單，就會落荒而逃。(我之前也是，所以跟它擦身而過好幾年)\u003c/p\u003e","title":"Koreader 中快速切換成直排閱讀模式"},{"content":"我的 matepad paper 復活了！ 2022 年上半年發布的華為 matepad paper，因為是第一代產品，所以在軟體穩定度和完整度上，一直被使用者詬病；但是，它的硬體堪稱是當時最強的也不為過，就算是放到現在，也比大多數的閱讀器要強很多。\n下面圖中我稍微裝了一下 geek bench 6 來跑一下 cpu 的性能，在跟右邊的 BOOX Tab Ultra C 做比較時，比分硬是高了快一倍。\nmatepad paper 的硬體雖然很強，但是它為了追求無殘影的效果，只提供了使用者兩種刷新模式：普通模式和智慧模式。普通模式就是畫質最好的刷新方式，很適和在看靜態的內容；而智慧模式則是由系統幫你決定在什麼情況下，要不要用稍微快一點的刷新速度來處理畫面變化。\n這個智慧模式的刷新速度跟文石的極速和快刷完全不行比，刷新速度相當慢，也因此，空有強大的 cpu 性能，在操作上總是會覺得它慢半拍。\n曙光 不過，前幾天在網友的分享下，得知原來早在 2023 年初就有神人透過修改 app 的 package id (系統用來辨識 app 的 id) 欺騙系統，讓閱讀器以為當前的 app 是個影音播放 app，這時，matepad paper 就會進入”隱藏版”的刷新模式，達到類似一般文石系統的極速刷新模式。雖然這個模式下，殘影明顯地多了起來，但很多時候操作上的流暢性是勝於輕微的殘影的。\n但是，這個作法的前提是要有辦法取得想要它刷新變快的 app 原始碼，自己重新修改過 package id 才行，沒辦法套用到一般從網路上抓到的 apk。\n希望 這時，神人又找到了另一個作法，他去改了一個叫”屏幕濾鏡”的 app，這個 app 原先的作用是可以在畫面上蓋上一層膜，用來適度地調整畫面呈現的顏色，或是亮度。而這個機制剛好可以造成不管目前畫面上是哪個 app 在運作，都可以有快刷的效果。\n不過，畢竟原先屏幕濾鏡不是要拿來做快刷的，所以在設定上很煩索。一般人要裝，在操作上也很麻煩；再者，這 app 中也多了一堆不需要的功能。\n自己動手作 因此，在了解了它的運作方式後，我也自己寫了一個小程式專門來做這件事，讓操作的步驟變得簡單很多。而且，佔用的體積也從原本的 4MB 降到了 28 KB。(雖然這點不是很重要就是了)\n結論是：目前 matepad paper 幾乎無敵啦，有好看的外表，有性能高的 CPU，還有隨時想快刷就快刷的作法，重量又輕(約360克而已，那些 400 多克的 10 吋機型請一邊排排站)。\n附上程式(HWQuickRefresh)的連結，要使用的人，在安裝後執行它，會跳出需要允許 accessibility 的權限(因為要蓋在畫面上)。同意後，回到桌面，再切到”智慧模式”，大部分的 app 應該就是快刷模式。如果想臨時關掉，就切到一般模式；要整個關掉的話，就再點一次 HWQuickRefresh app，即可。\n(ps. 如果執行時有遇到畫面全黑，可以從右上方下拉，進入 系統設定，再進入應用程式列表，把它反安裝就行)\n連結 Release 華為 Matepad Paper 快刷模式 v1.0 · plateaukao/AssistiveTouch\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%8F%AF%E7%82%BA-matepad-paper-%E5%BF%AB%E5%88%B7-app/","summary":"\u003ch3 id=\"我的-matepad-paper復活了\"\u003e我的 matepad paper 復活了！\u003c/h3\u003e\n\u003cp\u003e2022 年上半年發布的華為 matepad paper，因為是第一代產品，所以在軟體穩定度和完整度上，一直被使用者詬病；但是，它的硬體堪稱是當時最強的也不為過，就算是放到現在，也比大多數的閱讀器要強很多。\u003c/p\u003e","title":"華為 Matepad Paper 快刷 App"},{"content":"\n從日本回來一天了。是時候來好好回顧一下這趟的青森行。上次到青森滑雪，已經是四年前的事了，當時還是疫情即將爆發的時候呢。這一次則是臨時動心起念安排的。在十二月快要前往二世谷滑雪前，突然覺得，好像這個雪季只滑一趟的話，不大足夠；而且剛好看到虎航在 2023 年底剛開通飛往秋田的班機；想說似乎可以飛到秋田，然後去上次滑雪體驗還不錯的森吉山再滑個幾天。如果雪況好，又沒有什麼人的話，那真的會是個很完美的組合。\n但，計畫總是趕不上變化。一來，虎航雖然號稱廉航，但如果太晚訂機票的話，價錢甚至會比一般航空的機位還要貴，當買完，並加上一件託運行李後，價錢來到二萬二。而且這還是沒有含餐點的機位。真是貴啊～\n再來是：原先打算要去森吉山的阿仁雪場，經過一番研究後，發現它的交通還是太麻煩了，而且附近又只有幾間需要打電話預訂，房間數又少的民宿。如果雪況不好的話，可能就只能待在民宿裡打發時間。怎麼想都有點不保險。所以…最後決定到達秋田後，要拉車去青森，和 papa 他們會合，改成青森滑雪。\n雖然從地圖上看，飛秋田比飛仙台，離青森更近，但因為仙台是東北大站，搭車比秋田方便超多，坐新幹線的話不用兩個小時就可以到；反觀秋田，最快只有特急列車可以前往青森站，需時 2 小時 43 分。但，千金難買早知道。下次就乖乖地買飛到仙台就好，除了交通便利，還可以順便吃吃牛舌。\n第一天，一早 5 點就出門，一直到晚上 7 點多才到達青森的溫泉飯店，真的是很累。在吃過熟悉的餐廳美食後，隔天準備先和 Nijia 到 MoyaHills 雪場熱身一下。\n我們還是到老地方的站前公車站處等待，並且看著超多人排隊要去八甲田的。好在我們第一天不是要去跟大家人擠人。前往 MoyaHills 的車比去b八甲田的要晚一點，等到輪i我們上車時，人少人很多，只有我們和另外一組當地人上車而已.。過了四十分左右，來到了滑雪場。也因為 Nijia 需要借滑雪用具，才發現原來這裡也有租借的地方。\nMoyaHills\n今天這裡的雪況算很普通，就是該有雪的地方有雪，被 skiier 刮過的地方則是有些冰面。比較高的一個纜車沒有開放，因為雪量過少。\n所以，我們就在最初階的滑道上，待了大部分的時間，剛好拿來當熱身用。遊客的話，比想像中的還要多人，但還沒有到需要大排長龍的地步，上纜車頂多等個一兩分鐘吧。\n後來才知道原來這週五開始，是他們的三連休。難怪很多爸爸媽媽帶著小朋友來滑雪玩雪啊。\n滑雪第一天就出了狀況。我的右腳的 binding 的扣環壞了，皮帶裡的塑膠脆化，卡不住上面的扣環，整個掉了出來。在租雪具的地方問了半天，有一個比較好的店員比較配合，有拿著一盒鏍絲要給我試試；但有另一個光頭店員很凶，一看到又立刻把那個盒子拿回去了，直說不行不行，真的是小氣到不行。\n後來，我突發奇想，把新買的扣環的卡扣拆下來，硬是塞在舊的皮帶上。因為新的卡扣的卡損比較大，所以還能稍微地發揮作用。就這麼地，讓我下午不用直接下課。\n後來，跟 papa 到 max value 又買了快乾膠，把它固定得更牢固一點，就這麼地撐過了這個雪季的第二趟。\n上網找了一輪，看來要買 burton 的零件還真是不好買啊。可能我的款式已經五六年以上了吧。\n再來，就是要到八甲田了！久違的八甲田。有了車後，整個行程就方便多了，不用再需要一大早去跟別的遊客擠巴士，可以早他們一步到滑雪場，先開始排隊。這麼做至少可以讓我們多滑個一趟。八甲田的雪票還是一樣的佛心價。十回劵只需要10100日圓。平均一天滑四趟的話，等於一天只需要4000日圓左右，這幾乎是二世谷雪票的半價了。\n週六因為還在三連休當中，所以人潮很多，第一趟排了一個小時多才順便搭上纜車。\n因為這次跟同事來，他們的滑行還沒到能自行探索八甲田的路線，所以前一兩趟我是跟著他們一起滑，確保他們熟悉 direct course 的路況。後面幾天，就幾乎都是跟著之前一起滑雪的夥伴，在樹林裡衝來衝去。\n這次最大的收穫是，對於 direct course 應該算是完全摸熟了；而 forest course 也在今年少雪的狀況下，跑了好幾趟，在心中有了明確地熟悉感。下次再來應該就可以很開心地衝這兩個既定路線的滑道旁樹林了。\n第二天人就少了許多，除了第一趟外，幾乎都是排半小時以內就可以上纜車。第三天，週一，以為可以真正的獨自享受八甲田的雪道時，早上看了網頁發現竟然因為強風而運休中。這明明是因為週一沒有什麼遊客，所以故意停止營業的吧！\n好在事前早就已經有備案了，既然八甲田沒開，我們便趨車前往比較遠一點的青森之泉。這是個離市區要一個多小時的小而美雪場。\n青森之泉很像是小型的留壽都，有自己的飯店和餐廳，週邊沒有什麼其他的點，很獨立。這裡有三條主要纜車，quad 1, quad 2, gondola。今天 gondola 沒開，我想應該也是因為遊客太少，所以不開放。但光是 quad 1, quad 2 就很足夠我們玩了。兩邊都有樹林可以鑽進去，滑幾乎沒有什麼人的新雪。雖然雪溼了點，但比被滑爛的鬆雪好多了。\n最後一天的八甲田真是棒極了，很少，雪質好，又不會厚到需要拿著 pole 跟著滑。就算停在雪裡，只要稍微動一下就可以再次啟動滑行。\n在這一天我把我的11回劵給用完了，還追加了一張一回劵。最後兩趟都是自己一個人順順地衝 direct course，好不愉快！\n滑完雪後，這個雪季安全下莊。為了避免搭機太趕，所以隔天是先搭車到秋田市區逛一天；然後再隔一天才去機場。秋田市一樣的人少少的，連車站前的燈飾都很單調。去逛了之前很喜歡的秋田美術館，這次多逛了一旁的千秋公園，以及公園裡的音樂廳。這裡的感覺很像是札幌中島公園裡的音樂廳。\n再來，就要面對大魔王了。從秋田市區到秋田機場的巴士，在官網上有寫說不載送超過長一公尺的雪具或是其他運動用品。來的時候是因為機場那就接送的巴士司機人很好心，特地幫我橋了一個斜斜的擺放方式，硬塞到置物廂中。這一趟不知道是不是還能闖關成功。\n晚上我一度想說，是不是乾脆先拾火車到最近的和田站，然後再拖著行李走那區區的8公里到機場呢？反正天氣還不錯，就當是鄉間散步。路上完全沒有積雪，所以走個8公里並不是件難事。\n好在，最終還是忍住了這個想法，改成先去搭巴士看看。後來，在巴士站看到告示牌寫著：因為營運的關係，所以來的車子也有可能會是一般的公車。結果，這一趟來的真的是一般的公車而已！一般的公車就好辦了！因為所有人的行李都要拿到車上，不會有置物廂太小放不下的問題。而且，司機就一直坐在前面，根本不會來管從後門上車的乘客是帶著多大的行李。\n就這樣，有驚無險地到了機場。在經過一番折騰後，回到了台灣，結束了這個雪季的第二趟滑雪行。\n","permalink":"https://plateaukao.github.io/blog/posts/2024-%E9%9D%92%E6%A3%AE%E5%85%AB%E7%94%B2%E7%94%B0%E6%BB%91%E9%9B%AA%E8%A1%8C/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/a165b09e92b6/1_ZGbg2zRte6TgPlPWnFootw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e從日本回來一天了。是時候來好好回顧一下這趟的青森行。上次到青森滑雪，已經是四年前的事了，當時還是疫情即將爆發的時候呢。這一次則是臨時動心起念安排的。在十二月快要前往二世谷滑雪前，突然覺得，好像這個雪季只滑一趟的話，不大足夠；而且剛好看到虎航在 2023 年底剛開通飛往秋田的班機；想說似乎可以飛到秋田，然後去上次滑雪體驗還不錯的森吉山再滑個幾天。如果雪況好，又沒有什麼人的話，那真的會是個很完美的組合。\u003cbr\u003e\n但，計畫總是趕不上變化。一來，虎航雖然號稱廉航，但如果太晚訂機票的話，價錢甚至會比一般航空的機位還要貴，當買完，並加上一件託運行李後，價錢來到二萬二。而且這還是沒有含餐點的機位。真是貴啊～\u003cbr\u003e\n再來是：原先打算要去森吉山的阿仁雪場，經過一番研究後，發現它的交通還是太麻煩了，而且附近又只有幾間需要打電話預訂，房間數又少的民宿。如果雪況不好的話，可能就只能待在民宿裡打發時間。怎麼想都有點不保險。所以…最後決定到達秋田後，要拉車去青森，和 papa 他們會合，改成青森滑雪。\u003cbr\u003e\n雖然從地圖上看，飛秋田比飛仙台，離青森更近，但因為仙台是東北大站，搭車比秋田方便超多，坐新幹線的話不用兩個小時就可以到；反觀秋田，最快只有特急列車可以前往青森站，需時 2 小時 43 分。但，千金難買早知道。下次就乖乖地買飛到仙台就好，除了交通便利，還可以順便吃吃牛舌。\u003cbr\u003e\n第一天，一早 5 點就出門，一直到晚上 7 點多才到達青森的溫泉飯店，真的是很累。在吃過熟悉的餐廳美食後，隔天準備先和 Nijia 到 MoyaHills 雪場熱身一下。\u003cbr\u003e\n我們還是到老地方的站前公車站處等待，並且看著超多人排隊要去八甲田的。好在我們第一天不是要去跟大家人擠人。前往 MoyaHills 的車比去b八甲田的要晚一點，等到輪i我們上車時，人少人很多，只有我們和另外一組當地人上車而已.。過了四十分左右，來到了滑雪場。也因為 Nijia 需要借滑雪用具，才發現原來這裡也有租借的地方。\u003c/p\u003e","title":"2024 青森八甲田滑雪行"},{"content":"這篇文章將介紹怎麼把 iThome 鐵人賽的參賽文章集結成冊，變成可以離線閱讀的電子書。就算一時半刻沒有時間閱讀，也可以有種自己收藏了不少好書的滿足感。\n更新 最新版本只需要一行指令就可以製作好 epub 了。詳情請見 github repo:\nGitHub - plateaukao/ithome_ironman_crawler\n截自網頁: https://ithelp.ithome.com.tw/2023ironman/reward\n緣起 一到每年的九月十月，如果我不是在參加 iThome 鐵人賽的路上，就是在閱讀各個厲害的鐵人們每天攪盡腦汁生出來的文章。到十月中時，每個主題都陸陸續續完結，然後大約在十二月時會公布得獎名單，並在一月初頒獎。\n對於被評審青睞的作品，通常內容和文筆都有一定的水準，很值得拜讀。閱讀這些文章的方式不外乎連上網路，坐在電腦前，一篇篇地看；不然就是等著這些作者再接再厲與出版社合作，花上好幾個月甚至是半年的時間，把這些文章經過潤飾，變成實體的書籍。\n除了這兩個方式之外，是不是有可能自己將這些文章集結起來，做成離線的電子書方便自己隨時閱讀呢？如果會寫點 python 語言，又懂得如何利用 calibre 這強大的電子書管理工具的話，其實是可以做到的。以下是我開發的一些步驟說明。\n步驟 將某個想閱讀的參賽主題的每日文章抓下來。通常會有三十篇左右。 取出每篇文章中，屬於內文的部分，排除系統的元件(像是 header， footer，使用者評論等)。 將這三十篇左右的內文全塞進同一份 html 文件中。 用自己熟悉的瀏覽器打開這份 html，並且用 “完整的網頁” 另外新檔。 透過 calibre 軟體中提供的文件轉換工具 ebook-convert ，將 html 轉換成 epub 格式的電子書。 步驟 1 ~ 3 上述的步驟 1 ~ 3 可以透過一支 python script 搞定。我有把程式碼公開在 github上。\nithome_ironman_crawler/fetch_as_single_html.py at main · plateaukao/ithome_ironman_crawler\n如下面程式碼所示，要先帶入 iThome 主題的第一頁，它會從每一頁裡撈出每篇文章的 url，儲存網頁；再前往下一頁，處理下一頁的文章列表。\niThome 的設計還很原始，明明最多就三十幾篇文章，偏偏還要分成三頁，要使用者在看完十篇後，手動再進到第二頁，閱讀十一到二十篇，然後再往下)，\n以我在 2021 年參加比賽的主題來做例子 (https://ithelp.ithome.com.tw/users/20140260/ironman/4027)，下面就是第一頁的內容。\n儲存好每篇的文章後，再來是把它們的內文部分全寫到同一個檔案中。在下面的 49 行會先打開第一篇文章 html，從裡面抓出整個主題的標題，當成這份 merged html 的標題。\n接下來 61 行的 loop 是建立電子書目錄。這裡後來我就全 comment 掉了，因為目錄可以在最後一個步驟時使用 ebook-convert 生成就好。\n68 行開始從每一篇文章 html 中抽取重要的部分 (class name 為 qa-panel__content)，將其寫到 merged html 中。\n步驟 4 一切順利的話，在執行 python script 的目錄，會生成一個以主題標題為名的檔案夾，裡面會有一個 combined.html；而且 python script 會自動去開啟這個網頁。這時，使用者要用 “完整的網頁” 另外新檔。\n以我的參賽作品來說，要下的指令會是\npython3 fetch_as_single_html.py https://ithelp.ithome.com.tw/users/20140260/ironman/4027 而產生的檔案夾會像下面這個樣子\n目前還沒有找到比較合適的工具，所以必須要手動進行。(希望以後有機會也整合到 python script 中。\n這時，要利用瀏覽器的另外新檔，覆蓋掉目錄中的 combined.html。\n完成後，會看到除了原先就存在的 combined.html 後，還會把網頁中的圖片，css styles, 也都一併存到一個目錄中。\n步驟 5 完成步驟 1 ~ 4 後，最後一步就容易了。只要事前在電腦上安裝好 calibre，找到它附帶的 ebook-convert 路徑，再執行一下 command 就可以轉換成 epub 電子書。\n建立 alias calibre 在 Mac 上安裝後，預設會安裝到 /Application/calibre.app 下，而它附帶的 ebook-convert 也會在對應的目錄中。為了讓執行指令容易一點，可以先為它建立好 alias (這步驟是可以省略的，如果不嫌指令會過長的話)。\nalias convert=/Applications/calibre.app/Contents/MacOS/ebook-convert 執行 html to epub 的轉換 轉換很容易，只需要來源檔案名種，和目標檔案名稱。但如果想要轉得夠漂亮，而且還可以生成正確的書籍目錄，就必須要下點工夫才行。\n關於 ebook-convert 的參數設定說明，可以到官網看到很詳細的說明：\nebook-convert - calibre 7.4.0 documentation\n以下是我在轉換時使用到的一些參數。\n關於生成正確的書籍目錄，可以加上 — level1-toc “//h:h2[re:test(@class, ‘qa-header__title’, ‘i’)]” 。它會抓取 html 中具有 qa-header__title 名稱的 h2 元件，這些都是鐵人賽文章中用來表示文章標題的元件。\n再來是 ebook-convert 預設會在每個 h1, h2 的元件前畫蛇添足地加上 page break，從新的一頁開始顯示。這點對於鐵人賽的文章內容來說，很不必要，會讓閱讀體驗很差。所以，必須調整一下參數，讓它可以正常的顯示那些利用 h1 來表示小標題的內容。下面的參數就是告訴 converter，只有在章節 element 前再加上 page break 就好。其他的場合不用雞婆。\n--page-breaks-before \u0026#34;//h:h2[re:test(@class, \u0026#39;qa-header__title\u0026#39;, \u0026#39;i\u0026#39;)]\u0026#34; 最後一個要介紹的參數是— output-profile tablet ，這是為了避免 calibre 預設會將圖片做處理，將其 rescale 成較小的檔案。這參數也是可加可不加。如果產生的 epub 檔案太大的話，有可能是其中引用的圖檔太大，就可以考慮不加這參數，讓 calibre 幫忙把圖都重新處理過。不然，建議還是保留原始圖檔就好。\n完整的指令如下\nconvert combined.html einkbro.epub --level1-toc \u0026#34;//h:h2[re:test(@class, \u0026#39;qa-header__title\u0026#39;, \u0026#39;i\u0026#39;)]\u0026#34; --page-breaks-before \u0026#34;//h:h2[re:test(@class, \u0026#39;qa-header__title\u0026#39;, \u0026#39;i\u0026#39;)]\u0026#34; --chapter-mark \u0026#34;none\u0026#34; --output-profile tablet 執行指令後，等個幾秒鐘，或是十幾二十秒，電子書就完成啦！\n效果展示 以下是 einkbro 第一次參賽的文章產生出來的電子書模樣 (用電腦上的 ebook-viewer)\n在文石 Tab Ultra C 的效果如下：\n在 pubook 上用 koreader 閱讀的效果：\n在 calibre 中，一大排自己轉換好的書籍。有沒有注意到，這些電子書都是封面的。沒錯，在使用 ebook-convert 時，它會主動幫你生成一個帶有標題的封面，是不是很貼心呢！如果不喜歡的話，還可以手動再調整就是了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%87%AA%E8%A3%BD-ithome-%E9%90%B5%E4%BA%BA%E8%B3%BD%E5%8F%83%E8%B3%BD%E4%B8%BB%E9%A1%8C%E9%9B%BB%E5%AD%90%E6%9B%B8/","summary":"\u003cp\u003e這篇文章將介紹怎麼把 iThome 鐵人賽的參賽文章集結成冊，變成可以離線閱讀的電子書。就算一時半刻沒有時間閱讀，也可以有種自己收藏了不少好書的滿足感。\u003c/p\u003e\n\u003ch3 id=\"更新\"\u003e更新\u003c/h3\u003e\n\u003cp\u003e最新版本只需要一行指令就可以製作好 epub 了。詳情請見 github repo:\u003c/p\u003e","title":"自製 iThome 鐵人賽參賽主題電子書"},{"content":"執行 server 時，要用下面的方式，同網路下的設備才存取得到\n./server --host 0.0.0.0 --and_other_params ","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-macos-%E4%B8%8A%E8%B7%91-llamacpp-server-%E4%B8%A6%E4%BD%BF%E7%94%A8-mixtral-8x7b-llm-model/","summary":"\u003cp\u003e執行 server 時，要用下面的方式，同網路下的設備才存取得到\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e./server --host 0.0.0.0 --and_other_params\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"在 MacOS 上跑 llama.cpp server 並使用 Mixtral-8x7B LLM Model"},{"content":"\n2023 年 12 月 25 日到 2024 年 1 月 8 日去了一趟北海道。上次滑雪已經是 2020 年年初的事了，算一算，隔了快四年才又滑到雪，著實悶了很久很久，都是疫情害的。\n這次在日本待的時間比以往都長，比較有時間好好地體會一下二世谷和留壽都這兩個雪場。雖然每天都有在電子書閱讀器上稍微記錄一下當天的狀況，和所見所聞，但整體的心得和記錄倒是還沒寫。如果不寫下來的話，可能再一個月我又要忘光了。所以，在回來的兩週後，也就是現在，稍微再來整理一下吧。\n行 從疫情趨緩後，機票幾乎沒有便宜過，也因此，在 2023 年 5 月看到星宇從台北飛北海道札幌的機票只要一萬八時，幾乎沒有考慮太久就買了。這價錢比我 2023 年 8 月到東京的機票還便宜。兩相比較下，覺得應該及時把握。\n機票雖然相對上便宜，但因為它是最便宜的經濟艙，所以只能掛一件行李而已。好在，我的行李本來就不多，所以在出發時，只拉了一個滑雪板袋，裡頭包含了所有的衣服和滑雪設備。另外就是再揹了個滑雪用的背包，和一個隨身的側背包而已。在航空公司 checkin 櫃台量行李重量時，也只有 16 公斤而已。\n不過，這是很極至的打包方式，我幾乎沒有帶什麼更換的外衣，只有帶了幾件打算要換洗的內用衣物。\n在交通上，除了要飛到機場外，再來就是要前往雪場的巴士。這次是事前預約 Good Sports/Hokkaido Ground Service，費用是 4800 日圓。我飛機大約是兩點左右到，而我預約的時間是 15:30。因為拿行李沒有拖延太久，所以時間上是還蠻有餘裕的。但朋友在跨年前才出發，就因為行李等太久，差點趕不上也是三點半的巴士。\n搭這台巴士時，可以把雪袋放在行李置放處，坐起來還算舒服。由機場前往二世谷 Hirafu Welcome Center 時，建議可以坐在右邊靠窗的位置。因為車程中間會經過支芴湖，如果天氣好的話，比較可以拍到好看的照片。\n一大早就從台灣出發，搭巴士時已經很累了，所以稍微睡一下，很快就到了。到二世谷時，大約是晚上六點多一些。如果預訂的飯店就在 hirafu 鬧區的話，這時應該就是拖著行李，走去飯店。不過，這一趟來我預訂的住宿是在俱知安町街上。所以從 Welcome Center 是坐朋友租來的車先去吃飯的地方解決晚餐的，然後再一起回到俱知安街上。\n在北海道的這段時間，每天滑雪，吃晚餐，泡溫泉，全是靠著朋友開車完成的。有車之後，能夠活動的範圍完全是不同的世界。以前來都只能待在二世谷鬧區，跟所有的外國滑雪客，各地來的遊客一起預約那些又貴又不見得預約得到的餐廳；但有了車之後，泡溫泉的選擇多了，用餐也可以在俱知安町比較平價的餐廳，或是去很大的超市採買，回住宿的地方再小煮一下。\n連滑雪的雪場也不再被綁死，只能待在二世谷。也因此，這趟去了好幾次雪質也很好很厚的留壽都，讓我對它的地形有更進一步的了解。\n最後兩三天獨自行動，我拖著雪板袋從住宿點走到俱知安火車站，搭一大早的火車到札幌，其實也沒有想像中的不方便。大約兩個小時出頭就能抵達札幌市區。\n滑雪 二世谷和留壽都的雪票一年比一年貴。留壽都的雪票貴到是日本第一貴的，一日券要 11500 日圓。當然，這是在到雪場買的情況下才會那麼貴。不論二世谷還是留壽都都有些便宜一點的購票方式。\n二世谷 二世谷在十月十一月時，會在 Niseko United 官網上有早鳥票的販售。聽說只有開放兩天而已，所以要時時注意，並且手腳要快，錯過就沒有了。早鳥票有賣三日全山券(已經忘了價錢，但印象中比現場買單雪場的三張一日券便宜)。我買了兩份，共六天。如果去年有留著它的磁卡的話，在購買時，可以各省下 500 日圓。等到了雪場，在它的機器上刷一下收到的 QR code 就會拿到新的卡片；或是用原先的磁卡，再刷 QR code 充值。\n在 Hirafu 現場買一日劵的話，是 7800 日圓的樣子。\n留壽都 如果要在留壽都滑超過三天的話，一律建議買它的 25 小時劵。售價是 3 萬日圓。如果一天滑 6 小時的話，25 小時可以滑 4 天多，相當划算。但如果沒有要待那麼多天，也可以考慮事前在線上買一日券 (9700 日圓)，或是 5 小時券 (8500 日圓)。大約會是現場買的 85 折，而且不用排隊！\n現在賣票的方式已經很人性化了，如果事前已經有留壽都的磁卡，在線上買票時，可以直接刷磁卡上的 QR Code，就會直接把購買的票券或時數加到磁卡中。\n這等於一到雪場穿好裝備就可以馬上去搭纜車，完全跳過買票的環節，多好啊！\n吊舱、吊椅缆车票 - 留寿都度假村 北海道 日本\n(待續…)\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8C%97%E6%B5%B7%E9%81%93%E4%BA%8C%E4%B8%96%E8%B0%B7%E7%95%99%E5%A3%BD%E9%83%BD%E6%BB%91%E9%9B%AA%E6%94%BB%E7%95%A5/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/7051be2e4207/1_krB_iG1HlC2yQiyCkqlBcA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e2023 年 12 月 25 日到 2024 年 1 月 8 日去了一趟北海道。上次滑雪已經是 2020 年年初的事了，算一算，隔了快四年才又滑到雪，著實悶了很久很久，都是疫情害的。\u003c/p\u003e\n\u003cp\u003e這次在日本待的時間比以往都長，比較有時間好好地體會一下二世谷和留壽都這兩個雪場。雖然每天都有在電子書閱讀器上稍微記錄一下當天的狀況，和所見所聞，但整體的心得和記錄倒是還沒寫。如果不寫下來的話，可能再一個月我又要忘光了。所以，在回來的兩週後，也就是現在，稍微再來整理一下吧。\u003c/p\u003e","title":"北海道二世谷、留壽都滑雪攻略"},{"content":"沒想到在 2024 年初竟然迎來了這樣子的結果，真的是始料未及。下面會來說說被 suspend 的原委，希望讓剛好看到這篇文章的讀者能夠有所警惕，不要踩到 Google 大大的雷。\n在 2023 年 12 月，突然想到其實我可以在 EinkBro 中加入更方便的 App 更新機制。以我自己的使用習慣來說，我都會是安裝剛開發完的版本，所以都不是安裝 Google Play Store 上可能兩三週才更新一次的相對穩定版本。\n為了要安裝最新的開發版本，我的流程會是：\n開啟 einkbro 在 github 上的網頁 點擊 snapshot zip，下載它 到檔案總管 App 中解壓縮這個 zip 檔 到解壓縮後新建的資料夾中，點選 app-release.apk 安裝 這流程雖然不複雜，我也這麼進行了兩三年，但是身為工程師，就是會想要把所有 routine 的事用寫程式的方式來解決。所以，在 2023 年 12 月我在 EinkBro 中加了新的機制可以到設定畫面中點一下升級 button，就可以把上面的步驟都省略掉。\n為此，我還很開心地寫了一篇實作的文章。\n從 EinkBro App 中直接安裝升級版本\n在後續的改善中，也考慮到其實 Google Play Store 的使用者是無法享受到這個機制的，因為要上架到 Google Play Store 時，在編譯時用的是別把 upload key，上傳後 Google Play Store 會把收到的 aab 根據不同設備來的需求，拆成合適的版本，再發送到使用者的設備中。\n所以，只要是判斷操作的使用者是原先利用 Google Play Store App 安裝的話，就會在點擊 button 時，開啟 Google Play Store 的 EinkBro 畫面，讓使用者經由 Play Store App 來升級。\n可惜的是，我只做了半套。EinkBro 升級的按鈕有兩顆，一顆是升級成最新的 release 版本；一顆是升級成 snapshot 的版本。前者我有做了這個判斷，但後者卻忘了也加上這個判斷。\n也因此，應該是有些使用者試著去點了該按鈕，想要升級成 snapshot 版本，卻失敗了。然後 Google 的偵測機制抓到了這些 event，發現 EinkBro 中提供了非來自 Google Play Store 的升級機制。\n在完全不給機會改善的情況下，於 2024 年 1 月 16 日收到 Google 的來信，說 EinkBro 因為違反政策，遭到了 suspension。這邊 suspension 的意思是：關於 EinkBro (info.plateaukao.einkbro) 這個 App，所有的使用者數據，下載量，使用者留言，全部都被清除了。使用者無法在 Google Play Store 上再找到 EinkBro App。\n雖然來信中有提到可以申訴，不過，申訴了也沒有用，官方回覆了很官方的內容；而 EinkBro 也確實實作了升級的機制(雖然這機制 100% 無法在 Google Play Store 安裝的版本上有作用)。\n如果開發者(我)真的很想要再上架，而且我的開發者帳號還存在的話(還沒被 ban 掉)，我是可以在修正問題後(拿掉升級的機制)，重新將 App 的 package id 改掉，然後再換個新名字重新上架一個新 App。我不理解為什麼得要再重新用一個 package id，以及重新申請一個 App。但是，這就是 Google 訂下來的遊戲規則，只能怪我一時疏忽了。\n結論 原先上架 Google Play Store 也只是想說其他想安裝的人會方便一點，但沒想到弄到最後，我卻因為這件事被 suspend 了 app，而且在 Google 的名冊上多了一個 x ，感覺有點得不嚐失。我怎麼知道再重新上架的話，會不會哪天又不小心惹怒谷哥大大。\n為了自己還有一段開發者的生涯著想，要顧好自己的 Google Developer Account，所以我就不打算再上架一次了。在 Github 玩玩就好。如果真的覺得 EinkBro 很有幫助或很好用的人，自然會找到方式安裝；如果找不到的話，那就只能說無緣了。\n","permalink":"https://plateaukao.github.io/blog/posts/einkbro-app-%E8%A2%AB-google-play-store-suspend/","summary":"\u003cp\u003e沒想到在 2024 年初竟然迎來了這樣子的結果，真的是始料未及。下面會來說說被 suspend 的原委，希望讓剛好看到這篇文章的讀者能夠有所警惕，不要踩到 Google 大大的雷。\u003c/p\u003e\n\u003cp\u003e在 2023 年 12 月，突然想到其實我可以在 EinkBro 中加入更方便的 App 更新機制。以我自己的使用習慣來說，我都會是安裝剛開發完的版本，所以都不是安裝 Google Play Store 上可能兩三週才更新一次的相對穩定版本。\u003c/p\u003e","title":"EinkBro App 被 Google Play Store suspend"},{"content":"早該做的提升效率的事 — 從 EinkBro App 中直接安裝升級版本 手邊的 Android 手機，外加電子書閱讀器設備，總共有十幾台。常常在 EinkBro 更新後，不論在使用哪一台，都要開網頁到 Github 網站，下載 snapshot 版本，或是最新的 release 版，再安裝。\n如果是最新的 release 版，操作上容易一些，因為從 Release 網頁可以直接下載到 apk 檔案，下載完成後就能直接安裝 (但還是需要去檔案總管刪掉這個下載好的 apk 檔案)；如果是想要安裝剛開發完但還沒 release 的 snapshot 版本的話，就麻煩多了。因為在 EinkBro Github 首頁的下載連結，目前只有辦法拿到 apk 的 zip file，所以在手機上得要多一個步驟將 zip 解壓縮，才能安裝裡頭的 apk。\n在大部分的設備上，如果裝了 unrar 時或是 ZArchiver 之類的軟體，在 zip 下載後，可以選擇用它們來開啟，就有機會在不手動 unzip 的情況下，直接安裝 apk。不然，就是得要分成兩三個步驟：先下載 zip 檔案；到檔案總管想辦法 unzip 這個 zip file，然後再進到 zip 解開的目錄，去安裝裡頭的 apk 。\n偶爾做一兩次就算了，但我幾乎每週都有在開發；以前則是幾乎天天在開發。這麼一來，如果我不勤於在每台設備上更新版本的話，往往都會用到舊的版本，享受不到我剛開發好的新功能。但要更新又覺得有夠麻煩。\n終於，前一兩天心血來潮，把這件事變得比較容易一點了！！除了在設定的 About 頁面中，加入了一鍵從 Github 更新 release 版本外，還加上了今天要紹的如何實作更新至 snapshot 版本。\n實作內容 主要會有以下幾個步驟：\n下載 zip file 解壓縮成 binary stream 將裡頭的 app-release.apk 檔案寫到暫存檔中 呼叫系統的安裝程式 Intent 前面暫時寫入的 File 物件有加上 deleteOnExit() ，所以當 App 被關閉時，這個暫存檔理論上就會被刪除。 詳細說明 下載 zip file 不論是下載一般檔案，或是 zip 檔，作法都是一樣的。\n先將 snapshot zip url 包裝成 Request，再呼叫 OkHttpClient 的 newCall()。這時，會回傳 response, 就可以再拿著 response 做一番操作。\n這裡使用的是 execute() 而不是 queue() 是因為我把整個大函式設定為 suspend function。\nval url = \u0026#34;https://nightly.link/plateaukao/einkbro/workflows/buid-app-workflow.yaml/main/app-release.apk.zip\u0026#34; val request = Request.Builder().url(url).build() client.newCall(request).execute() 拿到 response 後的實作：從 response.body 中取得 byteStream，然後再餵給 extractApkAndInstall() 函式，抓出裡面的 apk，和安裝它。\nclient.newCall(request).execute().use { response -\u0026gt; if (!response.isSuccessful) throw IOException(\u0026#34;Failed to download file: $response\u0026#34;) val inputStream = response.body?.byteStream() extractApkAndInstall(inputStream, context) } 完整的 extractApkAndInstall() 實作如下：因為已經事前知道回傳資料是 zip，所以這裡會用 ZipInputStream 把它包起來，再 while loop 去取裡面名稱叫做 app-release.apk 的文件。找到後，將它寫入到 cache 中的暫存檔，取名為 app.apk。如果這個檔案事前已經存在了(比方說，前不久也裝過新的版本)，就會先刪掉舊的，再寫入一次。這邊呼叫了 tempFile.deleteOnExit()，希望可以在離開程式時，系統會自動將 app.apk 刪掉，但試了幾次好像都不成功。反正，名稱都是取一樣的，而且會刪除已經存在的檔案，所以，最多系統裡就只會存在一個 app.apk，才 4 MB，就算了。\nprivate fun extractApkAndInstall(inputStream: InputStream?, context: Context) { val zipInputStream = ZipInputStream(inputStream) var zipEntry = zipInputStream.nextEntry while (zipEntry != null) { if (zipEntry.name == \u0026#34;app-release.apk\u0026#34;) { val tempFile = File(\u0026#34;${context.cacheDir.absolutePath}/app.apk\u0026#34;) if (tempFile.exists()) { tempFile.delete() } tempFile.createNewFile() tempFile.deleteOnExit() FileOutputStream(tempFile).use { fos -\u0026gt; zipInputStream.copyTo(fos) } installApkFromFile(context, tempFile) break } zipEntry = zipInputStream.nextEntry } zipInputStream.closeEntry() zipInputStream.close() } 最後一個函式是 installApkFromFile() ，這就沒有什麼好解釋的了，網路上都找得到。比較需要注意的地方是：因為暫存檔是寫到 EinkBro App 的相關 cache 資料夾，所以要把該檔案分享給其他 App 或是系統使用時，得透過 fileprovider 的方式來提供。這件事我在之前就做過了，所以這裡只要單純的利用 FileProvider 拿一下 Uri 就行。\nprivate fun installApkFromFile(context: Context, file: File) { val apkUri = FileProvider.getUriForFile( context, BuildConfig.APPLICATION_ID + \u0026#34;.fileprovider\u0026#34;, file ) val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(apkUri, \u0026#34;application/vnd.android.package-archive\u0026#34;) flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_GRANT_READ_URI_PERMISSION } context.startActivity(intent) } 開發完成後，以後想要更新版本，就只要進到 Settings \u0026gt; About，點一下就行了。未來還是有可以再簡化的地方：比方說，隔個幾天就查一下是不是有新的版本，有的話不用等使用者點，可以在畫面上直接提示用戶。\n這種作法有可能會有點煩人，所以之後如果有要做的話，應該也會做成是選項，預設會是關閉的。只有像我這種一定都要用最新版的人，再自己去打開來用就好。\nAbout 頁面 相關連結 feat: add update with snapshot feature · plateaukao/einkbro@1b8a487\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%BE%9E-einkbro-app-%E4%B8%AD%E7%9B%B4%E6%8E%A5%E5%AE%89%E8%A3%9D%E5%8D%87%E7%B4%9A%E7%89%88%E6%9C%AC/","summary":"\u003ch3 id=\"早該做的提升效率的事從-einkbro-app-中直接安裝升級版本\"\u003e早該做的提升效率的事 — 從 EinkBro App 中直接安裝升級版本\u003c/h3\u003e\n\u003cp\u003e手邊的 Android 手機，外加電子書閱讀器設備，總共有十幾台。常常在 EinkBro 更新後，不論在使用哪一台，都要開網頁到 Github 網站，下載 snapshot 版本，或是最新的 release 版，再安裝。\u003c/p\u003e","title":"從 EinkBro App 中直接安裝升級版本"},{"content":"從去年開始做回顧，有做回顧的好處是，比較會省視一下自己一整年來的成長和值得記憶的事件。今年，就先參考去年的方式來列一下各方面的努力和收穫吧。\nEinkBro 首先，重頭戲自然是 EinkBro 上的成就。今年最大的成就是終於出書了。做為一個軟體工程師，寫寫軟體是再也普通不過的事。為了記錄自己的開發經驗，很多人也會寫技術文章。但只有少數人會累積到足夠多的量，再進而出書的。一來，出書很花時間，二來，出書通常賺到的是名聲，而不是金錢。要投入大量的時間來做這種吃力不討好的事，很多人因為生涯規劃，時間安排的考量，不會進到這一階段。\n在連續兩年獲得 iThome 鐵人賽的佳作後，原先沒有想要那麼花時間寫書的我，也忍不住跟出版社連繫。第一次獲獎，可以說是僥倖，剛好題材比較特別；但是第二次以同樣主題再次參加，也依然獲獎時，應該是真的有點料吧。這時自信心又再大了些，想要再朝下一步邁進。\n寫書的過程還算平淡，畢竟大部分的內容都已經經過兩次鐵人賽的鍛鍊，剩下的是轉換成書籍需要的內容細緻度，排版上的要求，圖片的要求。寫書的經驗也有另外撰寫成一篇文章。\nAndroid開源專案「真」實戰啟航：瀏覽器App EinkBro開發者帶你逐步從UI設計、UX提升到多功能實現秘技全解析（iThome鐵人賽系列書） (電子書)\n再來是開發相關的一些數據：\n大 Release 26 次: 9.17.0 ~ 9.23.0, 10.0.0 ~ 10.17.0, 11.0.0，維持在兩週一次的發布頻率。只是從下半年後，想要做的功能愈來愈少，所以下半年的發布間隔通常都會是二到三週，而且沒有什麼在進 codes。 commit 數：371。平均一週有 7.1 次，比去年少了些。今年工作上也比較忙，再加上沒有什麼改善的點子了，所以數目比較少。(但今年還是實作了好幾個我自己很喜歡的功能) 星星數：從去年的 458，一路來到 777。到年底前應該有機會突破 780 吧。雖然不如別人的 Open Source 專案可以一上線就萬眾矚目，拿到破千或破萬的星星，但穩定的成長也表示持續有(很少)人注意到 EinkBro 這個專案。 技術演講：今年沒有。去年在 COSCUP 2022 的演講感覺有點冷清。一方面是因為主題冷僻；另一方面是因為這年頭 Mobile App 的開發愈來愈式微了，想聽的人本來就不多了。所以，今年還是把主要的時間花在寫書，和寫文章上頭。 技術文章分享(不僅限於 EinkBro 相關)：18 篇，比去年的 14 篇多；但因為中間多了好幾篇非 EinkBro 的文章。 * Kotlin Coroutine Channel 的使用簡介 * 如何在 Android 設備上修改顏色模式 其他 Side Project 今年在其他 side project 上也是大豐收，寫了好幾個自己覺得實用的工具。\nflutter_mira 在 MacOS 上可以快速調整文石 BOOX Mira 電子紙螢幕參數的小工具。\nquikrotate 去年的作品，今年加入了可以將系統色彩切換為黑白的模式，對於彩色電子紙設備來說，臨時想要進入黑白模式提高解析度時很有用。\nnotable 別人利用文石的 SDK 寫出來的筆記 App，使用了 Jetpack Compose。我有拿來稍微改了一下，讓它支援更多手勢操作。\nOpenSettings 在部分電子紙閱讀器上，廠商都把系統的設定 App 隱藏起來，不讓使用者可以設定一些比較細節的功能。這個小 App 單純只是讓使用者可以再把系統設定 App 叫出來。\nflutter_translate_image 可以截取電腦上的畫面，直接做翻譯的小 App。\naskeinkbro 整合 EinkBro 到 Koreader 中。可以直接在 Koreader 中叫起 EinkBro 的小視窗，查看翻譯，或是自定的搜尋字串。\n讀過的書 今年的狀況比去年好，下半年還有持續在看書，雖然看的都是小說就是了。而且常常發懶，看了沒有做記錄，所以過程中應該少了好幾本。算下來大約 41 本書(其中今野敏的警長小說有 6 本，5本中文，1 本日文；四月看的 RING 也是日文小說)。\n運動 今年的運動進展只能說是一波三折。年初前三個月都還很正常的在跑步，但到了春假時因為髁骨莫名奇妙痛到睡不著，去看了醫生後，漸漸恢復，就不太敢再去跑步了。\n從八月底開始，在同事的號召下，開始加入公司的網球社打網球，也因緣際會，加入了社區的退休人士健康網球團，固定週六日早上六七點會一起打兩個小時的球，平均一次可以打個三盤比賽。累積下來，我現在也是有幾十盤實戰經驗的網球愛好人士了(只是大多數時間其實都是在揀球)。\n語言學習 今年太廢了，沒有什麼系統性的學習和成長，明年應該要再來訂一下目標，有系統地學習一下。\n","permalink":"https://plateaukao.github.io/blog/posts/2023-%E5%B9%B4%E5%9B%9E%E9%A1%A7/","summary":"\u003cp\u003e從去年開始做回顧，有做回顧的好處是，比較會省視一下自己一整年來的成長和值得記憶的事件。今年，就先參考去年的方式來列一下各方面的努力和收穫吧。\u003c/p\u003e\n\u003ch3 id=\"einkbro\"\u003eEinkBro\u003c/h3\u003e\n\u003cp\u003e首先，重頭戲自然是 EinkBro 上的成就。今年最大的成就是\u003cstrong\u003e終於出書了\u003c/strong\u003e。做為一個軟體工程師，寫寫軟體是再也普通不過的事。為了記錄自己的開發經驗，很多人也會寫技術文章。但只有少數人會累積到足夠多的量，再進而出書的。一來，出書很花時間，二來，出書通常賺到的是名聲，而不是金錢。要投入大量的時間來做這種吃力不討好的事，很多人因為生涯規劃，時間安排的考量，不會進到這一階段。\u003c/p\u003e","title":"2023 年回顧"},{"content":"\n2016/08/21 (日) 台北-\u0026gt;富山機場\n富山機場-\u0026gt; 富山 (巴士)\n富山-\u0026gt;立山(電鐵富山)\n立山-\u0026gt;室堂(立山 cable car)\n室堂走山路到雷鳥莊\n看夕陽\n住宿: 雷鳥莊\n2016/08/22 (一) 健行 (7:29~ 15:08):從雷鳥莊出發，立山繞一圈。只差沒有上劍岳\n住宿: 雷鳥莊\n2016/08/23 (二) 室堂-\u0026gt;扇澤\n中間停留大觀峰，黑部平，黑部湖，黑部水庫。\n搭乘黑部湖游覽船\n從扇澤搭車到信濃大町\n(中間有跑去大町溫泉鄉的溫泉泡湯，兼等車)\n溫泉的休息室\n住宿: 信濃大町的旅籠\n2016/08/24 (三) 從信濃大町搭電車到松本\n住宿: 松本城旁的 youth hostel\n逛松本市美術館(有草間彌生作品)\n參加當地的啤酒節\n夜訪松本城，走回 youth hostel\n2016/08/25 (四) 從松本前往上高地\n逛上高地\n住宿：明神池旁的民宿還是旅館\n2016/08/26 (五) 再逛上高地\n前往平湯溫泉\n再前往白川鄉合掌村\n逛白川鄉\n泡白川鄉之湯\n住宿：白川鄉\n2016/08/27 (六) 逛合掌造，民家園\n搭高速巴士回富山車站\n到富山公園，看看面河的星巴克\n2016/08/28 (日) 早起跑步，從富山公園，跑到海邊\n逛一下富山美術館\n上市役所俯看富山市\n搭飛機回國\n","permalink":"https://plateaukao.github.io/blog/posts/2016-%E7%AB%8B%E5%B1%B1%E4%B9%8B%E6%97%85/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_f2-YcUWxMkLTeCV6B6ZYRw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_RL-SDOEOnKCoyYzDmJ3JXg.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_hjnQE-AcHCJL_V1zPKOgCA.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"20160821-日\"\u003e2016/08/21 (日)\u003c/h3\u003e\n\u003cp\u003e台北-\u0026gt;富山機場\u003c/p\u003e\n\u003cp\u003e富山機場-\u0026gt; 富山 (巴士)\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_T6xFhPpIcs1cZ6dRIAg2mw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_qaUA1YLJHD37a3np9VvhZQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e富山-\u0026gt;立山(電鐵富山)\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_Eq_XayzybKEggXUhzbEutA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e立山-\u0026gt;室堂(立山 cable car)\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/52d62035d51e/1_aJj09WMBHu4mZ07WcExzNw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e室堂走山路到雷鳥莊\u003c/p\u003e","title":"2016 立山之旅"},{"content":"這篇文章會分享 EinkBro 在整合 OpenAI tts API 時，藉由 Kotlin Coroutine Channel 處理 API 邏輯和播放的實作。\n前言 原先在 EinkBro 的朗讀功能，是利用系統的文字轉語音功能。這樣做的好處是，不用處理太多複雜的邏輯，只需要把文字丟給系統就好；但缺點是每一台設備上的文字轉語音功能支援度不同。很多時候還需要使用者事前先下載好相關資料檔，或是從系統設定中指定好想要的語音語言。對一般使用者來說，難度有點高。在使用時，也需要請使用者自己指定一下語言，不然可能會唸不出來。即便是我自己的設備，也不見得每一台都能夠正常運作。\n2023 年 11 月初，OpenAI 公布了一支 tts 的 API，可以很完美地將文字轉為語音，效果比市面上大部分系統內建的 tts 好很多。如果是英文的話，幾乎聽不出來是機器唸的，該有的抑仰頓挫也都有。\n既然 EinkBro 已經整合了 chatgpt API，再順便整合一下 tts API 並不需要太大的工夫。\n整合 OpenAI tts API 這部分是比較單純的地方，只要對著 endpoint 把需要的 request parameters 代入就可以得到產生好的語音 binary。\ntts 的 endpoint 是 https://api.openai.com/v1/audio/speech ，需要的參數我把它包裝成 TTSRequest class：\n@Serializable data class TTSRequest( val input: String, val model: String = \u0026#34;tts-1\u0026#34;, val speed: Double = 1.0, val voice: String = \u0026#34;alloy\u0026#34;, val format: String = \u0026#34;mp3\u0026#34; ) 詳細的 tts API 說明可以參考官網：https://platform.openai.com/docs/api-reference/audio\n回傳值的處理 如果一切順利的話，在 response 中會是語音的 binary 內容。在這裡，我先用 SuspendCoroutine 實作，確保 tts 可以在拿到語音後，才回傳 ByteArray 結果。\nsuspend fun tts(text: String): ByteArray? = suspendCoroutine { continuation -\u0026gt; val request = createTtsRequest(text) client.newCall(request).execute().use { response -\u0026gt; if (response.code != 200 || response.body == null) { return@use continuation.resume(null) } try { continuation.resume(response.body?.bytes()) } catch (e: Exception) { continuation.resume(null) } } } 拿到語音內容後，關於怎麼播放，主要有兩種方式：一是利用 Android 很早就有提供的 MediaPlayer 來進行；另一個方式是更有彈性，能提供更多細微操作的 ExoPlayer。後者實作必須要再 import 相關的函式庫才行，所以為了實作上簡便，而且不想增加 App size，我選擇了前者。\nMediaPlayer 的實作雖然很簡單，但它有個限制是：它無法直接播放來自於記憶體中的 ByteArray。為了繞過這個問題，拿到語音內容後，必須先把它儲存成文件，再讓 MediaPlayer 去播放該 File。\n下面的實作便是先建立暫時檔案，將資料寫入，再交由 MediaPlayer 播放。MediaPlayer 實例是在事前先建立好的，避免不斷產生新的 instance，造成資源的浪費。每次播放完畢，會收到 onCompletionListener ，這時，可以呼叫 reset() ，讓 MediaPlayer 可以再進行下次的播放。\nprivate suspend fun playAudio(context: Context, data: ByteArray) = suspendCoroutine { cont -\u0026gt; // Creating a temporary file val tempFile = File.createTempFile(\u0026#34;temp\u0026#34;, \u0026#34;aac\u0026#34;, context.cacheDir) tempFile.deleteOnExit() val fos = FileOutputStream(tempFile) fos.write(data) fos.close() java.io.FileInputStream(tempFile).use { fis -\u0026gt; mediaPlayer.setDataSource(fis.fd) mediaPlayer.prepare() mediaPlayer.start() mediaPlayer.setOnCompletionListener { tempFile.delete() mediaPlayer.reset() cont.resume(0) } } } 在 Android 官網上，可以看到 MediaPlayer 的狀態圖如下：\n利用 Coroutine Channel 控制播放進度 基本的播放功能完成後，再來是這篇文章的重點：怎麼利用 Coroutine Channel 來控制播放的內容。\n一般的網頁，少說會有幾十個句子，多的話，上百個句子也是很正常的。如果要一次把所有的文字內容餵給 tts API，上面的 API 說明中可以看到，它最多一次只能吃 4096 個字元而已；如果不特別檢查的話，可能常會遇到過長的情況。\n再說，如果餵的文字太多，openAI tts 產生語音的時間也會隨之拉長(我猜的)。所以，我先將網頁中的文字依照句號(. 或是。)建立成 List ，打算一次只餵一句話或兩句話，讓朗讀功能能夠很快就開始運作，而且不會一次就把所有內容處理完，會邊播放，邊處理接下來的文字內容。畢竟，tts API 不便宜啊。\n處理文字成 List 的方式：\nval sentences: List\u0026lt;String\u0026gt; = text.split(\u0026#34;(?\u0026lt;=\\\\.)|(?\u0026lt;=。)\u0026#34;.toRegex()) 再來是 Channel 的建立。我打算一次最多就處理三個句子，一旦句子的語音資料回來後，就可以依序讓 MediaPlayer 播放。而 Channel 能夠很有效地處理這種情形。\nprivate var byteArrayChannel: Channel\u0026lt;ByteArray\u0026gt;? = null fun readText(context: Context, text: String) { // 在 Channel 中，最多能塞三個元素 byteArrayChannel = Channel(3) viewModelScope.launch(Dispatchers.IO) { val sentences: List\u0026lt;String\u0026gt; = text.split(\u0026#34;(?\u0026lt;=\\\\.)|(?\u0026lt;=。)\u0026#34;.toRegex()) for (sentence in sentences) { val data = openaiRepository.tts(sentence) if (data != null \u0026amp;\u0026amp; byteArrayChannel != null) { // 拿到句子的語音檔，確定有資料後，便往 channel 中送。 // 這邊代表著 producer byteArrayChannel?.send(data) } } } 有了 Producer 後，再來是 Consumer 端的實作。Consumer 也是實作在 readText() 函式中。先將它包在 ViewModelScope 中，再利用 for loop 去取得 byteArrayChannel 中的元素。只要上面的實作不斷地有資料 send 到 channel 中，這裡的 for loop 就會不斷地播放語音內容。\nfun readText(context: Context, text: String) { ... viewModelScope.launch(Dispatchers.IO) { for (data in byteArrayChannel!!) { playAudio(context, data) delay(200) } byteArrayChannel = null } } 最後，要再處理一下，如果使用者聽一聽，不想要再聽的時候，必須把整個 Channel 停掉。我在 ViewModel 中實作了 stop()，將該停的該關的 resource 做處理。這麼一下來，下次使用者再點擊朗讀時，又可以再次正常運作。\nfun stop() { byteArrayChannel?.cancel() byteArrayChannel = null mediaPlayer.stop() mediaPlayer.reset() } 完成的功能影片 後續 目前的實作是每一句話都會去打一次 API 和儲存成文件。理論上應該可以做些實驗，找出大約多少句子為一個單位去打 API，讓系統的反應速度會是最佳的，又不會過度讀寫檔案系統。但，除非是有很多使用者重度地在使用這功能，不然，現在的版本就很夠用了。\n相關資料 feat: roughly working stop tts. feat: use single mediaplayer, add setting in chatgpt to use it for tts feat: add openai tts api ","permalink":"https://plateaukao.github.io/blog/posts/kotlin-coroutine-channel-%E7%9A%84%E4%BD%BF%E7%94%A8%E7%B0%A1%E4%BB%8B/","summary":"\u003cp\u003e這篇文章會分享 EinkBro 在整合 OpenAI tts API 時，藉由 Kotlin Coroutine Channel 處理 API 邏輯和播放的實作。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2b95a342a8b3/1_CZivT9zQC0OMJ7kW6Z6Peg.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"前言\"\u003e前言\u003c/h3\u003e\n\u003cp\u003e原先在 EinkBro 的朗讀功能，是利用系統的文字轉語音功能。這樣做的好處是，不用處理太多複雜的邏輯，只需要把文字丟給系統就好；但缺點是每一台設備上的文字轉語音功能支援度不同。很多時候還需要使用者事前先下載好相關資料檔，或是從系統設定中指定好想要的語音語言。對一般使用者來說，難度有點高。在使用時，也需要請使用者自己指定一下語言，不然可能會唸不出來。即便是我自己的設備，也不見得每一台都能夠正常運作。\u003c/p\u003e","title":"Kotlin Coroutine Channel 的使用簡介"},{"content":"會想要修改模式是因為在彩色電子紙閱讀器上，廠商通常只有提供增強整體顏色，加強鮮豔度和色彩亮度等選項，但並沒有提供 R, G, B 個別顏色的調整方式。\n每個人對於色彩的接受程度不同，比較嚴重的像是色盲，無法分辨自然世界中的某些顏色。這時，如果可以調整顏色模式的話，對於閱讀來說，會更加舒服。另一點是，雖然買的是彩色電子紙閱讀器，但有的時候還是會希望畫面以灰階的方式呈現，避免顏色的干擾。純脆黑白的畫面，解析度也比較高。\n手動調整 今天在看 Android 預設的 Setting App 時，發現在無障礙設定下有個色彩校正的選項，開啟後可以調整為”綠色弱視”、”紅色弱視”和”藍色弱視”。雖然這是用來讓色弱的人透過調整不同顏色的比重，讓他們更容易識別畫面，但是對於一般人來說，剛好也可以拿來切成自己比較喜歡的色彩組合。像我常常覺得文石 Onyx Tab Ultra C 的紅色總是過於鮮豔，如果切換到”紅色弱視”的選項，整個畫面對我來說會更協調。\n將這個發現分享到網站上後，有網友問說，不知道有沒有可以切換成灰階模式的方法。仔細想想，確實有的時候會希望將模式改成黑白的。在彩色閱讀器上，如果畫面是黑白的，它的解析度是 300ppi，但在顯示彩色內容時只有 150 ppi。如果是在文字較多的情況，或是不想因為顏色而分心的場合時，能夠切換到灰階模式會是更為理想的。\n經過一番搜尋後，發現其實可以透過設定 Developer Options 中的參數達成。在開發者進階選項中，比一般設定裡多了一個 “全色盲” 的選項，設定後就會讓整台設備以灰階的方式呈現。\n開發快速鍵來開關灰階模式 找到灰階模式可行的方式後，接下來就想說是不是能有更方便的操作方式。不然，每次都要進到開發者選項也是很煩人的。既然是在開發者選項中，通常都會有其他非手動的設定方式，能讓開發者快速地做調整，進行測試。\n很幸運地，找到 Android 原始碼中關於 Accessibility 的相關程式碼片段:\nplatform_frameworks_base/core/java/android/view/accessibility/AccessibilityManager.java at master ·…\nplatform_frameworks_base/services/core/java/com/android/server/display/color/ColorDisplayService.jav…\n再順著這些關鍵字到 github.com 上找一找，就找到已經有人做了我想要的功能：把切換灰階模式實作在 TileService 中。如此一來使用者就可以很方便的將這個功能加到系統選單中。\nGitHub - fei-ke/Greyscale: 快速切换灰度模式\n整個程式主要就兩個檔案，一個是 TileService 的實作，而真正的邏輯是在 Util.java 中。\npublic static boolean isGreyscaleEnable(Context context) { ContentResolver contentResolver = context.getContentResolver(); return Secure.getInt(contentResolver, DISPLAY_DALTONIZER_ENABLED, 0) == 1 \u0026amp;\u0026amp; Secure.getInt(contentResolver, DISPLAY_DALTONIZER, 0) == 0; 如同其他系統設定，想要修改的話，得透過調整 Secure Setting 中的參數；因此，需要事前為 App 取得相關的權限。\n取得權限的方式，不像一般的權限可以跳出對話框讓使用者同意，而是需要使用 adb，在電腦上操作，或是如果已經 root 過的設備，可以利用 su 來給予權限。\nadb 的指令如下(info.plateaukao.quickrotate 是我的 application id)：\nadb shell pm grant info.plateaukao.quickrotate android.permission.WRITE_SECURE_SETTINGS 整合到 QuickRotate 雖然有好心人已經開發出來 App 了，但早就已經為好多個功能寫了 TileService 的我怎麼會想就這麼再裝一個別人寫好的 TileService 呢？所以，迅速打開 QuickRotate 專案，新增了一個 TitleService，把找到的實作給搬了進來。這就是 Open Source 的威力呀~\nfeat: add colorspace grayscale toggle · plateaukao/quickrotate@66dbefe\n示範影片 demo of QuickRotate App with Grayscale toggling\n相關連結 QuickRotate v2.2.0 ","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%9C%A8-android-%E8%A8%AD%E5%82%99%E4%B8%8A%E4%BF%AE%E6%94%B9%E9%A1%8F%E8%89%B2%E6%A8%A1%E5%BC%8F/","summary":"\u003cp\u003e會想要修改模式是因為在彩色電子紙閱讀器上，廠商通常只有提供增強整體顏色，加強鮮豔度和色彩亮度等選項，但並沒有提供 R, G, B 個別顏色的調整方式。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/4d4eead4ed19/1_ZR1KLpfP94QtjFuIsBSSqA.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e每個人對於色彩的接受程度不同，比較嚴重的像是色盲，無法分辨自然世界中的某些顏色。這時，如果可以調整顏色模式的話，對於閱讀來說，會更加舒服。另一點是，雖然買的是彩色電子紙閱讀器，但有的時候還是會希望畫面以灰階的方式呈現，避免顏色的干擾。純脆黑白的畫面，解析度也比較高。\u003c/p\u003e","title":"如何在 Android 設備上修改顏色模式"},{"content":"\n之前為了讓文石的 Mira 螢幕可以有比較方便的模式/參數調整方式，修改了 Open Source 的 Node.js App，讓它可以支援更多的參數。但是，該程式是 cli 型式，需要開啟 Terminal 並輸入指令才可以執行。雖然我有利用 Automator 做了幾個 script，並綁定系統快速鍵來操作；但還是覺得，如果能有個 UI 介面，滑鼠點一點就好的話，應該會是更理想的。\n所以，用 Flutter + menubar plugin 快速刻了一個小 MacOS App，能夠常駐在 menubar 上，提供幾個我常用的選項(模式切換，重新啟動 antishake，關前光燈，畫面重繪等)。\n不過，在使用上卻遇到了一個問題：如果是在 Android Studio 中編譯並順便執行的話，它可以正常運作；如果是從 commandline 執行的話，程式也可以正常運作。但偏偏最常的啟動方式 (從應用程式中，或是從 Finder 中雙擊程式執行) 是無效的。\n由於在實作裡，是透過 dart:io 的 Process.start 呼叫已經編譯好的 Node.js App — mira ，所以我懷疑應該是這裡出了問題。要嘛就是沒有正確讀到所需要環境變數；不然就是 Flutter 要呼叫 Node.js 的 cli 時，需要再多動點手腳才行。\n經過一番研究後發現，沒想到 mira 這 Node.js 程式，竟然是個 soft link，連結到一個 cli.js 檔案。也就是說 mira 其實底子裡是個 javascript 檔案。當 Flutter 透過 Process.start 想要去執行它時，應該是遇到了不知道怎麼處理 javascript file，所以無法正常執行。\n為了解決這件事，我去找了一下要怎麼在 Terminal 中執行 Node.js 程式的說明。下面是個簡單的範例，可以透過 node some_javascript.js 來執行。\necho \u0026#34;console.log(1+1);\u0026#34; \u0026gt;\u0026gt; test-node.js node test-node.js 當然，這樣子的呼叫方式是假設 node 已經在 PATH 環境變數中。如果想要降低這件事的不確定性，可以將 node 換成絕對路徑就行。在 Mac 上如果是用 brew 安裝 Node.js 的話，它的路徑應該會是\n/opt/homebrew/bin/node\n有了這些資訊後，我把呼叫 mira app 的函式再包了一層，變成以下的實作方式：\nFuture\u0026lt;void\u0026gt; _commandMiraJs(List\u0026lt;String\u0026gt; action) async =\u0026gt; await Process.start( \u0026#39;/opt/homebrew/bin/node\u0026#39;, [MIRA_JS_PATH, ...action], ); const String MIRA_JS_PATH = \u0026#39;/opt/homebrew/bin/mira\u0026#39;; 每個指令都會利用絕對路徑的 node 來執行；除了第一個參數是 mira app 外，後面的列表就會是各個所需要相關參數。以切換到閱讀模式這件事來當例子，它的函式就會是這麼實作：\nFuture\u0026lt;void\u0026gt; _commandRead() async =\u0026gt; await _commandMiraJs( \u0026#39;settings --dither-mode 3 --contrast 7 --black-filter 10 --white-filter 12 --refresh-mode direct\u0026#39; .split(\u0026#39; \u0026#39;)); 解決了呼叫 mira 的方式後，以後就可以開心地使用這個由 Flutter 寫成的 UI 界面了！哪天如果還有什麼常用的設定組合，只要再多個 menu item，然後加上對應的參數，重新編譯一下就行了！\nFlutter Mira App UI 目前 Settings 是空的。哪天有空，會把一些可以調整的參數做成 progress bar，讓使用者(我自己)能夠臨時調整一些參數 (像是前光燈的色溫和亮度，對比度的高低，畫面更新快慢的程度等)。\n相關連結 mira-js 的開發 How to Run a Node.js Application on a Mac ","permalink":"https://plateaukao.github.io/blog/posts/%E4%BD%BF%E7%94%A8-flutter-%E5%9C%A8-macos-%E4%B8%8A%E6%99%82%E6%80%8E%E9%BA%BC%E5%9F%B7%E8%A1%8C-nodejs-%E7%9A%84%E7%A8%8B%E5%BC%8F/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/7a35cdf2b521/1_MmAoOHIWq8Y2UMbUeJL6fQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e之前為了讓文石的 Mira 螢幕可以有比較方便的模式/參數調整方式，修改了 Open Source 的 Node.js App，讓它可以支援更多的參數。但是，該程式是 cli 型式，需要開啟 Terminal 並輸入指令才可以執行。雖然我有利用 Automator 做了幾個 script，並綁定系統快速鍵來操作；但還是覺得，如果能有個 UI 介面，滑鼠點一點就好的話，應該會是更理想的。\u003c/p\u003e","title":"使用 Flutter 在 MacOS 上時，怎麼執行 Node.js 的程式"},{"content":"期待了很久，一直不知道怎麼實作的功能終於做出來了！在 Android WebView中，一般來說，只可以長按非連結的文字，選取文字進行複製或其他的動作。如果想要選取連結上的文字的話，必須要用些小技巧才能達成。\n下面的 sequence diagram 說明了如何透過一系列的操作達到這效果。\n步驟 在網頁載入完成後，要塞一段程式去偵測文字選取範圍改變時的區域 當下達指令要選取連結文字時，要把點擊處的 a node 的 href 屬性拿掉 再模擬一次長按的行為 (simulateLongClick()) 等選取好連結字串後，再回復原先 a element 中的 href 屬性 接著再改善 把原先的 ActionMode DialogFragment 變成是Compose 的 Dialog，讓它不會對 ActionMode selection anchor 的顯示造成影響 自行處理視窗超出畫面的問題 第一步中的 udpateSelectionRection() 會回傳 Selection Range 的 position，可以用來調整 Context Menu 的位置 View gist\nView gist\nView gist\n相關連結 Release Release v10.16.0 · plateaukao/einkbro\n","permalink":"https://plateaukao.github.io/blog/posts/einkbro-%E6%9C%9F%E5%BE%85%E5%B7%B2%E4%B9%85%E7%9A%84%E5%8A%9F%E8%83%BD-%E9%81%B8%E5%8F%96%E9%80%A3%E7%B5%90%E7%9A%84%E6%96%87%E5%AD%97/","summary":"\u003cp\u003e期待了很久，一直不知道怎麼實作的功能終於做出來了！在 Android WebView中，一般來說，只可以長按非連結的文字，選取文字進行複製或其他的動作。如果想要選取\u003cstrong\u003e連結上的文字\u003c/strong\u003e的話，必須要用些小技巧才能達成。\u003c/p\u003e","title":"EinkBro 期待已久的功能 — 選取連結的文字！"},{"content":"這篇文章將說明怎麼實作 Jetpack Compose Dialog，讓它的外圈能夠沒有暗掉的效果，而且對話框周圍能有黑色的框線。\n這個需求主要是來自於在開發 EinkBro 時，原生 Android Dialog 的介面並不是很適合於電子紙閱讀器。外圈的 dim effect (暗化、遮罩效果)在一般手機平板上，能讓使用者更專注於對話框的操作；但在閱讀器上，會造成畫面上的所有元素都要重新調整對比度，增加留下殘影的機會。\n所以，如果能夠在顯示對話框時，單純只呈現對話框本身，不去調整它之外的區域，在電子紙上會是比較恰當的行為。\n為了要達成這種效果，在早期使用 xml + viewbinding 開發介面時，能夠透過客製化的主題設定(Theme style xml)達成。而在使用 Jetpack Compose 時，我是最近才找到相關的實作方式！得知這個 UI 上的大障礙怎麼解決後，就可以逐步把 EinkBro 中既有的原生對話框一一改寫為 Compose 囉。\n實作 下面來說明一下怎麼實作。主要可以分為兩個部分：\n確保對話框以外的區域不要暗化 (dim effect) 為對話框加上黑色框線，讓使用者依然可以明確的知道對話框大小 外圍不暗化 Compose Dialog 底層其實還是有 Window 存在。所以，只要能取得它底層的 window 元件，就可以去改變它的性質，達到想要的效果。真正的解法，說穿了，只需要一行程式碼：\n(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(0f) 以下是來自於 ChatGPT 的詳細解說：\nLocalView.current.parent: 這一部分是使用 Jetpack Compose 的 LocalView 功能，它可以讓你訪問當前視圖（View）的層次結構。.parent 表示取得當前視圖的父視圖，也就是包含這個視圖的容器。\nas DialogWindowProvider: 這一部分是將父視圖轉換為 DialogWindowProvider 類型。這意味著我們假設父視圖是一個能夠提供對話框窗口的提供者，這是用來設定對話框屬性的一種方式。\n.window: 一旦我們將父視圖轉換為 DialogWindowProvider 類型，我們可以使用 .window 來訪問與對話框相關的窗口屬性。\n.setDimAmount(0f): 最後，我們調用 .setDimAmount(0f) 方法，將背景模糊度設置為 0。這意味著對話框的背景不會變得模糊，完全透明，因為模糊度設置為 0。\n總結來說，這行程式碼的作用是設定 Jetpack Compose 中的對話框，讓其背景不會變得模糊，完全透明。這可能用於需要在對話框彈出時讓背景保持清晰的情況。\n解決了這個問題後，下一個就容易多了。\n加上黑框線 在 Compose 中，如果想為元件加上邊線的話，只需要在 modifier 中，使用 border 的語法就行。如下圖，我為對話框加上了 1.dp 粗的框線，還加上圓弧邊角的效果。\n這麼一來，就完成了 Compose Dialog 所需要客製化。出來的效果，也跟原先 xml 實作的方法差不多。\n效果圖 目前，僅先在設定畫面中的 ChatGPT action list 中採用。\n相關連結 commit: https://github.com/plateaukao/einkbro/commit/9c43625198d5c5ca79e13d0fcee9cd7917b13726\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%AA%BF%E6%95%B4-jetpack-compose-dialog-%E7%9A%84%E5%A4%96%E8%A7%80/","summary":"\u003cp\u003e這篇文章將說明怎麼實作 Jetpack Compose Dialog，讓它的外圈能夠沒有暗掉的效果，而且對話框周圍能有黑色的框線。\u003c/p\u003e\n\u003cp\u003e這個需求主要是來自於在開發 EinkBro 時，原生 Android Dialog 的介面並不是很適合於電子紙閱讀器。外圈的 dim effect (暗化、遮罩效果)在一般手機平板上，能讓使用者更專注於對話框的操作；但在閱讀器上，會造成畫面上的所有元素都要重新調整對比度，增加留下殘影的機會。\u003c/p\u003e","title":"調整 Jetpack Compose Dialog 的外觀"},{"content":"2023/09/13\n테슬라 AI슈퍼컴 ‘도조’가 뭐길래…머스크의 큰 그림은 뭐길래: 是什麼意思\n큰 그림은: big picture，大局\n11일(현지시간) 테슬라 주가가 미국 나스닥 시장에서 10.1%나 폭등했다. 모간스탠리가 테슬라의 ‘도조’(Dojo)’ 컴퓨팅 가치를 낙관하며 목표 주가를 400달러로 올린 덕분이다. 모건스탠리는 도조 프로젝트만으로도 테슬라의 기업 가치가 최대 5000억 달러(약 664조원)에 달할 것으로 평가했다.\n나스닥: Nasdaq\n모간스탠리: Morgan Stanley\n낙관하며: 樂觀地\n기업 가치: 企業價值\n에 달할 것: 高達\n도조 프로젝트는 일론 머스크 최고경영자(CEO)가 2021년 8월 처음 공개한 AI 슈퍼컴퓨터 개발 프로젝트다. 테슬라가 완전자율주행 소프트웨어를 만들기 위해 지난달부터 본격적으로 가동을 시작한 ‘도조’는 초당 100경 번 연산이 가능한 엑사플롭스급 성능을 갖추고 있다.\n최고경영자: CEO 最高經營者\n완전자율주행: 全自動駕駛\n본격적으로: (本格的)正式，全面\n갖추고 있다: 擁有(某個特性)\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9F%93%E5%9C%8B%E8%AA%9E%E6%96%B0%E8%81%9E%E8%A7%A3%E6%9E%9020230913/","summary":"\u003cp\u003e2023/09/13\u003c/p\u003e\n\u003ch4 id=\"테슬라-ai슈퍼컴-도조가-뭐길래머스크의-큰그림은\"\u003e\u003ca href=\"https://v.daum.net/v/20230912210517721\"\u003e테슬라 AI슈퍼컴 ‘도조’가 뭐길래…머스크의 큰 그림은\u003c/a\u003e\u003c/h4\u003e\n\u003cp\u003e뭐길래: 是什麼意思\u003c/p\u003e\n\u003cp\u003e큰 그림은: big picture，大局\u003c/p\u003e\n\u003cp\u003e11일(현지시간) 테슬라 주가가 미국 나스닥 시장에서 10.1%나 폭등했다. 모간스탠리가 테슬라의 ‘도조’(Dojo)’ 컴퓨팅 가치를 낙관하며 목표 주가를 400달러로 올린 덕분이다. 모건스탠리는 도조 프로젝트만으로도 테슬라의 기업 가치가 최대 5000억 달러(약 664조원)에 달할 것으로 평가했다.\u003c/p\u003e","title":"韓國語新聞解析(2023/09/13~)"},{"content":"\n這篇文章會說明我即將出版的書 “Android 開源專案「真」實戰啟航：瀏覽器 App EinkBro 開發者帶你逐步從 UI 設計、UX 提升到多功能實現秘技全解析” 的漫長出書過程。\nAndroid 開源專案「真」實戰啟航：瀏覽器 App EinkBro 開發者帶你逐步從 UI 設計、UX 提升到多功能實現秘技全解析（iThome鐵人賽系列書）【軟精裝】 | 博碩文化股份有限公司\n開發 Side Project — EinkBro 兩年多前，因為個人興趣，開始了一個 side project — EinkBro，逐步修改 github 上的某個 Open Source Android 瀏覽器。當時的電子書閱讀器不論是CPU性能或是電子紙的更新速度都還只能稱得上是堪用。為了讓自己手邊的閱讀器能更有價值，我在這個瀏覽器中加上了許多適合閱讀器特性的功能。\n雖然這些功能不見得需要很高超的實作技術才做得出來，但也經常是需要在網路上做做研究，找到最好的作法，或是合適的函式庫，再加上一些加工，才被加入 side project 中。怕時間一久，就忘了自己當初的思維是什麼，所以也開始會將這些實作的細節記錄在 Medium 平台上。一方面幫助自己日後可以快速回想起開發時的想法；另一方面想說，這些實作內容說不定也能協助到其他有共同需求的開發者。\n參加 iThome 鐵人賽 在2021年9月時，看自己一年下來也累積了十幾二十篇文章，應該能夠再稍微整理一下，來挑戰一下 iThome 鐵人賽。這是一個針對開發者的創作比賽：參賽者要連續三十天針對同一主題發表文章或是影片。參賽者得具有足夠的毅力和內容才有辦法做到這種高強度的要求。仗著我已經有二十篇左右的原始資料了，要將它們擴充成三十篇並不是件太難的事。\n如何開發適合電子書閱讀器使用的瀏覽器 Android APP :: 2021 iThome 鐵人賽\n第一次參賽，對於流程沒有那麼熟悉，在操作上花了不少心思。最終，順利完賽，還順便拿到了佳作獎。賽後也有寫了相關的比賽心得。\n參加 iThome 鐵人賽的感想\n又過了一年，時間來到2022年9月。這一年一樣是 EinkBro 功能爆增的一年。我再次帶著許多寫好的 Medium 文章原稿參加 iThome 鐵人賽。這次參賽從容了許多，不論是在主題的編排上，程式碼的準備，或是文章中需要的截圖，都比第一次有條理許多。\n而且，不同於其他每年參賽的人需要再找新的題目來寫，我依然是圍繞著 EinkBro App 的開發來參賽。除了一般功能的實作介紹外，還導入了 Jetpack Compose，以及多設備間互傳資訊的機制等。\n從開發瀏覽器 APP 學習 Android 實戰技巧，並搭上 Jetpack Compose 的列車 :: 2022 iThome 鐵人賽\n很幸運地，這次又得了佳作獎。雖然個人很希望第二次能夠再上一層樓，得到優選或是冠軍，不過事與願違，看來寫作能力還是有很大的改善空間。\n在連續參加兩屆鐵人賽而且都有得獎後，對於寫作這件事，心裡覺得似乎可以再往前多跨一步，是不是要來出本書呢。鐵人賽的得獎者都會收到來自博碩出版社編輯的合作邀請，看得獎者是不是想要把得獎的內容，再經過一些努力，編寫成書籍，讓更廣大的讀者看到。\n2021年時參賽並沒有想那麼多，光是連續寫作三十天，就夠讓人筋疲力盡了，寫書擺明了賺不到錢，為什麼要淌這趟混水呢？但2022年再次參賽得獎後，心境上有了點轉變。對於在 Medium 上寫文章，以及參加鐵人賽都已經有了不少經驗的我，是不是能再多嘗試點什麼呢？於是，出書這件事開始進入了腦中。\n出書歷程 在一月初的鐵人賽頒獎典禮後，原以為出書流程會跑很快的，但聽說這一屆想要出書的鐵人特別多，而且我又不是冠軍或優選的作品，不知道是不是因為這樣，真正整個啟動的時間其實已經是四月底的事了。\n下面是以我的例子，列出關於出書的一些時間點，可以供大家做參考：\n\u0026lt;前期資訊溝通與簽訂合約\u0026gt; 2023/01/06 在 LINE 上加出版社產品經理為好友，提供我的個人資訊\n2023/01/018 產品經理提供關於出書可以先做哪些準備的一些說明\n2023/02/06 整理好下面資料並交出\n我的基本資料 將得獎的作品全部複製到一份文件中，用來計算大約會有多少頁 新書規劃：需要自己先去市面上看看同領域的書有哪些，並且提出來自己書籍的特色在哪裡，以及對於書籍內容的規劃 2023/02/18 收到開會前作業的訊息，提供自己方便跟出版社開會的時段。對方也會提供了一些關於出書的相關文章，讓我有更多的心理準備，和事前準備。\n2023/04/28 線上會議，終於要真的動起來了。前一天產品經理提供了樣章，並針對之前我提出的問題做解答。\n2023/05/02 繳交下面資訊：\n書名和小標 預計完成書籍原稿的時間：分成樣章、半書、全書、預定出版時間(由產品經理提供)。以我自己來說，我提出的時間是 5/12，6/30，8/20，相當的保守。 選擇供參考的樣書(二至三本)：這是出版社提供給作者參考用的一些書籍。可以挑選跟自己相同領域的書籍，或是覺得內容排版自己想要參考的書。 \u0026lt;真的開始寫書稿\u0026gt; 買了 Office365 一年份，開始認真寫書啦。有沒有 Office365 不是重點。重點是要找個自己用得順手的文字編輯器，因為你即將跟它朝夕相處兩三個月。\n2023/05/12 交出書稿的樣章，因為需要檢查一下是不是已經能掌握一般的寫作格式和編排。\n2023/05/17 產品經理告知合約後續的說明\n2023/06/30 交出半本書的書稿。\n2023/07/20 原先預定繳交全書稿是在 8/20，不過我提前在 07/20 就交了全稿，前面幾章編寫順了之後，後面的章節就比較得心應手。花比較多的時間是用在學習 Word 排版(真的…)；將書中的截圖重新抓一次；程式碼原先是用截圖的，可能會轉回用文字的程式碼框框；還有將書中比較需要快速參考到網址，利用 QR Code Generator 生成 QR Code Image。\n下圖就是我自己規劃的章節完成時程表。只要能夠符合下面的進度就算安全，如果能夠超前的話，自己在心態上就會更為從容。\n\u0026lt;校稿、更多資訊提供、邀推薦序\u0026gt; 2023/08/10 在放完長假後，開始和編輯合作。\n2023/08/14 準備到時候書籍需要的封面和封底字樣，於 8/25 交出。\n2023/08/21 收到樣章的試排檔，確認是否 Okay，同時也被告知，可以開始找人幫忙寫推薦序，可以在 09/08 前提供就行。\n2023/08/22 封面設計完成！\n2023/08/23 書稿完成排版，可以請推薦者看。並且開始進行第一次校稿。\n2023/08/29 收到需要二校的 pdf 檔，並交出推薦序的內容。\n2023/08/30 再次收到封面的樣子，ISBN 已經有了。\n2023/09/04 收到來自編輯加上內文的 pdf，並請我從中挑出16頁要當成試讀頁，到時會放在網路書城的介紹頁中。\n2023/09/05 在出版社的官網已經有書籍的介紹頁。\nAndroid 開源專案「真」實戰啟航：瀏覽器 App EinkBro 開發者帶你逐步從 UI 設計、UX 提升到多功能實現秘技全解析（iThome鐵人賽系列書）【軟精裝】 | 博碩文化股份有限公司\n被告知將於 9/5 送印，並在 9/22 上市；現在就可以開始宣傳本書即將上市。\n2023/09/11 看到博客來和天瓏書局開始預購。\n預計 9/22 到 9/26 會正式發行；電子書版本的話，會在兩個月後發行。\n心得 寫書真的是良心事業。\n即使扣除掉前兩年在功能開發完之後，寫初版文章記錄下來的時間，單純只算今年與產品經理溝通，走流程，繳交各式文章；再花了兩個多月寫書；然後跟編輯一同校稿，和提供書面書封相關的內容；以目前上班的薪水來看，就算是賣了一兩千本，應該還是很不划算。難怪大部分出書的作書都是抱著記念和求個名聲的心情在做這件事。\n如果真的想賺錢的話，有太多其他方式能更有機會達成。而對於我來說，獲得寫書的經驗自然是比它實質上能帶來的金錢重要得多。藉由寫書的過程，了解到出版社是怎麼跟作者簽訂合約的；知道版稅的分成大概有多少；並了解到整個流程的各個步驟。如果哪天又累積了某種特別的知識想要分享並出書時，就大概有個譜，知道可以怎麼準備，以及怎麼跟出版社談。\n目前書還沒真的開賣。等到時候開賣了，不論賣得好或不好，應該都會再有一點心得可以分享。\n講了這麼多，希望大家可以點下面的連結買一本來看看囉！預購有打折！(雖然真的開賣後，應該也是會有折扣)\n購買連結 天瓏 天瓏網路書店 | Android 開源專案「真」實戰啟航：瀏覽器 App EinkBro 開發者帶你逐步從 UI 設計、UX 提升到多功能實現秘技全解析（iThome鐵人賽系列書）【軟精裝】\n博客來 Android 開源專案「真」實戰啟航：瀏覽器 App EinkBro 開發者帶你逐步從 UI 設計、UX 提升到多功能實現秘技全解析（iThome鐵人賽系列書）【軟精裝】\nMOMO 連結還沒出來\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%88%91%E7%9A%84%E5%87%BA%E6%9B%B8%E4%B9%8B%E8%B7%AF/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/bfd54159ead8/1_gBRgsvay_ax-ibBlfbUDZA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e這篇文章會說明我即將出版的書 “\u003cstrong\u003eAndroid 開源專案「真」實戰啟航：瀏覽器 App EinkBro 開發者帶你逐步從 UI 設計、UX 提升到多功能實現秘技全解析”\u003c/strong\u003e 的漫長出書過程。\u003c/p\u003e","title":"我的出書之路"},{"content":"MLC-LLM是今年五月出現的專案，用來提供一個通用的系統，試圖讓 LLM 可以執行於各種平台上，並能利用各平台的 GPU 性能，使其表現更佳。這篇文章將說明我在 Android, iOS, MacOS 平台上編譯和執行時的一些理解和心得。\n從 ChatGPT 橫空出世到現在也才快要一年，各式各樣的 LLM 如雨後春荀不斷冒出來。有的強調精確度，有的注重在瘦身 model size，有的專注於處理 coding 相關的問題。雖然大部分的 LLM 還是以執行在性能強大的電腦上，或是架設於雲端為主，但是也漸漸有人想要把它移植到移動式設備上，做所謂的 on-device computing。MLC-LLM 是其中之一，不過它的目標更遠大，它想要讓 LLM 只需要透過同一套流程，就能執行在各種平台上。而移動式設備只是它其中想支援的一環而已。\n但這對我來說，就很足夠了。光是能讓 LLM 跑在手機上，我就願意來嘗試看看。反正目前手邊也有 Mac 電腦，能同時讓它支援手邊的設備，算是個附加好處吧。\nMLC-LLM 介紹 MLC-LLM 是一套來自於 CMU 大學的通用型方案，能讓各種 LLM 模型以 native 的型式發布到許多不同的平台上。並提供工具，能夠針對不同平台或需求，進一步優化模型的輸出和效能。\n它的目標是讓每個人都能夠開發、優化、和以原生的型式發布 AI 模型到每個人的設備中。\n下圖是目前它已經支援的平台，和各個平台上面的 GPU API framework。\n來自官網\n什麼是 MLC (Machine Learning Compilation) 在解釋 MLC 前，先來看一下整個 MLC-LLM 方案的流程。下圖可以分成三部分。\n是來自網路上各式各樣的 LLM 模型，或是自己訓練出來的模式； 是這一小節要介紹的 MLC； 是產生出來適用於不同平台的 runtime libraries，以及最終編譯出來的各平台執行程式。 來自官網\n大概知道 MLC 在整套流程中的位置後，來聊聊何謂 MLC。雖然有個 Compilation 字樣，但它和一般寫程式的 compilation 只是概念上類似，但真正的產出物卻不大一樣。其實比較好的說法應該是 converter。\n它會透過 TVM Unity compiler 將 1 中的語言模型做 quantization，然後將結果連同 quantized model weights 全放到一個目錄中。這過程都是用 python 開發出來的。\n下圖是更詳細的流程圖。對於 compilation 步驟有興趣的話，讀者可以再去找 tvm 的文章來看。這篇文章重點會是在解釋和說明當 compilation 完成後的步驟。\n來自官網\n執行 Compilation 的方式 大概了解 compilation 的概念和它在流程中扮演的角色後，再來看看實際上要怎麼操作。\n首先，必須要安裝許多 python dependency，再來是執行下面的指令。\npython3 -m mlc_llm.build \\ --model MODEL_NAME_OR_PATH \\ [--hf-path HUGGINGFACE_NAME] \\ --target TARGET_NAME \\ --quantization QUANTIZATION_MODE \\ [--max-seq-len MAX_ALLOWED_SEQUENCE_LENGTH] \\ [--reuse-lib LIB_NAME] \\ [--use-cache=0] \\ [--debug-dump] \\ [--use-safetensors] 以 Android 當例子，如果想要編譯 Llama2 的模型，使用 quantization 參數為 q4f16_1 的話，可以執行下面的指令。\nmkdir -p dist/models cd dist/models git clone https://huggingface.co/meta-llama/Llama-2-7b-chat-hf cd ../.. python3 -m mlc_llm.build --model Llama-2-7b-chat-hf --target android --max-seq-len 768 --quantization q4f16_1 一切順利的話，它會將原先的 Llama2 模式轉換成之後 MLC Chat runtime 能夠讀取的格式。\nCompilation後的步驟 在 compilation完成後，產生的 model weights，model libs 還有 chat config，需要再編譯成每個平台上執行時所需的 MLC Chat Runtime。\n如下圖，當有了 MLC Chat Runtime library 之後，就能利用它提供的 API 在真正的應用程式裡跟 LLM 互動。\n來自官網\n關於 chat config 能設定些什麼，其實跟一般在執行 LLM 時的參數差不多，能夠調整 temperature ，repetition_penalty 等等參數。詳細內容可以參考這篇官方介紹。\n有了 runtime library 後，再來就是看看程式中怎麼去使用它。這邊先來看一下 library 中提供的函式有哪些(其實很陽春，就夠用而已)。這邊拿 C++ 當例子，會有以下的函式可以呼叫：不外乎 prefill, decode, reset_chat, 等。\nPackedFunc prefill = mlc_llm-\u0026gt;GetFunction(\u0026#34;prefill\u0026#34;); PackedFunc decode = mlc_llm-\u0026gt;GetFunction(\u0026#34;decode\u0026#34;); PackedFunc stopped = mlc_llm-\u0026gt;GetFunction(\u0026#34;stopped\u0026#34;); PackedFunc get_message = mlc_llm-\u0026gt;GetFunction(\u0026#34;get_message\u0026#34;); PackedFunc reload = mlc_llm-\u0026gt;GetFunction(\u0026#34;reload\u0026#34;); PackedFunc get_role0 = mlc_llm-\u0026gt;GetFunction(\u0026#34;get_role0\u0026#34;); PackedFunc get_role1 = mlc_llm-\u0026gt;GetFunction(\u0026#34;get_role1\u0026#34;); PackedFunc runtime_stats_text = mlc_llm-\u0026gt;GetFunction(\u0026#34;runtime_stats_text\u0026#34;); PackedFunc reset_chat = mlc_llm-\u0026gt;GetFunction(\u0026#34;reset_chat\u0026#34;); PackedFunc process_system_prompts = mlc_llm-\u0026gt;GetFunction(\u0026#34;process_system_prompts\u0026#34;); 官方有提供 Android 的 sample app，是以對話型式的 UI 呈現，並用上了比較新的 Jetpack Compose framework。\n先來看一下UI的實作，很單純地用了 LazyColumn，根據帶入的 chatState.messages 產生對應的每個訊息 MessageView；並依照 chatState.messages.size 調整現在訊息列表要捲動到在最下方。\nLazyColumn( modifier = Modifier.weight(9f), verticalArrangement = Arrangement.spacedBy(5.dp, alignment = Alignment.Bottom), state = lazyColumnListState ) { coroutineScope.launch { lazyColumnListState.animateScrollToItem(chatState.messages.size) } items( items = chatState.messages, key = { message -\u0026gt; message.id }, ) { message -\u0026gt; MessageView(messageData = message) } item { // place holder item for scrolling to the bottom } } 在 ViewModel 內，則是負責與 MLC Chat runtime 互動。當使用者輸入訊息後，會呼叫 requestGenerate()。這邊又是用 executorService，又是用 coroutine，感覺有點多餘。後來雖然我有把 executor service 拔掉，試著只透過 coroutine 執行，但不論是哪種作法，在執行時都還是會整台手機幾乎當住。目前的手機性能還是無法輕鬆地負荷這麼大的運算量吧。\nfun requestGenerate(prompt: String) { require(chatable()) switchToGenerating() executorService.submit { appendMessage(MessageRole.User, prompt) appendMessage(MessageRole.Bot, \u0026#34;\u0026#34;) if (!callBackend { backend.prefill(prompt) }) return@submit while (!backend.stopped()) { if (!callBackend { backend.decode() val newText = backend.message viewModelScope.launch { updateMessage(MessageRole.Bot, newText) } }) return@submit if (modelChatState.value != ModelChatState.Generating) return@submit } val runtimeStats = backend.runtimeStatsText() viewModelScope.launch { report.value = runtimeStats if (modelChatState.value == ModelChatState.Generating) switchToReady() } } } 執行後的感想 幾經波折後，替官方抓了不少文件上的不足，也發了 PR 去修正。\n目前能夠成功地在 Android，iOS 還有 MacOS 上執行 MLC 的程式。以結果來看：在 M2 或 M1 CPU 加持下的 MacOS 上，是能很順暢地執行這些 LLM 的；只是產出的結果跟 chatgpt 或是 gpt4 比起來還差得很遠。\n而 Android 上，執行速度很慢，連要拿來平常測試用的程度都還不到。不過，至少它是能夠跑我 compile 出來的 Taiwan-Llama2 模型的。\niPhone 的話，我的 iPhone 14 Pro 雖然一執行 Taiwan-Llama2 模型就會因為記憶體不足而 crash，但是其他的模型運作在 iPhone 上卻有不錯的反應速度，已經可以拿來偶爾玩玩，跟它亂聊一通的程度。\n相信再過不久，可能幾個月內，長則半年一年，手機上執行 LLM 的環境就會更加的成熟。在那之前，也希望能想到一些應用，好好地來利用這些能拿來當成手機上大腦的 LLM。\n相關連結 https://github.com/mlc-ai/mlc-llm/tree/main https://mlc.ai/mlc-llm/ https://mlc.ai/mlc-llm/docs/ https://huggingface.co/danielkao0421/mlc-chat-Taiwan-LLaMa ","permalink":"https://plateaukao.github.io/blog/posts/mlc-llm-%E7%9A%84%E4%BB%8B%E7%B4%B9%E5%92%8C%E5%9F%B7%E8%A1%8C%E6%96%BC-androidiosmacos/","summary":"\u003cp\u003eMLC-LLM是今年五月出現的專案，用來提供一個通用的系統，試圖讓 LLM 可以執行於各種平台上，並能利用各平台的 GPU 性能，使其表現更佳。這篇文章將說明我在 Android, iOS, MacOS 平台上編譯和執行時的一些理解和心得。\u003c/p\u003e","title":"MLC-LLM 的介紹和執行於 Android/iOS/MacOS"},{"content":"\n現在的電子書閱讀器已經多元到可以滿足大部分人各式各樣的需求了。對我來說，也曾經一直在找那”最適合”的設備。以看書的使用習慣來說，我最常看書的場所是坐在沙發上，單手持閱讀器。閱讀時長從半小時到兩三小時不等。\n隨著年紀愈來愈大，總希望畫面也跟著愈來愈大，但是設備的重量卻不行太重。重了，就拿不久。針對這個需求，在今年初先是買了掌閱已經出了一年多的 Ocean2，7吋大小，重量只要176克，而且造型仿照Kindle Oasis，相當適合手持，還有實體翻頁鍵。雖然它號稱是一台封閉式的設備，但是有些方式可以很容易地安裝上其他軟體。所以我主要是用它來閱讀Hyread上借的電子書。相當開心，也讀了不少本。\n最近掌閱又推出了Ocean3 Plus，除了造型跟Ocean2一模一樣外，它的尺吋來到8吋，但是重量依然維持在200克左右。應該是目前7.8吋，8吋設備中重量最輕的(並沒有，最輕的是Kobo forma 8.0 只有197克)！Ocean3 Plus 不愧是最新出的機型，反應速度比Ocean2 又快了一級(雖然對於看書來說，原先的速度就很順暢了)。不要小看它只是從7吋變成8吋，多了一吋；實際上畫面卻是多了30%！\n唯一的缺點是：之前在Ocean2上可以操作的安裝Apk方式，竟然被修正了！我試了好久都不得其門而入。想要拿它來看Hyread電子書的想法暫時無法實現了。不過，之前也已經在各平台買了不少書，也夠我利用Ocean3 Plus看的了。Hyread借來的書，還是只能先交給 Ocean2。\n隨文附上我陸續買進的7.8吋，8吋設備，以及它們透過我家小磅砰量出來的重量讓大家有個對比。不要小看那幾十克，每一克都能讓我多讀個幾頁書。\nPubook, Nova AirC, Likebook, MooInk PlusC, Ocean2, Ocean3 Plus\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%8E%8C%E9%96%B1ocean3-plus%E7%9A%84%E4%B8%80%E4%BA%9B%E6%84%9F%E6%83%B3/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/574620954c57/1_mGAbFwmireHGuyn4X0749Q.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e現在的電子書閱讀器已經多元到可以滿足大部分人各式各樣的需求了。對我來說，也曾經一直在找那”最適合”的設備。以看書的使用習慣來說，我最常看書的場所是坐在沙發上，單手持閱讀器。閱讀時長從半小時到兩三小時不等。\u003c/p\u003e","title":"掌閱Ocean3 Plus的一些感想"},{"content":"\n趁著去日本旅遊的機會，順便去有樂町逛了一下Bic Camera，在那兒看到了Kindle Scribe。在試了一下之後，決定要入手一台。在買的時候也沒有仔細看規格，所以是在買完以後才知道我買的是 16GB 的 PREMIUM PEN 方案。\n第一個感想是：畫面真的清晰許多！而且對比也很高。重量嘛，以10.3吋的尺吋來說，只能說是符合預期(的重)。金屬機身讓它無法輕到哪兒去，拿起來很高級，也很冰涼。機器背面還有四個橡皮凸起物，讓機器不會整個背面接觸到桌面。我想，這應該是為了讓它有較大的摩擦力，在書寫時避免設備滑來滑去，在桌面上摩擦吧。\n包裝內的簡易說明，做得很有設計感。除了文字外，還有些圖示，讓它不會過於枯燥。\n開機後，在登入既有的日亞帳號後，就可以把之前買過的書一一下載下來閱讀。使用操作上，跟之前的 Kindle 沒有太大的差別。很久沒有買 Kindle 了，已經忘了這種綁手綁腳的感覺：畫面的更新率沒得調整，就只有一種模式。在大多數的情況下都很夠用，也很順暢；只是，常常會看到它為了確保畫面沒有殘影，三不五時全刷畫面。不特別去注意的話，並不會防礙閱讀的體驗。\n買來後，已經用它看完 The Housemaid 第一集。目前介面是設定為日文，所以出來的字典也都是英日或是英英為主。好在這書的用字不難，加減查一查單字，就能了解故事情節在說什麼。\n電力方面，不用太擔心。只要不開機，基本上就是不會耗電。我懷疑它根本過一段時間後就是直接關機。然後在重新啟動時，做到了極致，可以很快速地在幾秒內就開機完成。所以，並不會讓你有，唉呀，怎麼系統又關機了，得要等上一分鐘才能開始使用的感覺。\n7/31買來到現在，應該只有充過一次電而已。充電時，應該還有三四十左右的電力吧。\n筆記部分，還沒認真用過。目前做筆記時，我習慣是使用 Tab Ultra C 在記錄，因為可以邊寫邊錄音，事後要重聽比較方便。礙於這一點，遲遲沒有開始認真地使用 Kindle Scribe 的筆記功能。\n不過，本來買這台機器的最主要目的，還是看上它那塊10.3吋的 300 ppi 黑白電子紙螢幕，和 Kindle 的耐用性。筆記功能就等以後有比較多的使用心得再分享了。\n","permalink":"https://plateaukao.github.io/blog/posts/kindle-scribe-%E7%9A%84%E4%BD%BF%E7%94%A8%E6%84%9F%E6%83%B3/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/be3cb1e0fb03/1_k53Py5g-MX1eWqLs3ah7mQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003e趁著去日本旅遊的機會，順便去有樂町逛了一下Bic Camera，在那兒看到了Kindle Scribe。在試了一下之後，決定要入手一台。在買的時候也沒有仔細看規格，所以是在買完以後才知道我買的是 16GB 的 PREMIUM PEN 方案。\u003c/p\u003e","title":"Kindle Scribe 的使用感想"},{"content":"Koreader 是個跨設備的閱讀 App，提供強大的閱讀功能以及相當多的客製化彈性。除了內建的許多功能外，它也提供開發者可以透過撰寫 lua script，擴充新功能或是修改原先的行為。這篇文章將會說明怎麼開發一個簡單的 koreader plugin，讓使用者在呼叫辭典時，可以連結到 EinkBro 做搜尋。\n大綱 koplugin 基本架構 lua 教學 askeinkbro.koplugin 功能 參考 Koreader 現有架構並實作 怎麼把 plugin 安裝到 Koreader 中和 Debug koplugin 基本架構 關於 koplugin 的開發範例，可以參考官方 github.com 中最簡易的 hello.koplugin。\n每個 plugin 目錄中至少會有兩個檔案：_meta.lua 和 main.lua。\n_meta.lua: 定義該 plugin 名稱，和一個簡單的功能描述。 main.lua: 真正的實作所在。如果要寫的內容太多，也可以把實作寫到其他的 lua 檔案中，然後再利用 require() 的方式，將其引入到主要邏輯開發中。 _meta.lua 下面是 hello.koplugin _meta.lua 的實作內容。\nlocal _ = require(\u0026#34;gettext\u0026#34;) return { name = \u0026#34;hello\u0026#34;, fullname = _(\u0026#34;Hello\u0026#34;), description = _([[This is a debugging plugin.]]), } 這邊利用了 local _ = require(“gettext”) 把用來處理多國語系的模組引入。如果你的模組沒有做其他語言的翻譯的話，不特別引入 gettext 也是可以的。\n大部分 plugin 的 _meta.lua 也都是這麼簡潔。下面再來看個例子，keepalive.koplugin ：\nlocal _ = require(\u0026#34;gettext\u0026#34;) return { name = \u0026#34;keepalive\u0026#34;, fullname = _(\u0026#34;Keep alive\u0026#34;), description = _([[Keeps the device awake to prevent automatic Wi-Fi disconnects.]]), } main.lua 這個檔案會是主要的實作區域，內容可以簡單分成四部分：\n引用現有模組的宣告 建立並初始化 plugin 物件 各種需要用到的函式，或是覆寫既有函式的實作 回傳初始化的 plugin 以 hello.koplugin 來說，對應如下。在建立 hello plugin 時，它是繼承 WidgetContainer 而來的物件。Hello:init() 是每個 plugin 都會實作，用來把需要啟動或做連結的邏輯都放在這兒。\nlua 教學 lua 的語法很容易，這邊不做詳細的介紹，只是給一下相關的官方教學連結。以筆者在開發 plugin 時的經驗來說，我完全沒有閱讀下面的連結，全是參考現有 plugin 的作法，就足以開發出想要的功能。\nProgramming in Lua (first edition)\n簡而言之，如果不是太複雜的 plugin 的話，應該參考現有的實作會比較快。如果想要開發規模比較大的功能，可能投入點時間閱讀一下 lua 文件會比較有效率。\naskeinkbro.koplugin 看完 hello world 版的 koplugin 後，接著要講自己開發的 plugin 就容易多了。會開發這個 plugin 是因為有使用者在問，是不是能在使用 koreader 閱讀時，比較快速地把選取的文字帶到 EinkBro 中做查詢。\n在沒有這個 plugin 之前，使用者必須要在選取文字後，點選”分享文字”，再從系統的 action picker 中點選 EinkBro App 才能進到 EinkBro 中。如果有 plugin 的話，這個操作可以節省至少一個步驟(從系統清單中選擇 EinkBro)，甚至有機會做到選完單字後，直接帶到 EinkBro 的搜尋結果畫面去。\n_meta.lua (link) 先來看看這個 plugin 的定義內容。雖然我也照抄了 gettext 的引入，但其實使用者不管用什麼語系，都只會看到 AskEinBro 就是了。目前版本很快地已經來到了 0.4.1，因為過程中修正了不少臭蟲。\nlocal _ = require(\u0026#34;gettext\u0026#34;) return { main.lua (link) 主要的實作如下。有了上面的架構後，再來看 askeinkbro.koplugin 就清楚多了。這裡繼承的 UI 模組是 InputContainer。至於為什麼，其實我也還沒去細究，這是沿用 AskChatGPT (請見下面參考連結) 的實作而來的。\n在初始化的函式 (53 ~ 88 行) 中，包含了三個重點，分別是：\n15, 39 行：將 plugin 加入在選取單字後，能直接查詢的字典選單中 67 行：在字典結果畫面中，增加 Query EinkBro 的按鈕 54 行：在選取文字的清單中，增加 Query EinkBro 的按鈕 這幾個功能實作會在後面展開來講解。\n選取單字後直接查詢 EinkBro 對於查詢頻繁的使用者來說，點完單字就直接跳到特定字典(或是 EinkBro)會是最方便的行為。這在原本的 Koreader 中就有支援，只差沒有把 EinkBro 列進去。所以這裡實作的重點是，把 EinkBro 加到字典清單中；並且在當它被選取後，能夠呼叫正確的方式，把 EinkBro 叫起來。\n讓我們先來看看原先在 Koreader 裡的實作。它在 koreader/frontend/device/android/device.lua 中定義了一個列表 external，其中包含了許多字典 App 的名稱和對應的 package name，以及適用的 action (aard2, search, send, quickdic, text, etc)。如果想讓 EinkBro 也出現在列表中，就必須要覆寫這個列表 (後來想想，好像不用覆寫，只要抓出 external.dicts ，幫它加上一條新的資訊就好吧？) 。\n以下是修改後的版本。\n28 行是新加入的資訊，定義了 EinkBro 的名稱，package name，以及想要的 action 為 text。 35 行將它定為 getExternalDictLookupList。這函式將會在下面使用到。 39 行的 doExternalDictLookup() 是真正用來叫起 EinkBro App 的實作。這裡用到了內建的 android.dictLookup() 函式。 有了這些準備後，接下來是在 AskGPT:init() 中，把一切串起來。下面可以看到：\n64 行把原先系統的 Device.getExternalDictLookupList 換成我的版本 65 行把原先系統的 Device.doExternalDictLookup 實作換成我的函式 這樣子的操作基本上說明了大部分 koreader 的 plugin 可以怎麼實作：\n在可以攔截列表生成的函式中，加入自己想要新增的功能選項 將系統原先的功能函式置換成自己想要的行為 圖例 下面就是關於快速查詢字典的相關畫面結果。\n在字典結果畫面新增 Query EinkBro 按鈕 在用一般字典查詢完單字後，有的時候可能找不到結果，或是內容不是自己想要的，這時可能會想要再延伸去 EinkBro 上看看。所以，使用者會希望在字典結果頁上也能有個快速前往 EinkBro 的方式。\n在經過一番尋找後，發現在這功能是實作在 koreader/frontend/ui/widget/dictquicklookup.lua 中。看來這是個常常有被修改需求的元件，所以它在它的 init() 實作中，多了下面的邏輯：看看有沒有其他人定義了 tweak_buttons_func，如果有的話，就呼叫一下，讓其他元件可以透過這方式來調整原先的 buttons 列表。\nif self.tweak_buttons_func then self:tweak_buttons_func(buttons) end 有了這樣的認知後，我們再回過頭來看一下 askeinkbro plugin 中的 init() 函式吧：\n66行：先把原先的 tweak_buttons_func 保留下來，因為我們的目的是要加按鈕，而不是要把別人的客製化內容都覆蓋掉 68 行：在做其他事之前，先呼叫一下別人的實作 69 ~ 75 行：加上一個 boolean flag，確保不會在多次呼叫下，一直加入一樣的按鈕 76 ~ 86 行：做了點判斷後，利用 table 在 buttons 中加入按鈕；而 callback 是被點擊後要執行的實作。這邊一樣是利用了 android.dictLookup() 函式。83 行則是要記得把這個對話框關閉。不然從 EinkBro App 回來時，這個對話框還會在畫面上。 圖例\n來看看完成後的畫面吧。\n在選取文字的清單中新增 Query EinkBro的按鈕 (link) 在看完前兩個功能的實作後，第三個功能就是重覆一樣的動作而已。首先，找到文字選取後的新增功能清單定義在 koreader/frontend/apps/reader/modules/readerhighlight.lua 。下面可以看到類似我想要做的事：在 Highlight 對話框中新增一個項目。而這裡的 readerhighlight 其實會被生成到 self.ui.highlight 上。\n所以，在 AskGPT:init() 中就依樣畫葫蘆，放進一個 Query EinkBro 的按鈕。實作跟上面的兩個功能大同小異。\n圖例\n完成啦，來看看長出 Query EinkBro 的畫面。\n如何安裝 plugin 和 Debug 安裝 plugin 目前剛成開發的 askeinkbro plugin 都會壓成 zip 檔案，放在 github 上。https://github.com/einkbro/askeinkbro/releases\n要安裝到設備的 Koreader 的話，需要先把 zip 下載到設備上解壓縮，並把目錄名稱重新命名為 askeinkbro.koplugin，再移動到 koreader/plugins 下面。\n如何 Debug 不確定有沒有更好的方式，不過，目前筆者的作法是在修改完 main.lua 喔，利用 Android Studio 中的 File Explorer 把它移到 askeinkbro.koplugin 目錄下，覆蓋掉舊的檔案，再重新啟動 Koreader。\n如果寫的 lua script 有問題的話，在 Android Sutdio 的 logcat 畫面中會有相關的除錯訊息。通常這些訊息都寫得很清楚，能夠很正確地跟你說問題出在哪。\n如果對於 lua 語法不熟，或是看不懂出錯在哪的話，通常問一下 ChatGPT 可以得到不錯的回答。畢竟，lua 已經存在很久了，koreader 也是；所以 ChatGPT 有足夠的知識回答 koreader plugin 開發上遇到的問題。\n相關連結 AskEinkBro koreader plugin: https://github.com/einkbro/askeinkbro AskGPT: https://github.com/drewbaumann/AskGPT ","permalink":"https://plateaukao.github.io/blog/posts/%E9%96%8B%E7%99%BC-koreader-plugin/","summary":"\u003cp\u003eKoreader 是個跨設備的閱讀 App，提供強大的閱讀功能以及相當多的客製化彈性。除了內建的許多功能外，它也提供開發者可以透過撰寫 lua script，擴充新功能或是修改原先的行為。這篇文章將會說明怎麼開發一個簡單的 koreader plugin，讓使用者在呼叫辭典時，可以連結到 EinkBro 做搜尋。\u003c/p\u003e","title":"開發 Koreader Plugin"},{"content":"前不久寫了一篇關於Pubook的使用心得，雖然有些小問題，但整體來說，是一台不錯的閱讀器，在攜帶性，電力消耗性，和閱讀品質上有不錯的平衡。今天要來說說在6月Pubu推出Pubook SE設備後，開放讓原有Pubook使用者申請升級成開放系統的感想。\n申請方式 申請方式很容易，只要透過系統跟客服反應，並附上設備的序號，在上班時間一兩天內，就會收到新版本更新的通知。收到通知後，啟動更新，過程中可能會重開一兩次機。就讓它順順跑完就好。\n感想 升級為開放式後的第一個感覺是：原先在封閉式中為了簡化使用者操作的一些設計，頓時變成了操作不順的主因。\n畫面上方三個點的按鈕設計 這在封閉式閱讀器上是好的介面，使用者可以看到閱讀器專有的一些選項，也能快速地回到書庫或是設定畫面。但是變成開放式後，開啟其他App的頻率變得很高，而且一般的App可能會有一些通知需要呈現給使用者知道；或是一般的App會需要依賴系統的Back鍵，或手勢返回前一個畫面；但是Pubook卻還是只有一個很陽春的客製化的系統清單。\n點擊書庫後也不是回到開放式比較適合的”應用列表”畫面，而是進到內容庫。這時要再多點一下才能又來到應用列表。\n另外，也沒有多個應用時，彼此間切換的已開啟應用的列表畫面。\n這些因素造成開放式Pubook不是一台適合常常切換App的閱讀器，因為整個流程會比一般的Android平板還要複雜。\n缺少檔案選取的系統程式 這點對一般使用者來說，可能不會特別造成什麼不便。不過，對於我開發的EinkBro來說，卻造成了很大的困擾。在EinkBro中，為了避免檔案選取在不同Android版本間帶來的麻煩事，我後來全改成了使用SAF(Storage Access Framework)建議的作法，使用系統開出來的ACTION_OPEN_DOCUMENT或是ACTION_CREATE_DOCUMENT來開啟或新增檔案到系統中。\n少了系統提供的檔案選取程式，在EinkBro中不論是要匯出epub檔案，或是要選取字型檔案，都會直接crash，等於很多好用的功能在Pubook上是沒有作用的。\n另外，我自己修改過的萊姆輸入法：sweetlime，想要從別台設備中備份的資料檔再復原到Pubook中的話，也是用同樣的方式在選取檔案。一樣會遇到crash。\n後來雖然可以安裝其他的檔案管理App解決掉部分的問題，但關於file picker還是無法像其他設備可以正常運作。希望這一點在接下來的更新可以有所改善。\n擾人的 toast 訊息 我可以理解系統常常需要去同步使用者的閱讀資訊，或是下載新的資訊到設備上，但每次一回到內容庫就要同步一次，而且還大喇喇地一直跳出同步已完成，或是沒有網路，所以無法連線，就超超超級惱人的。\n這種資訊應該不需要一直跳出來通知使用者才對。保持同步是要默默地做，如果沒有網路，那就等有網路時再做。一直跳出來超級干擾使用體驗的。\n適用的場景 基於以上提到的問題，和Pubook天生硬體上的品質，目前個人覺得Pubook比較適合封閉式的操作。不論是要閱讀自己匯入的檔案，或是在Pubu書城買的書，都會很順暢。\n如果想要買或升級成開放式的系統，那建議只安裝幾個基本的閱讀軟體就好，不要想著要把它當成Android平板來使用，保證你會很挫折的。如果大部分的時間就是在讀書，這台設備還是很稱職的。\n補充 不論是封閉式還是開放式的Pubook，內建的EinkBro看起來使用的簽章是Pubu自家的，所以無法正常從Google Play Store，Github或是Fdroid平台做版本更新。這有點可惜。如果想要使用最新版的話，只能自己換個 application id編譯一版來用才行。\n","permalink":"https://plateaukao.github.io/blog/posts/pubook%E7%94%B3%E8%AB%8B%E6%94%B9%E7%82%BA%E9%96%8B%E6%94%BE%E5%BC%8F%E5%BE%8C%E7%9A%84%E6%84%9F%E6%83%B3/","summary":"\u003cp\u003e前不久寫了一篇關於Pubook的使用心得，雖然有些小問題，但整體來說，是一台不錯的閱讀器，在攜帶性，電力消耗性，和閱讀品質上有不錯的平衡。今天要來說說在6月Pubu推出Pubook SE設備後，開放讓原有Pubook使用者申請升級成開放系統的感想。\u003c/p\u003e","title":"Pubook申請改為開放式後的感想"},{"content":"\n這篇文章會來說說2023年4月多就拿到手的Pubook，這段時間以來的使用心得，包含硬體上的感受，以及軟體使用上的感想。內容會很主觀，不過我至少也長期使用過十幾二十台電子書閱讀器了，理論上應該也算是客觀才對。\n硬體 https://www.pubu.com.tw/campaign/ebook/pubook.html\n以上規格取自官網。它是一台7.8吋的黑白電子紙閱讀器，解析度跟其他主流的7.8吋設備一樣，配有300ppi的畫面，在清晰度上屬於前段班，黑白對比度也夠高。\nCPU 是1.8GHz 4核心的晶片，在一般使用上(看epub/pdf書，飽讀的報紙/雜誌的開啟與縮放)都很順暢。所以在效能上沒有什麼問題。但是，偶爾在使用EinkBro一段時間後會卡死，然後隔一會兒會重新進到設定畫面。這應該是軟體的問題，所以待會兒在軟體部分會再多說一點。就硬體來說，這顆CPU是很足夠一般的使用的，包含瀏覽網頁。\n配有雙色前光，但我都沒有開來用，所以這塊直接跳過。\n儲存空間64GB，對於我來說相當夠用。雖然我也買了不少漫畫，但通常不會在每一台設備上全部都下載下來，只會在想看的時候，或是買新的系列時，才會特別下載。至於其他買的書籍，也沒有多到可以塞滿64GB。\n這邊有一點要特別提的是：Pubook機身真的很薄。由於Pubu從去年到最近才推出這一台而已(十月即將多一台10.3吋的Pubook Pro)，不大可能做一台設備符合所有使用者的需求，所以(個人猜測)他們做了必要的取捨：不支援觸控筆手寫功能(老實說，7.8吋要做筆記，對我來說也是小了點，一頁寫不了多少字)，讓畫面可以再輕薄些；沒有內建喇叭和麥克風，讓機身能因此再瘦身。\nPubook的外型很…沒有外型，就是一台很四四方方的設備。但由於它機身薄，而且背面材質很防滑，所以雖然機身採用金屬材質，整台設備在手持時很舒服。270克，老實說，稱不上是這尺吋最輕型的(BOOX Air C 268克，Mooink Plus 2C 280克)，不過，金屬機身感覺很有質感。\n隨機附贈的保護套比文石送的都還要好，是前翻式的設計。在把機身卡進去的部分雖然都是塑膠的，但不像文石的保護套用了幾個月後，可能保護殼的邊邊角角就會開始裂開。\n軟體 軟體部分，可以再細分為使用內建 App 閱讀書籍的體驗、使用飽讀服務的感想、使用 EinkBro 的體驗、輸入法以及整體系統的使用心得。待我下面一一道來。\n閱讀書籍的體驗 閱讀可以再分為一般的 epub 格式或是 pdf。先說說 epub 格式，Pubook提供的功能不多，但是很夠用。能夠調整字型大小，選擇不同字型，或是自行放入喜愛的字型。能調整行距，或是轉換看書時是要直排還是橫排排版。甚至還支援深色模式，雖然個人覺得在電子紙上面，其實不需要特別設定成深色模式。\n能加書籤和標註書籍內容。但有三點(小？大？問題)要提醒的是：\n有的時候我在閱讀時想要選取文字做標註時，不論手指再怎麼圈選，它就是無法正確地選取到我要的範圍。這問題也同樣發生在安裝於其他設備中的 Pubu App 中。感覺是 Pubu 在整合 Adobe Reader SDK 時沒有處理好。這點很讓我困擾。雖然少劃幾行重點不會怎樣，但就覺得有那麼一點不開心。 另一點是目前Pubu在閱讀書籍後，對於標註的內容只支援不同 Android 設備間的備份和還原。咦，不對吧。一般使用者之所以想要匯出標註的內容，是因為想要整理這些重點，而不是想要在另一台設備上也看到吧。如果你是個很看重自己標註的內容的人的話，建議先不要用Pubu看書，直到他們正式支援匯出註記時再閱讀。 閱讀書籍時，畫面上方會一直顯示返回鍵，目錄鍵，系統功能鍵，書籤鍵和設定鍵。如果是在看文字內容的書，感覺還不會那麼強烈；但如果是在看報紙，雜誌，或是漫畫時，你就會一直看到畫面內容一直會跟上面的這些功能鍵重疊。對於有某種潔癖的人來說，很難忍受在這樣的畫面上閱讀。 可改善的第二點\n可改善的第三點\n上面這三個很明顯的問題如果都能忍受或忽略不計較的話，使用Pubook看書還蠻愉快的，機身薄，可以久拿；而且畫面也很細緻，對比夠高。\n再來說說閱讀 pdf 格式的感想。飽讀裡的雜誌和報紙應該就都是 pdf 型式的內容。先說結論，在Pubook上閱讀 pdf 格式的內容是很舒暢的。雖然7.8吋不論是雜誌或是報紙，都不得不放大才能看清內文，但因為Pubook上的兩指縮放效果超好，反應也很靈敏，所以不會讓你在操作上覺得煩燥。\n唯一要挑的小缺點或是不足的地方是：把畫面打橫時，原先顯示的頁面只能呈現上半部分或下半部分。如果這時的逐次點擊效果是能仿照 Kindle App的作法，先把一個頁面的內容從上到下位移一次，然後再跳到下一個頁面的上半部，這種實作方式可以讓使用者省下不少縮放畫面的步驟。\n飽讀 購買Pubook時有附贈六個月的飽讀服務，可以閱讀Pubu書城中的許多雜誌，還有部分書籍。飽讀中的書籍有新有舊，新的書籍看起來不多，但我還是從中還是找到不少想看的書籍，一個月大概會看兩三本左右。\n最近有在看的像是「底層邏輯」、「大人學的選擇」、「記者、科技與改變世界的怪胎們」、「我只是計程車司機」、「京都力」、「這個房仲太狠了」、「藥品帝國」、「消逝的韓光」、「這才是真正的韓國」、「我是醫生，在監獄上班」等等。\n以官網的定價來看，感覺一個月只要讀一到兩本書，就算是值得的，特別是前六個月還是送的。\n雜誌和報紙，我偶爾會看看商業週刊和每日的工商時報。其他類別的雜誌就比較少看。\n飽讀還有一個類別是影音，我就真的很少接觸，這邊不多做說明。\nEinkBro的使用體驗 這邊要給Pubook拍拍手。身為封閉式的系統，但在今年 4 月的系統更新中，率先內建了EinkBro瀏覽器，讓使用者在看書之餘，也能利用它上上網，不論是要看新聞，找資料，或是閱讀網路小說，都大大擴展了Pubook這台設備的可用性。\n比較可惜的是，當時內建的版本是v9.22.0。雖然到現在也才過了兩三個月，但這段期間內 EinkBro 又加了許多我想要的功能，在介面上也做了不少改進。目前Pubook封閉式的架構只能等下次它再系統升級時，再順便幫忙更新一下EinkBro的版本，無法讓使用者自行升級。\n不過，接下來Pubook也推出了SE版本，出廠時就是開放式的，可以讓使用者自行安裝程式，如果對於開放式系統比較有興趣的人，建議可以買開放式的版本。\n在使用上，大部分EinkBro提供的功能都可以正常運行。少部分關係到系統檔案讀取或寫入的功能，是無法使用的，像是：將網頁存成 epub 檔案，或 pdf 檔案，選取自訂的字型來呈現網頁內容。\n最後，有一點比較困擾我的是，EinkBro App 在使用一陣子後，常會遇到整個畫面就沒有反應，要等很多秒鐘後，它會自動關閉，我才能再重開它一次。但是這個問題並沒有發生在其他有安裝EinkBro App的設備上。所以，我想這應該是Pubook在某些地方還沒有調整好的關係，希望在往後的系統更新上能再多多改善這問題。\n輸入法 一看到這鍵盤，就知道Pubook內建的App是來自於萊姆輸入法。不過，應該是我還沒有改過的版本。一來，原先萊姆輸入法的配色並沒有做過調整，在Pubook上的對比沒有到很高，但至少看起來夠清楚；但另一點是：當點擊每個字母時，它有做按鍵浮起的效果，這在電子紙上其實是可以考慮移除的，只要在邊框加粗，有點效果就好。\n另外，內建只有倉頡、速成、注音、拼音、英文等輸入法。既然用了萊姆輸入法，如果能讓使用者也自行匯入字根就好了。我注音不好，所以通常會採用拼音輸入，結果拼音並沒有把常用字放在最前面的候選字，在打字時常常還需要把整個候選字區域展開，從幾十個字中挑我要的字。\n整體系統使用心得 Pubook做為一個封閉式的閱讀器，確實做到了設備輕薄有質感，看起來耐用，操作介面也很簡潔好用。\n如果不想要為了看書，要學一堆操作，或是灌一堆軟體，做一堆設定的話，Pubook會是個不錯的選擇。既可以在家中長時間閱讀，或是帶出門偶爾拿出來看看文章。\n相關文章 Pubook申請改為開放式後的感想\n","permalink":"https://plateaukao.github.io/blog/posts/pubook%E9%9B%BB%E5%AD%90%E6%9B%B8%E9%96%B1%E8%AE%80%E5%99%A8%E7%9A%84%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/cca916ab4265/1_3THIEyB_FALTXV6BsgRMGA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e這篇文章會來說說2023年4月多就拿到手的Pubook，這段時間以來的使用心得，包含硬體上的感受，以及軟體使用上的感想。內容會很主觀，不過我至少也長期使用過十幾二十台電子書閱讀器了，理論上應該也算是客觀才對。\u003c/p\u003e","title":"Pubook電子書閱讀器的使用心得"},{"content":"這篇文章將講解怎麼在 EinkBro 中利用攔截 http request，將 Youtube 影片在呈現字幕時，能夠順便顯示第二種外語字幕。\n這樣子的功能通常都是在 PC 瀏覽器上利用外掛的 extension 完成的。在手機或是平板上幾乎很少有瀏覽器可以支援這樣子的效果。之前我也一直很想要在平板上有類似這樣子的功能，原先是想要修改很好用的 Youtube Alternative App NewPipe，無奈它採用了 ExoPlayer 當播放器，而 ExoPlayer 對我來說又過於複雜，最終一直沒有試出來。\n但是，回到 EinkBro App 上的話，要做類似的修改就容易多了。以下就來說說我是怎麼實作的。\n找出字幕的 url request 利用 Google Chrome Debugger Tool 觀察 Youtube 顯示和不顯示字幕時，會多出一個 http request，如下圖。\n它的全部 url 會類似下面這個例子\nhttps://m.youtube.com/api/timedtext?v=-Y8ATrBvx3A…..\n所以，目前的作法是只要看到有 timedtext 的 url，我就認定它應該是 Youtube要顯示字幕，需要開始來做點處理。\n同時下載兩個語言的字幕檔 在 WebView 中想攔截 http resource request 的話，可以實作在 WebViewClient 中的 shouldInterceptRequest() 函式。\n下面的程式碼寫得落落長，但重點只在於 newUrl = “$url\u0026amp;tlang=zh-Hant” 和下載 oldCaption，newCaption。要從 timedtext 的 url 延伸成其他翻譯好的字幕語言，只需要加上 tlang 的參數就行了，是不是很方便。\n下載後，再來就是把兩個字幕檔合併在一起，只要合併得好，其實Youtube 並不知道它究竟顯示的是它原先需要的原字幕檔，還是經過我處理的版本。合併的方式在下一小節說明。\nprivate fun handleWebRequest(webView: WebView, uri: Uri): WebResourceResponse? { ... if (url.contains(\u0026#34;timedtext\u0026#34;)) { val newUrl = \u0026#34;$url\u0026amp;tlang=zh-Hant\u0026#34; val oldCaption = runBlocking { BrowserUnit.getResourceFromUrl(url) } val newCaption = runBlocking { BrowserUnit.getResourceFromUrl(newUrl) } val oldCaptionJson = json.decodeFromString(TimedText.serializer(), String(oldCaption)) val newCaptionJson = json.decodeFromString(TimedText.serializer(), String(newCaption)) // 合併兩個字幕檔 return WebResourceResponse( \u0026#34;application/json\u0026#34;, \u0026#34;UTF-8\u0026#34;, ByteArrayInputStream( json.encodeToString(TimedText.serializer(), oldCaptionJson).toByteArray() ) ) } ... } 合併兩個字幕檔 仔細看 timedtext API 呼叫後回傳的資料結構(如下)，它是一個 json 檔案，說明格式為 wireMagic 的 pb3。而重點就在於其中的 events。events 中定義了每段字幕的起始/結束時間，和字幕內容。不論哪個語言都是以這種型式表現。\n{ \u0026#34;wireMagic\u0026#34;: \u0026#34;pb3\u0026#34;, \u0026#34;pens\u0026#34;: [ { } ], \u0026#34;wsWinStyles\u0026#34;: [ { } ], \u0026#34;wpWinPositions\u0026#34;: [ { } ], \u0026#34;events\u0026#34;: [ { \u0026#34;tStartMs\u0026#34;: 366, \u0026#34;dDurationMs\u0026#34;: 1834, \u0026#34;segs\u0026#34;: [ { \u0026#34;utf8\u0026#34;: \u0026#34;\\t它画质只有2.7K\u0026#34; } ] }, { \u0026#34;tStartMs\u0026#34;: 2200, \u0026#34;dDurationMs\u0026#34;: 7633, \u0026#34;segs\u0026#34;: [ { \u0026#34;utf8\u0026#34;: \u0026#34;但是…我喜欢…今儿是新品影石insta360 Go3的主场\u0026#34; } ] }, { ... }] } 了解資料結構後，先建立一堆 data class，把 json 轉成可以操作的對象。\n@Serializable data class TimedText( @SerialName(\u0026#34;wireMagic\u0026#34;) val wireMagic: String, @SerialName(\u0026#34;pens\u0026#34;) val pens: List\u0026lt;Pen\u0026gt;, @SerialName(\u0026#34;wsWinStyles\u0026#34;) val wsWinStyles: List\u0026lt;WsWinStyle\u0026gt;, @SerialName(\u0026#34;wpWinPositions\u0026#34;) val wpWinPositions: List\u0026lt;WpWinPosition\u0026gt;, @SerialName(\u0026#34;events\u0026#34;) val events: MutableList\u0026lt;Event\u0026gt; ) @Serializable class Pen @Serializable data class WsWinStyle( @SerialName(\u0026#34;mhModeHint\u0026#34;) var mhModeHint: Int? = null, @SerialName(\u0026#34;juJustifCode\u0026#34;) val juJustifCode: Int? = null, @SerialName(\u0026#34;sdScrollDir\u0026#34;) var sdScrollDir: Int? = null ) @Serializable data class WpWinPosition( @SerialName(\u0026#34;apPoint\u0026#34;) val apPoint: Int? = null, @SerialName(\u0026#34;ahHorPos\u0026#34;) val ahHorPos: Int? = null, @SerialName(\u0026#34;avVerPos\u0026#34;) val avVerPos: Int? = null, @SerialName(\u0026#34;rcRows\u0026#34;) val rcRows: Int? = null, @SerialName(\u0026#34;ccCols\u0026#34;) val ccCols: Int? = null ) @Serializable data class Event( @SerialName(\u0026#34;tStartMs\u0026#34;) val tStartMs: Long = 0, @SerialName(\u0026#34;dDurationMs\u0026#34;) val dDurationMs: Long = 0, @SerialName(\u0026#34;id\u0026#34;) val id: Int = 0, @SerialName(\u0026#34;wpWinPosId\u0026#34;) val wpWinPosId: Int? = null, @SerialName(\u0026#34;wsWinStyleId\u0026#34;) val wsWinStyleId: Int? = null, @SerialName(\u0026#34;wWinId\u0026#34;) val wWinId: Int? = 1, @SerialName(\u0026#34;segs\u0026#34;) val segs: MutableList\u0026lt;Segment\u0026gt;? = mutableListOf() ) @Serializable data class Segment( @SerialName(\u0026#34;utf8\u0026#34;) var utf8: String, @SerialName(\u0026#34;acAsrConf\u0026#34;) val acAsrConf: Int = 0 ) 下面是合併兩個檔案的方式：對於 startMs 相同的片段，就把它們的字幕組合到同一個區段中。\noldCaptionJson.wsWinStyles.forEach { if (it.mhModeHint != null) { it.mhModeHint = 0 } if (it.sdScrollDir != null) { it.sdScrollDir = 0 } } oldCaptionJson.events.forEach { event -\u0026gt; if (event.segs != null \u0026amp;\u0026amp; event.segs.size \u0026gt; 0) { val first = event.segs.first() first.utf8 = event.segs.map { it.utf8 }.reduce { acc, s -\u0026gt; acc + s } first.utf8 += \u0026#34;\\n\u0026#34; + newCaptionJson.events.firstOrNull { it.tStartMs == event.tStartMs }?.segs?.map { it.utf8 } ?.reduce { acc, str -\u0026gt; acc + str } ?: \u0026#34;\u0026#34; event.segs.clear() event.segs.add(first) } } 這麼一來，就完成啦！\n示範影片 相關程式碼 commit: https://github.com/plateaukao/einkbro/commit/ab6f79c285e6d546dcfa1e8e5c6efb09e9abcd60\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-einkbro-%E4%B8%AD%E6%94%AF%E6%8F%B4-youtube-%E5%BD%B1%E7%89%87%E7%9A%84%E9%9B%99%E8%AA%9E%E5%AD%97%E5%B9%95/","summary":"\u003cp\u003e這篇文章將講解怎麼在 EinkBro 中利用攔截 http request，將 Youtube 影片在呈現字幕時，能夠順便顯示第二種外語字幕。\u003c/p\u003e\n\u003cp\u003e這樣子的功能通常都是在 PC 瀏覽器上利用外掛的 extension 完成的。在手機或是平板上幾乎很少有瀏覽器可以支援這樣子的效果。之前我也一直很想要在平板上有類似這樣子的功能，原先是想要修改很好用的 Youtube Alternative App NewPipe，無奈它採用了 ExoPlayer 當播放器，而 ExoPlayer 對我來說又過於複雜，最終一直沒有試出來。\u003c/p\u003e","title":"在 EinkBro 中支援 Youtube 影片的雙語字幕"},{"content":"這篇文章將說明在已經整合好 ChatGPT 後，怎麼將 API 改成支援 stream 型式連續回傳部分結果，讓使用者可以更快地得到回應。\n前一陣子將 EinkBro 整合完 ChatGPT 後，常常會使用它來翻譯日文或韓文的網頁內文，並試著改善在 system role和 user prompt 中要代入的字串。不過，不論我怎麼怎麼修改，ChatGPT的回覆速度總是不盡人意，慢吞吞的，等個五到十秒是家常便飯，還常常會 timeout，連結果都不給了。\n因此，我又再研究了一下，將它改成能夠接收部分的結果回來。一來，API 的第一反應速度快了超多，通常一秒內最多兩秒就會開始回傳資料；二來使用者可以馬上開始閱讀結果，只要閱讀速度夠快，就能在它回傳結束時，也同時閱讀完畢。如此一來，等於是幾乎沒有什麼等待的時間。\n以下就是我做一些修改。\n加入 sse 函式庫 目前整合 ChatGPT 的實作，是自己利用 okhttp3 函式庫硬刻出來的；所以，想要有 stream 型式回傳方式的話，要再加入 okhttp3 的 sse 函式庫。SSE 是 server sent event 的縮寫。\nWiki 上的原文解釋如下，這裡也附上 ChatGPT 翻譯的結果：**Server-Sent Events（SSE）是一種伺服器推送技術，使客戶端能夠透過HTTP連接自動接收伺服器的更新。**它描述了伺服器在建立初始客戶端連線後如何啟動資料傳輸給客戶端。SSE通常用於向瀏覽器客戶端發送訊息更新或連續的資料流，並透過一個名為EventSource的JavaScript API來增強本地、跨瀏覽器的串流功能。透過EventSource API，客戶端可以請求特定的URL以接收事件串流。EventSource API是作為HTML5的一部分，由WHATWG標準化。SSE的媒體類型是text/event-stream。\nServer-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection, and describes how servers can initiate data transmission towards clients once an initial client connection has been established. They are commonly used to send message updates or continuous data streams to a browser client and designed to enhance native, cross-browser streaming through a JavaScript API called EventSource, through which a client requests a particular URL in order to receive an event stream. The EventSource API is standardized as part of HTML5[1] by the WHATWG. The media type for SSE is text/event-stream.\n可以在 build.gradle 中加入以下的內容\nimplementation \u0026#39;com.squareup.okhttp3:okhttp:4.10.0\u0026#39; implementation \u0026#39;com.squareup.okhttp3:okhttp-sse:4.11.0\u0026#39; // for http sst 修改 HTTP Request，加入 stream 參數 OpenAI 的 ChatGPT API 本身就有支援 stream，可以在 request 中將其設為 true。修改 request 就是這麼簡單。需要更動比較大的部分是在回傳值處理上。請見下一段的說明。\nprivate fun createRequest( messages: List\u0026lt;ChatMessage\u0026gt;, stream: Boolean = false, // 在函式中新增這個參數，想使用時，將其設為 true ): Request = Request.Builder() .url(endpoint) .post( json.encodeToString(ChatRequest(\u0026#34;gpt-3.5-turbo\u0026#34;, messages, stream)) .toRequestBody(mediaType) ) .header(\u0026#34;Authorization\u0026#34;, \u0026#34;Bearer $apiKey\u0026#34;) .build() } // ChatRequest @Serializable data class ChatRequest( val model: String, val messages: List\u0026lt;ChatMessage\u0026gt;, val stream: Boolean = false, // 新增加的代入參數 val temperature: Double = 0.5, ) 處理 stream 型式的回傳資料 當建立好 HTTP Request 後，由於我們已經預期要收到 stream 型式的結果，這個 request 在呼叫上的方式會有些許不同。這邊會使用到一開始引入的 okhttp3-sse 函式庫中的 EventSources.createFactory() ，藉此建立 factory。然後再利用 factory.newEventSource 建立 EventSource。這時，會有個 onEvent 函式能讓你收到一直傳來的 data string，你可以在這邊做結果的處理。\nfun chatStream( messages: List\u0026lt;ChatMessage\u0026gt;, appendResponseAction: (String) -\u0026gt; Unit, failureAction: () -\u0026gt; Unit, ) { val request = createRequest(messages, true) factory.newEventSource(request, object : okhttp3.sse.EventSourceListener() { override fun onEvent( eventSource: EventSource, id: String?, type: String?, data: String ) { if (data == null || data.isEmpty() || data == \u0026#34;[DONE]\u0026#34;) return try { val chatCompletion = json.decodeFromString\u0026lt;ChatCompletionDelta\u0026gt;(data) appendResponseAction(chatCompletion.choices.first().delta.content ?: \u0026#34;\u0026#34;) } catch (e: Exception) { failureAction() } } }) } 因為 stream 型式回來的資料型態有些不同，所以我又另外實作了幾個 data class 來做轉換，包含 ChatCompletionDelta 和 ChatChoiceDelta 和 ChatDelta。這裡主要就是對照著官網的回傳結果實作出來的。\n@Serializable data class ChatCompletionDelta( val id: String, val created: Int, val model: String, val choices: List\u0026lt;ChatChoiceDelta\u0026gt;, ) @Serializable data class ChatChoiceDelta( val index: Int, val delta: ChatDelta, @kotlinx.serialization.Transient @SerialName(\u0026#34;finish_reason\u0026#34;) val finishReason: String? = null, ) @Serializable data class ChatDelta( val content: String? = null, ) 在畫面上即時更新 資料部分都處理好了，再來是把相關的結果即時更新在畫面上。在 chatStream 中可以看到，在收到 onEvent 呼叫時，除了將結果轉成 data class 外，還會再呼叫 appendResponseAction() 。這個函式會是在原先的 GptViewModel 中實作。下面可以看到，因為已經在 GptViewModel 中有實作一個 _responseMessage 的 MutableStateFlow，所以這裡只要很單純地去把新拿到的結果 append 到它的 value 上就行。\nCompose UI 本來就會collect _responseMessage 的狀態改變而跟著重繪內容。UI 和 ViewModel 間的實作完全不用動到！這就是用了 ViewModel 的好處呀~\nfun query(userMessage: String? = null) { ... if (config.enableOpenAiStream) { openaiRepository.chatStream( messages, appendResponseAction = { _responseMessage.value += it }, failureAction = { _responseMessage.value = \u0026#34;Something went wrong.\u0026#34; } ) return } ... } 示範畫面 相關程式碼 整合 stream https://github.com/plateaukao/einkbro/commit/5010d5c0d883dad7613a86ac42ebebe5859e4394 新增開關 stream 的設定 https://github.com/plateaukao/einkbro/commit/dc0f844c7cdc34487b2c0d82e3695108dd8c1fb6 ","permalink":"https://plateaukao.github.io/blog/posts/%E6%95%B4%E5%90%88-chatgpt-stream%E5%9E%8B%E5%BC%8F%E7%9A%84-api-%E7%B5%90%E6%9E%9C/","summary":"\u003cp\u003e這篇文章將說明在已經整合好 ChatGPT 後，怎麼將 API 改成支援 stream 型式連續回傳部分結果，讓使用者可以更快地得到回應。\u003c/p\u003e\n\u003cp\u003e前一陣子將 EinkBro 整合完 ChatGPT 後，常常會使用它來翻譯日文或韓文的網頁內文，並試著改善在 system role和 user prompt 中要代入的字串。不過，不論我怎麼怎麼修改，ChatGPT的回覆速度總是不盡人意，慢吞吞的，等個五到十秒是家常便飯，還常常會 timeout，連結果都不給了。\u003c/p\u003e","title":"整合 ChatGPT stream型式的 API 結果"},{"content":"前不久有人推出了 Desktop 上瀏覽器的 Immersive Translation Plugin，可以在看外文網頁時，以段落的方式翻譯內容。這種方式對於正在學習語言或是想要雙語對照著看的用戶來說，真的是一大福音。\n雖然它很好用，但是在手機上有支援的瀏覽器 App 並不多。在 iPhone 上，Safari App 的 Plugin 可以安裝；但是在 Android 平台上，只有少數幾個選擇 : kiwi browser，或是用起來怪怪的 xBrowser。\n想當然爾，目前 EinkBro 並不支援這 plugin 。如果想要支援的話，得要先整合 GreaseMonkey 相關的 API set 才有機會；不然，就是要自己參考它的方式，在 EinkBro App 中自己實作類似的功能。\n後來我選擇了後者，因為目前的架構下，自己實作會是比較單純的。\n大綱 利用原有的 Reader Mode 解釋整個流程 Google Translate 的兩種實作方式 利用原有的 Reader Mode 在兩年前曾經介紹過怎麼在 EinkBro 中實作 Reader Mode：在 Firefox 中的 readerview 模組以及 readability.js 的協助下，EinkBro 可以為大多數的網頁提供乾淨的閱讀內容。\n打造 E-ink 專用的瀏覽器: Part V\n當畫面元素只剩下核心的內容元件後，要支援沉浸式翻譯就相對上容易許多，因為 Reader Mode 的實作已經先對亂七八糟的 html elements 做了一次過濾，只留下含有文字的 html elements。\n流程解釋 流程圖 sequenceDiagram autonumber 步驟 當使用者按下 Immersive Translate 按鈕時，會先跟 WebView 傳達該要進入 Reader Mode 了。 這時，WebView 會將 Readability.js 載入，並且請它把 html 內容過濾過濾，取出當中屬於本文的文字內容。 回傳本文的文字內容 要進入閱讀模式的話，這一步就可以直接顯示本文的文字內容；但是因為我們想要的功能是沉浸式翻譯，所以要再把拿到的文字內容交給 Jsoup 函式庫處理處理。這裡的處理指的是：4.1 為每個文字元件加上一個 to-translate 的 class name，然後還順手在它們的 sibling 加上一個 元件, 做為翻譯結果的存放處。4.2 對這些文字元件加上 visibility 的 listener。當它們出現在畫面上時，才需要去翻譯該段文字。 回傳完成的整包結果 讓 WebView，把整包結果顯示出來。(這時，lisener 開始在運作) 為了讓 WebView 中 web 的 visibility event 能夠傳回 Android native 的實作中，這裡建了一個 class JsWebInterface。 callback 回來時，會呼叫 JsWebInterface 中的 getTranslation()，裡頭會呼叫已經實作好的 translate repository 的函式，拿到翻譯後的文字。 翻譯好的文字會透過 evaluateJavascript 再帶回 Web 中。 相關程式碼 步驟 4 中的 Jsoup 處理方式如下。行 4 ~ 10 就是在加 tag 和補一個 的元件。行 15 則是加入 visibility listener 。\n下面是載入的 Javascript。行 8 建立了一個 IntersectionObserver，並在 31行為每個文字元件加上這個 observer。\n行 12 判斷 entry 被顯示時，會在行 17 呼叫 bridge 的函式 getTranslation()，等結果回來時，行 1 的 myCallback 會被執行，將翻譯好的內容帶入之前建立好的 元件。\n接下來，我們來看看步驟 8 中的 getTranslation() 函式。下圖中的行 7 出現了 Semaphore！為什麼這邊要使用 semaphore 呢？因為通常在捲動畫面時，常常會讓多個段落一下子顯示在畫面上，如果讓他們一個接著一個去取得翻譯，整體的反應時間會比較久。所以，在這邊設定了 semaphore 4，希望段落翻譯可以盡量地同步進行。\n行 16 呼叫了 Google Translate 的實作。當翻譯結果回傳時，會被存在 translatedString 中，在行 23 處再利用 webView.evaluateJavascript 將這結果送回 web 的 callback 中。\nGoogle Translate 的兩種實作方式 嚴格來說，應該是有三種方式：\n付費去申請 Google Translate API 的使用權，依使用量付費 利用 http request 去呼叫 Google Translate 網頁，把取得的網頁內容做處理，取出其中翻譯的結果 利用網路上其他人發現的方式，呼叫 Google Translate API 第一種方式請大家參考 Google 官網的介紹就好。\nCloud Translation documentation | Google Cloud\n在 EinkBro 中，先是使用第二種方式，後來改成第三種。在這邊分別來說說實作的方式。\n採用 Google Translate 網頁 行 25: 因為實作裡的 okhttpclient 是以 callback 的型式回傳結果，這裡使用的是 suspendCancellableCoroutine，它可以把 callback 的用法包裝成一般的 suspend function，方便呼叫的人使用。 行 26: 可以看到，這裡使用的是一般的網頁連結 https://translate.google.com。代入需要的參數後(最重要的是 q，它的值就是想翻譯的字串) 行 39: 將組好的 url 交給 okhttpclient 去處理 行 50: 取出 body 內容，交給 Jsoup 處理。Google Translate 網頁中，會把翻譯結果放在 result-container 的 html element 中。只要能從其中取出文字，就表示翻譯成功。 採用網路上找到的 Google Translate API 新的實作方式，除了改用 API 外，也移除了原先的 callback 實作，看起來更加簡潔。\n行 70: 一樣要利用 HttpUrl 建立 url，但這次使用的是 translate.googleapis.com。然後這裡有個神奇的參數(client=gtx)，加上後就可以正常取得翻譯結果。 行 88: 換成 coroutine 的方式去打 API 行 93 ~ 98: 從 response 的 json 中，取出翻譯文字。這邊的實作有點醜，因為當時還沒有引入任何 json parsing 的函式庫。之後應該會再小小地改寫一下吧。 示範畫面 相關連結 開始支援的版本 v10.3.0\nRelease Release v10.3.0 · plateaukao/einkbro\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-einkbro-%E4%B8%AD%E5%AF%A6%E4%BD%9C%E6%B2%89%E6%B5%B8%E5%BC%8F%E7%9A%84%E7%BF%BB%E8%AD%AF%E6%95%88%E6%9E%9C/","summary":"\u003cp\u003e前不久有人推出了 Desktop 上瀏覽器的 \u003ca href=\"https://youtu.be/0nIzWCseLVo\"\u003eImmersive Translation Plugin\u003c/a\u003e，可以在看外文網頁時，以段落的方式翻譯內容。這種方式對於正在學習語言或是想要雙語對照著看的用戶來說，真的是一大福音。\u003c/p\u003e","title":"在 EinkBro 中實作沉浸式的翻譯效果"},{"content":" 用 BOOX Tab Ultra C 瀏覽 Readmoo 書城\n這一篇文章將會說明 BOOX Tab Ultra C 的硬體特色，以及這一週來使用上的感想。\n從以前到現在，購買文石的電子書閱讀器少說也有五台以上了，從 6 吋的 Poke4 S，7.8吋的 Nova 3 Color, Nova Air C，到10.3吋的 Note Pro, Note3, 再到 13 吋的 Max3 都有使用過，對於文石家的軟硬體都有一定的熟悉度。不過，這次發行的 Tab Ultra C (後面簡稱 TUC) 還是讓我購買後，每天期待它的到來。\n在使用幾天後，打算先來寫一下初期的使用感想。\n硬體 配置 CPU: Qualcomm 8核心\n螢幕大小: 10.3 吋\n螢幕材質: Kaleido 3 Carta 1200 glass screen\n解析度: 黑白 2480x1860 (300ppi)，彩色 1240x930 (150ppi)\n記憶體 (RAM): 4GB\n可儲存容量 (ROM): 128GB\nG-sensor: 可以自動旋轉畫面\n相機: 16MP 後置鏡頭\n指紋辨識的開機按鈕\n重量: 480 克\n硬體初步感想 這台最大的特色就是那塊使用了 Kaleido 3 技術的彩色電子紙螢幕。它的表現確實比前幾代的技術要進步許多。除了解析度來到可用程度的彩色 150 ppi；在顯示黑白內容時的 300 ppi，也是10吋設備中的最高解析度。目前也只有 Kindle Scribe 在 10 吋上有達到這解析度。\n在高解析度的加持下，10吋的畫面終於看起來不再糊糊的了。這塊螢幕的底色雖然還比不上黑白電子紙的白晰，但已經來到了可以不開燈的情況下使用的地步。使用性大大地提升了。\n整台機身是金屬材質的，很有質感，但是缺點是很重。跟華為 Matepad Paper 的 360 克比較起，**足足重了 120 克！**如果要長時間閱讀的話，單手應該是不大可能，通常要放在腳上或是放在桌上比較合適。如果是在家閱讀的話，我通常會把皮套拆下來(磁吸式的，不重，很好拆卸)使用，會稍微輕那麼一點點。\n文石 Tab Ultra C 金屬邊框近照\n關於電池的續航力 剛拿到後，除了把一些常用的 App 安裝好外，幾乎都是拿它來看網頁。一開始並沒有特別計算使用時間；但是，長時間用來上網瀏覽網頁的話，兩三天一充是免不了的。\n不過，如果單純是拿來看書而不是拿來上網的話，在不開 wifi 的情況下，看書約三個多小時，電量還有 92%。以此推算，純閱讀的話，三十四個小時不是問題。以一天閱讀兩小時為基準，有機會可以撐到兩三週充一次電。\n這部分等之後有更多數據再跟大家分享。\n**5/19更新：**在平常除了看書外，還會上上網，做做點小筆記，電量看起來很夠用。\n分別是使用了2天和3天時的狀況\n使用心得 開箱後的第一個感觸是：怎麼那麼重！平常閱讀時，習慣使用 170 克的 7 吋掌閱閱讀器 Ocean2，頂多會再用 BOOX Note3 (325克) 或華為 Matepad Paper (360 克)看網頁。所以初次拿起重達 480 克的 TUC時，相當有感。\n跟7.8 吋的 Nova Air C 比起來， TUC 畫面的底色更白了。大部分我在使用的時候都沒有開前光。\n由於有 BSR 螢幕快刷晶片的加持，無論是畫面的切換或是網頁的瀏覽、捲動，整個操作都很流暢，也幾乎沒有殘影。畫面刷新模式有 HD, Balanced, Fast, Ultra Fast 和 Regal。HD 和 Regal 是畫質最高的模式，畫面需要大部分重繪，速度上比較慢一些。除此之外，個人覺得 Balance, Fast, Ultra Fast 都很快，都可以用來瀏覽網頁，在畫面上捲動內容而不用擔心有殘影或是速度過慢。\n瀏覽網頁 ONYX BOOX Tab Ultra C: browsing web\n在 Kindle App 中看漫畫 Onyx BOOX Tab Ultra C: read manga in Kindle app\n拿到手大約五天，已經在 TUC 上用 Hyread App 看完一本書，和看了不知道多久的網頁。跟黑白閱讀器比起來， TUC 更適合看網頁，因為網頁中總是會有圖案或是利用不同顏色來表示不同的重要性或區塊。有顏色的區別會更容易辨識。而一般小說的話，如果是純文字沒有圖片，或是只有少數圖片的話，平常的黑白閱讀器就很夠用了。\n目前還沒有很重度地使用筆記功能，這點也是等以後有更多心得時再更新。\n5/19 更新：\n目前筆記 App在非 HD 或 Regal 模式下，會有筆劃顯示不全的問題。讓我困擾了好幾天；但是發現在 HD 模式下是正常時，至少可以先用這方式避開問題，等官方早日提供更新檔。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3-onyx-boox-tab-ultra-c-%E5%BD%A9%E8%89%B2%E9%9B%BB%E5%AD%90%E7%B4%99%E8%9E%A2%E5%B9%95%E9%9B%BB%E5%AD%90%E6%9B%B8%E9%96%B1%E8%AE%80%E5%99%A8%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/70bfe43b1178/1_kttVU3ZUdUluuVUtECiN8Q.png\"\u003e\n\u003cem\u003e用 BOOX Tab Ultra C 瀏覽 Readmoo 書城\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e這一篇文章將會說明 BOOX Tab Ultra C 的硬體特色，以及這一週來使用上的感想。\u003c/p\u003e\n\u003cp\u003e從以前到現在，購買文石的電子書閱讀器少說也有五台以上了，從 6 吋的 Poke4 S，7.8吋的 Nova 3 Color, Nova Air C，到10.3吋的 Note Pro, Note3, 再到 13 吋的 Max3 都有使用過，對於文石家的軟硬體都有一定的熟悉度。不過，這次發行的 Tab Ultra C (後面簡稱 TUC) 還是讓我購買後，每天期待它的到來。\u003c/p\u003e","title":"文石 Onyx BOOX Tab Ultra C 彩色電子紙螢幕電子書閱讀器使用心得"},{"content":"本文記錄了怎麼在 Ridibooks 上購買韓文書籍，以及事後的書籍管理方式。\n學習韓文，在差不多到達一定程度後，會開始找更多的資料來廣泛閱讀。除了線上的各種媒體外，也可以找自己有興趣的書籍來看。這幾年韓國也有不少好看的小說進到台灣的市場來。由於翻譯需要時間，所以，如果想要看第一手的韓文小說，或是想要讀市面上暢銷書的韓文原版的話，通常得要到韓國的線上書城才買得到。\n線上購買書籍 這篇文章要介紹的是怎麼從 RIDIBOOKS 上買書。首先，當然是先申請個帳號囉。登入後，先選一本自己想要購買的書籍，然後點擊右下方的購買。\n點下後，會出現付費的選擇畫面，這時，要選擇最下面的海外發行信用卡的選項。\n再來，就是填入信用卡相關的訊息，就購買完成囉。\n下載已購買的書籍 要將書籍下載到電腦上，必須要先安裝它在桌上型電腦的軟體。連結可以在官網畫面下方取得。紅色框框的部分寫的是 Viewer Download。\n因為我使用的是 Mac Studio，所以我選擇了 Mac OS 的版本安裝。\n安裝完後，一樣可以用自己建立好的帳號登入。該軟體無法截圖，所以我是用手機拍攝的。它會列出你已經購買好的書籍，這時，可以點擊下載，將它們都先下載到本機上，但，切記，不要點開來看。\n這時，可以點擊右上方的設定按鈕，它會列出下載的書籍會放在哪個目錄下。以我自己來說，書籍都是放在下面這個路徑：\n~/Library/Application Support/Ridibooks/library/plateaukao/…\n這時，雖然書籍都下載下來了，但無法直接用一般的閱讀 App 開啟，因為都有受到 DRM 的保護。以下是一些主要書商使用的 DRM 格式：\nAmazon Kindle : Amazon’s own DRM Kakao Page : Teruten MediaShell Kyobo eBook : Fasoo.com crema : MarkAny Ridibooks : MarkAny (Android) / Fasoo.com (PC) 後續處理 — 轉成雙語書籍 在經過一些處理後，已經可以在一般的 App 中閱讀這些書籍。但是，畢竟全都是韓文的內容，就算大概看得懂，閱讀的速度還是會相當地慢。\n隨著時代的進步，目前已經可以使用 AI 來幫忙做翻譯這件很費時費力的事了。一本好幾萬字的外文書，只要花幾個小時讓 AI 來處理一下，在平均花不到一美元的情況下，就能完成。\n我使用的是 github 上的 bilingual_book_maker 這個 repository。目前已經翻過三、四本書，品質都足夠好，可以拿來做一般的閱讀用。偶爾遇到不知所云，前言不對後語的情形時，再回過頭來看一下原文就可以了。\n結語 這年頭，想要學好一個語言，沒有學習管道不再是一個藉口。網路上還有各式各樣的工具和內容可以嘗試，總是會找到適合自己的學習方法。\n","permalink":"https://plateaukao.github.io/blog/posts/ridibooks-%E7%9A%84%E6%9B%B8%E7%B1%8D%E8%B3%BC%E8%B2%B7%E8%88%87%E7%AE%A1%E7%90%86/","summary":"\u003cp\u003e本文記錄了怎麼在 Ridibooks 上購買韓文書籍，以及事後的書籍管理方式。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e學習韓文，在差不多到達一定程度後，會開始找更多的資料來廣泛閱讀。除了線上的各種媒體外，也可以找自己有興趣的書籍來看。這幾年韓國也有不少好看的小說進到台灣的市場來。由於翻譯需要時間，所以，如果想要看第一手的韓文小說，或是想要讀市面上暢銷書的韓文原版的話，通常得要到韓國的線上書城才買得到。\u003c/p\u003e","title":"RIDIBooks 的書籍購買與管理"},{"content":" 找到合適的 OpenAI 函式庫 包裝成 Repository 和 ViewModel 可以拖拉的對話框 新增設定介面 光是請 ChatpGPT 來幫忙寫 code 已經無法滿足我了，下一步是將 ChatGPT 整合進 EinkBro 中，讓使用者自己決定怎麼跟它互動。\n目前的構想是：使用者可以先針對網頁選一段文字，然後再跟 ChatGPT 說你想要對這段文字做什麼事，看是要請它解釋給你聽，翻譯成其他語言，或是抓出其中的重點等等。\n找到合適的 OpenAI 函式庫 這點倒是沒有花太多時間在上頭，反正 ChatGPT 的 API 沒有很複雜，它沒有提供認證的流程，使用者必須事前取得一把 key 才行。有了 key 之後，就只是單純的一支 API call。\n後來我串接的是 com.aallam.open:openai-client:3.2.0 。\n包裝成 Repository 和 ViewModel 因為 OpenAI 函式庫已經把 repository 要做的工作做完了，所以實際上我並沒有實作 repository layer，而是直接在 ViewModel 中生成 OpenAI instance。\nclass GptViewModel : ViewModel(), KoinComponent { private val config: ConfigManager by inject() private val openai: OpenAI by lazy { OpenAI(config.gptApiKey) } ... } ViewModel 中最重要的，也是唯一真正有呼叫 API 的函式是 query()。這邊寫死是使用 gpt-3.5-turbo 模型，雖然我最近終於排到了 gpt-4 model 的 API 使用權，但…太貴了，所以還是先用用 3.5 版本的就好。\nfun query(userMessage: String? = null) { if (userMessage != null) { _inputMessage.value = userMessage } val messages = mutableListOf\u0026lt;ChatMessage\u0026gt;() if (config.gptSystemPrompt.isNotBlank()) { messages.add(config.gptSystemPrompt.toSystemMessage()) } messages.add(\u0026#34;${config.gptUserPromptPrefix}${_inputMessage.value}\u0026#34;.toUserMessage()) val chatCompletionRequest = ChatCompletionRequest( model = ModelId(\u0026#34;gpt-3.5-turbo\u0026#34;), messages = messages ) viewModelScope.launch(Dispatchers.IO) { val response = openai.chatCompletion(chatCompletionRequest) _responseMessage.value = response.choices.first().message?.content ?: \u0026#34;\u0026#34; } } 可拖拉移動的對話框 在網頁上選取文字後，Android 系統會跳出 ActionMode menu；這個選單是系統在控制何時出現，以及會呈現哪些選項供使用者使用。在此，我打算完全捨棄系統的 menu，並改成我實作的 Context Menu。關於實作細節，今天不在這裡展開，我們先來講講，當 Context Menu 上的 GPT 按鈕被點擊後的處理方式。\n我建立了一個 GPTDialogFragment 來顯示 ChatGPT 的處理結果。在建構子 (constructor) 中塞了一個 anchorPoint 的參數。這參數是從 BrowserActivity 點擊時取得的，可以用來決定 DialogFragment 出現時能夠顯示在手指點擊的附近。\n而 setupDialogPosition() 就是依照 anchorPoint 的指示，把對話框移到合適的位子。\nclass GPTDialogFragment( private val gptViewModel: GptViewModel, private val anchorPoint: Point, ) : ComposeDialogFragment() { ... private fun setupDialogPosition(position: Point) { val window = dialog?.window ?: return window.setGravity(Gravity.TOP or Gravity.LEFT) if (position.isValid()) { val params = window.attributes.apply { x = position.x y = position.y } window.attributes = params } } } 一開始，顯示的位置對是對了，但如果想要在對話框還沒關閉前，可以把它移到別的位置的話，要怎麼做呢？這個需求不光是 GPT 的對話框有需求，剛剛提到的 custom ActionMode context menu 也需要這功能。所以，我把這功能抽到了一個 base 的 DraggableComposeDialogFragment.kt\n首先，如同 GPT 對話框，它要能被指定一個起始的位置，所以這裡實作了 setupDialogPosition() 。\nabstract class DraggableComposeDialogFragment: ComposeDialogFragment() { private var initialTouchX: Float = 0f private var initialTouchY: Float = 0f private var initialX: Int = 0 private var initialY: Int = 0 @SuppressLint(\u0026#34;ClickableViewAccessibility\u0026#34;) protected fun setupDialogPosition(position: Point) { val window = dialog?.window ?: return window.setGravity(Gravity.TOP or Gravity.LEFT) if (position.isValid()) { val params = window.attributes.apply { x = position.x y = position.y } window.attributes = params } supportDragToMove(window) } private fun Point.isValid() = x != 0 \u0026amp;\u0026amp; y != 0 ... } 再來是處理使用者手指點在對話框上移動時的邏輯。老實說，這整個 Class 都是 ChatGPT 提供給我的。\nprivate fun supportDragToMove(window: Window) { val windowManager = requireContext().getSystemService(Context.WINDOW_SERVICE) as WindowManager window.decorView.setOnTouchListener { _, event -\u0026gt; when (event.action) { MotionEvent.ACTION_DOWN -\u0026gt; { // Get the initial touch position and dialog window position initialTouchX = event.rawX initialTouchY = event.rawY initialX = window.attributes.x initialY = window.attributes.y true } MotionEvent.ACTION_MOVE -\u0026gt; { // Calculate the new position of the dialog window val newX = initialX + (event.rawX - initialTouchX).toInt() val newY = initialY + (event.rawY - initialTouchY).toInt() // Update the position of the dialog window window.attributes.x = newX window.attributes.y = newY windowManager.updateViewLayout(window.decorView, window.attributes) true } else -\u0026gt; false } } } 新增設定選項 這部分就不細講了。自從設定介面改用 Jetpack Compose 實作後，要新增設定的內容就單純多了。\n目前的作法是，如果使用者有在設定中輸入 ChatGPT API key 的話，當選取文字後，就會看到 ChatGPT 的按鈕；反之則無。\n圖片 相關連結 整合 OpenAI 函式庫 可拖拉的對話框 ","permalink":"https://plateaukao.github.io/blog/posts/%E6%95%B4%E5%90%88-chatgpt-%E5%88%B0-einkbro-%E4%B8%AD/","summary":"\u003cul\u003e\n\u003cli\u003e找到合適的 OpenAI 函式庫\u003c/li\u003e\n\u003cli\u003e包裝成 Repository 和 ViewModel\u003c/li\u003e\n\u003cli\u003e可以拖拉的對話框\u003c/li\u003e\n\u003cli\u003e新增設定介面\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e光是請 ChatpGPT 來幫忙寫 code 已經無法滿足我了，下一步是將 ChatGPT 整合進 EinkBro 中，讓使用者自己決定怎麼跟它互動。\u003c/p\u003e","title":"整合 ChatGPT 到 EinkBro 中"},{"content":"\n利用 ChatGPT 給出基本的認證流程 請它再實作出 add Url to Pocket 的函式 最後，請它將其中一個 request 從 callback 的型式改成 coroutine Pocket 服務能讓使用者將網路上想要保存的網頁儲存到 Pocket 的空間去，利用有優化過的 Reader Mode 來閱讀和整理分類。雖然我使用它的頻率不高，但如果能把它整合到 EinkBro 中，對於在電子書閱讀器上有安裝 Pocket App 的人來說，能夠更方便地將文章加入到 Pocket 清單中。\n沒有安裝 Pocket App 的人也可以開新的分頁連結到 Pocket 網站，閱讀之前從各個設備加入的網頁文章。\n之前都還沒有研究過 Pocket 的 API 支援，想說 Pocket 也已經推出好幾年了，應該 ChatGPT 當初也有 train 到它相關的 API 文件或整合的程式碼吧，趁著這次機會，來問問 ChatGPT。\n利用 ChatGPT 給出基本的實作 一轉眼 ChatGPT 也推出快要半年多了，對於軟體工程師來說，ChatGPT 加上 Github 的 Copilot，寫起 code 來真的會有種加速衝刺的感覺；可以讓人的心思都放在核心想要解決的步驟上，其他需要查找的實作細節，這兩個工具可以幫忙完成至少六七成以上的程式碼，剩下的部分才需要自己再稍做修改，或是這時再搬出 Google 和 StackOverflow 來驗證和微調。\n以下是我跟 ChatGPT 的對話：\n我：act as a senior android programmer, teach me how to integrate pocket api into an android browser app (created with webview), let’s start from the authentication part. please give me sample codes.\nChatGPT:\nAs a senior Android developer, I’d be happy to help you integrate the Pocket API into your Android browser app built with WebView. First, we’ll cover the authentication part. To use the Pocket API, you’ll need to register your application and obtain a consumer key. You can do that here: https://getpocket.com/developer/apps/new\nOnce you have the consumer key, follow these steps:\nObtain a request token Redirect the user to Pocket for authorization Convert the request token into an access token Here’s a step-by-step guide with sample code for each step:\nStep 1: Obtain a request token\nFirst, you need to make a POST request to https://getpocket.com/v3/oauth/request to obtain a request token. Use the following sample code:\nimport okhttp3.* import java.io.IOException val client = OkHttpClient() val consumerKey = \u0026#34;YOUR_CONSUMER_KEY\u0026#34; fun getRequestToken(callback: (String) -\u0026gt; Unit) { val requestBody = FormBody.Builder() .add(\u0026#34;consumer_key\u0026#34;, consumerKey) .add(\u0026#34;redirect_uri\u0026#34;, \u0026#34;yourappname://pocket-auth\u0026#34;) .build() val request = Request.Builder() .url(\u0026#34;https://getpocket.com/v3/oauth/request\u0026#34;) .post(requestBody) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // Handle error } override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { val body = response.body?.string() val requestToken = body?.replace(\u0026#34;code=\u0026#34;, \u0026#34;\u0026#34;) if (requestToken != null) { callback(requestToken) } } } }) } Step 2: Redirect the user to Pocket for authorization\nOnce you have the request token, you need to open the authorization URL in your WebView. Here’s how you can do that:\nfun openAuthUrl(requestToken: String) { val authUrl = \u0026#34;https://getpocket.com/auth/authorize?request_token=$requestToken\u0026amp;redirect_uri=yourappname://pocket-auth\u0026#34; webView.loadUrl(authUrl) } Step 3: Convert the request token into an access token\nAfter the user has authorized your app, you’ll receive a callback to the redirect_uri you specified earlier. You\u0026rsquo;ll need to intercept this callback in your WebView and obtain the access token. To do that, you can override the shouldOverrideUrlLoading method:\nclass MyWebViewClient(private val onPocketAuth: (String) -\u0026gt; Unit) : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { if (url.startsWith(\u0026#34;yourappname://pocket-auth\u0026#34;)) { val requestToken = url.substringAfter(\u0026#34;code=\u0026#34;, \u0026#34;\u0026#34;) getAccessToken(requestToken, onPocketAuth) return true } return super.shouldOverrideUrlLoading(view, url) } } fun getAccessToken(requestToken: String, callback: (String) -\u0026gt; Unit) { val requestBody = FormBody.Builder() .add(\u0026#34;consumer_key\u0026#34;, consumerKey) .add(\u0026#34;code\u0026#34;, requestToken) .build() val request = Request.Builder() .url(\u0026#34;https://getpocket.com/v3/oauth/authorize\u0026#34;) .post(requestBody) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // Handle error } override fun onResponse(call: Call, response: Response) { if ( 我: continue\nChatGPT:\nresponse.isSuccessful) { val body = response.body?.string() val accessToken = body?.substringAfter(\u0026#34;access_token=\u0026#34;)?.substringBefore(\u0026#34;\u0026amp;\u0026#34;) val username = body?.substringAfter(\u0026#34;username=\u0026#34;) if (accessToken != null) { callback(accessToken) } } } }) } 註：上面這段程式碼沒在 markdown code snippet 中，所以格式都跑掉了，但貼到 Android Studio 中倒還好，IDE 也聰明地幫忙重新縮排\nNow, put everything together in your Activity:\nclass MainActivity : AppCompatActivity() { private lateinit var webView: WebView private lateinit var webViewClient: MyWebViewClient override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) webView = findViewById(R.id.webView) webView.settings.javaScriptEnabled = true webViewClient = MyWebViewClient { accessToken -\u0026gt; // Save the access token for future use, and load your desired URL webView.loadUrl(\u0026#34;https://example.com\u0026#34;) } webView.webViewClient = webViewClient getRequestToken { requestToken -\u0026gt; openAuthUrl(requestToken) } } } In this example, replace YOUR_CONSUMER_KEY with your actual consumer key, and change yourappname to the name of your app. After completing these steps, you should have a working authentication flow for the Pocket API integrated into your Android browser app.\nNow that you have the access token, you can start making API calls to the Pocket API to retrieve, add, or modify items. More details can be found in the Pocket API documentation: https://getpocket.com/developer/docs/overview\nPlease note that the sample code provided uses the OkHttp library for network requests. If you haven’t added it to your project already, include the following dependency in your build.gradle file:\nimplementation \u0026#39;com.squareup.okhttp3:okhttp:4.9.3\u0026#39; 新增連結到 Pocket 服務中 我：and, teach me how to add a url to pocket with the accesstoken just received\nChatGPT: Once you have the access token, you can use it to add a URL to Pocket using the /v3/add endpoint. Here\u0026rsquo;s a function to add a URL to Pocket:\nfun addUrlToPocket(accessToken: String, url: String, title: String? = null, tags: String? = null, callback: (Boolean) -\u0026gt; Unit) { val requestBodyBuilder = FormBody.Builder() .add(\u0026#34;url\u0026#34;, url) .add(\u0026#34;consumer_key\u0026#34;, consumerKey) .add(\u0026#34;access_token\u0026#34;, accessToken) title?.let { requestBodyBuilder.add(\u0026#34;title\u0026#34;, it) } tags?.let { requestBodyBuilder.add(\u0026#34;tags\u0026#34;, it) } val requestBody = requestBodyBuilder.build() val request = Request.Builder() .url(\u0026#34;https://getpocket.com/v3/add\u0026#34;) .post(requestBody) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // Handle error callback(false) } override fun onResponse(call: Call, response: Response) { callback(response.isSuccessful) } }) } Here’s how to use the addUrlToPocket function:\nval urlToAdd = \u0026#34;https://www.example.com\u0026#34; val title = \u0026#34;Example Title\u0026#34; val tags = \u0026#34;tag1,tag2,tag3\u0026#34; addUrlToPocket(accessToken, urlToAdd, title, tags) { success -\u0026gt; if (success) { // URL added successfully } else { // Failed to add URL } } In this example, replace accessToken with the access token you received earlier. The urlToAdd variable should contain the URL you want to add to Pocket. You can optionally provide a title and tags (comma-separated) for the URL. The addUrlToPocket function will send a POST request to the Pocket API to add the URL, and it will call the callback function with a boolean value indicating whether the operation was successful.\nPlease note that this example assumes you are using the OkHttp library for network requests, as demonstrated in the previous code samples.\n請 ChatGPT 提供 coroutine 的實作 上面 ChatGPT 提供的程式碼幾乎是貼上就能使用，讓我省下許多時間。不過，它的實作是透過 enqueue 和 callback 完成的；在習慣使用 coroutine 後，對於 callback 有點感冒，所以又請它利用 coroutine 提供了其中一個 API 的實作方式。\n我：change addUrlToPocket function with coroutine implementation\nChatGPT: To change the addUrlToPocket function to use coroutines, you can use the suspendCoroutine function and the suspend keyword. First, add the necessary dependencies for coroutines to your project by adding these lines to your build.gradle file:\nimplementation \u0026#39;org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0\u0026#39; implementation \u0026#39;org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0\u0026#39; Then, update the addUrlToPocket function as follows:\nimport kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume suspend fun addUrlToPocket(accessToken: String, url: String, title: String? = null, tags: String? = null): Boolean { return suspendCancellableCoroutine { continuation -\u0026gt; val requestBodyBuilder = FormBody.Builder() .add(\u0026#34;url\u0026#34;, url) .add(\u0026#34;consumer_key\u0026#34;, consumerKey) .add(\u0026#34;access_token\u0026#34;, accessToken) title?.let { requestBodyBuilder.add(\u0026#34;title\u0026#34;, it) } tags?.let { requestBodyBuilder.add(\u0026#34;tags\u0026#34;, it) } val requestBody = requestBodyBuilder.build() val request = Request.Builder() .url(\u0026#34;https://getpocket.com/v3/add\u0026#34;) .post(requestBody) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { if (continuation.isActive) { continuation.resume(false) } } override fun onResponse(call: Call, response: Response) { if (continuation.isActive) { continuation.resume(response.isSuccessful) } } }) continuation.invokeOnCancellation { client.dispatcher.executorService.shutdownNow() } } } The addUrlToPocket function is now a suspending function. It returns a Boolean value indicating the success of the operation. You can call this function within a coroutine scope, like this:\nimport kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking fun main() { val accessToken = \u0026#34;YOUR_ACCESS_TOKEN\u0026#34; val urlToAdd = \u0026#34;https://www.example.com\u0026#34; val title = \u0026#34;Example Title\u0026#34; val tags = \u0026#34;tag1,tag2,tag3\u0026#34; runBlocking { launch(Dispatchers.IO) { val success = addUrlToPocket(accessToken, urlToAdd, title, tags) if (success) { // URL added successfully } else { // Failed to add URL } } } } In this example, replace YOUR_ACCESS_TOKEN with your actual access token. The urlToAdd variable should contain the URL you want to add to Pocket. You can optionally provide a title and tags (comma-separated) for the URL. The runBlocking function creates a coroutine scope, and the launch function creates a new coroutine on the Dispatchers.IO dispatcher. The addUrlToPocket function is called within the coroutine, and the result is used to determine whether the operation was successful.\n它利用 suspendCancellableCoroutine 把原先的 networking API 包了起來，雖然有達到我的要求，但總覺得 OkHttp 會有更 elegant 的解法，不過，這就有待以後再做 refactoring 了，目前修改過後的 addUrlToPocket 的實作已經是 suspend function 了。\n後面還追問了怎麼新增一個 snackbar，因為我希望在加成功後，能回傳 Pocket article 的連結，如果使用者想要直接開始閱讀的話，點個連結就會進到 Pocket 的文章內文。\n另外，還請它幫忙解析 addUrlToPocket() 回傳回來的資料，因為我想從中拿出 Pocket article url，它也都很快速地給我實作方式。\n最終，我大部分的時間是花在將他的建議實作整合到我程式的架構中，在需要按鈕的地方加上按鈕，在需要加入攔捷 url request 的地方，放上 ChatGPT 來的實作。\n這種合作開發方式，真的讓我能更專心在我想要完成的功能上，而不是去查這些技術的細節。\n相關連結 整合 Pocket 的 commit 將 addToPocket 換成 suspend function 並且加入 snackbar ","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-einkbro-%E7%80%8F%E8%A6%BD%E5%99%A8%E4%B8%AD%E6%95%B4%E5%90%88-pocket-%E7%9A%84-api/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/836ccdd395df/1_nuyvULFtogktKnCtU-frJg.png\"\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e利用 ChatGPT 給出基本的認證流程\u003c/li\u003e\n\u003cli\u003e請它再實作出 add Url to Pocket 的函式\u003c/li\u003e\n\u003cli\u003e最後，請它將其中一個 request 從 callback 的型式改成 coroutine\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003ePocket 服務能讓使用者將網路上想要保存的網頁儲存到 Pocket 的空間去，利用有優化過的 Reader Mode 來閱讀和整理分類。雖然我使用它的頻率不高，但如果能把它整合到 EinkBro 中，對於在電子書閱讀器上有安裝 Pocket App 的人來說，能夠更方便地將文章加入到 Pocket 清單中。\u003c/p\u003e","title":"在 EinkBro 瀏覽器中整合 Pocket 的 API"},{"content":"被發了一條網頁分頁 favicon 更新不同步的 issue。雖然我自己也常常遇到，但因為無傷大雅，而且一直沒找到對的時機點修正，所以一直沒有理它。\nTabs show the previous web page\u0026rsquo;s favicon icon · Issue #224 · plateaukao/einkbro\n但是，既然被發了 issue，就來解決看看吧。對於 Jetpack Compose 的元件更新機制的了解程度還是一團漿糊，所以胡亂試了好久，最終放棄亂槍打鳥，請出 chatGPT 給一下意見。\n原來如此啊！如果想用 mutableStateOf() 代入一個列表，而且又希望列表中的項目的內容有改變時，能自動更新對應的 Composable，只要在項目中對應的內容也變成 mutableStateOf() 就可以了！\n早點知道這件事的話，現在的一堆廢 code 應該就可以再找時間清一清了。\n相關程式碼 下面的程式碼就不特別再講解了，單純是照著上面的作法，將 Album 中的 bitmap 多出另一個參數 stateOfBitmap 來記錄 mutableStateOf()，然後再把手動去踢 Composable 的程式片段移除。\nfix: #224 update album cover when updated · plateaukao/einkbro@6b5f700\n","permalink":"https://plateaukao.github.io/blog/posts/%E4%BD%BF%E7%94%A8-jetpack-compose-%E6%99%82%E5%A6%82%E4%BD%95%E5%BE%97%E7%9F%A5-list-%E4%B8%AD%E7%9A%84%E5%85%83%E4%BB%B6%E7%9A%84%E5%B1%AC%E6%80%A7%E6%9C%89%E4%BA%86%E8%AE%8A%E5%8C%96%E9%80%B2%E8%80%8C%E8%87%AA%E5%8B%95%E6%9B%B4%E6%96%B0-composable/","summary":"\u003cp\u003e被發了一條網頁分頁 favicon 更新不同步的 issue。雖然我自己也常常遇到，但因為無傷大雅，而且一直沒找到對的時機點修正，所以一直沒有理它。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/plateaukao/einkbro/issues/224\"\u003eTabs show the previous web page\u0026rsquo;s favicon icon · Issue #224 · plateaukao/einkbro\u003c/a\u003e\u003c/p\u003e","title":"使用 Jetpack Compose 時如何得知 List 中的元件的屬性有了變化，進而自動更新 Composable"},{"content":"\n前言 Ocean2 真的是台很輕薄的閱讀器，就閱讀來說，已經是很夠用了，它的硬體配備不高，所以也沒有設計成可以安裝各種軟體的開放式。但是，它的底子裡跑的是 Android，所以想要安裝其他軟體的話還是有機會的。\n安裝軟體的步驟 在 Mac 上安裝 Android File Transfer 軟體 連接 Ocean2 到 Mac 上 開啟 wifi 連線，去設置-\u0026gt;工具中，點擊右上角的商店按鈕，然後點擊下載微信讀書。在下載進行中，再按一下 button，暫停下載進度 在 Mac 上開啟 Android File Transfer 軟體，打開 /iReader/app/com.tencent.weread.eink 目錄，把你想要的 apk，更名為 app.apk，拖拉到這個目錄中 在 Ocean2 上，再點一次下載按鈕，讓它跑完安裝的流程 回到前一頁，你的 apk 應該就安裝完成了 加速安裝其他軟體的方式 在網路上找到”創建快捷方式”的 apk，透過上述的方式安裝 在 Ocean2 打開創建快捷方式 App，點擊右上方三個點，選擇 “也顯示系統應用” 找到 “設置”，點擊它的 活動列表，再點擊第一個 設置 右邊的詳情 點擊詳情畫面中的 打開 按鈕 點擊畫面中的 應用和通知 找到你其他安裝的程式，像是 EinkBro 或是 apkpure 的 App，點擊它 點擊高級，看到 安裝未知應用 ，將它改為允許 這樣子設定完成後，就可以透過 EinkBro 下載 apk，並且直接執行安裝\n相關連結 掌阅电子书安装第三方应用详细教程\n[转载]掌阅ireader smart xs pro 安装第三方软件 - 电纸书爱好者\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%9C%A8%E6%8E%8C%E9%96%B1-ocean2-%E4%B8%8A%E5%AE%89%E8%A3%9D%E8%BB%9F%E9%AB%94/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1b7dd98451e5/1_QAqn3gT02bCdAVy7JS-z8Q.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1b7dd98451e5/1_-REN5LQ1SXEV-sz06w5rPA.jpeg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"前言\"\u003e前言\u003c/h3\u003e\n\u003cp\u003eOcean2 真的是台很輕薄的閱讀器，就閱讀來說，已經是很夠用了，它的硬體配備不高，所以也沒有設計成可以安裝各種軟體的開放式。但是，它的底子裡跑的是 Android，所以想要安裝其他軟體的話還是有機會的。\u003c/p\u003e","title":"如何在掌閱 Ocean2 上安裝軟體"},{"content":"從 v9.7.0 開始，EinkBro 支援在畫面上固定顯示分頁的列表。如果開啟這功能的話，整個介面看起來會更像是傳統的瀏覽器；能夠在不同分頁間快速切換。很適合在一般的電子書閱讀器上開啟。\n這個功能完成後，就一直到了現在，覺得應該可以讓它再多發揮點功用。所以，我為它加入了：\n點擊目前已經是顯示中的分頁：將畫面跳至網頁內容最頂端。 點擊目前已經是顯示中的分頁，而且也已經是在網頁內容最頂端的話：重新載入目前的網頁內容。 實作細節其實沒有什麼複雜的地方：原先在點擊當前分頁時，不會做任何動作；現在多了 if-else 判斷是否在內容頂端，是的話就重新載入網頁，不是的話就跳至內容最上方。\nisAtTop() 是新增的判斷函式；剩下的函式都是現成的。\n重構 功能實作雖然很快速和簡短，但是仔細看一下上面的程式碼：怎麼在 showAlbum() 中去根據 WebView 的狀態決定要移動內容位置還是重新載入畫面呢？這兩件事好像都跟 showAlbum() 沒有什麼關係啊？\n其實這只是因為當分頁被點擊時，會來呼叫 showAlbum() 函式，所以最直覺的修改就是直接做在 showAlbum() 中。這樣子快雖快，但會有好幾個衍伸的問題：\n單純看 showAlbum() 的實作，會無法理解為什麼裡面會有這種邏輯；而且跟 show album 字面上的意義沒有直接的關係。 showAlbum() 還有被使用在其他場合。其他場合不見得會需要根據 WebView 的狀態做不同的反應。 所以，在思考後，又多發了一個 commit 來改寫已經可以用的程式碼。對於原先的 showAlbum 不做更動，然後針對分頁點擊時的第一個函式更名，讓它更符合函式實作的內容：\n這邊的 browserController 是一個抽象的介面，姑且可以把它想成能用來操作 WebView 的中介；而 Album 則是用來表示分頁列表上每個分頁的物件。當分頁被點擊時，Album.showOrJumpToTop() 會被呼叫，依照 browserController 提供的狀態決定要做怎樣的行為。\n這麼一來，就不會去改到原本的 showAlbum()。\n相關連結 feat: click on tab will scroll top to and second time will refresh tab refactor: click on album behavior. ","permalink":"https://plateaukao.github.io/blog/posts/einkbro-%E5%88%86%E9%A0%81%E5%88%97%E8%A1%A8%E7%9A%84%E5%B0%8F%E6%94%B9%E9%80%B2/","summary":"\u003cp\u003e從 v9.7.0 開始，EinkBro 支援在畫面上固定顯示分頁的列表。如果開啟這功能的話，整個介面看起來會更像是傳統的瀏覽器；能夠在不同分頁間快速切換。很適合在一般的電子書閱讀器上開啟。\u003c/p\u003e","title":"EinkBro 分頁列表的小改進"},{"content":"3/27 K웹툰 도둑질 피해 8000억…’어둠의 경로’로 더 많이 봤다\nK網絡漫畫被盜損失8000億韓元…更多的是通過’黑暗的路徑’看的\n피해:「損害」或「受害」。這個單詞常用於描述因意外、災害、犯罪等原因導致的損失或傷害。在韓文句子中，”피해” 可作名詞或動詞使用。\n웹툰을 원작으로 한 영화, 드라마가 글로벌 시장에서 흥행하는 등 웹툰이 K콘텐츠의 주역으로 떠오르고 있지만 여전히 불법 복제가 근절되지 않고 있다. 불법 사이트가 주로 외국에 근거를 두고 활동하는 까닭에 단속이 쉽지 않고 처벌 수준도 낮다는 지적이다. 웹툰업계는 도용을 방지하기 위해 인공지능(AI) 기술을 적용하는 등 자구책 마련에 나섰다.\n雖然網絡漫畫改編的電影、電視劇在全球市場大賣等,網絡漫畫成爲K內容的主角,但盜版仍然沒有得到根除。 有人指出,由於非法網站主要以外國爲依據進行活動,因此很難管制,處罰水平也很低。 網絡漫畫業界爲了防止盜用,開始制定適用人工智能(AI)技術等自救對策。\n흥행: 「繁榮」或「成功」，通常用於描述電影、音樂、演出等娛樂產品在市場上的成功表現。”흥행” 主要用作名詞，描述商業成功或吸引大量觀眾的現象。\n26일 한국콘텐츠진흥원에 따르면 2021년 국내 웹툰 플랫폼의 전체 트래픽(페이지뷰·PV)은 286억 건으로 집계됐다. 한국어로 서비스된 불법 웹툰 사이트의 PV는 334억 건으로 합법 사이트의 트래픽보다 많았다. 트래픽이 가장 높은 불법 사이트의 PV는 121억 건으로 네이버웹툰(111억 건)을 웃돌았다. 이를 기반으로 추산한 웹툰 불법 유통시장 규모는 8427억원으로 전년 대비 53.6% 증가했다. 웹툰업계 한 관계자는 “불법 웹툰의 수요가 유료 콘텐츠에 집중된다는 점을 고려하면 불법 사이트로 인한 피해가 막대하다”고 토로했다.\n26日,據韓國文化信息振興院統計,2021年國內網絡漫畫平臺的整體流量(頁面瀏覽•PV)爲286億次。 用韓語服務的非法網絡漫畫網站的PV爲334億次,比合法網站的流量還要多。 流量最高的非法網站的PV爲121億次,超過了NAVER網絡漫畫(111億次)。 以此爲基礎推算的網絡漫畫非法流通市場規模爲8427億韓元,比前一年增加了53.6%。 網絡漫畫業界某相關人士透露:”考慮到非法網絡漫畫的需求集中在收費內容上,非法網站造成的損失非常大。”\n웃돌았다: “웃돌다”（超過、超越）的過去式，意為「超越了」或「超過了」。這個詞通常用於表示某人、團隊或事物在比賽、成績、排名等方面超越了對手或其他事物。\n업계: 「行業」或「產業」。這個詞用於描述與特定商業、產業或專業領域相關的範疇。”업계” 是一個名詞，常用於描述各種商業活動和經濟部門。\n막대하다（막대하다）在韓文中意為「龐大」、「巨大」或「大量」。這個詞常用於描述事物的數量、規模、程度等方面非常大的情況。”막대하다” 是一個形容詞，可以用來形容名詞。\n新聞連結: https://v.daum.net/v/20230326173401931\n3/26 ‘챗GPT 충격’에 승부수 던진 네이버·카카오\n在”聊天GPT衝擊”下決一勝負的NAVER和KAKAO\n챗GPT, 한글 기반 데이터 및 신뢰성에 문제 지적돼\n네이버 검색 기반 ‘서치GPT’, 카카오 채팅 활용한 ‘코GPT’ 주목\n聊天GPT、以韓文爲基礎的數據及信賴性被指出存在問題,以NAVER搜索爲基礎的”搜索GPT”、利用Kakao聊天的”Co GPT”備受關注。\n네이버의 ‘하이퍼클로바’가 챗GPT의 대항마로 꼽힌다. 네이버는 하이퍼클로바 개발을 위해 2020년 700페타플롭스(PF) 성능의 슈퍼컴퓨터를 도입했다. 1PF는 1초에 1000조 번 수학 연산을 수행할 수 있다. 현재 전 세계에서 두 번째로 빠른 슈퍼컴퓨터인 일본의 후가쿠는 442~537PF, 테슬라가 자율주행차용 AI 탑재를 위해 목표하는 슈퍼컴퓨터는 1000PF 수준이다.\nNaver的”Hyperclova”被認爲是聊天GPT的對手。 Naver爲了開發Hyperclova,在2020年引進了700 Peta Flops(PF)性能的超級電腦。 1PF每秒可進行1000兆次數學運算。 目前,全球第二快的超級計算機日本福國爲442~537PF,特斯拉爲搭載無人駕駛汽車用AI而目標的超級計算機爲1000PF。\n네이버의 하이퍼클로바는 국내 최대 규모의 슈퍼컴퓨팅 인프라를 바탕으로 5600억 개의 한국어 토큰을 학습했다. 기존 네이버 언어모델 데이터와 비교해 약 3000배, GPT-3의 한국어 데이터와 비교해도 약 6500배 이상 많은 양이다. 이는 50년치 뉴스, 9년치 블로그 글에 달한다. 네이버는 하이퍼클로바가 GPT-3의 매개변수(파라미터) 수인 1750억 개보다 많은 2040억 개의 파라미터 규모라고 밝혔다. 매개변수란 인간의 사고 과정에 관여하는 ‘시냅스(Synapse)’에 해당한다. AI에선 매개변수 수에 따라 고도화 여부를 판단한다. 즉, 하이퍼클로바는 한국어를 가장 잘 이해하고 구사할 수 있는 한국어 인공지능 모델인 셈이다.\nNaver的Hyperclova以國內最大規模的超級計算基礎設施爲基礎,學習了5600億個韓語代幣。 與現有的Naver語言模型數據相比,約高出3000倍,與GPT-3的韓國語數據相比,也高出約6500倍以上。 這相當於50年的新聞、9年的博客文章。 Naver表示,Hyperclova的參數規模比GPT-3的參數(參數)數1750億個還要多,達到2040億個。 參數相當於參與人類思考過程的”突觸(Synapse)” 在AI中,根據參數數判斷是否實現高度化。 也就是說,Hyperclova是最能理解和運用韓語的韓國語人工智能模型。\n앞으로 네이버는 하이퍼클로바를 고도화하고, 차세대 검색 프로젝트 ‘서치GPT’를 선보일 방침이다. 서치GPT는 한국어 번역에 한계가 있는 챗GPT의 단점을 해결하고 최신 데이터를 기반으로 신뢰할 만한 정보를 제공할 수 있다는 설명이다. 최수연 네이버 대표는 지난해 4분기 실적 발표 컨퍼런스콜에서 “올해 상반기에 네이버만의 검색 경험인 서치GPT를 선보이는 것이 목표”라며 “(챗GPT 같은) 생성형 AI의 단점으로 꼽히는 신뢰성, 최신성 부족, 비용 효율화 문제를 해결하는 실험의 장을 별도로 선보이겠다”고 말했다.\n今後Naver計劃將Hyperclova高度化,推出新一代搜索項目”Search GPT” Search GPT可以解決韓語翻譯有限的聊天GPT的缺點,以最新數據爲基礎提供值得信賴的信息。 naver代表崔秀妍(音)在去年第四季度業績發表會議上表示:”今年上半年目標是推出naver獨有的搜索經驗’搜索GPT’”,”將另外推出解決被認爲是(像聊天GPT)生成型AI缺點的信賴性、最新性不足、費用效率化問題的實驗平臺”\n新聞連結: https://v.daum.net/v/20230325160803394\n3/6 챗GPT 돌풍에 ‘한국어 잘하는 AI’로 승부 보는 네이버·카카오\nNaver 和 Kakao 在 ChatGPT 爆炸中與“擅長韓語的 AI”競爭\n승부 보다: 分勝負\n챗GPT 돌풍 속에서 네이버와 카카오가 ‘한국어 잘하는 인공지능(AI)’으로 승부수를 띄우고 있다. 한국어 질문에 답변이 부실하다는 챗GPT의 약점을 파고든 전략이다.\n在ChatGPT風暴中，Naver和Kakao憑藉“擅長韓語的人工智能（AI）”贏得了比賽。這是一種挖掘 ChatGPT 弱點的策略，即對韓國問題的回答很差。\n부실하다는: 不可靠的\n파고들다: 鑽研\n5일 업계에 따르면 네이버에서 오는 7월 공개하는 초대규모 AI ‘하이퍼클로바X’의 가장 큰 특징은 ‘챗GPT 대비 한국어 학습량이 6500배 많다’는 것이다. 하이퍼클로바X는 기존 하이퍼클로바를 토대로 사용자가 요구하는 응답을 즉시 제공하도록 업그레이드한 기술이다. 그동안 하이퍼클로바의 학습 데이터가 주로 한국어로 이루어진 만큼, 한국어에 최적화된 AI 언어 모델이라는 게 네이버 측 설명이다. 영어 데이터 기반인 챗GPT가 최근 한국어로 된 질문에 부정확한 답변을 내놓은 사례가 여럿 알려지면서 시장에선 신뢰성에 의문을 제기하고 있다.\n5日，業內人士稱，Naver將於7月發布的超大規模AI“HyperClova X”的最大特點是“韓國語學習量是ChatGPT的6500倍”。 HyperClova X 是基於現有 HyperClova 的升級技術，可立即提供用戶所需的響應。 Naver 解釋說，這是一個針對韓語優化的 AI 語言模型，因為 Hyperclova 的學習數據一直以韓語為主。市場對基於英文數據的 ChatGPT 的可靠性提出質疑，因為最近報導了幾起用韓語給出不准確答案的案例。\n이루어진: 實現的\n최적화: 最佳化\n부정확한: 不準確的\n카카오의 AI 자회사 카카오브레인은 올해 상반기 한국어에 특화한 AI 언어 모델 ‘코(Ko)GPT’의 새 버전을 출시할 계획이다. 김광섭 카카오브레인 최고기술책임자(CTO)는 지난 2일 온라인 기업설명회에서 “초거대 한국어 텍스트 생성 모델에 대한 기반 기술을 보유하고 있다. 적어도 한국에서는 언어 모델에 있어 압도적 경쟁력을 가질 수 있도록 투자를 아끼지 않을 예정”이라고 강조했다. 카카오브레인은 코GPT를 바탕으로 한 AI 챗봇 서비스 ‘코챗 GPT’도 연내 출시한다.\nKakao 的 AI 子公司 Kakao Brain 計劃在今年上半年發布新版本的“Ko)GPT”，這是一種專門針對韓語的 AI 語言模型。 Kakao Brain 首席技術官 (CTO) Kim Gwang-seop 2 日在在線公司說明會上表示，“我們擁有超大型韓文文本生成模型的基礎技術。至少在韓國，我們計劃不惜一切代價在語言模型上擁有壓倒性的競爭力。” Kakao Brain 還將在年內推出基於 CoGPT 的人工智能聊天機器人服務“CoChat GPT”。\n적어도: 至少\n압도적: 壓倒性的\n新聞連結: https://v.daum.net/v/20230305162414602\n3/5 10대 정신건강 망쳐서? 중국 틱톡은 왜 전 세계서 퇴출되나\nChinese Translation:\n“破坏青少年心理健康？为什么中国的抖音被全球禁用？”\nDifficult Usage Explanation:\n정신건강 (mental health): Refers to the state of an individual’s psychological and emotional well-being.\n망치다 (ruin, destroy): Refers to the act of causing harm or damage to something or someone.\n퇴출되다 (be banned, be expelled): Refers to the act of being removed or prohibited from a certain place or situation.\n青少年 (teenagers): Refers to young people who are between the ages of 10 and 19.\n破坏 (destroy, disrupt): Refers to the act of causing damage or harm to something, disrupting its normal functioning.\n중국의 동영상 공유 플랫폼 ‘틱톡’(Tik Tok)이 전방위적 압박을 받고 있다. 그동안 틱톡을 견제해온 미국뿐 아니라 캐나다, 유럽, 일본도 최근 정부 기관에 ‘틱톡 금지령’을 내리기 시작했다. 서방 국가들은 왜 평범한 동영상 애플리케이션(앱)처럼 보이는 틱톡을 밀어내는 걸까.\n「틱톡」, 중국的短視頻分享平台，正面臨全方位的壓力。除了一直在限制「TikTok」的美國之外，加拿大、歐洲和日本最近也開始對政府機構下達「禁止TikTok」的指令。西方國家為什麼要推出「看起來像一個普通的視頻應用程序（應用程式）」的「TikTok」呢？\n전방위적: 全方位的。\n압박: 壓力。\n견제하다: 限制。\n금지령: 禁令。\n틱톡은 15초~1분가량의 동영상을 공유하는 모바일 앱이다. 중국 기업 바이트댄스가 만든 이 앱은 짧은 시간 안에 유행하는 댄스나 패션, 코믹 영상 등을 즐길 수 있어 10대와 젊은 층에 인기다. 전 세계 이용자는 10억명 이상으로 트위터 이용자보다 많다.\n「TikTok」是一款可分享15秒至1分鐘左右的短視頻的手機應用程序。這款應用程序由中國企業字節跳動開發，可以在短時間內享受流行的舞蹈、時尚、喜劇等視頻，受到10多歲和年輕人的歡迎。全球用戶數超過10億人，比Twitter用戶還要多。\n짧은 시간: 短時間。\n미국 백악관 예산관리국(OMB)은 지난 27일(현지시간) 연방정부 전 기관에 30일 안에 모든 장비와 시스템에서 중국 동영상 공유 플랫폼인 ‘틱톡’을 삭제하라는 지침을 내렸다. 캐나다 정부도 28일부터 정부에 등록된 모든 기기에서 틱톡 사용을 금지하기로 했다. 유럽 의회도 이날 전 직원에게 틱톡 앱 사용을 금지했다. 덴마크 의회도 모든 의원과 직원에게 업무용 기기에 설치된 틱톡 앱을 삭제하도록 했다.\n美國白宮預算辦公室（OMB）在美國當地時間27日向聯邦政府所有機構發出指示，要求在30天內從所有設備和系統中刪除中國的短視頻分享平台“TikTok”。加拿大政府也決定從28日起禁止在政府註冊的所有設備上使用TikTok。歐洲議會當天也禁止其全體員工使用TikTok應用程序。丹麥議會也要求所有議員和職員從其工作設備中刪除安裝的TikTok應用程序。\n장비: 設備\n삭제하다: 刪除\n설치된: 安裝的\n이들 국가가 틱톡을 금지하는 핵심적인 이유는 틱톡이 사용자의 정보를 과도하게 수집하거나 개인정보를 수집해 중국 정부에 넘긴다는 의혹 때문이다. 미 백악관은 이번 지침이 민감한 정부 데이터에 대한 틱톡의 위협에 대처하는 것이라고 설명했다. 마이크 매콜 미 하원 외교위원장은 “틱톡을 다운로드한 사람은 누구나 자신의 개인정보를 중국 공산당에 넘길 수 있는 백도어를 제공한 것”이라고 말했다. 캐나다 정부도 틱톡의 데이터 수집 방식이 이용자를 사이버 공격에 취약하게 만들 수 있다는 내부 검토 결과에 따른 결정이라고 밝혔다.\n這些國家禁止TikTok的核心原因是懷疑TikTok過度收集用戶信息或收集個人信息並轉交給中國政府。美國白宮解釋，此次指示是為了應對TikTok對敏感政府數據的威脅。眾議院外交委員會主席麥克·麥考爾表示：“下載TikTok的人都是提供給中國共產黨可以轉移個人信息的後門。”加拿大政府也表示，根據內部審查結果，TikTok的數據收集方式可能使用戶容易受到网络攻击。\n新聞連結: https://v.daum.net/v/20230304000712479\n3/4 테슬라 주식 담는 개미들…올해 들어 역대급 매수세\n持有特斯拉股票的螞蟻…今年最大的一筆收購\n주식: 股票\n두달만에 작년 전체와 비슷한 순매수…하루 평균 순매수 압도적 1위\n在兩個月內，與去年全年類似的淨購買量……壓倒性的平均每日淨購買量\n순매수: 淨購買量(純買收)\n올해 들어 전 세계 개미(개인투자자)들이 테슬라 주식을 역대급 페이스로 쓸어 담은 것으로 나타났다.\n發現今年全球螞蟻（個人投資者）以前所未有的速度席捲特斯拉股票。\n개미(개인투자자)\n통상 테슬라는 위불에서 하루 주식 거래량의 10% 미만을 차지하지만, 올해는 35%를 넘은 날이 사흘이나 됐다고 데니어 CEO는 전했다.\n通常情況下，特斯拉在 Webul 上的每日股票交易量不到 10%，但今年，首席執行官丹尼爾表示，這一數字連續三天超過了 35%。\n거래량: 交易量\n사흘: 三天\n금융정보업체 팩트셋에 따르면 테슬라의 12개월 선행 주가수익비율(PER)은 43배로 가장 높았던 200배보다는 많이 낮아졌지만, S\u0026amp;P 500 평균인 17.5배보다는 여전히 크게 높은 수준이다.\n根據金融信息公司 Factset 的數據，特斯拉的 12 個月遠期市盈率 (PER) 為 43 倍，遠低於最高的 200 倍，但仍明顯高於標準普爾 500 指數 17.5 倍的平均水平。\n여전히: 仍然\n新聞連結: https://v.daum.net/v/20230304004205648 3/1 Naver\u0026quot;外賣鏈接指南\u0026quot;中角蘭媒體公司\n戶外鏈接 :\n角南:生氣\n媒體史:新體\n\u0026ldquo;禁止廣告數量限制、登錄網站移動、正常化措施不當\u0026rdquo;\n“限制廣告數量，禁止登錄頁面移動，採取常態化引導措施”\n網上新聞協反對、會員公司意見後,將傳達給Naver上\n反對在線報紙合作社，計劃收集成員公司的意見並將其傳遞給Naver\n可以引進東亞日報 戶外鏈接\u0026hellip;10日前通知\n東亞日報 Outlink 可能被引入…… 10天通知\n限制:◦\n禁止:◦\n措施不當 :\nNaver版外鏈接服務標準發佈後,媒體公司紛紛表示不滿。網絡新聞協會(會長河英春韓京網站代表、溫信協)從上個月22日開始通過3天的說明會發表的戶外鏈接適用標準中,禁止編輯△廣告數量限制。溫信協計劃收集各會員公司的意見,傳達給Naver方面。\n當Naver的外鏈服務標準公佈時，媒體不滿到準備“聯合回應”。從上月22日起，在為期3天的說明會上，Naver宣布了網絡報業協會（會長Ha Young-chun、Hankyung.com代表、Onsinhyup）的外鏈申請條件之一，△限制廣告數量△禁止移動到媒體公司的登錄網站 △違反媒體編輯部運營指南時的規範化 指南措施等規定存在較大問題的立場。 Onshinhyup 將收集每個成員公司的意見並將其傳遞給 Naver。\n不滿正在增大:不明的。\n조항: 條款(條項)\n新聞連結:http://www.mediatoday.co.kr/news/articleView.html?idxno=308783\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9F%93%E5%9C%8B%E6%96%B0%E8%81%9E-31/","summary":"\u003ch3 id=\"327\"\u003e3/27\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003eK웹툰 도둑질 피해 8000억…’어둠의 경로’로 더 많이 봤다\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eK網絡漫畫被盜損失8000億韓元…更多的是通過’黑暗的路徑’看的\u003c/p\u003e\n\u003cp\u003e피해:「損害」或「受害」。這個單詞常用於描述因意外、災害、犯罪等原因導致的損失或傷害。在韓文句子中，”피해” 可作名詞或動詞使用。\u003c/p\u003e","title":"韓國新聞 (3/1 ~)"},{"content":"最近 chatGPT 相當熱門，不論有什麼問題需要網路的協助，總是可以先找 chatGPT 聊聊，看看它有什麼想法。不過，在使用 EinkBro 想要登入 chat.openai.com 時，它會跳出下面的畫面要我先檢驗我是 human。然後，我怎麼按它都會回傳失敗。也因此，一直無法在 EinkBro 中使用 chatGPT 網頁的服務。\n這幾天在 EinkBro 的 issue 列表中，恰好也有人回報了類似的問題，描述有些網站如果有利用 cloudflare 的 captcha 服務的話，會無法正常執行。\nhttps://github.com/plateaukao/einkbro/issues/221\n解法 目前的解法，可能不是最好的方式，但至少是有效的：將 WebView 的 useragent 字串換掉就可以了。這解法是從另一個也被發了一樣 issue 的 github browser repo 看來的。\n一模一樣的問題，卻是由不同的帳號詢問，而且是發在另一個 Open Source 的瀏覽器上。這個瀏覽器的作者也找到了解法，只要先把 useragent 字串換成 Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.50 Mobile Safari/537.36 就可以了。\n在 EinkBro 上試了一下，確實可行。後來再找了一下，也在 reddit 上提到一樣的作法。\n目前這作法雖然可行，但有點麻煩，因為在 EinkBro 中，useragent 字串的設定埋得有點深 (誰會沒事一直在換 useragent 字串)。希望哪天能找到真正的原因並把它完美地解決掉。\n完成畫面 修改 EinkBro 雖然看起來只要修改 useragent 字串就可以成功，但目前在 EinkBro 中，只要有設定自訂的 useragent 字串，就默認會採用；也就是說如果在其他的網頁想要切回原本系統預設的 useragent 時，得要把原先加的 useragent 清空才行。這麼一來，下次再需要時，又要找個地方把那長長的 useragent 再複製貼上一遍，讓它生效。\n這樣子太累人了，所以我在設定中新增了一個”是否要使用自訂的 usergent”的選項；這麼一來字串設定完後就可以一直留著，不用刪來刪去的。\n相關連結 fix: support cloudflare check. · plateaukao/einkbro@f85195b\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E8%A7%A3%E6%B1%BA-cloudflare-%E8%AA%8D%E8%AD%89%E5%95%8F%E9%A1%8C-%E6%9A%B4%E5%8A%9B%E7%89%88/","summary":"\u003cp\u003e最近 chatGPT 相當熱門，不論有什麼問題需要網路的協助，總是可以先找 chatGPT 聊聊，看看它有什麼想法。不過，在使用 EinkBro 想要登入 chat.openai.com 時，它會跳出下面的畫面要我先檢驗我是 human。然後，我怎麼按它都會回傳失敗。也因此，一直無法在 EinkBro 中使用 chatGPT 網頁的服務。\u003c/p\u003e","title":"如何解決 CloudFlare 認證問題 (暴力版)"},{"content":"Android 原本內建的 Preference UI 還算方便，只要利用 xml 定義好想要的 SharedPreference 項目並指定好相關的標題、描述、key 值，預設值，Android 就可以幫忙建立對應的畫面。\n不過，既然導入了 Jetpack Compose，逐步地移除 xml 是必然的事，其中自然也包含了 Preference UI。Preference UI 方便歸方便，但畢竟是用 xml 描述的，不是透過程式碼來指定 key 值或預設值。一旦程式碼有任何更動，或是想要對 preference 的讀取或操作做更細微的邏輯判斷時，內建的 Preference UI 就力有未逮了，也是要在 Activity 中把一個個 preference 抽出來，然後針對它的行為做攔截，加上自己想要的動作。\n如果把它全換成了 Jetpack Compose 的話，就不會有這種兩段式處理的煩惱了。只是，網路上查了一下，似乎 Android 官方並沒有出一套 Jetpack Compose 版的 Preference UI。雖然自己刻一套也不算太難，但…就覺得 Android 官方不能好人做到底嗎？\n所以，對於 EinkBro 中需要的所有 Preference UI，我都自己刻了一套。其中有些需要透過顯示對話框修改值的情況，我還是保留舊的方式 — 呼叫傳統的 Dialog 起來做事。Jetpack Compose 的 Dialog 要透過 state 來 show/hide，目前我還是覺得很不習慣，情願用比較單純的傳統 Dialog。\n架構 關於 Setting 的實作，全放在同個 pakcage 下：\n程式主要分為資料部分和介面部分；資料部分全都繼承了 SettingItemInterface，然後針對不同的資料，提借了額外變數。而介面部分則是對應到不同的資料，利用 Compose 描繪了這些資料，在畫面上負責畫出各自該有的現狀。SettingScreen 則是可以容納不同設定的集合，它可以吃進一個資料的列表，然後把這個列表依照各自的型態畫出來。\nData\nUI 的實作約略如下：\nSettingScreen\n最基本的 SettingItemUi\n再由最基本的 SettingItemUi 衍伸出其他稍微不同的\n而最上層的 SettingActivity 則是所有設定的 Compose UI 的入口點，它也實作了許多需要和其他地方互動的功能，像是叫起對話框，書籤管理的呼叫等。\n整套開發下來，建立資料格式和刻 Ui 的時間並不長，主要的時間都是花在把各項資料從原本的 xml 中拔出來，放到建立好的資料結構中，並確保沒有塞錯，全都能正常運作。\n完成後的畫面 前兩張看起來似乎跟原先的 xml preference 時看起來差不多，只有選項右側多了個打勾的圖案；但實際上它的實作已經是 Jetpack Compose 了。一旦設定介面是由程式碼所建立起來的，它的彈性頓時變得很大：能夠在不同的畫面寬度下，很輕易地呈現不同的效果。\n手機上，因為畫面較窄，所以用類似原本 preference screen 的方式條列式呈現；如果是在平板或閱讀器上，因為畫面比較寬，就可以改為雙列的方式顯示。大部分的設定畫面也因此可以在一個畫面中全部看得到，不再需要捲動畫面。\n正常在手機大小上的顯示畫面\n當設備畫面大於一定寬度時，會採用雙列方式呈現，更加節省畫面的使用，一次能顯示更多項目\n對於需要跳出對話框修改設定值的項目，也整合了原生的對話框，避免需要在程式中多出一堆判斷是不是要顯示對話框的狀態。\n相關連結 Comparing v9.15.0\u0026hellip;v9.16.0 · plateaukao/einkbro\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%B0%87-android-preference-ui-%E6%94%B9%E5%AF%AB%E6%88%90-jetpack-compose-ui/","summary":"\u003cp\u003eAndroid 原本內建的 Preference UI 還算方便，只要利用 xml 定義好想要的 SharedPreference 項目並指定好相關的標題、描述、key 值，預設值，Android 就可以幫忙建立對應的畫面。\u003c/p\u003e\n\u003cp\u003e不過，既然導入了 Jetpack Compose，逐步地移除 xml 是必然的事，其中自然也包含了 Preference UI。Preference UI 方便歸方便，但畢竟是用 xml 描述的，不是透過程式碼來指定 key 值或預設值。一旦程式碼有任何更動，或是想要對 preference 的讀取或操作做更細微的邏輯判斷時，內建的 Preference UI 就力有未逮了，也是要在 Activity 中把一個個 preference 抽出來，然後針對它的行為做攔截，加上自己想要的動作。\u003c/p\u003e","title":"將 Android Preference UI 改寫成 Jetpack Compose UI"},{"content":"2/23 카카오톡 단체 대화방 ‘조용히 나가기’ 도입된다\nKakaoTalk群聊室“安靜退出”推出\n단체 대화방: 群組聊天室(團體對話房)\n[아시아경제 이승진 기자] 카카오톡 단체 대화방에서 상대방에게 알리지 않고 퇴장할 수 있는 기능이 도입된다.\n[亞洲經濟報導記者李升鎮]引入了一種功能，可以讓您離開KakaoTalk群組聊天室而不通知其他人。\n퇴장: 退場\n상대방에게 알리지 않고: 對方不知情的情況下\n카카오는 23일 “단톡방 조용히 나가기 기능의 적용 범위 확대를 준비하고 있다”면서 “그 외에도 사용자의 커뮤니케이션 피로감을 줄일 수 있는 여러 기능을 계속 검토하고 있다”고 밝혔다. 다만 이 기능이 도입되는 시점은 구체적으로 정해지지 않았다.\nKakao 23日表示，“我們正準備擴大該功能的適用範圍，悄悄離開群聊室。”不過，具體何時推出該功能尚未確定。\n그 외에도: 除此之外\n피로감: 疲勞\n줄이다:減少\n정해지다: 定\n카카오톡의 일반 단체 대화방과 오픈 채팅에서는 대화방에서 나가는 순간 ‘○○○ 님이 나갔습니다’ 라는 메시지가 뜬다. 지난해 말 카카오톡 유료 서비스인 ‘톡서랍’ 이용자들이 만들 수 있는 단체 채팅방인 ‘팀 채팅방’에는 조용히 방을 나갈 수 있는 기능을 도입했다.\n在KakaoTalk的一般群聊室和開放式聊天室中，離開聊天室的瞬間，會出現“○○○已離開”的消息。去年年末，付費Kakao Talk服務“Talk Drawer”的用戶可以創建的群組聊天室“Team Chat Room”引入了允許用戶安靜離開房間的功能。\n일반 단체 대화방에서도 해당 기능 도입을 요구하는 목소리가 이어졌다.\n在一般的群聊中，要求引入該功能的聲音不斷。\n요구하는 목소리： 要求的聲音\n新聞連結: https://v.daum.net/v/20230223211309439\n2/22 교양과목: 通識課\n감점: 扣分\n허가한다: 許可\n도록 하다: 促使\n겸임교수: 兼任教授\n절약하다: 節省\n본인 사고 한계를 넘는 것: 超越本人思考的界限\n新聞連結: https://v.daum.net/v/20230221203558780\n2/17 물러난다: 離職，退休\n이끌어왔다: 經營，主導\n합류하다: 加入\n선점하다: 搶先\n新聞連結: https://v.daum.net/v/20230217174928314\n2/16 SNS平台「BonDi」引起熱潮…個人資料由誰管理？\n열풍: 熱潮(熱風)\n這邊應該是同音的梗，該app 稱為 본디，而語尾的본디則是보다的變化。\n불특정:非特定\n끼리:之間\n맺다:連結\n新聞連結: https://v.daum.net/v/20230216195942932\n2/15 놀이터: playground, 遊樂場\n뚫이다: 打穿，打通\n급선무: 當務之急\n지속적으로: 不斷地\n재점검: 再檢驗\n목소리: 主張，意見\n변조: 變造，修改，改變\n비롯하다:為首，始於\n新聞連結: https://v.daum.net/v/20230215175621585\n2/13 알만한 기업: 知名企業\n뛰어들다: 跳入，闖入，衝入\n탑재: 搭載\n옮겨붙고: 挪动\n미리다: 落後\n조폐의 갈림길: 困境\n후발 주자: 追隨者\n적극 뛰어들다: 積極地跳進\n新聞連結：https://v.daum.net/v/20230213222702770\n2/12 춘천: 春川\n팔만대장경： 高麗大藏經\n공급： 供給\n친환경：環保（親環境）\n뒷받침：支持\n다다른：接近（reach)\n갖고 있다: 保持\n新聞連結: https://n.news.naver.com/mnews/article/087/0000951907?sid=105\n2/11 열광: 熱情(熱狂)\n퍼지다: 擴展\n뜨거웠던: 熾熱的\n선두주자인: 領頭羊 (先頭)\n양상: 樣子 （樣相）\n아기자기하다： 可愛，美妙，美麗\n풍미했던：過去風靡一時\n꾸미다: 裝潢，裝飾，收拾，整理\n이울러： 同時，並且\n서로의： 彼此的\n다만： 至少，但是\n폐쇄성： 封閉性（閉鎖性）\n관건： 關鍵\n新聞連結： https://v.daum.net/v/20230211161501494\n2/10 혁신: 創新\n청구서: 帳單\n도움으로써: 通過幫助\n정확하다: 正確\n新聞連結: https://v.daum.net/v/20230211014109320\n2/9 촉발: 引發(觸發)\n행보: 步伐(行步)\n방파제: 防波堤\n서두르다: 急於，急著，趕\n생태계: 生態系\n좋게 말하자면: 好的方面 (往好的方面想)\n這…韓國會不會太樂觀了一點。下面是 chatGPT 翻出來的效果，明顯比 Google Translate 好，我想，應該也會比 naver papago 好吧。\nfrom chatGPT\nfrom naver papago\n선보인다고 밝혔다: (展示，宣布)\n커나가고 있다: 正在擴大\n모토: 口號，標語\n리뷰 요약: 評論摘要\n챗GPT가 단순 채팅 서비스만 지원하는 것과 달리, 실용성과 상업성 모두 달성하고 있다. ==\u0026gt; 這句話真敢寫啊，說 chatGPT 只是單純的 chat ；明明它就可以幫忙產生程式碼了。雖然 Naver 推了很多服務，背後都有 AI 的技術在支援，但這並不表示 Naver 的 AI 比 chatGPT 好用啊。\n독자적인 성장세: 自主發展的態勢(生長勢)\n종속: 從屬，附屬\n꾸준히: 堅持不懈地\n긍정적: 肯定的，認可的。\n只能說，這記者想太多了\n2/8 개인정보: 個人資料\n강제: 強制\n과태료: 罰款(過怠料)\n불법 수집: 違法收集\n처분을 받았다: 收到處份\n맞춤형 광고: 個人化廣告(定制型)\n거부하면: 拒絕的話(拒否)\n위반한 것: 違反\n점검: 清點，檢查(點檢)\n부과하다: 徵收，課賦\n新聞連結 https://v.daum.net/v/20230208155007577\n2/6 인센티브: incentive\n줄이다: 縮小，縮減\n사상 최대 매출: 銷售歷史新高(史上最大賣出)\n역성장: 逆成長\n효율화: 效率化\n임직원: 員工 (任職員)\n전망: 預計 (展望)\n양도제한조건부주식: RSU\n新聞來源 https://v.daum.net/v/20230206182807290\n2/5 전쟁: 戰爭\n챗: chat\n둘러싸고: 圍繞\n주목받고 있는: 受到關注的\n대화형: 對話型\n대체: 取代\n입증: 立證，證明\n글로벌 빅테크: global big tech\n창립자: 創立者\n비공개: 非公開，未公開\n앞섰다: 走在前面，領先\n평가를 받는: 收到~評價的\n애저: Azure\n셈이다: 算是\n회의록: 會議記錄\n관심: 關心，關注\n모으다: 收集，收攏\n밝혔다 → 밝히다: 宣布，公布\n월간활성이용자(MAU): 月間活性使用者\n점유율: 佔有率\n선언하다: 宣言，宣布\n대응에 나섰다: 採取了~的對應\n가운데: 中間，中心\n언어 병렬 처리 기술: 語言平行處理技術\n맥락과 의미: 脈絡與意義\n新聞連結 https://v.daum.net/v/20230205181804937\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%96%B1%E8%AE%80%E9%9F%93%E5%9C%8B%E6%96%B0%E8%81%9E-25/","summary":"\u003ch3 id=\"223\"\u003e2/23\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e카카오톡 단체 대화방 ‘조용히 나가기’ 도입된다\u003cbr\u003e\nKakaoTalk群聊室“安靜退出”推出\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e단체 대화방: 群組聊天室(團體對話房)\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[아시아경제 이승진 기자] 카카오톡 단체 대화방에서 상대방에게 알리지 않고 퇴장할 수 있는 기능이 도입된다.\u003cbr\u003e\n[亞洲經濟報導記者李升鎮]引入了一種功能，可以讓您離開KakaoTalk群組聊天室而不通知其他人。\u003c/p\u003e","title":"閱讀韓國新聞 (2/5~)"},{"content":"文石 Mira 電子紙螢幕的調整小工具 文石針對 Mira 和 Mira Pro 雖然有推出官方的工具軟體，但是一年更新不到一次，而且介面又是用 Javascript 和 html 寫的，寫得不是很好用。這篇文章要來介紹一個使用者自己反組譯 Linux 上的官方工具後，開發出來的 command line 程式 mira-js 。以及我後來對它做的一點小修改。\n# 大綱 * 介紹 Mira-js 和使用方式 * 少了什麼 * 了解目前 mira-js 的實作 * 如何修改它 * 怎麼使用修改後的版本 介紹 Mira-js 和使用方式 Mira-js 是個用來調整 Boox Mira 和 Boox Mira Pro 電子紙螢幕參數和模式的小工具，可在 Linux, MacOS, 和 Windows 上執行。與官方版本不同的是，它是透過指令執行的，不需要用滑鼠點來點去。\nGitHub - ipodnerd3019/mira-js: Utility for Boox Mira\n安裝方式很容易，一行指令搞定 (要事先有裝 nodejs 就是了)。\nnpm install -g boox-mira 想要執行時，就根據需求，下不同的參數：\nnpx mira refresh npx mira settings --speed 3 --contrast 11 少了什麼 對於 Windows 或 Linux 用戶可能這樣子就很好用了，但如果是 Apple 新 CPU 設備的用戶(M1 或 M2 系列)，很可能就會跟我一樣遇到一接上 Mira 螢幕，畫面會閃個不停。在 reddit 上也三不五時有使用者在反應：\n後來文石新版的官方小工具提供了一個新的選項，可以調整 anti-shaking 的程度。如果把它調到 high 的話，就能很有效地抑制 M1 CPU 系列的畫面閃爍問題。\n但是，這功能並沒有實作在 Mira-js 中。少了這個功能的話，我還是得要使用官方工具在每次接上螢幕時，重新做一次設定。每天往返在公司電腦和個人電腦間使用 Mira，操作到人都覺得煩了。\n所以，我想要來試試看能不能將這個功能也加入到 Mira-js 中。\n了解目前 Mira-js 的實作 首先，研究了一下 Mira-js 程式碼，它是用 javascript 寫成的，最終會變成 npm 的一個模組。所以，其他人在安裝時很方便，只要下一行指令就可以把作者發布到雲端的版本裝到 local。因為提供的功能不多，程式碼也不多，主要就兩個檔案：\nsrc/mira.js: 真正在執行調整的實作，會從系統去拿到一個可能是類似 file descriptor 的 instance，對著它寫入各種不同的指令，達到調整的效果。\nbin/cli.js: 處理下指令時輸入的參數，和做一些簡單的參數檢查和說明。\n要新增 anti-shake 功能的話，分別需要在 cli.js 加上新的參數名稱和在 mira.js 中加入對應的實作。cli.js 的部分比較簡單，可以直接看最後的 pull request； mira.js 的話，先來看一下原先的指令是怎麼處理的。這邊截取部分的程式碼來說明： OP_CODE 是每個指令對應的代號，在寫入 file descriptor 時，要帶入想要的代號，後面再接上數值。\n比方說想要設定畫面更新速度時，會用 OP_CODE.set_speed 這個代號，然後 adjustedSpeed 就是使用者輸入的數值。而單純想要讓畫面重新刷新的話，就只要代入 OP_CODE.refresh 就行。\nconst OP_CODE = { refresh: 0x01, set_refresh_mode: 0x02, set_speed: 0x04, set_contrast: 0x05, set_cold_light: 0x06, set_warm_light: 0x07, set_dither_mode: 0x09, set_color_filter: 0x11, }; async refresh() { await this.write([USB_REPORT_ID, OP_CODE.refresh]); } async setSpeed(speed) { let adjustedSpeed = clamp(speed, 1, 7); adjustedSpeed = 11 - adjustedSpeed; await this.write([USB_REPORT_ID, OP_CODE.set_speed, adjustedSpeed]); } 有了這基本認知後，再來就是要找出 anti-shake 的代號和它所需要的數值是多少。這就要開始到官方的程式中挖寶了。\n解開 electron APP，查看程式碼 Mac 上的官方 Mira 工具程式是用 electron 包裝起來的 javascript 軟體 (其他平台應該也是)。要看裡頭程式碼的話，可以直接按快捷鍵 option+command+i 開啟 chrome debugger tool 來看。但是這樣子看 javascript 很痛苦，因為沒有縮排、換行，而且還充滿了混滛後的不知所云函式/變數名稱。\n所以，首要步驟是先下載個好用的 IDE。原本想說用 VS Code 就好，但一想到還要熟悉它的操作，就覺得累。所以，利用之前申請到的 JetBrains license，直接下載 WebStorm 來用，整個介面就跟 Intellij IDEA 還有 Android Studio 幾乎一模一樣，完全無痛上手。\n解開 electron APP 的方式很簡單，只要照著下面的說明，找到主要的程式碼壓縮檔，執行一行程式碼就行。\nnpx asar extract app.asar folder_for_unpacked GitHub - jonmest/How-To-Tamper-With-Any-Electron-Application: This work-in-progress outlines known…\n解開來後，再用 WebStorm 打開來搜索想要的資訊。\n在茫茫程式碼中，找到了類似 mira-js 中的指令代號，其中的 autoDither: 18 應該就是我想要的 anti-shake 功能；程式碼裡稱之為 autoDither 。這裡先記著它的數值是 18。\nconst Q = { fullRefresh: 1, 有了指令代號後，再來要看官方工具中的 close , low, middle, high 要怎麼轉換成所需的數值。先沿著 autoDither 這條線索，找到它使用的方式：2875 行可以看到它會將 e 利用某個資料結構 te 轉為數值 t ，再呼叫 setSettings 功能。 setSettings 就是類似 mira-js 中的 write 函式。\n那麼，我們再來追一下 te 到底是什麼。果然沒錯，它就是個定義了 anti-shake 程度和數值之間的一個對照表。\n收集到這些資訊之後，後續的事就簡單啦，只要拿著這些資訊，到 mira-js 中新增一個指令出來就行。\n修改 cli.js 先在 cli.js 中，加上新的參數。在這裡我只加了一個 antishake 的參數，沒有吃後續的數值。因為，應該不會有人會想要把 antishake 關掉才對，會來呼叫的人應該都是用了 Mac 系統，受畫面閃爍之苦才對。\n修改 mira.js 最後，在 mira.js 中補上將指令代號和數值餵給 USB HID 的實作。\n下面可以看到，我在 OP_CODE 中新增了 set_auto_dither_mode ，把值設為0x12 (還記得剛剛查到的值是十進位的 18 嗎？這裡得要用16進位才行)；另外，autoDitherMode 的數值就仿照官方工具中的宣告建立一個 dictionary。\n都完成後。就可以把它推到 local 端的 node module repository 中試試。\nnpm install -g . 再來，先在官方的小工具中將 anti-shake 設為 Close，關掉官方小工具(這點很重要，不然 mira-js 會無法執行！)，然後在 Terminal 中測試下面的指令：\nnpx mira antishake 如果一切順利，畫面不再閃個不停，就可以發 pull request 給原作者了！\n怎麼使用 Mira-js 指令 指令，不就像最一開始介紹的那樣，在某個 Terminal 中輸入就好嗎？這麼說雖然沒錯，但每次想要調個參數都還要開個 Terminal 來執行指令，也是一樣麻煩啊。\n最方便的方式應該還是要跟官方版本小工具一樣，能夠設定全域快捷鍵；隨時隨地想呼叫時，都能按按鍵盤就完成。\n在 Mac 上，有很強大的 Automator 能幫忙達到這樣的小需求。只要先到 Automator 中新建一個 Quick Action，在其中加入 Run Shell Script，把 Mira-js 想要執行的指令輸入其中 (記得第一行要加上 PATH=/opt/homebrew/bin/:$PATH ，不然會找不到 npx )，為這個 Quick Action 取個好名字。\n然後，進到 System Preferences 的 keyboard 設定畫面，打開 Services，找到 General 的下拉選單，找到剛剛取好名字的 Quick Action，就能為它新增快捷鍵了。\n相關資料 修改的程式碼\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3-mira-%E9%9B%BB%E5%AD%90%E7%B4%99%E8%9E%A2%E5%B9%95%E7%9A%84-commandline-%E8%AA%BF%E6%95%B4%E5%B0%8F%E5%B7%A5%E5%85%B7/","summary":"\u003ch3 id=\"文石-mira-電子紙螢幕的調整小工具\"\u003e文石 Mira 電子紙螢幕的調整小工具\u003c/h3\u003e\n\u003cp\u003e文石針對 Mira 和 Mira Pro 雖然有推出官方的工具軟體，但是一年更新不到一次，而且介面又是用 Javascript 和 html 寫的，寫得不是很好用。這篇文章要來介紹一個使用者自己反組譯 Linux 上的官方工具後，開發出來的 command line 程式 \u003ccode\u003emira-js\u003c/code\u003e 。以及我後來對它做的一點小修改。\u003c/p\u003e","title":"文石 Mira 電子紙螢幕的 commandline 調整小工具"},{"content":"在網路上找大台北地區的跑步路線，看到一個很值得試試的路線：三重和蘆洲剛好可以繞成一圈。東邊是河濱路段，西邊則是微風運河和一堆親水公園，如果臨時體力不足想要徹退的話，隨時都可以往中間切，回到市區叫車。\n地點的話，可以任選一個公園，通常都會有大大的停車場可以停車。這次我選的是三重區的幸福水漾公園。這裡有很多兒童設施，所以遊客也是超級多。如果出門時段是人多的時段，建議可以選人少一點的公園。\n由於到達時已經下午兩點半了，想說如果想看夕陽的話，人在西邊會好一些，所以起跑是由水漾公園朝著鴨鴨公園的方向前進。沿路可以看到台北車站新光三越大樓和信義區 101 大樓的相對位置不斷地變換。\n鴨鴨公園有種一些景觀花，也有一些遊樂設施，所以有不少家長帶著小朋友在那邊拍照和遊玩。\n鴨鴨公園過了不久，會在步道中看到空軍三重一村的指示牌。跑著跑著，就跑出河濱去一探究竟。三重一村雖然小小的，但保存地很不錯。\n另外，這個路線也有規劃很好的馬拉松練習指示，可以跟著路邊的牌子還有地上的指示標做練習。\n東邊路線要跑完時，可以遠眺遠方的關渡大橋，以及關渡宮。如果在好天氣時來跑步的話，應該可以看到很漂亮的河景。\n接下來，來到西邊的路段，因為太陽漸漸下山，就沒有什麼好拍的了。如果能夠一大早來跑這個路段，應該可以繼續拍不少風景照吧。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%B0%E5%8C%97%E4%B8%89%E9%87%8D%E8%98%86%E6%B4%B2%E7%B9%9E%E4%B8%80%E5%9C%88-20-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e在網路上找大台北地區的跑步路線，看到一個很值得試試的路線：三重和蘆洲剛好可以繞成一圈。東邊是河濱路段，西邊則是微風運河和一堆親水公園，如果臨時體力不足想要徹退的話，隨時都可以往中間切，回到市區叫車。\u003c/p\u003e","title":"[新北][三重][蘆洲]繞一圈 20 公里"},{"content":"桃園南崁溪有很完善的自行車步道，可以一路從桃園龜山騎到竹圍漁港。因為我是從台北下去起跑的，所以找了個看起來好停車的中間點 — 三民運動公園；然後一路開始往竹圍漁港前進。因為久沒跑步，這次只跑了 14 公里左右，最遠來到了榮興橋，然後就返回了。很可惜體力不足以一路跑到竹圍漁港。下次應該會從榮興橋附近再完成後面的路線吧。\n取自桃園觀光導覽網\n受夠了陰雨綿綿的台北，來到桃園後，太陽終於又露了臉。桃園南崁溪河面不寬，所以它的河濱也就比較窄一點，兩旁都是住家或是正在興建中的高樓住宅。很多路段很像是日本東京的小溪風景。\n從三民運動公園開始，就有可以逛的景點。走個幾步路，有個文化中心，一樓就是土地公廟。建在高樓一樓的土地公廟，還真是第一次看到。\n然後，這來回14公里的路程中，沿途竟然有超過十間的土地公廟，超乎我的想像！每間又都裝飾得很仔細。邊跑可以邊看各種造型的土地公廟，也是種很特別的經驗。\n河的兩邊就是建築，和少不了的各種橋。河濱兩旁的公園，因為腹地不大，跟台北的河濱比起來就覺得沒什麼，比較像是一個又一個的小公園。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%A1%83%E5%9C%92%E5%8D%97%E5%B4%81%E6%BA%AA%E6%A1%83%E5%9C%92%E5%8D%97%E5%B4%81%E8%87%AA%E8%A1%8C%E8%BB%8A%E6%AD%A5%E9%81%93-14-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e桃園南崁溪有很完善的自行車步道，可以一路從桃園龜山騎到竹圍漁港。因為我是從台北下去起跑的，所以找了個看起來好停車的中間點 — 三民運動公園；然後一路開始往竹圍漁港前進。因為久沒跑步，這次只跑了 14 公里左右，最遠來到了榮興橋，然後就返回了。很可惜體力不足以一路跑到竹圍漁港。下次應該會從榮興橋附近再完成後面的路線吧。\u003c/p\u003e","title":"[桃園][南崁溪]桃園南崁自行車步道 14 公里"},{"content":"今年算是疫情的最後一年；在生活型態上，已經熟悉了在家上班的型式，下班的生活模式也算是很固定。在這 2022 年的最後一天，來列一下今年各方面的數據，當做明年再努力的參考。\nEinkBro 首先，自然是來看一下 EinkBro 在這一年來的進展。\n大 Release 28 次：v8.26.0 ~ v9.16.0。平均兩週會有一次發布。 commit 數: 439。平均一週有 8.4 次 commit。 git log --oneline --since=2022-01-01 |wc -l 星星數：458。從原先的兩百多，預期會來到 300；但因為華為推出 Matepad Paper，在其花粉俱樂部的推波助瀾下，來到了 458。 技術演講：在 COSCUP 2022 中做分享，也已經上了 Youtube。 技術文章分享：於 Medium 上共有14篇文章。 * Android SharedPreferences 的實作改善技巧\n* Google Play Store: 您的裝置與這個版本不相容\n* 解了一個萬年 EinkBro 臭蟲：savedInstanceState 的雷\n* Differences between onPause() and pauseTimers()for Android WebView\n* 濾掉網址中的追蹤碼\n* Android 裡從另一個 Activity 中取得資訊的方式\n* Go Compose, No Compromise\n* 利用 Github Actions 建立 Android 專案的 release apk\n* 從 Android WebView 中取得點選連結的文字\n* Flutter 和 Android 上 UDP Multicast 的應用：同網路下的資訊分享功能\n* Multi-touch Gesture Support for EinkBro APP\n* Annotation Support for Epub file in EinkBro App\n* 在 Android WebView 中使用手機上的字型檔案\n* 改善將網頁內容寫入既有 epub 的流程\n參加 iThome 鐵人賽 2022：獲得佳作。(去年也有參加，也是佳作) 其他 side project 以下是今年新建的 side projects 或是延續去年持續有在維護的案子。\n萊姆輸入法 (連結) 自從 LIME HD 從 Google Play Store 下架後，我維護的 sweetlime 因為在 ptt 和某些嘸蝦米的社群有被提及，所以一直有使用者來下載使用。而當初我自己開始維護的初衷是，本來我在 Android 上就一直是使用 LIME HD，因為在電子書閱讀器上的畫面，需要更乾淨的介面，所以有從 Github 上 fork 了一份來修改。隨著 Android SDK 不斷演進，也有調整一下程式，讓它可以繼續在新版的 Android OS 上安裝使用。\n文章就寫了一篇而已。\n萊姆輸入法(Lime HD)的替代品\nQuickRotate 讓使用者在文石 Onyx 的電子書閱讀器上可以快速地旋轉畫面，或是切換刷新模式的小工具。自從開發和安裝了這個小工具，就沒再用過 Onyx 內建的旋轉按鍵了。\nMyNewPipe NewPipe 是個很有名的 Youtube App Alternative，可以讓使用者更方便地在不需要登入 Google 帳號的情況下觀看影片。沒有廣告，可以背景播放，可以只有播放聲音，還可以下載影片檔或是字幕檔。雖然我已經是 Youtube Premium 的付費用戶了，但是因為 NewPipe 是 Open Source 的，方便我修改一些我想要的功能，所以很多時候我還是會利用 NewPipe 來觀看影片。\n我自己 fork 出來的這一個版本可以顯示 Youtube auto generated caption 的翻譯字幕。\n舉例來說，如果我在看一個韓文影片，Youtube 會自動產生這部影片的韓文字幕，然後，它還會將這個 auto generated 的字幕翻譯成各國語言。\n在 PC 網頁觀看 Youtube 時，可以選得到這些翻譯好的字幕；不過，目前在手機上的 Youtube App 並不支援這功能，只能看到 auto generated 的原文字幕。這對於外語學習還沒很到位的人來說，看原文字幕有些時候還是沒有辦法理解影片的內容。\n所以，在參考了網路上的一些資料後，我讓 NewPipe 可以在顯示字幕時，能夠選擇翻譯成中文的 auto generated caption。每次我需要看中文字幕時，就會把 YT 影片連結分享到 NewPipe 中觀看。\nZoom-Close 很小的一個 Chrome extension，可以將 zoom 的網頁在幾秒後自動關閉網頁。這對於常常在開 Zoom 會議的人來說，超級方便。(只要在 json 檔案中把 slack 的連結也加入的話，對於 slack 的網頁也可以有同樣的效果)\nMediumGoIncognito 這是另一個小小的 Chrome extension，可以在 Medium 隱藏文章內容時，將網頁快速切到無痕模式並重新載入。一鍵看文章！\n這麼一來，就不用費事地先開啟無痕模式，再 copy/paste 網址。\nSharik 跨設備間的傳訊一直是個困擾。如果是 Apple 生態系，Apple 已經提供了很好的解法，讓使用者能很方便地在 iPhone, iPad, Mac 間傳遞資訊；但是如果把範圍擴大到要包含 Android 平台，甚至是不見得有 Google Play Services 的電子書閱讀器的話，一直都還是沒有個比較好的方案。這邊的好，指的是除了要方便順手之外，最好還能是免錢，安全，然後，Open Source！\n今年在三月多時找到了 Github 上有人寫了一個 sharik 的工具，利用 flutter 開發，能夠在不同平台間方便地傳遞文字，圖片，或文件。雖然 App 中加了很多我覺得不必要的元素，但因為它是 Open Source 的，只要自己拿下來改成自己喜歡的樣子就好。\n所以，我自己 fork 了一版，拿掉不必要的元件，還改寫了核心的傳輸方式，讓它更有效率，提高成功率。甚至，還整合進了 EinkBro App，讓兩者之間能夠互動。\n目前我每台 Apple/Android 設備中都會裝上 Sharik App 囉。\nIntellij-Eink-Theme 去年買的電子紙螢幕文石 Mira ，也使用一年多了。雖然使用上一切順手，不過對於它在 Android Studio 中的呈現介面，總覺得還有改進的空間。所以，我寫了個 Intellij plugin，讓它在 Intellij-IDEA 上能夠有更高的對比效果。\nEink UI Theme - IntelliJ IDEs Plugin | Marketplace\n讀過的書 今年年初開始，給自己訂下了一個每天要讀書40分鐘的目標。前半年都有認真的執行，也著實讀了不少書。但是七月多開始，隨著要準備 COSCUP 的演講，和九十月在參加 iTHome 鐵人賽，整個人就鬆懈下來了。不過，整體來說，效果還是很不錯的。希望明年也能持續下去。每天 40 分鐘的目標比訂定一年要讀多少本書來要來得實際，因為有些書看得快，有些書看得慢，如果有數量目標的話，很有可能為了達標而忽略了某些需要細讀的部分。\n到目前為止，讀了43本書，其中不包含很多看了一半，還沒來得及看完的書。\nDaniel @ nowhere\n運動 去年下半年很頻繁地在玩滑板。今年就少了些，然後從七月開始，每天都是大晴天，開始回復到跑步。從十月後，次數又漸漸減少，因為…又開始天天下雨了。總的來看，461 公里並不多。不過，如果明年能從一月就開始乖乖跑的話，應該一整年下來可以達到 1000 公里吧。目前就先當成是個改進的目標。\n單純跑步有點無聊，所以除了平日會在下班時在住家附近跑跑之外，週末都會儘量挑河濱或是沒跑過的地方來跑步，順便拍拍照。也順手把這些記錄放到了 Medium。目前記錄的路線還不是很多，希望以後能愈來愈豐富，讓也愛跑步的人可以當作參考。\n邊跑邊拍\n韓文學習 兩三年前開始學習韓文，但自從考過 TOPIK I 之後，就沒有再有系統地學習。因縁際會，在去年年底時，加入了一個 LINE Openchat 的群組，開始了每天學習韓文並打卡的活動。\n早期有利用公司的補助買了 Jella 上的課程，不過一直沒有好好地讀過，趁著這次機會把它們都看過了一次。\n很認真的打卡下，完成了以下課程。高階下的課程，讀到一半，就又跑去看我的第一本文法的進階篇和高階篇了。之後應該會再回過頭來看吧。\n進階下的後半部因為是偏向會話的內容，我就先跳過了。\n在看我的第一本韓語文法時，也順便利用 logseq 做了筆記的 pdf，方便自己在不同的設備上做復習。目前正在看 Tomi 的中級講座。等看完後，應該也是能再產生一份 pdf，事後復習。\n展望 2023 年 希望 2022 年好的習慣能維持，在 2023 年能持續成長。2023 年算是後疫情時代，希望能有機會再出國走走，讓自己的生活能更多彩多姿。\n","permalink":"https://plateaukao.github.io/blog/posts/2022%E5%B9%B4%E5%9B%9E%E9%A1%A7/","summary":"\u003cp\u003e今年算是疫情的最後一年；在生活型態上，已經熟悉了在家上班的型式，下班的生活模式也算是很固定。在這 2022 年的最後一天，來列一下今年各方面的數據，當做明年再努力的參考。\u003c/p\u003e","title":"2022年回顧"},{"content":"在今年 iTHome 鐵人賽最後一天的文章中有介紹到，怎麼將 Boolean 的 SharedPreferences 包裝起來，讓原本很煩瑣的 value getter setter 可以透過 delegate 的方式，一行搞定。下面是包裝前和包裝後的程式碼。\n利用 getter / setter 讓外部可以簡單地對 SharedPreference 操作\n先實作一下 BooleanPreference ：\n改善之後的寫法如下。是不是更一目暸然呢？\n上面的 BooleanPreference 中有個函式叫 toggle() 原先是預期外部的使用者可以呼叫它來改變當前這個 sharedPreference 的值，可以將醜醜的 config.thisIsSomeKey = !config.thisIsSomeKey 變成 config.thisIsSomeKey.toggle() 就好。不過，在真正執行時，卻發現這個函式無法被存取到。\n原因在於當使用 config.thisIsSomeKey 時，ReadWriteProperty.getValue() 已經起了作用，拿到的已經是 Boolean 值了。 Boolean 值自然沒有什麼 toggle() 的函式可以使用。\n那麼，究竟可以怎麼做，才可以達成 config.thisIsSomeKey.toggle() 的效果呢？先看結論，用下面的函式，就能擴充上面的 BooleanPreference ，讓它能執行 toggle() 這個函式。\nfun KMutableProperty0\u0026lt;Boolean\u0026gt;.toggle() = set(!get()) 至於什麼是 KMutableProperty 呢？在 Kotlin 中，可以利用 reflection 取得程式中的一些元素，而 KMutableProperty 就是其中一種。\n一開始實作的 BooleanPreference 在 class 中被宣告為變數時，便可以利用 KMutableProperty0 來取得該元素，而不是被直接傳回 getter 執行後的 Boolean 值。\n然後，再透過擴充 KMutableProperty0 ，增加 toggle() 函式，就可以在還沒拿回 Boolean 值時，呼叫 toggle() 。而這裡的實作也利用了 KMutableProperty0 的原始函式 get() 和 set()，達成 true/false 互換的效果。\n另外，在使用時，有點不同的是原先預期想要透過 config.thisIsSomeKey.toggle() ，因為利用了 KMutableProperty0 來實作；在使用時要改成 config::thisIsSomeKey.toggle() ，才能正確地拿到 reflection 的變數。\n儘管只是一行程式碼，卻是我花了好幾個月才終於找到的解法。\n修改完後，除了程式碼看起來更加清爽外，語意也更為明顯。\n相關資料 EinkBro 中的修改 Kotlin专题「二十五」：反射_小丑超梦的博客-CSDN博客_org.jetbrains.kotlin:kotlin-reflect\n","permalink":"https://plateaukao.github.io/blog/posts/android-sharedpreferences-%E7%9A%84%E5%AF%A6%E4%BD%9C%E6%94%B9%E5%96%84%E6%8A%80%E5%B7%A7/","summary":"\u003cp\u003e在\u003ca href=\"https://ithelp.ithome.com.tw/articles/10304792\"\u003e今年 iTHome 鐵人賽最後一天的文章\u003c/a\u003e中有介紹到，怎麼將 \u003ccode\u003eBoolean\u003c/code\u003e 的 \u003ccode\u003eSharedPreferences\u003c/code\u003e 包裝起來，讓原本很煩瑣的 value getter setter 可以透過 delegate 的方式，一行搞定。下面是包裝前和包裝後的程式碼。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/5651a732158b/1_85hSkEsxO_BRbjsz3PhVRA.png\"\u003e\n\u003cem\u003e利用 getter / setter 讓外部可以簡單地對 SharedPreference 操作\u003c/em\u003e\u003c/p\u003e","title":"Android SharedPreferences 的實作改善技巧"},{"content":"\nEinkBro 做為一個瀏覽器，很多開發的功能都是圍繞著內容呈現的調整。今天，要來介紹 9.14.0 新加入的特別功能：語音閱讀網頁內文。\n在 Android 上，Text to Speech 功能早在 API level 4 就開始支援，不過，好像很少有瀏覽器有支援這功能。畢竟，大部分人使用瀏覽器都是想要用眼睛去看內容，而網頁的篇幅通常也不會太長。\nTextToSpeech | Android Developers\n在電子書閱讀器上使用 EinkBro 的使用者，很多時候是想要用它來看網路小說，在這種情況下，如果 EinkBro 可以提供語音閱讀的功能的話，在使用方式上就更加地多元。\n實作 包裝 TextToSpeech 儿件 系統內建的 TextToSpeech 元件提供了大部分語音閱讀需要的函式。首先，新增一個 TtsManager class，將 TextToSpeech 相關的操作都寫在裡面。\n在 28, 30 行，可以看到兩種不同的啟動語音閱讀的型態：一個是 QUEUE_ADD，一個是 QUEUE_FLUSH。前者是把文字片段加到語音閱讀的 queue 當中，而 QUEUE_FLUSH 表示，當呼叫後，就開始播放 queue 中的內容。\n另外，還有一點值得注意的是：25 行的 TextToSpeech.getMaxSpeechInputLength() 是每段語音的最大長度。超出這長度的文字，會造成 tts 不唸這段文字。為了要繞過這問題，這裡使用了 kotlin collection 的 chunked 將一大段文字切分成允許範圍內的多個片段。\n修改 EinkBroApplication 再來，因為 TextToSpeech 的實作是系統提供的，所以整個 App 共用一個實體就好，不需要在每個 Activity 中都生成一個。\n因此我們在 EinkBroApplication.kt 中建立起一個 TtsManager 實體，並且定義好當 onTerminate() 被呼叫時，要把它回收掉。不然，這個元件會在系統背後持續播放，直到你重開手機或是等到它把內容全部讀完。\n主要的使用邏輯 EinkBro 大部分的操作邏輯都在 BrowserActivity 中，這次也不例外。雖然這寫法很糟糕，不過，如果要重構成一堆 ViewModel，也是個大工程。這是題外話了，讓我們回到主題：\n之前實作翻譯功能時，已經有個函式 NinjaWebView.getRawText() 能夠取得當前網頁中的純文字內容。在 1777 行，我們將這個函式拿到的內容餵入 TtsManager.readText() 。因為 getRawText() 是非同步的，所以要用 lifecycleScope 包裝起來。\n1852 行的 toggleTtsRead() 則是藉由 TtsManager.isSpeaking() 判斷是不是正在語音閱讀中：如果是的話，這時的動作應該是要停止播放。\n語音設定 Android 語音閱讀有一些調整的彈性，包括更換不同語音引擎 (不同語言有可能存在更專業的語音引擎，可以透過 Google Play Store 下載)，切換發音的語言，調整發音的速度和發音的語調。有些引擎還支援同個語言裡有多個不同特性的語音。\n有鑑於大部分的調整都能在系統的設定中做改變，針對這部分我只實作了兩件是：\n調整語音閱讀的語速 將使用者快速帶到系統的語音閱讀設定中 前者很單純，只要使用者設定好想要的語速後，透過 TtsManager.setSpeechRate 就可以了。\n後者則是建立一個 Intent 來達成。\n後話 目前的實作還很陽春，但對於單篇網頁的閱讀來說，已經綽綽有餘。以後有機會的話，想要擴充這功能，讓它可以在閱讀文章時，加入其他文章的內容到 queueu 中。這樣子就可以一直邊看邊聽了。\n相關連結 Comparing v9.13.0\u0026hellip;v9.14.0 · plateaukao/einkbro\n","permalink":"https://plateaukao.github.io/blog/posts/android-%E4%B8%8A%E6%95%B4%E5%90%88-text-to-speech-%E5%8A%9F%E8%83%BD/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/d5f50eb920ac/1_6kubw9JkK-gY-FQNL7fWzQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003eEinkBro 做為一個瀏覽器，很多開發的功能都是圍繞著內容呈現的調整。今天，要來介紹 9.14.0 新加入的特別功能：語音閱讀網頁內文。\u003c/p\u003e\n\u003cp\u003e在 Android 上，Text to Speech 功能早在 API level 4 就開始支援，不過，好像很少有瀏覽器有支援這功能。畢竟，大部分人使用瀏覽器都是想要用眼睛去看內容，而網頁的篇幅通常也不會太長。\u003c/p\u003e","title":"Android 上整合 Text to Speech 功能"},{"content":"這也是老毛病了。三不五時會有使用者反應，雖然在 Google Play Store 中可以找到 EinkBro，但想要下載時，畫面上卻顯示 “您的裝置與這個版本不相容”。由於之前我也沒認真去研究為什麼會發生，所以通常解決方式都是：請使用者上 Github 網站，直接下載 apk 來安裝或是去 f-droid 上安裝。\n這幾天，想到了一個可能性是：前不久為了支援在某些語言教學網站上，需要錄音的需求，在 AndroidManifest.xml 中加了相關的需求：\n\u0026lt;uses-feature android:name=\u0026#34;android.hardware.microphone\u0026#34; /\u0026gt; \u0026lt;uses-permission android:name=\u0026#34;android.permission.AUDIO_CAPTURE\u0026#34; /\u0026gt; 會不會就是因為這些需要 uses-feature 的設定造成部分的閱讀器被 Play Store 過濾掉呢？\n有了這個懷疑後，自然是先去看一下 uses-feature 的官方說明。不看還好，一看才發現真的是那麼一回事。\n| Android Developers\n官網說得很清楚：\nGoogle Play 會使用應用程式資訊清單中宣告的 \u0026lt;uses-feature\u0026gt; 元素，篩選不符合應用程式硬體和軟體功能要求的裝置。\n您可以透過指定應用程式所需的功能，讓 Google Play 僅對裝置符合應用程式功能需求的使用者顯示應用程式，而不要向所有使用者顯示。\n當時在修改 AndroidManfest.xml 時不以為意，沒想到因此而排除掉了一些電子書閱讀器裝置。很多 6 吋或 7 吋的設備，為了節省成本，將觸角伸到更多普羅大眾，機器本身很常不帶麥克風，甚至是喇叭也不見得閱讀器是標配。\n解決方式 既然找到了原因，那要解決這問題就容易多了。加上 uses-feature 會讓 Google Play 去過濾掉裝置的話，那自然會有一些參數可以讓 Google Play 能忽略掉這些設定。其方式如下，在該 uses-feature 描述中，加上 android:required=”false” 的標識。\n\u0026lt;uses-feature android:name=\u0026#34;android.hardware.microphone\u0026#34; android:required=\u0026#34;false\u0026#34; /\u0026gt; 這樣就結束了嗎？還沒還沒。除了麥克風之外，會不會更早之前就有漏網之魚呢？再仔細研究一下 uses-feature 的說明後發現：原來有些 permission 的宣告，Android 是會幫你偷偷加上 uses-feature 的屬性的，也因此，會雞婆地幫你濾掉那些不符合的硬體設備。究竟有哪些 permission 會這麼幫倒忙呢？官網列了下面這張大表：\n在表格中可以發現，針對位置資訊的 permission ACCESS_FINE_LOCATION 和 ACCESS_CORSE_LOCATION，它們都會隱含了對於 android.hardware.location 的硬體需求。\n在 EinkBro App 中，有些網站會需要裝置的位置訊息來提供在地化的資訊，像是氣象網站之類的。這些應用場景 EinkBro 會跳出詢問使用者是否同意該網站要求的對話框。\n對於沒有位置資訊的設備，頂多就讓它在這些網站抓不到位置資訊就好，沒必要完全禁止它們安裝 EinkBro 才對。所以，為些，我們也要額外再加上下面的宣告，讓 Google Play 不要雞婆濾掉這些裝置。\n\u0026lt;uses-feature android:name=\u0026#34;android.hardware.location\u0026#34; android:required=\u0026#34;false\u0026#34; /\u0026gt; 結語 新版 EinkBro 在 Google Play 上發布後，果然，我的文石 Book Poke 4 s 就可以正常地從 Google Play 上安裝 EinkBro 了。\n相關連結 | Android Developers\n修正此問題的 commit ","permalink":"https://plateaukao.github.io/blog/posts/google-play-store-%E6%82%A8%E7%9A%84%E8%A3%9D%E7%BD%AE%E8%88%87%E9%80%99%E5%80%8B%E7%89%88%E6%9C%AC%E4%B8%8D%E7%9B%B8%E5%AE%B9/","summary":"\u003cp\u003e這也是老毛病了。三不五時會有使用者反應，雖然在 Google Play Store 中可以找到 EinkBro，但想要下載時，畫面上卻顯示 “您的裝置與這個版本不相容”。由於之前我也沒認真去研究為什麼會發生，所以通常解決方式都是：請使用者上 Github 網站，直接下載 apk 來安裝或是去 f-droid 上安裝。\u003c/p\u003e","title":"Google Play Store: 您的裝置與這個版本不相容"},{"content":"\n最近下載 EinkBro 的使用者多了一點。早期不太去理它的臭蟲，也因此發生得愈來愈頻繁。今天終於花了點時間找出原因，並且解決了它。\n從 App 版本和 Android OS 版本看來，這是個非特定設備下就會觸發到的問題。之前一直沒理它的原因是，雖然 Google Play Console 上有 Crash 的 callstack (如下圖)，但是有寫跟沒寫差不多，因為沒有真的寫出行數，只能在 BrowserActivity::onCreate() 裡自己找線索。\n從這一大串訊息中，可以大概看出：在進到 onCreate() 後，會進到 Android 系統的 FragmentManager，它想要執行 restoreSaveState()，藉此呼叫 instantiate() 函式，然後就壞了。\n在經過一番搜尋後，最終的原因是因為：雖然 BrowserActivity 中我沒有建立什麼特別的 Fragment 來顯示畫面，但是很多對話框我是利用 DialogFragment 來完成的。而這些客製的 DialogFragment 都在建構函式中必須要代入一些參數或功能函式。\n在官方文件中指出：如果想要繼承 Fragment 的話，應該要保留空的建構函式，然後把需要的參數利用 Bundle 一一塞入 Fragment 中。這樣子才可以確保在某些情況下 Fragment 被重新建立時，這些參數能夠再從 Bundle 中取回。\n目前因為已經實作了好多個 DialogFragment，我懶得改動這一部分。另一個可行的替代解法是：當 Android 系統想要回復 BrowserActivity 時，它會傳入 savedInstanceState 這個 Bundle。正常操作的方式是，在 onCreate() 的第一行執行：\nsuper.onCreate(savedInstanceState) 而這一行正是造成 crash 的原因。savedInstanceState 的內容會指示 Android 系統幫忙把之前的 Fragment 再嘗試著建立出來。所以，要讓它不 crash 的話，就是假裝沒這回事，直接塞 null 給它。\n這麼一來，就能避掉 crash 的產生囉。\n後話 或許有人會問，那之前的狀態怎麼辦？\n就，隨它去囉。對於 Dialog 性質的 Fragment，通常在離開 Activity 後再回來 Activity，把這些 Dialog 自動關掉也是比較合理。\n等過一兩週後，再來這裡更新一下新的 crash rate。\n相關連結 修正臭蟲的 commit 網路上找到的參考資料 ","permalink":"https://plateaukao.github.io/blog/posts/%E8%A7%A3%E4%BA%86%E4%B8%80%E5%80%8B%E8%90%AC%E5%B9%B4-einkbro-%E8%87%AD%E8%9F%B2savedinstancestate-%E7%9A%84%E9%9B%B7/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1b04a685008e/1_hvjSHf7-P99X9xE5OS2JZA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1b04a685008e/1_mT0JMjHvGeKA2PVein15JA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e最近下載 EinkBro 的使用者多了一點。早期不太去理它的臭蟲，也因此發生得愈來愈頻繁。今天終於花了點時間找出原因，並且解決了它。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1b04a685008e/1_UFzWODVkbdwuhgYrfFEb8Q.png\"\u003e\u003c/p\u003e\n\u003cp\u003e從 App 版本和 Android OS 版本看來，這是個非特定設備下就會觸發到的問題。之前一直沒理它的原因是，雖然 Google Play Console 上有 Crash 的 callstack (如下圖)，但是有寫跟沒寫差不多，因為沒有真的寫出行數，只能在 \u003ccode\u003eBrowserActivity::onCreate()\u003c/code\u003e 裡自己找線索。\u003c/p\u003e","title":"解了一個萬年 EinkBro 臭蟲：savedInstanceState 的雷"},{"content":"A long opened issue for EinkBro states that after creating new background tab, it’s not possible to continue certain behaviors normally (e.g., modify query string, or load more content, etc in Google, and Twitter website).\nIn the beginning, I couldn’t reproduce the issues on my several devices with various Android OS versions. My first thought is that: it’s caused by different WebView versions and some of them are having issues.\nAfter the user provided more information about his EinkBro app settings, I noticed that the root cause may be related to the handling logic of activation/de-activation of web tabs. And it brings us to today’s topic: the difference between onPause() and pauseTimers() .\nHere’s the official document explanation of these two WebViewfunctions:\nAccording to the documentation, it’s very clear thatonPause() is with scope of individual WebView instance, while pauseTimers() is used to control all WebViews globally.\nIn original EinkBro implementation, when a tab is moved from foreground to background, pauseWebView() will be called; it includes calling both WebView::onPause() and WebView::pauseTimers() . This is where issue happened: WebView::pauseTimers() also stops javascript execution globally, including the tab in the foreground (it could be Google Search page or Twitter service, etc).\nSolution After figuring out the root cause, the issue is fixed with no big hazards:\nRemove pauseTimers() from pauseWebView() since pauseWebView()function is for per WebView manipulation; and add pauseTimers() to Activity level onPause() lifecycle, so that all WebViews will stop running when EinkBro app is not in the foreground anymore to save battery consumption.\nReferences fix: #171 when opening new tab in background, it will cause current w\u0026hellip; ·…\n","permalink":"https://plateaukao.github.io/blog/posts/differences-between-onpause-and-pausetimers-for-android-webview/","summary":"\u003cp\u003e\u003ca href=\"https://github.com/plateaukao/einkbro/issues/171\"\u003eA long opened issue for EinkBro\u003c/a\u003e states that after creating new background tab, it’s not possible to continue certain behaviors normally (e.g., modify query string, or load more content, etc in Google, and Twitter website).\u003c/p\u003e\n\u003cp\u003eIn the beginning, I couldn’t reproduce the issues on my several devices with various Android OS versions. My first thought is that: it’s caused by different WebView versions and some of them are having issues.\u003c/p\u003e","title":"Differences between onPause() and pauseTimers()for Android WebView"},{"content":"追新的版本，免不了常常會遇到原本能編譯的程式碼，突然又動不了。不論是 Android 環境或是 iOS，不論熟不熟悉，遇到問題時還是要上網找別人的經驗分享。\n這次是 xcode 的問題，而且這問題我竟然遇到了兩次：一次是想要在 iPhone 上編譯一個 open source 的 browser app ；沒辦法，用其他的 browser 就是會覺得一直在被監視中。另一次是為了要把舊的手機中的一些檔案傳到新買的 iPhone 中，想要把之前魔改過的 Sharik (內網檔案互傳的跨平台 App) 裝進去，在編譯時卡住了。\nFlutter Browser App GitHub - pichillilorenzo/flutter_browser_app: A Full-Featured Mobile Browser App (such as the…\nSharik GitHub - plateaukao/sharik: Sharik is an open-source, cross-platform solution for sharing files via…\n錯誤訊息大致如下，關鍵字是它在抱怨看不懂 @available\n看來應該遇到的人還不少吧，比較容易的解決方式一下子就找到了。只要去修改一下 Podfile，指定平台是 iOS 14，跟把 Swift 版本設定為 5.0 就可以了。\nplatform :ios, \u0026#39;14.0\u0026#39; ... ... post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings.delete \u0026#39;IPHONEOS_DEPLOYMENT_TARGET\u0026#39; config.build_settings[\u0026#39;SWIFT_VERSION\u0026#39;] = \u0026#39;5.0\u0026#39; config.build_settings[\u0026#39;ENABLE_BITCODE\u0026#39;] = \u0026#39;NO\u0026#39; config.build_settings[\u0026#39;GCC_PREPROCESSOR_DEFINITIONS\u0026#39;] ||= [ \u0026#39;$(inherited)\u0026#39;, \u0026#39;AUDIO_SESSION_MICROPHONE=0\u0026#39;, \u0026#39;DISABLE_PUSH_NOTIFICATIONS=1\u0026#39; ] end end end 結語 能在 iPhone 上跑自己寫的程式，感覺真不錯。目前 Sharik 和 Flutter Browser 都可以正常執行了。接下來就要看要不要在 Flutter Browser 中，把一些 EinkBro 的功能移植過來，先頂一陣子。\n相關連結 簡書上的文章：https://www.jianshu.com/p/5d7af7b9eb71\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8D%87%E7%B4%9A%E5%88%B0-xcode-140-%E5%BE%8C-swift-compiler-%E7%9A%84%E7%B7%A8%E8%AD%AF%E9%8C%AF%E8%AA%A4/","summary":"\u003cp\u003e追新的版本，免不了常常會遇到原本能編譯的程式碼，突然又動不了。不論是 Android 環境或是 iOS，不論熟不熟悉，遇到問題時還是要上網找別人的經驗分享。\u003c/p\u003e\n\u003cp\u003e這次是 xcode 的問題，而且這問題我竟然遇到了兩次：一次是想要在 iPhone 上編譯一個 open source 的 browser app ；沒辦法，用其他的 browser 就是會覺得一直在被監視中。另一次是為了要把舊的手機中的一些檔案傳到新買的 iPhone 中，想要把之前魔改過的 Sharik (內網檔案互傳的跨平台 App) 裝進去，在編譯時卡住了。\u003c/p\u003e","title":"升級到 xcode 14.0 後 Swift Compiler 的編譯錯誤"},{"content":"這是人生第二次購買 iOS 系列的手機。上一支是十三年前 Apple 推出的第二支手機 3GS 了。經過這十幾年的演進，手機的機身除了愈來愈大，愈來愈重之外，功能面也不斷突飛猛進。\n雖然在八九年前，前一份工作時有幸使用到公司發的 iPhone 手機；但這近七年多來，完全沒有再接觸過 iPhone，倒是電腦由早期的 MacBook Pro 2011，一路升級到 iMac (intel CPU)，再到現在的 Mac Studio，對於 Apple 桌面環境早已駕輕就熟。\n這次之所以想要再跳槽到 iPhone 陣營，是因為最近開始拍攝影片。照片一直是我用來記錄生活的方式，過去也確實拍了不少自己還蠻喜歡的作品。不過，隨著技術的進步，和時代潮流的轉變，愈來愈多人利用影片在表達自己和記錄生活。為了讓自己的生活更加生動，和抱持著學習的態度，想要讓自己更加精進拍攝影片的能力，就下定決心換到 iPhone 的世界來。\n另一個原因是，儘管 Android 陣營的廠商在高階手機上堆料不手軟，技術方面也都跟 Apple 不相上下(有嗎？)，但是大部分的設計師或是科技大公司的員工，使用 iPhone 的比例還是佔大多數。在這種環境下，可以預期的是 iPhone 的設計應該是更經過挑戰的。要是它達不到大多數人的要求的話，應該早就很多人轉到 Android 平台上吧。\n現在 iPhone 的盒子，因為都不附充電頭了，所以盒身可以做得很薄。仔細摸的話，正面的 iPhone 圖案是有浮刻的，手指摸過去可以感覺到它微微地突起，很有質感。我買的顏色是金色的，因為沒有特別喜歡那個新的紫色，而黑色白色又覺得太一般了，所以庸俗地選了金色的。\n撕掉膜後，摸摸背面，是不會沾指紋的材質，很好。不過，畢竟是新手機，怕會手滑摔到或刮到，還是買了保護殼很快地就裝了上去。結果，保護殼超沾指紋的…殘念。\n剛上手的感覺，很沉。雖然跟我原本的主力機 OPPO Find N 的 275 克比起來，iPhone 14 Pro 輕了 50 克左右，不過，iPhone 14 Pro 比較薄，而且只有 6.1 吋，心理會覺得它應該要再輕一些的。\n講完外觀，再來聊聊軟體的部分。自從開發了 EinkBro 後，Facebook, Twitter, Instagram 還有很多新聞網站，我都很習慣會用瀏覽器開啟；所以切換到 iPhone 上時沒有什麼問題，用 Safari 一樣可以瀏覽。\nLINE LINE 的話，由於現在還沒有辦法跨平台移轉兩週前的歷史訊息，這部分只能放水流。需要記下來的，應該都有送進 Note 或是相簿裡了。因為現在 LINE 會幫忙回復這兩週的訊息內容，所以至少這幾天內還在進行中的對話之類的，能夠很方便的進行下去。\nGoogle 服務 在 Android 平台，用最多，最依賴的應該是 Google 的各個 App。通常在 iOS 上也都有對應的 App 可以安裝使用。比較不習慣的是 Google Photos，因為很多照片會是從別台手機上傳到雲端的，所以如果我臨時想要從 Google Photos 中分享到其他 App 中使用的話，必須要先下載該張照片，再進到其他的 App 中，去從系統的相片挑選器中選擇。\n在 Android 的話，比較單純，可以直接從 Google Photos 就選擇分享，然後就跳到別的 App 中做後續的處理。\n無蝦米輸入法 在 Android 手機上，我是使用自己魔改過的 Lime HD 輸入法，再放入無蝦米的字集；在 iPhone 上，有蝦米鍵盤 2 可以用，但…它的 UI 我不是很喜歡。所以雖然可以付費得到很多的調整功能，我還是棄用了。後來找到另一個也支援無蝦米輸入的 Kaiboard。它可以在輸入中文時，不切換中/英文的情況下，直接輸入英文字。這點很方便，因為現在哪個人在打訊息時不是中文夾雜的呢。再加上，它的介面也比蝦米鍵盤 2 好看一些，每天都要使用到的工具，外觀很重要。\n電量 很有感的不好。如果是有電量焦慮的人，用 iPhone14 Pro 應該會很痛苦。這兩三天在使用時，我有把 GPS 刻意關掉，但是有先留著 Always On Display，想說先體驗看看是不是適合自己的使用習慣，和自己觀察看看它的額外耗電比例。\n目前看下來，即使是在上班日，把它放在一旁當大時鐘來看，在幾乎沒有什麼操作它的情況下，還是會看到它電力不斷地下降。如果是在外面活動，而且常會拿手機來拍照或使用的話，很難不也帶著行動電源同行。\n喇叭 好，比我用過的 Android 手機都好。該有的低音有出來，而且不是很混濁的低音。是那種會讓人想用它來聽音樂的音質。\n然後…我才想到，它沒有耳機孔，也不行接 USB Type-C 的耳機，而我好像沒有什麼可以帶出門的藍牙耳機啊。\n相機 還沒有機會出門測試。等有拍了照，錄了影，再來補充這部分的使用心得。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A4%9A%E5%B9%B4-android-%E4%BD%BF%E7%94%A8%E8%80%85%E8%B3%BC%E5%85%A5-iphone14-pro-%E7%9A%84%E7%B0%A1%E5%96%AE%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e這是人生第二次購買 iOS 系列的手機。上一支是十三年前 Apple 推出的第二支手機 3GS 了。經過這十幾年的演進，手機的機身除了愈來愈大，愈來愈重之外，功能面也不斷突飛猛進。\u003c/p\u003e","title":"多年 Android 使用者購入 iPhone14 Pro 的簡單心得"},{"content":"\n南港展覽館附近的環東大道出口下來後，在橋下有比較隱密的停車位，平常比較少車子會停那兒，所以如果要從南港起跑的話，是個不錯的停車地點。路邊停好車後，走個五分鐘就可以走樓梯翻過高高的河堤牆，來到河濱跑道，中間還能看到很特別的無臉男大樓。\n下午，可以況著高牆跑，比較不會曬到太陽。對岸的建築都不高，時不時可以看見更遠方內湖的小山丘。\n大約跑個五公里後，來到饒河街旁著名的彩虹橋。傍晚時會有很多人來這兒納涼吹風，或是攜老扶幼散散步；再晚一點就會有夜景和燈飾可以看。下午的話…只會讓人覺得很熱。\n再往前跑一兩公里，草地開始變大片，有很多槌球場，視野也相對寬廣。\n再來，會經過滑板或直排輪玩家很熟悉的六號水門橋下。不論晴天雨天，這邊總是有很多人有做各種運動和教學。\n最終，來到大直橋。這邊的人比剛剛那兒多了許多；騎腳踏車的，跑步的，散步的，溜狗的都有。\n過了大直橋後，再往下跑就是圓山了，所以可以看到圓山大飯店。不過，由於太陽要下山了，哩程數也來到了十公里，到這兒差不多可以跑回程了。\n回程路上的每座橋，都會開始亮燈，搭配上黃昏的倒影，很容易讓人不斷停下來拍照。\n再次回到彩虹橋，是不是跟下午的氣氛完全不一樣呢？這裡剛好可以拍到下一座橋和水面，所以只要角度抓得好，就可以拍出不錯的照片來。\n站遠一點拍，也有不同的感覺。\n拍完後，回程的五公里因為天黑了，就比較無聊一點。看河的對面是很漂亮，但是這岸因為有一邊是高高的堤坊，路比較小條，燈比較晚。不太建議晚上跑這一小段路線。\n結語 是條城中蠻親民的路線，如果半途累了，隨時轉出河濱，應該都能很快接上捷運或是其他交通方式。但南港到彩虹橋這段比較枯燥乏味。下次應該會考慮從彩虹橋之後出發，往前推進個 5 公里看看。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%B0%E5%8C%97%E5%8D%97%E6%B8%AF%E5%A4%A7%E7%9B%B4%E5%8D%97%E6%B8%AF%E6%B2%B3%E6%BF%B1%E8%87%B3%E5%A4%A7%E7%9B%B4%E6%B2%B3%E6%BF%B1%E4%BE%86%E5%9B%9E-20-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/bf0354bb12c8/1_llS5OJynN45YXNqmv-466g.png\"\u003e\u003c/p\u003e\n\u003cp\u003e南港展覽館附近的環東大道出口下來後，在橋下有比較隱密的停車位，平常比較少車子會停那兒，所以如果要從南港起跑的話，是個不錯的停車地點。路邊停好車後，走個五分鐘就可以走樓梯翻過高高的河堤牆，來到河濱跑道，中間還能看到很特別的無臉男大樓。\u003c/p\u003e","title":"[台北][南港][大直]南港河濱至大直河濱來回 20 公里"},{"content":" 池上 12 公里路線圖\n經過一陣練習後，10 到 20 公里的距離都可以輕鬆駕御時，就可以來試試進階版的池上路線：建議在天還沒亮，或是天剛亮的時候，從池上車站附近出發。\n池上市區\n池上市區\n繞個半圈的大坡池，感受一下山水合一的景色後。\n池上大坡池\n池上大坡池\n池上大坡池\n再沿著田邊小路一路跑到伯朗大道。伯朗大道前的路口，一大早完全沒有車沒有人，跟平日充滿遊客的景象完全是兩個樣子。\n伯朗大道稍做休息後，緩緩往 7 公里處跟天堂之路的交界口跑去。\n日出於伯朗大道\n伯朗大道\n伯朗大道\n在 7 公里處來到與天堂之路的交接口，轉進天堂之路，一直到天堂之路的拍照最佳地點。這裡適合停下來，好好地享受一下婉蜒的田道，兩旁的稻田和遠方若隱若現的山脈。\n天堂之路\n天堂之路的鞦韆\n天堂之路\n來到 9 公里處的池上大水車也很有特色，可以看著水車不斷地被推著旋轉。\n池上大水車\n最後的 10，11 公里，可以一邊慢跑，一邊看著右側的稻田，和遠方的山脈；太陽剛出現時，在逆光下，一切是那麼的夢幻。\n雙飛\n遠方的山脈\n最後來一張在田間休息的自拍照。\n田間自拍\n後記 如果無法跑太長的話，可以參考一下另一篇十公里的路程，只會跑伯朗大道周邊。\n[台東][池上]稻田裡跑一圈 10 公里\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%B0%E6%9D%B1%E6%B1%A0%E4%B8%8A%E5%A4%A7%E5%9D%A1%E6%B1%A0%E5%8A%A0%E4%BC%AF%E6%9C%97%E5%A4%A7%E9%81%93-12-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/8b179e8c741b/1_7f0FthuLzPtDt-g1VVKZFw.jpeg\"\u003e\n\u003cem\u003e池上 12 公里路線圖\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e經過一陣練習後，10 到 20 公里的距離都可以輕鬆駕御時，就可以來試試進階版的池上路線：建議在天還沒亮，或是天剛亮的時候，從池上車站附近出發。\u003c/p\u003e","title":"[台東][池上]大坡池加伯朗大道 12 公里"},{"content":"\n上硬漢嶺的路線有很多，今天要介紹的是景觀平台出發的硬漢嶺步道(勇腳級)。這個路線幾乎全是階梯，總長1500m 多，在石梯上每隔 100 公尺都會有標示，很容易知道自己離山頂還有多遠。停車也還算方便。雖然這裡車位不多，但早點來的話，路邊也是可以停車的。\n各種路線的說明\n路線公里數\n來回三公里的距離，因為階梯蠻陡的，可以乘以三，約十公里，當成是平常跑步需要的體力。這條路線幾乎都是在樹林裡，如果太陽大的話，可以起到遮陽的效果；到了山頂，又可以有無敵的展望視野，能遠眺台北市的市景。\n沿路都有樹蔭\n出發點的觀景平台\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%85%AB%E9%87%8C%E8%A7%80%E9%9F%B3%E5%B1%B1%E7%A1%AC%E6%BC%A2%E5%B6%BA%E6%AD%A5%E9%81%93/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/dde05064906/1_oIgwUOx3oOSdj9Zg2nJeag.png\"\u003e\u003c/p\u003e\n\u003cp\u003e上硬漢嶺的路線有很多，今天要介紹的是景觀平台出發的硬漢嶺步道(勇腳級)。這個路線幾乎全是階梯，總長1500m 多，在石梯上每隔 100 公尺都會有標示，很容易知道自己離山頂還有多遠。停車也還算方便。雖然這裡車位不多，但早點來的話，路邊也是可以停車的。\u003c/p\u003e","title":"[八里][觀音山]硬漢嶺步道"},{"content":"池上給人的印象就是大大片的稻田，和多到不行再多的遊客和腳踏車。如果想要在這邊真的感受一下那田野的寧靜，很建議在這邊住一晚，然後在隔天天微亮時，來一趟慢跑，細細地感受沒有遊客時的池上風光。\n圖中的路線是從大池豆皮店附近的民宿出發，一路跑到伯朗大道，再繞到天堂之路，最後再回到出發點。右上角的大坡池，如果是安排全長 20 公里的路線，應該就可以一起包含進去；但是，因為這是清晨跑的，還要留點體力給白天旅遊時用，只能忍痛只跑個 10 公里意思意思。(其實只是因為當時的腳力也還沒好到可以一口氣跑個 20 公里而不腳酸)\n由於這次來池上的季節是剛收割完稻子，整個田地光禿禿的，沒有稻穗可看。少了點滿足感，只能多望望遠方的山丘和雲彩。不時有在低處盤桓的雲，沿著山脈順勢而下，像極了水流。\n伯朗大道，輕輕鬆鬆一人獨享。\n天堂之路，少了好天氣，和稻田的加持，看起來就是條很普通的鄉間小路。\n小結 看不到大景，只能安慰自己，這樣子跑起來比較舒服，不用曬太陽。希望下次再來跑池上，可以跑得很遠，看得更多，然後多點太陽的眷顧。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%B0%E6%9D%B1%E6%B1%A0%E4%B8%8A%E7%A8%BB%E7%94%B0%E8%A3%A1%E8%B7%91%E4%B8%80%E5%9C%88-10-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e池上給人的印象就是大大片的稻田，和多到不行再多的遊客和腳踏車。如果想要在這邊真的感受一下那田野的寧靜，很建議在這邊住一晚，然後在隔天天微亮時，來一趟慢跑，細細地感受沒有遊客時的池上風光。\u003c/p\u003e","title":"[台東][池上]稻田裡跑一圈 10 公里"},{"content":"陽光運動公園是個新店人很愛去的地方：佔地大，有大大的直排輪場地，有一大區小朋友的遊樂設施，還有大片的草地可以野餐或是搭帳蓬休閒。交通也是四通八達，可以透過河濱步道往南通往碧潭，往北通往公館或是福和運動公園。\n本篇介紹的是從陽光運動公園沿著河濱西岸跑，到達碧潭，並且登上和美山再折返的路線。\n從陽光運動公園到碧潭只有短短的 3.5 公里，沿途不時有樹蔭可以幫忙遮陽，前方更是可以看到遠方的山脈，不會看到滿滿的城市建築。\n到達碧潭後，上橋後在橋的入口旁可以看到和美山登山入口。和美山海拔只有 153 公尺，但因為一旁就是碧潭，所以登高後，視野相當好，可以遠眺台北盆地。登山的路線多是階梯，走起來有點辛苦。不過因為高度不高，對於有在跑步的人來說，不會是什麼太大的問題。\n和美山\n和美山\n和美山登山有兩條路線，一條是一路登到山頂的路線；一條是比較沿著河邊繞的路要，偶爾可以下到碧潭溪邊。時間夠的話，建議可以左去右回，或是右去左回，把兩個路線都走過一遍。\n飽覽碧潭風景後，可以走過碧潭橋，改走東岸河濱一路跑回陽光運動公園。回程路線，太陽會是在左手邊，所以如果時間選得好，剛好可以伴著夕陽跑。在碧潭風景區入口處的河邊還可以拍到景觀極好的落日照。\n碧潭夕照\n一大清早的碧潭\n東岸運動的人比西岸多，道路也比較大條。運動時比較不會覺得孤單，但也要時時小心快速通過的自行車就是了。\n彩虹橋上拍攝\n小結 想拍出漂亮的夕陽照，千萬不要錯過這條路線。除了能跑步鍛鍊身體外，帶著滿滿的夕陽照片回家，會讓人更期待下次出門跑步的。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%B0%E5%8C%97%E7%A2%A7%E6%BD%AD%E5%BE%9E%E9%99%BD%E5%85%89%E9%81%8B%E5%8B%95%E5%85%AC%E5%9C%92%E5%88%B0%E7%A2%A7%E6%BD%AD%E5%92%8C%E7%BE%8E%E5%B1%B1%E4%BE%86%E5%9B%9E-12-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e陽光運動公園是個新店人很愛去的地方：佔地大，有大大的直排輪場地，有一大區小朋友的遊樂設施，還有大片的草地可以野餐或是搭帳蓬休閒。交通也是四通八達，可以透過河濱步道往南通往碧潭，往北通往公館或是福和運動公園。\u003c/p\u003e","title":"[台北][碧潭]從陽光運動公園到碧潭和美山。來回 12 公里"},{"content":"永和的福和橋下有很大的停車場和休閒的空間，很適合來這邊玩滑板或是直排輪。當做跑步的地點也是很合適的。從這裡開始往西往北跑，沿途會經過綠寶石公園的極限運動體驗場、恐龍園區公園、光復賞鳥綠地，蝴蝶公園地景花海和華江碼頭；經過的橋有：中正橋、華中橋、光復大橋、華翠大橋、萬板大橋、華江陸橋和新北大橋。每一座橋都有不同的造型。如果是晚上來跑的話，還能看到不一樣的燈飾。\n跟一般河濱比較不同的是，它在河堤上還有做散步道。如果不想在河邊的路線跑步，可以上河堤跑。河堤上的視野更好，而且也比較不會有腳踏車出現。\n從綠寶石公園後，就會再回到河濱路線上。這裡的道路都舖得很好，不論是騎車，散步或是跑步，都很合適。\n下面就是不同陸橋的集錦：\n萬板大橋\n萬板大橋\n光復橋\n華江陸橋\n新北大橋橋下\n這裡也是很適合在夕陽時對著河面拍照。\n小結 想要居高臨下的跑步，看大片大片的雲彩，或是回程時想要一睹橋墩夜景的人，可以試試這個路線。每次跑都會有不一樣的感受。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%B0%E5%8C%97%E6%B0%B8%E5%92%8C%E6%9D%BF%E6%A9%8B%E5%BE%9E%E7%A6%8F%E5%92%8C%E9%81%8B%E5%8B%95%E5%85%AC%E5%9C%92%E5%88%B0%E8%8F%AF%E6%B1%9F%E4%BA%BA%E5%B7%A5%E6%BF%95%E5%9C%B0-10-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e永和的福和橋下有很大的停車場和休閒的空間，很適合來這邊玩滑板或是直排輪。當做跑步的地點也是很合適的。從這裡開始往西往北跑，沿途會經過綠寶石公園的極限運動體驗場、恐龍園區公園、光復賞鳥綠地，蝴蝶公園地景花海和華江碼頭；經過的橋有：中正橋、華中橋、光復大橋、華翠大橋、萬板大橋、華江陸橋和新北大橋。每一座橋都有不同的造型。如果是晚上來跑的話，還能看到不一樣的燈飾。\u003c/p\u003e","title":"[台北][永和][板橋]從福和運動公園到華江人工濕地 10 公里"},{"content":"\n華江堤外區停車場旁是華江直排輪溜冰場，還蠻好找的，當做跑步出發點的話，回程不怕跑過頭。從這裡起跑，先映入眼簾的是華江碼頭，傍晚或晚間這裡有昔日咖啡的攤位，能坐在溪邊吹風看夜景。\n往北跑一小段後，開始進入江子翠超大片的草地。從這裡開始，一直到終點的板橋環保公園，沿路有好幾個人工濕地(華江人工濕地、新海人工濕地、浮洲人工濕地)可以繞進去。在濕地裡跑步的好處是，因為散步的人不多，地面都長滿了草皮，跑起來就像是天然的 PU 跑道一樣，有避震的效果，兩旁的野草也都蠻高的，時不時能幫忙遮陽。少了腳踏車，跑起來也安心許多。\n河濱公園會經過新北彩繪廊，如果是跑河濱路線的話，就能邊跑邊欣賞。\n中間還會經過荷花公園，季節對的話，還能看到比台北植物園還要大片的荷花池。\n終點的板橋環保公園視野很開闊，能看落日，或是遠方綿延的山脈。\n小結 這條路線很適合在傍晚的時候來跑步，因為大部分時間可以看到西邊，邊跑能邊欣賞夕陽的景色。如果平常習慣跑河濱，想要在熟悉的環境中找找不一樣的感覺，可以來試試這邊的濕地路線，嘗嘗平地越野的感覺，幻想自己是跑在山林中。一旦膩了，隨時轉個彎出來，又是熟悉的河濱路線。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%B0%E5%8C%97%E6%9D%BF%E6%A9%8B%E6%B2%B3%E6%BF%B1%E5%BE%9E%E6%96%B0%E5%BA%97%E6%BA%AA%E8%8F%AF%E6%B1%9F%E5%A0%A4%E5%A4%96%E5%8D%80%E5%81%9C%E8%BB%8A%E5%A0%B4%E8%87%B3%E6%9D%BF%E6%A9%8B%E7%92%B0%E4%BF%9D%E5%85%AC%E5%9C%92-10-%E5%85%AC%E9%87%8C/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/ceafd0a1a569/1_jM-bbEjAkWEquHdB-6ChvA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e華江堤外區停車場旁是華江直排輪溜冰場，還蠻好找的，當做跑步出發點的話，回程不怕跑過頭。從這裡起跑，先映入眼簾的是華江碼頭，傍晚或晚間這裡有昔日咖啡的攤位，能坐在溪邊吹風看夜景。\u003c/p\u003e","title":"[台北][板橋]河濱：從新店溪華江堤外區停車場至板橋環保公園 10 公里"},{"content":"前一陣子看到新版 Firefox 102 版開始，可以讓使用者決定是不是要把網址中的一些 query parameters 過濾掉，常見的像是 facebook 的 id 追蹤 fbclid，還有 Google 的 ga_*, gclid 等；還有在追蹤活動時很常用的 utm_* 系列。如果這些足跡能在瀏覽網頁時去除掉，對於自己的隱私可以多一層保障。\n所以，我也打算在 EinkBro 中來實作一下。這個功能主要可以分成兩部份：第一部份是取得哪些是應該要過濾掉的 query parameters，將它們用適當的方式儲存下來；第二部份才是透過跟網址比對，決定哪些參數需要清除，哪些是正常的，需要保留下來。\n一開始，並沒有找到 Firefox 的實作方式，所以先在 Github 上找了其他 Open Source 的 repository 來參考。第一步是先將下面這個 Neat-URL 的資料納為己用：\nGitHub - Smile4ever/Neat-URL: Neat URL cleans URLs, removing parameters such as Google Analytics\u0026rsquo;…\n它的 Readme 中已經有列了很多常見的參數；有些是可以套在所有網站上的，有些是特定網站才會冒出來的參數。我先將這份 json 檔案存到了程式中，並且在 EinkBro 一開始執行的時候，就把下面的 params 存到了一個 List 中。每次要載入 url 時，都會去跟這些 params 做比較，如果有符合格式的話，就將其排除。\n{ \u0026#34;categories\u0026#34;: [ ... { \u0026#34;name\u0026#34;: \u0026#34;Amazon\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;_encoding@amazon.*\u0026#34;, \u0026#34;ascsubtag@amazon.*\u0026#34;, \u0026#34;pd_rd_*@amazon.*\u0026#34;, \u0026#34;pf@amazon.*\u0026#34;, \u0026#34;pf_rd_*@amazon.*\u0026#34;, \u0026#34;psc@amazon.*\u0026#34;, \u0026#34;ref_@amazon.*\u0026#34;, \u0026#34;tag@amazon.*\u0026#34;]}, { \u0026#34;name\u0026#34;: \u0026#34;Bilibili.com\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;callback@bilibili.com\u0026#34;]}, ... { \u0026#34;name\u0026#34;: \u0026#34;Campaign tracking (Google Analytics, ga)\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;ga_*\u0026#34;, \u0026#34;gclid\u0026#34;, \u0026#34;gclsrc\u0026#34;]}, ... { \u0026#34;name\u0026#34;: \u0026#34;Facebook\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;fb_action_ids\u0026#34;, \u0026#34;fb_action_types\u0026#34;, \u0026#34;fb_ref\u0026#34;, \u0026#34;fb_source\u0026#34;, \u0026#34;fbclid\u0026#34;, \u0026#34;hrc@facebook.com\u0026#34;, \u0026#34;refsrc@facebook.com\u0026#34;]}, { \u0026#34;name\u0026#34;: \u0026#34;Google\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;ei@google.*\u0026#34;, \u0026#34;gs_gbg@google.*\u0026#34;, \u0026#34;gs_l\u0026#34;, \u0026#34;gs_lcp@google.*\u0026#34;, \u0026#34;gs_mss@google.*\u0026#34;, \u0026#34;gs_rn@google.*\u0026#34;, \u0026#34;gws_rd@google.*\u0026#34;, \u0026#34;sei@google.*\u0026#34;, \u0026#34;ved@google.*\u0026#34;]}, { \u0026#34;name\u0026#34;: \u0026#34;Medium\u0026#34;, \u0026#34;params\u0026#34;: [\u0026#34;_branch_match_id@medium.com\u0026#34;, \u0026#34;source@medium.com\u0026#34;]}, ... ] } 目前判斷的邏輯還沒全部完成，但至少通用型的參數，目前都可以過濾掉了。實做方式大致如下：\nmatchNeatUrlConfig\n有了個別的參數比對函式後，再來是將 url string 分解成 Uri ，將裡頭的參數一個個拿出來做比較：\nstripUrlQuery\nFirefox 的實作 在套用 Neat Url 的作法時，我還是繼續想要找出 Firefox 的實作方式。原先以為它會是用 Android 的 kotlin 撰寫的，結果發現我的尋找方向錯誤：這功能實際上是用 C++ 開發的。\ngecko-dev/URLQueryStringStripper.cpp at 90c03c4ae7ce45aed5dac97b6d65ec769305273f ·…\nBrave 的實作 另外，也意外地找到了 Brave Browser 中的實作方式。但是，Brave Browser 的實作方式比較陽春就是了。\nbrave-core/brave_site_hacks_network_delegate_helper.cc at master · brave/brave-core\n相關連結 目前實作已經都完成，主要的 commit 有下面三條\nfeat: support url query parameter stripping. basic function.\nfeat: filter query parameters by host too.\nui: add filter query parameter option in UI Behavior settings.\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%BF%BE%E6%8E%89%E7%B6%B2%E5%9D%80%E4%B8%AD%E7%9A%84%E8%BF%BD%E8%B9%A4%E7%A2%BC/","summary":"\u003cp\u003e前一陣子看到新版 Firefox 102 版開始，可以讓使用者決定是不是要把網址中的一些 query parameters 過濾掉，常見的像是 facebook 的 id 追蹤 fbclid，還有 Google 的 ga_*, gclid 等；還有在追蹤活動時很常用的 utm_* 系列。如果這些足跡能在瀏覽網頁時去除掉，對於自己的隱私可以多一層保障。\u003c/p\u003e","title":"濾掉網址中的追蹤碼"},{"content":" https://themovieculture.com/our-beloved-summer-k-drama-review-and-summary/\n重新看一下這部韓劇，也順便來看看裡面的台詞吧。\n第一集 나는 네가 지난 여름에 한 일을 알고 있다 (웅) 보다시피, 어 ，촬영당하고 있어요\n~다시피: 如你所(見，聽，知，想，等等等)\n(연수) 어디 가? 你要去哪？\n(웅) 남이사 다 너한테 보고해야 되냐?\n別人的事 都 要跟妳報告嗎？\n(연수) PD님 저 잠깐만 끊어 주세요\n製作人，能不能暫停一下？\n(연수) 수업에 집중해 집중력이 그거밖에 안 되냐?\n專心上課，你的專注力就只有這樣嗎？\n(웅) 내가 옆에서 꼼지락거리든 말든\n妳別管我坐不坐得住\n너나 수업에 집중해 집중력이 그거밖에 안 되냐?\n妳專心上課就好啦 妳專注力這麼差嗎？\n(연수) 하긴, 뭐 넌 더 떨어질 성적이 없어서\n也是啦(確實)，你的成績已經爛到谷底了(成績無法再跌得更低了)\n第二集 넌 뭔데 5년이 지났는데 아직도 그렇게 화가 나 있는데?\n넌 뭔데 5년 만에 그렇게 쿨해진 건데?\n你到底憑什麼過了五年還這麼生氣?\n那你又憑什麼過了五年就變得這麼灑脫?\n第五集 這邊有個小高潮，努亞插畫家也出現在宴會會場，告知雄他也有被邀請參加現場作畫。雄有被欺騙的感覺，在問過國延秀後，憤而離場。這時，國延秀對張組長說了一段話：\n장도율 팀장님\n→ 네, 국연수 씨\n“방금 그게 무슨 말이죠? 이번 프로젝트에 누아 작가가 같이 한다는 말”\n剛才那是什麼意思？努亞插畫家也會參與這次的企劃？\n말 그대로죠\n就是字面上的意思\n제가 분명히 표절 의혹 아니라는 자료도 보내 드리지 않았나요?\n我不是把證明高午沒有抄襲的資料 都寄給你了嗎？\n(연수) 근데도 그런… — (도율) 잠깐만요 -你怎麼還… -稍等一下\n(통역사) [프랑스어] 실례합니다\n不好意思\n(도율) [한국어] 지금 저희한테 가장 필요한 게 뭐죠?\n妳知道我們現在最需要的是什麼嗎？\n이슈\n話題\n지금 이 시점에 가장 확실한 방법인 거 같…\n我認為這是眼下效果最好的…\n그렇지만 이건 명백히 계약 위반이고 아티스트를 존중하지 않는 무례한…\n但這個做法很明顯已經違約 而且是不尊重藝術家的失禮…\n(도율) 글쎄요\n是嗎？\n계약서는 이미 제가 한번 확인해 봤는데 큰 문제는 없는 거 같던데\n但我已經檢視過合約 看起來沒什麼大問題啊\n‘무례하다’라…\n至於失禮…\n씁, 유난히 감정적이시네요?\n妳似乎過於感情用事了\n어쩌면 두 작가분들께 좋은 기회가 될 수도 있을 거 같은데\n我認為這對兩位插畫家而言 也許都是很好的機會啊\n뭐가 문제인 거죠, 국연수 씨?\n到底哪裡有問題？國延秀小姐\n실망이네요\n妳讓我有點失望了\n(도율) 그래도 국연수 씨는\n我本來以為妳跟我會是同一種人\n나랑 같은 사람일 거라고 생각했는데 이렇게 공과 사를 구분을 못 하는 사람이었나?\n原來妳是這麼公私不分的人嗎？\n아무튼 오늘 일은 다시 한번 회의를 해 보죠\n我想今天的事就之後再開會…\n사과하셔야 할 겁니다\n你肯定會需要為此道歉\n네?\n-什麼？\n작가님께 반드시 사과하셔야 할 거라고 말씀드렸습니다, 팀장님\n-我說你之後肯定 會需要為此向插畫家道歉，張組長\n국연수 씨\n國延秀小姐 (연수) 감정적인 게 아니라 공감입니다\n我這不叫感情用事 而是同理心\n공감 능력 없이 지적 능력으로만 일 잘하는 건\n如果工作能力好是單憑理智 而沒有同理心，\n那沒什麼好驕傲的\n자랑이 아니죠 공감 능력도 곧 지능입니다\n同理心也是智力的體現\n기본적으로 작가님에 대한 예술에 대한 존중이 없으셨어요, 팀장님\n你的行為不管是對插畫家 還是對藝術都有失尊重，張組長\n오늘 일은 작가님을 만나 사과드리는 게 팀장님이 하셔야 할 일입니다\n今天的事情你必須當面向插畫家道歉\n주 진행 사항은 담당자인 제가 해야 할 일이고요\n至於後續事宜 就是我這個負責人的責任了\n다시는 이런 깜짝이벤트는 없었으면 합니다\n希望之後不要再有這種 突如其來的驚喜了\n제가 장도율 팀장님이랑 같은 사람으로 평가된다니\n沒想到竟然被你認為我們是同一種人\n제 지난 행동들을 반성하게 되네요\n我開始反省自己過去的行為了\n","permalink":"https://plateaukao.github.io/blog/posts/%EA%B7%B8-%ED%95%B4-%EC%9A%B0%EB%A6%AC%EB%8A%94/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/d358434b095d/1_sX2HV_NYKJ8MaGzE0ohLcg.png\"\u003e\n\u003cem\u003e\u003ca href=\"https://themovieculture.com/our-beloved-summer-k-drama-review-and-summary/\"\u003ehttps://themovieculture.com/our-beloved-summer-k-drama-review-and-summary/\u003c/a\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e重新看一下這部韓劇，也順便來看看裡面的台詞吧。\u003c/p\u003e\n\u003ch3 id=\"第一集-나는-네가-지난-여름에-한-일을-알고있다\"\u003e第一集 나는 네가 지난 여름에 한 일을 알고 있다\u003c/h3\u003e\n\u003cp\u003e(웅) 보다시피, 어 ，촬영당하고 있어요\u003c/p\u003e\n\u003cp\u003e~다시피: 如你所(見，聽，知，想，等等等)\u003c/p\u003e","title":"그 해 우리는"},{"content":"在 LimeHD 中有個功能是：如果懶得打字輸入的話，在候選列右邊有個麥克風圖案，可以點擊它啟動 Google Voice Input，直接換成語音輸入。這個功能從以前到現在都 work 得好好的。\n但是，有使用者反應升級到 Android 13 它就失效了。剛好我手邊的 Pixel 4 也升級到了 Android 13，所以我稍微試了一下，發現真的是一點反應也沒有。馬上打開程式碼來看出了什麼問題。\n原本的實作方式是去取得目前系統中的所有輸入法列表；一個個看哪一個是 Google Voice Input，找到後，再叫系統切換成那個輸入法。\n看到這實作方式後，第一個猜想是：Google Voice Input 換 package name 了？果然，下了中斷點來檢查 Android 13 中的系統輸入法，發現它變成別的名稱了。\ncom.google.android.tts/com.google.android.apps.speech.tts.googletts.settings.asr.voiceime.VoiceInputMethodService 找到問題後，要修改就容易多啦，只要再多一個 if 判斷式就可以解決。不過，原本的程式碼實在是寫得有點醜，所以就順手再小小 refactor 了一下。\n於是乎，又順手地發布了一個版本。\nRelease Release v6.8.0 · plateaukao/sweetlime\n","permalink":"https://plateaukao.github.io/blog/posts/limehd-%E5%9C%A8%E6%89%8B%E6%A9%9F%E5%8D%87%E7%B4%9A%E4%B8%8A-android-13-%E5%BE%8C%E7%9A%84%E5%B0%8F%E5%95%8F%E9%A1%8C/","summary":"\u003cp\u003e在 LimeHD 中有個功能是：如果懶得打字輸入的話，在候選列右邊有個麥克風圖案，可以點擊它啟動 Google Voice Input，直接換成語音輸入。這個功能從以前到現在都 work 得好好的。\u003c/p\u003e","title":"LimeHD 在手機升級上 Android 13 後的小問題"},{"content":"\nYoutube 和 Netlifx 的內容包羅萬象，很適合拿來學習語言，當做聽力的教材。不過，通常在觀看 Youtube / Netflix 時，預設都只能選擇一個語言的字幕。如果設定為原文字幕，可能會因為難度太高而看不懂或來不及看；但也不可能來來回回在兩個語言的字幕間切來切去。而且，很多時候在 Youtube 上只有 Google 幫忙產生的原文字幕，並沒有自己熟悉的中文字幕可以選擇。\n為了解決這問題，可以在瀏覽器 (Chrome) 上安裝 extension — Dualsub 。Dualsub 讓使用者在許多影音網站上 (包含這邊講到的 Youtube 和 Netlifx)能同時顯示兩種字幕；而且，如果缺少某個語言的字幕，它也有提供機器翻譯的結果。雖然相對上意思沒有那麼精準，但也好過只有原文字幕。\nHome | Dualsub\n如果嫌機器翻譯的效果不好，願意花錢買更好的服務的話，也可以訂閱 Dualsub 的服務，透過 Dualsub server 的專業翻譯，得到更好的品質。\n目前我是覺得光是使用機器翻譯就已經很不錯了，所以暫時還沒有打算訂閱。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%B0%87-youtube-%E5%92%8C-netflix-%E5%AD%97%E5%B9%95%E8%A8%AD%E7%82%BA%E9%9B%99%E8%AA%9E%E5%B0%8D%E7%85%A7/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2976fa971f5/1_3VePGIxHdfQMniO3y5eqKw.png\"\u003e\u003c/p\u003e\n\u003cp\u003eYoutube 和 Netlifx 的內容包羅萬象，很適合拿來學習語言，當做聽力的教材。不過，通常在觀看 Youtube / Netflix 時，預設都只能選擇一個語言的字幕。如果設定為原文字幕，可能會因為難度太高而看不懂或來不及看；但也不可能來來回回在兩個語言的字幕間切來切去。而且，很多時候在 Youtube 上只有 Google 幫忙產生的原文字幕，並沒有自己熟悉的中文字幕可以選擇。\u003c/p\u003e","title":"將 Youtube 和 Netflix 字幕設為雙語對照"},{"content":"使用文石的電子紙螢幕 Mira 差不多要一年了，平常拿來看網路上的文章外，還很常拿它來開發 side projects。不過，在使用 Android Studio 時，一直是很克難地使用系統內建的 IntelliJ Light 主題。雖然不會整個畫面一片黑，但是大面積的淺色背景依然不夠黑白分明，使用一段時間後，還是要不定時地手動刷新畫面。所以，今天花了點時間，寫了一個 Eink 螢幕專用的 theme，在這邊也順便記錄一下開發的方式。\n前置作業 Setting Up a Development Environment | IntelliJ Platform Plugin SDK\n其實，照著官方網上教學，一步步做就可以了。不過，因為官網都是寫英文，所以我還是稍微講一下我開發的過程。雖然我主要想裝 theme 的 IDE 是 Android Studio，但因為它跟 IntelliJ 系出同門，所以要開發 theme plugin 的話，需要先安裝 IntelliJ CE。這到下面官網抓來裝一下就行。\nDownload IntelliJ IDEA: The Capable \u0026amp; Ergonomic Java IDE by JetBrains\n安裝好後，到設定的 Plugins 中，把 Plugin DevKit 也安裝進來。\n新增一個 Plugin 專案 Creating a Plugin Project | IntelliJ Platform Plugin SDK\n從 IntelliJ CE 中，新增一個 IDE Plugin 的專案。\n完成後，專案的目錄結構會長得跟下面類似。resources 目錄下會有大部分的檔案：META-INF 中的 plugin.xml 會定義 themeProvider，這裡會指定你的 plugin id，還有 theme 的名稱；而 theme 目錄中的 einktheme.theme.json 則是用來設定各種 UI 元件在不同狀態時，所需要呈現出來的顏色。\n除了設定 IDE 整個介面的顏色外，如果也想要一併調整文字編輯器 (Code Editor) 中的套色，也可以如下圖中的第 17 行，指定另一個 xml 檔來修改相關的顏色設定。\n這邊的 Eink.xml 是我參考 Eink Color Scheme 這個套件來的，它也有把 source code 放在 github 上。\n官網的目錄說明\n基本上，這樣子就可以先把 theme plugin 編譯出來套用看看了。\n如何編譯和套用自製的 Plugin 在 Build 選單中，有個 Prepare Plugin Module xxxxxx For Development 的選項，點擊後，就會在 resources 目錄中產生一個 jar 檔。\n這時，可以進到設定的 Plugin 畫面，選擇 Install Plugin from Disk，然後選擇剛剛產生好的 jar 檔。安裝完後，再重開 IDE，就可以套用自製的 theme 了。\n調整 UI 元件的顏色 以下是最基本的設定檔內容，裡面都是 key-value 的 pair。\n下面的官網文章有介紹幾個基本元件的設定方式，包括 icons, ui, 等等。因為可以設定的值實在是太多了，文章中的建議竟然是要開發者利用 IDE 的 code completion 來找想要設定的屬性。這方式也太原始了吧，我怎麼知道我想設定的元件可能是叫什麼名字呢？後來，在設定時，有些時候我還是到網路上直接搜尋比較快知道該元件的名稱。\nCustomizing UI Themes - Icons and UI Controls | IntelliJ Platform Plugin SDK\n除了不是很好用的 code completion 外，在文章的最後面有提到可以利用另一個介面 Laf Defaults 來查找元件名稱，和目前元件的顏色。名字不見得能確定叫什麼，但看了它們的顏色，應該就比較能確認是不是自己想要修改的元件。\nLaf Defaults 可以在選單 Tools | Internal Actions | UI Laf Defaults 開啟。但是…預設 Internal Actions 是看不到的，得先用下列方式打開：\n打開 Help | Edit Custom Properties 在裡頭輸入 idea.is.internal=true ，然後重開 IDE Laf Defaults 在 Laf Defaults 視窗中，因為可以看到每個屬性的目前顏色，在搜尋上比較方便，比方說我打關鍵字 background，就可以看到還有多少背景沒有被我改成白色，再看看名稱決定是不是要做修改。\n調整後的內容 其實修改的地方不多，主要是畫面大區塊的淺灰色都能改成白色，整個的感覺就會好很多。其他細部的修改，就等以後有發現再慢慢調整。\n畫面比較 左圖：修改前，右圖：修改後\n相關連結 第一版 plugin\nRelease Release v1.0.0 · plateaukao/intellij_eink_theme\nEink Color Theme\nGitHub - leizhag/eink-color-scheme: A color scheme for JetBrains IDEs on eink displays based on…\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%96%8B%E7%99%BC-intellij-ide-%E7%9A%84-theme-plugin/","summary":"\u003cp\u003e使用文石的電子紙螢幕 Mira 差不多要一年了，平常拿來看網路上的文章外，還很常拿它來開發 side projects。不過，在使用 Android Studio 時，一直是很克難地使用系統內建的 IntelliJ Light 主題。雖然不會整個畫面一片黑，但是大面積的淺色背景依然不夠黑白分明，使用一段時間後，還是要不定時地手動刷新畫面。所以，今天花了點時間，寫了一個 Eink 螢幕專用的 theme，在這邊也順便記錄一下開發的方式。\u003c/p\u003e","title":"開發 IntelliJ IDE 的 theme plugin"},{"content":"最近開始又有一些韓國的 conference 開始舉辦了，是時候再來看看相關新聞了。\n8/14 폭우로 스마트폰이 침수됐다면\n大雨でスマートフォンが浸水したら\n최근 서울을 비롯한 수도권에 내린 기록적인 폭우로 내 스마트폰이 침수됐다면 어떻게 해야 할까.\n13일 정보통신기술(ICT)업계에 따르면 삼성전자와 애플 등 주요 스마트폰 제조사들은 기기가 물에 빠지는 침수 피해에 대해 유상 수리 정책 적용을 기본으로 삼고 있다.\n最近、ソウルをはじめ首都圏に降った記録的な豪雨で、私のスマートフォンが浸水したとしたらどうすればいいだろうか。\n13日、情報通信技術（ICT）業界によると、三星電子やアップルなどの主要スマートフォンメーカー各社は、機器が水に落ちる浸水被害に対し、有償修理政策の適用を基本としている。\n다만 수리 담당자가 피해 발생 배경과 정도 등을 점검한 결과에 따라 수리비 청구 액수가 달라질 수 있으며 경우에 따라서는 수리비가 청구되지 않을 수도 있다.\n기기가 물에 빠졌거나 심하게 젖었을 때는 일단 스마트폰 전원을 끄고 기기를 잘 건조해서 물기를 완전히 제거한 뒤 공식 서비스 센터를 방문하는 것이 바람직하다.\nただし、修理担当者が被害発生の背景や程度などを点検した結果によって修理費請求額が異なる場合があり、場合によっては修理費が請求されない場合もある。\n機器が水に落ちたり、ひどく濡れた時は、ひとまずスマートフォンの電源を切って機器をよく乾燥させて水気を完全に除去した後、公式サービスセンターを訪問するのが望ましい。\n8/6 ‘구글 스타일’ 쿨했지만..젊은 네이버 CEO 취임 이후 주가는 부진 [황정수의 테크톡] \u0026lsquo;구글 스타일\u0026rsquo; 쿨했지만..젊은 네이버 CEO 취임 이후 주가는 부진 [황정수의 테크톡]\n“谷歌風格” 雖然很酷…年輕的NAVER CEO就任後股價低迷[黃正秀的TechTalk]\n젊은 네이버 CEO: 年輕的 Naver CEO\n취임: 上任\n40대 초중반 네이버 경영진美 구글 CEO처럼실적설명회에서 소통”의도된 적자, 카카오와 다르다”솔직, 당당한 설명 호평。주가는 하락세성장성 우려 여전\n40代前半から半ばまでネイバー経営陣の米グーグルCEOのように実績説明会で疎通「意図された赤字、カカオとは違う」「率直、堂々とした説明好評株価は下落傾向の成長性憂慮が依然として残っている」\n경영[經營]\n실적[實績]\n호평: 好評\n애플, 구글, 테슬라, 메타플랫폼 같은 실리콘밸리 유명 테크기업의 대표(CEO)들은 분기마다 실적설명회(콘퍼런스콜)에 나와서 애널리스트들의 질문에 직접 답합니다. 일론 머스크 테슬라 CEO는 직설적입니다. 어떤 질문엔 한숨을 푹 쉬며 싫은 티를 내기도 합니다. 구글의 순다르 피차이는 친절합니다. 답할 때 질문자의 이름을 언급합니다. ‘Thanks, Doug’(고마워, 더그) ‘Good question, Stephen’(좋은 질문이야, 스티븐) 이런 식입니다. 애플의 팀 쿡은 담백합니다.\n蘋果、谷歌、特斯拉、Meta平臺等硅谷著名技術企業的代表(CEO)們每季度都會參加業績說明會(電話會議),直接回答分析師們的提問。 特斯拉CEO埃隆·馬斯克直言不諱。 對某些問題深嘆一口氣，露出討厭的樣子。 谷歌的桑達爾·皮查伊非常親切。 回答時提及提問者的名字。 “Thanks, Doug”(謝謝, Doug) “Good question, Stephen”(好問題啊, Stephen)就是這樣的。 蘋果的蒂姆·庫克很清淡。\n직설: 直說\n언급: 言及，提到\n담백합니다: 淡泊\n40대 초중반(최 CEO 1981년생, 김 CFO 1978년생)의 상대적으로 젊은 나이라서 그런지 몰라도 ‘솔직하다’, ‘소통하려고 한다’는 느낌을 받았습니다. 네이버는 주요 사업부별 손익을 처음 공개했습니다. 핵심 사업인 웹툰이 속해 있는 콘텐츠 부문에서 2분기 영업손실 950억원을 기록했습니다. 네이버가 야심 차게 진행 중인 해외 웹툰 사업의 손실 영향이 큽니다.\n也許是因爲40歲出頭（崔CEO1981年生，金CFO 1978年生）的相對年輕，感覺”坦率”、”想要溝通”。 Naver首次公開了各主要事業部的損益。 核心事業網絡漫畫所屬的內容部門第二季度的營業損失爲950億韓元。 NAVER野心勃勃進行的海外網絡漫畫事業的損失影響很大。\n야심 차게: 野心勃勃\n착한 기업과 강한 기업. 좀처럼 양립하기 어려운 두가지 목표를 네이버 경영진들은 어떻게 풀어갈까요. 마침 다음 콘퍼런스콜이 열리는 내년 11월은 최 CEO와 김 CFO의 내정 사실이 공개된 이후 1년을 향해가는 시점입니다. ‘허니문’이 끝나갈 그 때에도 두 경영자가 지금처럼 외부와 소통하며 우호적인 평가를 받을 수 있기를 기대합니다.\n善良な企業と強い企業。なかなか両立しにくい2つの目標をネイバー経営陣はどのように解決していくのでしょうか。 ちょうど次のカンファレンスコールが開かれる来年11月は、チェCEOとキムCFOの内定事実が公開されてから1年に向かう時点です。 「ハネムーン」が終わっていくその時にも二人の経営者が今のように外部と疎通し友好的な評価を受けられることを期待します。\n양립하기: 兩立 (對立)\n허니문: 蜜月\n7/25 카카오모빌리티 매각 반대 집회..더 커지는 매각 철회 요구 反對出售kakao mobility的集會，要求撤回更大的出售計劃\n카카오모빌리티 매각 반대 집회..더 커지는 매각 철회 요구\n카카오모빌리티는 지난 2016년 5월 대리운전 연결서비스를 시작하면서 기사들에게 20%의 수수료만 받았다. 이후 2018년 9월 피크타임에 2건의 운행을 완료하면 ‘일반 단독배정권’을 지급했다가, 약 두달 뒤 월 2만원을 내면 ‘단독배정권’ 2장을 지급하는 ‘프로서비스’를 도입했다. 단독배정권은 일반 단독배정권보다 우선해서 이용자를 배정받을 수 있다.\nkakao mobility在2016年5月開始提供代理駕駛連接服務時,只從司機那裏收取了20%的手續費。 此後,在2018年9月高峯時間完成2件運行後,將支付”一般單獨分配權”,約兩個月後,如果每月支付2萬韓元,將支付2張”單獨分配權”的”專業服務” 單獨分配權可以優先於一般單獨分配權獲得用戶分配。\n앞서 카카오가 카카오모빌리티를 사모펀드 MBK파트너스에 매각한다는 사실이 알려지면서 카카오모빌리티 구성원을 중심으로 카카오 공동체 내부에서 거센 반발이 일었다. 이에 카카오 측은 지난 7일 공시를 통해 완전 매각은 아니라면서도 “주주가치 증대와 카카오모빌리티의 지속적인 성장을 위해 카카오모빌리티의 지분 10%대 매각을 통한 2대 주주로의 전환 등을 검토 중”이라고 밝혔다.\n此前,隨着Kakao將Kakao Mobility出售給私募基金MBK Partners的事實被傳開,以Kakao Mobility成員爲中心,Kakao共同體內部引起了強烈的反對。 對此,kakao方面7日通過公告表示:”雖然不是完全出售,但爲了增加股東價值和kakao mobility的持續增長,正在討論通過出售kakao mobility10%的股份轉換爲第二大股東等問題。”\n7/23 “집에 로봇 친구 있나요?” 세계 1위 ‘로봇 나라’ 한국 家裡有機器人朋友嗎？世界第一的機器人國家 — 韓國 (真的嗎 XD)\n\u0026ldquo;집에 로봇 친구 있나요?\u0026rdquo; 세계 1위 \u0026lsquo;로봇 나라\u0026rsquo; 한국\n한국인 1만명당 932대.. 로봇 밀도 세계 1위\n식당·공항·편의점 이어 대통령 경호까지 수행\n“인간의 필요·사회적 타협이 로봇 역할 결정”\n每1萬名韓國人中有932台…… 機器人密度世界第一，餐廳、機場、便利商店，甚至是總統隨行介護，”人類的必要和社會妥協決定機器人的作用”\n로봇은 이제 한국인의 일상으로 파고들기 시작했다. 식당에서 음식을 나르는 ‘서빙 로봇’, 편의점에서 근거리 주거지로 상품을 배달하는 ‘로봇 라이더’가 이미 인간의 주변을 활보하고 있다. 로봇이 커피를 타고, 치킨도 튀긴다. 국내 모바일 배달 애플리케이션(앱) 점유율 1위인 배달의민족은 지난 18일부터 인천국제공항 제1여객터미널의 각 게이트에서 주문을 받은 음식을 로봇으로 배달하기 시작했다.\n機器人現在開始進入韓國人的日常生活。 在餐廳運送食物的”服務機器人”、在便利店向近距離居住地配送商品的”機器人騎手”已經在人類周圍闊步前進。 機器人可以泡咖啡，還可以炸炸雞。 國內移動配送應用程式(app)佔有率排名第一的”外賣的民族”從本月18日開始,在仁川國際機場第一航站樓各登機口用機器人配送。\n대통령 옆을 지키는 ‘로봇 경호원’을 보게 될 날도 머지않았다. 대통령 경호처는 지난 12일 삼성전자, 네이버 랩스, 카카오 엔터프라이즈 같은 국내 정보통신기술 기업 소속 전문가 11명과 태스크포스를 출범하고 ‘로봇 개’와 ‘경비 드론’ 도입을 준비하고 있다. 도입 목표 시점은 오는 9월 말이다.\n不久後,就會看到守護在總統身邊的”機器人警衛員” 總統警衛處12日與三星電子、Naver Labs、Kakao Enterprise等國內信息通信技術企業的11名專家成立了 taskforce ,正在準備引進”機器人狗”和”警備無人機” 引進目標的時間是今年9月末。\n7/22 Kakao Universe (Kakao 宇宙~) Kakao 的服務已經包山包海。接下來 Kakao 想要向(韓)國外發展，從目前的 1% 佔有率 (5000萬) 進而達到 99% 的佔有率(50億)。然後還提到了宇宙通信的夢想。\n안녕하세요 남군 입니다\nこんにちは南軍です。\n카카오는\nカカオ\n1997년 1 매일 s 로 시작해서\n1997年1毎日sで始まり\n다음 카카오톡 등 다양한 서비스를\n次のカカオトークなど様々なサービス\n출시하며 대한민국의 커뮤니케이션\n発売中の韓国のコミュニケーション\n방식을 바꿔 왔습니다\n方法を変えてきました\n이후\n以来\n모빌리티 엔터테인먼트 금융 등 새로운\nモビリティエンターテイメントファイナンスなど新\n영역에 도전하고\n地域に挑戦\n기술과 사람으로 더 나은 세상을\n技術と人でより良い世界を\n만들어 왔습니다\n作ってきました\n이처럼 카카오는 다양한 서비스들을\nこのように、カカオは様々なサービスを\n선보이며 성장을 해왔습니다\n披露して成長してきました\n기술적으로 성장 해 왔음에도\n技術的に成長してきたとしても\n다양한 사용자들의 니즈를 다\nさまざまなユーザーのニーズ\n만족시키지 못했습니다\n満足できませんでした\n제가 지모 카카오의 미래 10년의\n私がジモカカオの未来10年\n핵심 키워드인\nキーキーワード\n비욘드 코리아 비욘드 모바일의\nbeyond 韓国ビヨンドモバイル\n집중하기 위한 당한 방법을 고민해\n集中する方法を考えてください。\n왔습니다\n来ました\n7/22 당근페이 개발자들이 모였다: 당근페이 엔지니어링 데이 1회 당근페이 개발자들이 모였다: 당근페이 엔지니어링 데이 1회\n모이다: 集(**あつ)**まる\n지난 7월 6일, 당근페이 엔지니어링 데이 1회가 열렸어요. 코로나19로 인해 화면 너머로만 만날 수 있던 당근페이 엔지니어들이 오프라인에서 모여 자유롭게 이야기할 수 있는 시간이었는데요. 당근페이의 첫 개발자 공식 행사인 만큼 반응이 매우 뜨거였어요.\n今年7月6日，舉行了第一屆的 당근페이 Engineering Day 。 因爲新冠病毒，只能在螢幕另一邊看到的당근페이工程師們可以在線下聚在一起自由交談。 因爲是당근페이 首次的開發者正式活動，所以反應非常熱烈。\n당근페이는 동네에서 쉽고 편하게 쓸 수 있는 금융 서비스를 집중해서 고민하고 만드는 팀이에요. 지역에서 생기는 다양한 거래를 연결하고자 하는 게 목표로, 당근마켓과 다른 별도의 자회사로 떨어져 있답니다 :) 중고거래 송금뿐 아니라 동네 가게에서도 당근페이로 결제할 수 있게 고민하고 개발해요.\n당근페이是集中考慮在住家附近可以簡單方便使用的金融服務並製作的團隊。 以連接地區產生的多種交易爲目標,與당근마켓不同的子公司分開:)不僅是二手交易匯款,在附近店家裏也可以用당근페이結算,進行思考和開發。\n데이지: 당근페이 엔지니어들은 2주에 1번씩 1시간 정도 모여 기술 논의를 하던 시간을 가졌어요. 그 자리에서는 서비스 개발을 위한 기술적 고민뿐만 아니라, 기술 소개 같은 이야기를 공유했는데요. 각 팀이 커지면서 이 시간을 유용하게 쓰기 어려웠어요. 이 시간을 반년에 1번씩 올데이로 진행해보면 어떨지 기술총괄리더인 GD의 제안으로 공식 행사로 이어지게 되었어요.\nDaisy:당근페이工程師們每兩週聚一次，約1個小時左右，進行了技術討論。 在那裏不僅分享了開發服務的技術苦惱,還分享了技術介紹等故事。 隨着各隊的壯大，很難利用這個時間。 這個時間每半年進行一次,在技術總管GD的提議下,正式活動得以延續。\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%96%B1%E8%AE%80%E9%9F%93%E5%9C%8B%E6%96%B0%E8%81%9E722/","summary":"\u003cp\u003e最近開始又有一些韓國的 conference 開始舉辦了，是時候再來看看相關新聞了。\u003c/p\u003e\n\u003cp\u003e8/14 \u003cstrong\u003e폭우\u003c/strong\u003e로 스마트폰이 \u003cstrong\u003e침수\u003c/strong\u003e됐다면\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e大雨\u003c/strong\u003eでスマートフォンが\u003cstrong\u003e浸水\u003c/strong\u003eしたら\u003c/p\u003e\n\u003cp\u003e최근 서울을 \u003cstrong\u003e비롯한\u003c/strong\u003e 수도권에 내린 \u003cstrong\u003e기록적인\u003c/strong\u003e 폭우로 내 스마트폰이 침수됐다면 어떻게 해야 할까.\u003c/p\u003e","title":"閱讀韓國新聞(7/22~)"},{"content":"早期的寫法都是\n建立一個 Intent，指定想要的 Activity 或是塞一些 filter 讓系統或使用者幫忙找出能提供服務的 Activity。 呼叫 Activity.StartActivityForResult()，放入上個步驟中建立好的 Intent 和一個自己定義好，不會重覆的 request code，這時，系統就會去啟動另一個 Activity 起來做事。 覆寫 Activity 中的 onActivityResult 函式，判斷收到的 requestCode 是不是自己在步驟 2 中指定的那個數字，如果是的話，就可以拿 data 來做自己想要的後續步驟。 乍看之下，這方式似乎沒有什麼太大的問題。如果是個小 App，同個 Activity 中只有一兩個需要這麼處理的邏輯，而且不會常常改來改去的話，那在管理上還不會有太大的問題。但如果常常需要新增這類的應用的話，往往在 onActivityResult 中就會有大量的 request code 判斷和處理，也會產生許多 request code 需要細心地管理，避免重覆。\n新的作法 後來在 AndroidX 中，Android 推出了 registerForActivityResult() 的方式，在 ComponentActivity 和 Fragment class 中都有包含這個函式。對於需要呼叫其他 Activity 起來作事再回傳資訊的，可以改成事前註冊的方式。註冊完後會取得一個 ActivityResultLauncher 的 instance。這個 instance 就可以在真正需要呼叫其他 Activity 時使用它的 launch() 函式來達到一樣的效果。\n除了能讓不同的場景處理能夠很自然而然地分開到不同的 Launcher 中以外，在 Android 文件中還提到另一個好處是：常常在呼叫其他 Activity 起來後，由於系統資源不足，可能原本在等資訊的那個 Acitivity 會被系統砍掉。利用這方式能確保當它被系統 restore 時，同樣的 Launcher 還是會被生成，讓原先的 flow 能繼續進行下去。\nEinkBro 中的其中一個實作 由於 EinkBro 的前身是好幾年前就開始開發的，所以舊有的相關實作都還是利用 onActivityResult 完成。這次，為了要支援儲存 data url 的圖片，需要呼叫系統的檔案管理器畫面，讓使用者指定圖片想要儲存的位置和名稱。所以我在這流程中實用了這新的方式。\n建立 Launcher 首先，我在 BrowserActivity 中建立 saveImageFilePickerLauncher，它的真正實作放在 BrowserUnit 中，另外，我還塞入了一個當儲存完畢後的 postAction，讓我可以詢問使用者是不是要立即顯示圖片 (4 ~11行)。\n在 BrowserUnit 中的實作則是填入當收到 ActivityResult 時，用來處理的函式：\n這邊使用了 ActivityResultContracts.StartActivityForResult()，這是內建的幾種 Contract 之一，如果沒有特殊需求的話，使用這個就行了，它的操作方式和原本的作法差不多，也是準備一個 intent 就能操作。\n另外，要注意的是在 Callback 裡，呼叫了 handleSaveImageFilePickerResult()，這個函式會在下面一點的地方解釋。\n使用 Launcher Launcher 準備就緒後，再來便是在需要使用它時，呼叫 Launcher 的 launch()。下面的程式碼是 BrowserActivity 中判斷 url 是不是 data url 的片段；在得知 url 是 “data:image” 開頭時，會呼叫 BrowserUnit.saveImageFromUrl()，並帶入之前建立好的 Launcher\nBrowserUnit.saveImageFromUrl()，準備了些資訊，初始化好一個 intent，再來，就可以呼叫 Launcher.launch() 囉。當 16 行的 resultLauncher.launch(intent) 執行後，再來就是等 Callback 回來。\n在 Callback 中處理回傳的 ActivityResult 當使用者在 Activity 中完成操作後，相關的資訊會被代入 Launcher 中的 Callback。剛有提到的 handleSaveImageFilePickerResult 會被執行。\n這邊有點類似是在 onActivityResult() 裡要做的邏輯：首先，檢查回傳的 activityResult.resultCode 是不是正常，以及 activityResult.data 有沒有資料。經過初步判斷後，就可以做正事，把收到的 content uri 塞進 saveImage() 函式去寫入檔案。在第 9 行可以看到：postAction 也被當成參數代入。這樣子才能確保當檔案非同步寫入完成後，可以跳出對話框問使用者是不是要立即檢視。\n注意事項 在官方文件中有提到，對於呼叫 registerActivityForResult() 和 ActivityResultLauncher.launch() 的呼叫時機有一些限制：\nregisterActivityForResult() 必須要在 Activity 或 Fragment created 之前就呼叫。 ActivityResultLauncher.launch() 則是要在 Activity 或 Fragment created 之後才可以使用。 這兩項限制使得動態地 register / unregister Launcher 變得不可能，有點可惜。\n結語 新的 ActivityForResult 的機制，去除了自定 request code 的煩惱，程式碼能更加獨立。如果有更複雜的需求，也可以自訂 Launcher 或是 Contract；比起早期大鍋炒的 onActivityResult() 好多了。\n參考資料 其實官方資料的說明就很清楚了\nhttps://developer.android.com/training/basics/intents/result\n","permalink":"https://plateaukao.github.io/blog/posts/android-%E8%A3%A1%E5%BE%9E%E5%8F%A6%E4%B8%80%E5%80%8B-activity-%E4%B8%AD%E5%8F%96%E5%BE%97%E8%B3%87%E8%A8%8A%E7%9A%84%E6%96%B9%E5%BC%8F/","summary":"\u003cp\u003e早期的寫法都是\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e建立一個 Intent，指定想要的 Activity 或是塞一些 filter 讓系統或使用者幫忙找出能提供服務的 Activity。\u003c/li\u003e\n\u003cli\u003e呼叫 Activity.StartActivityForResult()，放入上個步驟中建立好的 Intent 和一個自己定義好，不會重覆的 request code，這時，系統就會去啟動另一個 Activity 起來做事。\u003c/li\u003e\n\u003cli\u003e覆寫 Activity 中的 onActivityResult 函式，判斷收到的 requestCode 是不是自己在步驟 2 中指定的那個數字，如果是的話，就可以拿 data 來做自己想要的後續步驟。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e乍看之下，這方式似乎沒有什麼太大的問題。如果是個小 App，同個 Activity 中只有一兩個需要這麼處理的邏輯，而且不會常常改來改去的話，那在管理上還不會有太大的問題。但如果常常需要新增這類的應用的話，往往在 onActivityResult 中就會有大量的 request code 判斷和處理，也會產生許多 request code 需要細心地管理，避免重覆。\u003c/p\u003e","title":"Android 裡從另一個 Activity 中取得資訊的方式"},{"content":"購買 我是從京東上購得 Matepad Paper。我特別選在 618 活動之前購買，差不多十天送達家裡。在付款後的頭一兩天，貨品打包的時間花得久了一點，不像前一兩年買海信手機時，幾乎是買完幾個小時內，商品就已經從倉庫出發。十天還在可以接受的範圍內，至少沒有因為 618 的降價活動而延後送達。不過，在 618 時，Matepad Paper 的 6+128GB 硬是從 3699元 降到了 3499元，就看要早送達，還是要再省下那兩百元了。\n因為京東大部分能送海外的商品都會提供直送，所以不用再等它進到集運艙。下完單付完款後，就每天關心易利委(EZWay) 有沒有商品進海關等著宣告就行。\n【华为HUAWEI MatePad Paper】华为墨水屏平板HUAWEI MatePad Paper10.3英寸电纸书阅读器 电子书电子笔记本 6+128G WIFI墨黑 含笔+皮套【行情 报价…\n硬體篇 外觀 很有質感，重量感覺不輕，但應該沒有其他家的 10 吋機器重。拿起來比我現在有的文石 Note3 還薄，背面和手握持的地方有舖上類似單眼相機的握把皮革，摸起來很舒服，又不會手滑或是累積指紋。\n皮套 皮套的質感也做得很像是主機的皮革感，但仔細看可以看出來紋路不大相同，質料也不同。皮套的固定方式是用磁吸式，打開時，可能吸得沒那麼緊，不過，因為扣環的地方會整個環繞住兩面，吸力比較強，在闔上時就很牢固，而且順便會把觸控筆也包覆住，一舉兩得。扣環打開時，還可以向後吸在背面，避免甩來甩去的，頗有巧思。\n觸控筆 觸控筆也是一樣很有質感。比起文石的各種黑色或白色塑膠筆，華為的觸控筆會讓人愛不釋手。看起來應該也是塑膠的，不過筆身是銀色的，摸起來很像是金屬，但又不會有金屬冰冰的感覺。筆身有一條凹槽，這讓它在吸附到 Matepad Paper 時，附著力提高許多。沒錯，Matepad Paper 可以讓筆直接吸附在主機上，避免臨時不用，亂丟，然後就不見了。\n這支筆需要充電，但它沒有任何插槽。它的充電方式正是讓它吸附在 Matepad Paper 上。而且，它是透過藍芽跟 Matepad Paper 連線的。這跟大部分使用 Wacom 技術觸控筆的其他電子紙廠商不同。雖然華為號稱它的手寫速度反應快了很多，但實際上我在書寫時，並沒有特別的感覺。\n儘管需要充電，而且要透過藍牙連接，我用了快一個月，也沒有特別感到有什麼不方便的地方。\n觸控筆的頭是橡膠的，聽說比較耐磨，可以用比較久。我試著在 Matepad Paper 上書寫時，覺得它比較滑，沒有文石觸控筆那種阻泥感。比較沒有在紙上書寫的感覺。不過，它的筆記軟體筆觸設計得比較好，寫出來的字的粗細更自然。\n按鍵 它的電源鍵還能充當指紋辨識。每次打開皮套蓋時，需要再用手接觸一下電源鍵才可以解鎖。我是覺得有點太麻煩，所以直接進了設定，把指紋解鎖的功能關了。\n在右上方附有音量鍵。這個位置不是很好，有點過高，不適合當成看書時的翻頁鍵。而且，如果當下沒有在使用觸控筆的話，通常筆是吸附在 Matepad Paper 的右邊，想要降低音量時，很難不被觸控筆卡到。\n喇叭 很高檔，聽得出來用料很好。大部分電子書閱讀器的喇叭都會被拿來播放有聲書。但華為這台的喇叭，會讓人比較想要用它來播放音樂。\n旋轉偵測 從上面的照片中可以看得出來，在設備的左邊設計得比較寬，適合手持。但如果有使用者想要用右手拿著設備的話怎麼辦呢？這時，只要把 Matepad Paper 轉 180 度，畫面就會跟著也旋轉。完全不用另外做什麼設定。重點是，也沒有任何設定可以讓你去控制。既然 Matepad Paper 支援這功能，表示它裡頭有內建 sensor 能感知畫面的旋轉。但是！它目前只支援 180 度而已。不知道為什麼它要把 90 度旋轉關掉。這點讓很多使用者感到不開心。很多 APP 其實也很適合在橫向的畫面使用的。\n為此，好在 EinkBro 在 App 中有支援 90 度旋轉。Matepad Paper 沒有的，我自己來提供。:D\n電池 沒特別去注意它的容量有多大，但是，完全比不上其他家的持久度。即使是在待機的狀態，還是會很明顯地耗電。開著 Wifi 操作，大概一小時會耗 10% 的電力，正常使用的話，應該一兩天就要充一次電。如果把它當成是 Android 平板的話，電力算是中等。但要拿它跟其他閱讀器比的話，應該是不及格吧。\n雖然它的持久力不好，但是它應該是把電力都拿去運算了，在各個 APP 間來回操作時，都很流暢，不太會卡住。\n我覺得 Matepad Paper 的定位是在商務使用，性能比電力重要，所以為了要保持一定的性能，華為犧牲了這台的電力表現。(當然也有可能是華為還沒有調整好啦)\n畫面 不要跟其他電子書閱讀器放在一起比較的話，可能會覺得還不錯。但是，當它被擺海信電子紙手機，或是文石設備旁時，可以很明顯地看出來，華為的畫面就是比較暗。\n沒有比較，沒有傷害。\n軟體篇 作業系統 除了不支援 Google Play Services 外，它連作業系統用的也不是 Android。Matepad Paper 採用了華為自家最近推出的鴻蒙系統。鴻蒙上可以執行大部分的 Android 軟體。它的強項是跟華為其他設備間可以很方便地傳遞資料，甚至是把 Matepad Paper 當成第二螢幕。但這一切的前提是，要先有華為全家桶才行。至少這在台灣是不大可能達成的，也因此，多設備互連的功能對我來說只是個享受不到的口號。\n內建軟體 — 華為閱讀 不及格。很想直接跳過不說明。建議用戶自行安裝其他書城的 APP，或是安裝靜讀天下，KoReader，或是其他免費的電子書閱讀軟體。\n這邊講一下它很明顯的缺點：\n不行替換自己的字型，內建的字型選擇又很少。 劃線的內容無法匯出(???) 沒有橫轉直排的選項 試著用它閱讀設定為直排的 epub 檔案，會無法正常顯示內容，只能顯示部分，完全無法閱讀。 用它來閱讀 pdf 時，更是令人傻眼：\n沒有裁切功能，只能用手指放大縮小。放大縮小時不順暢。而且一旦放大後，就無法手寫筆記。 想要使用手寫時，要先從選單中點選啟動。 對於想要當成商務機型的設備來說，提供良好的 pdf 閱讀體驗是最基本的吧。但這點 Matepad Paper 做得很差。 講了這麼多缺點和需要改進的地方，還是要幫它說幾句好話。華為閱讀也有翻譯的功能。使用方式跟文石差不多，開啟後會將畫面切為橫向，分為左右兩邊。左邊是原本的文件內容，右邊會顯示翻譯後的結果。文石提供三種翻譯結果，百度，Bing和Google。Google 永遠翻譯失敗，百度和 Bing 在大部分情況下可以翻譯成功。而華為的翻譯功能則是能很穩定地翻譯成功。這點是還不錯的地方。\n另外，華為閱讀的目錄列表佔畫面比較大的比例，可以完整的把較長的章節名稱完整顯示出來。這比只顯示幾個字，完全看不出來每個章節標題是什麼的文石好太多了。\n內建軟體 — 筆記 堪用。筆跡效果很好。雖然筆觸比較滑，不過寫一寫，久了就習慣了。有些覺得可以再改善的地方是：\n筆記的模板很少，而且不行自製。 上方的工具列和下方的翻頁鍵都是固定的，不行動態的隱藏。好好的一個 10 吋設備，寫起筆記，可以下筆的地方就跟一台 7.8 吋的設備差不多。也由於畫面上下被工具列佔住，筆記最終產生的 pdf 大小並不是 A4 尺寸，而是一種怪怪的矮胖比例。 內建軟體 — 設定 設定的畫面很像是 iPad 的兩欄式呈現方式，左邊是大的項目列表，右邊是項目的第二層內容。從設定的畫面來看，看得出來華為有在 UI 上下過工夫，針對電子紙設計更合適的元件。\n內建軟體 — 系統下拉選單 介面簡潔漂亮。但是系統的刷新模式只有提供普通模式和智慧模式。普通模式畫質較佳，刷新速度慢，適合用在看電子書；智慧模式則是用在其他所有的場合。智慧模式比文石的 Speed 模式還要慢。不過，它殘影抑制的比較好。在使用上我幾乎很少需要手動去按刷新。\n另外，因為這個下拉選單是華為自己改過的，所以無法像文石的設備一下，能夠利用 Android 平台開放的 API 加入自己想要的功能和按鈕。\n自行安裝 APP 不得不說，這點完全是華為 Matepad Paper 的強項，也是我當初購買它的主要原因。論閱讀功能，短時間應該很難有廠商能超過文石提供的閱讀器 APP，這也是為什麼明明文石使用的硬體都已經是 n 年前的 CPU 了，賣那麼貴使用者還是會買單。\n但是，如果使用者想要的是一台性能強的平板加上一塊電子紙螢幕的話，華為 Matepad Paper 會是更好的選擇。有了強大的 CPU 後，裝上別家的書城 APP 一樣可以開心地看書。而且也可以順暢地使用平常在手機上使用的軟體。\n在拿到 Matepad Paper 後，我先利用內建的瀏覽器裝上 EinkBro APP，然後就刪除了內建的瀏覽器。再來利用 EinkBro 下載 Aurora Store APP，它可以讓你在沒有登入 Google 帳號的情況下，下載 Google Play Store 上的軟體。我安裝了 Hyread APP，Readmoo App, 微信閱讀，還有 Oreilly APP。\n以我自己的使用習慣來說，平常用最多的功能分別是：\n用內建筆記寫寫東西 用 Hyread APP 看圖書館借來的書 用 EinkBro 看一堆網頁 (這佔大部分使用時間) 用 Oreilly App 看電腦技術相關的英文書籍 在微信閱讀利用免費卡看電腦書籍 就上述的使用情景來說，Matepad Paper 表現得很稱職。\n目前內建的軟體還是很悲劇，華為也很認真的幾乎每隔一兩個月就會推出更新。這麼頻繁地更新是其他廠商做不到的。希望它內建的軟體能早日達到堪用的程度，激發其他廠商的憂患意識，讓產業能以更快的速度演進。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%8F%AF%E7%82%BA-matepad-paper-%E8%B3%BC%E8%B2%B7%E5%BF%83%E5%BE%97%E8%88%87%E4%BD%BF%E7%94%A8%E6%84%9F%E6%83%B3/","summary":"\u003ch3 id=\"購買\"\u003e購買\u003c/h3\u003e\n\u003cp\u003e我是從京東上購得 Matepad Paper。我特別選在 618 活動之前購買，差不多十天送達家裡。在付款後的頭一兩天，貨品打包的時間花得久了一點，不像前一兩年買海信手機時，幾乎是買完幾個小時內，商品就已經從倉庫出發。十天還在可以接受的範圍內，至少沒有因為 618 的降價活動而延後送達。不過，在 618 時，Matepad Paper 的 6+128GB 硬是從 3699元 降到了 3499元，就看要早送達，還是要再省下那兩百元了。\u003c/p\u003e","title":"華為 Matepad Paper 購買心得與使用感想"},{"content":"Android 的 Jetpack Compose 也推出好一段時間了，從前一兩年的 beta 版，再到去年的正式版，網路上的相關教學文章也愈來愈多，相關的函式庫也愈來愈成熟。是時候可以跳進這個坑了。\nEinkBro 現況分析 目前 EinkBro 真正有需要顯示自己的畫面的地方並不多，畫面上大部分的空間都留給了 WebView。在全螢幕的情況下，更是一點其他的 UI 元件都沒有。稍微歸納了一下還稱得上是 UI 的畫面如下：\n一堆對話框 點擊翻頁的設定 主要功能選單 工具列設定 書籤列表 字型設定 快速切換設定選單 …\n設定畫面 進入設定後的 PreferenceScreen，這算是 UI 的大宗，但大部分都只需要設定 preference xml，和在 fragment 中接一下 action 就可以搞定了 工具列 畫面下方除了有很彈性的工具列外，還包含了網址輸入框以及網頁內搜尋的介面。工具列得要配合工具列設定還有 Preference 才能完美運作。 開雙視窗的翻譯功能 網頁全文翻譯時，可以將畫面一分為二。主視窗依然可以利用工具列操控；副視窗則是會在畫面上有一條新增的工具列可以使用。這功能採用了自己客製出來的 TwoPaneLayout ，不僅可以上下分頁，還可以左右分頁，或是主副對調位置；更厲害的是，能透過畫面中的拉桿，調整兩個視窗的比例。 選擇逐步採用策略 說多不多，但說少，想要一次全部改成用 Compose 實作，也是不大可能，尤其是我對於 Jetpack Compose 的認知也還極其有限，修改起來應該要花不少工夫。\n最終，選定的策略是先從最單純，相依性不高的對話框開始改起。對話框的邏輯通常不會太複雜，給定資料後，只管呈現出來，再把最終使用者選的項目，做的修改，再帶回給呼叫的人處理就可以了。不會有太多的狀態變化要管理。這麼一來可以讓我先不去接觸 state management 這一塊。\n在這麼多對話框裡，再選出一個相對單純的應用來試刀：快速切換設定選單。畫面上只需要幾筆項目，每一筆項目有圖，有文字，還有個 Checkbox，點下去會呼叫對應的 preference 改變。\n快速切換設定選單 第一支 commit 在此。\n原本有打算直接使用 Jetpack Compose 的 dialog 來實作，或是利用原生的 Dialog，在裡頭塞一個 ComposeView 。但這兩個方式都失敗了。最後，退而求其次，改成實作 DialogFragment，並在 DialogFragment 中的 Dialog 放入所需的 ComposeView。\nFastToggleDialogFragment\n原生的程式碼就這樣而已，主要是設定一下 Dialog 的屬性，讓它不要畫 Shadow，而且可以放置在畫面上我想放的位置，然後，設定好 ComposeView。(第 39 行的 Column 可以忽略。我不知道那時候在想什麼。)\nFastToggleItemList\nFastToggleItemList 就有點像是在刻 xml 的 layout，把一個個的項目所需的 resource 都指定到 FastToggleItem 中，最後還包含了一個 action，用來切換對應的 SharedPreference 值。\nFastToggleItem\nFastToggleItem 就很像一般的 Jetpack Compose 教學，利用 Row 將 Checkbox, Icon, 和 Text 放在一行上。這邊有用到 remember 來記錄 Compose 中的 isChecked 狀態。\nScreenshot of FastToggleItemList\n程式寫得差不多後，在 Android Studio 的 Preview 畫面也能即時看到畫面大概會長成怎麼樣。雖然沒有像 Flutter 的 hotreload 那麼快速，但也遠比要一直去調整 layout xml 來得好多了。\n功能選單 完成快速切換設定畫面後，再來處理的是充滿各種功能的選單畫面。\nMenuItems\n實作的第一支 Commit 在這兒。\n當初在用 xml 撰寫這個畫面時，一直覺得很厭世。每個功能都要先用 LinearLayout 包一張圖，一個字串，然後再塞到對應的 Horizontal LinearLayout 中。再加上每個元件都要設定寬高大小，整個 xml 常常要複製貼上一大段 code。\n換成 Jetpack Compose 就好做多了。可以先利用 Preview 把單一 Item 的樣子寫好(如下)：\nMenuItem screenshot\n實作如下。(其實看起來跟 FastToggleItem 長得差不多)\nMenuItem\n確定外觀沒錯後，就能再把外圈的 layout 構建出來。\nMenuItems\n由於我不想要在 DialogFragment 中實作每個按鈕的動作，所以我在這裡定義了 MenuItemType，當某個按鈕被點擊時，我會將這資訊帶給呼叫的人，並告知這是哪種 Type，這麼一來，外面的人就知道可以怎麼處理這情形。\n要讓程式看起來更乾淨的話，其實可以將 MenuItem title 和 icon resource id 都塞到 enum type 中，這樣子 MenuItem 就可以只塞 MenuItemType 就好。\n工具列設定畫面與主畫面的工具列 之前最想利用 Jetpack Compose 來改寫的，應該就是這兩個畫面吧。原本要新增一個新的工具列功能，必須要先在 ToolbarAction enum 中新增一個 type，然後去改主畫面的 xml layout，在工具列 layout 上新增一個按鍵；再來，還要工具列設定的實作中，確定新增的 type 有被列入。整個過程很煩索，要改好幾個地方。常常都是改了之後，發現還有地方忘了改而 crash，再事後補上。\n工具列設定與工具列\n調整成 Jetpack Compose 後，就沒這問題了，因為都是用程式碼寫出來的介面，而 single source of truth 就是定義好的 ToolbarAction enum 和存在 sharedpreference 中的目前工具列狀態。\n現在想要新增功能的話，只要在 ToolbarAction enum 中加一個新的項目，然後在主畫面的 BrowserActivity 中去實作該 ToolbarAction 被點擊時要做的事就行了。兩個畫面的 UI 實作完全不用再改動。\n下面就是目前的主畫面工具列實作方式。在 18 行會 for loop 整個 ToolbarAction enum 去決定怎麼生成每個 ToolbarIcon。\nComposedToolbar\nToolbarIcon 提供了 onClick 和 onLongClick 兩種函式，並依據 toolbarActionInfo 決定當下的 icon resource id 為何。(因為有些按鈕有開和關的不同狀態，所以我把它包在 ToolbarActionInfo 中。)\nToolbarIcon\n工具列設定畫面有點類似快速功能啟動設定畫面，這裡就不細說了。\n改造設定畫面第一層 Dialog 在摸熟後，逐漸可以比較快速地翻新成 Jetpack Compose 的實作。再來，要處理的是設定的畫面。原本設定的畫面如下，就很一般的 Preference 介面。雖然是設定，但其實第一層幾乎都是空的，負責將使用者帶往真正要設定值的下一頁去。\n舊的設定畫面\n以 EinkBro 的用戶來看，大部分使用者使用的會是電子書閱讀器，通常畫面會比手機大。如果一行只顯示一個項目的話，實在有點浪費畫面。所以，我打算把它改成是按鈕式的，而且是一行兩個元件的型式。\n修改後的畫面\n這次修改的 commit 在此。\n這次定義了 SettingItemType enum，並在裡頭加入了字串和圖案的 resource id。\nSettingItemType\n比較不同的是，在實作 Setting item 時，加入了我想要的點擊效果：讓按鈕的邊框會加粗。這會比整個按鈕反白來得自然，又不會在電子紙上產生過多的殘影。\nSettingItem\n第 60, 61 行的 interactionSource，和 51, 52 行的實作，就是讓 Composable 知道當下點擊狀態的方式。知道狀態後，在 59 行，可以依照狀態決定邊框的粗細。下圖就是點擊後的示意圖。如果有其他的 Compose 元件想要根據點擊狀態來做一些處理，都可以用這方式實作。\n結語 在經過這些嘗試後，開始體會到 Compose 方便的地方。與其事前定義好 xml layout，然後事後拿出對應的 View 元件去設定屬性或調整畫面，不如畫面全部都利用程式碼產生，這麼一來各種邏輯寫法都可以套在產生 UI 這件事上，不再被 xml 給侷限住。\n目前離全部轉換成 Jetpack Compose 還有很遠的一段路要走，但我已經跨出了最艱難的第一步了。:)\n補充一點 因為採用 Jetpack Compose，讓 EinkBro 的 apk size 大了 900 KB 左右。這對一般 app 來說，並不是很大的差別。但是對於一個原本只有 3.47 MB 的 EinkBro 來說，整個 App 就這麼因此大了快 1/3。這對我來說是個很痛的決定，不過，為了以後能更方便地調整 UI，也只能接受這結果。\n對於 EinkBro Jetpack Compose Migration 有興趣的人，可以去看一下 9.0.0 和 9.1.0 之間的 commits。\n相關連結 GitHub - plateaukao/browser: An Android web browser based on webview, which is specialized for…\n","permalink":"https://plateaukao.github.io/blog/posts/go-compose-no-compromise/","summary":"\u003cp\u003eAndroid 的 Jetpack Compose 也推出好一段時間了，從前一兩年的 beta 版，再到去年的正式版，網路上的相關教學文章也愈來愈多，相關的函式庫也愈來愈成熟。是時候可以跳進這個坑了。\u003c/p\u003e","title":"Go Compose, No Compromise"},{"content":"大綱 Word Wise 和 X-Ray 是什麼 替代方案介紹 安裝方式 如何使用 範例畫面 其他設備上的效果 Word Wise 和 X-Ray 是什麼 在亞馬遜官網有介紹：X-Ray 功能讓你在閱讀時可以很方便地知道人物、地點和事件。如果一本書很厚，人物錯綜複雜，或是看書的前後時間拉很長，常常會忘記誰是誰。這時， X-Ray 可以讓你很方便地回想起書中目前提到的人物是誰。Word-Wise 則是當你在閱讀其他語言的書籍時，把一些較難的單字直接在書籍中用小字標上簡單的說明。讓你不用一直要手動再去翻字典也能看得懂內容。\nKindle Features: Search, X-Ray, Wikipedia and Dictionary Lookup, Instant Translations\nfrom WordDumb github\n替代方案介紹 Word Wise 和 X-Ray 只限於在亞馬遜購買的書，在 Kindle 上能使用而已。如果是在其他書城，或是沒版權的書，就無法享用到這麼方便的功能。不過，方法是人想出來的。有好心人推出了類似的功能 — WordDumb，而且開放原始碼在 Github 上。這是由 Facebook 粉絲團裡的網友推薦的。今天我安裝，試了一下，感覺還不錯，所以打算來詳細介紹怎麼安裝和使用。看得懂英文的，可以直接參考下面的連結就行。\nGitHub - xxyzz/WordDumb: A calibre plugin that generates Kindle Word Wise and X-Ray files for KFX…\n安裝方式 這功能是依附在 Calibre 上的。Calibre 是個跨平台的電子書管理兼閱讀軟體。功能超級強大，只是介面個人覺得不是很好看。但是，用久了也就會習慣的。Calibre 的安裝方式網路上很多，基本上就是去官網抓安裝檔下來，點個幾下就行。 calibre - Download calibre\n(非必需，如果你 KFX 格式的書籍再裝就行了) 到 mobileread 網站下載 KFX Input 的 plugin。安裝的方式是：進到 Calibre 軟體中，進到它的設定畫面，選擇 plugins，然後選 Load plugin from file，把下載下來的 KFX Input.zip 指給它。 [Conversion Plugin] KFX Input - MobileRead Forums\n3. 再來就是這篇文章的重頭戲：安裝 WordDumb plugin 請看影片，可以直接從 Clibre 中安裝。\nhttps://user-images.githubusercontent.com/21101839/124686751-39f3aa00-df06-11eb-9b07-8c8f98544683.mov\n簡單的說明如下，最後，重新開啟一下 Clibre 就可以來使用啦！\n使用方式 全部安裝好以後，在 Calibre 的上方工具列會有個紅色海星。只要選幾本書，再點它，就會開始運作。第一次使用時，可能要好幾分鐘才會完成，因為第一次需要去抓一堆函式庫回來安裝。不過都是自動地，不用擔心，過一陣子就可以看到右下方的 Jobs 完成。\n執行完成後，請進到書籍所在的目錄 (點畫面中的 Path: Click to open)，可以看到多了一份 xxxxx_x_ray.epub 檔案。檔案大小可能會大很多，因為其中還包含了許多圖片。\n點紅色海星右邊的下拉選單可以看到很多選項，大家可以自己摸索一下。\n範例畫面 先來看看原本書籍在 Kindle 中的樣子，就是很平淡的樣子，沒有任何額外的資訊。\n再來看看在經過處理後的檔案 (有 Word Wise 和 X-Ray了)\n再點選單中的 X-Ray 來看看，人物，術語和相關圖片都整理地好好的。\n在其他設備上看的效果 同樣的輸出檔，試了在 Huawei Matepad Paper 和文石的 NeoReader，Koreader，還有靜讀天下軟體中閱讀。**目前只有靜讀天下 (Moon Reader) 可以正常顯示所有的 X-Ray 相關解釋。**而 Word Wise 則是看不到。\n大部分的人名和專有名詞都會是藍色的\nBig Island 的說明，可以往下捲動，還有附地圖！\n本書主角的說明\n後話 還蠻有趣的 plugin，讓書籍的參考資料能在開始閱讀前都先塞到書籍中。之後應該找幾本書來看看是不是有很大的幫助。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%88%A9%E7%94%A8-calibre-worddumb-plugin-%E4%BE%86%E9%81%94%E5%88%B0-word-wise-%E7%9A%84%E5%8A%9F%E8%83%BD/","summary":"\u003ch3 id=\"大綱\"\u003e大綱\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eWord Wise 和 X-Ray 是什麼\u003c/li\u003e\n\u003cli\u003e替代方案介紹\u003c/li\u003e\n\u003cli\u003e安裝方式\u003c/li\u003e\n\u003cli\u003e如何使用\u003c/li\u003e\n\u003cli\u003e範例畫面\u003c/li\u003e\n\u003cli\u003e其他設備上的效果\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"word-wise-和-x-ray是什麼\"\u003eWord Wise 和 X-Ray 是什麼\u003c/h3\u003e\n\u003cp\u003e在亞馬遜官網有介紹：X-Ray 功能讓你在閱讀時可以很方便地知道人物、地點和事件。如果一本書很厚，人物錯綜複雜，或是看書的前後時間拉很長，常常會忘記誰是誰。這時， X-Ray 可以讓你很方便地回想起書中目前提到的人物是誰。Word-Wise 則是當你在閱讀其他語言的書籍時，把一些較難的單字直接在書籍中用小字標上簡單的說明。讓你不用一直要手動再去翻字典也能看得懂內容。\u003c/p\u003e","title":"利用 Calibre + WordDumb plugin 來達到 Word Wise 的功能"},{"content":"網路上已經有很多教學在講怎麼利用 Github Actions 建立 CI 流程，在 push commits 或是發出 pull requests 後，啟動相關的 action flow 執行 unit test 和產生 debug apk。\n而這一篇主要的重點在於，怎麼利用 Github Actions 編譯 release apk。編譯 release apk 比較不一樣的地方是，必須要指定 keystore 給它。以往在發布 EinkBro release version 到 Github 中時，我都是自己手動在電腦上執行\n./gradlew clean assembleRelease -Pandroid.injected.signing.store.file=/path_to/browser.keystore -Pandroid.injected.signing.store.password=xxx -Pandroid.injected.signing.key.alias=xxx -Pandroid.injected.signing.key.password=xxx 等編譯好後，再手動在 Github 上新增一個 release 版本，將 apk 從電腦拖拉進畫面，寫寫 what’s new，再按下送出。\n目前這 flow 也沒有什麼不好，只是畢竟 release version 不會天天發布，但 code 倒是有可能天天在寫，時不時都會加點新的功能進去，或是又修了些畫面，解了些臭蟲。在驗證時，一般也只會在手邊的一台機器上進行而已。\n如果家裡的其他台手機或閱讀器也想要安裝還沒正式發布的版本的話，就要乖乖接上電腦，把編譯好的版本傳進去，或是利用之前改過的 Sharik APP，從別台 Android 設備上傳進去(如果那台機器剛好就在手邊的話)。\n不過，既然每次改完程式碼都會 push 到 Github 上，難道就不行叫 Github 幫我在上頭也直接針對最新的程式碼產生可用的 release 版本嗎？這麼一來我隨時想要裝最新的都可以直接連到網路下載就行了。\n實作 建立 Github Actions 需要的 yml 設定檔 將本地端在使用的 keystore 檔案先 encode 成 base64 的字串，當成 secret 塞到 Github 中 在 yml 中指定 tasks，然後在編譯完後，指定要留存的 artifacts (以我要的場景來說，就是那個 app-release.apk 接下來我們一步步來看\n建立 yml 設定檔 建立一個 yml 放在 .github/workflows 下\n將 keystore encode 成字串 在 Mac 上，執行下面的指令\nopenssl base64 \u0026lt; ~/browser.keystore | tr -d \u0026#39;\\n\u0026#39; | tee browser_keystore_base64_encoded.txt 然後，將上面的 txt 文件內容，連同幾個必要的值 (alias, store password, keystore password) 填到 Github Settings 中的 Secrets 中。\n撰寫 yml 檔內容\nhttps://github.com/plateaukao/browser/blob/main/.github/workflows/buid-app-workflow.yaml\n2 行設定只有在 pull requests 和 commit 被 push 時，會被執行，\n7 行是先把程式碼抓下來，\n9 行把 keystore 字串還原成檔案，\n15 行開始編譯。這邊會利用指令把 Github secrets 中存的值取出來使用\n17 行是在編譯結束後取出想要的 artifacts。因為我只想留 apk ，所以在 21 行只寫了 apk 的路徑。\n實際運作畫面 一切都設定正確後，就會看到 Actions 中只有要 pull requests 或是 commit pushed，都會有新的 workflow 被執行。\nhttps://github.com/plateaukao/browser/actions\n如果想要取得最新的 release build，可以點畫面最上方的 Successful build，就能看到 app-release.apk 供用戶下載。\nhttps://github.com/plateaukao/browser/actions/runs/2443113086\n目前有個小缺點是：雖然只設定了一個 app-release.apk 的 artifacts，但點擊後，其實它下載的會是一個 zip 檔。所以如果想要安裝的話，還得要先解壓縮才行。這似乎是目前 Github Actions 的限制，還沒有找到可以解決這行為的方式。\n後記 雖然前後花了點時間才找到怎麼完成想要的功能，但不得不說 Github Actions 還蠻方便的。編譯好的 artifacts 預設會保留 90 天，也很夠使用了。如果單純只是要驗證 pull requests 的 debug apk 的話，說不定可以設定成幾天後就 expire。\n另外，還找了一下是不是有 fixed url 可以取得 latest workflow built artifacts。有的話我就可以把它放在 readme.md 中，不用再層層進到 Actions 下載 artifacts。不過，目前似乎還沒有比較直覺或官方的作法。這點應該會再找時間看能不能改善。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%88%A9%E7%94%A8-github-actions-%E5%BB%BA%E7%AB%8B-android-%E5%B0%88%E6%A1%88%E7%9A%84-release-apk/","summary":"\u003cp\u003e網路上已經有很多教學在講怎麼利用 Github Actions 建立 CI 流程，在 push commits 或是發出 pull requests 後，啟動相關的 action flow 執行 unit test 和產生 debug apk。\u003c/p\u003e\n\u003cp\u003e而這一篇主要的重點在於，怎麼利用 Github Actions 編譯 release apk。編譯 release apk 比較不一樣的地方是，必須要指定 keystore 給它。以往在發布 EinkBro release version 到 Github 中時，我都是自己手動在電腦上執行\u003c/p\u003e","title":"利用 Github Actions 建立 Android 專案的 release apk"},{"content":"在用 WebView 開發瀏覽器時，一個很常見的功能是：長按連結時，會跳出 ContextMenu Dialog，提供針對這個連結的一些功能。大部分的瀏覽器都會包含像是開新分頁，在背景開啟分頁，分享連結網址等等的選項。\n有些時候，除了連結外，如果也能夠取得被點選的連結的字串那就更好了。因為使用者的本意可能是想要看到的字串，而不是它真正的連。\n在經過一番尋找後，找到了一個特別的用法，在此記錄下來。\nhttps://developer.android.com/reference/android/webkit/WebView#requestFocusNodeHref(android.os.Message)\nWebView 有個函式叫 requestFocusNodeHref(), 它會回傳下面三個值：\nurl: anchor 的 href 值 src: 圖片 src d 值 title: anchor 的字串 其中的 title 就是我想要取得的資料。requestFocusNodeHref() 的使用方式有點間接。下面列出我在 EinkBro 中的實作：\n1 行寫到，必須建立一個 Message 物件，並餵給它 target，然後，它會在 handleMessage 的 callback 函式中，傳回我們要的資料。拿到後，再去做後續的處理。因為我只需要 anchor 的文字，所以我只拿取了 title 而已。\n順利拿到字串後，接下來不論是想要 copy 這個字串，或是將連結建立成新的書籤，都能更方便地進行了。\n相關連結 fix: when new tab is not loading in background, need to get the title\u0026hellip; ·…\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%BE%9E-android-webview-%E4%B8%AD%E5%8F%96%E5%BE%97%E9%BB%9E%E9%81%B8%E9%80%A3%E7%B5%90%E7%9A%84%E6%96%87%E5%AD%97/","summary":"\u003cp\u003e在用 WebView 開發瀏覽器時，一個很常見的功能是：長按連結時，會跳出 ContextMenu Dialog，提供針對這個連結的一些功能。大部分的瀏覽器都會包含像是開新分頁，在背景開啟分頁，分享連結網址等等的選項。\u003c/p\u003e","title":"從 Android WebView 中取得點選連結的文字"},{"content":"用 Flutter 開發出來的 Sharik APP 是個可以安裝在多平台( Android, iOS, Windows, Mac, Linux) 上的軟體，可以在同個網路下快速地分享字串、檔案，甚至是 Android App 上已經安裝的軟體 apk。\n它很有效地解決了在同個網路下多個設備間的訊息交換需求。因為是用 Flutter 開發的，所以我的 iMac, mac mini, Macbook pro，Android 手機們，Android 電子書閱讀器們，都能隨時開啟功能，將當下的網址、文件，甚至是在使用中的 APP apk 檔案，很方便地傳送到另一台設備去。\n不過，Sharik APP 中的實作不是很有效率。在發送端的設備，它寫了一個簡易的 Web Server，可以處理 sharik.json 的 request，傳出設備的一些基本資訊，以及想要傳送的字串；而 base url path / 則是會回傳想要傳送的檔案或 apk。\n在接收端的設備，為了找到同個網路下哪個 IP有開啟 Web Server 可以用來取得資料，它的作法是先查出自己的 IP 為何，然後產生所有 local subnet 的 IP 列表，對著這些 IP 一個一個去 ping ping 看，看有沒有 timeout，沒有的話就放到一個清單中。然後再針對這個清單的 IP 發出 http request，看能不能取得 sharik.json 。可以的話就表示對方有在分享資料，這時，就會把這些資料傳給 UI 去呈現，讓使用者可以確認後點擊，然後帶到瀏覽器去做下載的行為。\n發送端沒有什麼太大的問題，但接收端的作法很暴力。上面描述的邏輯是塞在同一個函式中，用 Flutter 的 compute function 包住，每隔一兩秒就會再執行一次；在搜尋的過程中，一直重覆著同樣的 ping -\u0026gt; fetch sharik.json 。\n原本的發送端與接受端實作\n調整 Sharik App 的作法 後來，在朋友的提示下，我把整個機制改成 UDP multicast 來實作：在發送端開始分享時，每隔一秒對同個網路發出 broadcast ，直接送出 sharik.json 的內容；在接收端的設備，也是註冊了同一個 multicast 網路，然後 listen ，等著看是否有訊息進來。如果有的話，就直接可以 parse 然後呈現在 UI 上。\n這作法少了很多不必要的邏輯，重點是在接收資料時，比原先有時收得到有時收不到的效果好太多了。通常一按下接收，資料就進來了。\n修正後，導入 multicast 的實作方式\n以下是發送端的作法：\nSharing Service\n55, 56 行是 bind 到一個 Socket 上，\n57~64 行是後來多加的，可以用來知道遠端是誰接收了這資料。\n67~70 行則是簡單地利用 Timer 定期透過 Socket 發送訊息出去。\n接收端的實作也很單純：\nReceiver Service\n41 ~ 43 行是 bind 到同個 Multicast 上。\n45 ~ 64 行則是在收到 socket 傳來的 datagram 時，針對內容做些資料的處理。\n50 ~ 54 行是在處理當 datagram 資料字串是 http 開頭時，這情況應該就是由 EinkBro 送來的，所以可以把內容當成是 url，直接叫 UI 呈現出來。\n55 ~ 60 行就是收到 sharik.json 後的處理。\n這邊之所以要特別針對 EinkBro 做處理的原因是：我也把這整套機制加到 EinkBro 中了！如果只是單純要傳網址，或是設備只想拿來當接收端的話，完全不需要再安裝 Sharik APP。只要打開 EinkBro，再點接收資料的按鈕就行了。\nEinkBro 的實作 下面就也來講講在 EinkBro APP 上的實作。主要可以分成兩部分來說，一個是 UI 的呈現，一個是底層網路的操作。\nUI 部分，我先是做了兩個 Dialog (如下)：\nSendLinkDialog\nReceiveDataDialog\n從 EinkBro APP 中，只能分享網址，所以它的分享畫面只是一個單純的對話框，中間有個轉不停的 progressbar 。想要接收資料時，也是先顯示一個對話框，一旦收到資料後，不論是收到字串，檔案，或是 apk，其實都是要透過分享端的 http server 來下載，這時，只要把 url 代入 EinkBro 中，就能很自然地串到下載的邏輯。\n我們再來看看 ShareUtil 中的 幾個函式是如何實作的：\nShareUtil\n26, 25 行的 multicast IP還有 port 都是跟 Sharik APP 中使用一樣的值；這樣能確保兩者間能夠相通。\nstartBroadcastingUrl() 在 17 行建立好 multicast socket 後，每隔一秒會發出想要分享的網址。\nstartReceiving\n如果是接收端的 EinkBro，也是一樣會先生成 multicast socket，然後處理收到的 datagram 字串。handleSharikScenario 只是做做 json parsing 的邏輯，這裡就不特別說明了。\n如此一來，兩台設備間的 EinkBro 就也可以很方便地傳送網址了。我大部分的網頁瀏覽都是在電子紙閱讀器上的 EinkBro 完成的，但有時看到一些 Youtube 網址的分享，或是有比較多圖片的網頁時，還是會想要用一般的手機來閱讀。這時，我就能很快地利用兩台設備的 EinkBro 交換網址，無縫繼續使用。\n或許有人會說，為什麼不用 Chrome 或是 Firefox 這類瀏覽器，它們都可以登入帳號，然後你在哪台設備上做的事，也可以很快地在別台設備上看到。雖然它們可以滿足一部分類似的需求，但它們做不到傳檔案的需求，而且，它們的任何操作都是要經過 cloud 的。\n目前在 Sharik APP 和 EinkBro 中，是完全沒有 tracking 的機制在的，沒有什麼行為資訊會被送到遙遠的遠端做分析。我自己用起來比較開心。\n相關連結 Forked 而且改過的 Sharik APP\nGitHub - plateaukao/sharik: Sharik is an open-source, cross-platform solution for sharing files via…\n已經發了 commit，但還沒發行最新版的 EinkBro repo\nGitHub - plateaukao/browser: An Android web browser based on webview, which is specialized for…\nSharik APP 調整實作的前後 Sequence Diagram\nRefactoring of Send Receive Mechanism · plateaukao/sharik Wiki\n","permalink":"https://plateaukao.github.io/blog/posts/flutter-%E5%92%8C-android-%E4%B8%8A-udp-multicast-%E7%9A%84%E6%87%89%E7%94%A8%E5%90%8C%E7%B6%B2%E8%B7%AF%E4%B8%8B%E7%9A%84%E8%B3%87%E8%A8%8A%E5%88%86%E4%BA%AB%E5%8A%9F%E8%83%BD/","summary":"\u003cp\u003e用 Flutter 開發出來的 \u003ca href=\"https://github.com/marchellodev/sharik\"\u003eSharik APP\u003c/a\u003e 是個可以安裝在多平台( Android, iOS, Windows, Mac, Linux) 上的軟體，可以在同個網路下快速地分享字串、檔案，甚至是 Android App 上已經安裝的軟體 apk。\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/751ea08c1921/1_ueMy4iBa8dn-Gu8fui5A-Q.png\"\u003e\u003c/p\u003e\n\u003cp\u003e它很有效地解決了在同個網路下多個設備間的訊息交換需求。因為是用 Flutter 開發的，所以我的 iMac, mac mini, Macbook pro，Android 手機們，Android 電子書閱讀器們，都能隨時開啟功能，將當下的網址、文件，甚至是在使用中的 APP apk 檔案，很方便地傳送到另一台設備去。\u003c/p\u003e","title":"Flutter 和 Android 上 UDP Multicast 的應用：同網路下的資訊分享功能"},{"content":"Re-reading Clean Architecture. Still in progress, but the mindmap could be shared here first.\n","permalink":"https://plateaukao.github.io/blog/posts/mindmap-of-clean-architecture/","summary":"\u003cp\u003eRe-reading Clean Architecture. Still in progress, but the mindmap could be shared here first.\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/679b107fa9d6/1_mL5s9io5wjS9lfSpk59u3Q.png\"\u003e\u003c/p\u003e","title":"Mindmap of Clean Architecture"},{"content":"在電子書閱讀器上用瀏覽器時，有些時候會覺得當下的內容用手機或電腦看會比較恰當，想要快速地切換過去。或是不同的設備間也常常會有想要把檔案傳到另一台上做處理或閱讀。以前可以使用 LINE Lite 來稍微達到這需求，但隨著 LINE Lite 停止服務後，一直找不到一個合用的工具。\n需求 每個人的使用需求不大一樣，下面是我自己想要解決的問題：\n跨平台，希望能包含 Android 設備(有的設備無內建 Google Services)，MacOS, iOS 設備。 能夠傳輸字串、檔案。 步驟不要太複雜(不然我自己打網址就好了)。 能不要跨到外網去就不要(當然最好也不要有任何 tracking 的記錄)。 研究 LANDDrop: 之前有在使用 LANDrop，它可以在同個網路下，不同設備間很方便的傳輸檔案，但並不支援單純只傳字串。另外，LANDrop 在 Mac 上的操作步驟多了點，不是那麼順手。\nAndroid Nearby Share: 只有部分的 Android 設備支援。使用前必須要開啟 Wiki 和 Location Service。\nPushBullet (TBD)\nSendAnywhere (TB)\nSharik 和修改內容 後來終於找一個還不錯的 open source 軟體 — Sharik，可以達到我的需求；而且因為是開源的，所以想要怎麼改或是加功能都可以。它原本就已經包含了大部分我需要的功能：在同個 wiki 網路下，兩台設備間傳送”字串”，”檔案”，或 APP，只是他的 UI 沒那麼直覺。所以我把它拿來改了改，並且在 MacOS 版本，加上了比較直覺的 drag and drop 的文件分享功能，並且拿掉了追縱使用者行為的相關程式碼。\n移除不必要的 UI 元件 Sharik 就只提供分享檔案，和接收別台設備分享出來的檔案，就功能來說很單純；但是原作者加了一堆(我覺得不必要的)畫面，像是 introduction 用的三個畫面，切換介面語言的 UI，是否要關閉 user tracking 的對話框，和大大的 About 畫面。\n這些都是可以拿掉，讓整個畫面看起來更簡潔的。這部分沒有什麼細節可以分享，單純是把一堆 Screen 都移除。\ncommit 程式碼：\nremove redundant UI. shorten searching time · plateaukao/sharik@e779454\n加入拖拉檔案，啟動分享的功能 在手機上，檔案拖拉沒有那麼實用，但是在 MacOS 上如果想要分享文件的話，從開好的 Finder 中拖拉檔案到某個區域的話，會是比較好的操作流程 (跟開啟一個莫名的 File Picker，再自己一層層地進到檔案所在的目錄比較)。\nSharik 不意外，是用 Flutter 開發的。為了讓 MacOS 支援拖拉的功能，先是加入了 desktop_flutter 這個套件：\ndesktop_drop: ^0.3.3 再來是把現在的主畫面，用 DropTarget 包起來，處理當有物件放進來時的行為。下圖的 62 行可以看到當 onDragDone 行為被呼叫時，會去執行 _handleDroppedFile 函式。\n_handleDroppedFile的內容很單純，把收到的檔案訊息包裝成原本就已經實作好的 SharingObject，再去呼叫也是原先就實作的 _shareFile() 函式，就會進入分享的畫面。\ncommit 程式碼：\nadd file drag and drop feature for macos · plateaukao/sharik@cdb5bf9\n加速分享來源的搜尋速度 原本的實作方式是先取得目前設備中的網路 ip，經過分析後，將 ip 的最後一個區段變換為 1 ~ 254 的數字，利用 for loop 去 ping 這些 ip (的某兩個 寫死的 port)；在一定的時間內有反應的話，就表示該 ip 有別台設備正在做分享。\n這時，會進行下一個步驟：將該 ip 轉成網址，去抓取遠端設備上提供的 json 檔案。json 檔案中包含了遠端設備的 OS, name, 分享的種類(文字、檔案或 APP)和 ip 訊息。\n因為要把 1 ~ 254 個 ip 都掃過一次，常常要好幾秒才會完成；所以我在這邊做了點改善：如果已經掃描過，而且完成過分享的 ip，我會把它記錄下來。下次要再接收別台設備的分享時，就會優先掃描這些已經傳送過的 ip。\n以正常的使用情況來說，一個人頂多擁有五到十台設備，掃描十台設備，比起掃描 254 個 ip 還是快很多的。\n儲存已經傳送過的 ip\nSharik 使用 Hive 套件儲存設定的資訊，所以我也利用同樣的機制來儲存 ip 清單。\n我先把 NetworkAddr class 抽出來成為獨立的檔案，並加上要讓 Hive generator 能看得懂的 annotations。\n然後利用下面的指令產生所需的 Adapter class\nflutter packages pub run build_runner build --delete-conflicting-outputs 在 APP 啟始時，會去初始化 hive 相關的 storage:\nconst KEY_SENDER_IP_LIST = \u0026#39;sender_ip_list\u0026#39;; Box\u0026lt;NetworkAddr\u0026gt;? senderIpList; ... senderIpList = await Hive.openBox\u0026lt;NetworkAddr\u0026gt;(KEY_SENDER_IP_LIST); 在有成功傳送後，會將其寫入 hive storage\nreceiverService.addListener(() { if (receiverService.receivers.isNotEmpty) { final address = receiverService.receivers.first.addr; if (senderIpList?.values.contains(address) != true) { senderIpList?.add(address); } } }); } 下次，在掃描 254 個 ip 前，就可以針對 hive storage 中的 ip 先掃描一次，有找到的話就直接回傳，沒有的話才會再往下執行：\n這麼一來，只要是之前有收過檔案的來源，下次再分享的話，就可以馬上收到，不用等上太久。\ncommit 程式碼：\nkeep a list of transferred sender, for speeding up search · plateaukao/sharik@7ffe317\n傳送網址的示範影片 相關連結 下載位置 Releases · plateaukao/sharik\n原始碼 GitHub - plateaukao/sharik: Sharik is an open-source, cross-platform solution for sharing files via…\n最原本的 Sharik 版本\nSharik - file sharing via WI-FI - Apps on Google Play\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%B9%E9%80%A0-sharik%E4%B8%80%E5%80%8B%E5%85%A7%E7%B6%B2%E8%A8%AD%E5%82%99%E9%96%93%E5%82%B3%E9%80%81%E6%96%87%E5%AD%97%E6%AA%94%E6%A1%88app-%E7%9A%84%E8%B7%A8%E5%B9%B3%E5%8F%B0%E8%BB%9F%E9%AB%94/","summary":"\u003cp\u003e在電子書閱讀器上用瀏覽器時，有些時候會覺得當下的內容用手機或電腦看會比較恰當，想要快速地切換過去。或是不同的設備間也常常會有想要把檔案傳到另一台上做處理或閱讀。以前可以使用 \u003cstrong\u003eLINE Lite\u003c/strong\u003e 來稍微達到這需求，但隨著 \u003cstrong\u003eLINE Lite\u003c/strong\u003e 停止服務後，一直找不到一個合用的工具。\u003c/p\u003e","title":"改造 Sharik：一個內網設備間傳送文字、檔案、APP 的跨平台軟體"},{"content":"QuickRotate Utility on Android Devices — 1.8 MB 與 57.9 KB 的距離 在文石的電子書閱讀器上，旋轉畫面的工具很不好用，要三個動作才能完成：1. 從畫面上方，下拉控制區塊；2. 點擊旋轉按鈕；3. 找到想要旋轉的方向，點下去。\n圖1. 文石電子書閱讀器的旋轉功能\n為了讓第三個步驟能省略掉，我寫了一個小工具來幫我。Android 從 7.0 開始，有提供可以客製化 Quick Tile Settings 的功能。平常需要開開關關的功能，都可以包裝成一個 TileService，放到下拉通知欄的區域。詳細的教學可以看一下下面的文章。\nQuick Setting 實作 Quick Settings Tiles\n利用這方式，我建立了一個簡單的 TileService 來做畫面旋轉的功能。安裝後就可以在文石閱讀器中，加入新的旋轉按鈕，不用再從上面圖1中的四個方向做選擇。\nGitHub - plateaukao/quickrotate: quickly rotate screen on Android devices without second thought\n圖示 圖2. 加入了左轉的按鈕\n如何縮小程式的大小 當我在完成這個小工具時，發現到明明只有寫了幾十行的程式碼，和放了兩張左旋轉和右旋轉的圖案，編譯出來的 apk 竟然要 1.8 MB 之大。這超出了我的想像。\n我試著在 build.gradle 中加入 proguard 的設定，但似乎編譯完後，沒有什麼幫助。我再研究了一下 build.gradle 中的有使用到的函式庫：\nimplementation \u0026#39;androidx.core:core-ktx:1.7.0\u0026#39; implementation \u0026#39;androidx.appcompat:appcompat:1.4.1\u0026#39; implementation \u0026#39;com.google.android.material:material:1.5.0\u0026#39; implementation \u0026#39;androidx.constraintlayout:constraintlayout:2.1.3\u0026#39; 這個利用 Android Studio 建立出來的 Empty Activity project，預設就塞了這些函式庫。以這個小工具來說，除了在還沒取得系統設定讀寫的功能前，需要跳出系統的同意畫面讓使用者同意外，完全不會有 UI 呈現。\n所以，這些函式庫應該都要有辦法拿掉才對。於是，我先把程式中唯一的 MainActivity 的 layout xml 從 ConstraintLayout 改成 LinearLayout；然後，把 MainActivity 的 parent class 從 AppCompatActivity 改成 Activity。\n最後，我把 Android Studio 幫我生成的 App Theme 也拔掉了，這麼一來，就也可以把 android.material 和 androidx.core 的依賴也移除。\n如此一來，整個程式的大小馬上縮小到 57.9 KB 。\n詳細的 commit 可以看下面這個連結。\nrefactor: remove un-necessary stuff: androidx support, material desig\u0026hellip; ·…\n結論 如果只是想寫個 quick setting tile 的話，是可以把 Android 的各種函式庫都拔掉的。1.8 MB 和 57.9 KB 的差別，中間差了 30 倍呢。(雖然都很小就是了)\n","permalink":"https://plateaukao.github.io/blog/posts/quickrotate-utility-on-android-devices-%E8%82%A5%E5%A4%A7%E7%9A%84-android-library/","summary":"\u003ch3 id=\"quickrotate-utility-on-android-devices18-mb-與-579-kb的距離\"\u003eQuickRotate Utility on Android Devices — 1.8 MB 與 57.9 KB 的距離\u003c/h3\u003e\n\u003cp\u003e在文石的電子書閱讀器上，旋轉畫面的工具很不好用，要三個動作才能完成：1. 從畫面上方，下拉控制區塊；2. 點擊旋轉按鈕；3. 找到想要旋轉的方向，點下去。\u003c/p\u003e","title":"QuickRotate Utility on Android Devices — 肥大的 Android Library"},{"content":"EinkBro 是一個專門為 Eink 電子紙閱讀器開發的瀏覽器 APP。由於介面長得跟一般的瀏覽器 APP 很不一樣，而且又有很多按鈕可以按，和很多設定可以調整，所以寫了這篇文章來介紹一下它。首先會講解基本功能和設定。然後在第二部分說明一些更進階的用法，以及我自己在使用上的技巧。\n安裝好 EinkBro 後，一進入到 APP 中可以看到預設的網頁是 Google 搜尋畫面。馬上會注意到，平常常見的工具列不是在畫面上方，而是畫面下方。這是第一個要習慣的地方(後面會提到，可以調整設定，將它移到畫面上方)。\n第一次開啟 EinkBro 的畫面\n工具列 工具列\n工具列看似簡單，但提供了很豐富的功能。從左到右，分別的功能是：\n網頁標題：除了可以看到標題外，點選它也可以進入修改/輸入網址的模式。當工具列放入過多工具時，可以左右滑動，看到被遮住的部分。 書籤：為了要快速連結到常用的網站，EinkBro 在工具列上放了書籤列表按鈕，一鍵開啟書籤頁。 書籤(長按): 將當下瀏覽的網頁加入書籤。 已開啟的網頁列表：EinkBro APP 支援同時開啟多個網頁。這個按鈕上的數字代表了目前已開啟的網頁數目。點擊它會列出目前的所有網頁列表，方便使用者切換分頁。 已開啟的網頁列表(長按): 開啟無痕模式。瀏覽某些網頁時，並不希望被該網站追蹤。這時，可以長按分頁按鈕，進入無痕模式，再按更新按鈕。 新增分頁: 點擊後可以開啟新的分頁。 新增分頁(長按): 新增新的 EinkBro 視窗，方便在較新版的 Android 設備上，利用 Multi-Window 的方式，同時顯示兩個網頁。 返回: 回到上一頁。 返回(長按): 顯示最近 5 筆歷史瀏覽記錄。 重新載入: 重新載入網頁。 重新載入(長按):進入全螢幕模式。 觸控翻頁：讓網頁可以透過觸控畫面特定區域來翻頁，減少畫面的殘影。 觸控翻頁(長按): 開啟觸控翻頁的設定畫面。可以指定觸控翻頁的區域。能開關是否顯示可點區域的範圍；或是在輸入文字時，暫時隱藏點擊區域。有時，需要翻頁的方向相反的話，也可以在這裡設定。 字體設定(長按)：進入閱讀模式。 閱讀模式：如同 Chrome 或是 Safari 中的 Reader Mode，可以將網頁上不重要的元素暫時移除，只留下本文的部分，方便閱讀。(本功能僅適用於大部分網頁。部分網頁不支援，故點擊後可能沒有任何反應) 設定：開啟選單，提供更多的功能和設定。詳細內容請看下一段的說明。 設定(長按): 開啟快速切換選單。後面會介紹到快速選單是幹嘛用的。 選單 EinkBro 的功能多到有點眼花暸亂，選單中的第二列和第三列，如果點擊右方的 \u0026gt; 可以將所有的功能展開，變成下圖的樣子：\n接下來，就一排一排來介紹：\n內容標記 EinkBro 在網頁上選取文字後，支援劃重點的功能。劃完的重點可以在 ”內容標記” 中閱讀。\n開啟首頁 回到設定的首頁。(預設是 Google 網站)\n關閉分頁 關閉當前分頁。如果目前只有一個分頁的話，則會順便關閉 EinkBro\n離開 離開 EinkBro，不論現在開了幾個分頁。\n這一區塊主要提供跟分享網頁或儲存相關的功能。\n接收資料/傳送網址 如果你有兩台以上的設備安裝 EinkBro，可以透過這兩個功能幫你快速地分享網址到另一台設備上。如果在 MacOS 上安裝了 Sharik，也可以利用它來傳送文件/檔案到有 EinkBro 的設備上。\n加入書籤 將當前網頁加為新的書籤。\n建立捷徑 將當前網頁的連結新增到桌面上，方便更快速地打開平常常用的網頁。\nApp 開啟 有些網址可以用已經安裝的 App 來開啟，像是 Youtube, Instagram, Facebook，等等。這時，可以點擊 “App 開啟”, 將該網址在對應的 App 中呈現。\nCopy link 複製網址。\n分享網址 快速地將網址分享到其他 App。\n加到 Pocket 如果想要稍後閱讀當前的網頁，可以利用此功能，快速地將網頁送至自己的 Pocket 帳號。\n儲存網頁 將網頁儲存成 mht 的格式，可以在離線時，從文件管理器中點擊該檔案，再由 EinkBro 開啟閱讀。\n儲存 epub 將當前網頁儲存為 epub 格式的檔案。儲存後，可以利用設備上的其他閱讀軟體開啟。如果在儲存時，選擇了之前已經存在的 epub 檔案，則可以將當前的網頁存成該 epub 文件中的新章節。方便將多個網頁集結成書。\n開啟已存 epub 可用來開啟之前利用 EinkBro 儲存的 epub 檔案。\n存成 pdf 將當前網頁儲存成 pdf 檔案，完整保留網頁的樣貌。\n第三區塊是關於網頁呈現方式的微調功能。\n分割畫面 點擊後，可將畫面一分為二，同時顯示兩個網頁。適合畫面較大的平板或是電子書閱讀器。\n全文翻譯 可將當前網頁內容翻譯成其他語言\n直排閱讀 可將當前網頁內容以直排的方式呈現。適合較長的中文文章閱讀。\n閱讀模式 如同 Chrome 或是 Safari 中的 Reader Mode，可以將網頁上不重要的元素暫時移除，只留下本文的部分，方便閱讀。\n翻頁設定 設定點擊翻頁的區域。\n朗讀內容 將網頁的文字內容利用系統的文字轉語音功能，讀出來。\n反轉顏色 將網頁的整個內容顏色反轉。(在某些黑底的網頁上可以使用看看)\n白底黑字 適合使用在類似 ptt 的網站，將畫面轉為白色。目前會記住哪些網站有設定過。下次再瀏覽時會保持設定。\n黑色字體 有些網站的字型顏色不是很適合閱讀器。此按鈕可以將網站所有文字的顏色調整成黑色。\n粗體字型 把字型變粗體\n字型大小 開啟字體大小對話框，能快速調整網頁的字體。也可以將字型調整成 serif 字型(如果系統有內建的話)，Google Serif (Google 的雲端字型)，或是使用者自行在手機目錄下安裝的字型。如果要選用 ”自訂字型” 的話，請先確保有在下方的 “設定自訂字型”處，設定好自訂字型。有時字型會無法當下就套用，可能要重新載入畫面才能生效。或是，有些網頁並不支援該功能。\n常要調整字型大小時，可以將其加入工具列。在工具列時，如果長按，可以直接啟動閱讀模式；節省一個按鈕的空間。\n最後一排按鈕的介紹如下。\n網頁內搜尋 搜尋網頁內的資訊\n開啟下載 開啟系統的下載目錄\n工具列設定 能自訂工具列上的按鈕。詳細內容將在下一小節介紹。\n快速切換 有些藏在設定畫面裡的功能，為了要讓它們比較容易開關，所以多了這個選項。點擊後會出現下面的對話框。裡頭的開關應該都蠻清楚是幹嘛用的。特別說明一下，EinkBro 也有支援音量鍵翻頁，如果不喜歡這功能的話，可以在這裡關閉它。另一個要稍微說明的是 背景播放 功能。EinkBro 在播放影片時，如果回到 Home，或是進入其他 APP，這時畫面的影片是會停止播放的。如果希望 EinkBro 在離開當下畫面還能繼續播放影片或音樂的話，請勾選該項目。\n設定 進入設定頁\n工具列設定選項說明 有許多功能可以加到工具列上。預設只放了幾個常用的，如果你的設備畫面比較大，可以自行再加入更多的功能。下面將針對前面還沒介紹到的功能逐一介紹。另外，要注意的是，長按功能前方的”二” 的話，就可以移動功能的 順序。\n左為工具列的按鈕第一頁，右為按鈕的第二頁\n上一頁，下一頁：可在工具列上透過這兩個按鈕，對網頁翻頁\n工具列設定：如果常需要調整工具列的話，可以直接將它置於工具列上。\n直排閱讀：可將當前網頁內容以直排的方式呈現。適合較長的中文文章閱讀。\n粗體字型: 加粗網頁字體\n字型加大，字型變小: 調整網頁字體大小\n全螢幕: 隱藏工具列，讓畫面可視範圍變大\n往前：網頁記錄往前一頁\n旋轉畫面：將畫面轉為橫向。設備不支援畫面轉向時，可以使用此功能。\n全文翻譯：將網頁翻譯成其他語言。詳細的用法會在後面介紹。\n關閉分頁：關閉當前分頁\n輸入網址：如果覺得不需要”網頁標題”，但又需要輸入網址時，可以加入此功能。\n新增分頁：原先的新增分頁功能在”分頁列表”按鈕中。如果常需要新增分頁，可以加入此功能。\n桌面版網站：此功能可以在快速設定頁設定，但可以加入工具列中，讓它可易於切換。\n全文翻譯 預設的翻譯方式是 Google 的全文翻譯。點擊按鈕後，會在畫面上方顯示翻譯列，可以切換成其他翻譯的語言。\n如果想要更換成其他的翻譯方式，可以在將該功能加入工具列後，長按該按鈕。目前有支援 Google 文字翻譯畫面(雙開)，Papago 文字翻譯畫面(雙開)，Google 全頁翻譯(雙開)，Papago 全頁翻譯(雙開), 大螢幕的設備的話，也可以試試 Papago Dual Pane。\n最後一個選項是預設的。如下圖所示，一條 Google 翻譯的工具列會出現在畫面上方。\nGoogle 文字翻譯畫面(雙開) 適合使用在單純只想翻譯文字的場合。右邊其實是一個 Google 翻譯的網頁，它會將左邊網頁中的文字擷取出來，讓右邊的 Google 網頁做翻譯。\n因為 Google 網頁的文字翻譯有字數上限，所以可以看到右邊畫面的下方有 1, 2, 3, 4 的數字鍵，這是分頁的按鈕。目前是在第一頁，如果想接著看下去的話，就需要再按 2 。\n右下方的其他按鈕分別是：\n切換直式/橫式畫面: 如果覺得現在看到的畫面很難看清楚兩邊的資訊的話，可以改成在畫面中間切一刀，讓文字的寬度比較大，方便閱讀。\n長按上面切換的按鈕：可以將畫面兩邊的內容對調。\n同步滾動：這功能在其他的翻譯模式下比較有用。當開啟時，如果你在捲動左邊的網頁，右邊的翻譯結果也會一起往下捲。\nA+, A-：翻譯畫面的字型大小調整。\nX: 關閉翻譯結果。\n畫面正中間的紅框，有一個灰色的色塊。那是一個拖拉條，可以調整畫面左右兩邊的比例。\nGoogle 全頁翻譯 看起來跟文字翻譯類似，但是它會保留網頁的樣子，而且不用再分頁了。大部分的工具列跟上面講的差不多，只多了一個 TW 的按鈕。這是用來切換成不同翻譯語言的按鈕。\n下面這張圖就是選了日文的效果截圖\n預設的 Google in-place 翻譯 最後來介紹一下預設的翻譯效果。這是目前效果最好的，但就沒有兩邊對照的好處了。如果想關掉它的話，就按顯示原文，或是重新載入網頁就可以了。\n以上就是 EinkoBro APP 的基本功能介紹。還有很多可以調整的功能在設定中，就等待下次有空再詳述了。希望這篇文章能讓你更加知道如何使用這個看起來不太一樣的瀏覽器。\n","permalink":"https://plateaukao.github.io/blog/posts/einkbro-app-%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%95%99%E5%AD%B8/","summary":"\u003cp\u003eEinkBro 是一個專門為 Eink 電子紙閱讀器開發的瀏覽器 APP。由於介面長得跟一般的瀏覽器 APP 很不一樣，而且又有很多按鈕可以按，和很多設定可以調整，所以寫了這篇文章來介紹一下它。首先會講解基本功能和設定。然後在第二部分說明一些更進階的用法，以及我自己在使用上的技巧。\u003c/p\u003e","title":"EinkBro APP 基本使用教學"},{"content":"用文石閱讀器內建的 neo reader 來看 pdf 格式的書籍，堪稱是目前閱讀器中功能最強的(可以自行裁邊，一頁分多區塊閱讀，加強畫面對比度，去除背景浮水印，變成流式方式閱讀等等)；但是用來閱讀最常見的 epub 格式電子書時，還是有些不夠完美的地方。其中一點是，一直到 v3.2.1 之前，都還無法將書籍改為直排閱讀。以中文書的閱讀習慣，我個人相當偏好直排閱讀。無奈之下，平常要做大量閱讀時，還是會改用博閱的 Likebook Mars ，Likebook Mars 的直排閱讀就做得很好。\n不過，今年開始，文石陸續將市面上的機種升級到新的韌體 3.2.1；其中終於加入了直排閱讀的功能，我也開始較常使用文石的閱讀器來看書。不過，還是注意到一點不夠完善的地方：neo reader 常常無法更換 epub 電子書中的字型。\n這問題跟電子書裡的字型格式設定有關。如果電子書沒有特別指定字型的話，neo reader 就能自在地依使用者喜好切換成其他字體。但如果電子書為了讓內容有不同的區格而針對不同段落使用不同字型的話，就可能造成文石 BOOX 閱讀器無法正常的切換字體。我在 Nova3 Color, Max3 和 Note3 上都遇到了這個問題；所以這應該是 neo reader 的通病，而不是某個版本的問題。\n雖然可以直排閱讀了，但要我長時間用細圓體(文石閱讀器內建使用的字型是方正准圓體)閱讀電子書，我實在是做不到。在經過一番測試，和臉書社團上的網友指導下，終於找到一個比較方便的作法：可以在小小的改動電子書後，讓 BOOX 閱讀器能完美的調整電子書字型。\n作法\n首先，要在電腦上安裝 Calibre 軟體。這是一套很強大的電子書管理修改閱讀系統，在 Windows 或 Mac 上都可以免費下載使用。\ncalibre - Download calibre\n下載後，可以把自己的電子書先拉到這個軟體中。以後也可以利用它很方便地把書籍同步進閱讀器裡。\nCalibre 畫面\n舉例來說，我想在閱讀器上閱讀”極限返航”這本書，而且我發現無法正常的在 neo reader 中調整字型。我可以先在 Calibre 中選取這本書，然後按右鍵，選擇”Edit Book”。\n這時會看到一個新視窗跳出，左邊有一堆看不懂的文件列表。這些都不用去管它。我們只要確認在工具列上有一個 TTF 的按鈕，點它就對了。\nEdit Book\n按下 TTF 按鈕後，就會看到一列字型。這些就是”極限返航”這本書裡，書商有指定的字型。有些字型有內建在書中，有些則是仰賴閱讀器系統提供。如果連閱讀器中也沒有的話，就會用閱讀器的預設字型呈現。\nManage Fonts\n要讓 neo rader 能正常調整字型的方式是：把上面的字型(沒有內建在書裡的)都刪掉！不過，全刪掉的話，這本書的字型格式不就都不見了嗎？沒錯，確實如此。\n一個比較保險的方式是，只刪掉 serif 和 Serif。這兩個字型通常是用來顯示書中的主要內容，而且是依賴閱讀器幫忙提供的。在文石的機器上，就是全成了方正准圓體。只要刪掉這兩個後，應該大部分的書籍內容就可以依照喜好更改。\n在 “Manage Fonts” 這個視窗刪完字型後，記得在 Edit Book 這個畫面點選工具列上的第三個按鈕，把剛剛做過的更動儲存。\n這時，就可以把這本書再傳一次到閱讀器上，應該就可以正常地調整字型了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3onyx-boox-%E9%9B%BB%E5%AD%90%E6%9B%B8%E9%96%B1%E8%AE%80%E5%99%A8%E7%9A%84%E5%AD%97%E5%9E%8B%E8%A8%AD%E5%AE%9A%E5%B0%8F%E6%8A%80%E5%B7%A7/","summary":"\u003cp\u003e用文石閱讀器內建的 neo reader 來看 pdf 格式的書籍，堪稱是目前閱讀器中功能最強的(可以自行裁邊，一頁分多區塊閱讀，加強畫面對比度，去除背景浮水印，變成流式方式閱讀等等)；但是用來閱讀最常見的 epub 格式電子書時，還是有些不夠完美的地方。其中一點是，一直到 v3.2.1 之前，都還無法將書籍改為直排閱讀。以中文書的閱讀習慣，我個人相當偏好直排閱讀。無奈之下，平常要做大量閱讀時，還是會改用博閱的 Likebook Mars ，Likebook Mars 的直排閱讀就做得很好。\u003c/p\u003e","title":"文石(Onyx) BOOX 電子書閱讀器的字型設定小技巧"},{"content":"2022/04/20\n틱톡의 허위정보와 공익[IT칼럼]\n틱톡의 허위정보와 공익\n抖音的虛假信息和公益\n허위정보: 假情報\n틱톡의 위세가 대단하다. 지난해 구글의 월 방문자 수를 넘어서더니, 올해엔 예상 광고매출액이 트위터와 스냅챗을 합친 것보다 더 클 것이라는 전망까지 나왔다. 아직 페이스북의 그것엔 미치지 못하지만, 이 기세라면 머지않은 시간에 페이스북을 역전할 것으로 보인다. 국내에서도 사용자가 이미 500만명을 훌쩍 넘어서면서 ‘대세 영상앱’으로 자리매김하는 중이다.\n抖音聲勢浩大。 去年月訪問人數超過了谷歌的月訪問人數,甚至有人預測今年的廣告銷售額將比推特和Snapchat的總和還要大。 雖然還不及facebook,但照此氣勢,預計不久後facebook將會逆轉。 在韓國國內,用戶已超過500萬人,正在成爲”大勢視頻應用程序”\n위세(威勢): 聲勢\n합치다: 合一\n전망: 展望(預測)\n미치다: 到達\n역전: 逆轉\n훌쩍: 輕輕地飛過\n대세(大勢): 人多\n2022/04/19 지난해 45억원 받은 네이버 연봉킹 \u0026lsquo;이 남자\u0026rsquo;, 결국..\n지난해 45억원 받은 네이버 연봉킹 ‘이 남자’, 결국..\n去年拿了45億韓元的naver年薪王”這個男人”，結果…\n연봉킹: 年薪王\n네이버 창업자인 이해진 글로벌투자책임자(GIO)의 최측근 중 하나인 최인혁 전 네이버 최고운영책임자(COO)가 해피빈재단 대표에서 사퇴했다. 이로써 네이버와 관련한 모든 직책에서 물러나게 됐다. 그는 지난해 발생한 직장 내 괴롭힌 사건에 대한 책임을 지고 COO, 네이버파이낸셜 대표 등의 직을 그만뒀다. 지난해 그는 45억원 이상의 보수를 챙기며 네이버 ‘연봉킹’으로 등극한 바 있다.\nNaver創始人、全球投資負責人李海鎮(GIO)的親信之一、前Naver首席運營官(COO)崔仁赫(音)辭去了Happy Bean財團代表一職。 至此,他辭去了與naver相關的所有職務。 他辭去了COO、Naver Financial代表等職務,對去年發生的職場欺凌事件負責。 去年,他獲得了45億韓元以上的報酬,成爲了naver的”年薪王”\n창업자: 創辦人\n사퇴: 辭退\n직장 내 괴롭: 職場欺凌\n2022/04/18 뭐 먹지? 메뉴 고르는 뇌의 작동 원리, 인간이나 원숭이나 같다[신경과학 저널클럽]\n뭐 먹지? 메뉴 고르는 뇌의 작동 원리, 인간이나 원숭이나 같다\n吃什麼呢？選擇菜單的大腦運作原理，人和猴子一樣\n주말이 가는 것이 아쉬운 일요일 저녁, 허전한 마음을 달래는 방법 가운데 하나로 야식이 선택되곤 한다. 이런 마음을 꾹 억누르는 이들도 있겠지만, 일단 배달 앱을 켜보자. 꼭 뭔가를 시켜먹자는 것은 아니고, 두뇌의 작동 원리를 알아보기 위해서다. 야심 찬 서비스를 제공한다는 새로운 업체도 보이고, 믿고 먹는 단골집도 눈에 들어온다. 긴 한 주를 다시 시작해야 할 나를 위해 어떤 선택이 최선일까.\n對於週末的過去感到遺憾的週日晚上，夜宵經常被選爲撫慰空虛心情的方法之一。 雖然也有壓抑這種心情的人,但先打開外賣應用程序吧。 並不是一定要點東西吃，而是爲了瞭解大腦的運作原理。 既有提供雄心勃勃服務的新企業,也有值得信賴的常客。 爲了重新開啓漫長的一週的我，選擇什麼纔是最佳選擇呢？\n최선(最善): 最佳\n2022/04/17 [여적] 우크라이나 엄마 작가의 \u0026lsquo;전쟁일기\u0026rsquo;\n우크라이나 엄마 작가의 ‘전쟁일기’\n우크라이나: 烏克蘭\n전쟁: 戰爭\n또 다른 감동적 ‘일기’가 책으로 나왔다. 전쟁의 참혹함을 겪고 있는 우크라이나의 유명 그림책 작가인 올가 그레벤니크의 \u0026lt;전쟁일기\u0026gt;(정소은 옮김·이야기장수 간)다. 이 책은 그레벤니크가 2월부터 3월까지 다이어리에 일기로 기록한 글과 그림이다. 그의 일기는 ‘새벽 5시30분, 폭파 소리에 잠에서 깨어났다’(2월24일)로 시작된다. 이후 폭격을 피한 지하실 생활, 아홉 살 아들·네 살 딸과 해외 피란, 남편·어머니와의 생이별, 난민으로 머무는 불가리아에서의 삶이 날것으로 실렸다. 전쟁 전 화려한 색감·환상적 그림으로 주목받은 그다. 지금은 오직 연필 한 자루로 ‘하루아침에 무너진 삶’을 쓰고 그린다.\n另一本感人的”日記”被寫成書。 這是正在經歷戰爭殘酷的烏克蘭著名畫冊作家奧爾加•格雷文尼克的《戰爭日記》(鄭素恩譯•故事作者之間)。 這本書是格雷文尼克從2月到3月在日記本上記錄的文章和圖片。 他的日記以《凌晨5點30分，被爆炸聲驚醒》（2月24日）開始。 之後避開轟炸的地下室生活、9歲的兒子、4歲的女兒和海外避難、與丈夫、母親的生離死別、作爲難民停留的保加利亞生活等。 他在戰爭前以華麗的色彩和夢幻般的畫作備受矚目。 現在只用一支鉛筆寫和描繪”一夜之間崩潰的人生”。\n참혹함을 겪고: 經歷殘酷\n새벽:凌晨\n화려한: 華麗的\n오직: 只\n무너지다: 崩潰\n2022/04/16 블루보틀도 꼼짝 못하는 실리콘밸리 최애 커피 [정혜진의 Whynot 실리콘밸리]\n블루보틀도 꼼짝 못하는 실리콘밸리 최애 커피\n藍瓶也無法動彈的矽谷最愛咖啡\n꼼짝 못하는: 動彈不得的\n최애: 最愛\n구글, 애플, 링크드인, 트위터 같은 기업들은 사내에서 어떤 커피를 마실까요. 아침이 유독 긴 실리콘밸리 지역에서 빅테크 직원들이 연료삼아 흡수하는 이 커피 브랜드가 있습니다. 마이크 슈뢰퍼 전 메타 플랫폼(옛 페이스북) 최고기술책임자(CEO)를 비롯해 빅테크의 유명 인사들이 직접 엔젤 투자를 하기도 한 브랜드 ‘필즈 커피’입니다.\n谷歌、蘋果、LinkedIn、Twitter等企業會在公司內部喝什麼咖啡呢? 在早晨特別長的硅谷地區,有大型科技職員們作爲燃料吸收的這個咖啡品牌。 這是包括前Meta平臺(前Facebook)首席技術官(CEO)邁克•施若珀在內的BigTech的知名人士親自投資天使的品牌”Fields Coffee”\n유독(唯獨) 긴: 特別漫長的\n연료(燃料)삼아: 做為燃料\n팬데믹 이전에 애플 직원들은 재택 근무를 뜻하는 ‘WFH(Working from home)’ 대신 ‘WFP’라는 말을 종종 썼다고 합니다. 필즈 커피에서 일하고 있다는 의미라고 하는데요. 미국 캘리포니아주 쿠퍼티노에 있는 본사에서 횡단보도 하나를 사이에 두고 있을 만큼 가까운 필즈 커피 쿠퍼티노 지점에서 일하는 애플 직원들을 종종 발견할 수 있는 이유이기도 합니다. 이들은 필즈의 바리스타에 해당하는 아티스트가 컵 주문마다 하나씩 ‘푸어 오버’ 방식으로 내린 커피를 마시며 필즈 특유의 자유분방한 분위기에서 일을 합니다.\n據說,在大流行之前,蘋果職員經常使用”WFP”一詞,而不是意味着在家辦公的”WFH(Working from home)” 意思是在菲爾茲咖啡公司工作。 這就是爲什麼在位於美國加利福尼亞州庫比蒂諾的總部,經常能看到在附近的菲爾茲咖啡庫比蒂諾分店工作的蘋果職員的原因。 他們喝着相當於菲爾茲咖啡師的藝術家們每點一個杯子,以”pure over”的方式喝着咖啡,在菲爾茲特有的自由奔放的氛圍中工作。\n종종(種種): 常常，不時\n횡단보도(橫斷步道):十字路口\n지점(支店): 分店\n이렇듯 실리콘밸리의 ‘로컬 커피’로 사랑을 받다 보니 네슬레가 블루보틀을 2017년 4억2500만 달러(약 5000억원)에 인수하면서 커피 왕국 건설에 도전장을 내밀었지만 막상 샌프란시스코를 비롯한 베이 에어리아에서는 독점적 지위가 쉽지 않은 상황입니다. ‘실리콘밸리의 최애(Darling of Silicon Valley)’라는 애칭의 주인공인 필즈 커피는 2002년 샌프란시스코 미션 스트릭트 내 24번가에 처음 문을 열었습니다. 시기가 절묘했습니다. 당시 클라우드 솔루션 기업 세일즈포스가 막 3년차를 맞았을 때였고요. 트위터, 에어비앤비, 우버 등 기업이 문을 열기도 전이었습니다. 스타트업 붐이 일면서 학생들과 개발자들이 몰려들면서 이들이 모여서 일을 하거나 네트워킹을 하고 투자 계약을 성사시킬 곳이 필요했는데요. 이들은 필즈만의 독특한 문화(에스프레소가 없는 카페)와 철학(만족할 때까지 제조 후 결제)에 끌렸습니다. 이후 필즈 커피가 각종 테크업계 역사의 무대가 됐습니다.\n像這樣,由於矽谷的”本地咖啡”受到人們的喜愛,雀巢在2017年以4億2500萬美元(約5000億韓元)收購了Blue Bottle,向建設咖啡王國發出了挑戰書,但在舊金山等北愛麗亞地區,其壟斷地位並不容易。 被稱爲”矽谷最愛(Darling of Silicon Valley)”的主人公菲爾茲咖啡於2002年在舊金山Mission Strict內24號街首次開業。 時機很妙。 當時雲解決方案企業Salesforce剛迎來第3年。 推特、Airbnb、優步等企業尚未開業。 隨着創業熱潮的興起,學生和開發者蜂擁而至,需要他們聚在一起工作或聯網,成功簽訂投資合同的地方。 他們被Fields獨有的文化(沒有濃縮咖啡的咖啡廳)和哲學(直到滿意爲止,製造後結算)所吸引。 之後菲爾茲咖啡成爲了各種技術行業的歷史舞臺。\n도전장(挑戰狀): 挑戰書\n막상: 實際上\n2022/04/15 \u0026ldquo;돌아와요, 사무실로..\u0026rdquo; 엔데믹에 막 내리는 재택근무, \u0026lsquo;출근 묘안\u0026rsquo; 찾는 테크기업들\n“돌아와요, 사무실로..” 엔데믹에 막 내리는 재택근무, ‘출근 묘안’ 찾는 테크기업들\n“回來吧，回到辦公室……”在大流行病剛降臨時的居家辦公，尋找”上班妙策”的科技企業們\n재택근무: WFH\n직원들 재택 선호 분위기 강해\n인력 이탈 우려에 신중하게 추진\n職員們喜歡在家的氛圍很強烈\n因擔心人力流失，慎重推進\n신중하게: 慎重的\n출근하면 음식 무료 제공, 퇴근하면 회사 인근에서 콘서트와 맥주 시음회. 이런 ‘당근’으로 재택근무에 익숙한 직원들을 회사로 불러들일 수 있을까.\n上班免費提供食物，下班後在公司附近舉行演唱會和啤酒品嚐會。 這樣的”胡蘿蔔”能把熟悉在家辦公的職員叫到公司嗎?\n인근: 鄰近\n시음회: 試飲會\n익숙한: 熟悉的\n코로나19 대유행이 시작되면서 발 빠르게 재택근무제를 도입했던 정보통신기술(ICT) 기업들이 이제는 사무실 출근을 검토하고 있다. ‘비대면 회의’로는 부서 간 협업 효과 등을 극대화하는 데 한계가 있다고 보기 때문이다. 하지만 직원들은 지난 2년간 생산성과 효율성에 큰 차이가 없었다며 사무실 출근을 반기지 않는 분위기다.\n新型冠狀病毒 隨着大流行的開始,曾迅速引進在家辦公制的信息通信技術(ICT)企業現在正在考慮到辦公室上班。 因爲他們認爲”非面對面的會議”在最大化部門間的合作效果等方面存在侷限性。 但是職員們表示，在過去的2年裏，生產效率和效率性沒有太大差異，對上班並不歡迎。\n대유행: 大流行\n부서(部署): 部門\n한계(限界): 限制\n반기지 않는: 不歡迎，不喜歡\n14일 업계에 따르면 국내 최대 정보통신(IT) 기업인 네이버는 오는 6월 이후에는 사무실 출근과 재택근무를 병행하는 ‘하이브리드 근무’를 실시할 것으로 보인다. 네이버가 최근 본사 직원을 대상으로 설문조사를 한 결과 주 5일 재택근무를 원하는 직원이 41.7%, 하이브리드 근무제(주 1~4회 사무실 출근)를 원하는 직원은 52.2%였다. 최수연 대표는 지난 13일 기자간담회에서 “직원들에게 재택근무 선택지를 주되 만나서 협업해야 하는 부분도 있는 만큼 하이브리드 근무 방식 등을 고민 중”이라고 밝혔다.\n14日,據業界透露,國內最大的信息通信(IT)企業naver將於今年6月以後實施辦公室上班和在家辦公並行的”混合工作” Naver最近以總公司職員爲對象進行的問卷調查結果顯示，希望每週5天在家辦公的職員佔41.7%，希望實行混合工作制（每週1~4次到辦公室上班）的職員佔52.2%。 崔秀妍(音)代表13日在記者招待會上表示:”給職員在家辦公的選擇,但也有需要見面合作的部分,因此正在考慮混合工作方式等。”\n본사: 總部\n기자간담회: 記者招待會\n고민 중: 考慮中\n(이)라고 밝혔다: 表示\n2022/04/14 아시아 국민앱 \u0026lsquo;라인\u0026rsquo;이 찾는 사람은?\n아시아 국민앱 ‘라인’이 찾는 사람은?\n亞洲國民應用程序”line”尋找的人(才)是？\n대만과 일본에서 ‘국민앱’으로 불리는 메신저 ‘라인’을 운영하는 ‘라인플러스’ 얘기다. 현금 결제를 선호하는 일본 시장에서 라인의 모바일 선물하기 기능을 사용한 이들이 지난 3월 기준 2200만명을 넘겼다니, 그 인기가 짐작이 된다.\n這是在臺灣和日本運營被稱爲”國民應用程序”的聊天工具”line”的”line plus” 在喜歡現金結算的日本市場,使用LINE手機送禮功能的人以今年3月爲準超過了2200萬人,其人氣可想而知。\n불리는: 被稱為\n선호하는(選好): 偏好 ~ 的\n짐작(斟酌)이 된다: 可想而知\n라인의 글로벌 조직에서 플랫폼 개발을 맡고 있는 신동식입니다. 라인이 대만에서 EZ 스토어와 함께 운영하고 있는 서비스 중에 라인 기프트숍이 있어요. 카카오톡 선물하기 기능을 생각하시면 됩니다. 실물 상품이 아닌 바우처, 쿠폰, 상품권 같은 것을 선물로 주고받는 서비스예요. 이 서비스가 원활하게 운영되게 하는 개발 일을 하고 있습니다.\n我是LINE全球組織中負責平臺開發的申東植。LINE在臺灣與EZ商店一起運營的服務中有LINE禮品店。 想一想kakao talk送禮功能就可以了。 不是實物商品，而是作爲禮物贈送券、優惠券、商品券等的服務。 我正在做開發工作，使這項服務能夠順利運行。\n맡고 있는: 負責 ~\n바우처: voucher\n주고받는: 交換(的)\n원활(圓滑): 順利\n2022/04/13 https://news.v.daum.net/v/20220413012502717\n퀄컴에 ‘안방의 75%’ 내준 삼성 반도체의 어두운 현실[실리콘밸리나우]\n三星半導體將75%的”主場”拱手讓給高通，黑暗的現實[硅谷NOW]\n어둡다: 暗\n세계적인 반도체 기업 퀄컴(QUALCOMM)의 크리스티아노 아몬 최고경영자(CEO)가 최근 미국 경제방송 CNBC에 출연해 삼성전자에 대해 이야기했다. 먼저 ‘애플의 강력한 경쟁업체’라고 삼성전자를 띄워준 아몬 CEO는 갤럭시 스마트폰에 들어간 퀄컴 AP(애플리케이션프로세서)의 점유율을 공개했다. AP는 스마트폰의 두뇌 역할을 하는 반도체로 퀄컴의 주력 제품이다. 고객사(삼성전자) 제품의 자사 부품 점유율에 대해 납품업체가 얘기하는 건 흔치 않은 일이다.\n世界級半導體企業高通(QUALCOMM)的首席執行官(CEO)克里斯蒂亞諾•阿蒙最近參加了美國經濟廣播CNBC節目,談論了三星電子。 首先,被稱爲”蘋果強有力的競爭企業”的三星電子CEO阿蒙公開了進入Galaxy智能手機的高通AP(應用程序處理器)的佔有率。 AP是起到智能手機大腦作用的半導體,是高通的主力產品。 供貨企業談論顧客公司（三星電子）產品的自有零部件佔有率，實屬罕見。\n출연: 出演\n점유율: 佔有率\n두뇌: 大腦\n흔치 않은 일이다: 少見的事 (珍しいことだ。)\n2022/04/12 \u0026ldquo;넷플릭스 잡아라\u0026rdquo; 워너미디어·디스커버리 합병 \u0026lsquo;공룡 미디어\u0026rsquo; 탄생\n“넷플릭스 잡아라” 워너미디어·디스커버리 합병 ‘공룡 미디어’ 탄생\n“抓住Netflix吧” 華納媒體· Discovery 合併誕生”恐龍媒體”\n합병: 合併\n공룡: 恐龍\n작년 5월 두 회사의 합병 소식이 나온 후 1년이 지나 합병이 완료된 것으로 워너미디어가 미 최대 통신회사 AT\u0026amp;T에서 분리돼 디스커버리에 합쳐진 형태다. WBD는 이번 합병으로 영화 ‘해리포터’ 시리즈부터 보도전문 채널 CNN, 드라마 ‘왕자의 게임’으로 유명한 HBO 채널 등을 거느리게 됐다. 뉴욕타임스는 “할리우드에 새로운 거인이 생겼다”고 보도했다.\n自去年5月兩家公司合併的消息傳出後,經過1年時間完成了合併,華納媒體從美國最大的通信公司AT\u0026amp;T分離出來,合併到Discovery中。 WBD通過此次合併,將擁有電影《哈利波特》系列、報道專門頻道CNN、以電視劇《王子的遊戲》而聞名的HBO頻道等。 《紐約時報》報道說:”好萊塢出現了新的巨人。”\n거인: 巨人\n미디어 시장 변화를 노리는 곳엔 아마존도 있다. 아마존은 지난달 할리우드 제작사인 MGM 인수를 완료했다. 인수가격은 85억달러(10조3000억원)다. 아마존이 MGM을 사들인 이유는 자사 온라인 스트리밍 서비스(OTT)인 ‘아마존프라임 비디오’를 강화하기 위해서다. 작년 말 기준 아마존프라임의 전세계 가입자는 1억7500만명으로 알려졌는데, 이번 MGM 인수로 콘텐츠가 늘어나면 세계 1위 OTT 업체인 넷플릭스와 본격적인 경쟁이 가능할 것이라는 전망도 나온다. 우선 아마존은 MGM의 간판이던 007시리즈를 활용한 리얼리티쇼 ‘007 로드 투 어 밀리언’을 제작하기로 했다. 100만파운드 상금을 걸고 일반 참가자들이 영화 007시리즈의 주인공인 제임스 본드처럼 게임하며 우승자를 가리는 것이다. 이 프로그램은 아마존프라임 비디오에서 방영된다.\n瞄準媒體市場變化的地方還有亞馬遜。 亞馬遜上個月完成了好萊塢製作公司MGM的收購。 收購價格爲85億美元。 亞馬遜購買MGM的理由是爲了強化本公司的在線流媒體服務(OTT)”亞馬遜prime video” 據悉,以去年年末爲準,亞馬遜prime的全球用戶達1.75億人,如果通過此次收購MGM內容增加,有望與世界排名第一的OTT企業Netflix展開正式的競爭。 首先,亞馬遜決定利用MGM的招牌007系列製作真人秀《007 Road to a Million》。 以100萬英鎊的獎金爲賭注，普通參賽者像電影007系列的主人公詹姆斯·邦德一樣，通過玩遊戲來決出冠軍。 這個節目在亞馬遜Prime Video上播放。\n인수: take over\n사들: buy in\n본격적인: 本格的\n리얼리티쇼: 真人秀\n애플도 최근 동영상 스트리밍 시장에서 영향력을 행사하고 있다. 애플은 넷플릭스나 디즈니+와 수적인 콘텐츠 경쟁이 아닌, 질적 경쟁을 벌이겠다는 전략이다. 애플의 OTT 서비스인 애플TV+가 제작한 오리지널 영화 ‘코다’는 올해 아카데미 시상식에서 최우수 작품상을 수상했다. CNBC는 “애플TV+의 목표는 넷플릭스 수준의 구독자 확보가 아니다”라며 “애플의 관심사는 애플TV+가 아카데미상이나 에미상 후보작처럼 고전이 될 만한 우수작을 모아놓은 서비스가 되려는 것”이라고 분석했다. 틈새 시장에서 강점을 발휘해 사용자들이 찾는 OTT 서비스를 만들고, 이를 통해 애플 생태계를 확대하겠다는 전략이다.\n蘋果最近也在視頻流媒體市場行使着影響力。 蘋果的戰略不是與Netflix或迪斯尼+進行數量上的內容競爭,而是展開質量競爭。 蘋果的OTT服務蘋果TV+製作的原創電影《Coda》在今年奧斯卡頒獎典禮上獲得了最優秀作品獎。 CNBC分析稱:”蘋果TV+的目標不是確保Netflix水平的訂閱者,蘋果關心的是蘋果TV+要成爲像奧斯卡獎或艾美獎候選作品一樣能夠成爲經典的優秀作品的服務。” 其戰略是,在縫隙市場發揮優勢,打造用戶尋找的OTT服務,並以此擴大蘋果生態界。\n수적인: 數量上\n질적: 質量上\n생태계: eco-system\n2022/04/11 [단독] 네이버가 투자한 유럽 스타트업 7곳, 유니콘 등극\n[단독] 네이버가 투자한 유럽 스타트업 7곳, 유니콘 등극\n[單獨] NAVER投資的7家歐洲創業企業成爲獨角獸企業\n단독(單獨): 獨家\n등극(登極): 登基\n◆ 진격의 네이버 ◆\n進擊的 naver\n네이버가 초기 투자한 유럽 스타트업 7곳이 기업가치 1조원을 넘는 ‘유니콘’ 기업으로 성장한 것으로 확인됐다. 전 세계로 사업 영토를 넓히고 유망 벤처를 발굴하기 위해 유럽 투자 펀드를 결성한 지 6년 만에 달성한 성과다.\n經確認,naver初期投資的7家歐洲創業企業成長爲企業價值超過1萬億韓元的”獨角獸”企業。 這是爲了向全世界擴大事業領土,發掘有潛力的風險投資公司,在成立歐洲投資基金6年後取得的成果。\n넓히고: 擴大(範圍)\n유망(有望): 有前途的\n발굴하다: 發掘\n유럽 시장에서 상당한 영향력과 인지도를 가진 현지 유력 스타트업에 대한 지분 투자와 사업 협력은 네이버가 현지 영향력을 키우는 데 마중물 역할을 할 수 있다. 또한 네이버의 현지 사업이 잘될수록 스타트업에 대한 네이버 지분 가치도 자연스럽게 커질 수 있어 ‘윈윈’ 모델이 될 수 있다는 계산이다.\n對在歐洲市場具有相當大的影響力和知名度的當地知名創業企業的股份投資和事業合作,對Naver擴大當地影響力起到引水作用。 另外,Naver在當地的事業越成功,Naver對創業公司的股份價值就越大,因此有可能成爲”雙贏”的模式。\n영향력: 影響力\n현지(現地): 當地\n마중물: 引水\n윈윈: Win Win\n2022/04/10 \u0026ldquo;한정판이래\u0026rdquo; 15초만에 완판 행렬..NFT에 푹 빠진 \u0026lsquo;MZ 펀슈머\u0026rsquo;\n“한정판이래” 15초만에 완판 행렬..NFT에 푹 빠진 ‘MZ 펀슈머’\n“說是限量版”15秒內售罄的隊伍….沉迷於NFT的”MZ Funsumer”\n한정판(限定版): 限量版\n완판(完販): 完售\n펀슈머(fun consumer): 指共享趣味和快乐并消费的顾客\nMZ세대(밀레니얼+Z세대)의 소비 키워드는 ‘재미’다. 단순 상품 구매를 떠나 재미를 추구하는 ‘펀슈머’(Funsumer) 기질이 강한 MZ세대는 상품의 가치를 따져 구매 우선순위를 매긴다. 희소성을 띠는 ‘대체불가토큰’(NFT)이 MZ세대에게 매력적으로 다가오는 이유다.\nMZ一代(千禧年+Z一代)的消費關鍵詞是”樂趣” 拋開單純購買商品,追求樂趣的”Funsumer”氣質較強的MZ一代會根據商品的價值來確定購買優先順序。 這就是具有稀有性的”不可替代的代幣(NFT)”對MZ一代具有魅力的原因。\n떠나: 離開，脫離\n추구하다: 追求\n기질이 강한: 気質の強い\n희소성(稀少性): 稀缺性\nNFT는 기업 마케팅 수단으로 적극 활용되며 MZ 소비자를 몰고 있다. 에버랜드, 기아차, 젝시믹스 등 국내 브랜드뿐 아니라 루이비통, 구찌 등 글로벌 명품 기업들도 잇따라 NFT 상품을 출시하고 있다. 코로나19(COVID-19) 시대 비대면 소비가 증가하는 틈새로 ‘MZ 펀슈머’를 선점하려는 NFT 마케팅이 새로운 트렌드로 자리잡고 있는 것이다.\nNFT積極利用爲企業營銷手段,吸引着MZ消費者。 不僅是愛寶樂園、起亞汽車、Xexymix等國內品牌,路易威登、古馳等全球名牌企業也接連推出了NFT商品。 在新型冠狀病毒(COVID-19)時代,試圖在非對面消費增加的縫隙中搶佔”MZ Funsumer”的NFT營銷正在成爲新的趨勢。\n수단으로: 做為手段\n뿐 아니라: 不僅\n잇따르다: (引ひき)続つづく, 相次あいつぐ\n비대면(非對面): 非面對面\n선점하려는: 取得先機 (先取りしようとする)\n2022/04/09 구글 몽니 OTT는 \u0026lsquo;나 몰라라\u0026rsquo;..결국 소비자 지갑 열어 수수료 낸다\n구글 몽니 OTT는 ‘나 몰라라’..결국 소비자 지갑 열어 수수료 낸다\n몽니:耍賴\n‘인앱결제 강제방지법’을 둘러싼 구글과 한국 정부간 갈등이 고조되고 있다. 빅테크의 일방적 수수료 정책에 제동을 건 ‘세계 최초’ 입법으로 평가받았지만, 법의 허점을 노린 구글의 반격과 정부의 규제의지가 ‘2라운드’로 번지는 흐름이다. 입앱결제 강제 논란을 둘러싼 갈등과 법의 한계, 궁극적 해법을 짚어본다.\n인앱결제 강제방지법: 強制 in-app purchase 防止法\n고조: 高潮\n일방적: 單方面的\n허점: 弱點\n흐름이다: 流動，蔓延\n궁극적: 終極的\n2020年韓國國內 app 市場的各家市佔率 from Daum.net\nGoogle: 66.5%\nApple: 21.5%\nOne Store: 11.7%\n6일 모바일 콘텐츠 업계에 따르면, 주요 온라인동영상서비스(OTT)와 음원 플랫폼 등은 이달 초 인앱결제 도입에 따른 수수료 인상 여파로 잇달아 서비스 가격을 올렸다.\n온라인동영상서비스: OTT\n음원 플랫폼: 線上音樂服務 (platform)\n여파: 余波 (after effect)\n방송통신위원회가 구글 인앱결제 의무화 조치에 제동을 걸었지만, 이미 안드로이드 앱에서 인앱결제를 도입하고 요금을 인상한 국내 모바일 콘텐츠 업계는 ‘당장 되돌릴 수는 없다’는 입장이다. 수수료를 낮춘 외부결제가 단시간 내 허용되긴 어려운 만큼, 구글 가이드라인이 바뀔 때까지는 요금 인상 폭을 그대로 유지할 계획이다. 결과적으로는 늘어난 수수료 부담을 그대로 소비자에게 떠넘기는 셈인데, 콘텐츠 업계가 당장의 이익을 위해 소비자를 볼모로 삼았다는 지적이 나온다.\n제동(制動): 阻止\n입장: 立場\n단시간 내: 短時間內\n유지: 維持\n지적이 나온다: 指摘\n셈인데: 等於是，算是\n2022/04/08 억대 연봉, 황금빛 미래 보장?..IT업계 허와 실\n억대 연봉, 황금빛 미래 보장?..IT업계 허와 실\n億台の年俸、黄金色の未来保障？IT業界の虚実\n연봉: 年薪\n보장: 保障\n허와 실: 虛實\n직장생활에서 월급이 얼마나 중요할까요? 누군가는 꿈을 위해 지금 주머니는 조금 가벼워도 괜찮다고 여길 것이고, 다른 누군가는 이리저리 치이는 인간관계에 지쳐 돈은 덜 벌더라도 좋은 사람들과 일하고 싶다고 생각할 겁니다.\n在職場生活中工資有多重要？有些人爲了夢想會認爲現在口袋稍微輕一點也沒關係，有些人則會因爲到處亂跑的人際關係而疲憊，即使少掙錢，也會想和好人一起工作。\n주머니: 口袋\n다고 여길 것: 當做，以為\nIT업계는 전통적으로 ‘3D 업종’으로 손꼽혀왔습니다. 과거엔 더럽고(dirty) 어렵고(difficult) 위험한(dangerous) 분야의 일로 받아들여진 거죠. 야근을 밥 먹듯이 하고 업무도 힘든데다 박봉인 업종으로 인식됐습니다.\n“IT行業歷來被認爲是”3D行業”。過去被認爲是髒、難（dirty）、危險（difficult）、危險（dangerous）領域的事情。加班像吃饭一样,工作也很累,而且被认为是低工资行业。\n손꼽혀왔습니다: 屈指可數\n밥 먹듯이 하다: 跟吃飯一樣\n박봉(薄俸): 低薪\n카카오의 1인당 평균 보수는 2020년 1억800만원 수준이었는데 이번 한 해에만 60% 가까이 뛰면서 평균 연봉 상단에 있던 SK텔레콤, 삼성전자, 네이버 등을 가뿐히 제쳤습니다. 스톡옵션 행사 영향이 컸는데 신정환 전 최고기술책임자(CTO)의 경우 스톡옵션 행사로만 121억6800만원을 벌었습니다. CTO란 회사의 기술 개발을 총괄하는 경영자로, 그는 지난해 카카오 임직원 중 가장 많은 보수를 받은 임원이기도 합니다.\nKAKAO的人均報酬在2020年是1.08億韓元，但僅今年一年就增加了近60%，輕鬆超過了平均年薪上端的SK電信、三星電子、Naver等。股票期權行使的影響很大，但前首席技術官（CTO）申正煥僅股票期權就賺了121億6800萬韓元。CTO是總管公司技術開發工作的經營者，也是去年可可員工中報酬最高的管理人員。\n1인당 평균 보수: 人均報酬\n상단: 前段班\n가뿐히: 輕輕鬆鬆地\n최고기술책임자(最高技術責任者): CTO\n총괄하다: 總管\n2022/04/07 달라진 카카오의 \u0026lsquo;신뢰회복\u0026rsquo;.. 상생기금 3000억 원, 소상공인·개인창작자 지원에 쓴다\n달라진 카카오의 ‘신뢰회복’.. 상생기금 3000억 원, 소상공인·개인창작자 지원에 쓴다.\n달라진:改變了的\n소상공인: 小企業主\n개인창작자:個人創作者\n카카오가 향후 5년간 3,000억 원을 상생기금으로 투입한다. 그동안 각종 논란으로 꼬리표처럼 따라왔던 ‘비호감’ 주홍글씨를 떼고 사회적인 신뢰 회복에 주력하겠다는 뜻으로 읽힌다.\n향후: 今後\n상생기금: 相生基金 (互助基金)\n꼬리표: 標籤\n비호감: 非好感\n주력하다 (注力): 專注於\n한편 이날 카카오는 올해는 글로벌 시장 진출 전략의 구체적인 청사진도 공개했다. 주요 계열사들이 참여하는 ‘글로벌 시너지 태스크포스(TF)’를 조직해 카카오픽코마와 카카오엔터테인먼트, 카카오게임즈 등을 중심으로 계열사 간 시너지를 통해 일본과 유럽, 미국과 아시아 시장을 본격 공략하겠다는 방침이다. 김 의장은 “현재 전체 매출의 10% 정도인 해외 매출 비중을 3년 안에 30%로 확대하고, 올해 카카오 공동체의 해외 매출도 전년 대비 40% 이상 끌어올리겠다”고 강조했다.\n한편: 除此之外，同時\n청사진(青寫真): 藍圖\n본격(本格): 正式\n방침: 方針\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%96%B1%E8%AE%80%E9%9F%93%E5%9C%8B%E6%96%B0%E8%81%9E202204-%E6%AF%8F%E6%97%A5%E6%9B%B4%E6%96%B0/","summary":"\u003cp\u003e2022/04/20\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://news.v.daum.net/v/20220420093922943\"\u003e틱톡의 허위정보와 공익[IT칼럼]\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e틱톡의 허위정보와 공익\u003c/p\u003e\n\u003cp\u003e抖音的虛假信息和公益\u003c/p\u003e\n\u003cp\u003e허위정보: 假情報\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e틱톡의 위세가 대단하다. 지난해 구글의 월 방문자 수를 넘어서더니, 올해엔 예상 광고매출액이 트위터와 스냅챗을 합친 것보다 더 클 것이라는 전망까지 나왔다. 아직 페이스북의 그것엔 미치지 못하지만, 이 기세라면 머지않은 시간에 페이스북을 역전할 것으로 보인다. 국내에서도 사용자가 이미 500만명을 훌쩍 넘어서면서 ‘대세 영상앱’으로 자리매김하는 중이다.\u003c/p\u003e","title":"閱讀韓國新聞(2022/04) — 每日更新"},{"content":"前不久得知，在 Google Play Store 上的萊姆輸入法 (Lime HD) 被下架了。剛好前一陣子因為想要讓萊姆輸入法可以在 Android 平台的電子書閱讀器上有更好的 UI 呈現方式，所以有從萊姆輸入法在 Github 上的 repository 那兒 fork 了一份程式碼來做適度的修改。詳情可以看我上一篇文章：\n改造 Android 上的無蝦米輸入法 — LimeHD\n在初步改完後，已經讓萊姆輸入法大幅度地瘦身了，也拿掉了(我覺得)很多不必要的功能和函式庫，讓它從原來的 10 MB 左右縮小到 4 MB。\n今天是要再來說明一下到目前為止，還有做了哪些功能上的改善；技術細節的話，則是會記錄在下面。先在這兒附上最新版的連結：\nReleases · plateaukao/sweetlime\n新增功能 將目前在打的拼字內容顯示在鍵盤正中間 當鍵盤型式是左右分離式，讓它的尺寸更大一些 長按 123 按鈕時，切換成 9 宮格的數字鍵盤 讓空白鍵更大，適合需要利用空白鍵送出候選字的輸入法 技術細節說明 將拼字內容顯示在鍵盤中間 之所以會有這功能是因為，當我在使用無蝦米輸入法時，除了要看目前手上打的字母對不對，還要時不時的看一下候選字的區域，確認一切輸入是正常的。眼睛這樣子在候選字區域和鍵盤間來回，其實是比較沒有效率的。\n當拼字字母可以顯示在鍵盤中間時，目光可以都注視在中間，然後輸入的字母其實用眼睛餘光就可以很精準的鍵入。這麼一來，可以省下眼睛不斷移動的缺點。\n左圖：原本的介面；右圖，將拼字字母顯示在鍵盤中間\n這只是我個人想要的功能，並非所有人都希望在畫面中看到礙眼的字母出現，所以我在設定畫面中加了一個選項可以開關這個功能。\n第一支處理這功能的 commit 在這兒。主要的作法是把目前整個鍵盤的 layout 外層包上 FrameLayout，然後在正中間放上用來顯示拼字字母的 candidateHint TextView 。再來，就是隨著左上角的 candidate 有改變時，同時去改變 candidateHint 的字串內容。\n長按 123 按鍵時，切換至九宮格數字鍵盤 這是 Github 上有使用者提出的需求。我自己也覺得這功能很實用，就研究了一下怎麼達成。最常遇到的使用場景是，在網路上常有需要輸入身份證字號的情況。通常在輸入第一個英文字後，後面全是數字。要在一長排的數字中，連續按上那麼多次，不如將畫面切換成面積更大的九宮格數字。\n長按數字 123 按鈕\n這數字按鍵，是不是好按多了呢\n這功能的實作 commit 在這兒。長按 123 按鈕時，我送出了一個新定義的 KEYCODE_SYMBOL_KEYBOARD。\n然後在處理 onKeyboardAction 時，針對這個 keycode，將鍵盤型式切換到電話鍵盤。\n讓空白鍵變更大 目前所有的鍵盤格式，幾乎都已經把畫面佔得滿滿的。想要把空白鍵變得更長，勢必有其他按鍵必須犧牲，做相對應的縮小。\n看來看去，就只有最左邊的”閉閉鍵盤”按鈕最適合。\n輸入法的程式，關於鍵盤的畫面配置，是透過定義 xml 來指定每一行要有什麼按鍵，對應到什麼 keycode，和每個按鍵的大小。萊姆輸入法因為支援了相當多不同型式的輸入法和鍵盤，所以程式裡也有許多對應的鍵盤配置 xml。\n下圖以最基本的 lime.xml 來看，可以看到最後一行 (Row) 是由 sym_keyboard_done，EN, , , sym_keyboard_space，等等按鍵組成。找到這些 xml 後，只要將 sym_keyboard_done 的寬度從原本的 15%p 改成 10%p，然後再把 sym_keyboard_space 的大小由原本的 30%p 改到 35%p 就行了。\n最近的修改\n拿掉了一堆原先功能才需要用到的 permission\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%90%8A%E5%A7%86%E8%BC%B8%E5%85%A5%E6%B3%95lime-hd%E7%9A%84%E6%9B%BF%E4%BB%A3%E5%93%81/","summary":"\u003cp\u003e前不久得知，在 Google Play Store 上的萊姆輸入法 (Lime HD) 被下架了。剛好前一陣子因為想要讓萊姆輸入法可以在 Android 平台的電子書閱讀器上有更好的 UI 呈現方式，所以有從萊姆輸入法在 Github 上的 repository 那兒 fork 了一份程式碼來做適度的修改。詳情可以看我上一篇文章：\u003c/p\u003e","title":"萊姆輸入法(Lime HD)的替代品"},{"content":"緣由 今年開始比較用心地看書。除了看從 Google Play Book 和 Kobo 上面買來的書籍外，也從 Hyread 系統中借了不少圖書館的書。利用文石的設備(Onyx BOOX Note3, Max3)裝上 Hyread APP 使用時，除了在看 pdf 書籍時下方的頁碼在換頁時都會閃一下外，一般的 epub 格式書籍使用上都很順暢。\n當看完每一本書時，我也劃了不少註記。為了要事後能夠再回味這些書中的重點，很希望能夠把註記的內容匯出到其他地方，統一整理。目前文石閱讀器的內建 neoReader 有將註記匯出的功能；讀墨的服務也可以在網站上選擇購買的書籍，匯出註記。就只有 Hyread 的 App 或網站完全沒有提供這功能。唯一的可能方式是買一台 Hyread 閱讀器。Hyread 閱讀器中的軟體才有支援註記匯出或同步到 Evernote。為了這功能，我才特地買了一台最便宜的 Hyread 閱讀器 — Gaze One S。\n硬體 詳細的規格可以直接看下面的官網網頁比較清楚。\nhttps://ebook.hyread.com.tw/activity/201902gaze/productDetail.jsp?id=735#N2\nGaze Pocket One S 的外觀\n6 吋的電子紙畫面，300 ppi 的細緻度，重量 210 克。充電和傳輸的介面是 Type-C；有耳機孔，而且還有 SD 卡插糟。整台的硬體都很中規中矩，沒有特別突出的地方。SD 卡插糟對於文件比較多的使用者來說，應該是個不錯的加分元素。\n從畫面中可以看到，閱讀器背面是 Tiffany 藍，塑膠感很重(因為本來就是塑膠)。但是不會沾指紋，也蠻好握持的。\n軟體 封閉式 **最重要的一點是，它是封閉式的。**雖然底層系統應該還是用 Android，但由於系統性能不高，而且屬於相對低價的產品，有可能是要賣給較為入門的使用者，所以開機後的介面都圍繞在閱讀這件事上；並沒有其他的 APP 或是任何進入點可以讓使用者安裝自己想要的軟體。這點在買之前就知道了，所以有了心理準備，雖然還是很希望它能適度地開放較進階的使用者，至少可以把那輸入法換成自己比較順手的軟體。\nHyread Pocket One S 的畫面\n主畫面 畫面上可以看到，系統的一些調整選項都在主畫面右下方的設定。如果在看書的時候想要做這些操作的話，得要先回到主畫面才行。有點麻煩，但相對上單純，使用者不用去學習像是 Android 平板可以從畫面上方下拉的行為。\n閱讀功能 Hyread 軟體也有提供文字排列橫轉直的功能。這功能在 Android 上的 APP 也有提供，是個我很愛用的功能。除此之外，在 Hyread 閱讀器上，還能更換字型。內建有幾種字型可以選；也可以自己傳入自己喜歡的字型。像畫面上呈現的就是我很喜歡用的文楷字型。\n這台不但能讀 epub 檔案，也能讀 pdf 檔案。不過，能歸能，勸大家不要用它來開 pdf 檔案，這對它來說是種折磨，對自己的眼晴來說，也是。\n註記匯出功能 沒想到這種基本的功能，竟然要特別列在心得中。這功能是我為什麼買這台閱讀器的主因。因為讀書是一回事，讀完能不能從眾多讀過的書中將重點之間的關係連結在一起，才是更重要的事。為了要更有機會做到這件事。圖書館借的書通常兩週或十天後就要歸還。如果沒有將閱讀過的註記留存下來的話，很有可能過兩三天我就全忘得差不多。\n為此，只好購入這台閱讀器。\n缺點 雖然買這台機器主要是用來匯出註記而已，但只要借的書是 epub 格式的話，偶爾我還是會利用這台機器來閱讀。畢竟，它很輕，攜帶又方便；而且還能為借來的書換上自己喜歡的字型。\n這台機器上有客製化過的輸入法，繁體中文只提供注音和倉頡。這點就算了，我想抱怨的是：6 吋的畫面已經很小了，為什麼輸入的鍵盤四周還要留那麼多空白呢。把按鍵變得大一些不是更好嗎？我的手指已經很小了，還是很容易按錯；那其他人在輸入時不是更加困擾。\n另一點小小的困擾是，雖然 Gaze Pocket One S 有提供兩種顯示模式，但是切換的位置埋得很深：使用者必須先回到主畫面 → 點選設定 → 點選更多設定 → 點選顯示與閱讀設定 → 點選螢幕更新頻率設定，這時才會出現 (系統預設，每頁刷新，極速刷新) 三種選項可以切換。\n在主畫面時想要借書或是瀏覽一下自己從 SD 卡匯入的書籍時，總是會想切到極速刷新讓這些操作能夠更順暢。但是它被埋在這麼深的角落，常常會讓人打退堂鼓，情願看著畫面一直閃。\n如果之後這功能能抽到更方便的設定第一層的話，就太好了。\n結語 購買一個月以來，使用上還算順手，已經用它匯出過好幾本書的註記到 Evernote 中。電力也在可以接受的範圍內：放著它沒用一段時間後，會自動關機；再重開後，基本上沒有什麼掉電。\n這台有內建燈光，還有內建大部分市面上閱讀器所沒有的偵測旋轉用的 G-sensor。不過我都沒有需要，所以就沒有特別提相關的心得。\n對於電子書初心者來說，是台蠻推薦的閱讀器。光是可以從圖書館借到各式各樣的書，在還沒花錢買書前就能先感受一下電子書的滋味，這點就很超值的了。\n","permalink":"https://plateaukao.github.io/blog/posts/hyread-gaze-one-s-%E7%9A%84%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97%E6%84%9F%E6%83%B3/","summary":"\u003ch3 id=\"緣由\"\u003e緣由\u003c/h3\u003e\n\u003cp\u003e今年開始比較用心地看書。除了看從 Google Play Book 和 Kobo 上面買來的書籍外，也從 Hyread 系統中借了不少圖書館的書。利用文石的設備(Onyx BOOX Note3, Max3)裝上 Hyread APP 使用時，除了在看 pdf 書籍時下方的頁碼在換頁時都會閃一下外，一般的 epub 格式書籍使用上都很順暢。\u003c/p\u003e","title":"Hyread Gaze One S 的使用心得感想"},{"content":"\nThe design concept of EinkBro APP is to reduce UI element by default as much as possible. When users need more features, they can add function icons to the toolbar, or enable features from Settings page. Multi-touch gesture support is implemented in this concept too. By default, it’s not turned on; users could browse the web just like other browsers. Once the user wants more flexible control over the web tab, they can enable multi-touch feature to access more quick functions by swiping on the screen with two fingers.\nIn this article, I would explain how this is implemented in EinkBro APP, and what mechanism is taken into consideration to prevent it from interfering web content zoom in/out feature.\nMulti-touch Gesture Detection To detect touch events, we need to implement an View.OnTouchListener first, and override onTouch function in it. From the argument, an MotionEvent object is given. We could get how many pointCount there is for the touch. Because we just want to handle two finger scenarios here, any other pointCount other than 2 are skipped (by returning false).\nInside onTouch, we need to handle ACTION_POINTER_DOWN, ACTION_POINTER_UP and ACTION_MOVE.\nACTION_POINTER_DOWN keep track of the finger starting position, and set inSwipe flat to true.\nACTION_MOVE keep track of where user’s fingers are now.\nACTION_POINTER_UP When user’s fingers leave the screen, we need to check end positions of fingers, and compare it to starting position to see if any actions should be called.\nisValidSwipe is called, to check if the moved offset is larger than a predefined threshold. We don’t want to trigger these gesture functions accidentally. Once we think the swipe is valid, then we check what the movement is: up, down, left, right, and then, call corresponding configured functions.\nWith above implementation, the multi-touch gesture detection main logic is done. However, sometimes when the web content can be scaled by using two fingers, the zoom in/out behavior may accidentally trigger multi-touch gestures too. We need a mechanism to detect whether user wants to scale the web content or just swiping the screen when they use two fingers on screen.\nScale Gesture Detection In Android, a built-in ScaleGestureDetector class is available to reduce our effort on figuring out whether the gesture is for scaling, and what’s the scale ratio.\nWe created a ScaleGestureDetector instance in our MultitouchListener and pass all touch events to it too.\nScaleListener is a listener to receive onScale events. We can get changed scale factor here, and do what we want with the factor. In order to know the scale factor in MultitouchListener, we use a local variable scaleFactor to keep track of the value.\nWe extend MultitouchListener’s onTouch function as below:\nACTION_POINTER_DOWN\nreset scaleFactor to be 1.0\nACTION_POINTER_UP\nHere, we will check isValidSwipe function. In addition to check swipe threshold, we also need to check the scaleFactor. If difference is within a threshold, we don’t think it’s a scale at all. If so, we think user is scaling now, and skip the two finger swipe action.\nThat’s all for the detail implementation. As for the supported functions for two-finger swiping:\nswipe types\nSupported swipe functions\nFor my personal preference, I have following configuration:\nSwipe up: show bookmarks\nSwipe down: close current tab\nSwipe left: show next tab\nSwipe right: show previous tab\nIt’s much faster than looking on the toolbar, and clicking on the buttons.\nReference Commit 1: Multi-touch listener\nCommit 2: Zoom in/out prevention\n","permalink":"https://plateaukao.github.io/blog/posts/multi-touch-gesture-support-for-einkbro-app/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/54b74b643f76/1_eMV0FPgRLDDajEZgPMC1cw.png\"\u003e\u003c/p\u003e\n\u003cp\u003eThe design concept of EinkBro APP is to reduce UI element by default as much as possible. When users need more features, they can add function icons to the toolbar, or enable features from Settings page. Multi-touch gesture support is implemented in this concept too. By default, it’s not turned on; users could browse the web just like other browsers. Once the user wants more flexible control over the web tab, they can enable multi-touch feature to access more quick functions by swiping on the screen with two fingers.\u003c/p\u003e","title":"Multi-touch Gesture Support for EinkBro APP"},{"content":"\n衝浪滑板的經驗值累積到台中來了。台中有兩個比較有名的滑板場，一個在中正公園，一個在豐富公園。由於豐富公園的滑板場有碗池可以滑，所以這次我來探路的是豐富公園。台中除了豐富公園外，還有一個豐樂(雕塑)公園。一般台中人只對豐樂公園有印象，因為比較大。有滑板場的是豐富公園，不是有一堆雕塑的豐樂公園。\n交通 豐富公園的四周都有劃停車格，理論上應該不難停車。\n滑板場狀況 看網路上的影片，有時碗池會有各種塗鴉，但應該常常有在管理，所以隔沒多久又會被漆回雪白一片。這邊的碗池是水泥做的，跟內湖極限運動場的木頭比起來，雖然硬了許多，但不用擔心時不時有木片鬆落或是鏍絲掉出來的問題。只是，護具要戴好，以免跌倒時，跟地面硬碰硬。\n碗池的邊緣都有鐵桿突起，比較適合技術板卡板和做招。但對於衝浪滑板來說，如果想要從碗池上方滑一滑後直接下碗池的話，可能要小心被鐵桿卡住而仆街。我是都乖乖地從碗池邊 drop in。但每次 drop in 都要先把板子卡住，有點麻煩就是了。\n滑板場注意事項 目前是三月初，但碗池裡充滿了落葉。第一次中午來到這兒時，有個爸爸好心地把落葉不斷掃出去。但當天晚上再來一次時，發現又是落葉滿地了。少量落葉其實不大會干擾到滑行，但落葉太多的話，在加速時如果剛好輪子在落葉上，可能會打滑，讓施力沒有著力點，整個會滑出去噴板。所以在滑行時要注意避開有大量落葉的區域。\n有這麼好的場地，來滑的人自然不少；其中當然包含了很多小屁孩。晚上在滑的時候，最誇張的是：竟然有人在碗池正中間就開始練起了 kick flip。明明旁邊有一區就是給技術板練習的場地，還有大空地。他不去那邊練習，偏偏要站在碗池中間練。這讓站在碗池上方，打算要 drop in 的我覺得很無言。\n除了白目的小孩外，還有橫衝直撞的小孩。這裡的碗池沒有特別標明滑行的路線，碗池也不是封閉式的。滑一滑隨時有可能會有小朋友從其他路線滑進來。所以在練習時要不斷地注意其他方向，準備應付突如其來的小朋友。不然，就是選擇在一大早，或是晚上九點多十點後再來。通常這些時間段比較不會有小朋友出現。\n心得 目前滑過這麼多滑板場，這是我覺得最適合已經有一點點程度的 surfskateboarder 的場地。之所以會說適合，是因為它的路線設計可以讓 boarder 不斷地在碗池中穿梭，浪壁有高有低，不會太過於單調。\n其他的滑板場，有時屈就於要將各種使用場景(技術板、衝浪滑板、直排輪、腳踏車)都考慮進來，結果對於每一項運動反而都不是最佳的組合。\n最近竹北有新開一個滑板場，看起來也很適合衝浪滑板。希望近期內也能去開箱。\n地圖 ","permalink":"https://plateaukao.github.io/blog/posts/%E8%A1%9D%E6%B5%AA%E6%BB%91%E6%9D%BF%E5%A0%B4%E5%9C%B0%E8%B8%A9%E9%BB%9E%E5%BF%83%E5%BE%97-%E5%8F%B0%E4%B8%AD%E5%8D%97%E5%B1%AF%E8%B1%90%E5%AF%8C%E5%85%AC%E5%9C%92/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/6855f19feaa8/1_rS3fCKPkvFOMC_REih6IIg.png\"\u003e\u003c/p\u003e\n\u003cp\u003e衝浪滑板的經驗值累積到台中來了。台中有兩個比較有名的滑板場，一個在中正公園，一個在豐富公園。由於豐富公園的滑板場有碗池可以滑，所以這次我來探路的是豐富公園。台中除了豐富公園外，還有一個豐樂(雕塑)公園。一般台中人只對豐樂公園有印象，因為比較大。有滑板場的是豐富公園，不是有一堆雕塑的豐樂公園。\u003c/p\u003e","title":"衝浪滑板場地踩點心得 — 台中南屯豐富公園"},{"content":" OPPO Find N + Hisense A7\n2021 年年底 OPPO 宣布 OPPO Find N 折疊機時，Youtube 上各大網紅都有在第一時間發評測文；台灣也有個發表會讓拿不到實機的台灣網紅們也能寫寫文章或是拍拍粗淺的類似官方文稿的介紹影片，就此展開了一陣 OPPO Find 旋風。\n託朋友的福，人在台灣的我也入手了這台最近很夯的折疊螢幕手機。Youtube 上的評測大多都很公關式介紹，把規格講一輪，把專門為折疊機開發，看似好用的功能說一輪就結束了。真正的體驗還是要自己拿到手了才準。\n外觀和重量 原本想說我已經習慣了拿 6.8 吋重似磚塊(225g)的海信 A7 一年了，對於 275g 的 OPPO Find N 應該也是沒啥感覺吧，但真的到手後，才發現確實還是需要再習慣這多出來的 50g 才行。\n很多人會評論說，現在折疊螢幕手機那麼貴，買台中上的手機再搭上 iPad mini 也比它便宜，而且兩者各自的性能更好，更輕，畫面更大。殊不知折疊螢幕手機的最大好處就是讓你身上只需要帶一台設備，只需要充一台設備，要大可大，要小可小，而且隨時可以在尺寸上做切換。如果看不上這點的人，確實還是買兩個設備比較實在。\nOPPO 出的這款，打破了以往折疊螢幕的慣例，讓大畫面的比例幾乎是一比一。網路很多評論也是說這比例在看影片時，因為大多數影片的長寬比是 16: 9，畫面的留黑太多，還不如買支大尺寸的手機。但，大家其實都忽略了(我很在意的)一點：正因為影片的比例只能讓它在螢幕中間，當叫出字幕時，字幕可以很自然地顯示在影本下方黑色的部分，不用再老是壓在影片上妨礙美感，或是造成閱讀困難。\n超會黏灰塵的保護貼\n另一個方型螢幕帶來的好處是：一般手機在看影片時，如果想要得到最佳的觀影效果，難免要切到全螢幕，然後把手機轉 90 度變成橫向來看。這時如果又想做其他操作，比方說看評論或是選其他影片來看，免不了手機要一下打直，一下全螢幕；整支手機就那麼一直轉來轉去很麻煩。而 OPPO Find N 就方便多了，不論是全螢幕影片或是一般瀏覽的場景，畫面全都是正向的，不存在要轉手機方向的問題。\n相機 目前用 Find N 的相機機會不多，但出門拍了幾次後，覺得兩年前的 pixel 4 出來的畫質還是比較好。Find N 配有廣角鏡，在戶外時很自然地會什麼都想要用廣角鏡來捕捉更多的景色。雖然它確實可以拍到更廣的畫面，但畫面邊緣變形得蠻嚴重的。看來要多用標準鏡拍拍看。\n網紅們介紹的可以用前螢幕預覽，看自己自拍的樣子，我是覺得很雞肋。一般拍照誰會想要把折疊螢幕打開來拍，我都嘛是在折疊的狀況下，像是一般手機的方式拍一拍而已。\n另一點要講的是，在某個流量不高的小評測主的影片中有提到：由於 Find N 機身很小巧，但一樣塞了三顆鏡頭，在拍廣角的時候，真的很容易會拍到自己的手指。我是覺得會用到折疊機的人，應該也不是因為看上手機的相機拍照效果而買的，既然機身做得這麼小，那是不是只要留兩個鏡頭或甚至只留一個鏡頭就好呢？一來更節省成本，二來可以把精力放在更需要加強的地方。\n軟體 Google Play Store / Google Play Services 首先來講一下大家最關心的問題：目前 Find N 只有在中國市場推出，並沒有內建 Google Play Services，那買回台灣之後，有什麼方式能把 Google Play Services 裝上去然後安裝 Google Play Store 中的 APP 嗎？\n答案是肯定的。雖然沒有內建，但要裝回來其實很容易：\n在內建的 App Market 先下載安裝 Google Chrome，它應該就會把相關的函式庫也都順便搞定。 去 apkpur.com 隨便抓一版 Google Play Store 來安裝。這樣子就可以了。 這樣子操作後，有沒有遇到什麼問題呢？完全沒有喔。接下來就跟一般的手機一樣，要什麼 APP 都可以在 Google Play Store 上安裝，連之前付費過的 APP 也可以。Google 的 Youtube, Gmail, Photos, Maps, Drive 等全都可以正常運作。\n折疊螢幕相關的系統功能 雙指下滑\nFind N 提供的雙指下滑變 Multi-Window 的手勢，比目前市面上其他折疊螢幕手機方便多了，但這功能其實在前年的海信 A5 電子紙手機上就有類似的操作( A5 是要三指下滑)，已經不是個很新的作法了。\n講到這點，不得不說 Android 早在很久前的版本就支援 Multi-Window 功能，但啟動的方式一直都是很隱藏地要從 recent apps 畫面中長按 app icon 才行，不明白為什麼 Google 老是在體驗上做不到位，而只顧著推出新功能，或是把 UI 愈改愈糟 (沒錯，我在說的就是 Android 12 的 UI)。\n雙指下滑的手勢，方便歸方便，網紅也狂吹這件事，但大家都沒有講到一個重點：展開來的大螢幕也只有 7.1 吋而已，硬要在這塊畫面上切一半，左右開啟不同軟體來操作，在很多情況下是不好用的。這時如果需要輸入字串，再把輸入法叫出來，那畫面更是尷尬。\n手動調整每個 APP 的畫面比例\n從小畫面切到大畫面時，Find N 會預設將 APP 切到橫向模式。但目前有很多 APP 並不支援橫向模式顯示。這會造成原本應該正向顯示的 APP 畫面，變成轉了 90 度*。*這時，要嘛就是使用者自己要把手機轉 90 度用；不然就是可以進到設定裡，手動將該 APP 的比例固定在 16: 9 或是 4:3。\n這麼做的話，那個 APP 會以設定的比例，正向地顯示在大畫面中。但這樣做的缺點是沒有辦法完全地使用到大螢幕。\n全螢幕時的小缺點\n目前 Find N 在某些 APP 的全螢幕模式會有些問題，下圖可以看到在畫面的最上方會是全白的，並沒有將 APP 內容延展到原先狀態列的位置。雖然這並不影響使用，但沒辦法把大螢幕發揮它全部的威力，還是有點可惜。我自己寫的 APP 也遇到了一樣的狀況，要特別將 APP 做些調整才能避掉這問題。希望之後 Find N 的更新能修正這個問題。\nHyread APP 開啟 pdf 書籍\n總結 雖然上面挑了一堆問題出來，但整體而言，OPPO Find N 真的是台好手機，我在使用上也相當順手。題外話是，手機在開合時，闔上手機的那個聲音，聽起來相當悅耳，開啟大螢幕時，桌面的動畫也讓人看了很舒服。\n最後放兩張 Find N 拍的照片來結尾\n補充 [2022/10/09] 一直覺得內建的 Launcher 不是很好用。在 xda 上找到了怎麼換成其他 Launcher 的方式，記錄在這邊：\nadb shell cmd package set-home-activity com.teslacoilsw.launcher ","permalink":"https://plateaukao.github.io/blog/posts/oppo-find-n-%E5%85%A5%E6%89%8B%E4%B8%80%E5%80%8B%E6%9C%88%E5%BE%8C%E7%9A%84%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2e981ca3205f/1_A4s-7lUOzRpEngu_f3SJow.jpeg\"\u003e\n\u003cem\u003eOPPO Find N + Hisense A7\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e2021 年年底 OPPO 宣布 OPPO Find N 折疊機時，Youtube 上各大網紅都有在第一時間發評測文；台灣也有個發表會讓拿不到實機的台灣網紅們也能寫寫文章或是拍拍粗淺的類似官方文稿的介紹影片，就此展開了一陣 OPPO Find 旋風。\u003c/p\u003e","title":"OPPO Find N 入手一個月後的使用心得"},{"content":"\nEinkBro browser is developed to support similar behavior of ereader APP to a normal browser, including page-by-page scroll feature, and more powerful text style customization. Nevertheless, no matter how well EinkBro is developed, sometimes normal ereader APPs are still more suitable for long-term reading. As a consequence, I also implemented the “Export to epub file” feature, so that users can export their favorite web content to a standard epub file for later reading on other devices or APPs.\nIn order to support this feature, an epub manipulation library called epublib is added. It’s very well written that I can easily create an epub file with several lines of codes. This library not only supports creating a new epub file, but also reading an existing epub file. That’s one of the reasons why I decided to support reading epub file for EinkBro APP: no extra library is needed. And the main reason is: it’s more convenient to read what’s being created in EinkBro, just inside EinkBro APP itself. The user experience may not be as good as other ereader APPs; however, users can use consistent toolbar actions to read the content, and also benefit from the powerful translation modes that EinkBro provides.\nImplementation Details Useful Example Although epublib supports reading epub files, I can barely find examples of how to do it in Android platform. The most complete and useful code snippet is from here. This code snippet works; however, its coding convention is not following normal Java-like languages: all functions and variables are named in capital letters. Never mind; some refactoring is necessary later anyway.\nIt creates EpubReaderView based on WebView, and utilizes epublib to read content from epub file and write them to local file storage for later use. There are three steps in this flow:\n1.OpenEpubFile\nProcess TOC and then process Chapters so that a more logical chapter list is created. During this step, a local folder path is created in order to put image resources in it (at line 438).\n2.DownloadResource\nIn this step, image resources are filtered and write to local storage one by one.\n3.GotoPosition\nAfter step 1 and 2, all materials are put to correct place; in step 3, we feed these materials to WebView by using loadDataWithBaseURL() , with ResourceLocation as its base. In this way, the image files written to local storage can be accessed by WebView directly.\nWell, this flow works, but it’s not very elegant. Originally epub is a single file; now its content is written into several files in a temporarily folder. When reading each chapter, more IO is necessary to access these files by system.\nAs you know, we can intercept WebResourceRequest in WebView, we can refactor the codes to bypass image file writing step.\nRefactoring In NinjaWebViewClient, we refactor shouldInterceptRequest() to support possible resources request from epub file. At line 215, url scheme is checked to see if it’s a specific scheme that I used in epub file for images. This may not be working for normal epub files. However, if the epub is created form EinkBro, the images would be correctly displayed without writing any files to local storage.\nText Selection Refactoring epublibDroid also supports annotation to the displayed epub file. However, now it’s only effective to the opened epub, without saving the annotation information to database nor the file itself, which means that if user opens the file again, or even changing chapters, the annotation effect would be gone.\nTo save the annotation information, the selected text info and annotation method should be refactored first. That’s the refactoring we will do here.\nSelectedTextInfo class is created to better describe the selected text inside web content. Originally, only a json string is used and passed around for data manipulation.\nRefactoring annotation function\nCreate enum class to represent different annotation methods (originally it’s decided by hard-coded integers). Now, a well descriptive SelectedTextInfo object is used, and annotation type enum is passed into annotate() function.\nActual annotate implementation in Javascript\nThis is something new to me. Inside web content, if I want to annotate html text element, I can use following commands to document object.\ndocument.execCommand(\u0026#34;HiliteColor\u0026#34;, false, \u0026#34;$hashcolor\u0026#34;) document.execCommand(\u0026#34;underline\u0026#34;); document.execCommand(\u0026#34;strikeThrough\u0026#34;) As code below, according to annotation method, corresponding javascript snippet is used to annotate the selection range.\nFuture Works Now the selected text information is composed of selection range. This is not standard annotation method used in epub. A more common of way recording this is using EPUB CFI Fragment Selector. The selector would be like string below. I believe this is not supported in epublib. Before really implementing the saving feature, I may need to study whether there are existing solutions for it on Android platform.\nepubcfi(/6/4[chap01ref]!/4[body01]/10[para05]/3:10) Demo Source codes commit 1 commit 2 References epublib epublibDroid ","permalink":"https://plateaukao.github.io/blog/posts/annotation-support-for-epub-file-in-einkbro-app/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/fe8c2ac87bf7/1_2caSccnCro9WChmRIef4ug.png\"\u003e\u003c/p\u003e\n\u003cp\u003eEinkBro browser is developed to support similar behavior of ereader APP to a normal browser, including page-by-page scroll feature, and more powerful text style customization. Nevertheless, no matter how well EinkBro is developed, sometimes normal ereader APPs are still more suitable for long-term reading. As a consequence, I also implemented the “\u003cstrong\u003eExport to epub file\u003c/strong\u003e” feature, so that users can export their favorite web content to a standard epub file for later reading on other devices or APPs.\u003c/p\u003e","title":"Annotation Support for Epub file in EinkBro App"},{"content":"這個需求來自於每台手機或電子書閱讀器的預設字型都長得不太一樣，有的是用比較粗的黑體，有的是用我不太喜歡的細圓體；有些時候網頁還會套上自己想要的雲端字體。面對這麼多系統或網頁提供的字型，看久了總是會覺得膩，或是單純地只是想要用一套自己最喜歡的字型就好。\n很久以前就有試著解決這問題，無奈網上的解決方案都是繞著使用編譯到 apk 中 assets 目錄下的固定字型。這方式無法套用到使用者自行放到手機上的字型檔案；我也不想因此在 2.5 MB 的 APP 中，放入一個動輒 10 到 30 MB 的字型檔案，這樣有點太本末倒置了。\n最近，又開始研究這個問題，終於讓我試出了一套可行的方式。雖然還不是很完美的方案，但在此記錄下來，希望以後有機會再把它變得更完美。\n實作 步驟不多，簡單來說：要讓使用者先選擇一下字型檔，在 WebView 中注入 CSS style，自定義一個字型，把它的 src 指定到一個特別的 uri。當 WebView 在讀取這個 CSS font 時，會經過 WebViewClient 的 shouldInterceptRequest ，這時要把字型檔的 binary stream 餵進去。\n下面就是詳細的步驟內容。\n步驟一：從系統中選擇想要的字型檔案 這步驟不難，只是因為要讓使用者選擇字型的話，還是要把這部分的 UI 邏輯先做好。我寫了一個 openFontFilePicker 的函式，叫起系統的 file picker。\n當 Activity 或 Fragment 收到系統回傳的 intent 時，我會把它記錄到 SharedPreferences 中，並且利用第 71, 72 行，取得之後 APP 繼續能夠讀取這個檔案的權限。\n塞在 SharedPreferences 的字型資料，寫了一個簡單的 class 儲存檔名和收到的 content uri。\n步驟二：注入 CSS Font 資訊到 WebView 中 注入的方式，網路上倒是有不少文件，而且其實都寫得大同小異。我定義了一段要注入的 script 如下：新增一個 font-face，它的 font-family 為 customfont，來源是隨便訂的 url(‘mycustomfont’)，然後把所有 body 的字型全換成我的 customfont。補充說明一下，在參考加入 Google Web fonts 時(https://fonts.googleapis.com/css2?family=Noto+Serif+TC:wght@400\u0026amp;display=swap)，看到 Google 字型設定設定中都會加入 font-display: swap; 後來發現，加入後畫面在更換字型時就不會再閃爍。更多的說明可以參見下方的 Controlling Font Performance with font-display。\nControlling Font Performance with font-display | Web | Google Developers\n注入 CSS 的方式之前我也有包成一個函式了：\n步驟三：攔截 WebRequest，傳回字型資訊 利用 injectCSS，將 customFontCSS 注入後，接著要處理 intercept Web Request：在 WebViewClient 中覆寫 shouldInterceptRequest()。主要處理落在下圖的第 5行到 17 行：如果來的 request url 是事前定義好的 mycustomfont 的話，這就是 Web 要來找字型資訊的時機，10, 11 行透過 context.contentResolver 建立檔案串流，再生成一個 WebResourceResponse 回傳。\n基本上這樣子就可以正確地看到手機上的字型檔案有被載入，用來顯示網頁的內容。\n未來需要改進的地方 步驟三可以看到，每次網頁來要求字型資訊時，我都是建立新的 inputstream，回傳一個新的 WebResourceResponse。這邊我有嘗試著 re-use inputstream 或是 WebResourceResponse，但都還沒有成功。所以，也就只能先停在這兒。至少這已經是個可以運作的方案了。\n另外一點是，很多網頁的字型都會在不同的 element 中另外設定。這種情況下，直接改 body 的字型會沒有作用，這也造成大部分網頁可以；但少部分的網頁還是會維持原來的字型。這問題稍微可以透過把網頁轉成 Reader mode 來解決，但畢竟不是一個很漂亮的作法。也是希望未來會有更完美的處理方式。\n示範畫面 程式碼連結 大部分的程式碼都在這個 commit 了，雖然後來還有些小修改。\nsome what works now · plateaukao/browser@23acbaa\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-android-webview-%E4%B8%AD%E4%BD%BF%E7%94%A8%E6%89%8B%E6%A9%9F%E4%B8%8A%E7%9A%84%E5%AD%97%E5%9E%8B%E6%AA%94%E6%A1%88/","summary":"\u003cp\u003e這個需求來自於每台手機或電子書閱讀器的預設字型都長得不太一樣，有的是用比較粗的黑體，有的是用我不太喜歡的細圓體；有些時候網頁還會套上自己想要的雲端字體。面對這麼多系統或網頁提供的字型，看久了總是會覺得膩，或是單純地只是想要用一套自己最喜歡的字型就好。\u003c/p\u003e","title":"在 Android WebView 中使用手機上的字型檔案"},{"content":"目前 EinkBro 有提供儲存網頁成 epub 檔案的功能，這大概是少數(或是唯一)瀏覽器有提供的功能之一。不過，礙於 Android 每次升版對於檔案的存取方式都不斷在(亂無章法地)改進，到目前為止，最通用的開啟手機上檔案方式是：利用 Intent.ACTION_CREATE_DOCUMENT 叫起系統的 file expolorer default APP，從那很不好用的介面中選取想要的檔案，或新增一個檔案。\n而 EinkBro 從一開始提供這功能時，就是採取了這方式。這作法在新增 epub 時，沒有什麼太大的問題，使用者只需要在對的目錄下輸入想要的檔案名稱就好， EinkBro 就會收到系統傳來的 content uri。\n但如果使用者是想要將網頁儲存到既有的 epub 檔案中時，這方法就顯得很麻煩。開啟檔案選取時，有可能當下的目錄不是使用者想要的，也有可能當下的目錄有一堆 . 開頭的目錄或檔案，顯示在畫面最上方，必須要經過不斷捲動，或是在各個目錄間移動，才能找到想要的那個 epub 檔案。偶一為之還好；如果每天都有想要儲存的文章時，就會煩到想要放棄這功能，乾脆再另存一個新檔比較快。\n解法的關鍵 我自己在使用了好長一段時間後，終於受不了了，想來看看有沒有辦法改善這個流程。昨天終於找到方法了。關鍵在於：在新建 epub 檔案時，那個 conten uri 的存取權限一般來說只可以使用到下次手機重開。一旦使用者重開手機，就只能再利用 file picker 選擇想要的檔案。這問題 Android 官方給出了解法：\nhttps://developer.android.com/training/data-storage/shared/documents-files#persist-permissions\n如果有想要拿到的 content uri，在未來不論何時都可以再次讀寫的話，可以加上下面的片段：\n這麼一來，這個 uri 就可以保留在自己的 APP 中，之後利用它來讀取檔案的內容或是將新的資料寫入進去。\nEinkBro 中的實作說明 我把這段程式碼加在 openBook() 中，並且在 save epub 時，將這個檔案的 content uri 存到 sharedpreference 中 (是的，不是寫到資料庫，因為我懶得再開一個 table)。然後稍微調整了一下儲存 epub 的對話框，將原本的兩個選項(但都是開啟 file picker)：\nSave to existing epub New an epub 改為\nSelect Saved Epub new Epub or from picker 選項 1 不再跳出系統難用的 file picker ，而是列出 EinkBro APP 曾經儲存過的 epub 檔案，讓使用者可以直接選擇！\n雖然這只是一個很不起眼的流程小改善，但對於我的閱讀方式來說，卻有很大很大的幫助！也希望這個功能的改善，會讓大家更快速地儲存 epub 。\n程式碼片段 加入獲取權限的 commit https://github.com/plateaukao/browser/commit/3dca8703b72488cb33e2659e3aaafa7fd2292dca\n將 epub 檔案的 uri 存入 sharedpreference，並呈現為列表 https://github.com/plateaukao/browser/commit/73e227a6be807b7fd0a3cfe0d5cbc271f5812c26\n示範影片 ","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%B9%E5%96%84%E5%B0%87%E7%B6%B2%E9%A0%81%E5%85%A7%E5%AE%B9%E5%AF%AB%E5%85%A5%E6%97%A2%E6%9C%89-epub-%E7%9A%84%E6%B5%81%E7%A8%8B/","summary":"\u003cp\u003e目前 EinkBro 有提供儲存網頁成 epub 檔案的功能，這大概是少數(或是唯一)瀏覽器有提供的功能之一。不過，礙於 Android 每次升版對於檔案的存取方式都不斷在(亂無章法地)改進，到目前為止，最通用的開啟手機上檔案方式是：利用 Intent.ACTION_CREATE_DOCUMENT 叫起系統的 file expolorer default APP，從那很不好用的介面中選取想要的檔案，或新增一個檔案。\u003c/p\u003e","title":"改善將網頁內容寫入既有 epub 的流程"},{"content":"2021/12/16 今天看的是 NAVER DEVIEW 的 For Better Image Translation (Papago Image/AR Translation) tv.naver.com/v/23649339 #NAVER #DEVIEW\n主要在講 Papago App (Naver出的翻譯軟體) 中對於圖片翻譯的改善技術。講得有條有理，而且投影片也做得很棒！\n더 감쪽같은 이미지 번역을 위해 (파파고 이미지/AR 번역 개발기) NAVER Engineering | 노영빈/김승재 — 더 감쪽같은 이미지 번역을 위해 (파파고 이미지/AR 번역 개발기)\n這幾張 slide 很清楚地描述了圖片(in-place)翻譯的四個步驟：1. 文字認識, 2. 組成單字, 3. 機器翻譯, 4. 重繪出結果。這個演講的主題主要是在講如何改善第二步和第四步，讓翻譯效果在某些情況下可以有更好的表現。最後還有一個 section 是在講即時翻譯的技術。\n在組成單字，句子，和段落時，如果遇到圖片是菜單、收據，或是商品包裝上的介紹時，換行的位置常常需要判斷是要當成同一行，還是是不同的項目。這邊利用了 BERT 技術來做這個判斷。從第三張圖可以看出來，有了精準的斷行分析後，在產出結果時，不會再把每行黏在一起。\n再來是講到繪製翻譯結果的改善：從第一張圖可以看得出來，翻譯後的文字背景很人工，是很明顯的色塊。這裡利用了 GAN 的技術，去更有效地算出文字的背景和前景，還跟其他演算法的效果做了比較。(中間有一大段聽不懂)。\n從改善前改善後的差異看得出來，有了很明顯的進步，幾乎看不出來翻譯的文字是貼上去的，完全跟原本的圖片背景融合在一起。\n最後一段講的是即時翻譯，這邊有利用到 object tracking 的技術，大概的步驟是：1. 找出代表性的 frame, 2. 進行該 frame 的翻譯, 3. 持續利用 Optional flow 的方式追蹤翻譯好的區塊並重繪。\n我試了一下用 Papago App 跟 Google Translate 來翻譯這篇演講的大綱韓文版。先不論翻譯的結果，單就呈現效果來說， Papago 的效果確實好太多了。\n另一個有不同背景顏色和前景顏色的翻譯繪製效果。看起來好的那個是用 Papago App 翻譯的。\n2021/12/16 今天看的是 NAVER DEVIEW 的這篇: 開發 CLOVA App 的 Android 開發者，後來也開發了 server side 所需要提供的相關 API。 tv.naver.com/v/23652504 後來發現 DEVIEW 介紹頁有英文版本，所以加減看了一下。結果發現竟然不是將韓文全部翻譯過來？連標題都不大一樣 #NAVER #DEVIEW\nAndroid 앱 개발자는 왜 자진해서 서버 개발자가 되었나: 클로바앱의 점진적 배포와 호환성 NAVER Engineering | 정언 — Android 앱 개발자는 왜 자진해서 서버 개발자가 되었나: 클로바앱의 점진적 배포와 호환성 관리\n這頁原本還蠻想聽的， CLOVA 機器的演進和發表日期。結果講者竟然說因為時間的關係，所以直接跳過。這不是線下預錄的影片嗎？怎麼會有超過時間的問題。如果真的超過時間，那應該一開始就不放進投影片啊。圖片中 2017 的那個 CLOVA 機器，我家裡也有一台，好懷念啊。\n這個講者的投影片特色是，文字一堆，而且塞得很滿。這張算是還好的，因為至少中間是圖，而且很像是手繪的。這應該不是 designer 畫的吧？這頁也是少數我比較聽得懂的。後來在講怎麼設計 server side API，確保在不同機器間可以有不同的相容性之類的內容，我全都聽不懂。\n常聽到的問題：這些真的都是你一個人開發的嗎？\n回答：幾乎是。\n為什麼沒有使用 RN 或是 Flutter 之類的技術？\n因為有要串接藍牙，而且還有一堆機器，不同的 OS，可能會有很多底層要串接的，所以很難不用系統原生的 SDK 和 Kotlin 還有 Siwft。\n原以為會聽到更多 APP 層面的內容，但比較多著墨在 API 的設計上，一直在講 json schema，和怎麼使用。再加上每一個畫面都充滿了文字，講沒幾句就換下一頁，實在是很難跟上他的演講。等聽力好一點後再回來聽一次看看。\n2021/12/15 今天聽的是 NAVER DEVIEW 的影片 Replacing a natively developed app with Flutter (One year of applying Flutter to Naver Blog App) tv.naver.com/v/23649861\n네이티브로 개발 된 앱을 플러터로 바꾸고 있습니다. (네이버 블로그앱에 플러터 도입 1년 NAVER Engineering | 김승원 — 네이티브로 개발 된 앱을 플러터로 바꾸고 있습니다. (네이버 블로그앱에 플러터 도입 1년의 과정)\n之前案子也有用 Flutter 開發，剛好可以參考他們導入的經驗。他們的 App 是原先就有 Native 的版本，然後不斷地利用 Add-to-app 的方式局部加入用 Flutter 開發的模組。下面兩張圖就是 Native, Flutter 混用的畫面，有在 Native 畫面上顯示 Flutter 元件的場景，也有兩種型式的全畫面互相切換的場景。\n有提到某些元件是 Native 上才有的，所以在 Flutter 的實作上，會利用 platform-view 內嵌在 Flutter 的畫面中。\nAndroid 和 iOS 的原生元件長得不一樣，在 Flutter 上一直有爭論是不是要在 Flutter 上刻跟原生長得很像的元件。後來他們團隊討論的結果，自己開發在 Blog App 上統一的，比較中性的 UI 元件。\n5.4 提到的是他的一些感想，這頁的內容比較有收穫。目前都是每個畫面由同一個人做，如果能兩個平台各一個人一起做的話會更好。Add-to-app 的資訊比較少，做起來，在實作上或跟 Native 連動上並不容易。還提到個人未來發展的相關考量。\n5.5 講到哪些案子和怎樣的團隊適合用 Flutter 開發專案。主要有說到以內容消費為主的 App (像 Blog App 就是)，或是 UX 體驗大於複雜的商業邏輯(或很強調服務性能的)。或是在設計上，有很多 custom UI 的 App 也很合適。還有談到團隊成員的意願和能力也很重要就是了。真的。\n2021/12/14 今天看的是 NAVER 在 2020 東京奧運時直播運用到的 NAVER LIVE CLOUD 介紹 (tv.naver.com/v/23651957)。簡單來說，流程大致上可以分為”發送訊號”，”中繼”，”生產”，”傳送”，”播放”等步驟。 #DEVIEW #NAVER\nhttps://tv.naver.com/v/23651957\n도쿄는 무관중, 네이버는 무한관중. 라이브로 함께한 2020 올림픽\nNAVER Engineering | 노혜성 — 도쿄는 무관중, 네이버는 무한관중. 라이브로 함께한 2020 올림픽\n前三個步驟都還是一對一對關係，但後面兩個步驟就會是一對多，或是需要傳送到無限多的使用者去播放內容。\n有趣的一點是，他有提到不同直播性質的活動，人員的流動模式也會有所不同：購物直播的話，會在開播時衝很高慢慢下降；運動比賽的直播的觀看人數會逐漸上升，並在不同局數休息間有起伏；新聞類直播的話，則是從頭到尾都會維持在一定的水準。這對於在安排 server 上，其實會有所影響。\n關於直播的流量主要分為兩種，一個是會經由 CDN 的影像播放；另一個則是播放時， Player 端需要用到的一些資料傳輸，必須呼叫到後台的 Server API。(然後後面有些聽不懂)\n關於直播時遇到 traffic 突發狀況的應對策略階段：\n1: 由國內 CDN，追加 Global CDN\n2: 拿掉 Player 中的高畫質選項\n3: Step 2 之前已經在看高畫質的觀眾，改變其觀看的畫質\n4: 為保障已經加入在觀看的觀眾，不再接受新觀眾進入直播\n有提到在 Player 端針對 QoE 有做哪些資料的 monitoring，其中包含使用的設備，網路環境，播放初始時間長度，buffering的時間等等。\n最終，很酷的是 NAVER 招募資訊竟然是寫成 gitbook 放在 gitbook.io 上！ naver-career.gitbook.io/kr/service/ete…\n等 conference 有興趣的影片看得差不多了，應該要來看一下他們的招募內容都寫了些什麼。\n2021/12/12 今天聽的是 NAVER Live Commerce team 的演講 tv.naver.com/v/23651510 分享在疫情期間，針對遠端工作建立起來的工作文化。很多內容其實在非疫情期間也很適用就是了。影片中針對 LIVE Commerce 的服務只有稍稍介紹一下而已，第一張照片倒是讓我學到了什麼是 CBT (封測)和 OBT (公測) #NAVER #DEVIEW\nhttps://tv.naver.com/v/23651510\n演講稿沒有好好 review，竟然有標題拼錯的情況發生。2. KPT 跟 Scrum 的 retro 步驟差不多，只是換個名稱而已。3. 4. 利用 gather town 來模擬實際上的上班場景和進行 workshop。 這個蠻實用的，有講到怎麼在 IntelliJ 和 VS Code 中利用 plugin 來進行 pair programming。\n整個演講的架構還不錯，雖然長達40分鐘，但有把 key phrase 利用縮寫列出來，方便聽眾掌握整個演講的重點。 演講最後一段，利用 github API 進行 PR 的管理，也有 open source 出來讓大家參考用。\nhttps://github.com/withearth/deview-2021\n• • •\n2021/12/11 今天聽的是 NAVER DEVIEW 2021 的 frontend session: tv.naver.com/v/23652538 介紹 NAVER 出的瀏覽器 Whale，以及怎麼為它開發 plugin。雖然 Whale 也是以 Chromium 為基礎，但它的 plugin 可以有個 sidebar 的介面，可以跟主畫面的元件互送訊息，還可以存 storage。應該要裝來看看有啥好玩的 plugin\nhttps://tv.naver.com/v/23652538\n슬기로운 웨일앱 개발NAVER Engineering | 김동훈 — 슬기로운 웨일앱 개발\n演講中的範例是在 instagram 網站上，顯示單一 post 時，利用 css 把右邊的留言欄隱藏，在圖片或影像左上方加一個下載的按鈕。點下去後會把資訊送到右邊的 sidebar 網頁，並存到 storage 中。\n感覺這 UI 可以拿來寫個字典用用。\n#NAVER #DEVIEW\n2021/12/09 if(kakao)2021 竟然有 NFT 的主題，而且長達四十多分鐘，不過聽了一下，感覺是在科普 NFT，後面有一半以上的時間是在訪談數位創作者 NFT 對他們來說帶來的好處。 https://if.kakao.com/session/50\n2021/11/17 kakao 在網站 if(kakao) 2021 上有公開了 90 個以上的 sessions，分享他們開發中各項服務的技術。今天看了其中一部是在講如何用 android 開發 kiosk 機器，還蠻有趣的。除了講到跟一般手機上開發的差別外，也有實際提到實作面的內容。 https://if.kakao.com/session/103\n","permalink":"https://plateaukao.github.io/blog/posts/twitter-%E4%B8%8A%E7%9A%84%E5%BF%83%E5%BE%97%E6%95%B4%E7%90%86-i/","summary":"\u003ch3 id=\"20211216\"\u003e2021/12/16\u003c/h3\u003e\n\u003cp\u003e今天看的是 NAVER DEVIEW 的 For Better Image Translation (Papago Image/AR Translation) \u003ca href=\"https://tv.naver.com/v/23649339\"\u003etv.naver.com/v/23649339\u003c/a\u003e \u003ca href=\"https://threadreaderapp.com/hashtag/NAVER\"\u003e#NAVER\u003c/a\u003e \u003ca href=\"https://threadreaderapp.com/hashtag/DEVIEW\"\u003e#DEVIEW\u003c/a\u003e\u003cbr\u003e\n主要在講 Papago App (Naver出的翻譯軟體) 中對於圖片翻譯的改善技術。講得有條有理，而且投影片也做得很棒！\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/efad4a1b7f1e/0_sMcGr4hAbNhwZFnc\"\u003e\n\u003cem\u003e더 감쪽같은 이미지 번역을 위해 (파파고 이미지/AR 번역 개발기) NAVER Engineering | 노영빈/김승재 — 더 감쪽같은 이미지 번역을 위해 (파파고 이미지/AR 번역 개발기)\u003c/em\u003e\u003c/p\u003e","title":"Twitter 上的心得整理 (I)"},{"content":"**Shopping of Untact era, Naver Shopping Live Development Story —**NAVER DEVIEW 2021 這篇是 Naver Shopping Live Development Story 的摘要。前一陣子也有在做跟 LIVE Commerce 相關的案子，剛好這篇可以拿來參考一下別人的經驗。\n언택트 시대의 쇼핑, 네이버 쇼핑라이브 개발기\n主要內容 介紹從前身 Selective 演變成 Shopping LIVE 的歷史 如何快速開發 確實的？開發 安定地營運系統 Shopping LIVE 的現在與未來展望 1. Selective Service to Shopping LIVE Selective 一開始還沒有 LIVE 的功能，後來為了要試試 LIVE 是否有這市場，所以在兩個月內快速地開發了封測的版本 (CBT)，一個月後推出對外測試版本；然後再三個月後推出了正式版。\n2. 如何快速開發 原來的 Selective 服務是 monolith 型式，為了要讓 LIVE 服務可以快速推出，對於 LIVE 相關的實作，是建立在原先 Selective 服務之外，但中間會有模組是去跟原先的服務溝通，將 LIVE 需要的功能提供出來。\n針對主要的功能(直播、聊天、商品呈現)他們也儘量採用 NAVER 現有的技術，降低重新開發的時間。最後一張圖紫色的部分就是使用 NAVER 現有的技術。\n下面的流程則是說明怎麼透過 NAVER 提供的方案來完成由直播方開始一場直播，到顧客觀看直播。\n3. 確實的開發 第三點比較是偏向伺服器端的介紹，這裡只稍微帶一下最終的摘要。\nmonolith 的架構 → 及早開發和測試 主要功能切分成不同服務 → 能錯誤發生的地方隔離開來 IDC 由原先的單一位置，改為兩個地方(?)，提高系統的可用性 主要功能切分成不同服務 → 在開發時可以提高生產力而且系統更為穩定 Auto Scaling, Rate Limiting → 可處理流量爆衝的情況，擴大高可用性 4. 安定地營運 主要是提到關於防止系統出錯的相關測試，如何進行各項功能的 monitoring，以及如果系統出錯時，怎麼樣將使用者導到說明的畫面。\n5. Shopping LIVE 的現在與未來 ","permalink":"https://plateaukao.github.io/blog/posts/naver-deview-2021-shopping-of-untact-era-naver-shopping-live-development-story/","summary":"\u003ch3 id=\"shopping-of-untact-era-naver-shopping-live-development-storynaver-deview2021\"\u003e**Shopping of Untact era, Naver Shopping Live Development Story —**NAVER DEVIEW 2021\u003c/h3\u003e\n\u003cp\u003e這篇是 Naver Shopping Live Development Story 的摘要。前一陣子也有在做跟 LIVE Commerce 相關的案子，剛好這篇可以拿來參考一下別人的經驗。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://tv.naver.com/v/23651144\"\u003e언택트 시대의 쇼핑, 네이버 쇼핑라이브 개발기\u003c/a\u003e\u003c/p\u003e","title":"NAVER DEVIEW 2021 — Shopping of Untact era, Naver Shopping Live Development Story"},{"content":"安裝 NAVER 開發的瀏覽器 Whale 時，發現它有一個很好用的功能：可以同時在電腦畫面上顯示兩個網頁的內容，而且可以設定在其中一個網頁點擊任何連結時，都用另一邊來開啟。\n用 Naver Whale 瀏覽 LINE TODAY 新聞網站\n這種使用方式很適合在看新聞或是某些由 listview 要帶到 detailview 的場景。在 Android 的世界中，如果使用的是平板的話這功能就可以用來一邊開啟想要學習的網頁，另一邊開啟字典之類的網站。\n雖然類似的功能有機會利用 Android 的 Multi-Window 來達成，但因為目前有支援 Multi-Window 的電子書閱讀器只有文石(ONYX)，而且它是固定只能在畫面正中間切割，不能自行調整兩邊視窗的大小。所以，如果在 EinkBro 中也能支援同時顯示兩個網頁的話，應該就可以滿足我要的需求。\n開發 在 EinkBro APP 中已經開發過兩個畫面的元件了: TwoPaneLayout ，而且為了要支援全文翻譯的功能，有再把它包裝到 TranslationViewController 中，幫忙處理第二個畫面的隱藏顯示，或是左右換邊，轉換成上下切割等功能。(詳情請看下面這篇文章)\nAndroid 中可以調整畫面大小的雙視窗畫面(TwoPaneLayout) — EinkBro (12)\n在這個基礎上，要支援顯示不同的網頁內容就容易多了。首先是先把原本的 TranslationViewController 改名為 TwoPaneController ，因為接下來它除了要支援原本的翻譯功能外，還要負責用來顯示其他的網頁內容。雖然這樣寫有點打破 Single Purpose 的原則，但我不想要重覆太多的程式碼再生一個 class ，也不想要花時間抽出這兩者的共同部分用繼承的方式。所以，我選了最簡單的方式，在既有的 class 中加入相關的功能，並且把 class 名稱改得更 generic 一些。\n第一版的功能比較陽春一點，我在到處可以開啟的選單(書籤、歷史記錄、長按連結)中多了個按鈕 Split Screen。只要你想開啟網頁的地方，都多了這個按鈕可以將第二個畫面叫出來，並載入新的網址。\n調整 TwoPaneController 中的實作 原先有些按鈕是要給翻譯用的，所以如果第二個畫面被用來顯示一般網頁時，必須要隱藏一些按鈕。\n差不多這樣小改一下，功能就完成了！\n示範畫面 左邊是 Naver Webtoon 網站的漫畫；右邊是 Naver dict 網站，用來查詢不會的韓文單字\n示範影片 Demo of two web page in one screen in EinkBro APP\n程式碼 support split screen feature. · plateaukao/browser@f33b3ef\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%AF%E6%8F%B4%E5%90%8C%E6%99%82%E9%A1%AF%E7%A4%BA%E5%85%A9%E5%80%8B%E7%B6%B2%E9%A0%81%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8/","summary":"\u003cp\u003e安裝 NAVER 開發的瀏覽器 Whale 時，發現它有一個很好用的功能：可以同時在電腦畫面上顯示兩個網頁的內容，而且可以設定在其中一個網頁點擊任何連結時，都用另一邊來開啟。\u003c/p\u003e","title":"支援同時顯示兩個網頁的瀏覽器"},{"content":"學習韓文也有一段時間了，但從去年年中考完 TOPIK I 之後，感覺程度就一直停留不前。對於 3 級到 6 級要背的文法和單字，因為數量很多，而且又不再是那麼常用的單字，所以一直背得二二六六的。\n因為已經具備了很基本的閱讀能力，所以一直很希望接下來是透過閱讀其他有趣的內容來代替死板的教科書學習。雖然學習教科書會是比較有系統的吸收，但真的會比較難讓人堅持下去。\n不過，有趣的內容不好找，符合我粗淺程度的內容更難找。最近終於找到比較能夠長時間專心學習的內容，所以想說在這邊分享一下。\n由於本身工作是跟科技相關，所以平常涉獵的也都是科技類的訊息，跟一般人學習韓文是主要用來追星或是韓劇有些不同(雖然韓劇我也是會看啦)。科技方面的內容的話，我發現韓國當地企業舉辦的 conference 內容相當適合學習。\n一來 conference 分享的內容裡會有很多科技用語，而這類詞彙通常腦筋稍微轉一下就能聯想回英文的用子，二來分享的主題會是自己在工作上已經比較熟悉的領域，就算沒真的聽很懂，也可以從演講中的一些關鍵字配合圖文並貌的投影片，掌握講者想要表達的意思。\n再來看這類 conference 內容有一個更大的收穫是：韓國跟日本一樣，是技術比較封閉的國家；對於自己國家內開發的產品或服務，通常都只在自己的國家內推行，不會再往世界推廣。這類研討會的主要對象都是針對韓國當地的開發者發表的。事後頂多有些外媒的記者會寫很摘要的重點，但真正每一場分享的內容，是很少會出現其他語言的翻譯，或是有其他人寫相關的中/英文觀後心得的。如果我自己去看的話，等於是藉由韓文的能力，讓我接觸到更多的資訊。\n另一個內容來源是在 Youtube 上介紹科技新知的韓文頻道。原因也跟上面差不多：領域是我比較熟悉的，單字會比較少；內容是我有興趣的，可以長時間聽而不覺得無聊或想要中斷。\n前言似乎有點過長了。下面就列一下最近閒暇時比較有在看的內容吧。\n研討會 Naver DEVIEW 2021 DEVIEW 2021\n身為韓國最大的入口網站，提供的服務包山包海，幾乎生活所需的所有事情都可以在上面完成。由 Naver 推出的技術研討會也是很有可看性的。它所推出的內容，如果覺得講太快，可以將播放速度降低，只差沒有附上字幕而已。因為這些演講內容都看得出來是預錄的，或是有事前寫好稿子，所以在聽的時候，講者的咬字都會比較清楚，跟稍微放慢速度，很適合聽力的練習。真的聽不懂時，就看看投影片了解目前的重點是什麼，再試著去理解說出來的內容。\nKakao if(kakao) 2021 if(kakao)2021 기술 세션 모아보기🔎\nKakao 是韓國最大的通訊軟體。早期雖然是以聊天起家，但現在推出的服務也是包羅萬象，今年的市值也曾一度超越 Naver，它在韓國的影響力可見一斑。由 Kakao 推出的技術研討會自然也有許多值得學習和了解的內容。它一次推出了超過 90 個演講主題，每段演講的時間都不長，約莫在 12 分鐘至 30 分鐘左右。空閒時看一兩段還蠻合適的。\nKakao 的演講投影片都比 Naver 的要可愛多了，畢竟 Kakao 有許多角色可以使用，而且部分 Kakao 的演講內容是有字幕的！如果真的有聽不懂的地方，可以利用 Google Translate 拍下畫面翻譯字幕。當然這方法在聽不懂的比例很高時是不大適用的。\n科技類 Youtube 頻道 JoCoding\n기술노트with 알렉\n나도코딩\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%8E%A5%E8%A7%B8%E6%9B%B4%E5%A4%9A%E7%9A%84%E9%9F%93%E6%96%87%E5%85%A7%E5%AE%B9%E7%AE%A1%E9%81%93/","summary":"\u003cp\u003e學習韓文也有一段時間了，但從去年年中考完 TOPIK I 之後，感覺程度就一直停留不前。對於 3 級到 6 級要背的文法和單字，因為數量很多，而且又不再是那麼常用的單字，所以一直背得二二六六的。\u003c/p\u003e","title":"接觸更多的韓文內容管道"},{"content":"在開始玩衝浪滑板後，網路上瘋狂地找哪裡適合玩滑板。台北比較知名的有南港極限訓練中心，內湖的極限運動公園，最近新開放的美堤極限運動場(之前的美堤曲棍球場)；新北的話則是永和中正橋下的綠寶石極限運動場等。\n出了大台北地區，桃園新竹台中台南也都有些場地蠻有名的。但在這麼多場地中，我覺得最漂亮的應該就屬花蓮的北濱極限運動場吧！\n這裡的設施高度都比較高，適合已經有點滑行能力的人來玩。\n花蓮北濱極限運動場全景圖\n每個設施的介紹 1.5 人高的 ramp\n較淺的ㄇ型 ramp\n另一面是另一個 1.5 人高的 U 型 ramp，斜坡加上比較陡的ㄇ型 ramp\n中間則是小 ramp 和適合雙翹板的小平台\n站遠一點拍北濱極限運動場，遠方有山景可以欣賞，另一邊則是大海。\n整個場地漆滿藍色，配上藍天白雲，滑起來心情超好\n場地外圍也畫上了許多當地的景點：\n秀姑巒溪泛舟\n看海豚、太魯閣等\n金針花、向日葵\n北迴歸線地標\n最後附上一張自己在滑的到此一遊照片\n地址 其他場地介紹 衝浪滑板場地踩點心得 — 樹林極限運動場\n衝浪滑板場地踩點心得\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%A1%9D%E6%B5%AA%E6%BB%91%E6%9D%BF%E5%A0%B4%E5%9C%B0%E8%B8%A9%E9%BB%9E%E5%BF%83%E5%BE%97-%E8%8A%B1%E8%93%AE%E5%8C%97%E6%BF%B1%E6%A5%B5%E9%99%90%E9%81%8B%E5%8B%95%E5%A0%B4/","summary":"\u003cp\u003e在開始玩衝浪滑板後，網路上瘋狂地找哪裡適合玩滑板。台北比較知名的有南港極限訓練中心，內湖的極限運動公園，最近新開放的美堤極限運動場(之前的美堤曲棍球場)；新北的話則是永和中正橋下的綠寶石極限運動場等。\u003c/p\u003e","title":"衝浪滑板場地踩點心得 — 花蓮北濱極限運動場"},{"content":"使用 EinkBro APP 時，常會遇到網頁背景是黑色的，最常見的應該就是在開啟 ptt 的文章時。由於電子紙的特性，在黑底白字的使用場景下，很容易就會產生很多殘影，需要更頻繁地重新更新畫面。\n為此，我為 EinkBro 加上了將畫面轉為白底黑字的功能。這個功能的實作，其實沒有什麼特別的地方，主要是在選單中增加一個新的按鈕，可以透過它來切換這功能。\n而啟動這功能則是在 WebView 中加入自定義好的 CSS style。下面是部分程式碼：\n如果想看完整 commit 的話，可以參考下面這個連結：\nadd whiten background feature: especially useful for websites like PTT · plateaukao/browser@2c2e944\n下面是示範的影片：\n","permalink":"https://plateaukao.github.io/blog/posts/%E7%9C%8B%E7%B6%B2%E9%A0%81%E6%99%82%E6%94%AF%E6%8F%B4%E5%B0%87%E8%83%8C%E6%99%AF%E7%94%B1%E9%BB%91%E8%BD%89%E7%99%BD/","summary":"\u003cp\u003e使用 EinkBro APP 時，常會遇到網頁背景是黑色的，最常見的應該就是在開啟 ptt 的文章時。由於電子紙的特性，在黑底白字的使用場景下，很容易就會產生很多殘影，需要更頻繁地重新更新畫面。\u003c/p\u003e","title":"看網頁時支援將背景由黑轉白"},{"content":"衝浪滑板場地踩點心得分享 — 樹林極限運動場 樹林滑板公園\n環繞一圈的影片 場地 場地還蠻大的，有極限街道式廣場、U型半管區、水泥道具場。但場地的維護品質看不好。這裡的設施都是水泥做成的，上面再舖上類似 PU 的材質。可能是因為常下雨的關係，很多地方的 PU 塗層有裂縫或是受潮膨脹。在下過雨後，地上還半溼不溼時，如果滑經裂縫的地方，可能會有水漬從裡頭流出來沾到滑板，造成滑板打滑失控。\n所以下完雨後，如果地上還有水漬的話，最好是不要在這個場地練習，不然很容易仆街。這裡的設施比永和綠寶石極限運動場的都還要大一號，很適合在熟悉基本的小斜坡上上下下後，再來這邊練習。\n樹林滑板公園 U形 ramp\n從上面的照片中可以很清楚看到，斜坡上的 PU 有脫落的現象，坡底的 PU 也有浮起來的跡象。\n樹林滑板公園\n上圖是適合技術板的角落。(其實大部分的設施都是適合技術板練習)\n樹林滑板公園\n場地中央有個很大塊的鑽石台。以小朋友當比例尺，可以看得出來蠻高的。建議練習時還是戴上護具比較安全。\n樹林滑板公園\n從另一個方向看滑板場。上面照片的左下角可以看到有好幾個 bump，但造型不是做得很好。雖然可以用來加速前進，每個突起之間並不是很連貫。\n交通 就在土城交流道下來後，接 65 號快速道路走一小段過河，就可以來到這裡。假日早點出門，不塞車的話，從台北過來可能只需要二十幾分。算是交通很便利。而且公園內就有停車場，不用特地去找其他的停車場。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%A1%9D%E6%B5%AA%E6%BB%91%E6%9D%BF%E5%A0%B4%E5%9C%B0%E8%B8%A9%E9%BB%9E%E5%BF%83%E5%BE%97-%E6%A8%B9%E6%9E%97%E6%A5%B5%E9%99%90%E9%81%8B%E5%8B%95%E5%A0%B4/","summary":"\u003ch3 id=\"衝浪滑板場地踩點心得分享樹林極限運動場\"\u003e衝浪滑板場地踩點心得分享 — 樹林極限運動場\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/3591dd73f958/1__rPiDzkdjDPJyL-x4wqNXg.png\"\u003e\n\u003cem\u003e樹林滑板公園\u003c/em\u003e\u003c/p\u003e\n\u003ch4 id=\"環繞一圈的影片\"\u003e環繞一圈的影片\u003c/h4\u003e\n\u003ch3 id=\"場地\"\u003e場地\u003c/h3\u003e\n\u003cp\u003e場地還蠻大的，有極限街道式廣場、U型半管區、水泥道具場。但場地的維護品質看不好。這裡的設施都是水泥做成的，上面再舖上類似 PU 的材質。可能是因為常下雨的關係，很多地方的 PU 塗層有裂縫或是受潮膨脹。在下過雨後，地上還半溼不溼時，如果滑經裂縫的地方，可能會有水漬從裡頭流出來沾到滑板，造成滑板打滑失控。\u003c/p\u003e","title":"衝浪滑板場地踩點心得 — 樹林極限運動場"},{"content":"EinkBro App 中的實作大都是用很舊很舊的技術。雖然隨著功能不斷增加，我有逐漸把一些檔案翻新成 Kotlin，和盡量把相關的邏輯抽出到獨立的 class 或檔案中，不過整體來說，架構還是很老派(其實就是沒有什麼架構，全部的邏輯幾乎都塞在同一個 Activity 中)。\n前不久才導入了 DI library Koin，讓部分元件可以用注入的方式在程式中的各個地方能夠存取得到。今天要介紹的是如何利用 Room + Flow 的組合，讓畫面上的書籤列表可以自動更新，不用在每個有 CRUD 的場景手動呼叫。\n步驟 首先要在 build.gradle 中加入 ViewModel 的支援 implementation ‘androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1’ 在 Dao 類別中，加入會回傳 Flow 的函式。原本雖然已經有回傳 List 的函式，但型式是 suspend，所以可以透過 coroutine 拿到非同步的結果，但拿完之後，並不會收到事後的任何更新。如果想要再次拿到資料庫中最新的資料，得要再呼叫一次才行。但改成回傳 Flow 的話，當資料庫有修改時，利用 Flow 的人都還是會收到通知。 @Query(\u0026#34;SELECT * FROM bookmarks WHERE parent = :parentId ORDER BY title COLLATE NOCASE ASC\u0026#34;) fun getBookmarksByParentFlow(parentId: Int): Flow\u0026lt;List\u0026lt;Bookmark\u0026gt;\u0026gt; 建立 ViewModel，以便資料傳遞給 UI 層 class BookmarkViewModel( private val bookmarkDao: BookmarkDao ): ViewModel() { fun bookmarksByParent(parentId: Int): Flow\u0026lt;List\u0026lt;Bookmark\u0026gt;\u0026gt; = bookmarkDao.getBookmarksByParentFlow(parentId) } class BookmarkViewModelFactory( private val bookmarkDao: BookmarkDao ) : ViewModelProvider.Factory { override fun \u0026lt;T : ViewModel?\u0026gt; create(modelClass: Class\u0026lt;T\u0026gt;): T { if (modelClass.isAssignableFrom(BookmarkViewModel::class.java)) { @Suppress(\u0026#34;UNCHECKED_CAST\u0026#34;) return BookmarkViewModel(bookmarkDao) as T } throw IllegalAccessException(\u0026#34;Unknown ViewModel class\u0026#34;) } } 在 BrowserActivity.kt 中建立 bookmarkViewModel，以利後面使用 private val bookmarkViewModel: BookmarkViewModel by viewModels { BookmarkViewModelFactory(bookmarkManager.bookmarkDao) } 當要開啟書籤列表時，把 bookmarkViewModel 傳進去，讓它可以跟 adapter 串在一起。 再來就是 adapter 的初始化部分。193 行到 197 行是這個重構最主要的關鍵。bookmarkViewModel.bookmarksByParent() 會回傳 Flow 物件；在這邊利用 collect 來取得結果，並將它代入 adapter 中。當資料庫有改變時，collect 會再次傳呼叫 195 行，讓 adapter 會再次代入新的資料，從而達到更新畫面的效果。當 submitList 送來新的資料時，BookmarkAdapter 因為有實作 DiffUtil.ItemCallback\u0026lt;Bookmark\u0026gt;()，它會只針對有更新的部分做變化。 經過上述的修改，書籤列表的呈現就會在資料庫有改變時，自動更新畫面。之前在加新書籤，刪除書籤，或是修改書籤時實作的手動改變 adapter 內容的實作，就可以全部拿掉啦。\n目前 EinkBro App 中還有其他地方有使用到資料庫，像是瀏覽記錄，擋廣告白名單等，但大都還沒有改成用 Room 儲存到資料庫中，希望以後有空時，可以把所有資料庫相關的處理和呈現也都利用這種方式重構，減少不必要的手動更新畫面的邏輯，程式也可以看起來更加地簡潔。\n參考資料 Android 官方的教學 比我的說明清楚許多。一步一步照著作就可以完成我上面的那些內容。\nhttps://developer.android.com/codelabs/basic-android-kotlin-training-intro-room-flow#1\nEinkBro App 的修正 commit https://github.com/plateaukao/browser/commit/771925da495b5e2995f060591d803498ee3da0b1\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-android-%E4%B8%AD%E5%88%A9%E7%94%A8-room-flow-viewmodel-recyclerview-%E8%AE%93%E7%95%AB%E9%9D%A2%E4%B8%8A%E7%9A%84%E5%88%97%E8%A1%A8%E8%87%AA%E5%8B%95%E5%8F%8D%E6%87%89%E8%B3%87%E6%96%99%E5%BA%AB%E7%9A%84%E8%B3%87%E6%96%99%E6%9B%B4%E6%96%B0/","summary":"\u003cp\u003eEinkBro App 中的實作大都是用很舊很舊的技術。雖然隨著功能不斷增加，我有逐漸把一些檔案翻新成 Kotlin，和盡量把相關的邏輯抽出到獨立的 class 或檔案中，不過整體來說，架構還是很老派(其實就是沒有什麼架構，全部的邏輯幾乎都塞在同一個 \u003ccode\u003eActivity\u003c/code\u003e 中)。\u003c/p\u003e","title":"在 Android 中利用 Room + Flow + ViewModel + RecyclerView 讓畫面上的列表自動反應資料庫的資料更新"},{"content":" 綠寶石極限運動體驗場\n拿到板子後，很期待到可以滑滑板的地方練習。以下列出自己去練習過的地點，跟它們的優缺點，供大家作參考。\n內湖極限運動公園 由於上班地點在內湖，下班時要滑滑板的話，在內湖會是最方便的。剛好內湖運動公園就在公司附近，開車過去不用十分鐘。短短兩週我就來了兩次。第一次來的時候，天空也開始飄著雨，當天忘了帶護具出門，只有板子在車上，所以什麼都沒戴就開始滑。結果，滑個幾分鐘後，就因為下雨場地的木板和鐵板太溼，上坡後要轉頭下坡時板子整個滑掉了。左腳和右手肘撞到了地面，掛彩了。\n隔週再來第二次時，就學乖了，該戴的護具全戴上了，而且沒有在下雨。人也因為這樣子多了不少。我在圖一的場地一直練習上下坡的正向轉彎流暢度和平衝感。到快熄燈(晚上十點)前，才有到圖二的 Ramp 去練習來回 pumping。在圖二的 U型板上練習時才發現，原來這動作那麼吃大腿的肌肉。要盪得高，就要壓得深；要壓得深，在下坡時大腿就要不斷地出力，讓速度變得更快。通常我來回個十次，大腿就沒力了，得放慢速度停下來，先休息好一陣子，才能再繼續。\n雖然 U型板很好玩，但這裡的 U 型板品質很不好，每塊木板間的接縫，很多是不平整的，很容易會微微卡到滑板，要自己想辦法避開，或是有技巧地滑過去。對於還不是那麼會玩 U 型版的人來說，要考慮的事情太多了。\n在這邊，因為練習的人蠻多的，所以自己滑累了，可以在一旁滑滑手機，或是看別人練習的情況，比較不會無聊。以後只要沒下雨，應該都還是會來這邊練習吧。\n這兩週最終的進展是，可以從圖三右邊快一個人高的斜台下滑下來，然後成功地在左邊的U型斜坡上迴轉。\n能在斜坡上 pumping 的感覺真的很舒服。不過，我得要再多練練大腿的肌力才行。\n內湖極限運動公園 圖一\n內湖極限運動公園 圖二 Ramp\n內湖極限運動公園 圖三\n永和綠寶石極限運動體驗場(中正橋滑板場) 一大早來這邊的話，車子很好停，可以就停在路邊，而且還不會有什麼小朋友出現。斜坡坡度都不高，但對於滑板初學者來說，也是個很好的練習場地。地板比柏油路光滑，滑起來很順，但是又不像 PU 地面抓地力那麼強。\n那個馬蹄形長條的 ramp，個人覺得坡度怪怪的，不是很好上上下下，所以我大部分都是在那兩個鑽石斜坡間來來回回。\n這裡的娛樂性沒有內湖運動公園來得高，但因為離家裡比較近，所以想小練一下的話，這裡會比較方便。\n綠寶石極限運動體驗場\n綠寶石極限運動體驗場\n後來幾週有再來這個中正橋滑板場，在比較熟悉衝浪滑板的玩法後，這個公園就變得比較好玩了，坡度也不再覺得很陡，能夠很順利地上坡和下坡。\n這裡有個好處是號稱 24 小時有路燈，晚上即使很晚也可以來練習。下面兩張是晚上八九點實際拍攝的照片。馬蹄形 ramp 區偏暗 (實地比照片再暗一些)，但勉強可以看清地形，足以練習。另一區的兩個鑽石台，因為路燈就在旁邊，亮度很夠。不過由於前來練習技術板的小朋友都會在那邊練翻板和跳障礙物，所以衝浪滑板比較不會在那區活動。\n永和中正橋滑板場\n永和中正橋滑板場\n微樂山丘 — 自行車躍動體驗廣場 陽光橋往小碧潭方向滑行的話，幾分鐘就可以來到著名的微樂山丘。雖然這是給小朋友的滑輪車玩的場地，但因為坡度都很小，其實也很適合衝浪滑板上上下下的。只是，如果速度不夠快的話，常常沒有辦法一口氣滑完整圈。平常都會有小朋友在玩，所以我都是選比較晚的時候，或是早上很早的時候來這邊。\n微樂山丘\n微樂山丘\n新北市新店陽光運動公園 週間因為白天還要上班，只能利用下班時到附近的河濱稍微練習一下。最先選擇練習的地方是陽光運動公園，和它的陽光橋。陽光橋在下橋的部分，接近平地的地方比較平緩，比較容易練習滑行，又不用自己出太多力。而且它的路面很寬，要做轉向和減速也比較容易。等練熟之後，在車子少，人少的時候，也是可以從橋的上方一路滑到橋下。\n新北市新店陽光運動公園\n新北市新店陽光運動公園\n這裡還有個很大的溜冰場，不過晚上通常都會有小朋友在進行直排輪教學，所以不見得能在裡面滑滑板。但是，等到九點左右，所有小朋友的課程就結束了，整個大大的溜冰場愛怎麼滑就怎麼滑。和一般公園裡小小的溜冰場不同的地方是，陽光運動公園溜冰場的兩側是有做弧度的(很緩的斜坡)，很適合衝浪滑板衝上去，然後轉板下來。因為場地面積很大，所以轉不動時，順著慢慢下坡也很安全。\n唯一的小缺點時，場內有蠻多地方有沙子的，如果剛好經過這些區域然後在做轉彎的動作的話，可能會打滑。有可能是因為前幾天有下雨的關係吧。\n新店陽光運動公園滑板場\n大新店游泳池旁的空地 在進入大新店游泳池的那條路上有停車場。停車場旁走到河濱道路間有一塊很大的空地是只能人跟腳踏車通過的。雖然是柏油路，但舖得很平整，很適合讓衝浪滑板練習前進。一旁還有大樹和石椅可以休息時遮蔭。空地跟河濱間是微微的下坡，可以練習左右壓 edge ，慢慢前進。\n大新店游泳池\n羅東極限運動場 趁著雙十連假，去了一趟羅東，想說可以試試當地的極限運動場。但出門前因為台北連續好久都是好天氣，竟然沒有先確認一下天氣就出發了。到了之後發現竟然羅東在飄雨。不死心地硬是拿著板子到運動場，還沒滑一分鐘，雨就變大了。結果離開飯店時也沒順便拿雨傘，滑板沒滑到，整個人卻淋成了落湯雞。看來只好下次好天氣時再來一次了。\n羅東極限運動場\n社區籃球場 最近疫情狀況減緩，也開始適度地開放這類場地。只要選對時間來，就可以一個人包場練習。只是因為什麼設備都沒有，單純就只能練練直行而已，比較無聊。\n另外在網路上查到了很多適合衝浪滑板玩的場地，只是礙於時間(才剛拿到板子兩週而已)，還無法一一造訪。所以，我都先將它們加到了 Google Map 列表中，希望能在不久的將來一個一個去試試。這也是目前能讓自己走出家門的最大動力了吧。\nGoogle Map 地圖分享 Daniel Kao\u0026rsquo;s \u0026ldquo;滑板場地\u0026rdquo; List\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%A1%9D%E6%B5%AA%E6%BB%91%E6%9D%BF%E5%A0%B4%E5%9C%B0%E8%B8%A9%E9%BB%9E%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/f89811632f73/1_4zo2DGqC1E47m-fVwDWMcA.png\"\u003e\n\u003cem\u003e綠寶石極限運動體驗場\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e拿到板子後，很期待到可以滑滑板的地方練習。以下列出自己去練習過的地點，跟它們的優缺點，供大家作參考。\u003c/p\u003e\n\u003ch3 id=\"內湖極限運動公園\"\u003e內湖極限運動公園\u003c/h3\u003e\n\u003cp\u003e由於上班地點在內湖，下班時要滑滑板的話，在內湖會是最方便的。剛好內湖運動公園就在公司附近，開車過去不用十分鐘。短短兩週我就來了兩次。第一次來的時候，天空也開始飄著雨，當天忘了帶護具出門，只有板子在車上，所以什麼都沒戴就開始滑。結果，滑個幾分鐘後，就因為下雨場地的木板和鐵板太溼，上坡後要轉頭下坡時板子整個滑掉了。左腳和右手肘撞到了地面，掛彩了。\u003c/p\u003e","title":"衝浪滑板場地踩點心得"},{"content":"購買衝浪滑板 YOW PIPE 32\u0026quot; 在網上看了很多介紹衝浪滑板的影片，也稍微知道了有哪些牌子，但對於它們之間的差異，還是沒有很了解。後來主要是看了下面兩個影片，才稍微有了點概念。雖然這個 Youtuber 不是專業的滑板手，也沒在衝浪，但他的介紹方式讓我聽得比較懂。所以在聽完之後，我就決定我要買 YOW 的板子了。\n如何選擇 在影片中主要介紹了YOW, Smoothstar, Carver, Waterborne 和 SwellTech。\nSwellTech 的前輪的旋轉幅度最大，可以360度；要平衝真的抓得很好才能滑得好，很容易就噴出去。\nYOW 和 Smoothstar，就像是衝浪滑板牌子中的 Mercedez Benz 和 BMW，一來價錢比較高，二來，也是操控性最好的。Smoothstar是專門做來給想要練習衝浪感覺的人滑的；他的板面比較彎，腳比較容易固定在上面。而 YOW 的板面是幾乎平的，比較適合長距離的前進。YOW 有兩種 truck，一個是 S5，一個是新出的 Meraki。影片作者最喜歡 YOW 的板子，但 S5 truck 在滑動時會有雜音，而 Meraki 沒有。\n再來的 Carver，也是很多人的選擇。它的 truck 跟前兩者比起來比較緊。影片作者說它 跟 Slide 像是滑板界的 Toyota 和 Honda。\n因為我沒在衝浪，所以不特別需要像衝浪感覺的 Smoothstar，而我又不想要 truck 太緊，所以我最終決定買 YOW 的板子。\n哪裡購買 由於最近疫情的關係，而且平常還要上班，我又不是很想要出門，所以只能先從網路上找得到的購買管道查起。目前台灣有在網路上販賣的店家似乎不多，大部分的滑板店也都還是在實體店面販售比較多。\n而且，有在賣 YOW 滑板的線上商店，就又更少了！終於我找到了一間有在賣 YOW 牌子的店家 — LTS (Live to Surf / Live to Skateboard)。至於哪一間比較便宜，或是哪一間的貨比較多，其實我也沒有特別去做比較，能找到有在賣的店家就已經很了不起了。\n決定要買 YOW 板子後，每個牌子的款式也都是多到眼花燎亂，所以我單純就花色，板長，和 truck 種類做選擇。只要 truck 是 Meraki 系統，然後板長不要太長，花色不要不喜歡，應該就是可以入手的選擇。\n最後，我選了 YOW PIPE 32\u0026quot;。大紅色的，還蠻顯眼的，也沒有太多花俏的圖案。然後，似乎我買了它最後一塊板，買完這個款式就售完了。:D\nPIPE 32″ (整組) - YOW 衝浪滑板\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%B3%BC%E8%B2%B7%E8%A1%9D%E6%B5%AA%E6%BB%91%E6%9D%BF-yow-pipe/","summary":"\u003ch3 id=\"購買衝浪滑板-yow-pipe32\"\u003e購買衝浪滑板 YOW PIPE 32\u0026quot;\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/99679ed85b5e/1_13T4PGQeMvUabfodZUVqaw.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e在網上看了很多介紹衝浪滑板的影片，也稍微知道了有哪些牌子，但對於它們之間的差異，還是沒有很了解。後來主要是看了下面兩個影片，才稍微有了點概念。雖然這個 Youtuber 不是專業的滑板手，也沒在衝浪，但他的介紹方式讓我聽得比較懂。所以在聽完之後，我就決定我要買 YOW 的板子了。\u003c/p\u003e","title":"購買衝浪滑板 YOW PIPE"},{"content":"今年因為開發 EinkBro App，寫了不少與之相關的開發記錄文章。剛好有同事建議可以參加看看鐵人賽，就報名了。在之前完全不知道國內有這種活動，看來我平常接觸的資訊太少了。\n鐵人賽，顧名思義，是要長時間進行一項活動的比賽。雖然名為比賽，但我想大部分參賽的人應該都只是想要自我挑戰吧：連續三十天，針對自己報名時提出的主題，每天寫一篇文章。\nhttps://ithelp.ithome.com.tw/2021ironman\niThome 鐵人賽的主題範圍很廣，從 AI, Data，到 DevOps, IT 管理等領域，都可以報名參加。因為我要寫的是 EinkBro App 的實作內容說明，所以我報名的是 Mobile Development。報名時間是八月多，正式開始進行是九月一日到九月十五日，每個人可以決定從哪一天開始。一旦開始發文後，就要連續三十天發文。\n覺得不好的地方 網頁呈現方式 雖然這是個以發表技術文章的平台，但網頁的呈現風格很像是早期的網站。標題部分，中規中矩，但累計文章數，參賽組數，團隊組數和完賽人數這些數字，就那麼大剌剌地呈現在畫面的正中間，每次載入網頁時，還要很畫蛇添足地跳動一下每個數字，讓它看起來是不斷增加到現在的實際數字。可以理解主辦方是想要讓它看起來比較有活力一點，有在比賽的感覺；但對於參賽者來說，每天都會來看一下，或是好幾次，然後每次都要看到那些數字一直閃，看兩三次後就覺得很礙眼。\n網站雖然有分別做了電腦版和手機版的呈現方式，但在要進入個人的鐵人賽文章系列時，它都會先載入電腦版的畫面，然後隔一下子才切換成手機版的畫面。很奇怪為什麼不行一次到位。這個畫面也是幾乎每天都會連進來看自己前幾天寫好的文章，有時候會需要參考；然後就一直會看到它在兩個版本間切換。而且電腦版的資訊比手機版的還要詳細，有累計瀏覽人數，每篇文章的觀看人數，和追蹤人數等等，但在手機版畫面，就全被拿掉了。這些資訊並不會佔太多空間，搞不懂為什麼要刻意在手機版畫面移除。\n文章中的某些標籤看起來很像是可以點的，像是 2021 iThome 鐵人賽，Mobile Development，但其實只是標示的文字，點了沒有作用。XXX 系列也是一樣，原以為點了可以回到文章列表，但它真的就只是文字而已。\n文章編輯器 iThome 提供的文章編輯器支援 Markdown 語法，這點還不錯。通常我會先在 Mac 上利用 Joplin App 來編輯要發表到鐵人賽的文章。Joplin 也支援 Markdown 的語法，所以我在 Joplin 中寫作時，就已經能大概看得出來到時候發表文章時的長相。但除了這點外，對於它的編輯器，我也是有一堆小地方想要抱怨的。\n上傳圖片很麻煩。對於已經習慣 Github 的上傳照片方式 (直接拖拉本地圖片到文字輸入框，它就會自動上傳，並產生對應的圖片連結語法)的我來說，每次要上傳照片時，要先捲動畫面向上，露出畫面上方的工具列，然後再點擊上傳圖片的按鈕。這時，會跳出檔案選擇畫面，然後再從電腦中選擇要上傳的檔案。上傳後，會跳出上傳完成的畫面，得再按 OKAY，才會真的幫你把圖片加到文章中。如果想上傳十張照片的話，就得這麼重覆十次。這麼繁複的動作，讓寫作效率大大降低。所以我在準備文章時，都會先依照天數，把當天需要的圖片放到特定的目錄中，然後等文章貼到官方的編輯器後，再跳到需要圖片的每個地方，一張一張照片選擇，和上傳。 官方的線上編輯器有提供兩種模式，一個是寫作模式，一個是預覽模式，在切換上還算順手。每當寫到一個段落後，都會想要先儲存一下目前的進度。儲存草稿時，畫面中間都會跳出大大的儲存成功的畫面，雖然隔一秒左右就會自動消失，但還是覺得很礙眼。對於這種常會進行的動作，不能把它做得局部一點嗎。 活動相關說明 在活動簡章中只有看到活動何時開始，何時結束；但是並沒有說結束之後的流程是什麼，和相關的時間點。\n因為活動最晚可以從九月十五日開始，所以最後完成鐵人賽的參賽者，會陸續完賽一直到十月十五日。但十月十五日之後，評審什麼時候開始進行，評審多久，何時公布最終的結果，我都沒有看到相關的說明。\n我在九月三十日完賽後，也只有網站上的通知有一則 “恭喜您！您的鐵人系列文「如何開發適合電子書閱讀器使用的瀏覽器 Android APP」成功完賽。鐵人鍊成不易，堅持到最後挑戰成功實在太厲害了，趕快跟朋友分享這份榮耀吧！”\n沒了，就這樣。\n因為不確定接下來什麼時候會有新的消息，所以得要每隔一兩天就上網站看看有沒有更新什麼資訊。\n雖然上述抱怨了一堆，但，還是要說有廠商舉辦這樣子的活動，讓大家有機會在一個平台上磨鍊自己的發表能力，或是整理自己的學習經驗，讓其他人也能因此受益，整件事還是很值得鼓勵的。\n然後，在參賽的過程中，有順便看看其他人寫的內容。有的人真的寫得很專業，但也有很多人是來打醬油的。自己一篇一篇看的話太花時間了。應該還是要等最終評審結果出來後，再找那些有獲獎的參賽者文章來看就好。\n如何開發適合電子書閱讀器使用的瀏覽器 Android APP :: 2021 iThome 鐵人賽\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%83%E5%8A%A0-ithome-%E9%90%B5%E4%BA%BA%E8%B3%BD%E7%9A%84%E6%84%9F%E6%83%B3/","summary":"\u003cp\u003e今年因為開發 EinkBro App，寫了不少與之相關的開發記錄文章。剛好有同事建議可以參加看看鐵人賽，就報名了。在之前完全不知道國內有這種活動，看來我平常接觸的資訊太少了。\u003c/p\u003e","title":"參加 iThome 鐵人賽的感想"},{"content":"快上加快 — EinkBro (15) 原以為這麼小的一個 App，不到 3MB (扣除擋廣告的資料檔)的實作，既沒有任何的 tracking 機制，也沒有去網路抓任何非網頁內容的資料回來，理論上網頁載入速度要快到不行才對。\n但這半年來使用時，總覺得在切換網頁時，那條進度條總是會卡在 10% 的地方一下下，才有繼續往下進行。一開始以為是因為要去找每個 url 的 dns 位址，所以慢了半拍。但是別的瀏覽器又沒有這樣子的毛病。\n移除造成載入緩慢的邏輯 終於！在昨天晚上陰錯陽差的解其他問題時，讓我仔細看到了 updateProgress() 的實作。看完後差點沒吐血。當 WebView 在載入網頁時，會不斷回報目前的載入進度，這時，這個函式就是用來更新相關畫面的。但它除了更新畫面的進度條外，竟然還做了很多意想不到的事情。\n除了 1108 行到 1114 行是在更新進度條的呈現，1114 行的 updateAutoComplete() 是去資料庫中撈取新的資料出來，以備使用者要手動輸入網址時，可以提供相關的資訊；scrollChange() 則是在設定 WebView 的 ScrollListener。而 HelperUnit.initRendering() 是在檢查要不要把畫面顏色整個做反轉。\nupdateAutoComplete() 這件事，完全不需要在這邊出現。只要等使用者點下網址列，想要開始輸入時再去做一次就好了(如果那時候覺得有需要)。它被放在這邊，每次載入網頁，都會被呼叫許多次，在資料庫中不斷地查重覆的資料出來。如果使用者記錄了很多書籤，和瀏覽記錄已經累積了好幾百筆幾千筆的話，這樣子每個呼叫都要花不少時間。\n這也難怪我會覺得每次在載入網頁時，進度列總是會卡住一下，因為 updateAutoComplete() 正在瘋狂地跟資料庫索取資料。把它移走後，果然速度就有了很大幅度的提升！\n至於 scrollChange()，這種只需要在 WebView 生成的時候，或是網頁完全載入的時候呼叫一次就好的函式，我也把它搬到網頁載入完成後的邏輯中。\n最後的 HelperUnit.initRendering() ，我就整段拿掉了。因為我的 EinkBro 已經把這功能拿掉了。\n最終的實作變成這樣：\n限制歷史記錄的容量 剛剛提到造成載入速度慢的其中一點是：在更新 AutoCompleteTextView 所需的資料時，會去從資料庫讀取書籤和歷史記錄。在原本的實作中，歷史記錄是無上限的。\n也就是說，如果你使用了 EinkBro 半年的話，你的 App 中包含了你這半年來的所有瀏覽記錄，這將會造成每次在讀取資料庫時，時間會愈來愈長。\n為了解決這問題，我在每次新增瀏覽記錄時，會去把 EinkBro 中超過 2 週的歷史記錄刪掉。透過這方式來限制瀏覽記錄的無限增長。\n做了這些改善後，就再也不用等候慢慢的載入速度啦！\n參考版本 Release Version v8.16.1 · plateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%BF%AB%E4%B8%8A%E5%8A%A0%E5%BF%AB-einkbro-14/","summary":"\u003ch3 id=\"快上加快einkbro15\"\u003e快上加快 — EinkBro (15)\u003c/h3\u003e\n\u003cp\u003e原以為這麼小的一個 App，不到 3MB (扣除擋廣告的資料檔)的實作，既沒有任何的 tracking 機制，也沒有去網路抓任何非網頁內容的資料回來，理論上網頁載入速度要快到不行才對。\u003c/p\u003e","title":"快上加快 — EinkBro (14)"},{"content":"可自訂的工具列 — EinkBro (14) 隨著開發的功能愈來愈多，工具列的空間已不足以將所有的功能都排在上面；而且也不是每個功能都是使用者會常用的功能。為了要解決這個問題，工具列開始支援自訂，讓使用者可以自訂工具列上的功能和排序方式。如果能在上面放上最常用的功能，使用者就不用頻繁地開啟層層選單找尋功能按鈕。\n我們先來看看一般瀏覽器的工具列都長得怎樣\nChrome / Firefox / NeoBrowser\nChrome 包含了幾個常用的功能，Onyx 電子書設備內建的 NeoBrowser 跟 Chrome 蠻相似的；Firefox 則是相當簡潔，幾乎沒有任何按鈕可用。\n擴充工具列界面 原本的工具列只是在一個水平的 LinearLayout 中，放入多個 ImageButton，為了避免使用者塞入太多按鈕，造成工具列超過畫面寬度，我們先在外面包上一層 HorizontalScrollView。\n在畫面初始化時，我們就會先把所有的按鈕生成出來；之後再根據使用者的設定，將其重新排列和隱藏不必要的按鈕。\n對於工具列的操作，因為有點多，所以另外寫了一個 ToolBarViewController 來處理。其中比較重要的函式是 reorderIcons()。\nfun reorderIcons() { toolbarActionViews.size val iconEnums = config.toolbarActions // 先移除全部按鈕，再依照設定中的資訊來重新塞入按鈕 if (iconEnums.isNotEmpty()) { iconBar.removeAllViews() iconEnums.forEach { actionEnum -\u0026gt; iconBar.addView(toolbarActionViews[actionEnum.ordinal]) } // Settings 這個按鈕如果不小心被使用者刪除了，這裡會強制將它顯示出來 // 因為沒有 Settings 按鈕，就無法再進到工具列設定的畫面。 if (ToolbarAction.Settings !in iconEnums) { iconBar.addView( toolbarActionViews[ToolbarAction.Settings.ordinal]) } // 重新配置工具列的呈現 iconBar.requestLayout() … } } toolbarActionViews 則是在畫面生成的時候，先把裡頭的每個按鈕元件的 reference 都先記錄下來。\nprivate val toolbarActionViews: List\u0026lt;View\u0026gt; by lazy { val childCount = iconBar.childCount val children = mutableListOf\u0026lt;View\u0026gt;() for (i in 0 until childCount) { children.add(iconBar.getChildAt(i)) } children } 建立可拖拉排序的選項對話框 這一部分的實作需要一個可以拖拉的 ListView。花了點時間在網路上找到堪用的元件 DragSortListView，將它置於對話框 ToolbarConfigDialog 內，並且依照使用者儲存的設定，初始化它的狀態。\n然後，為了儲存和管理所有的工具列按鈕，建立了 ToolbarAction 的 Enum：\n全部實作完後，只要再在設定畫面中，再加上一個工具列設定的按鈕就行了！BrowserActivity 裡設定 onClick 的實作\nR.id.toolbar_setting -\u0026gt; ToolbarConfigDialog(this).show() 下面是完成的工具列設定對話框\n下面是我在 Onyx Nova 3 Color 上常用的設定\n標題/刷新/返回/觸控翻頁/放大/縮小/翻譯/分頁列表/書籤/直排/閱讀模式/設定/旋轉畫面\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%8F%AF%E8%87%AA%E8%A8%82%E7%9A%84%E5%B7%A5%E5%85%B7%E5%88%97-einkbro-13/","summary":"\u003ch3 id=\"可自訂的工具列einkbro14\"\u003e可自訂的工具列 — EinkBro (14)\u003c/h3\u003e\n\u003cp\u003e隨著開發的功能愈來愈多，工具列的空間已不足以將所有的功能都排在上面；而且也不是每個功能都是使用者會常用的功能。為了要解決這個問題，工具列開始支援自訂，讓使用者可以自訂工具列上的功能和排序方式。如果能在上面放上最常用的功能，使用者就不用頻繁地開啟層層選單找尋功能按鈕。\u003c/p\u003e","title":"可自訂的工具列 — EinkBro (13)"},{"content":"第十一回 –(으)ㄹ지、–는데요、–는걸요 【中級韓国語講座 第11回】-(으)ㄹ지、-는데요、-는걸요\n[–(으)ㄹ지]「〜するか」→表示對前面子句的疑問\n歐爸能不能在約定的時間到達呢，感到擔心。\n[-는데요]「〜ですよね」→加重語氣，傳達意料之外發生的事感到驚訝。或是請求對方多做說明的講話方式。\n와, 오늘 날씨가 굉장히 더운데.哇，今天天氣也太熱了吧。\n몇 시에 나가는데? 都幾點了，還要出去？\n[-는걸요]「〜だからですね」→傳達知道新事物的驚喜之意。表示沒什麼大不了的，或輕微的反駁對方的意見。\n날씨가 점점 추워지는걸.天氣漸漸變冷了呢。\nㄱ: 혼자 다 할 수 있겠어요?\nㄴ: 괜찮아요. 이 정도는 아무렇지도 않은걸요.沒問題的，這種程度，沒有什麼大不了的。\nㄱ: 도와줘서 정말 고마워요.\nㄴ: 아니에요. 당연히 해야 할 일인걸요.沒這回事，這是應該的啦。\n第十二回 -는가(요)?、-는가 보다、-(으)ㄴ들 【中級韓国語講座 第12回】-는가(요)?、-는가 보다、-(으)ㄴ들\n第十三回\n第十四回\n第十五回\n第十六回\n第十七回\n第十八回\n第十九回\n第二十回\n第十一回至第二十回測驗\n","permalink":"https://plateaukao.github.io/blog/posts/%E3%83%88%E3%83%AA%E3%83%AA%E3%83%B3%E3%82%AC%E3%83%AB%E3%81%AE%E3%83%88%E3%83%9F%E4%B8%AD%E7%B4%9A%E9%9F%93%E5%9B%BD%E8%AA%9E%E8%AC%9B%E5%BA%A7-%E7%AD%86%E8%A8%98-%E4%BA%8C/","summary":"\u003ch4 id=\"第十一回-으ㄹ지는데요는걸요\"\u003e第十一回 –(으)ㄹ지、–는데요、–는걸요\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://trilingual.jp/intermediate/20210322-12912/\"\u003e【中級韓国語講座 第11回】-(으)ㄹ지、-는데요、-는걸요\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e[–(으)ㄹ지]「〜するか」→表示對前面子句的疑問\u003c/p\u003e\n\u003cp\u003e歐爸能不能在約定的時間到達呢，感到擔心。\u003c/p\u003e","title":"トリリンガルのトミ中級韓国語講座 筆記 (二)"},{"content":"\n這一系列的教學，經過半年多，也來到了三十幾課。邊聽邊忘的結果是，還得要回過頭來再復習前面的部分。既然要復習，就隨手把摘要記錄下來。如果得要再回頭復習第二次的話，就能事半功倍了。\n另外，由於講座內容都是日文的，有些時候如果日文沒學到位的話，可能日文和韓文的掌握度都會沒到位，所以在學習時，還是會對照著”我的第一本韓語文法”的內容來看。以下內容，如果在”我的第一本韓語文法”初級篇、進階篇或是高級篇有出現的話，會採用類似 (進階篇單元 3–2) 的方式來提供對照，方便翻閱和學習。\n系列講座簡介 中級韓国語講座が始まります！単語2600個＋文法150個！\n初級講座如果都完成後，對於韓文應該就有了基本的認識，日常的打招呼和單純的對話應該足以應付。但如果要再向前一步的話，就要把基本詞彙擴大，並且學習更多常用的句型和文法。中級講座裡有文件列出了 2600 個單字，和總共 150 個文法。每次講座會教三個文法，並要求學員背 40 個單字。當 60 回講座上完後，應該能把 2600 個單字和 150 文法都學齊。\n第一回 間接表達的 아/어 달라고 하다、아/어 주라고 하다、(ㄴ/는)다면서요？ 【中級韓国語講座 第1回】間接話法の아/어 달라고 하다、아/어 주라고 하다、(ㄴ/는)다면서요？\n在(進階篇單元 5)引用別人的話或文章時，有相同的文法出現。表示間接說話者的意圖，給予，或是說話者確認聽說的內容時使用的文法。\n第二回 間接表達的 (ㄴ/는)다고 해서、(ㄴ/는)다、(ㄴ/는)다는 【中級韓国語講座 第2回】間接話法の(ㄴ/는)다고 해서、(ㄴ/는)다、(ㄴ/는)다는\n(ㄴ/는)다고 해서 因為誰誰誰說了什麼，所以…\n(進階篇單元 3 陳述體)，有提到 (ㄴ/는)다 是用於報導、報告等進行陳述時使用，也可以用於寫日記時。對話時也會用到，通常是自言自語時，或是對朋友以及較年輕的人使用。\n(ㄴ/는)다는 指的是前面講的這件事實。\n第三回 半語體 【中級韓国語講座 第3回】반말(タメ口)のいろいろ表現！肯定、疑問、命令、勧誘\n(進階篇單元 3-02) 半語體有同樣的教學。主要提到\n陳述句的 [-야/이야]\n禁止時用的 [-지 마]\n表達疑問的 [-니] 和 -냐\n命令型式的 [-아/어라]\n勸誘的 [-자]\n第四回 表示使動的用法、아/어 놓다/두다、아/어 버리다 【中級韓国語講座 第4回】使役動詞、아/어 놓다/두다、아/어 버리다\n(進階篇單元 10)也有教到表示使動時的用法。通常是在動詞後面接上[-이-, -히-, -기-, -리-, -우(ㅣ우)-]。但是 하다 的情況，則是要代入 [-시키다]。\n-아/어 놓다と-아/어 두다 ～しておく這中文有點難理解，但日文一看就懂。前後兩者意思一樣，前者只能用於動詞。(進階篇單元 18–01，18–02)\n-아/어 버리다 ～してしまう 這個用法也是，日文有很明確的對應文法。(進階篇單元 22–03)\n第五回 慣用句、四字熟語、답다/스럽다 【中級韓国語講座 第5回】慣用句、四字熟語、답다/스럽다\n(進階篇單元19 表示屬性和性質時 02, 03)\n답다: 主要前面會有名詞。[남자답다]、[사람답다]、[학생답다]、[어른답다]，表示具有本來就應該有的資格。\n스럽다:前面會放名詞，或是部分語幹。[어른스럽다]、[고급스럽다]、[자랑스럽다]、[의심스럽다]。表示看起來好像具有前面名詞含有的屬性、資格。\n第一回到第五回的測驗 中級韓国語講座【第1回～第5回】確認テスト\n第六回 아/어 가지고、를/을 가지고、거든요 https://trilingual.jp/intermediate/20210215-12706/\n[-아/어 가지고] →「〜して」，主要用於口語，跟아/어서 的意思相同。(進階篇單元16–02) 以其結果做後子句的行為。\n[-를/을 가지고] →「〜で」、「〜をもって」表示方法和手段，跟 [-로/으로]很像，但主要是用在口語。\n[가지고]的部分可以簡化成 [-를/을 갖고]。\n[-거든요] →「〜なんですよ」 (進階篇單元4–01) 用於回答對方提問，或是陳述自己本人的想法時，這些理由和事實是對方不知道的。\n第七回 았/었었다、-는 걸 보니까、-는 김에 https://trilingual.jp/intermediate/20210222-12731/\n[았/었었다]過去完成式\n[-는 걸 보니까] →「〜するのを見ると」\n[-는 김에] 趁著在做什麼事的時候~\n第八回 (으)ㄴ 채(로)、(으)ㄹ 만큼、는 반면(에) https://trilingual.jp/intermediate/20210301-12792/\n[-(으)ㄴ 채(로)] → ~著。不行接過去式或未來式。\n그는 눈을 감은 채 소파에 누워 있었다. 他眼睛閉著，躺在沙發上。\n[-(으)ㄹ 만큼]→表示程度，到達某個程度\n그 방에는 발 디딜 틈이 없을 만큼 사람이 많이 있었다. 這房間人多到沒有地方站的地步。\n[발 디딜 틈이 없다] → 慣用語，連站的地方都沒有。\n[-는 반면(에)]→~的反面\n그 친구는 한국어를 잘하는 반면에 영어는 좀 못해요. 這朋友的韓文很好，但另一方面英文不太行。\n第九回-는/(으)ㄴ 법이다、-는 탓에、-는 모양이다 https://trilingual.jp/intermediate/20210308-12830/\n[-는/(으)ㄴ 법이다]→表示當然。\n기대가 크면 실망도 큰 법이죠. 期待愈大，失望也愈大，是當然的事。\n[-는 탓에]→因為什麼的緣故(不好的事情)\n술을 너무 많이 먹은 탓에 머리가 아파. 因為酒喝多了，頭在痛。\n[-는 모양이다]→~的樣子。(自己看了之後，感覺到的事情)\n사람들이 우산을 쓰는 걸 보니까 비가 오는 모양이에요. 看到人人都開了傘，似乎要下雨了的樣子。\n第十回 –(으)ㄹ 줄(을) 알다/모르다、-(으)ㄴ 줄 알았다/몰랐다 https://trilingual.jp/intermediate/20210315-12855/\n[–(으)ㄹ 줄(을) 알다/모르다]→(不)知道怎麼做。可以學習而來的能力。\n넌 춤출 줄도 몰라? 你連跳舞也不會？\n옛날에는 글자를 읽을 줄 모르는 사람이 많았대요. 從前，看不懂字的人很多的樣子。\n[-(으)ㄴ 줄 알았다/몰랐다]→還以為~，沒想到~\n엄마가 청소한 줄 알았다. 還以為媽媽打掃了。\n엄마가 청소한 줄 몰랐다. 沒想到媽媽打掃了。\n第六回到第十回的測驗 https://trilingual.jp/intermediate/20210315-12874/\n","permalink":"https://plateaukao.github.io/blog/posts/%E3%83%88%E3%83%AA%E3%83%AA%E3%83%B3%E3%82%AC%E3%83%AB%E3%81%AE%E3%83%88%E3%83%9F%E4%B8%AD%E7%B4%9A%E9%9F%93%E5%9B%BD%E8%AA%9E%E8%AC%9B%E5%BA%A7-%E7%AD%86%E8%A8%98-%E4%B8%80/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/746f8d92ca27/1_fK7NUUN-yuZHIp8qnUVy_A.png\"\u003e\u003c/p\u003e\n\u003cp\u003e這一系列的教學，經過半年多，也來到了三十幾課。邊聽邊忘的結果是，還得要回過頭來再復習前面的部分。既然要復習，就隨手把摘要記錄下來。如果得要再回頭復習第二次的話，就能事半功倍了。\u003c/p\u003e","title":"トリリンガルのトミ中級韓国語講座 筆記 (一)"},{"content":"\n買了不少電子紙的產品，包含6吋到13吋的閱讀器，以及5.8吋，6.7吋的手機。用來用去，其實一天看最久的電子產品還是上班時在用的傳統螢幕。當看了八九個小時的螢幕後，即使下班都只用電子紙產品來看書或是看網頁，眼睛的疲勞感已經存在，再怎麼縮短時間，幫助也不是很大。\nOnyx BOOX Max3 嚐鮮 在今年五月前，市面上只有一家公司(大上)在販售用電子紙做成的螢幕，有13吋和25吋兩種大小。25吋的是天價，可以直接跳過；13吋的價格也比文石13吋的電子書閱讀器來得貴。文石的 13 吋閱讀器輕薄而且閱讀 App 又寫得很好，所以當初買了一台來，想說除了看 pdf 外，也可以透過它的 hdmi 埠連上 iMac，達到跟大上 13 吋螢幕的效果。\n大上科技-DASUNG\n但，其實差遠了。雖然文石 13 吋閱讀器有 HDMI 接頭，連接電腦當外接螢幕的效果卻很不好：一來因為解析度不行調到合適的大小，畫面字體不夠清晰；二來因為畫面更新率太低，滑鼠很很很不跟手。常常滑動滑鼠後，要等個500毫秒或1秒，才能知道最終滑到哪裡；如果滑過頭了或還沒到位，又要再重新移動一次，再重新看一次結果。用了幾次後，雖然偶爾還是拿它來當外接螢幕使用，但應用場景很受限。因為大部分情況下都會需要用滑鼠操作。\nOnyx BOOX Mira 今年五月底六月初，文石終於也推出了類似的產品：電子紙的純螢幕(Boox Mira, Boox Mira Pro)。沒有內建系統讓你可以看書或上網，也沒有內建的電池。你只能將它連上電腦或其他行動設備(手機或平板)，讓它來幫你顯示這些設備的畫面。13 吋的價錢，一樣是很高價位。但相比於大上同等級的產品，文石的價錢親民許多，甚至比 13 吋的電子書閱讀器還便宜。(想想也算合理，因為少了作業系統的開發和維護，也少了大大的電池，唯一要專注的地方是把電子紙當成螢幕的這件事做好)\nhttp://zh.boox.com/mira/\n購買 上市是一回事，能不能買得到就又是另一回事了。上市後，先是只有中國可以預購。等預購結束後，常常會進入缺貨的狀態。之前電子紙產品我大都是在京東上購買，然後直送台灣；但針對文石 BOOX Mira 這款產品，不知道是不是因為產量太低還是其他因素考量，一直沒有對台灣開放。在京東上只有兩家直營店有列出這台設備，但常常是在缺貨的狀態，即使有貨，也會顯示”台灣無貨”。我只能不定期地上京東網站逛逛，看何時會對海外開放，對台灣開放。\n前幾週有位日本網友在推特上說，他訂購了。看到他的訊息後我再上京東網站查詢，一樣是無貨的狀態。直到上週，這位日本網友好心地跟我說，好像京東上又有存貨了，而且台灣可以購買，我才趕快手刀下單。\n由於不是京東官方的產品，而是在它上面的店家購買的，所以不行走官方的直送方式。京東類似淘寶一樣，有集貨的運送方式，可以先將商品送到集貨區，再由合作的第三方貨運公司轉送到台灣來。當然，因為多了這一步，集貨轉送是要收運費的(之前直送是不用額外付運費)。這是我第一次嘗試在京東上集貨，好在它的介面還蠻好懂的，在下單兩天後，貨就送到了集貨區，在 App 中可以進入集貨後付運費的畫面。除了螢幕外，還送了保護包、平板架、保護膜、藍牙搖控器、USB碟，總共需要 85 人民幣的運費。我是覺得還在可以接受的範圍內。\n從下單到拿到貨，大概花了七天左右，比京東直送約慢了一天，但比台灣的某些小店家的運送速度還是快了許多。外包裝是軟的，雖然包很了多泡泡紙，但 BOOX Mira 的硬紙盒的某一個邊角還是稍微有被擠壓到。好在實機保護得很好，開箱時一切完好。\n開箱 Boox Mira 的盒子裡附件很少，除了主機和已經磁吸上去的保護套兼立架外，還附了一本小小薄薄的各國語言說明書(看來有打算進軍國際)和兩條線。一條是 USB Type-C to Type-C，另一條是 HDMI 連結線，但有一頭是比較小的頭。\n剛開完箱時，還在想說怎麼接上電源，但實際上操作時，連接 iMac 完全不需要另接電源線，只要將 USB Type-C to Type-C 接上就會看到畫面有反應。其他人的解析度設定是建議 1280x 960 或是 1600x1200，但我之前在設定 BOOX Max3 時有設定過 HiDPI 的解析度，所以我選了 1100x825(HiDPI)，感覺比前兩者的顯示效果更好。\nBOOX Mira 機子上，左邊有個功能鍵，右邊有個撥動輪，畫面右下方有個刷新按鈕。在還沒有看簡易說明書前，隨便按一按就可以抓到怎麼操作，算是蠻容易理解的。看到別人的心得文都有提到，在電腦上(Windows, Mac)可以裝 BOOX Mira 的設定軟體，做很多的畫面顯示調整，但我翻遍了說明書，也沒找到哪裡有提到怎麼下載這軟體。找了許久後才在台灣的 Onyx BOOX 網站一篇文章中找到下載的位置(但明明台灣還沒有進貨啊，為什麼會先發布怎麼將 BOOX Mira 調整成最佳效果的文章)：\n如何將 BOOX Mira 顯示器效果調成最佳狀態？讓我來教教你！\n如果你也是買了 BOOX Mira，卻苦尋不著軟體下載的位置的話，你就來對地方了：\n下載連結\n[更新 2022/08/13] 為了要利用 Mira 來開發程式，寫了一個給 IntelliJ 和 Android 使用的 Eink theme。有興趣的人可以直接到 github 下載。\n開發 IntelliJ IDE 的 theme plugin\n[更新 2022/08/03] 有的時候，將 Mira 連接到 M1 的 Mac mini 時，還是會看到畫面有閃爍的情況發生。這時，可以開啟 Mira 在 Mac 上的設定程式，在左下角的通用設定中，手動再設定一次 ”自動防抖動” 到高(就算原先的設定就是高了，還是要再選擇一次)。然後，你就會發現畫面不會再閃爍了。\n在 Mac 中輸入文字時，文字的位置會有一條直線 (cursor) 不斷閃爍，隨著直線的閃爍，畫面的其他部份也會跟著抖動起來。想要避免這件事的話，可以透過 command line 把直線 (cursor)的重繪頻率設得很大，基本上它就不會動來動去了，只會直直地顯示靜態的一條線。\ndefaults write -g NSTextInsertionPointBlinkPeriodOn -float 100000\ndefaults write -g NSTextInsertionPointBlinkPeriodOff -float 1\n如果想要復原的話，可以輸入下面的指令。\ndefaults delete -g NSTextInsertionPointBlinkPeriodOn\ndefaults delete -g NSTextInsertionPointBlinkPeriodOff\n以上資訊來自於\nChange OS X text cursor blink rate\n[更新 2022/06/26] 在使用 Android Studio 或是 IntelliJ 時，除了可以先把它的主題套上 light 的版本外，對於 Editor 中的主題，還可以再下載下面這個主頭，然後在 preference 中的 editor 中套上。這麼一來，它就會用底線，粗體，或是文字框，等方式來表示不同的情境。\nEink Color Scheme - IntelliJ IDEs Plugin | Marketplace\n[更新 2022/06/10] 沒想到在沉寂了快一年的 Mira 設備，竟然在 2022 年 6 月又推出了新的韌體！如同下圖說的，這次改善了顯示的效果。這樣寫雖然很模糊，但實際體驗上，它修正了在 Apple M1 CPU 設備上的閃爍問題 (還是會有一點，但大部分場景下，並不是很明顯)，加深了文字的對比度，而且整個畫面的反應又進步了一些。\n以我自己的使用場景來看，目前用來寫程式，真的是可以完全只看 Mira，不用時不時拉回彩色螢幕看特定的文字格式或標示了。\n上圖是我利用 Mira 在開發 EinkBro 的畫面，可以看得出來不論是 IDE 的介面，或是 code syntax 的文字標示，都很清楚。\n我比較常用的是圖像模式和極速模式。兩者給我的感覺差不多：滑鼠的跟手性極佳，以及畫面的殘影控制得很好。雖然在一陣子畫面更新後，還是會有可見的殘影，但搭配著自己設定好的鍵盤快捷鍵，時不時更新一下就行了。\n[更新 2021/12/30] 目前官方已經有放出下載桌面軟體的網頁了[在這裡]，建議從這裡下載最新版本的 Mira App。在 Mac 上，原先的 v1.0.9 或是更舊的版本，雖然會顯示有新的 firmware 可以升級，但當 Mira 設備升級完後，其實完全看不出來是不是升級了，讓人一頭霧水。最近新的 Mira App for Mac v1.1.2 ，終於修正了這個問題，在 firmware 版本列表中會標示目前 Mira 設備的版本是哪一個；也修正了一進 Mira App 就一直出現連線中的畫面，無法正常使用。\n另外，Mira App 中還多了一個”閱讀模式”，更加適合用來看靜態的文件，因為它會加深文字的粗細和對比度；但滑鼠反應明顯地慢了很多。\n使用心得 將螢幕接上電腦後，應該就可以開心地使用了。在其他使用者的心得分享文中可以看到各種軟體的主題設定，讓它們可以在電子紙畫面上有更好的對比。如果你也有相關需求的話，可以參考文章最下面提供的連結，這邊只在補充一點點我自己的額外增加的設定和心得。\nFirefox 除了設定一個比較適合電子紙的主題或是安裝 extension 可以快速切換主題外，還可以把輸入文字時的 caret 設定成不要閃爍。一開始我也不是很在意這件事，但自從有位使用者提及這設定之後，在電子紙畫面上我就也很希望 caret 不要一直閃。明明就一條直線或方塊顯示出來就好，一直閃真的有點礙眼。\n首先在網址列輸入 about:config ，然後新建兩個數字值\nui.caretBlinkTime 0 ui.caretWidth 2 第一行是讓 Firefox 不再閃爍，第二行是讓 caret 粗一點，比較分辨得出來。\n英文詳細說明可以看這兒：\nHow to disable caret blinking in textboxes in Firefox?\niTerm2 iTerm2 也是可以停止 caret 閃爍，設定的地方在下面右邊的 Blinking cursor：\n在主螢幕和電子紙螢幕間移動視窗 — Rectangle Rectangle 這工具不光是對電子紙螢幕外接有用，對於一般的外接螢幕也很有幫助。它可以讓你利用鍵般快速鍵直接把視窗移到另一個螢幕去，也可以很快速地改變視窗在畫面中的大小。一般螢幕單純用滑鼠去移動，並沒有什麼太大的差別，頂多是慢了點，或是移過去後還要手動將視窗放大成全螢幕。但在使用電子紙螢幕時，雖然它的更新率已經是快得跟一般螢幕差不多了，但免不了的會留有殘影，所以如果能讓它少重畫一點，就能少點殘影，就能少點重新刷新畫面的次數。\n各種顯示方式的快捷鍵設定\nGitHub - rxhanson/Rectangle: Move and resize windows on macOS with keyboard shortcuts and snap…\nAndroid Studio 在設定中，可以反勾選 Caret blinking。\n觸控螢幕 買這款螢幕的最主要目的是減少看發光螢幕的時間，能不能點著螢幕操作，倒是可有可無，但既然它提供了，還是可以想想怎麼樣好好利用這功能。它只支援單點觸控。點在畫面的當下，它其實就送出了 touch down 的事件，所以無法利用觸控來滑動畫面，只能利用觸控來選取畫面的文字；而長按畫面會送出滑鼠右鍵的功能。這兩者的組合可以完成一些基本的操作，比方說選取網頁的文字，開啟選單，選擇翻譯或拷貝字串。\n觸控操作還有另一個好處是，就不用在畫面上一直滑動滑鼠，造成電子紙螢幕的局部重畫。\n在連接的電腦中使用 Android 模擬器 為什麼要介紹這個呢，因為我自己寫的 Android 瀏覽器 App — EinkBro 有蠻多針對電子紙開發的功能，所以我常常會在電腦中開啟模擬器來使用它。現在接上電子紙螢幕後，一樣可以把模擬器拉到這個螢幕中來操作。這時，神奇的事發生了。在模擬器中，Boox Mira 的觸控是可以用來捲動畫面的！\n這麼一來，在 Boox Mira 畫面中操作模擬器的感覺，其實就跟在操作一般的 Android 平板很像，重點是：它的 CPU 是強大的桌上型電腦的 CPU，跑什麼動作都是飛快！\n以上是使用一兩天的心得。之後，其實它就是個螢幕，在使用時忘了這件事，開心地正常操作就行。\n相關連結： [開箱分享] 設定淺色主題 (Light theme) - 感受 文石 Boox Mira 13.3\u0026quot; 純黑白體驗 - 清新下午茶\n[開箱分享] 文石 Boox Mira 13.3吋 電子紙顯示器 - 神級刷新的 E-ink 電子紙螢幕 上手體驗 - 清新下午茶\nMira Eink Display Setup\nMira 一周体验\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3-boox-mira-%E7%9A%84%E8%B3%BC%E8%B2%B7%E6%96%B9%E5%BC%8F%E8%88%87%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/c3a7f8bb1d53/1_kRn72d25A4x9kdzehMjJaA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e買了不少電子紙的產品，包含6吋到13吋的閱讀器，以及5.8吋，6.7吋的手機。用來用去，其實一天看最久的電子產品還是上班時在用的傳統螢幕。當看了八九個小時的螢幕後，即使下班都只用電子紙產品來看書或是看網頁，眼睛的疲勞感已經存在，再怎麼縮短時間，幫助也不是很大。\u003c/p\u003e","title":"文石 BOOX Mira 的購買方式與使用心得"},{"content":"之前在 EinkBro 全文翻譯的作法是：為了要避免丟進翻譯網頁的文字內容充滿太多非本文的部分，會在事前先利用 Reader mode 將網頁內容淨化，然後再把 Reader mode 的內文餵到翻譯網頁中。這樣子雖然可以解決畫面過於淩亂的問題，但畢竟網頁的格式被簡化了，不見得是使用者想要的觀看方式。\n前幾天發現，原來目前 EinkBro 支援的 Google Translate 以及 Naver Papago 兩種全文翻譯網站，除了可以餵字串給它們之外，也可以直接餵網址給它們；這麼一來它們就會輸出一個格式一模一樣的網頁，只是文字內容都被翻譯成設定的語言。\n這功能真的太棒了！我原本還一直想要找出 Chrome App 是怎麼實作全文翻譯的，想從那個方面下手，把相關的功能實作搬到 EinkBro 中。現在就不用那麼麻煩了，直接利用現在的翻譯網頁來設定參數操作就可以。\n在前一篇全文翻譯的文章中有提到大概的步驟是：\n在畫面中開啟另外一個 WebView，讓它佔有畫面的一半。 將網頁轉為 Reader mode，並從中抽出文字的部分。 將文字部分做 pagination，根據目前的頁數餵進全文翻譯的網站，讓它幫忙翻譯出設定的語言。 打造 E-ink 專用的瀏覽器 (X) — 支援全文翻譯對照\n關於步驟 1，由於有實作了 TwoPanelLayout ，在設定上更為簡便；步驟 2 和 3 在這次功能的實作上就可以直接跳過，只要將當下網頁的 url 當成參數帶入全文翻譯的網站就行。\n以 Google Translate 網頁來說，它的網址支援下面幾種參數設定：\nsl (source language) tl (target language) text (text content to be translated) // 新發現的參數 u (web url to be translated) 所以，在舊有的實作上，只要省去原本的 text 參數，改為代入 u 的參數，就可以得到跟原始網頁長得一樣的翻譯結果，而且不用再額外考慮單純翻譯文字內容時，會有字數超過限制的問題，整份網頁都會被翻譯到。\n如此一來，在對照閱讀上就更加方便。\n網頁翻譯的結果畫面 ","permalink":"https://plateaukao.github.io/blog/posts/%E5%85%A8%E6%96%87%E7%BF%BB%E8%AD%AF%E5%8A%9F%E8%83%BD%E5%86%8D%E9%80%B2%E5%8C%96-einkbro-13/","summary":"\u003cp\u003e之前在 EinkBro 全文翻譯的作法是：為了要避免丟進翻譯網頁的文字內容充滿太多非本文的部分，會在事前先利用 Reader mode 將網頁內容淨化，然後再把 Reader mode 的內文餵到翻譯網頁中。這樣子雖然可以解決畫面過於淩亂的問題，但畢竟網頁的格式被簡化了，不見得是使用者想要的觀看方式。\u003c/p\u003e","title":"全文翻譯功能再進化 — EinkBro (13)"},{"content":"目前在 M1 chip 的 Mac 設備上，可以很方便的安裝 iOS App，執行上也都沒有什麼問題，除了有些 App 因為還沒有針對 MacOS 做處理，所以畫面大小不能很彈性地調整。如果是 Android App 的話，就沒那麼好命了；要在 Mac 設備上執行的話，還是得透過 Emulator 來執行。\n目前比較流行的是 Blustacks 和 Genymotion，還有開發者最常用的 Android Emulator (常常會讓風扇高飛)。前兩者我在 MacOS Moneterey 上安裝都有些問題，所以還是只能退而求其次，利用 Android Emulator 來執行 Android Apps。\n之所以要在 MacOS 上執行 Android App 的原因是：自己寫的 EinkBro App 可以雙開全文翻譯，用來看外文網頁相當方便。在 MacOS 上要做到一樣的功能的話，得要自己手動開兩個 Chrome Window，同時連到同樣的網址，然後將其中一個 Chrome 全文翻譯成其他語言。偶爾如此操作還可以；但如果每天都有一堆文章想要這麼觀看的話，就會覺得很花時間。這時，如果能夠在 MacOS 上直接開啟 EinkBro App 來看網頁的話，就可以很方便地以我習慣的方式瀏覽網頁。\n利用命令列啟動 Android Emulator Android Emulator 有支援命令列的呼叫方式, 詳細參數資訊，可以參考官網的說明：\nhttps://developer.android.com/studio/command-line/avdmanager\n可以先用下面指令找出目前系統有建立的 Android Emulators:\navdmanager list avd 以我的情況，系統會列出下面資訊：\nAvailable Android Virtual Devices: Name: Nexus_10_API_30 Device: Nexus 10 (Google) Path: /Users/danielkao/.android/avd/Nexus_10_API_30.avd Target: Google APIs (Google Inc.) Based on: Android API 30 Tag/ABI: google_apis/x86_64 Skin: 2560x1600 Sdcard: 512 MB --------- Name: Pixel_XL_API_21 Device: pixel_xl (Google) Path: /Users/danielkao/.android/avd/Pixel_XL_API_21.avd Target: Based on: Android 5.0 (Lollipop) Tag/ABI: default/x86_64 Skin: pixel_xl_silver Sdcard: 512M The following Android Virtual Devices could not be loaded: Name: Pixel_3_XL_API_30 Path: /Users/danielkao/.android/avd/Pixel_3_XL_API_30.avd Error: Google pixel_3_xl no longer exists as a device --------- Name: Pixel_3a_OS_8_27 Path: /Users/danielkao/.android/avd/Pixel_3a_OS_8_27.avd Error: Google pixel_3a no longer exists as a device 因為我想要大畫面的 Emulator來顯示網頁，所以我要啟動的是 Nexus_10_API_30 。我可以利用下面的指令來啟動它：\nemulator -avd Nexus_10_API_30 了解怎麼用指令來啟動 Emulator 後，算是完成了第一個步驟。但我真正想做的是，可以在 MacOS 的 Dock 上有個 icon 讓我點一下就啟動，而不是總是要開啟 Terminal 來執行指令，所以，下面會講怎麼讓指令能包裝一下，搬到 Dock 中。\n建立 Dock 上的 指令 Icon 在 Dock 上新增 Icon\n這件事比較容易達成。可以先建立一個文字檔，將其副檔名改為 .app 就可以成功將它拖拉到 Dock 上。以我的情況來說，我將檔名取為 emulator_tablet.app ，然後在裡面輸入以下內容：\n#!/bin/zsh ~/Library/Android/sdk/tools/emulator -avd Nexus_10_API_30 其實到這邊，就已經達到我要的目的了，但是還有一點不完美的地方是：Dock 上預設的 App Icon 有點醜，看不出來是要做什麼用的。\ndefault Dock icon: not recognizable\n更改指令 App 的圖案 為了讓 Dock 上的圖案更有辨識性，我們可以為文字檔加上圖案。首先，利用 command + i 叫出檔案訊息，然後可以在訊息框的最上方看到目前的圖案。我們要做的就是把下載好的圖案，拉到預設的圖案上，讓系統採用新的圖案。\n從上圖可以看到，右邊就是系統預設的圖案；而右邊的 emulator_tablet.app 的圖案則是我已經置換過的樣子。\n自己要畫出這些圖案不是件難事，好在網路上已經有人幫忙收集了很多 icon 供使用者下載使用。我是到下面這個網站找到合適的 icon：\nOver 5000+ free icons for macOS Monterey, Big Sur \u0026amp; iOS - massive app icon pack\n換完圖案後，這時再把檔案從 Finder 中拖拉到 Dock 上時，就會是新的圖案。\n調整 Android Emulator 外觀 由於我建立的是 Nexus 10 的 Emulator，預設在畫面下方會有系統的 navigation bar；但其實在畫面右端也有了相關的動作按鈕 (Back, Home, Recent Apps)，所以我想要移除畫面下方的 navigation bar，讓整個模擬器看起來更像是 MacOS 上的 App。\n作法可以參考下面這篇 StackOverflow 的介紹：\nHow to remove button bar at the bottom screen\n進到 ~/.android/avd/your_device_name.avd/config.ini ，然後把下面兩個值改成 yes 就可以了。\nhw.mainKeys=yes hw.keyboard=yes 大功告成！ 經過上述的調整後，在 Mac 上就能像是操作一般 App 的方式來啟動 Android Emulator 囉。省下了我重新開發 MacOS App 的時間。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-macos-%E5%BF%AB%E9%80%9F%E5%95%9F%E5%8B%95-android-emulator/","summary":"\u003cp\u003e目前在 M1 chip 的 Mac 設備上，可以很方便的安裝 iOS App，執行上也都沒有什麼問題，除了有些 App 因為還沒有針對 MacOS 做處理，所以畫面大小不能很彈性地調整。如果是 Android App 的話，就沒那麼好命了；要在 Mac 設備上執行的話，還是得透過 Emulator 來執行。\u003c/p\u003e","title":"在 MacOS 快速啟動 Android Emulator"},{"content":" 可以調整畫面大小的 Custom View\n在 EinkBro App 完成全文翻譯的功能後，使用上相當愉快，能夠快速地看左右對照翻譯完後的結果。但是用著用著，又覺得有那麼一點點不順手。原因是目前的實作方式是將畫面左右各切一半，左邊是原本網頁內容，右邊是翻譯後的結果。如果我是用海信 A7 手機在看網頁的話，由於手機的形狀是長形的，會造成兩邊的畫面相當窄，只能縮小字型來提高可見的文字量。有些時候會懶得一直對照著看。這時就希望視窗大小是可以調整的：想看翻譯時，可以把翻譯畫面變大；想看原文時，可以把原本網頁的部分變寬。甚至是，如果能改成上下分割的話，就更完美了！\n為了達到這樣子的功能，我先是在網路上找了找，想看看是不是有現成的元件可以幫我做到這樣子的效果。但是繞了一大圈，這看似很常見的功能，卻找不到其他人有實作過類似的元件。(不然就是我關鍵字下錯了吧)既然沒有現成的，自己寫一個 Custom View 應該也不會太困難才對，畢竟，不就一個 container 中塞兩個 View，然後再加個 drag handler 來處理畫面大小的調整。\nCustom View — TwoPaneLayout 為了讓這功能將來能夠應用到其他 App 中，我把它寫成一個 Custom View，然後再套用到 EinkBro 中，避免它跟 EinkBro App 綁太深，相依性太高。\n首先，我建立了一個 TwoPaneLayout 的 class，繼承自 FrameLayout。在 Android 中繼承 View，如果是用 Kotlin 開發的話，可以用下面的寫法，把傳統的三個 constructor 都涵蓋到：\n程式碼 I\nconstructor 中的第 23 行是處理這個 Custom View 特有的屬性。待會兒下面會有更多的說明。\n第 25 行的 initViews() 被包在 doOnLayout 中的原因是：初始化 View 時會需要知道元件被賦予的寬跟高，所以得先等 onLayout 完成後才拿得到。\n版面配置 畫面中兩個視窗的內容是需要使用者自己設定進來的，所以沒有辦法一開始預先建立。但是為了讓畫面可以調整大小，TwoPaneLayout 中要顯示一個可以拖拉的元件才行；另外，在沒有拖拉時，為了避免畫面兩側的邊界不是很明顯，我還加了一條淺淺的線在中間，讓使用者稍微看得出來兩者間的界線。這兩個元件都是事先產生好的。為此，我建了一個 two_pane_layout.xml\n程式碼 II\n17 行的 ImageView 是一個長長的方塊，平常時它的透明度是 30%。一旦使用者開始拖拉它，我會將它以及 11 行的 View (一條線) 都調整成全黑的，讓使用者感覺到拖拉是有作用的。\n新增專用的 View 屬性 這在網路上找得到的 Custom View 教學中都會看到怎麼新增。一般會是在 values/attr.xml 中加入自定義的 declare-styleable element。以下是我針對 TwoPaneLayout 想要提供的參數加入的內容：\n程式碼 III\n第 4 行: 當畫面建立時，是不是直接顯示第二個視窗\n第 5 行：當拖拉 drag handler 時，是不是即時更新畫面大小 (在電子紙的情況下，會需要關閉這功能，避免畫面一直閃), 還是等手放開時才更新。\n第 6 行：要垂直切割畫面，還是橫向切割。\n這些參數建立好後，在實際使用 TwoPaneLayout 時就可以在 layout xml 中指定想要的初始值。範例如下：\n程式碼 IV\n上面的 layout 會建立一個雙視窗的畫面，預設第二個畫面也會顯示，切割方式是橫向的(會產生左右兩個畫面，左邊是 panel1 ，右邊是 panel2)；在拖拉時，畫面大小會即時更新。\n讀取 attrs.xml 定義好的參數 剛剛在程式碼 I 中的第 23 行有看到，初始化 TwoPaneLayout 時，會順便把 layout xml 中設定的參數都讀進來。\n程式碼 V\n第 79 行會將 xml 中的參數讀出來變成一包資料，再利用 getBoolean(), getInt() 等方式將它們轉成 class 中的變數以供後續初始化的執行。\n功能實作 接下來會稍微說明一下各個功能是如何實作出來的。一樣一樣來看的話，其實都不會太複雜。\n顯示/隱藏第二個視窗 為了要做到顯示或隱藏第二個視窗，我們要先找出使用者塞進來的兩個 View。這件事是實作在 initView() 中：\n程式碼 6\n在講解 layout 時有提到，我們會事先建好分隔線(separator)，拖拉長方塊(dragHandle)，所以只要把事前建好的這些 View 排除掉(第 119 行)，剩下的兩個 View 就(應該)是使用者塞進來的兩個元件。如果數量不是 2 的話，那就天下大亂了，因為目前我沒有做任何錯誤處理。\n將這兩個 View 分別記入 panel1 和 panel2 參數，便可以根據剛剛讀來的 shouldShowSecondPane 值決定是否顯示。\n程式碼 7\n如果要顯示的話，panel 1 和 pane2 的大小在一開始會先各分一半畫面的寬度(橫向的情況)。緃向的話，則是各分一半畫面的高度(第163行，省略)。由於畫面的分割位置會隨著拖拉後有所改變，所以 showSubPanel 需要代入目前調整後的位置。\n拖拉後調整畫面大小 這部分是整篇文章的精華。在長方塊(drag Handler) 被拖拉時，會收到 Touch 相關 event。針對這些 event 我們要記錄下來相關的變化，然後反應到畫面上。\n程式碼 8\n這邊一樣是以橫向的例子來說明。第 221 行到 228 行會先將分隔線和拖拉長方塊初始化。在橫向時，長方塊要是直的，分隔線也要直的；在緃向時，長方塊要是橫的，分隔線也要是橫的。\n第 230 行開始，實作 dragHandler 的 TouchListener。當收到 ACTION_DOWN 時，長方塊要變成全黑的；接著，不斷收到 ACTION_MOVE 時，要調整 drag Handler 的位置和 finalX 的值。如果 dragSize 是設定為 true 的話，便要直接調整畫面大小(第 249 行)。當最後收到 ACTION_UP ，使用者手離開畫面時，再調整一次畫面大小(第 254 行).\n切換畫面切割方向 這功能的實作很單純，把 orientation 值換掉，再重新初始化就行了。\n程式碼 9\n切換兩個畫面的位置 這只要把 panel1 和 panel2 對調就行。\n程式碼 10\n套用到 EinkBro App 中 在 EinkBro 中，預設是不會開啟全文翻譯畫面，而且在拖拉時，不要即時更新畫面。所以在 xml 中是這麼寫的：\n程式碼 11\n第 316 行和 321 行分別是顯示網頁的 WebView 和負責全文翻譯的另一個 WebView (和翻譯時需要的一些小按鈕)。\n另外，跟翻譯相關的邏輯全部都寫在一個 TranslationViewController 中。從它的 constructor 可以看到，我們傳入了 TwoPaneLayout 。\n程式碼 12\n在收到要全文翻譯的需求時，TranslationViewController 會去做一大堆事情，然後利用 TwoPaneLayout 顯示負責翻譯的 WebView (第94行)。\n程式碼 13\n在 TwoPaneLayout 中，這個參數在被賦值時，會同時更新畫面：\n程式碼 14\nDemo 到這裡，關於 TwoPaneLayout 的實作，以及它的應用就都說明完了。下面是它在 EinkBro 中操作的效果。(為了顯示 drag and resize 的功能，我特地編譯了一版是會即時更新的版本)\n相關連結 EinkBro - Apps on Google Play\nEinkBro Source Code Repository plateaukao/browser\nTwoPaneLayout.kt plateaukao/browser\nTranslationViewController.kt plateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/android-%E4%B8%AD%E5%8F%AF%E4%BB%A5%E8%AA%BF%E6%95%B4%E7%95%AB%E9%9D%A2%E5%A4%A7%E5%B0%8F%E7%9A%84%E9%9B%99%E8%A6%96%E7%AA%97%E7%95%AB%E9%9D%A2twopanelayout-einkbro-12/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/5db7795ccf48/1_2DXukqc4AXSXmuOSfVMDNw.png\"\u003e\n\u003cem\u003e可以調整畫面大小的 Custom View\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e在 EinkBro App 完成全文翻譯的功能後，使用上相當愉快，能夠快速地看左右對照翻譯完後的結果。但是用著用著，又覺得有那麼一點點不順手。原因是目前的實作方式是將畫面左右各切一半，左邊是原本網頁內容，右邊是翻譯後的結果。如果我是用海信 A7 手機在看網頁的話，由於手機的形狀是長形的，會造成兩邊的畫面相當窄，只能縮小字型來提高可見的文字量。有些時候會懶得一直對照著看。這時就希望視窗大小是可以調整的：想看翻譯時，可以把翻譯畫面變大；想看原文時，可以把原本網頁的部分變寬。甚至是，如果能改成上下分割的話，就更完美了！\u003c/p\u003e","title":"Android 中可以調整畫面大小的雙視窗畫面(TwoPaneLayout) — EinkBro (12)"},{"content":"Due to the Work From Home policy recently, I need a way to do some exercise at home. I bought a 1/4 size table tennis table, and a practice board (red rectangle in the video) to help re-bouncing the ball back. While it works as a way to sweat a bit, it’s not showing how’s the practice performance, and how much time I spent. I would like to have a way to track all the information for each practice, as a reference for later improvement. Moreover, humans like to read numbers. Numbers are magic to motivate people to do something more.\nAfter some trial and error, I built up a system to show the most important numbers during my practice:\nhow much time passed, how many balls I used, how many hits I made, current continuous hits, and maximum continuous hit. Now, I will explain how the system is built up.\nArchitecture The system is built by three components:\na mobile phone to record the practice scene and serve as an RTSP streaming server; a desktop PC (I use iMac) for processing the streaming content, and serve as the webserver too; and a tablet, or another phone to show the current statistics. A mobile phone is used to record the practice scenes, and stream the video to an iMac machine where python script is run to process image information, and then a web server is set up on iMac to show the current statistics to a remote Android tablet that is close to the table tennis table.\nRTSP streaming The first part is the easiest part; I downloaded Spydroid from the Fdroid website. It’s a mobile app that can record videos and stream them through RTSP protocol with several flexible configurations. Since I planned to leave all the heavy work done on PC, I just need a device to capture the practice images continuously and send them to the PC. Spydroid just fits my needs perfectly.\nSpydroid | F-Droid - Free and Open Source Android App Repository\nfyhertz/spydroid-ipcamera\nImage Processing Image processing is done by python scripts with the help of the OpenCV library. OpenCV library is a very mature and popular solution for dealing with images. A lot of resources can be found on the internet too.\nI use OpenCV to detect whether there’s any image difference (comparing to the first image where the ball is not entering the screen yet) on the left side of the image. Usually, I stand at the right side of the table, and the practice board is set on the left side. That’s why I only detect the left side of the image. if so, count it as one hit after the image difference is gone (which means the ball leaves the screen again).\nThe algorithm is referenced from the following links:\nBasic motion detection and tracking with Python and OpenCV - PyImageSearch\nThe main implementation of the image processing is at detact_area_change.py .\nplateaukao/pingpongpad\nHere’s a video to demonstrate how the image processing goes:\nAnd a simple explanation for that process:\nCode Explanation In the beginning, several frames are skipped due to the video recording is not stable, the contrast will become stable after 1 to 2 seconds, I skipped these frames in lines 79 ~ 82. At lines 96~ 98, each frame is resized to a smaller one, and convert to a gray image (since the color is irrelevant), and applied Gaussian blur for reducing small noises.\nWhen all is done, the first frame will be remembered (at line 101) as the background for comparing the following changes.\nIn lines 106 to 108, images are compared and filter some threshold to make sure the result is what I wanted. And then, callfindContours for following actions.\nEach contour data is measured to filter out too small changes (at line 116). Only the left side of the image (at line 121) is considered. A rectangle is drawn for the area. And then line 125 ~ 148 is calculating whether it’s a new ball, a new hit, or a new record for max consecutive hits.\nIf the time difference between two hits is close enough, say, within 1 or 2 seconds, I assume they are consecutive hits with the same ball (at lines 132 ~ 134). This is how I calculate current consecutive hits and the maximum consecutive hits.\nWhen the image data is processed, the results should be sent somewhere so that I can see it while practicing table tennis (at line 148).\nWeb Server by Python and Flask I decided to write a web server on iMac instead of writing an App for tablet or mobile hone because it’s easier. The web server is written with Python and Flask. And I also used SocketIO library to make it faster.\nSequence diagram for iMac and Tablet\nWhen receiving hit_status events from detect_area_change.py through SocketIO, the Web server will emit a status_response event to Web page to update what user can see now.\nShowing Statistics on Web When getting status_response from the webserver, the web page will update the content accordingly. Here’s the code snippet: on the web page, there are dedicated elements for the information I need. I just change the text when new values come.\nHere’s the sample web content demo:\nStart and Stop the Tracking A Tracking button is added to the bottom of the web page. If it’s clicked, it will trigger the webserver to start reading RTSP streaming data from the mobile phone, and start the whole process.\nWhen clicking on the same button again, it will stop the image processing, and save current statistics to a local file. These data can then be used to present the training trend and effort.\nThe codes are not well written, since it’s only a proof-of-concept solution and used for personal use. However, I think the concept can be easily applied to other possible scenarios too. Hope you find this article helpful for your own sports practice at home.\nSource Codes for the Project plateaukao/pingpongpad\n","permalink":"https://plateaukao.github.io/blog/posts/table-tennis-practice-tracking-dashboard/","summary":"\u003cp\u003eDue to the Work From Home policy recently, I need a way to do some exercise at home. I bought a 1/4 size table tennis table, and a practice board (red rectangle in the video) to help re-bouncing the ball back. While it works as a way to sweat a bit, it’s not showing how’s the practice performance, and how much time I spent. I would like to have a way to track all the information for each practice, as a reference for later improvement. Moreover, humans like to read numbers. Numbers are magic to motivate people to do something more.\u003c/p\u003e","title":"Table Tennis Practice Tracking Dashboard"},{"content":"Text Translation in Browser by Using an extra WebView: EinkBro (11) Why is this necessary Surfing on the internet is not only suitable for getting new knowledge, but also good for learning a new language. By browsing websites in a different language, you can learn how expressions are used, and what are concerned for the people who use that language. However, before you’ve got to a certain familiarity of the language, you may be intimidated by so many unknown words to be looked up for.\nWhat Chrome offered \u0026amp; why it does not fit my needs Google Chrome provided a solution to somewhat solve this problem: it translates the whole page into a different language on the fly. With the original web content layout, every part of the text content are translated.\nThe way Chrome did is suitable for quickly understanding the web content, but not a perfect way for language learners, because the original content is gone. Not being able to compare the content side by side, it decreases the benefit of surfing web in another language.\nGoogle Chrome’s way\nAlthough Chrome also provides another way that user can select texts to be translated, and show a popup translation dialog, it’s too tedious to do so if the web article is very long; and user has to do it manually multiple times.\nWhat I want to achieve is like the following diagram: displaying another UI component that contains the translation result, aside to the original web content.\nEinkBro’s way\nBy doing so, you can easily look up for the sentences you don’t understand by reading right side of the screen.\nHow to achieve it To achieve this, several things need to be done.\nShow a separated WebView beside the original browser. Grab text content from original web. Translate the text content and display the results in the newly created WebView. Let’s go through these steps one by one.\nShow a Separated WebView This is the easy part. I created a LinearLayout to hold both the original browser WebView container, and the translation WebView container. Both have android:layout_weight=1. In most cases, translation WebView container is empty, and visibility is set to GONE.\nOnly when user triggers full text translation feature, the container is set to VISIBLE, and a WebView is created inside, so that it can take up half of the screen space.\nExtract Content Text from Original Web Thanks to previous implemented feature of EinkBro Browser, it’s possible to turn most web pages into Reader Mode: keeping only the most essential content, purging out all other minor components. In this mode, it’s a line of code the get the plain text of the content part, since in JavaScript implementation (originated from Firefox mobile browser), the article object already has a variable to keep the text content for estimating reading time.\nAfter turning on Reader Mode, by using evaluateJavascript function, the text can be extracted from WebView.\nText Translation and Display in Separated WebView This is the tricky part. Everyone knows that Google Translate is good for translating texts. So, for normal users, it’s very common to open up a Google Translate Web page, and copy/paste content into it to get the result; or, select a part of the text, and show the popup Translation Dialog.\nHowever, for 3rd party apps, if anyone (other than Google itself) wants to integrate Google Translate service, he has to use Google’s APIs, and pay by the used quota. This is not what I am going to do. Since I don’t charge App users anything for using it, I don’t want to pay for their usage too.\nBecause Google Translate Web page is still free for users, I go another way: showing the Google Translate Web page directly in the new WebView.\nWell, If I stop here, then users still have to copy/paste content by themselves to see the translation results. Luckily, Google Translate Web url provides a convenient way of passing the to-be-translated text into url directly! While opening the Google Translate page, I just need to append the text to the end of the url! The pattern is as below:\nhttps://translate.google.com/?text=\u0026#34; this is a book\u0026#34; Content Pagination The url querystring solves the translation problem, but comes with a price: URL length is limited. If the web page contains too many texts, Google Translate cannot handle the url correctly.\nTo by pass this side effect, pagination is applied. If the text is longer than a certain threshold, it’s divided into segments, and paginated buttons are also displayed on the right bottom side of the screen. Users can know there are multiple pages to be translated; once he finished one page, he can click on next page for more translations.\nDEMO Screenshot Video Reference This feature is included in release v8.10.0\nSome issues are found and will be fixed in next minor release.\nplateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/use-an-extra-webview-for-text-translation-in-browser-einkbro-11/","summary":"\u003ch3 id=\"text-translation-in-browser-by-using-an-extra-webview-einkbro11\"\u003eText Translation in Browser by Using an extra WebView: EinkBro (11)\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/3813fb663df/1_DxG9oTXv_P3bBpO1YzvxXA.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"why-is-this-necessary\"\u003eWhy is this necessary\u003c/h3\u003e\n\u003cp\u003eSurfing on the internet is not only suitable for getting new knowledge, but also good for learning a new language. By browsing websites in a different language, you can learn how expressions are used, and what are concerned for the people who use that language. However, before you’ve got to a certain familiarity of the language, you may be intimidated by so many unknown words to be looked up for.\u003c/p\u003e","title":"Use an extra WebView for Text Translation in Browser: EinkBro (11)"},{"content":"\n從開始用手機以來，中文輸入法就一直是使用 LimeHD 加上嘸蝦米的字根檔。由於用習慣了，對於它一直沒有什麼新功能也不是很在意。不過，在這兩年，使用電子紙設備的時間愈來愈多，所以開始有了想要改造它的念頭。\nLimeHD 雖然在某些畫面塞了廣告，不過它有開放原始碼在 Github 上，所以只要稍具開發能力的話，就可以抓下來自己調整成自己喜歡的樣子。以下是這陣子以來，我針對 LimeHD 有做的修改。另外，我把自己在修改的版本改稱為 sweetlime，希望它是個有甜味的檸檬。\n整理原始碼 把原始碼版本控制輕量化 拿掉其他不必要的輸入法設定 拿掉跟 Google Drive 和 Dropbox 的整合 拿掉用不到的函式庫 拿掉廣告 新增的功能 長按鍵盤按鈕，叫起切換鍵盤的系統介面 支援 scope storage 長按空白鍵，叫起對話框，增加新增詞彙的功能 將介面改成適合電子紙螢幕的設備 減少候選字，改善執行的效能 整理原始碼 lime-ime/limeime\n輕量化 一開始下載原始碼時發現，它的 Github repository 的大小竟然有 900 多MB。對於一個只有不到 10 MB的輸入法來說，似乎不成比例。仔細看了一下，很多是其他輸入法的資料，然後每次更新都會再疊加上去，造成 git repository size 太大。所以，我的第一步就是先把這些歷史痕跡刪掉，反正最終我只想留的就只有那個 customization 的自定義輸入法和偶爾不知道怎麼拼字時，得應急的注音。\n重新建立一個 git 後，再來是把設定畫面中的其他輸入法選項都移除。既然是用不到的功能，留在畫面上只是增加畫面的複雜度 。\n移除雲端備份還原 再來，是拿掉雲端備份的功能。這功能說實在話，還蠻有用的。在用正式版 LimeHD 的時候，我也經常使用 Google Drive 的備份幫我的新設備輸入法還原無蝦米的字碼庫。不過，既然要整理，就順手把它移除了。原因是還原和備份(對我來說)並不是常常會做的事。如果真的有需要的話，可以先備份到手機端資料夾，再傳到新的設備上進行還原就好。或是，可以把手機端備份好的 ziop 檔，自行上傳到常用的雲端儲存去。等有需要時再手動下載 zip 檔，然後讓 LimeHD 從手機端進行還原。\n這麼做確實會有點麻煩，但移掉這功能後，LimeHD 就可以完全不需要 internet 的權限，還可以拿掉 android.permission.GET_ACCOUNTS 的權限。這在現今的輸入法 app 中，應該是碩果僅存完全不需要網路功能的輸入法吧。\n移除無用的函式庫 原本的 dependency 有下面這麼多：\n移除後只剩下：\n移除廣告 再來是拿掉廣告模組，這樣子就完全不需要網路功能了。\n經過這一番整理後，binary size 已經從 10MB 減少到小於 4MB。再來要再瘦身就會有點困難了，因為很大一部分的 size 是來自於裡頭的資料庫，包含表情字符對照，簡繁轉換對照，還有英文字典檔。\n新增功能 快速切換鍵盤 幫 sweetlime瘦身只是順手做，但主要目的是為了要加入一些我自己想要的功能。由於有學習其他的語言，常常會需要利用不同的輸入法 App 輸入文字：英文法文的話，習慣用 Gboard；日文的話，會用 simeji 或是 Google 日文輸入法；韓文的話則是 Naver SmartBoard。\n要在不同的輸入法間切換，通常要先 focus 到文字輸入框，下拉系統通知欄，點擊通知欄中輸入法通知，再做切換。一直這麼操作很花時間。Gboard 有個功能是長按地球圖案就可以列出系統中所有安裝的輸入法。但這功能似乎沒有下放給非 Google 自家的其他輸入法。\n唯一能做的就是從自己的輸入法介面中呼叫系統輸入法選單，讓使用者再自己切換過去。\nval imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.showInputMethodPicker() 這點 LimeHD 也有做，但步驟有點多。要長按畫面左下方的鍵盤圖案，等到 popup 畫面出來後，再按系統輸入法切換。這時才會跳出系統的輸入法選項。\n我的修改只是把長按鍵盤按鈕，直接變成是跳系統的輸入法選單。然後原本該要出現的 popup 畫面，則改成是長按空白鍵時出現。\n支援 scope storage 新版的 Android，對於系統的權限愈來愈嚴。之前只要取得讀寫 external storage 權限，就可以隨意讀取系統的外部儲存裝置的任何目錄，但現在已經不行這麼做了。\nhttps://developer.android.com/about/versions/11/privacy/storage\n因為我把 compileSdkVersion 和 targetSdkVersion 都改成了 30，所以舊的備份還原功能會有問題，我把它改成了使用 system document picker 的方式。\n快速新增詞彙 在使用手機或平板時，常常需要輸入自己常用的電子郵件地址，這在比較新的輸入法中，都可以自動幫使用者記下這些資料，讓使用者可以快速再次輸入。但是 LimeHD 並沒有這功能，所以我幫它加了一個快速的選項，能夠自行加入常用的字串。\n(在中文輸入法中)長按空白鍵後，跳出的 popup 最下方的 新增常用字，可以讓使用者加入字串。\n修改介面 除了一般的手機和平板外，我還有許多 Android 的電子書閱讀器，通常只有黑白畫面，而且對比度不好，更新頻率不高。對於這行設備來說，動畫效果和大幅度的更新畫面內容都是不好的。為了讓 sweetlime 也可以適用於電子書閱讀器，我把按鍵的 layout 做了修改。只剩下外框；點擊後的效果也改為只加粗外框。\n改善執行效率 在電子書閱讀器上 CPU比較弱，而且檔案系統的讀取速度(可能)也比較慢，所以如果能減少 IO 的話，會讓效能有所提升。\n無蝦米以重覆字不高著名，所以其實在輸入時，並不需要從資料庫中取出太多候選字。因此，我稍微改了一下 sweetlime 的參數，讓它不要做太多白工。\n後話 雖然 LimeHD 很久沒有新功能了，但該有的功能都有了，經過一番調整後，還是可以再戰十年的。關於上述的所有修改，已經有編譯成 apk 放在下面的 github 連結中，有興趣的人也可以裝來試試看。\n相關連結 下載連結\nReleases · plateaukao/sweetlime\n原始碼連結\nplateaukao/sweetlime\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%B9%E9%80%A0-android-%E4%B8%8A%E7%9A%84%E7%84%A1%E8%9D%A6%E7%B1%B3%E8%BC%B8%E5%85%A5%E6%B3%95-limehd/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/6e903668c21e/1_CMWmaQRJWVJoFFqZlaSDOg.png\"\u003e\u003c/p\u003e\n\u003cp\u003e從開始用手機以來，中文輸入法就一直是使用 LimeHD 加上嘸蝦米的字根檔。由於用習慣了，對於它一直沒有什麼新功能也不是很在意。不過，在這兩年，使用電子紙設備的時間愈來愈多，所以開始有了想要改造它的念頭。\u003c/p\u003e","title":"改造 Android 上的無蝦米輸入法 — LimeHD"},{"content":"寫著寫著，竟然寫到第十篇了，真是一段漫長的歲月(嚴格來說，其實也只有幾個月而已)。這次開發的功能，對我個人來說，是個 killer feature 啊！雖然還有其他的幾十個功能也是。\n單純瀏覽網頁的話，其實不大需要全文翻譯的功能。但總有些時候，會需要看外文的網頁，而外文裡可能有許多單字，或是看外文的速度沒有那麼快，閱讀起來會很辛苦。這時，如果能夠跟 Chrome 一樣，提供全文翻譯的話，對於使用體驗上會好很多。如果是有學習語言需求的話，能讓翻譯的結果跟原文對照著看，會是更好的呈現方式。\n在文石(Onyx)的電子書閱讀器上透過內建 Reader App 閱讀書籍時，可以開啟雙畫面，一邊是電子書的內容，另一邊是原文翻譯出來的結果。原文翻譯的功能，其實是利用了系統的 Multi-Window 功能，把畫面切成兩半，一半是 Reader App；另一半則是另一個內建的字典 App。內建的字典 App 可以用來查詢單字，句子，以及文章。Reader App 很巧妙地把這兩件事串起來，讓使用者只要點選雙開翻譯畫面，就能馬上看到翻譯的結果。\n雙開翻譯畫面對我來說超有用的，而且我發現它的翻譯品質比想像中來得好，大概每一頁有八成以上九成是準確的翻譯，少部分內容才需要自己看著原文確定它真正的意思是什麼。下面附上一頁 Kilian Jornet (越野跑步大神) 的介紹書籍 Courir ou mourir 法文版內容，右邊是翻譯出來的中文效果。要不是有些句子一看就不合理，大部分的句子通順到連我自己都寫不出來。\n既然 Onyx 自己內建的 Reader App 可以這麼做，不曉得第三方的 App 是不是也有可能在文石的設備上達成類似的功能呢？很早之前就有這個想法，但一直不知道怎麼實作，就這麼擱著。最近幾天終於研究出來作法了，也很順利地把它套用在 EinkBro App中。下面就要來解釋怎麼做到的。\n實作細節 開關 Multi-Window 模式 Multi-Window 模式是在 Android 7.0 加入的功能。隨著手機愈來愈大，和平板的普及化，Android 開始支援一次可以開啟兩個 App。但是提供的支援很陽春，只能把畫面切成兩半，一邊塞一個 App。而且啟動方式很很很難用，必須先進到最近開啟的程式列表，長按其中一個程式的 icon，才會跳出一個選單讓使用者啟動分割畫面。在大部分的 Android 設備中，Multi-Window 模式都是要使用者手動啟動的。\nhttps://developer.android.com/guide/topics/ui/multi-window\n但是，既然在 Onyx 上的 Reader App 能夠一鍵就進入分割畫面模式，說不定我也可以做到。後來，真的讓我研究出來了！其實方式很簡單，只要透過 intent 發個 action 就行了！\n在 756 行可以分別看到啟動和關閉分割畫面的 action 字串。把它塞到 intent 中再餵給 sendBroadcast() 就可以了。\n呼叫字典程式的全文翻譯功能 在系統中，Reader App 和字典 App 分屬兩個不同的程式。Reader App 能把字典 App 叫起來，最常見的方式就是透過 intent 的傳遞，把相關的參數傳入。系統會幫忙找到能處理該 intent 的 App，並把參數餵給它。\n觀察了一下當 Reader App 進入雙開翻譯模式時，字典 App 的 CloudTranslationActivity 被啟動了。所以應該是該 Activity 收到了需要翻譯的需求，開始執行。\n在經過一番研究後，找到了它需要的 intent action 和參數資料格式，就可以正確地啟動字典翻譯功能。\n取得需要翻譯的網頁全文 萬事俱備，只欠東風。東風就是需要被翻譯的網頁內容。\n網頁內容千奇百怪，如果直接抓取整個網頁的 text，其中會有很多不必要的資訊，像是標題，側邊欄，其他相關文章連結說明，留言，等等等。\n這時，之前開發好的閱讀模式就可以派上用場了。閱讀模式的主要功能正是把不相干的元件都去除，只留下真正重要的內容。如果先在網頁上套用閱讀模式，再抓取內容，就可以得到比較純正的內容。把這些內容再拿去翻譯，才不會顯示雜亂無章。\n閱讀模式功能採用的 Readability.js 很好心的提供了一個 textContent 的變數，讓我可以直接拿到裡頭的純文字部分。(第 563 行)\n經過上述三個環節，就大功告成啦。由於這功能只支援 Onyx 的設備，所以我在工具列中加了一個全文翻譯的按鈕，但只有在 Onyx 的設備中才會顯示。\n以後，希望能夠另外開發利用 Google Translate Web 畫面的全文翻譯功能，這麼一來，就可以不用只受限於 Onyx 的設備。\n示範畫面 示範影片 ","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-x-%E6%94%AF%E6%8F%B4%E5%85%A8%E6%96%87%E7%BF%BB%E8%AD%AF%E5%B0%8D%E7%85%A7/","summary":"\u003cp\u003e寫著寫著，竟然寫到第十篇了，真是一段漫長的歲月(嚴格來說，其實也只有幾個月而已)。這次開發的功能，對我個人來說，是個 killer feature 啊！雖然還有其他的幾十個功能也是。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器 (X) — 支援全文翻譯對照"},{"content":"程式碼寫多了，總是會有技術債要還。剛開始改造 FOSS Browser 時，因為懶，而且為了求快，在把既有的 icon 改成純黑色時，都是直接用 @android:color/black 寫死在 xml 中。將各種對話框改成純黑白型式，或是加外框時，也都是直接用上面的黑色色碼。\n現在，因為我也常常在一般手機中使用 EinkBro App，就很希望它能也提供黑色的介面，讓我在使用手機時不要那麼刺眼。但一想到要改一大堆 icon 的顏色和對話框的顏色，就覺得很麻煩，所以遲遲未動手。\n昨天心血來潮，開始了這個大工程。下面列出我實作的步驟，以後如果有類似的需求，就可以拿這次的經驗當作參考。\n設定符合 App 需求的 Theme 使用 DayNight 的主題 原本 EinkBro 使用的主題是從 Theme.AppCompat.Light.NoActionBar 延伸而來的。如果想讓 App 可以跟著系統當下的設定採用一般或夜間的主題，要將主題改成像是 Theme.AppCompat.DayNight.NoActionBar 才可以。\n在主題中指定自己想要的主要顏色 以下是針對電子紙，也就是在一般主題下的設定值\n\u0026lt;style name=\u0026#34;AppTheme\u0026#34; parent=\u0026#34;Theme.AppCompat.DayNight.NoActionBar\u0026#34;\u0026gt; \u0026lt;item name=\u0026#34;android:textColor\u0026#34;\u0026gt;@android:color/black\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;colorControlNormal\u0026#34;\u0026gt;@android:color/black\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;android:colorAccent\u0026#34;\u0026gt;@android:color/black\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;backgroundColor\u0026#34;\u0026gt;@android:color/white\u0026lt;/item\u0026gt; \u0026lt;/style\u0026gt; colorControlNormal 是用來指定 Vector Asset 中的線條顏色。以正常主題而言，我希望它們全是純黑色的，可以在電子紙上達到最高的對比度。\n設定夜間主題的顏色 雖然 DayNight 的主題已經有幫忙指定夜間模式下的相關顏色變化，但因為我還是想要微調，所以，必須先建立 values-night 目錄，在底下也放入 styles.xml，然後在相同的主題名稱下，指定夜間模式的顏色選擇。\n\u0026lt;style name=\u0026#34;AppTheme\u0026#34; parent=\u0026#34;Theme.AppCompat.DayNight.NoActionBar\u0026#34;\u0026gt; \u0026lt;item name=\u0026#34;android:textColor\u0026#34;\u0026gt;@color/lightGray\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;android:colorAccent\u0026#34;\u0026gt;@color/lightGray\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;colorControlNormal\u0026#34;\u0026gt;@color/lightGray\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;backgroundColor\u0026#34;\u0026gt;@android:color/black\u0026lt;/item\u0026gt; \u0026lt;item name=\u0026#34;background\u0026#34;\u0026gt;@android:color/black\u0026lt;/item\u0026gt; \u0026lt;/style\u0026gt; 針對夜間模式的字型顏色，我希望它不是完全純白的，稍微有點灰灰的感覺看起來比較舒服，所以我自己定義了一個 lightGray 的顏色。\n原先寫死的顏色設定，改成參考 Theme 中的值 先從 Vector Asset 改起 很多 icon 是我利用 Android Studio 的 Asset Studio 產生的，附檔名全是 xml，所以比較容易利用字串找到所有 @android:color/black 的 xml 檔，然後再利用取代的功能把它們全部換成 ?attr/colorControlNormal 。在正常模式下，會是全黑的；在夜間模式下則是 lightGray。\n處理各種對話框和 layout xml 因為之前寫介面時的壞習慣，色碼散在到處的 layout 中；有的 TextView 也莫名奇妙地寫死了顏色。所以當一切到夜間模式時，很多對話框都是一片黑，都看不到字。\n關於這一個步驟，只能一個一個修。有些 icon 並不是 xml 型式，而是png。這時，只能利用 app:tint=”?attr/colorControlNormal” 將它的顏色換掉。\n修改程式碼 開啟 App 對夜間模式的支援 新建一個 Application class，並在裡頭加入 setDefaultNightMode\n動態改變主題 雖然使用到的機會不多，但總是希望當使用者從通知欄開啟夜間模式時，當下的 App 介面能馬上切換到黑色的主題。\n為了達到這功能，Activity 必須要去聽 configuration change，當改變的是 uiMode時，再做相對應的處置。簡單起見，我直接將 App重啟。\nWebView 的夜間模式 因為 EinkBro 是個瀏覽器，所以除了改改介面變黑色模式外，如果 Web 部分也可以變成夜間模式的話，會是更好的體驗。Android 官方有專門的文章在解釋怎麼做。以下我只講我的實作方式。\nhttps://developer.android.com/guide/webapps/dark-theme\n首先要在 build.gradle 加入 webkit 的函式庫：\n然後在 WebView 初始化的時候，根據系統的狀態和對夜間模式的支援程度來開啟夜間模式。\n修正：上面畫面中設定的 ForceDarkStragegy 用的是 DARK_STRATEGY_WEB_THEME_DARKENING_ONLY。這麼設定的話，其實只有少部分的網站內容會跟著變成黑色底的顯示方式。 如果把它改為 DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING 則大部分的網站都可以以黑底的方式呈現 修正後如下：\n看來畫面啦\n後話 如此一下，就大功告成啦。美中不足的是，很多網站目前還沒有支援 Dark mode，所以常常還是只能看到工具列是黑的，但內容是白的。以後有時間的話，應該要再研究一下 Firefox 的 plugin — Dark Reader 是怎麼動態把大部分的網頁都變成黑色底的。\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-ix-%E6%94%AF%E6%8F%B4%E5%A4%9C%E9%96%93%E6%A8%A1%E5%BC%8F/","summary":"\u003cp\u003e程式碼寫多了，總是會有技術債要還。剛開始改造 FOSS Browser 時，因為懶，而且為了求快，在把既有的 icon 改成純黑色時，都是直接用 \u003ccode\u003e@android:color/black\u003c/code\u003e 寫死在 xml 中。將各種對話框改成純黑白型式，或是加外框時，也都是直接用上面的黑色色碼。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器 (IX) — 支援夜間模式"},{"content":"\n儘管網路上已經充滿各種免費的韓國語學習資料，如果肯付些許費用的話，能夠接觸到更多系統化的學習內容，和一般免費教材所沒有的進度追蹤或成效記錄。這篇文章將會介紹幾個網路上的付費學習資料供大家參考。\nmasterTOPIK masterTOPIK\nmasterTOPIK\nmasterTOPIK 在 Youtube 上有開頻道，提供每個課程的部分章節，可以先去試聽看看，看教材是不是適合自己，和授課老師是否能夠接受。\n針對韓文初級中級，有利用不同語言的授課教師。不過，如果已經具備一點聽力的話，所有程度都有直接用韓文上課的系列，讓你可以一邊學韓文，一邊練聽力。有個小缺點是，用韓文授課的系列，並沒有字幕可以看。所以建議先確定自己聽得懂再決定要不要付費。\n付費的方式是訂閱式的，主要有每個月 13.99 美金的標準會員和月付 19.99 美金的高級會員。高級會員比標準會員多了可以每個月做一次模擬考試。如果不是為了考試而唸書的話，應該標準會員就很夠用了。\n高級會員特有的功能\n成為付費會員後，可以稍微在線上做一下 masterTOPIK 的測驗，大概知道自己的程度，然後選擇適合自己的課程。當然也可以跳過考試，自己憑感覺選擇想要的課程。\n課程內容除了 TOPIK 考試的教材學習外，還有常用的(初中高級)韓文聽說讀寫。在首頁會很清楚地列出你參與的課程以及目前你的進度。一方面方便自己查看目前完成了多少，另一方面隨時可以督促自己，最好右邊的月曆每天都有點進展。\n這是目前對我個人而言，接受度最高的學習內容。它每一課的長度大約是15分到40分之間。如果是15分左右的內容，我可以連續聽三四個課；如果是40分左右的課程，我也可以一口氣聽完一集。比較不會有精神不集中的問題。\n影片中的教材，初級中級都可以從網站下載 pdf 學習；但從高級開始，就只能再額外購買他們網站上販售的書，一本大約23 美金。雖然不買也是可以光看影片。\n在使用時，有發現一個困擾是，晚上我家裡用中華電信連上網站時，播影片都很不順暢，斷斷續續的。但是用遠傳門號的手機網路來學習時，又通常播放得很順。這點大家可以在晚上時多試試看，確認一下自己要使用的網路環境是好的。\nJella Jella! 為語言而生 | 最專門的語言學習平台\n在台灣應該還蠻多人聽過的線上教學平台，除了韓國語之外，也有提供日文和法文的教學。付費方式是單買各個課程。有時會有促銷，一次買進階上下兩個課程或高階上下兩個課程會有折扣。主要教學的老師是一位叫胃酸人的韓國人，在台灣待了很久，所以中文超級好。解釋文法時，都會以台灣人比較能夠理解或想像的方式說明，讓學生更容易記下各種用法。\n入門和進階都還是用中文在授課。從高階上開始，就全是韓文上課，但影片很貼心地附上了中文和韓文字幕，可以依需求開啟，也可以兩個語言的字幕同時開啟。\nJella 除了網站外，也有開啟 App。建議學習時可以多利用 App，因為裡頭除了單純看影片說明外，還多了很多互動，或是要你口說練習，更能加深學習的效果。\n價錢請看官網，可能會有所變化\nTalk to Me in Korean Learn Korean with Talk To Me In Korean - Books \u0026amp; Online Courses\n關於 Talk to Me in Korean，我在另一篇免費的韓國語學習資料裡已經講過，有興趣的人可以直接去看下面這篇文章：\n韓國語學習資料匯整\nEZ Daily 電子報 EZ Daily 槓桿每日報\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9F%93%E5%9C%8B%E8%AA%9E%E7%B7%9A%E4%B8%8A%E4%BB%98%E8%B2%BB%E5%AD%B8%E7%BF%92%E8%B3%87%E6%96%99%E5%8C%AF%E6%95%B4/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/6fddb8bde697/1_kOQHb-QHgAQoHH1-Cq1xOA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e儘管網路上已經充滿各種免費的韓國語學習資料，如果肯付些許費用的話，能夠接觸到更多系統化的學習內容，和一般免費教材所沒有的進度追蹤或成效記錄。這篇文章將會介紹幾個網路上的付費學習資料供大家參考。\u003c/p\u003e","title":"韓國語線上付費學習資料匯整"},{"content":"在新的 M1 系列出來後，這一兩年有買 Mac 電腦的人不禁覺得苦惱。想要一嚐 M1 的強大性能，卻又苦於已經擁有了一台不算慢的 Mac 電腦，現在想轉賣又不見得能賣到什麼好價錢。一個折衷的方案是：買台最便宜的 M1 設備 Mac min，然後讓原本的 Mac 電腦能呼叫這台 Mac mini 幫忙執行很花 CPU 的工作，像是編譯程式！\nmainframer 為了要實現這想法，首先要來介紹一下一個好用的工具：mainframer。\nbuildfoundation/mainframer\nmainframer Github 上寫得很清楚，這個工具可以協助你在遠端上執行指令，同時把執行後的結果再同步回電腦。\nA tool that executes a command on a remote machine while syncing files back and forth. The process is known as remote execution (in general) and remote build (in particular cases). 在 Github 上，預設的分支是 3.x，但 3.x 的版本是有問題的（至少我試不出來，有人試成功的話，請留言告知，感謝），要先切到 2.x 的分支。\nmainframer 的概念 在本地端執行指令 ➊ 後，mainframer 會私底下幫忙執行 ❷ ➌ ➍ ➎。\nmainframer 設定 在 README 中有說明怎麼設定，我這邊快速用中文講一下。\n舊的 Intel Mac 設定 有三個變數先定義如下：\nREMOTE_MACHINE_ALIAS — 遠端 M1 Mac mini 的 SSH alias (我就取名為 m1) REMOTE_MACHINE_IP_OR_HOSTNAME — 遠端 M1 Mac mini 的　IP REMOTE_MACHINE_USERNAME — M1 Mac mini 上的使用者名稱 確認已經安裝了 ssh 和 rsync。 產生使用者的 SSH key ssh-keygen -t rsa -b 4096 -C \u0026#34;{REMOTE_MACHINE_USERNAME}\u0026#34; 把下面設定加到 ~/.ssh/config 中 Host {REMOTE_MACHINE_ALIAS} User {REMOTE_MACHINE_USERNAME} HostName {REMOTE_MACHINE_IP_OR_HOSTNAME} Port 22 IdentityFile ~/.ssh/id_rsa PreferredAuthentications publickey ControlMaster auto ControlPath /tmp/%r@%h:%p ControlPersist 1h 以我自己的例子來說，會是長成下面這樣：\nHost m1 User danielkao HostName 192.168.1.145 Port 22 IdentityFile ~/.ssh/id_rsa PreferredAuthentications publickey ControlMaster auto ControlPath /tmp/%r@%h:%p ControlPersist 1h 利用下面指令複製 SSH Key，下面遠端的部分會用到**。** $ pbcopy \u0026lt; ~/.ssh/id_rsa.pub .mainframer/ 目錄設定 將最新版的 mainframer script 複製到 Android 專案的根目錄中。\n在專案中建立.mainframer 目錄，並建立 config 檔案，填入以下資訊： remote_machine={REMOTE_MACHINE_ALIAS} 以我的例子來說，會是：\nremote_machine=m1 設定 .mainframer/ignore 一堆跟編譯無關的檔案，都不需要同步到 m1 去。\n.gradle .git/* .gitmodules /.idea /local.properties /.mainframer 其他設定 官方文件還有許多細步的設定，可以在整個流程都通了之後，再斟酙修改。\nbuildfoundation/mainframer\n遠端 (M1 Mac mini) 先確認已經安裝了 SSH Server 和 rsync。 設定 user 的 SSH key (從剛剛 intel Mac 上 copy 來的值) $ mkdir -p ~/.ssh $ chmod u+rwx,go= ~/.ssh $ echo {SSH_KEY} \u0026gt;\u0026gt; ~/.ssh/authorized_keys $ chmod u+rw,go= ~/.ssh 安裝編譯程式所需的其他軟體。以開發 Android 來說，我有先在 M1 Mac mini 中安裝 Intellij Community Edition，並且透過它安裝了 Android 相關的 SDK。另外，還裝了 Zulu 的 ARM 64 java，確定 Android projects 可以正常在 M1 Mac mini 中編譯。詳情可以看下面這篇文章： 在 M1 Mac Mini 上開發 Android App\n執行 一切設定好之後，進到 Mac 的 Android 專案下，可以來執行 mainframer 了！\nsh mainframer.sh ./gradlew clean assembleRelease 如果你的 Android 專案原本在 Mac 中的編譯時間需要到一兩分鐘或更久的話，透過遠端的 M1 機器執行，再同步 binary 回本機，將會感受到差異。\n後話 如果嫌打指令太麻煩的話，可以在 Android Studio 中建立 External Tools，並且加到工具列上頭，這樣子就可以點一下讓遠端的 M1 為你編譯程式了。\n另外，遠端讓 M1 編譯還有一個好處時，當 binary傳回本機後，依然可以使用 emulator debug ，不用受限於現在還不是很完整的 M1 Android Emulator。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8intel-mac-%E4%B8%8A%E5%88%A9%E7%94%A8-m1-mac-mini-build-codes-mainframer/","summary":"\u003cp\u003e在新的 M1 系列出來後，這一兩年有買 Mac 電腦的人不禁覺得苦惱。想要一嚐 M1 的強大性能，卻又苦於已經擁有了一台不算慢的 Mac 電腦，現在想轉賣又不見得能賣到什麼好價錢。一個折衷的方案是：買台最便宜的 M1 設備 Mac min，然後讓原本的 Mac 電腦能呼叫這台 Mac mini 幫忙執行很花 CPU 的工作，像是編譯程式！\u003c/p\u003e","title":"在Intel Mac 上利用 M1 Mac mini build codes — mainframer"},{"content":"書籤功能在 PC 的瀏覽器是個很重要的功能，因為操作方便，所以通常會記錄一大堆連結，並且分門別類放在不同的目錄中。但是手機上的瀏覽器，情況略有不同。大部分人比較常用的功能，應該都已經下載對應的 App 了；剩下一些不常用的服務，才會開啟網頁偶爾使用；或是需要搜尋網頁內容時，會開啟瀏覽器；再來是很大一部分是從其他 App 中轉導到瀏覽器的。這些應用場景，都不見得需要像 PC 上一樣，建立書籤、管理書籤。\n但是 EinkBro 的性質又跟一般手機上的瀏覽器不大一樣，因為它是設計給電子紙設備使用的。在電子紙設備上，大部分的 App 使用體驗並不好，所以我預期(至少我自己)使用者在使用電子書閱讀器時，除了看電子書外，很多事情是在 EinkBro 上完成的。也因此，有足夠強大的書籤功能才能符合(我的)需求。\n書籤目錄 原本的書籤功能還算堪用，能新增/刪除，或是長按書籤做修改或其他小功能(加到桌面、存為首頁、前景/背景開啟新分頁)。這次花了點時間，幫它加上了新增目錄的功能。\n一般電子書閱讀器的尺寸至少6吋起跳，所以一個畫面可以顯示十幾二十個書籤；但是如果在電子紙手機上的話(像是海信 A5)，15 個以上就要開始滑動畫面以看到更多的書籤。如果能把相同性質的書籤放在同個目錄，那多按個兩下就可以開啟網頁，會比一直滑動畫面來得有效率。\nBookmarks on Hisense A5\nBookmarks on Hisense A7 with folders\n既然有了書籤目錄，自然要有方式來把書籤放到想要的地方去。所以，在編輯書籤的畫面多了一個目錄選項。Top 表示第一層，其他的選項，就是使用者已經建立的目錄名稱。建立目錄可以在書籤頁開啟後，點三個點按鈕，選擇 新增書籤目錄 。雖然這方式不是很直覺，而且要一個一個書籤去調整，不過，至少是個在有限時間內可以開發出來的功能。\n以後有時間的話，再考慮做成可以直接在書籤列表用拖拉的方式來調整。\nBookmark edit dialog and folder selection\n技術細節 舊版的書籤實作，是直接操作 SQLiteOpenHelper，寫了一堆 CRUD 的 SQL 語法，並且要自行處理 cursor 的開開關關。如果我要在這基礎上加上目錄的功能，感覺會很麻煩，而且很原始。所以我決定把它打掉重練。畢竟，Room 都出來那麼久了，沒道理不用比較新的，單純的作法。\n引入 Room 後，先是替書籤 data model 加入是否為目錄的判斷，並為每個書籤加上 parent 的概念。\n接下來是建立 Dao interface 來存取資料庫。從下面可以看到，這些函式都宣告成 suspend function。這樣子之後在使用時，就可以用 coroutine 輕鬆確保這些操作都不會跑在 main thread 中。\n最後，寫個 Manager 把建立資料庫實體的實作包裝起來：\n上面 47 行有個函式叫 migrateOldData()，這是用來將使用者舊版的書籤資料搬到新的資料庫；也是這次的實作中，唯一接觸到 cursor 的部分。搬運完成後，順手寫到 preference 中，以後開啟 App 時就不用再搬一次啦。\n這些都完成後，在 Activity 中需要顯示書籤時，呼叫上面的 Manager，拿到資料，再餵進 Adapter 就行了。這邊針對目錄和書籤有不同的處理方式，分別在 1212 行和 1214 行：前者是點擊後要把畫面的書籤列表更新為目錄中的項目；後者是把畫面的網頁載入書籤連結。\n這樣就大致完成啦！\n備分/還原書籤 這功能主要是因為我有太多台電子書閱讀器，平常常用的只有一兩台，偶爾要換到別台使用時，苦於上頭的 EinkBro 沒有完整的書籤列表，都得再透過搜尋的方式找到網頁。\n通常要同步資料的話， App 起手式作法是，跟雲端結合，把資料都往雲端送。但是我完全不想要讓 EinkBro 有把任何資料送到哪個雲端去的功能，所以，做個可以把資料備份出來，傳送到別台機器上，再用匯入的方式，是目前比較可行的作法。雖然步驟了點，但可以讓 EinkBro 不會因為扯上雲端而增加複雜度。那類功能就留給 Firefox, Chrome 就好了。\n目前的作法很簡單，把上面透過 Room 建立好的書籤資料庫全讀出來，轉成 json 格式，再寫到檔案系統中。預設的檔名是 bookmarks.json。拿到這檔案後，傳到另一台設備上，再匯入 json 檔就可以了。\n這功能是做在設定 → 備份畫面中。\n因為不想再引用其他的 json 函式庫，所以很簡單地包了 JSONObject 的實作：\n以上！\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-viii-%E5%8A%A0%E5%BC%B7%E6%9B%B8%E7%B1%A4%E5%8A%9F%E8%83%BD/","summary":"\u003cp\u003e書籤功能在 PC 的瀏覽器是個很重要的功能，因為操作方便，所以通常會記錄一大堆連結，並且分門別類放在不同的目錄中。但是手機上的瀏覽器，情況略有不同。大部分人比較常用的功能，應該都已經下載對應的 App 了；剩下一些不常用的服務，才會開啟網頁偶爾使用；或是需要搜尋網頁內容時，會開啟瀏覽器；再來是很大一部分是從其他 App 中轉導到瀏覽器的。這些應用場景，都不見得需要像 PC 上一樣，建立書籤、管理書籤。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器 (VIII) — 加強書籤功能"},{"content":"最近剛拿到 M1 Mac Mini 最基本規格(8GB RAM/256GB SSD)的機器，開始嘗試要用它來開發，所以會這兒記錄有哪些需要注意的地方。(持續更新中)\nAndroid Studio 下載號稱有支援 M1 CPU 的 Arctic Fox Canary 15，但是速度還是很卡。所以後來改成使用 IntelliJ Community Edition 2021.1.1。原以為在 IntelliJ 上得要自己安裝 Android plugin，但似乎原本就有內建了，所以裝好後就可以直接開啟或建立 Android project。但有一點要注意的是，Gradle使用的 JVM要選 jbr-11，不然在編譯時會有錯誤產生。 安裝有支援 M1 CPU 的 JDK。目前網上找到的文件，都是建議安裝 Zulu。 Java Download | Java 8, Java 11, Java 13 - Linux, Windows \u0026amp; macOS\n升級 Gradle 版本到 7.0。7.0 開始有支援 M1。 Gradle Release Notes\n安裝 Android Emulator 不知道為什麼，從 Intellij IDEA 中無法正常的下載 Arm64-v8a 的 ROM image，所以我是從裝好的 Android Studio Canary 版本中的 SDK Manager 中安裝，然後再回到 Intellij IDEA 中開發。目前只有 Android SDK 30 和 S 有適合 M1 CPU 的 ROM image，但也夠我用了，其他就接上實機來測試。\n在使用的時候，發現 Android SDK 30 的 ROM image 有問題，裝好後能開啟，但是無法正常連上網路。改成下載 Android S 的 ROM image 就 okay 了。\n安裝 ohmyzsh ohmyzsh/ohmyzsh\n安裝 Brew Homebrew\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-m1-mac-mini-%E4%B8%8A%E9%96%8B%E7%99%BC-android-app/","summary":"\u003cp\u003e最近剛拿到 M1 Mac Mini 最基本規格(8GB RAM/256GB SSD)的機器，開始嘗試要用它來開發，所以會這兒記錄有哪些需要注意的地方。(持續更新中)\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eAndroid Studio\u003c/strong\u003e 下載號稱有支援 M1 CPU 的 Arctic Fox Canary 15，但是速度還是很卡。所以後來改成使用 \u003cstrong\u003eIntelliJ Community Edition 2021.1.1\u003c/strong\u003e。原以為在 IntelliJ 上得要自己安裝 Android plugin，但似乎原本就有內建了，所以裝好後就可以直接開啟或建立 Android project。但有一點要注意的是，Gradle使用的 JVM要選 jbr-11，不然在編譯時會有錯誤產生。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/acf835681671/1_lezW1YgmFDsQFH7P6xmZfw.png\"\u003e\u003c/p\u003e","title":"在 M1 Mac Mini 上開發 Android App"},{"content":"\n購入文石 Onyx Nova 3 Color 一個多月了，差不多可以來寫篇心得文了。先把本篇的重點都寫在最前面好了。\n什麼人適合買 Nova 3 Color **已經有其他黑白電子紙產品，想要嚐鮮的人。**雖然彩色電子紙技術 Kaleido 已經演進到第二代了 (Kaleido Plus)，但由於色彩顯示的技術限制，注定了用 Kaleido 的電子紙產品在不開前光的情況下，畫面會比黑白電子紙還要暗，對比度也較差。在光線不好的環境下，不開前光會看得不舒服，開了前光，也會因為眼睛比較容易疲勞而不舒服。所以，如果已經有其他黑白電子紙產品的話，可以因應環境來決定當下要使用哪台閱讀器，不用強迫自己去適應機器。 看書看雜誌看漫畫看網頁一定要有彩色的人，就算顏色失真也沒關係，就算畫面有殘影也可以接受的人。Nova 3 Color 在部分顏色的呈現上，會失真的很嚴重，而且殘影相當嚴重(跟採用上一代 Kaleido 技術的海信 A5pro CC 比起來，Nova 3 Color 的更新效果完全不及格)。不過，儘管它的效果再怎麼差，也還是彩色的，在需要色彩來表達畫面效果或圖表的意義時，還是比黑白畫面要令人容易接受。殘影的困擾，文石在尚未無法根治問題前，提供了一個暫時的方案，讓使用者可以藉由一些手勢，快速地刷新畫面，把殘影清掉。習慣後還算方便，但畢竟不是正解。 **平常使用電子書閱讀器時光線總是很充足的人，或是不介意三不五時開啟前光的人。**在白天有自然光的環境下，是使用 Nova 3 Color 的最佳時機，即使不開啟前光，也可以清楚地看到色彩帶來的效果。到了傍晚或是在室內時，則可以開啟前光，讓 Nova 3 Color 的畫面足夠明亮，又不致於過於刺眼。 優點\n優點大都是因為彩色螢幕都延伸而來的。Nova 3 Color 的大小我也覺得是它的優點之一，比起 6 吋的設備，它更像是一般書的大小，而且在看漫畫上的體驗更好。攜帶性也是最適中的；10吋帶出門，有時就有點重了，而且不適合隨時隨地拿出來閱讀。7.8 吋也應該是單手持閱讀的極限了吧。10吋的話還是建議放在桌上用個書架，或是雙手持著。\n寫筆記和劃線可以有彩色了！雖然不見得會想要劃得花花綠綠的，但劃紅線總是比劃黑線來得有感覺。 Nova 3 Color 是開放式系統，可以安裝自己想要的 Android Apps；而且這些 Apps 都可以用彩色的方式顯示！大部分 Android App 沒有針對電子紙螢幕做調整，所以如果使用了太多顏色，在一般黑白的設備上反而會覺得對比不夠高，有種髒髒的感覺。彩色螢幕則不會有這問題。 缺點\n比較貴。原本文石的機器就是比較高價的，彩色的螢幕讓它的價格超過了一萬元，如果不是非電子紙非買不可的話，其實可以考慮買 iPad 或是 Android 平板。 新版的系統比較不穩定。以前在 Note3 和 Max3 上不大會發生在寫筆記時，有的筆畫落筆後還是沒有被描繪出來。往往得要按\u0026quot;更新\u0026quot;按鈕，或是再劃下一筆，它才會出現前一筆的筆跡。有時會很困擾。 殘影重。我覺得這是 Nova 3 Color 目前最大的問題。如果這點能透過軟體更新有大幅改善的話，那應該會有更多人能夠接受這台機器。 最後附上我的 Nova 3 Color 和海信 A7 的待機畫面\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%96%87%E7%9F%B3-onyx-nova-3-color-%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/e921801abf46/1_NO77v43Y1Zs2nxM2CRrlHg.png\"\u003e\u003c/p\u003e\n\u003cp\u003e購入文石 Onyx Nova 3 Color 一個多月了，差不多可以來寫篇心得文了。先把本篇的重點都寫在最前面好了。\u003c/p\u003e\n\u003ch4 id=\"什麼人適合買-nova-3color\"\u003e什麼人適合買 Nova 3 Color\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e**已經有其他黑白電子紙產品，想要嚐鮮的人。**雖然彩色電子紙技術 Kaleido 已經演進到第二代了 (Kaleido Plus)，但由於色彩顯示的技術限制，注定了用 Kaleido 的電子紙產品在不開前光的情況下，畫面會比黑白電子紙還要暗，對比度也較差。在光線不好的環境下，不開前光會看得不舒服，開了前光，也會因為眼睛比較容易疲勞而不舒服。所以，如果已經有其他黑白電子紙產品的話，可以因應環境來決定當下要使用哪台閱讀器，不用強迫自己去適應機器。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e看書看雜誌看漫畫看網頁一定要有彩色的人\u003c/strong\u003e，就算顏色失真也沒關係，就算畫面有殘影也可以接受的人。Nova 3 Color 在部分顏色的呈現上，會失真的很嚴重，而且殘影相當嚴重(跟採用上一代 Kaleido 技術的海信 A5pro CC 比起來，Nova 3 Color 的更新效果完全不及格)。不過，儘管它的效果再怎麼差，也還是彩色的，在需要色彩來表達畫面效果或圖表的意義時，還是比黑白畫面要令人容易接受。殘影的困擾，文石在尚未無法根治問題前，提供了一個暫時的方案，讓使用者可以藉由一些手勢，快速地刷新畫面，把殘影清掉。習慣後還算方便，但畢竟不是正解。\u003c/li\u003e\n\u003cli\u003e**平常使用電子書閱讀器時光線總是很充足的人，或是不介意三不五時開啟前光的人。**在白天有自然光的環境下，是使用 Nova 3 Color 的最佳時機，即使不開啟前光，也可以清楚地看到色彩帶來的效果。到了傍晚或是在室內時，則可以開啟前光，讓 Nova 3 Color 的畫面足夠明亮，又不致於過於刺眼。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e\u003cstrong\u003e優點\u003c/strong\u003e\u003c/p\u003e","title":"文石 Onyx Nova 3 Color 使用心得"},{"content":"\n第一課 -면 Vst + 을수록 愈愈 맛있을수록 잘 팔려요. 愈好吃，賣愈好。\n바쁠수록 건강이 중요해요. 愈忙，健康愈重要。\n많으면 많을수록 좋아요. 愈多愈好。\n갈수록 추워요. 愈來愈冷。\n第二課 -(으)ㄹ래요? 你想要 ~ 嗎？ 집에 갈래요. 我想回家。\n比較\n집에 가고 싶어요. 最一般和模糊的說法\n= I want to go home.\n집에 갈게요. 根據情況而說的\n집에 갈 거예요. 最直接的說法。表示你計畫要回家\n혼자 할래요. 不管別人的想法，我就是要一個人做\n커피 마실래요, 차 마실래요?\n이거 볼래? 你想看這個嗎？\n언제 만날래?\n第三課 -(으)ㄹ 리가 없어요, 할 리가 없어요 不可能 그럴 리가 없어요! 那是不可能的\n눈이 올 리가 없어요! 不可能下雪\n모를 리가 없어요. 他不可能不知道\n안 추울 리가 없어요. 지금 1월이에요. 不可能不冷，現在是一月。\n이상하네요. 안 될 리가 없어요. 奇怪，不可能不 work\n第四課 -지요/-죠 對方可能已經知道要講的內容 雙方都已經知道，想再確認 오늘 날씨 춥지요? 今天天氣冷吧？！\n재미있지요?\n이게 뭐지? 這是啥(問自己\n그럴 수도 있죠. 是的，那有可能\n第五課 당신 你 당신 뭐야? 你誰呀\n第六課 동(動) 운동 運動\n동작 動作\n작동 作動 = operation (of a device)\n활동 活動\n동물 動物\n연동 聯動 = linkage; interlocking\n행동 行動\n동영상 動映像 = video\n第七課 괜찮아요 沒關係 第八課-아/어/여도 돼요, 해도 돼요 也可以，你可以不用 내일 해도 돼요. 明天做也行\n이거 나중에 해도 돼요. 晚點做也行\n컴퓨터 써도 돼요? 我可以用電腦嗎？\n오늘 일 안 해도 돼요. 今天沒上班也行\n第九課 -(으)면 안 돼요, 하면 안 돼요 不應該~\n열면 안 돼요. 不行(不應該)打開\n던지면 안 돼요. 不應該丟\n第十課 사이에, 사이에서, 중에, 중에서 在~之間 A하고 B 중에서 A和B 之間\n이 중에서 고르세요. 從這些當中選擇\n사이에서 物或人\n친구들 사이에서 인기가 많아요.\n사이에 實際位置\n은행하고 공원 사이에 있어요.\n약국은 학교하고 경찰서 사이에 있어요.\n第十一課 아무나, 아무도, 아무거나, 아무것도 任何人、任何東西、任何地方 아무나 올 수 있어요. 誰都可以來\n아무거나 주세요. 給我任何東西都好\n아무데나 좋아요. 什麼地方都好\n아무도 없어요? 沒人在嗎？\n아무것도 만지지 마세요. 什麼都不要碰\n第十二課 -아/어/여 보다, 해 보다 試著做~ 이거 써 봤어요? 這你有試過嗎？\n이거 해 보고 싶어요. 我想試試這個\n알아보다 look into something\n이거 먹어 봐요. 試(吃)看看\n누구한테 물어볼까요? 我應該(先)問誰？\n第十三課 불(不) 불안 不安 = anxiety; anxious\n불안해하지 마세요.\n불편 不便 = inconvenient; uncomfortable\n불편해요.\n불완전 不完全 = incomplete\n불완전한 정보\n불균형 不均衡 = imbalance\n성비 불균형\n= gender ratio imbalance\n불만 不滿 = complaint\n저한테 불만 있어요?\n부정확 不正確 = incorrect; inaccurate\n부정확한 발음\n부주의 不注意 = carelessness\n제 부주의로 사고가 났어요.\n부당 不當 = wrong; unfair; unjust\n부당한 일을 당했어요.\n부적절 不適切 = inappropriate\n부적절한 예문\n第十四課 가끔, 자주, 별로, 맨날, 항상 가끔 = sometimes\n자주 = 經常\n항상 = 總是\n맨날 = 總是(每天)\n별로 = 很少\n전혀 = 一點也不\n거의 = almost (but) not at all\n第十五課 아무 아무 때나 任何時候\n아무 말도 / 아무 이야기도 = no word; no mention\n아무 말도 하지 마세요. 什麼都不要說\n아무렇지도 않다 = to be alright;\n저는 아무렇지도 않아요. 我沒事\n아무한테도 = to nobody\n아무한테도 주지 말고 혼자 먹어요. 不要跟任何人分享，自己吃就好。\n아무렇게나 = just in any way; however you like it\n第十六課 韓文中的空格 adj + nount 부지런한 사람\nadv + verb 빨리 말하다\nnoun + verb 그것(을) 만들었어요.\nnoun + noun 한국 여행\n不需要空格的情況\na noun/pronoun and a marker 저는\nnouns in a proper name 한국관광공사\n第十七課 주격 조사, 축약형 字的縮寫 主題\n저는 → 전\n나는 → 난\n이것은 → 이거는 → 이건\n서울에는 → 서울엔\n어제는 → 어젠\n主詞\n이것이 → 이게\n저것이 → 저게\n그것이 → 그게\n第十八課 최상급, 최고 最好 最 (+ adjective/adverb) = 제일 / 가장\n제일 예쁜 여자 = 가장 예쁜 여자\n제일 좋은 것 最好的(東西)\n요즘 가장 인기 있는 가수는 누구예요? 最近，最有人氣的歌手是誰？\n第十九課 덜 less, completely 덜 먹다 (少做點”吃”這件事，而不是吃少一點)\n어제보다 덜 추워요. 比昨天不冷。\n덜 비싼 것 없어요? 沒有比較不貴的嗎？\n그 맥주 다 마셨어요? 這酒全喝完了嗎？\n아니요. 덜 마셨어요. 沒，還沒全喝完。\nA: 제 책 돌려주세요. 還我書\nB: 아직 덜 봤어요. 我還沒全看完\n第二十課 造長句 오늘부터 한국어를 더 열심히 공부할 거예요.\n從今天開始，要更用心地學習韓文。\n아마 내일부터 일요일까지 비가 내릴 거예요.\n從明天起到週日，可能會下雨。\n내일 시간이 있으면, 같이 커피 마실래요?\n明天有時間的話，一起喝咖啡？\n第二十一課 띄어쓰기 空格 돌리다 轉\n돌려 주다 為某人轉\n돌려주다 歸還\n돌다 轉\n돌아 가다 轉回頭\n돌아가다 回到~\n빌리다 借(進來)\n빌려 주다 為某人借\n빌려주다 借（出去)\n알아보다 = to recognize something/someone; to look into something\n알아 보다 不存在這用法\n나다 = to be born; to get out; to be out of (not commonly used on its own)\n나오다 到外面\n나가다 走到外面\n들어오다 進到裡面\n들어가다 走到裡面\n第二十二課 장(場) 장소 場所\n운동장 運動場\n주차장 駐車場\n장면 場面\n시장 市場\n목장 牧場\n수영장 水泳場\n예식장 禮式場 （結婚場地\n자기장 磁氣場\n중력장 重力場\n第二十三課 字縮寫 이렇다 = to be like this; to be this way\n그렇다 = to be like that; to be that way\n저렇다 = to be like that (over there); to be that way (over there)\n이렇게 = like this, in this way\n그렇게 = like that, in such a way\n저렇게 = like that (over there)\n이렇게 하면 = if you do it like this\n그렇게 하면 = if you do it in such a way\n저렇게 하면 = if you do it like that\n이렇다면 → 이러면\n그렇다면 → 그러면\n저렇다면 → 저러면\n이렇게 하면 → 이러면\n그렇게 하면 → 그러면\n저렇게 하면 → 저러면\n어떻게\n어떻게 하다 → 어떡하다\n어떡할 거예요? → 어쩔 거예요?\n第二十四課 Much (more), Much (less) / 훨씬 이게 훨씬 더 좋아요. 這好多了\n일본어보다 한국어가 훨씬 더 쉬워요. 跟日文比起來，韓文容易多了\n* “much more + adjective/adverb”, the word 더 can be dropped from 훨씬 더.\n* However, you cannot drop 덜 from 훨씬 덜, which means “much less + adjective/adverb”.\n第二十五課 -(으)ㄹ + 명사, 할 것 읽을 책 將要讀的書\n보낼 편지 將要送的信\n먹을 것 食物\n탈 것 交通工具\n마실 것 飲料\n내일 할 일이 많아요. 明天要做的事很多\n냉장고에 먹을 것이 전혀 없어요. 冰箱裡完全沒食物\n第二十六課 -(으)ㄴ + 명사, 한 것 어제 본 영화 昨天看的電影\n친구가 말한 카페 朋友提到的咖啡廳\n-(으)ㄴ 것 過去式\n-는 것 現在式\n-(으)ㄹ 것 未來式\n第二十七課-(으)ㄴ/(으)ㄹ/ㄴ 것 같다, 한 것 같다, 할 것 같다 我認為\n“I think that…”\n-(으)ㄴ 것 같다\n-는 것 같다\n-(으)ㄹ 것 같다\n내일 비 올 것 같아요. 我覺得明天會下雨\n누가 한 것 같아요? 你覺得是誰做的？\n곧 도착할 것 같아요. 我覺得我再一會兒會到\n第二十八課 -아/어/여지다 變得 adj 예쁘 + -어지다 = 예뻐지다\n이상하 + -여지다 = 이상해지다\n재미있 + -어지다 = 재미있어지다\n날씨가 따뜻해졌어요. 天氣變溫暖了\n컴퓨터가 빨라졌어요. 電腦變快了\n내일 다시 추워질 거예요. 明天又要變冷了\n第二十九課 -게 되다, 하게 되다 漸漸～ v -게 되다\n１不想做，但最終還是做了\n안 먹고 싶어요. 그런데 먹게 돼요.\n２一開始並沒有打算做，但最終做了\n지난 주에 영화를 보게 됐어요.\n３當想要講說最終會做，不論是不是想做\n오게 될 거예요.\n내일 알게 될 거예요. 你明天會發現的\n어떻게 여기에 오게 됐어요? 怎麼來了\n第三十課 長句子 열 명 초대했는데, 아무도 안 올 수도 있어요.\n我邀請了十個人，但有可能誰都沒來\n오늘은 어제보다 훨씬 따뜻한 것 같아요.\n我覺得今天比昨天溫暖多了\n지금 카페에서 어제 산 책을 읽고 있어요. 現在正在咖啡廳看昨天買的書\n","permalink":"https://plateaukao.github.io/blog/posts/talk-to-me-in-korean-level-4-%E7%AD%86%E8%A8%98/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/eaee8e94974e/1_DZko6oxnmTD4M6nsm6hSzA.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"第一課--면-vst--을수록愈愈\"\u003e第一課 -면 Vst + 을수록 愈\u003cdel\u003e愈\u003c/del\u003e\u003c/h3\u003e\n\u003cp\u003e맛있을수록 잘 팔려요. 愈好吃，賣愈好。\u003c/p\u003e\n\u003cp\u003e바쁠수록 건강이 중요해요. 愈忙，健康愈重要。\u003c/p\u003e\n\u003cp\u003e많으면 많을수록 좋아요. 愈多愈好。\u003c/p\u003e\n\u003cp\u003e갈수록 추워요. 愈來愈冷。\u003c/p\u003e","title":"Talk to Me in Korean Level 4 筆記"},{"content":"\n第一課 너무 太多、非常 너무 커요. 太大\n너무 어려워요. 太難\n너무 맛있어요. 非常好吃\n너무 잘했어요. 幹得好\n너무 在古早之前，只能用在否定句；但在現代，正反都可以使用了。\n第二課 動詞 ＋ -고 代替 그리고 이 책은 재미있고 싸요. 這本書又有趣又便宜。\n어제 친구를 만나고, 영화를 봤어요. 昨天跟朋友碰面，看了電影。\n커피 마시고, 도너츠 먹고, 케익 먹고, 우유 마셨어요. 배불러요.\n喝了咖非、吃了甜甜圈、吃了蛋糕、喝了牛奶，飽了。\n第三課 方位 앞에, 옆에, 위에, 밑에, 뒤에 前面、旁邊、上面、下面、後面\n소파 위에서 자고 있어요. 正睡在沙發上。\n第四課 -(으)ㄹ까요? 要不要一起～？我在想，是不是～？ 帶有不確定性，想知道結果\n내일 비가 올까요? （你覺得／我在想）明天會下雨嗎？\n커피 마실까요? 맥주 마실까요? （我們要不要）喝咖啡呢？喝啤酒呢？\n어제 했을까요? （你覺得她）昨天做了嗎？\n第五課 쯤, 약, 정도 大約，大概 第六課 -(으)ㄹ 거예요 vs -(으)ㄹ게요 未來式比較 前者表示自己的意思，不受其他人／事影響。 後者主要是根據前後文，而下的決定。 -(으)ㄹ게요 使用情況\n* 你根據別人的反應而改變決定。\n* 你想了解其他人的反應或想法。\n* 因為別人說了什麼，所以你決定做什麼。\n내일 4시쯤에 갈게요. 괜찮아요?\n明天４點左右去，沒關係嗎？\n第七課 -아/어/여서 所以，因此 （表原因） 비가 와서 못 가요. 在下雨，所以去不了。\n오늘은 바빠서 영화를 못 봐요. 今天很忙，所以看不了電影。\n공원에 가서 책을 읽을 거예요. 去公園、看書。\n케익을 사서 친구한테 줄 거예요. 買蛋糕、給朋友\n固定用法\n-에 따라서 根據\n계획에 따라서 진행하겠습니다. 我會照著計畫進行。\n예를 들어서 舉例來說\n예를 들어서, 이렇게 할 수 있어요. 舉例來說，你可以像這樣做。\n第八課 같아요 看起來像，似乎是、我覺得… 비슷하다 [bi-seu-ta-da] = 像～\n도쿄는 서울하고 비슷해요? 東京像首爾嗎？\n그 이야기는 거짓말 같아요. 這故事像是謊言。\n第九課 -ㄴ 것 같아요 似乎是（＋動詞）、我覺得… 이상한 것 같아요. 看起來怪怪的。我覺的怪怪的。\n여기 비싼 것 같아요. 我覺得這裡很貴。這地方似乎很貴。\n그런 것 같아요. 我覺得是這樣。\n現在式\n詞幹 + -는 것 말하는 것 [ma-ra-neun geot] = 說話，某個人說的話，說話這件事\n過去式\n詞幹 + -(으)ㄴ 것\n말한 것\n未來式\n詞幹+ -(으)ㄹ 것 말할 것\n第十課 -기 전에 在（做某件事）之前 사기 전에 잘 생각하세요. 買之前好好想清楚。\n돈을 내기 전에 付錢之前。\n第十一課 ㅂ 불규칙 不規則變化 如果在 ㅂ 之前的母音是 오，那把它換成 오\n如果不是的話，就換成 우\n돕다 [dop-da] 幫忙\n도 + 오 + 아요 = 도와요\n어렵다 [eo-ryeop-da] 困難\n어려 + 우 + 어요. = 어려워요.\n第十二課 그래도 仍然 비가 와요. 그래도 갈 거예요? 下雨了，你還是要去嗎？\n저도 돈이 없어요. 그래도 걱정하지 마세요. 我沒錢，但不用擔心。\n第十三課 -ㄴ 將描述性動詞轉為形容詞\n작은 집 小的家\n빠른 차 快的車\n비싼 컴퓨터 貴的電腦\n더 작은 가방 있어요? 你有小一點的袋子嗎？\n第十四課 -는/(으)ㄴ/(으)ㄹ + 名詞 將一般動詞轉形容詞 좋다 + 사람 = 좋은 사람 好人\n어려운 게임 困難的遊戲\n노래하는 사람 在唱歌的人\n아는 사람 認識的人\n좋아하는 사람 某人喜歡的人／我喜歡的人\n자주 먹는 한국 음식 있어요? 有你常吃的韓國菜嗎？\n第十五課 그러면, 그럼 如果是這樣的話 그럼 이거는 어때요? 那麼，這個如何呢？\n第十六課 Let’s / -아/어/여요 讓我們一起～ -아/어/여요 [polite/plain]\n시작해요.\n-(으)시죠 [honorific]\n시작하시죠.\n-자 [informal]\n시작하자.\n-(으)ㄹ래요? [polite/casual]\n시작할래요?\n-(으)실래요? [polite/formal]\n시작하실래요?\n第十七課 위해 = 위해서 成為 (很正式) 건강을 위해서 為了健康\n일본어를 배우기 위해서 為了學日文\n第十八課 밖에 + 否定句 nothing but 밖 = 外面\n밖에 = 什麼之外\n콜라밖에 안 마시다. 只喝可樂。\n한국어 조금밖에 못해요. 我只會一點韓文。\n第十九課 다음에 ~之後 -(으)ㄴ + 다음에\n-(으)ㄴ + 후에\n-(으)ㄴ + 뒤에\n편지를 받은 다음에 收到信之後\n편지를 받은 후에\n편지를 받은 뒤에\n점심을 먹은 다음에, 도서관에 갔어요. 吃完午餐後，去了圖書館。\n第二十課 -아/어/여도 即使 택시를 타도, 시간이 오래 걸려요. 即使搭計程中，還是很花時間。\n第二十一課 動詞 + -는데 / 名詞 + -인데, 形容詞 + -ㄴ데 用法\n１。描述背景，接著要給建議，或提要求，或問問題。\n내일 일요일인데, 뭐 할 거예요? 明天是星期日，要做什麼呢？\n２。解釋情況，接著要講發生了什麼事。\n어제 자고 있었는데, 한국에서 전화가 왔어요. 昨天睡覺時，接到韓國來的電話。\n３。顯示結果，跟之前的動作或情況相反。\n아직 9시인데 벌써 졸려요. 已經９點了，但我還是想睡。 준비 많이 했는데(요)… 我準備了很多…（但是…\n４。表示驚喜或驚訝\n멋있는데(요)! 太酷了吧。\n５。問問題\n지금 어디에 있는데(요)? 現在人在哪兒啊？\n６。期待得到回答\n지금(요)? 지금 바쁜데(요). 現在？現在我很忙耶…\n第二十二課 -(으)ㄹ 수도 있어요 或許，可能 알 수도 있다 可能知道\n저 내일 못 올 수도 있어요. 我明天可能不行來。\n第二十三課 학(學) 학생 (學生)\n학교 (學校)\n학원 (學院)\n수학 (數學)\n과학 (科學)\n어학 (語學)\n언어학 (言語學)\n경제학 (經濟學)\n학자 (學者)\n유학 (留學)\n유학생 (留學生)\n전학 (轉學)\n전학생 (轉學生)\n학년 (學年)\n학기 (學期)\n방학 (放學) ＝＞ 放假的意思！\n장학금 (奬學金)\n장학생 (奬學生)\n복학생 (復學生)\n학습 (學習)\n독학 (獨學)\n第二十四課 르 不規則變化 고르다 選擇\n뭐 골랐어요? 你選了什麼？\n모르다\n저도 몰라요. 我不知道。\n빠르다\n비행기는 빨라서 좋아요. 飛機很好，因為它們很快。\n자르다\n누가 케이크 잘랐어요? 誰切了蛋糕？\n기르다 rasie, grow\n토끼를 5년 동안 길렀어요. 我養兔子５年了。\n第二十五課 -네요 ~呢！ 커요 大\n크네요 大！（帶有驚訝)\n아무도 안 왔네요. 誰都還沒來呢！\n第二十六課 ㄷ 不規則變化 듣다 聽\n듣고 있어요. 我在聽\n걷다 走路\n한 시간 걸었어요. 走了一小時。\n묻다 問\n물으면\n싣다 to load\n실을 거예요\n깨닫다 了解\n깨달았어요\n第二十七課 반말 / 존댓말 -ㅂ니다 最正式的說法\n-(아/어/여)요 [-(a/eo/yeo)yo] 有點正式\n-아/어/여\n詢問是否可以說 반말\n* 말 놔도 돼요?\n* 말 편하게 해도 돼요? * 우리 말 놓을까요?\n말 놓으셔도 돼요.\n말 편하게 하셔도 돼요.\n例子\n존댓말: 이거 뭐예요?\n반말: 이거 뭐야?\n존댓말: 어제 친구 만났어요.\n반말: 어제 친구 만났어.\n第二十八課 Verb stem + -자 親近的 let’s ~ 하자\n먹자\n내일 보자.\n안녕하세요 -\u0026gt; 안녕 [an-nyeong]\n안녕히 가세요 -\u0026gt; 안녕 [an-nyeong] / 잘 가 [jal ga]\n안녕히 계세요. -\u0026gt; 안녕 [an-nyeong] / 잘 있어. [jal i-sseo]\n저 -\u0026gt; 나 [na]\n~ 씨 / You -\u0026gt; 너\n네 / 예 -\u0026gt; 응 / 어\n아니요 -\u0026gt; 아니 / 아니야\n第二十九課 ㅅ 不規則 낫다 恢復，變好\n나아요. please feel better.\n감기 다 나았어요?感冒好了嗎？\n잇다\n이었어요. 連上\n짓다\n이 집을 누가 지었어요? 誰建了這房子。\n例外\n웃다 笑\n웃어요.\n씻다 洗\n씻을 거예요.\n벗다 脫（衣服）\n신발을 벗어 주세요. 請脫鞋。\n第三十課 실(室) 화장실 (化粧室)\n교실 (敎室)\n연습실 (練習室)\n대기실 (待機室)\n회의실 (會議室)\n병실 (病室)\n미용실 (美容室)\n사무실 (事務室)\n교무실 (敎務室)\n실장 (室長)\n실내 (室內)\n실외 (室外)\n","permalink":"https://plateaukao.github.io/blog/posts/talk-to-me-in-korean-level-3-%E5%BF%AB%E9%80%9F%E8%A4%87%E7%BF%92/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/62b71fa21800/1_H1B1vfMtAVl-AiA9sW0DpQ.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"第一課-너무太多非常\"\u003e第一課 너무 太多、非常\u003c/h3\u003e\n\u003cp\u003e너무 커요. 太大\u003cbr\u003e\n너무 어려워요. 太難\u003cbr\u003e\n너무 맛있어요. 非常好吃\u003cbr\u003e\n너무 잘했어요. 幹得好\u003c/p\u003e\n\u003cp\u003e너무 在古早之前，只能用在否定句；但在現代，正反都可以使用了。\u003c/p\u003e","title":"Talk to Me in Korean Level 3 快速複習"},{"content":"在前一篇提到，可以利用 epublib 將網頁內容儲存成 epub 檔案，便於事後用其他的閱讀軟體中操作。不過，由於當時對於 epublib 函式庫不夠熟悉，只能將網頁中的文字部分儲存下來。今天要來說說，怎麼實作儲存圖片的方式，和使用者體驗改善。\n如何實作儲存圖片 Sequence Diagram\n在一般網頁內容中，圖片的網址是來自於網路上的某個路徑；當要將網頁內容存進 epub 時，這些圖片的來源必須改成一個參考值 (reference)，然後在 epublib 中加入新的 Resource，把參考值和實際的 image binary content 連結在一起。所以，在儲存 epub 時，先用 jsoup 將網頁內容中的所有圖片的元件找出來，把它們的 src 改成特定的參考值；將這些參考值和真正的網址存進一個 map。然後呼叫 saveImageResources() 函式，把 map 中的所有圖片都下載下來，一一加入 epub Book 中。\n60 行：處理網頁圖片內容；64行：儲存圖片至 epub 中\n處理網頁內容，將 img 的 src 換成特定參考值，並傳出map\n99行：利用 epublib 儲存圖片；103行：從網路下載圖片\n上圖中的 99 行很重要，最後面帶入的 MediatypeService.JPG 一定要加上才行。不然在儲存到既有的 epub 時，會發生之前儲存好的圖片會消失不見。應該是因為在讀取既有的 epub 時，因為事前沒有給予正確的 mimeType 值，造成它無法順利地讀取出來，也就無法在第二次寫入時，再完整地寫進同個 epub 文件中。\n下載圖片的小枚角 上圖中的 getResourceFromUrl 看似簡單，但有時候卻會發生，明明 browser 中看得到圖片，可是轉到 epub 時，這些圖片依然沒有被正常的寫入。後來再追查了一下，有兩個地方可能會出錯。一個是需要加入 User-Agent 的 header，避免有些網站會擋掉沒有帶正常值的 request；第二點是有些圖片的網址可能原本是 http 型式，在 connection 回來時，可能不會立即回傳 binary data，而是回傳 responseCode 301 (redirect) ，要你重新去試一下 https 的網址拿資料。在這情況下如果沒有從 response header 中去抓出 Location 值重新要一次資料的話，就會無法正確地取回圖片。\n以下是再更改過的抓圖實作。這次，應該真的處理了大部分 fail cases 了吧。\nhandle redirect urls\n自動抓取 Reader Mode 的網頁內容再儲存至 epub 大部分的網頁充滿著各式各樣的元件，不是很適合當成電子書閱讀。原先的版本需要使用者自己手動切換到 Reader Mode 後再儲存成 epub 文件才會有比較好的效果。這動作做多了，就覺得很煩。所以趁著 8.7.0 版本發布，把它改成自動的了。為了要達到這功能，必須修改原先寫好的 MozReadability.js 。\n原先的 javasctipt 在最後段會透過 Readability 產生處理好的 article，再將 article 轉換成 html 塞到原本的網頁中。但是這步驟對於儲存 epub 來說是不必要的。\n原先的內容\n所以這一段被獨立了出來，只有在需要將現有網頁轉成 Reader Mode 時才注入。但是在儲存 epub 時，會利用下面的方式得到 Readability 處理好的 html 內容：\n將轉好html 回傳至 native，進行後續處理\n相關連結 版本 8.7.0 Release Version v8.7.0 · plateaukao/browser\n打造 E-ink 專用的瀏覽器 (VI) — 匯出 epub 檔案\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-vii-%E5%8C%AF%E5%87%BA-%E6%9B%B4%E6%A3%92%E7%9A%84-epub-%E6%AA%94%E6%A1%88/","summary":"\u003cp\u003e在前一篇提到，可以利用 epublib 將網頁內容儲存成 epub 檔案，便於事後用其他的閱讀軟體中操作。不過，由於當時對於 epublib 函式庫不夠熟悉，只能將網頁中的文字部分儲存下來。今天要來說說，怎麼實作儲存圖片的方式，和使用者體驗改善。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器 (VII) — 匯出 更棒的 epub 檔案"},{"content":"\n其實 F-Droid 上的文件也寫得蠻清楚的了，只是這兩週花了點時間在做這件事，想說還是記錄一下，以後如果打算再發佈其他 App 時，可以再回來參考一下。這篇文章會先提一下什麼是 F-Droid ，跟為什麼我要把 App 發佈到它上頭。接下來才是本文重點的 How-to 內容。\n什麼是 F-Droid 白話來說，就是個自由開源軟體的 Google Play Store 。官網上的簡介是這麼寫的：F-Droid 是一個可在 Android 平台上安裝的自由與開源的應用程式目錄，可供妳輕鬆瀏覽，安裝並持續追蹤裝置上應用程式的更新。\nGoogle 已經有推出了 Play Store，為什麼還會冒出 F-Droid 呢？Google Play Store 雖然方便，但要上架 App 必須要符合它訂下的各種規範；而且 Play Store 上的大部分 App 為了賺錢，或是為了保護自己的利益，都不會把程式碼釋出，而且還會在 App 中塞了一大堆追蹤使用者行為的實作，或是到處顯示廣告。\n對於部分比較在意個人隱私，或是在遇到 App 不好用時，更想自己動手東改西改的人來說，如果使用的 App 是 Open Source的，在使用上可以有更大的彈性。另外，有些 Android 設備則是原本就沒有內建 Google Play Store，需要有其他比較方便的下載 App 方式。\n不過，跟 Google Play Store 上的兩三百萬個 App 比起來，截至 2021 年 2 月，F-Droid 上只包含了約 3,800 個應用程式。少歸少，夠用就好，上面還有些軟體是 Google Play Store 上無法上架的好用工具，這就待大家自己去挖掘了。\n為什麼我要發佈 EinkBro App 到 F-Droid EinkBro 是一個專門開發給電子書閱讀器使用的瀏覽器。電子書閱讀器大致可以分成封閉式和開放式的系統。封閉式就是出廠時設備上有什麼軟體，就只能使用那些軟體，無法再自行安裝其他的程式。開放式則是可以透過某些方式安裝 Android App。但並不是所有的開放式閱讀器都支援 Google Play Store，這類的閱讀器使用者要自己想辦法下載 apk 再安裝，或是先找到一個專門在幫忙下載安裝 App 的軟體，省掉這些麻煩。\nEinkBro 剛上線到 Google Play Store 時，最常被問的就是，有沒有上架到 F-Droid，以及是不是可以支援 Reader Mode。\n所以，在完成 Reader Mode 之後，也花了點時間把它上架到 F-Droid，讓更多人可以方便地安裝和升級。\n怎麼進行 為了讓開發者可以順利的上架 App 到 F-Droid 上，F-Droid 官方已經有寫了蠻完整的文件，詳細方法可以看一下面這篇文章\nSubmitting to F-Droid Quick Start Guide\n再參考這篇\nHow to publish your apps on F-Droid?\n再來會介紹我操作的步驟和一些注意事項。這些內容可以縮短 merge request 時跟 F-Droid 維護者來來回回的修正。我大約花了一週才完成，有興趣的話，可以點文章最後面的 merge request 來看一下血̸淚̸史̸內容\nF-Droid 的程式碼都是放在 Gitlab 上，所以需要先建立自己在 Gitlab 上的帳號 git clone fdroidserver 和 fdroiddata 這兩個 repo 在 fdroiddata 中，加入自己 fork 好的 repo 為 remote branch 在 /metadata/ 目錄中，建立屬於自己 app 的 yml 描述檔。以 EinkBro 來當例子的話，我建立的文件是 ，內容如下 官方文件有寫到，可以利用 fdroid import --url github_repo app_dir 建立 yml 檔。但我一直試不成功。所以後來是抓 template 下來改的。抓 template 的方式是：\nwget -O metadata/info.plateaukao.einkbro.yml https://gitlab.com/fdroid/fdroiddata/raw/master/templates/app-full template 中的欄位很多，但其實只要填我上面的那些欄位就行了。其他的資訊，F-Droid maintainer 會叫你寫在 fastlane 的相關文件裡。\n在自己的 App Github repo 建立 fastlane 的相關訊息。fastlane 要寫哪些資料可以參考我在文章最後附的連結，主要是類似 Google Play Store Listing 中要填寫的資料。 將版本的 changelog 寫在特定的檔案，然後設定在 yml 檔案中，讓 F-Droid 可以讀得到 將使用到的權限要求寫在 fastlane 的文件中, 並解釋用途為何 後記 順利的話，在Gitlab Merge Request 中跟 F-Droid maintainer 來回溝通幾次，應該對方就會幫你 merge 描述檔到 fdroiddata repo 中。接下來，大約要等 2 到 4 天才會看到自己的 App 出現在 F-Droid 網站上或是 F-Droid App 中。\n雖然整個流程沒有像 Google Play Console 那麼方便，審核也是人工的，而且 deployment 要等的時間是以天來計算，但可以因此造福到不同族群的使用者，還是件好事。\n參考連結 EinkBro @ F-Droid EinkBro | F-Droid - Free and Open Source Android App Repository\nMerge Request on Gitlab add app with id info.plateaukao.einkbro (!8700) · Merge requests · F-Droid / Data\nFastlane Data example for EinkBro plateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E4%B8%8A%E6%9E%B6-android-app-%E5%88%B0-f-droid/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/952cc3ce6882/1_zhT1QmtbeKceJuD5ttes3g.png\"\u003e\u003c/p\u003e\n\u003cp\u003e其實 F-Droid 上的文件也寫得蠻清楚的了，只是這兩週花了點時間在做這件事，想說還是記錄一下，以後如果打算再發佈其他 App 時，可以再回來參考一下。這篇文章會先提一下什麼是 F-Droid ，跟為什麼我要把 App 發佈到它上頭。接下來才是本文重點的 How-to 內容。\u003c/p\u003e","title":"如何上架 Android App 到 F-Droid"},{"content":"為電子書閱讀器而開發的瀏覽器，在繞了一大圈之後，終於又繞回了電子書本身。原本的實作就已經可以將網頁輸出成 pdf 檔案。不過，如果希望產生的檔案可以更有彈性地調整字型大小，或是在不同的設備上閱讀，轉成 epub 格式會是更好的選擇。但目前市面上的行動瀏覽器都沒有這樣子的設計，頂多提供輸出成 pdf，mhtml 等格式。\n為了在 EinkBro 中加入這功能，當務之急是先找到一個可以在 Android 設備上新增、開啟和編輯 epub 的函式庫。找來找去，似乎選擇也不多，只有 Epublib 是個比較完整的方案。\npsiegman/epublib\nEpublib 是個純 Java 的函式庫，不過也支援在 Android 系統中使用。雖然它已經很久沒有什麼更新了，但看起來功能應該還算符合我的需求。它並沒有把它的函式庫放在常用的 public repository 中；文件中是建議直接下載 jar 檔案，塞在 Android app 中。不過這和發布到 F-Droid 的規則有抵觸，所以我去 mavenCentral 找到了其他人幫忙上傳的版本。比最新版差了些 commit，但並不影響我需要的功能。\nMaven Central Repository Search\n實作內容 目前我在實作上，會先跳出系統的 document picker ，讓使用者建立一個新檔名，或是選擇一個已經存在的 epub 檔案，再把目前網頁的 innerHtml 內容取出，利用 Epublib 注入到 epub 檔案中，當成一個新的章節。\ncode snippet for saving epub file\n677 行的 getRawHtml() 利用注入 javasript 取得 raw html。拿到手的 raw html 還不行直接拿來用，因為它裡頭有些字元已經被轉換過了，需要加入\nimplementation \u0026#39;org.apache.commons:commons-text:1.7\u0026#39; 然後呼叫 StringEscapeUtils.unscapeJava() ，還原成可用的字串。\n電子書名及章節名稱 getBookName() 和 getChapterName() 都會跳出 AlertDialog 讓使用者輸入字串。因為不想再用 callback 的方式，所以把它們都包成 suspendCoroutine，讓程式好讀一下。\n以 getChapterName() 為例，在代入恰當的參數到 TextInputDialog 後，就可以得到想要的章節名稱。\n而 TextInputDialog 只是很單純地實作了 show()，在使用者有任何互動後，把字串傳出。\n待改進的地方 目前只能把純文字的資料存進 epub 檔案裡。希望在不久的將來，也可以把圖片存下來，讓產生的 epub 更有價值。\n相關連結 從 v8.6.0 開始支援匯出 epub 檔案 EinkBro - Apps on Google Play\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-vi-%E5%8C%AF%E5%87%BA-epub-%E6%AA%94%E6%A1%88/","summary":"\u003cp\u003e為電子書閱讀器而開發的瀏覽器，在繞了一大圈之後，終於又繞回了電子書本身。原本的實作就已經可以將網頁輸出成 pdf 檔案。不過，如果希望產生的檔案可以更有彈性地調整字型大小，或是在不同的設備上閱讀，轉成 epub 格式會是更好的選擇。但目前市面上的行動瀏覽器都沒有這樣子的設計，頂多提供輸出成 pdf，mhtml 等格式。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器 (VI) — 匯出 epub 檔案"},{"content":"打造 E-ink 專用的瀏覽器 (V): 閱讀模式與直排模式 這一篇的技術成份稍微高一點點。要談到的功能，從一開始開發 EinkBro 就有想要做，但是一直找不到比較好的實作方式。在經過兩三週忙於其他的功能開發後，終於在這週找到比較恰當的切入點和相關技術的參考，得以完成心目中大致上的效果。\n講了一堆廢話，究竟是什麼功能呢？\n閱讀模式和直排模式！ 我們先從閱讀模式說起。不知道閱讀模式的人，可以看一下下面的文章介紹。這功能在兩年前由 Apple 先在 Safari 瀏覽器中推出，讓使用者可以更專心地閱讀網頁內容，不被廣告和不必要的元件(標題欄，底部欄，側邊欄位等)干擾。\niOS上Safari瀏覽器新功能：啟用閱讀介面，還能變更字體與背景 | T客邦 | LINE TODAY\n之後，各家瀏覽器大廠也開始推出了類似的功能。 Brave 瀏覽器在推出他們自家的 SpeedReader 功能時，有順便把市面上主要的實作都拿來做比較，有興趣的人可以下載下面的 pdf 檔來了解一下。主要比較了 Readability.js，Safari Reader View, Google Chrome DOM Distiller，BoilerPipe 和他們的 SpeedReader。\nOriginal Readability.js 對於這功能有了大致的了解後，要來決定一下怎麼開發 EinkBro 中的這功能。第一個想法自然是找 Readability.js 來使用；一來它是 Open Source 的，二來，許多瀏覽器的閱讀模式也是從它延伸而來的。用它的話，網路上可以找到的資源也會比較多一些。所以先上 Github 找了一個早期的版本來用。\nKerrick/readability-js\n把 readability.js 放到 Android 的 assets 目錄中，然後利用下面的方式載入檔案，塞到目前的網頁中。由於 readability.js 裡頭已經包含了初始化自己的程式碼，而且會把處理過的內容，直接蓋掉目前的網頁內容，所以只要載入它就等著它把畫面換成比較單純的顯示模式。\n這方式實作上雖然很簡單，但是產生出來的效果卻不是很好。第一點是，很多應該不屬於主要內容的部分，還是留在畫面中；第二點是圖片的部分，通常會過大。\nEinkBro v8.3.1\n字型忽大忽小尚可忽略不去計較，但是該小一點的圖，大得嚇人；和一堆不必要的元素依然存在，這效果很難讓人有想閱讀下去的念頭。\nOriginal readability.js + readability.css 仔細再研究了一下 readability.js 的內容，它在處理 html elements 時，除了會刪除不必要的元素之外，也會把想要留下的元素加上特定的 class 或 id；然後它另外還有一個 readability.css 檔案，應該就是用來規範這些新加的 class 是要怎麼呈現在畫面中的。\n難怪只有執行 readability.js 的話，跑出來的畫面有點慘不忍睹。\n於是接下來的版本，我把 readability.css 也加進去。(也順便把 WebView 從 Java 重構成 Kotlin 檔案，不然改起來有點痛苦)\nAdd readability.css in v8.4.2\n結果好像沒有什麼幫助。在 Medium.com 中的文章的圖片還是大得驚人。看來用原始的 readability.js 不是好的方向。(不然就是我對 javascript 和 css 太不熟了，找不出為什麼結果沒兜起來)\ncrux — stand-alone library 接下來的選擇原本是想要找別人已經包好的 library，可以直接處理 html raw data，產生出可以拿來呈現用的 html，我再塞回 WebView。在測試的時候，因為也需要先 inject javascript 到現有的網頁中，拿到 raw html，不知道為什麼常常會拿不到資料。有可能有時候在網頁還沒完全被載入時我就先按了按鈕造成的。不管怎樣，這個方式還是不理想。\nchimbori/crux\nreaderview feature from Mozilla Mobile 在開發的過程中，我主要拿來比較閱讀模式效果的 reference app 分別是 Brave Browser 和 Firefox。Brave 在前面的 pdf 中有提到，它們的作法是在畫面還沒有真的繪製之前就可以先處理，速度會比其他的方案快；但缺點是，它的作法相對上也比較複雜，我不見得能夠比照辦理。\n所以，我去找了 Firefox App的原始碼來看(早該這麼做了)。原來 Mozilla 也有把它們的原始碼放在 Github 上。而且針對 Readability 的改良版也特地獨立成一個 repository 開發。\nmozilla/readability\n一開始我很開心地拿了這版本來套用。但又犯了一開始就犯的錯誤。javascript 只處理了資料的去留，但是真正呈現的部分還是需要對應的 css style file 來輔助才行。於是我找到了 Firefox Mobile App 的 repository，也找到了它 reader view 真正實作的地方。\nmozilla-mobile/android-components\nandroid-components/components/feature/readerview/src/main/assets/extensions/readerview/ 目錄下，除了有上述的 readability.js 外，它又包了一層 readerview.js 和輔助的 readerview.css 。這兩者的實作才是真正發揮 Readability 威力的地方。有興趣的人可以進去看一下。大概說就是：readability 把資料處理完變成 article object 後， readerver.js 會拿 article 中的每個資料欄位，一個個貼上特定的 class name，然後在 readerview.css 中，針對這些 class 加上 UI 的呈現方式。\n因為 readerview.js 中有很多是跟 firefox App 互動的實作，我不行整個檔案直接拿來套用，所以我是抽取裡頭我需要的程式碼來用而已。抽出來的程式碼都在這兒：\nplateaukao/browser\n於是，跟 Firefox App 效果幾乎一樣的閱讀模式完成了！(還有預估閱讀時間要多久，不過這數值感覺不是很準確)\nv8.5.0 圖片大小正確，字型不會太雜亂\n直排閱讀 這功能對於瀏覽器來說，應該是個沒人想過會存在的功能。\n從十幾二十年前開始有瀏覽器以來，瀏覽器就一直是以橫讀為主。而中文閱讀習慣，也漸漸地變成橫式閱讀。除了實體出版的小說有一定比例還是會用直排發行之外，連電子書有支援直排功能的也不多(最近有愈來愈多的趨勢就是了，很好)。\n所以網路上找得到的文章或是討論，也大都圍繞著電子書的直排支援上。在電子書都還支援得不是很完整的情況下，何況是一般的網頁瀏覽呢？\n下面是一篇對我幫助很大的文章。裡頭提到中文直排的現況，和 css style 的相關語法支援。透過文章中提到的 css style 語法，我得以初步的將網頁轉為直排。\n電子書直橫轉換有什麼困難？\n程式碼如下：\n雖然文字部分可以成功轉為直排，而且是由左往右讀；但是畫面中的其他元素全都不受控制地散在畫面中：\nv8.4.2\n這功能雖然加了好幾週，但是轉換後的直排效果，就要看每個網頁的特性。有的看起來走位的元素比較少，可以正常的直排閱讀；有的就跟上面的例子一樣，把畫面東一塊西一塊地蓋住，想看也看不了。\nCombo 技： 閱讀模式 + 直排 直排功能的窘境，無法直接在直排的實作上排除，因為網頁上的元件寫法千千萬萬種，不大可能針對每一種都去處理直排的應變方式。\n但是，搭配上最近剛實作好，接近完美的閱讀模式，讓直排重生了！開啟閱讀模式後，畫面上的元件已經是可以完全(幾乎？)在掌控之中；這時再加上直排的處理 — 一個堪用的瀏覽器直排功能誕生了！\nv8.5.0 直排效果\n雖然還有些小地方要處理，但現在的直排模式已經可以拿來日常使用了。對於內容較多的中文網頁內容，切換成直排模式，在閱讀上的感覺會更接近於電子書。\n至於內容裡夾雜的數字，桌上型電腦的瀏覽器其實有支援 css style 語法可以將其轉正；但在 Mobile 上的 WebView 目前都還沒有支援；得要自己撈出這些數字，再利用 將它轉正。以後有時間應該會再補一下這個修正。而內容中的英文單字和句子…就無能為力了。\n參考資料 EinkBro - Apps on Google Play\nplateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-part-v/","summary":"\u003ch3 id=\"打造-e-ink-專用的瀏覽器-v-閱讀模式與直排模式\"\u003e打造 E-ink 專用的瀏覽器 (V): 閱讀模式與直排模式\u003c/h3\u003e\n\u003cp\u003e這一篇的技術成份稍微高一點點。要談到的功能，從一開始開發 EinkBro 就有想要做，但是一直找不到比較好的實作方式。在經過兩三週忙於其他的功能開發後，終於在這週找到比較恰當的切入點和相關技術的參考，得以完成心目中大致上的效果。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器: Part V"},{"content":"打造 E-ink 專用的瀏覽器 (IV): 字型 不同尺寸的電子紙設備\n不知不覺，這系列已經來到第四篇了。雖然沒有什麼人在看，但畢竟是花了一點時間東拼西湊來的，要整理後記錄下來才是自己的東西。如果還沒有看過前幾篇的話，可以先從下面第一篇看起，因為，這篇是繞著 WebView 中的字體在打轉，跟第一篇的內容有些關聯。\n打造 E-ink 專用的瀏覽器: Part I\n這次的重點是字型。先來說說字型大小。 現在市面上的電子紙設備大大小小各種尺寸都有：從海信出的 A5 手機(5.8吋)，A7(6.7吋)，Kindle (6吋)，到 Onyx 出的 Nova3 (7.8吋)，Note3(10吋)， Boox Max Lumi(13吋)。每種尺寸都有比較合適的字型大小，如果瀏覽器可以快速地調整字型大小的話，應該是個很方便的功能。\n這對 Android WebView 來說，是件很容易的事。在 WebSettings 中可以直接呼叫函式來達成。\nwebSettings.setTextZoom(100); 預設字型大小是 100，想要放大或縮小字型，只是改變這個數字就行。這方式雖然修改起來很快速，但是它只會調整到字型的大小，其他畫面上的元件並不會隨之放大。所以偶爾會看到放大的字體顯示超出它原本的範圍，是個稍微可以忽略的小缺點。\n調整字體粗細 有些網站為了美觀，字體可能會選用比較細的，或是在呈現上把顏色調得比較淡。這對電子紙來說，在閱讀上都會帶來困擾。部分電子書閱讀器廠商(像是 Onyx) 提供較多元的系統調整方式，讓使用者可以視情況自己將字體加粗，或是調高對比度。但這並非每家廠商都有類似的設定可以修改。\n所以，如果從瀏覽器本身能支援調體字型粗細和顏色的話，就能更不受 E-ink 設備本身的限制。實作這功能需要對 WebView inject javascript 。作法在第一篇文章中有提到，這篇再貼出來復習一下。將想要注入的 CSS Style 字串，先轉成 bytes ，再塞到下面的函式就可以。\nprivate void injectCss(byte[] bytes) { try { String encoded = Base64.encodeToString(bytes, Base64.NO_WRAP); loadUrl(\u0026#34;javascript:(function() {\u0026#34; + \u0026#34;var parent = document.getElementsByTagName(\u0026#39;head\u0026#39;).item(0);\u0026#34; + \u0026#34;var style = document.createElement(\u0026#39;style\u0026#39;);\u0026#34; + \u0026#34;style.type = \u0026#39;text/css\u0026#39;;\u0026#34; + \u0026#34;style.innerHTML = window.atob(\u0026#39;\u0026#34; + encoded + \u0026#34;\u0026#39;);\u0026#34; + \u0026#34;parent.appendChild(style)\u0026#34; + \u0026#34;})()\u0026#34;); } catch (Exception e) { e.printStackTrace(); } } 上面的作法大致上的概念是：注入 javascript，在現在的網頁中生出一個 \u0026lt;style\u0026gt; 的 element，裡頭有我們要的 style 描述。以加粗這個 style 來說的話，可以注入下面的內容：\n* { font-weight:700 !important; } font-weight 可以是 100 ~ 900。詳細的說明可以參考 MDN。而 !important 表示，要蓋掉所有其他的設定。\nfont-weight\nCSS !important Property\n效果如下：\n左：加粗 / 右：原始畫面\n更換雲端字型 開啟 inject javascript 這扇大門後，許多事都變得可能了。換字型這件事，原本我想要做的是讓使用者可以下載字型到手機上，然後再去讀取字型來呈現。不過目前還沒有找到可以怎麼做。\n網路上面的文章都只有提到怎麼將想要的字型塞到 Android 的 asset 目錄下，然後從 asset 目錄中讀取。這種方式沒有辦法解決使用者自行下載字型的情況。\n既然還找不到怎麼讀取下載的字型，我又不想塞字型檔到瀏覽器 App 中，轉個念頭，不如去載入雲端字型好了。雖然每次都要載入，但至少是目前唯一可以換字型又不增加 App 大小的方法。\n要載入網路字型的話，最有名的莫過於 Google 推出的 Google Font。其中包含了 CJK 字體(Chinese Japanese, Korean)，還有其他許多的選擇 (可以參考下面連結)。\nGoogle Fonts\n目前 Onyx Boox 最新 firmware 使用的字體是細圓體；而海信手機用的則是黑體。以可讀性來說，海信的黑體比細圓體還要易讀。但其實我最想要的是明體。所以在 Google Web Font 中找了明體的字型，利用下面的 CSS Style 把網頁的字型改掉。\n@import url(\u0026#39;https://fonts.googleapis.com/css2?family=Noto+Serif+TC:wght@400\u0026amp;display=swap\u0026#39;); @import url(\u0026#39;https://fonts.googleapis.com/css2?family=Noto+Serif+JP:wght@400\u0026amp;display=swap\u0026#39;); @import url(\u0026#39;https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@400\u0026amp;display=swap\u0026#39;); body { \u0026#34;font-family: \u0026#39;Noto Serif TC\u0026#39;, \u0026#39;Noto Serif JP\u0026#39;, \u0026#39;Noto Serif KR\u0026#39;, serif !important; } 原先只加了 Noto Serif TC ，也就是繁中的明體。但是因為平常還有在看日文和韓文的網頁，只有中文變成明體，混在文章裡的日文和韓文還是原來的字型，整個感覺很差。所以就也把 JP 和 KR 也加了進來，在繁中找不到時，會依序再去找日文和韓文的對應字型。效果如下：\n左：原本字型 / 右：Google 字型\n這些更動都已經更新到最新版的 EinkBro 了。有興趣的人也可以去下載來試試。\nEinkBro - Apps on Google Play\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-part-iv/","summary":"\u003ch3 id=\"打造-e-ink-專用的瀏覽器-iv字型\"\u003e打造 E-ink 專用的瀏覽器 (IV): 字型\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/effefafbbc1f/1_KvBG58L_-Sx9apLhgs1eHw.jpeg\"\u003e\n\u003cem\u003e不同尺寸的電子紙設備\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e不知不覺，這系列已經來到第四篇了。雖然沒有什麼人在看，但畢竟是花了一點時間東拼西湊來的，要整理後記錄下來才是自己的東西。如果還沒有看過前幾篇的話，可以先從下面第一篇看起，因為，這篇是繞著 WebView 中的字體在打轉，跟第一篇的內容有些關聯。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器: Part IV"},{"content":"海信 A7 的使用心得與海信A5 / A5proCC 的比較 A5proCC / A7 / A5\n購入海信 A7 大概有兩個月的時間了，也差不多可以來寫一篇心得文，把這陣子的使用感想記錄下來。\n購買管道與流程 跟之前的 A5 和 A5proCC 一樣，都是從網路上的京東商城直接下單，京東可以不透過集運就把商品送到台灣。運送時間大約是六到七天。中間要記得安裝 EZWay app 讓它可以順利過海關。\n【海信A7】海信（Hisense）A7 阅读手机A7 6.7英寸水墨屏 电纸书阅读器 6GB+128GB 全网通5G手机 曜石黑【行情 报价 价格 评测】-京东\n硬體 海信 A7 最大的賣點是 6.7 吋的電子紙螢幕，高達 300 ppi，以及用了顆號稱效能很好後且支援 5G 的晶片。螢幕品質和清晰度真的之前的 A5 好很多；但是那個晶片很糟糕。5G 的支援，對電子紙手機來說，是沒有必要的。由於換了這顆晶片，整體的效能甚至比早半年多推出的 A5 差，而且比之前還要耗電許多，以前可以三四天充一次或更久；A7 通常兩天就得充電一次。重度使用時，一天充一次也不是不可能的。前一陣子迎來官方第一次的更新，耗電程度似乎有些改善；不過畫面的反應速度還是沒有比 A5 好多少。官方說三月還會有一次更新，希望到時候可以對效能有更多的調校。\n另一點是重量。6.7 吋的設計，讓整台手機的重量相當的重。單手持使用，可能十幾分鐘就會覺得手酸。要長時間使用的話，建議兩手輪流拿，或是稍微靠在桌上使用。至於大小，習慣後倒還是可以接受，口袋也還放得下。\n螢幕的話，確實比 A5 要細緻，尤其是切換到均衡模式時，對比真的很高，看起來很舒服。\n軟體 從 A5 開始，海信在電子紙手機上就有很多實用的功能。比方說在通知欄可以設定 slide one screen。將它開啟後，滑動畫面時，可以以手指貼著螢幕慢慢移動。它會在手放開時才一併更新畫面。如此一來，可以減少因為滾動畫面更新而造成殘影。當然這功能也不是盡善盡美，在部分的 App 裡是無法使用的。\n另外，A7 的顯示模式面板比之前的版本又更方便了。除了可以快速在四種模式中切換，還可以進到 Eink 詳細設定畫面，做更多的細部調整。\nA7 提供四種模式，分別是清晰，均衡，順暢，快速。清晰模式是四者中解析度最好的模式，但是畫面的反應速度卻是最慢最有感，適合用在看電子書時。均衡模式是對比最高的一個模式，有時它的可看度甚至比清析模式還要好。它也是我最愛用的模式。\n快速和順暢基本上就沒有什麼太大的差別。平常在切換 app 或是大量需要捲動畫面的場景時，就比較適合使用這兩個模式。\n如果覺得某些 app 的畫面對比度不夠高，或是黑不夠黑，白不夠白，都可以在顯示模式面板中調整。雖然沒有提供依每個 app 設定不同的參數；但至少能夠整體性的調整，也算是很不錯的了。\nA7 有內建的應用程式商店，可以下載一些常用的 App ，但是並不支援 Google Play Store，所以在使用上有點麻煩。安裝應用程式得要從網路上 app pure 或是其他有提供 apk 下載的地方先下載後再自行安裝。如果是 Google Play Store 上曾付費過的應用程式，就無法這麼做。\n網路上有文章在解釋怎麼利用一些方式讓海信 A7 依然能夠安裝 Play Store 上的應用。我也有些了一篇說明，有興趣的話，可以參考下面這個連結：\n如何安裝 Google Apps 在海信 A5PRO / A5PRO CC / A7 上\n電子紙手機，除了要拿來上網，用 App 之外，看電子書也會是主要的功能之一。平常我比較常用的軟體有 Moon+ Reader Pro，雖然它的介面不是很好看，但是它的控制選項很多，還可以設定長按字串時，是要開啟設定好的字典搜尋或是劃線。對於常常要看外文書的我來說這功能很實用。\n多看也是我用來看自己的電子書軟體之一。選擇他的原因是，他支援直排顯示。只要在 pc 上先用 calibre 把書籍轉成直j排的，就可以匯入到多看中，用直排的方式來閱讀。目前可以匯入電子書又支援直j排的軟體很少，多看算是其中表現還不錯的軟體。\n再來是各家的電子書軟體，大都a也可以正常d的運作。我有裝來用的有： Readmoo， Kobo，HyRead3，Google Play Books，Kindle。HyRead 最近升級後，也開始支援直排閱讀，所以我比較有在它上面的圖書館借書來看。\nMoon+ Reader Pro / Kindle / HyRead3\nKobo / 多看 / ReadMoo\n看網頁的話，安裝了 firefox，xbrowser，和一個自己開發的 EInkBro。firefox 是因為可以跟電腦上的版本同步一些資訊，而m且支援度是最好的，所以在其他browser某些操作不正常時，會再回到firefox來使用。firefox 的網址列是在畫面下方，這也是一個很貼心的設計。\nxbrowser 是個不到 1MB 的小軟體，但是該有的功能它都有了，像j是去廣告，利用音量鍵來翻頁，縮放字型等。對於電子紙來說，它幾乎符合了大部分的需求。\nEInkBro 是我拿 Github 上的 Open Source FOSS Browser 東改西改的 browser，一樣有去廣告的功能，重點是它在介面上和操作上，考慮了更多電子紙的特性：比f方說它介面上的 icon 顏色都比較深，對比較高；有專屬的按鈕操作翻頁；還拿掉了大部分不必要的動畫效果。如果想要多了解 EInkBro 的話，可以參考下面這一篇文章：\n打造 E-ink 專用的瀏覽器: Part III\nEinkBro - Apps on Google Play\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%B5%B7%E4%BF%A1a7%E7%9A%84%E4%BD%BF%E7%94%A8%E5%BF%83%E5%BE%97%E8%88%87%E6%B5%B7%E4%BF%A1a5-a5procc%E7%9A%84%E6%AF%94%E8%BC%83/","summary":"\u003ch3 id=\"海信-a7-的使用心得與海信a5--a5procc的比較\"\u003e海信 A7 的使用心得與海信A5 / A5proCC 的比較\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/6807aad9baa8/1_mKl09uGCFWJWnsuPjYkSfg.jpeg\"\u003e\n\u003cem\u003eA5proCC / A7 / A5\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e購入海信 A7 大概有兩個月的時間了，也差不多可以來寫一篇心得文，把這陣子的使用感想記錄下來。\u003c/p\u003e\n\u003ch3 id=\"購買管道與流程\"\u003e購買管道與流程\u003c/h3\u003e\n\u003cp\u003e跟之前的 A5 和 A5proCC 一樣，都是從網路上的京東商城直接下單，京東可以不透過集運就把商品送到台灣。運送時間大約是六到七天。中間要記得安裝 EZWay app 讓它可以順利過海關。\u003c/p\u003e","title":"海信A7的使用心得與海信A5, A5proCC的比較"},{"content":"打造 E-ink 專用的瀏覽器 (III): 支援 VI 快速鍵 去不了的滑雪場\n離 Part II 文章完成竟然快要一年了！我們在 Part III 裡，就來聊聊這一年多了那些新功能吧。如果你是第一次看到這系列文章，歡迎從下面兩篇先讀起。\n打造 E-ink 專用的瀏覽器: Part I\n打造 E-ink 專用的瀏覽器: Part II\n利用音量鍵來控制翻頁 雖然下方的 bottombar 已經添加了上下按鈕，可以在不捲動畫面的情況下，往下一頁或往上一頁，但是能用來翻頁的方式，永遠不嫌多啊！最近購買的海信電子紙手機 A7 ，有特地把音量鍵做得比較偏於手機的中間，操作起來更像是一般電子書閱讀器翻頁鍵。所以為 EInkBro 瀏覽器加上音量鍵翻頁的功能，就顯得相當實用。\n這功能的改動很小，只要在 BrowserActivity 的 onKeyDown 函式裡，處理音量鍵的 key event 就行了。\n支援自己寫的字典 App 和支援 Multi-Window 在看網頁時，難免會看一些外文的網站。如果遇到看不懂的單字時，通常會長按看不懂的地方，把該字選擇起來，然後在跳出的 popup window 中選擇一個適當的 action 。比方說如果有裝 Google Translate 的話，就可以選它；不然就是會按 Copy，然後跳到其他字典 App 中查尋。\n如果次數少的話，這麼來來回回還不打緊；如果很頻繁的話，就會很惱人。為了解決這個問題，我另外寫了一個 naver dict ，把 Naver 字典包裝起來，讓它可以快速地幫忙查詢網頁。再透過 Multi-Window 的功能，一邊顯示 browser，一邊顯示 naver dict，達到不需要畫面跳來跳去的效果。對 naver dict 有興趣的人可以看一下下面這篇文章：\n更利於使用的 Naver 字典 App\n在 browser 中，長按文字後，跳出的選項相當多。常見的 Copy, Paste, 全選等，可能會列在最前面；自己安裝的一些 App 支援，則需要再點一次 more 才會看到。如下圖一般：\n如果需要自己中意的某些選項排在前面一點的話，可以透過 override Activity 的 onActionModeStarted ，在裡頭把拿到的 menu 拿來改寫一下，把自己想要的選項留下。因為這功能不見得是所有人需要的。所以我會先判斷 device 上是不是已經有安裝了 naver dict，有的話才進行這操作。\n修改完後，menu 就只剩下我自己想要的選項：Copy 和 naver dict。如下圖：\n關於 Multi-Window 的支援，只要在 AndroidManifest.xml 中小改一下就行。\n移除不必要的動畫 原本 browser 中許多 popup dialog 是利用 Android 的 BottomSheetDialog 實作的。 BottomSheetDialog 的好處是，會有上下滑動的感覺；但這效果在 EInk 設備上反而是種反效果，所以我把大部分的 BottomSheetDialog 都直接改成單純是 show/hide 的 View 而已，減少了畫面的閃動。\n讓浮動 Navigation 按鈕更明顯 browser 一開啟時，下方會有工具列。如果嫌它礙眼，可以長按 refresh 按鈕，它就會不見，變成一顆浮動的按鈕。需要時再點一下浮動按鈕展開它。浮動按鈕原本有底色，雖然有設定成半透明，但還是很礙眼。所以我把它改成只有一個圓圈，裡頭加三個點；而背景是全透明的。這麼一來，如果下方剛好有字或是圖案，就依然清晰可見。如下圖 (請見右下角)\n左圖：黑色底 / 右圖：白色底\n重新整理功能清單，並將字型縮放搬到第一層 原本的功能清單有點累贅，每四到五個分成一個 tab。如果常用的不在第一個 tab，就得要多點幾下才能啟動想要的功能。所以我把它們全部排在一個畫面，並加入了我很常用的字型縮放功能。\n左側：原本的設計 / 右側：新的設計\n更多的翻頁方式！ 除了工具列的翻頁按鈕，音量鍵按鈕，我還加入了手勢翻頁(無處不翻頁啊)！目前在設定中，可以針對工具列還有浮動按鈕的手勢做功能設定。我在裡頭多了兩個選頁，讓使用者可以利用手勢執行翻頁。\n支援 Vi Key Bindings 平常我會拿 13 吋的 Onyx Boox Max 3來看網頁。通常會把它放在架子上，然後連上藍芽鍵盤操作。這時如果要畫面內容上上下下，或是開新分頁，開網址，關閉分頁，切換分頁等動作時，免不了需要再用手去觸控畫面來達成。\n如果是用一般 Mac 電腦的話，我都會在瀏覽器中加裝支援 Vi Key Binding 的套件，讓我省去大部分需要滑鼠操作的動作。那麼，要是在自製的 EInkBro browser 也能支援 Vi 的話那該有多好啊。\n抱著這個想法，我把一些基本的 Vi Key Bindings 也都加上了！目前有支援的快捷鍵有以下這些：\nb : open bookmarks\nd : remove current tab\ngg : go to top\nj : page down\nk : page up\nh : go back\nl : go forward\no : focus on url bar\nt : new a tab\nvi : zoom in font\nvo : zoom out font\n/ : search in page\nG : go to bottom (好像有點問題)\nJ: show next tab\nK : show previous tab\n雖然離第一版修改已經隔了快一年，但一直到最近幾週才有再認真的調整成適合 EInk 設備的操作方式。希望接下來能再做更多好用的功能，讓電子書設備能夠愈來愈適合看網頁。\n下載連結與參考資料 EinkBro - Apps on Google Play\nplateaukao/browser\nEInkBro v8.0 Release\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-part-iii/","summary":"\u003ch3 id=\"打造-e-ink-專用的瀏覽器-iii-支援-vi快速鍵\"\u003e打造 E-ink 專用的瀏覽器 (III): 支援 VI 快速鍵\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/5b0528220252/1_jRjy4uExMSLbeMhksIrQUg.png\"\u003e\n\u003cem\u003e去不了的滑雪場\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e離 Part II 文章完成竟然快要一年了！我們在 Part III 裡，就來聊聊這一年多了那些新功能吧。如果你是第一次看到這系列文章，歡迎從下面兩篇先讀起。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器: Part III"},{"content":"對於 Android SDK 或 Library 的開發者來說，最近有件大新聞，JFrog 要 sunset 他們家的 JCenter, GoCenter, ChartCenter 服務了。JCenter 是目前大部分 Android 開發者在發佈函式庫時的主要去處。這項服務即將在明年(2022) 2 月中止，而新的版本發布則是可以到這個月底 (2021/03/31)。\nInto the Sunset: Bintray, JCenter, GoCenter, and ChartCenter\n為了因應這項改變，原先發布的 library，都要趁著這段過度時間趕快搬到 mavenCentral 去才行。下面會介紹怎麼改成由 mavenCentral 來發布 library。步驟不多，但要注意的細節蠻多的，一不小心就會踩坑。\n主要步驟 建立 sonatype.org 的帳號 在 Jira 上開張票建立一個新的專案 證明你有 groupId domain name 的權限 加入 gradle plugin，並填寫所有必需的設定 產生 artifacts ，用 GPG 加密，並上傳 發布 設定 Github Actions (非必要) 建立 sonatype.org 帳號 到下面這個網址建立一個 sonatype.org 的帳號\nSign up for Jira - Sonatype JIRA\n帳號建立好後，如果這個 groupId (domain) 是你自己的，那請直接進行下一個步驟；如果你要發布的 library/sdk 是要針對一個已經存在 mavenCentral 上的 groupId，你必須在 JIRA 中先開一條 ticket，請已經有權限的人 comment 一下，這樣子 sonatype 的人才會幫你加發布的權限。範例如下：\nAdd \u0026ldquo;Deployer Role\u0026rdquo; to new user for \u0026ldquo;com.linecorp.*\u0026rdquo; project\n在 Jira 中開票建立新專案 前往這個連結，開啟一個新的 project 的票。\nissues.sonatype.org\n證明你是 groupId domain 的持有者 你必須證明你擁有 groupId 倒過來的 domain。舉例來說，如果你的 groupId 定為 info.plateaukao ，你就必須在 plateaukao.info 的 DNS provider 中加入下面的 TXT 記錄：\n@ TXT 3600 OSSRH-xxxxx OSSRH-xxxx 要填入你建立 project 的那張票的 id，讓 Sonatype 可以去驗證你的 domain。\n加入 gradle plugin，並填寫所有必需的設定 在案子的最上層 build.gradle 加入下面設定 View gist\n在 library 的 build.gradle 加上所需的 plugin 和相關設定。這邊以 com.linecorp.linesdk 為例子 View gist\n第4，5行的 maven-publish 和 signing 是 gradle 內建的 plugin，所以不需要額外的 implementation，只要引用就行。\n第 25 行開始的 afterEvaluate 則是當 artifact 都建立後要進行 deploy 的相關設定。第 33 ~ 35 行將 binary aar，和 javadoc，sources codes 都列為 artifacts。這三者都要存在才可以正常的發布到 mavenCentral。\n72 行設定的 credentials 是 sonatype 上的帳號和密碼，可以在 gradle.properties 中設定 repositoryUsername 和 repositoryPassword 。如果專案是 Open Source 的，不想把相關的資訊寫在 gradle.properties ，可以參考一會兒提到的 Github Actions 設定方式，讓 gradle 在建置時，可以從 Github 設定中取得相關資訊。\n80 行設定的 signing 區塊是針對 GPG signing 的設定。GPG signing 要先建立 GPG key。 在 Mac 上的話，可以先透過 Brew 安裝 gpg。\nbrew install gpg 然後 GPG key 可以在電腦上利用下面的指令建立：\ngpg --full-generate-key 建立好 GPG key 後，要把 public key 上傳到某個 server 上。我是選擇上傳到 ubuntu.com\ngpg --keyserver hkp://keyserver.ubuntu.com --send-keys ur_pgp_key_id 在電腦上要驗證上傳 artifacts 的話，可以先產生 secret key 的檔案 (利用下面的指令產生 secring.gpg 檔案。\ngpg --export-secret-keys -o secring.gpg 然後把 build.gradle 中的 signing 區塊改成\nsigning { sign publishing.publications } 如果不是指定 useInMemoryPgpKeys 的話，signing 會試著去找 sign 的下面相關參數\nsigning.keyId=xxx signing.password=xxxxx signing.secretKeyRingFile=secring.gpg 因為最後我們會希望可以透過 Github 的 Actions 來直接發布 library，所以無法將 secring.gpg 直接放在 git 中，我們要用另一個型式把它存在 Github 的 project secrets 設定裡。這會在下個步驟才講到。\n如果一切都設定得當，這時可以執行下面的指令把 library 的 artifact 上傳到 sonatype 上。\n./gradlew publish 順利的話，你可以在這個連結中，點選左邊的 staging repositories 看到你上傳的 library artifacts。\noss.sonatype.org/#stagingRepositories\n這時，你可以手動點選該 repository，先把它 Close，再選 Release，就完成了發布。\n發布 不過，如果希望同樣是透過 gradle task 發布的話，可以加上 89 ~ 95 行的 nexusStaging 設定。基本上就是填入跟 73, 74 行一樣的帳號密碼，然後執行下面的指令，完成 release。\n./gradlew closeAndReleaseRepository 設定 Github Actions 設定 Github Actions 包含兩個步驟：第一個步驟是先去 Github project settings 的 secrets 設定好所需的參數。透過 mavenCentra 發布的 task，會需要用到下面幾個參數\n第二個步驟則是在 repository 的 .github/workflows/ 下產生 actions 的 yml 設定檔。舉例來說，可以建立一個 ossrh.yml 。內容可以參考下方\nView gist\n關於 Github Actions 的設定，可以另行尋找其他的文件，在此不多做說明。\n第 27 行就是上面提到的 task，將 artifacts 上傳到 sonatype staging repositories；第 33 行則是真的 release 到 mavenCentral 去。\n這邊有一點要注意的是：signing 的參數跟一開始提到的不太一樣。如果是在電腦上手動進行 sign 的動作時，需要的參數是 keyId, password 和 secret ring file。但如果想要透過 Github Actions 來做的話，就要把 keyId 和 secret ring file，換成 signing key。 signing key 的值在 MacOS 中可以透過下面的指令取得：\ngpg --armor --export-secret-key your_email@address | pbcopy 一切設定好之後，在 Github project 的 Actions tab 下，會出現Deploy to OSSRH 的選項，讓你可以透過它直接發布版本到 mavenCentral。\n為了避免誤觸這個 Action，在 7 ~ 9 行，還有 29 行加了條件，要先輸入 release 字樣才會真的開始動作。\n後話 目前還沒有找到比較容易的檢驗方式，所以都需要不斷地把設定 merge 到 github repository 後，然後試著執行 Github Actions 看成不成功。如果之後有找到好的方法，會再更新於本篇心得中。\n參考資料 英文的教學文件\nHow to upload jars to maven central\nSonatype 官方文件\nOSSRH Guide\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%9C%A8-mavencentral-%E4%B8%8A-release-libraries/","summary":"\u003cp\u003e對於 Android SDK 或 Library 的開發者來說，最近有件大新聞，JFrog 要 sunset 他們家的 JCenter, GoCenter, ChartCenter 服務了。JCenter 是目前大部分 Android 開發者在發佈函式庫時的主要去處。這項服務即將在明年(2022) 2 月中止，而新的版本發布則是可以到這個月底 (2021/03/31)。\u003c/p\u003e","title":"在 MavenCentral 上 Release Libraries"},{"content":"Web Browser for Android E Ink Devices (EinkBro) As the CPU power is getting stronger, and screen refresh speed keeps improving for E-Ink devices, more and more people start to use E-Ink devices to browse the internet. It’s doable; however the experience is still not good enough. Existing browsers are not designed for E-Ink devices. So many UI elements are designed for normal Android devices: fancy animations, dimming background when dialogs pop up, etc.\nTo make a browser suitable for E-Ink devices, two principles should be followed while designing UI interactions:\nFewer repaint counts Make repaint area as small as possible Fewer Repaint Counts To meet this criteria, the first step is to remove animations. Animation means showing the gradual transition status from one state to another state. It’s very nice to see transitions for normal devices; however, on E Ink devices, due to its low refresh rate, the transition is usually not smooth enough, and also it causes unnecessary residual image that make screen hard to read. Removing animations not only reduces CPU processing, but also reduces screen refresh counts.\nIn addition, while browsing web pages, it’s very common to scroll down the screen to read more content. This behavior also produces image residuals on Eink devices. It’s better to browse the web just like reading a book: page by page. To fulfill this, specific buttons can be added to handle page up and page down. If the E Ink devices has physical volume keys, we can also use them for the same purpose.\nMake Repaint Area as Small as Possible If the repaint area is large, it’s more likely to create more image residual on E Ink device. So, reducing the area means fewer ghost images. An obvious example is showing a dialog. On mobile devices, Apps display dialogs for asking user whether they want to do something or not; or showing option dialogs to allow user choose from multiple items. When dialog pops up, to make user more concentrated on the dialog, system usually dims the rest of the screen. The intention is good, but this causes E-Ink device to flash the whole screen. If the dialog is well designed to achieve the same goal, the dimming effect could be removed.\nCreate an E Ink Specific Browser App So far, I haven’t seen a dedicated browser for E-Ink devices yet. I decided to make one, based on a Github project written by Gaukler Faun. FOSS Browser is a lightweight browser that supports many features, including ad blocking, tab control, gesture control, etc. I took some time to refactor the codes and make necessary modifications to make it more appealing for E-Ink devices. Now it has a lot of useful features for E Ink devices. I draw a mindmap image to layout the main features:\nNow it has following E-ink related features: Reader mode (remove headers, ads, sidebars, footers, for easier reading) Full text translation mode Vertical reading mode (suitable for Chinese and Japanese) Content display customization (bold font, font size change, font style change) Export web content as epub file PageUp/pageDown by (touch area / volume keys / onscreen buttons) tool bar configuration (show/hide actions, or re-arrange order) All available actions on toolbar\nDesktop mode feature All icons in high contrast colors Refactor most popup dialogs, so that there’s no longer animations. Remove gray mask when dialog pops up. And more generic features Re-organize menu items so that they can be accessed without switching between tabs. Action Menu\nFeature Introduction Video EInkBro Browser EinkBro - Apps on Google Play\nF-Droid Download EinkBro | F-Droid - Free and Open Source Android App Repository\nGithub Binary Release Releases · plateaukao/browser\nGithub source codes: plateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/web-browser-for-android-e-ink-devices/","summary":"\u003ch3 id=\"web-browser-for-android-e-ink-devices-einkbro\"\u003eWeb Browser for Android E Ink Devices (EinkBro)\u003c/h3\u003e\n\u003cp\u003eAs the CPU power is getting stronger, and screen refresh speed keeps improving for E-Ink devices, more and more people start to use E-Ink devices to browse the internet. It’s doable; however the experience is still not good enough. Existing browsers are not designed for E-Ink devices. So many UI elements are designed for normal Android devices: fancy animations, dimming background when dialogs pop up, etc.\u003c/p\u003e","title":"Web Browser for Android E-Ink Devices"},{"content":"在學習韓文的路上，Naver 字典是公認的好用。但如果在平板或手機上想一邊看電子書或是看網頁，然後一邊使用 Naver 字典的話；不論是網頁版或是 App 版本的 Naver 字典，用起來都很麻煩，得在不同的 App 間不停地切換。來回個幾次之後，就會放棄這麼做。\n為了有效解決這個問題，我寫了一個 Android App (naverdict)，裡頭包了一個 WebView，負責幫忙去網頁版的 Naver 字典做查詢。它的運作方式如下：\n在電子書 App 裡，從字典清單中選用 naverdict 當成預設的字典 先將 naverdict 開成 multi-window 的其中一個 App 使用者在電子書 App 中查詢單字 naverdict 被開啟，並在其 WebView 中的 Naver 網站做查詢 從字典清單中選用 naverdict 一般電子書 App 都會支援 colordict 或 mdict 的字典搜尋方式。以 Android 程式碼實作來看的話，這些 App 都會去掃系統有哪些程式支援 colordict.intent.action.SEARCH Action 的 intent 。能吃這個 Action 的程式，就是能用來查詢單字的程式。只要在我的 naverdict 的 Activity 加上這個 Action 的 intent-filer 就可以達到被其他 App 喚起的功能。更詳細的設定可以參考這篇文章：\nColorDict Intent API for 3rd party developers\n如果是想出現在一般瀏覽器選擇字串後的 action bar 上的話，則是要支援 android.intent.action.PROCESS_TEXT 才行。\n設定好的 Activity xml 大概會長成下面這樣：\n當 Activity 收到這 intent 時，只要從 intent 中抽出字串，把該字串組合成 Naver 字典網頁查詢的 url ，往 WebView 中送就行了。實作如下：\n將 naverdict 開成multi-Window 的其中一個 App Android 從 7.0 開始有支援 Multi-Window 的功能，但是因為使用上不是很好用，而且也不是那麼直覺地可以開啟這功能，所以一直沒有被廣為使用。關於 Multi-Window 的介紹可以自行前往下面 Android 官網瞧瞧。\nhttps://developer.android.com/guide/topics/ui/multi-window\n通常它的啟動方式是做在 Recent App 的介面中，中文叫做「分割畫面」。在進入 Recent App 列表後，點選該選項即可。然後畫面的下方再看想要開啟什麼電子書 App 或是一般的瀏覽器。\n在電子書 App 中查詢單字 我比較常用的電子書 App 是 Moon+ Reader Pro。它支援長按畫面文字時，會自動截取前後 space 間的字串，然後餵給事先設定好的字典程式。\n比方說在看書時，遇到 나뉘고，不確定這是什麼意思，也不知道它是什麼樣的動詞形容詞變化而來的時候，長按畫面，它會很聰明地抓取這段文字，然後往我寫好的 naverdict App 送。這時上方的字典就會去 naver 網站查詢，並把結果顯示出來。\n畫面已經切成兩個同時開啟的 App 了，所以不會有畫面蓋來蓋去的問題。Naver 字典也很聰明地會幫你判斷這個字串，真正的單字原形是什麼，並且給你相關的解釋。\n改善 Naver 字典網站的呈現方式 如果直接到 Naver 字典網站查詢一樣的 나뉘고，你會發現畫面中充滿了不必要的元素：厚厚的標題、搜尋列、輸入方式選項、不同結果的 tab。這些元素在長按單字啟動搜尋的情境中，都是可以拿掉的。\n所以，在 naverdict 中的 WebView 還動了點手腳：在收到 onPageFinished callback 時，塞了點 JavaScript 到網頁中，把上述的那些原始移除或是隱藏。實作方式如下：\n這麼一來，進入 Multi-Window 模式時，不論是上方的 naverdict 或是下方的 moon+ Reader Pro，都沒有什麼多餘的元素干擾閱讀，可以很享受地在這樣子的組成中開心地讀書和查詢不懂的單字。\n後話 目前只串了 Naver 字典，因為目前只有學習韓文的需求。如果想把字典換成日文或英文的話，只要有合適的線上字典，稍微改一下 App 就可以使用了。\n參考資料 plateaukao/naverdict\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%9B%B4%E5%88%A9%E6%96%BC%E4%BD%BF%E7%94%A8%E7%9A%84-naver-%E5%AD%97%E5%85%B8-app/","summary":"\u003cp\u003e在學習韓文的路上，Naver 字典是公認的好用。但如果在平板或手機上想一邊看電子書或是看網頁，然後一邊使用 Naver 字典的話；不論是網頁版或是 App 版本的 Naver 字典，用起來都很麻煩，得在不同的 App 間不停地切換。來回個幾次之後，就會放棄這麼做。\u003c/p\u003e","title":"更利於使用的 Naver 字典 App"},{"content":"電子書雜談 內容 電子紙技術 電子書設備歷史、種類和現況 電子書設備哪裡買 電子書格式 電子書商城 電子書社群 電子書軟體 如何擴充 Calibre 電子紙技術 https://youtu.be/1ob0kiftAwY\n特性 如同普通紙一樣，透過環境光照亮 耗電量低 可視角極廣：理論上為 180 度 對比度高 原理 運用施加電場重新排列帶電色素粒子以顯像的技術。\nfrom Wiki\nfrom Wiki\n彩色顯式方案 印刷式彩色电子纸在传统的黑白显示层之上增加了一层彩色滤光片。缺點，解析度降低。\nfrom Hisense\nE-Ink 技術持有公司 元太\n電子書閱讀器歷史和現況 Amazon Kindle 業界龍頭，價格不貴，而且很耐用，買來好幾年都還可以運作。但功能最陽春，而且沒有什麼擴充性。\nAmazon Kindle\nSONY Libre 早期的投入者。很久以前有書城，後來收掉了。在後期有推出一些超薄的電子紙平板，但因為是封閉式系統，功能有限。不過如果想要超輕薄設備的話，還是可以考慮 SONY 的產品。\nSONY Libre\n封閉式電子書設備 Kobo , 讀墨 MooInk: 參考下方 wiki 連結\nfrom 上鋐網站\n開放式電子書設備 (可裝 Android app) 文石 Onyx, 博閱 Boyue, Hyread\nfrom 上鋐網站\nfrom 上鋐網站\n（寧心舍製作）電子書閱讀器 列表（2021年1月版） ← 這一份有列比較詳細的各廠商產品列表。\n電子紙手機：海信 想要體驗電子紙帶來的好處(善待眼睛)，平常又沒什麼在讀書(像我一樣)，那可以考慮買台以電子紙為螢幕的設備。海信推出了一系列的電子紙手機。以開放性和功能性來考量的話，平板首選會是文石 Onyx 的系列；而手機則是海信。其實電子紙手機，放眼市場，目前真的有認真投入的也只有海信了，想選也沒得選。不過，目前使用上的體驗，因為 CPU 都是比較快的，而且系統也有不斷在調整效能，所以畫面的更新速度和整體的操作上，都是很好的。\n如果想要購買的話，可以在京東官網直接下單，就可以直送台灣。\nRemarkable: 強調手寫的感覺，主打筆記。閱讀和擴充性不如其他家設備。沒買過，所以沒有什麼感想。唯一一個感想是，台灣沒有代理商，想買的話可能要找代購。購買上比較麻煩。\nHome | reMarkable\n螢幕：大上 (Dasung) 畫面更新速率快，可當一般螢幕使用。價格高。很想買來試試，但…還是再觀望一下好了。\n大上科技-DASUNG\n大上Paperlike電子紙螢幕試用心得\n怎麼選擇 大部分的書哪裡來：如果都是在特定書城買的話，可能買書城推出的設備，整合度會比較好。如果有想要在電子圖書館借書的話，可以考慮 Hyread 或是開放式的設備，再裝 Hyread App。 大部分書籍的格式：如果想要閱讀的是 pdf 文件的話，可能10吋或13吋的設備比較合適。 開放式、封閉式：有需要裝其他 app，當成 tablet 來用嗎；有想要拿來當筆記嗎；有需要在書上做筆記的需求嗎？ 使用的情境：如果常常想要帶出門，可能小台一點比較方便 (6吋或 7.8 吋) 哪裡買 如果是書城本身有在推閱讀器，那建議直接在各家書城(像是 Amazon, Kobo, 讀墨，Hyread, 等) 買，通常會有方案是比較優惠的\n上鋐: 我之前 Onyx device 都是這家買的。保固內螢幕有問題的話，可以寄回去，他會幫你送修，但他會直接再寄一台給你，不知道是全新的，還是也是修回來的。\n熊老闆: 感覺還蠻用心在經營的，可以考慮看看。\n電子書商城：讀墨、HyRead、樂天 Kobo\n大陸網站：淘寶、京東。海信手機我都是從京東買的，下單後大約 6 天可以到貨，不需要集運，比跟國內代理商還快(如果他們有代理的話)。但保固就…看個人人品了。\n電子書格式 text 最簡單的格式，沒有任何排版。\npdf 最能保存顯示模式的格式。可以加入 DRM，很多採取 Adobe DRM。一般用於論文、電腦相關書籍。比較適合用10吋以上的設備閱讀。\nepub2, epub3 許多電子書商採用的格式。可以依照設備的畫面大小，字型的大小，調整呈現的內容。可以加入 DRM 機制，很多採取 Adobe DRM。\nepub格式解析\nprc, mobi, azw, kfx mobi 是 Mobipocket 公司開發的電子書格式，成立於 2000 年，於 2005 年被 Amazon收購，做為早期 kindle 支援的主要格式之一，並在其基礎上加了 DRM，改名為 azw。 kfx是 amazon 最新的格式名稱。\n電子書商城 Amazon\nGoogle Play Book\n樂天 Kobo\n讀墨 Readmoo\nHyread\nPubu\nUdn\n博客來\n電子書社群 Facebook 電子閱讀與電子紙應用交流會｜紙本書、電子書、閱讀器 討論區\nFacebook Groups\nForum MobileRead Forums\n電子書軟體 Android Moon+ Reader Pro\nReasily\niOS Apple Books\nMac/PC\nAdobe Digital Edition\nAmazon Kindle\nKobo\nCalibre\nCalibre擴充 利用 Calibre Plugin 將 Netflix 字幕轉成 epub 電子書\n參考資料 电子纸\nE Ink 元太科技│電子墨水技術\nAmazon Kindle Amazon Kindle\nSONY Libre Sony Reader\nKobo eReader Kobo eReader\n電子書閱讀器代理商 書包網:電子書閱讀器;Kobo forma;Kobo clara HD;Kobo libra H2O;Kobo nia;Kobo;樂天;onyxboox;boox;文石;Onyx Boox Note…\n電子書概論 【介紹】電子書格式比較，平台及裝置推薦\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9B%BB%E5%AD%90%E6%9B%B8%E9%9B%9C%E8%AB%87draft/","summary":"\u003ch3 id=\"電子書雜談\"\u003e電子書雜談\u003c/h3\u003e\n\u003ch3 id=\"內容\"\u003e內容\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e電子紙技術\u003c/li\u003e\n\u003cli\u003e電子書設備歷史、種類和現況\u003c/li\u003e\n\u003cli\u003e電子書設備哪裡買\u003c/li\u003e\n\u003cli\u003e電子書格式\u003c/li\u003e\n\u003cli\u003e電子書商城\u003c/li\u003e\n\u003cli\u003e電子書社群\u003c/li\u003e\n\u003cli\u003e電子書軟體\u003c/li\u003e\n\u003cli\u003e如何擴充 Calibre\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"電子紙技術\"\u003e電子紙技術\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"https://youtu.be/1ob0kiftAwY\"\u003ehttps://youtu.be/1ob0kiftAwY\u003c/a\u003e\u003c/p\u003e\n\u003ch4 id=\"特性\"\u003e特性\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e如同普通紙一樣，透過環境光照亮\u003c/li\u003e\n\u003cli\u003e耗電量低\u003c/li\u003e\n\u003cli\u003e可視角極廣：理論上為 180 度\u003c/li\u003e\n\u003cli\u003e對比度高\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"原理\"\u003e原理\u003c/h4\u003e\n\u003cp\u003e運用施加電場重新排列帶電色素粒子以顯像的技術。\u003c/p\u003e","title":"電子書雜談(Draft"},{"content":"從去年起(2020年)，海信出了一系列純電子紙螢幕的手機，從一開始的 A5，到 A5PRO 系列，再到去年底的 A7，海信不斷地改善電子紙做為手機螢幕上的體驗。但礙於市場的關係，這幾款手機都沒有內建 GMS (Google Mobile Service)：沒有 Google Play Store, Gmail, Youtube等等。其他有跟 Google Play Service 整合的 App 在這幾款手機上也會遇到無法登入的問題，讓很多人因此打了退堂鼓。\n不過，只要用的人夠多，總是會有神人找出方式來達到想要的使用方式。A5在去年時，有人發表了怎麼透過 root 來裝上 GMS 的服務，其中要用到一些刷機的程式。一來，我不想 root 手機，因為通常 root 完，就表示跟之後官方的自動升級說 bye bye，享受不到升級帶來的新功能；二來那些刷機程式是 Windows 版的，我手邊只有 Mac，就算想裝也很麻煩；所以當時也只是看看而已。\n但就在前幾天，我入手了海信 A7，開始又在網路亂逛相關資訊時，看到了一篇介紹如何為 A5Pro 系列和 A7 裝上 Google 服務的文章，經過十幾二十分的操作，在沒有 root 的情況下，我的 A7 就可以正常地使用 Youtube App，而且還登入了我購買了 Premium 的 Google Account。瞬間讓 A7 的價值大大提升，之後，又陸續裝了幾個之前 Google Play Store 上面買的 App，也都可以正常運作。\n以下，就來介紹一下怎麼樣讓你的海信電子紙手機，在不 root 的情況下，就可以正常地使用 Google Apps 和其他整合 Google Play Services 的 Apps。\n安裝步驟簡介 以下是簡短的步驟列表，後面會一項一項解釋。\n前置步驟：確保你的手機已經開啟了開發人員選項。\n下載並安裝 adb 程式 關閉一堆系統內建的 Apps 的功能(非必要的步驟，自行決定) 下載 Aurora Store (之後 Apps 都要從這兒下載) 先 依照順序 安裝 4 個 apk 進到設置 App 下的 其他帳戶，登入 Google 帳號 再 依照順序 安裝 3 個 apk 從 Aurora Store 安裝 2accounts App 從 Aurora Store 下載想要的 App，再透過 2accounts App 來使用它 前置步驟 請利用 Google 搜尋 android 開啟開發人員選項 ，然後找自己看得懂的文章操作。下面附上三星手機的作法。在 A7, A5proCC, A5 上也是類似。\n設定 : 如何開啟 / 關閉「開發人員選項」？ | Samsung 台灣\n步驟 1 下載並安裝 adb 程式 Windows 平台的話，可以考慮原作者寫的另一篇教學\nInstall and use ADB on HISENSE A5PRO / A5PRO CC\n如果是 Mac 平台的話，可以透過 Hombrew 來安裝\nbrew install android-platform-tools 然後試一下有沒有作用\nadb devices 步驟 2 停掉一堆系統內建的 Apps (Optional) 其實這邊我覺得不見得要執行。建議可以把步驟 2 移到最後一步再做，或是如果看得懂下面是在停哪些 App 的話，只執行自己想停的就好。\n比方說 com.hmct.music 是系統內建的播音樂軟體，停掉的話，就得自己再裝一個。所以看不懂的人，建議先不要進行這一個步驟。\nadb shell pm disable-user --user 0 com.android.hplayer adb shell pm disable-user --user 0 com.android.browser adb shell pm disable-user --user 0 com.android.calendar adb shell pm disable-user --user 0 com.android.firewall adb shell pm disable-user --user 0 com.android.sos adb shell pm disable-user --user 0 com.hmct.account adb shell pm disable-user --user 0 com.hmct.antivirus adb shell pm disable-user --user 0 com.hmct.assist adb shell pm disable-user --user 0 com.hmct.imageedit adb shell pm disable-user --user 0 com.hmct.mobileclear adb shell pm disable-user --user 0 com.hmct.questionnaire adb shell pm disable-user --user 0 com.hmct.theme adb shell pm disable-user --user 0 com.hmct.voiceassist adb shell pm disable-user --user 0 com.hmct.voicetranslate adb shell pm disable-user --user 0 com.hmct.music adb shell pm disable-user --user 0 com.hmct.hmctmanual adb shell pm disable-user --user 0 com.hmct.userexperienceprogram adb shell pm disable-user --user 0 com.tencent.soter.soterserver adb shell pm disable-user --user 0 org.hapjs.mockup adb shell pm disable-user --user 0 com.hmct.jdreader adb shell pm disable-user --user 0 com.tencent.android.location adb shell pm disable-user --user 0 com.hmct.hiphone.juplugin adb shell pm disable-user --user 0 com.hmct.ftmode adb shell pm disable-user --user 0 com.hmct.semantic.analysis 步驟 3 下載 Aurora Store 用你的海信手機，到下面的網頁下載 Aurora store 的 app，然後安裝它。\nAuroraOSS\n步驟 4 依序安裝 4 個 apk 到下面的網址下載 Huawei.zip\nHuawei\n上面這連結不知道何時會失效，失效了就無解了。所以想要操作的人，趕快先下載一份下來。\n下載後，解開 zip 檔案可以看到 7 個 apk 檔案。請開個 Terminal 進到這個目錄下執行以下指令，把前 4 個 apk 裝上。\nadb install 001-Google\\ Play\\ services-com.google.android.gms-19275048-v19.2.75\\ \\(120408-269183835\\).apk adb install 002-Google_Account_Manager.apk adb install 003-Google\\ Play\\ Store.apk adb install 004-com.google.android.syncadapters.contacts_10-29_minAPI29\\(nodpi\\)_apkmirror.com.apk 步驟 5 在”設置 App”中的“其他帳戶”中登入 Google Account 進到手機中內建的設置 App，可以看到 其他帳戶 中多了 Google 的項目可以新增帳號，請在這邊加入你的 Google 帳號。\n步驟 6 安裝剩下的另外 3 個 apk 用下面的指令安裝剩下的三個檔案\nadb install 005-Google\\ Services\\ Framework-com.google.android.gsf-29-v10.apk adb install 006-modagain1gsm.apk adb install 007-com.google.android.gms2.apk 步驟 7 從 Aurora Store 安裝 2Accounts App 這步很直覺，沒有什麼需要額外說明的\n步驟 8 按照下面方式安裝 App 來使用(以Youtube為例) 開啟 Aurora Store 從 Aurora Store 中安裝 Youtube 裝好後，去執行一次 Youtube App。這一步可有可無，聽說會讓之後第7步比較順一些 開啟 2Accounts App 2Accounts 畫面右下角有顆 + 號的圓型按鈕，請點下去，並找到 Youtube，按它右邊的 + 號。 完成 5 之後，2Accounts 會複製一份 App 資料；然後你在 2Accounts 畫面上會看到多了 Youtube。長按那個 Youtube，會跳出個選單，點第一個項目，建立桌面捷徑。 回到桌面，點你剛剛建立好的 Youtube捷徑，就可以在 Youtube 中登入你的 Google 帳號囉。 注意事項 當一切都可以正常運作後，平常需要什麼軟體，就可以透過 Aurora Store 下載或更新。但是，千萬要注意！千萬不要升級 Google Play Services 和 System WebView。這兩樣如果不小心升級了，可能會造成需要 GMS 的 App 產生異常。\n參考資料 【海信A7】海信（Hisense）A7 阅读手机A7 6.7英寸水墨屏 电纸书阅读器 6GB+128GB 全网通5G手机 曜石黑【行情 报价 价格 评测】-京东\nInstall the GAPPS on HISENSE A7 / A7CC / A5PRO / A5PRO CC (updated on 15/01/2021)\n歡迎大家轉些贊助金鼓勵上面這篇文章的作者\nPay davide patanè using PayPal.Me\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%AE%89%E8%A3%9D-google-apps-%E5%9C%A8%E6%B5%B7%E4%BF%A1-a5pro-a5pro-cc-a7-%E4%B8%8A/","summary":"\u003cp\u003e從去年起(2020年)，海信出了一系列純電子紙螢幕的手機，從一開始的 A5，到 A5PRO 系列，再到去年底的 A7，海信不斷地改善電子紙做為手機螢幕上的體驗。但礙於市場的關係，這幾款手機都沒有內建 GMS (Google Mobile Service)：沒有 Google Play Store, Gmail, Youtube等等。其他有跟 Google Play Service 整合的 App 在這幾款手機上也會遇到無法登入的問題，讓很多人因此打了退堂鼓。\u003c/p\u003e","title":"如何安裝 Google Apps 在海信 A5PRO / A5PRO CC / A7 上"},{"content":"在前一篇文章中有提到怎麼將從 Netflix 下載下來的字幕轉成電子書(請看文章最下方連結)，不過流程很複雜，而且還有許多要打指令的地方。這一篇則是要介紹如何藉由 Calibre Plugin 來達到同樣的目的。\n安裝 Calibre 在 Calibre 官網下載最新的版本，並安裝。 (目前只支援 Calibre 5.x)\ncalibre - Download calibre\n安裝 Calibre Plugin — WebVttConverter 先到 GitHub 下載最新的 WebVttConverterPlugin-xxx.zip 開啟 Calibre，進到它的設定介面，點選 Plugins (畫面左下角) 在 Plugins 視窗中，點選右下方的 Load plugin from file ，選擇剛剛下載好的 WebVttConverterPlugin.zip。於下方的兩個對話框中，點 Yes，和 OK。然後重新啟動 Calibre。 Security Risk Confirmation\nInstallation Success\n此時，plugin 已經安裝好了，但畫面上還看不到。我們需要將它加到主畫面的工具列上。請再次進到設定中，點選第一排右邊的 Toolbars \u0026amp; menus ，在下拉選單中，選取 main toolbar。 WebVtt Converter at the left panel\n在左邊的 panel 最下方，可以看到一個 WebVtt Convert，請選擇它，並按畫面中間的 \u0026gt; 讓它加入 main toolbar 中，然後按畫面下方的 Apply 按鈕，再按 Close 。這時，應該在主畫面的右上方就可以看到 WebVtt Convert 了。 如何操作 WebVttConverter 如果不知道要如何下載字幕的 zip 檔案，請先看文章最後的連結。\n下載好字幕檔後，在 Calibre 中點 WebVtt Converter 按鈕，會跳出一個設定的對話框： WebVtt Converter Dialog\n字幕 zip 檔沒有解開的話，可以點 subtitle zip file，從系統中選擇 zip 檔案；如果 zip 檔案有解開來的話，可以點 subtitle directory ，選擇該目錄。選完後，Main language 和 Second language 會跳出字幕語言的選項，請選擇想要的語言。如果只想要有一種語言的話，Second language 就保持 - 。\n(此步驟非必須) Calibre 預設的 epub 電子書封面很醜，所以我多了一個可以自己設定字幕書封面的功能。你可以先從網路上下載想要的圖片(建議是直的，因為書本通常是直的)，然後點Choose Cover image按鈕。\n最後，再按下 Convert 讓它開始運作。\nPlugin 在處理檔案時，右下角會有運作中的圖示(Jobs: 1) 在旋轉，等一切完成後，就會出現完成的對話框。\nJobs running\nConversion done dialog\n關掉對話框，就可以雙擊生成的電子書起來看看效果囉。下面是 Calibre 內建的 e-book viewer 大概會看到的樣子。 將轉完的電子書，一字排開，真是賞心悅目啊~\n如何將 Netflix 上的字幕檔轉成 epub 電子書\nTampermonkey\nNetflix - subtitle downloader\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%88%A9%E7%94%A8-calibre-plugin-%E5%B0%87-netflix-%E5%AD%97%E5%B9%95%E8%BD%89%E6%88%90-epub-%E9%9B%BB%E5%AD%90%E6%9B%B8/","summary":"\u003cp\u003e在前一篇文章中有提到怎麼將從 Netflix 下載下來的字幕轉成電子書(請看文章最下方連結)，不過流程很複雜，而且還有許多要打指令的地方。這一篇則是要介紹如何藉由 Calibre Plugin 來達到同樣的目的。\u003c/p\u003e","title":"利用 Calibre Plugin 將 Netflix 字幕轉成 epub 電子書"},{"content":"韓文鍵盤輸入的記憶方式 學習一個語言，除了閱讀之外，另一個重點是書寫。早期都是用紙筆在練習，但現在的溝通很多都是在線上進行的。所以，能夠有效地透過鍵盤輸入另一個語言是很重要的。\n之前在學習日文時，因為可以用羅馬拼音打字，所以不覺得這是項苦差事。但是韓文卻不是這麼一回事。儘管韓文同樣也可以透過拼音來表示，但在輸入時，我卻沒有找到可以用羅馬拼音的輸入法。網路上最流行的應該是 2-set 的鍵盤輸入方式，將韓文字母一一對應到鍵盤上。\n除了死背之外，是不是有更好的記憶方式呢？在網路上找到一篇英文的教學，藉由一段特地編出來的英文小故事，轉成三段口訣，再將這三段口訣對應到韓文字母在鍵盤上的排列方式。想要自己看完整篇的話，可以參考下面的連結。\nTyping in Korean: How to Learn the Korean Keyboard\n想要快速掌握它的方式的話，可以記得下面三個句子\nBungee Jump, Don’t Get Scared!\nMale Ninjas, Only Long Hair\nUse Karate To Chop Pears\n這三句剛好可以對應到鍵盤上左邊，子音的部分。(最後一句的順序要調整為 K T CH P U)。\n我稍微試了一下，雖然可以透過聯想的方式，把想要的子音找出來，但是速度超級超級慢。原因是一般要輸入的時候，腦中先浮現的是韓文字的樣子或發音。這時，要先把發音挑出來，找到上面對應的 character，然後再把三句唸一下，知道它確切的位置，然後才可以輸入。\n而且，右邊的母音部分，就沒有比較好的記憶方式了。\n每個字都要這麼處理的話，怎麼樣也快不起來啊。\n所以，我打算建立起一套自己的記憶方式：將韓文每個字的外型，想辦法跟鍵盤上的英文子母聯想在一起。由於英文子母的輸入已經熟到不能再熟了；這樣子的聯結，可以讓腦中的處理只單純剩下一件事而已。\n在經過十幾二十分的思考後，我畫了下面的圖：\n以下是我自己的記憶方式：\nㅂ：(PQ) P 的下個子母是 Q\nㅈ：(WiTZ) 某個朋友的名字\nㄷ：E 少了中間一橫\nㄱ：R 的右上角\nㅅ：(Sit) S 對應到 T\nㅁ：這子母看起來像是牌，牌的最大張是 Ace\nㄴ：(LiSt) 這個有點難想\nㅇ：跟 D 的關係跟月亮一樣，全圓跟半圓的差別。\nㄹ：(Lofe) 發音上的聯結\nㅎ：(HcG)\nㅋ：(Coz)\nㅌ：(Ex)\nㅊ：(ch) → C\nㅍ：看起來像是 I I，把它想成是羅馬數字2 ，所以要找羅馬數字 5 V\nㅠ：台語的預備 → uB\nㅛ：形狀跟 Y 有像\nㅕ：日文的yo，所以是 U (這個也有點難想像，常會忘記)\nㅑ：壓抑 ya → I\nㅐ：(OH) 看到 H 就選 O\nㅔ：(yep) ye → P\nㅗ：(OH) 看到 o 就選 H\nㅓ：左邊的- 掉下來一點，就改成 J\nㅏ：K 去掉一撇\nㅣ：L 的小寫\nㅜ：TelevisoN\nㅡ：嘴巴閉起來的樣子\n再搭配上自己寫的韓文鍵盤輸入練習程式，練個幾次，就可以全部背下來啦~\nplateaukao/flutter_typing_game\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%8D%B5%E7%9B%A4%E9%9F%93%E6%96%87%E8%BC%B8%E5%85%A5%E7%9A%84%E8%A8%98%E6%86%B6%E6%96%B9%E5%BC%8F/","summary":"\u003ch3 id=\"韓文鍵盤輸入的記憶方式\"\u003e韓文鍵盤輸入的記憶方式\u003c/h3\u003e\n\u003cp\u003e學習一個語言，除了閱讀之外，另一個重點是書寫。早期都是用紙筆在練習，但現在的溝通很多都是在線上進行的。所以，能夠有效地透過鍵盤輸入另一個語言是很重要的。\u003c/p\u003e","title":"鍵盤韓文輸入的記憶方式"},{"content":"之前寫了一篇文章，描述如何用 LINE 的 LIFF API 分享帶有圖案的連結訊息。除了介紹 LIFF 外，也提到了 python 的 opengrapher 函式庫。當時並沒有深入了解它，也因此錯失了一些資訊。\n開啟 LINE LIFF v2 的無限潛力 — liff.shareTargetPicker()\n在此，要再補充一點它的功能。下面先附上它的連結：\n0rang3max/opengrapher\n上篇文章有提到，它的應用很簡單，只要下面的呼叫方式就可以得到該 url 的 opengraph 資訊：\nresult = opengrapher.parse(url)\n不過，真的在使用時發現，最常分享的 facebook link 以及 twitter link 都會失敗。原因是 twitter 有做 client side rendering，所以無法在一開始打 http request 時就取得相關的 meta data。(facebook 我就不知道了，可能是一樣的原因吧)。\n對於這個問題，opengrapher 在一開始就考慮到了，它可以藉由代入 user-agent 的方式，讓 facebook / twitter 知道這個 request 是來自於 bot ，這樣子 facebook / twitter才會把 client side rendering 改成 server side rending，將 meta data 事前就先塞好。\n在 github 的 README 最下方提及了這件事。(當時應該好好地把 README 看完的…)\n關於更詳細的解釋，可以參考 stackoverflow 的這篇文章：\nTwitter website doesn\u0026rsquo;t have open graph tags?\n現在我的 LIFF url sharer 可以正常的分享 twitter 連結了。:)\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%80%8F%E9%81%8E-python-opengrapher-%E6%8A%93%E5%88%B0-facebook-%E5%92%8C-twitter-%E7%9A%84-opengraph/","summary":"\u003cp\u003e之前寫了一篇文章，描述如何用 LINE 的 LIFF API 分享帶有圖案的連結訊息。除了介紹 LIFF 外，也提到了 python 的 opengrapher 函式庫。當時並沒有深入了解它，也因此錯失了一些資訊。\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://danielkao.medium.com/%E9%96%8B%E5%95%9F-line-liff-v2-%E7%9A%84%E7%84%A1%E9%99%90%E6%BD%9B%E5%8A%9B-liff-sharetargetpicker-24b47b0b4252\"\u003e開啟 LINE LIFF v2 的無限潛力 — liff.shareTargetPicker()\u003c/a\u003e\u003c/p\u003e","title":"透過 python opengrapher 抓到 facebook 和 twitter 的 opengraph"},{"content":"想要跟大家分享一下怎麼把 Netflix 上喜歡的影集或電影的字幕轉換成電子書。這個需求主要是來自於自己平常看影片一方面是休閒，另一方面希望同時也可以訓練自己的外語能力。\n在 PC 上的瀏覽器可以裝 extension 擴充 Netflix 讓它支援雙語字幕，達到看影片時同時練習外語的目的，但有時看完影片後，想要再回味影片對白時，就會很希望能有一份影片中的對白。而且這份對白最好還是雙語對照的，省下自己事後再翻譯的時間。\ndannvix/NflxMultiSubs\n步驟 以下是大概的步驟。後面會再針對每個步驟有詳細的說明：\n前置作業 在瀏覽器 (Chrome) 中安裝 Tempermonkey 在 Tempermonkey 中安裝Netflix subtitle downloader 在 PC (or Mac) 上安裝 calibre (一個看電子書，建立電子書的軟體) 手動作業 在瀏覽器中透過 Netflix subtitle download 下載想要的影集的字幕 zip 檔案 利用我寫的 python script 將影集字幕按照時間，集結成單一的文字檔 將文字檔加入到 calibre 中，並設定相關參數，讓 calibre 幫忙產生 epub 檔案 前置作業 前置作業中需要的 extension 和軟體分別列在下面：\nTampermonkey\nNetflix - subtitle downloader\ncalibre - Download calibre\n手動作業 在瀏覽器中透過 Netflix subtitle downloader 下載字幕。這一個步驟可以參考下面的連結。下載時，記得要選格式為 vtt，因為後面的處理是針對 vtt 格式進行的。 Netflix 字幕下载\n下載好 zip 檔案後，先把它解開來，可以看到目錄中包含所有影集的字幕。檔案名稱的組成：前面是片名，接著是第幾季，第幾集；再來會寫到語言。語言常見的是 en (英文)，zh-Hant (繁體中文), ja (日文), ko (韓文), fr (法文) 等等。如果是顧及聽障朋友的字幕的話(除了對白外，還包含畫面中聲音的描述)，會加上 [cc] 的結尾。cc 是 Closed Captioning 的縮寫。 例子\n然後也把我寫的 script 下載下來，記得要先安裝 python module webvtt-py 唷。\npip install webvtt-py 或是 pip install -r requirements.txt plateaukao/webvtt_to_html\n在 Terminal 中執行下面的指令格式：\npython convert.py lang_main lang_sub path_to_subtitles/*\\[cc\\].vtt \u0026gt; output.html lang_main 和 lang_sub 要替換成你有下載的語言；還有 path_to_subtitles要換成真的目錄位置 。以上面的例子來說，我只下載了韓文和日文的字幕檔。我想要電子書中主要顯示韓文字幕，然後日文字幕充當成翻譯，以防有些句子看不懂。所以真的指令可以下：\npython convert.py ko\\[cc\\] ja path_to_subtitles/*\\[cc\\].vtt \u0026gt; output.html 執行時間很快，應該不用幾秒就會處理完成，產生一個 output.html 。這時，可以把 html 檔案打開來看一下內容，是不是有正確產生所需的內容：\n從上圖可以看到，韓文部分都有被標上 \u0026lt;h3\u0026gt; 的 tag，日文則是標上一般的 \u0026lt;p\u0026gt;tag。這樣表示 python script 處理出來的檔案是正確的。\n開啟 calibre 軟體，把生成的 output.html 拉進 calibre 中，點選該檔案，按右鍵，然後選擇convert books -\u0026gt; convert individually。 這時會跳出一個編輯的畫面，你可以在這修改電子書的名稱(Title)，作者(Author)等資訊。\n更改好之後，請點左側的 Table of Contents，因為我們想要為電子書建立目錄；不然一部影集少說都有十幾集，沒有目錄的話，很難在每一集中跳轉。\n請在右側的 Level 1 TOC 填上 //h:title ，這樣子 calibre 才會在建立電子書時，將我在 html 中預先塞好的 title 位置當成每一集分段的地方加到目錄中。\n設定完成後，就可以按右下角的 OK，讓 calibre 開始工作囉！\n建立好 epub 電子書後，可以用 calibre 內建的 epub viewer 來觀看。在 Mac 上的話，可以用 Mac 提供的 iBooks 來觀看。 iBooks 中的效果大概如下：\n到這邊為止，已經達到了一開始的目的，可以順利地將字幕轉成電子書，透過不同的 epub reader 來閱讀。但仔細看一下，這樣子的排版似乎可讀性不是很高：韓文跟日文的字型大小都一樣；很難一眼看到主要想閱讀的主語言，有點賓客不分。\n所以，我們其實可以再進一步地改善這個建立好的電子書。在 Calibre 中，在這本書上點右鍵，選擇 Edit Book。\n這時，會跳出一個編輯書籍的介面。請從左邊選擇 stylesheet.css ；這時中間會列出所有的 html element，以及它們現在的 style 設定。 .calibre 是第二語言的 style，而 .calibre2 則是主要語言的 style。所以如果我想要韓文字大一些的話，我可以透過 font-size 的參數調整，讓它變大。\n畫面最右邊是書籍內容的即時預覽，你可以邊調整邊看是不是有符合需求。等確定後，再按 toolbar 上的 Save Icon 就行了\n經過調整，可以看到韓文字變大了一些，同時日文部分也比較真的像是註釋的感覺。\n目前整個流程還是充滿著手動的步驟，所以操作起來很煩瑣(雖然我做起來還是很快就是了)。希望之後有機會能寫成一個 plugin 加到 calibre 中，只要從 Netflix subtitle downloader 拿到 zip 檔後，就能全在 calibre 中處理。\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%B0%87-netflix-%E4%B8%8A%E7%9A%84%E5%AD%97%E5%B9%95%E6%AA%94%E8%BD%89%E6%88%90-epub-%E9%9B%BB%E5%AD%90%E6%9B%B8/","summary":"\u003cp\u003e想要跟大家分享一下怎麼把 Netflix 上喜歡的影集或電影的字幕轉換成電子書。這個需求主要是來自於自己平常看影片一方面是休閒，另一方面希望同時也可以訓練自己的外語能力。\u003c/p\u003e","title":"如何將 Netflix 上的字幕檔轉成 epub 電子書"},{"content":"開啟 LINE LIFF v2 的無限潛力 — liff.shareTargetPicker 從 LINE LIFF 開放使用以來，一直不斷有在更新 API。去年底 v2 正式上線，然後在今年三月多官方開式有文件介紹怎麼使用新的 API — shareTargetPicker。關於基本的 LIFF App 設定，可以參考本文最下面列的參考資料，在這邊就不多說了。\n今天要來談談強大的 shareTargetPicker API 的應用。 大家都知道，現在如果從其他 App 分享連結到 LINE app 中的話，通常是以純文字的方式呈現在聊天室中。雖然 LINE app 已經有做了簡單的 Url Meta Data fetcher，將連結內的主題和內文和圖片抓回來顯示，但是介面實在是很過時，而且遇到有些網站喜歡把中文字放到網址中，然後分享到 LINE 裡時，又會經過 UrlEncoding，造成一大串完全不知道在寫什麼的網址時，看到那麼一大段不可讀的 url 字串，實在是令人很無言。\n既然分享這些連結的重點是網頁裡頭的內容，為什麼在 LINE 聊天室中針對分享的連結，在呈現時不是以內容為主體，而是把一般人看不懂的連結文字顯示出來呢？\n這個想法在我腦中存在了很久，直到有一天我突然想到，這不是可以透過 LINE LIFF 的 shareTargetPicker() API 來解決嗎？\n架構 作法大致如下：\n第一步，建立 LIFF App 和 LIFF API Server\n第一步 建立一個可以輸入 url 的 LIFF App，在按下 Get Preview 按鈕時，它會去問 LIFF API server 關於這個 url 相關的 meta data。等到拿到資料後再呼叫 shareTargetPicker API，讓使用者可以選擇把資料分享到哪個聊天室。這時，我們可以把拿到的 meta data (title, description, image)用好看的 flex message 包裝起來，在該聊天室中呈現。效果大概如下：\n由於我的 LIFF API Server 是用 python 的 flask framework 寫的，在取得 meta data時，是透過 opengrapher 這個 package 完成的：\nparse url meta data\n利用 liff 送出 flex message 的方式如下面程式碼：在52行的 previewUrlInfo() 會將取回的 meta data 存到 global variables；然後 143 行會拿現有的 flex message template與 global variables 組在一起，送出訊息。\n其他程式碼是基本的 liff 操作，可以參考文章最下方附的連結說明。\nView gist\n當 LIFF App 上線後，就可以透過它送出漂亮的 Flex Message，而不再是醜醜的純文字連結。但是這時仍然有一個很大的問題：每次要分享連結前，都要特地找到 LIFF App 的連結，在 LINE App 中將它打開；把連結貼上，取得相關資料，再選擇要送往的聊天室。這過程中包含的步驟太多了，不會有人想要這麼做分享的。\n所以，需要再加上第二步。\n第二步 第二步，整合負責處理大部分流程的 native App\n建立一個 Android App (或 iOS App，但我還沒空寫 iOS App)，這裡稱它為 LINE Share，讓它負責：\n從其他 App 那兒取得 url ； 把 LINE App 叫起來，並自動開啟 LIFF App； 將取得的 url 貼進 LIFF App，讓 LIFF App 自動去抓取 meta data，抓到自動開啟share target picker 的 UI。 第一點很容易，是 native app 很普通的應用。在 Android 下，只要在 AndroidManifest.xml 中的 Activity 區段加入以下的 intent-share 設定，\n第二點則是在 Activity 的實作中去處理來自 ACTION_SEND 的 Intent 就可以了。\nView gist\n讓我們來看一下從其他 App 中分享的整個流程：\nLINE Share App Demo\n到這邊，整個應用就已經蠻完整的了。但是，你以為到這邊就結束了嗎？其實在 Android 平台還能加入更方便的使用方式。\n更上一層樓的第三步 有些時候，並不是每個 App 都會提供系統介面的分享畫面，舉一個例子來說，下面這張圖是 LINE Today 文章的分享方式：\nLINE App 中的 LINE Today 新聞文章畫面\n它只提供了客製化的 LINE 分享， Facebook 分享， Keep 儲存，和複製網址四個選項。前三者都是單純的分享文字連結。如果要透過第二步寫好的 LINE Share App的話，勢必得要選複製網址，然後再回到 Home，開啟 LINE Share App，貼上 url，進行分享。這樣子的流程還是很麻煩。\n因此，我們可以利用 Android 提供的 Quick Tile Setting來解決這問題。Quick Tile Setting 是在 Android 7.0 開始推出的功能，讓 App可以在通知欄中加入自己 App 相關的 Tile，讓使用者可以快速開關一些功能，或是叫起常用的畫面，詳細說明可以參考下面這篇文章：\nQuick Settings Tiles\n我們先在 LINE Share App 的 AndroidManifest.xml 中加入所需的 Service 設定\n然後建立 LiffSharetileService 這個 class ，加上在被點擊時想要處理的方式(叫起 LINE Share App 的畫面)\nView gist\n當 LINE Share App 被喚醒時，它必須去查看是否現在在 Clipboard 中有資料，有的話(表示剛剛使用者複製了某些資料)，就直接接回第二步，處理資料的邏輯。要檢查 ClipBoard 是否有資料，可以在 Activity 的 onWindowFocusChanged 函式中實作\nView gist\n完成 Quick Tile 的實作後，就可以從通知欄的設定中，把這個新增的 Tile 拉到比較前面的位置，方便之後使用。讓我們再來看一次有 Quick Tile的操作方式。\n還有沒有可以改進的地方呢？當然有啊。其實我最想解決的是下面這個情況：每次分享 Facebook 的連結時，因為現在 Facebook 很機車，一定要登入它才能讓你抓到連結相關的 meta data。如果把下面的連結貼到一個未登入的瀏覽器中，它只會顯示要你登入的畫面。\n這問題有沒有解呢？如果你想要分享的 Facebook 連結的瀏覽權限是地球的話(所有人都可以看)，那其實有辦法抓到相關的 meta data的。\n原本以為必須要透過 Facebook API 或 SDK 才可以拿到相關資料，但查了一輪之後，發現似乎串了 Facebook API 也無法拿到文章的 meta data。但網路找了一輪之後，終於在某個地方找到解決方式。目前 https://mobile.facebook.com/ 可以在沒有登入的情況下看到所有人可見的 facebook 文章。於是，只要把任何 facebook 的 url 的 host 換成 mobile.facebook.com，就可以順利地抓到所需的文章 meta data。\nView gist\n在一開始介紹到的 previewUrlInfo 中，先判斷 url 的 host 是不是來自於 facebook，如果是的話，先把它換成 mobile.facebook.com，再去抓資料，就可以抓到了。效果如下：\n以上，是 liff.shareTargetPicker 的小小應用。透過少少的程式碼實作，即可有效提升資訊分享的效果。\n希望在未來可以看到更多關於 liff.shareTargetPicker 的有趣應用。\n網站連結 可以在網站登入 LINE 帳號，直接 Share Flex Message 的網站 (Heroku 很脆弱，請大家不要打掛它)\n使用方式：\n先點 Login，登入 LINE 之後， 貼上想分享的連結，按下 Get Preview 如果下面有正常出現 title, description 和縮圖的話 (有時會抓不到，要看網址)，再按下 Open ShareTargetPicker 按鈕。 選個聊天室，送出訊息。 Share Url Content Message\nLINE Share App 原始碼 plateaukao/liff_url_share_app\n參考資料 以下是一些官方文件可以參考基本的設定和使用方式：\nLIFF v2 新增支援外部瀏覽器與二維條碼掃描器等功能 - LINE ENGINEERING\nShare Target Picker 已經公開，透過 LIFF 來分享訊息將更加的便利 - LINE ENGINEERING\n在 Vue3 中引入 LIFF 的 ShareTargetPicker 分享 FlexMessage 訊息給 LINE 好友 - LINE ENGINEERING\n另外關於 LIFF v1 的介紹，也可以看一下我很久以前寫的一篇文章：\n初探 LINE Message API 的新功能 LIFF\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%96%8B%E5%95%9F-line-liff-v2-%E7%9A%84%E7%84%A1%E9%99%90%E6%BD%9B%E5%8A%9B-liffsharetargetpicker/","summary":"\u003ch3 id=\"開啟-line-liff-v2-的無限潛力liffsharetargetpicker\"\u003e開啟 LINE LIFF v2 的無限潛力 — liff.shareTargetPicker\u003c/h3\u003e\n\u003cp\u003e從 LINE LIFF 開放使用以來，一直不斷有在更新 API。去年底 v2 正式上線，然後在今年三月多官方開式有文件介紹怎麼使用新的 API — shareTargetPicker。關於基本的 LIFF App 設定，可以參考本文最下面列的參考資料，在這邊就不多說了。\u003c/p\u003e","title":"開啟 LINE LIFF v2 的無限潛力 — liff.shareTargetPicker()"},{"content":"開發 Flutter App 時，偶爾需要使用 WebView 提供一些既有的功能，或是短暫地開啟一個 App 內的 Web 畫面讓使用者在不離開 App 的情況下，能夠使用其他網站的內容。這時常常會遇到別人的 Web 服務需要先登入 Google 或 Facebook 才能繼續使用。如果沒有為此修改一下 WebView 實作的話，通常是會登入失敗的。\nAndroid WebView 中的 User Agent String 在 Android 平台上，第一件要做的事是把 WebView 的 user agent 給改掉。Android WebView 預設的 user agent string 帶有 wv 的字樣，Google 在登入時會先檢查是不是一般的 browser；如果它在 user agent string 中發現 wv 字樣的話，就會顯示 403 錯誤，跟你說這不是一個合格的 browser，請你換其他的 browser 再試著登入看看。\n解決這第一個問題很容易，只要把 WebView user agent string 中的 wv 字樣刪除就可以。\nNew Window 問題 串接 Google / Facebook 登入時，Web Service常會利用 target = _blank 或是 window.open 的方式將登入相關的邏輯用新的 Web Window 呈現。在 Desktop 或是手機上 browser 的運作方式會開啟一個新的 tab 來執行，等到登入完成後，會再回到原本的 Web 畫面，更新登入狀態。\nAndroid 的 WebView 也有提供開啟新的 Web Window 的介面 WebChromeClient.onCreateWindow() ，如果開發者想要達到跟 browser 一樣的功能的話，可以自己實作這個函式。\nhttps://developer.android.com/reference/android/webkit/WebChromeClient#onCreateWindow(android.webkit.WebView,%20boolean,%20boolean,%20android.os.Message)\n在 Flutter 中要怎麼做呢？這邊以 flutter_inappwebview 做例子。flutter_inappwebview 在很早之前有打通了 native 層 onCreatWindow 的介面；但在測試之後，發現在最近的 v4.0.0 才真的能做到讓 Google / Facebook 登入的功能。\n以 Android 來說，在建立 flutter_inappwebview 的 InAppWebView 時，要先在 android 專屬的 AndroidInAppWebViewOptions 中設 supportMultipleWindows 為 true (第94行)；然後實作 onCreateWindow 。(第103行)\n_onCreateWindow 的實作則是再建立一個新的 InAppWebView，用來處理 target = _blank 或是 window.open 裡帶來的 url。\niOS 也需要更改 user agent String\n這裡，我們透過 AlertDialog 包裝 InAppWebView；新建立的 InAppWebView 要更改其 userAgent，不能包含 wv，針對 iOS 也要改一下，不然會遇到跟 Android 一樣 403 的問題。關於怎麼設定，可以參考下面的 stackoverflow 連結。\nGoogle Authentication - User agent gives error on WebView (Nylas api)\n關閉 New Window 視窗\n當新 WebView Window 完成登入後，會呼叫 onCloseWindow callback。這時是關閉 AlertDialog 的好時機，我們可以利用 Navigator.pop(context); 來關閉。\n不過不知道為什麼，Facebook 登入的流程會被呼叫兩次 onCloseWindow，為了避免這件事，這裡用了一個變數 _isWindowDisplayed 來確保 AlertDialog 只會被關閉一次。\nDemo 結語 如果能有個包裝得更高層的 WebView，把這些常見的問題都事先處理好，或是提供內建的處理方式，只需要設設 flag (像是 enableSocialNetworkLogin)就可以支援相關功能的話，那該有多好啊。\n範例程式碼連結 plateaukao/flutter_webview_login_demo\npichillilorenzo/flutter_inappwebview\n看更多 與 Flutter WebView 奮鬥的故事\n輕鬆完成 Flutter 上的 i18n\nImplementation of Instagram-like Long Press popup Dialog in Flutter\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%9C%A8-flutter-webview-%E4%B8%AD%E7%99%BB%E5%85%A5-google-%E6%88%96-facebook-%E5%B8%B3%E8%99%9F/","summary":"\u003cp\u003e開發 Flutter App 時，偶爾需要使用 WebView 提供一些既有的功能，或是短暫地開啟一個 App 內的 Web 畫面讓使用者在不離開 App 的情況下，能夠使用其他網站的內容。這時常常會遇到別人的 Web 服務需要先登入 Google 或 Facebook 才能繼續使用。如果沒有為此修改一下 WebView 實作的話，通常是會登入失敗的。\u003c/p\u003e","title":"如何在 Flutter WebView 中登入 Google 或 Facebook 帳號"},{"content":" Flags in Annecy\n對於 Flutter app 中，字串多國語系化這件事，官網中提供了步驟繁複的文件，深怕大家不知道它有多難搞。所以網路上出現了各種奇奇怪怪 plugin，有的是利用 json 來處理；有的是要你寫 dart class 來放翻譯的內容。這幾天找到了一個還算單純的方式，而且是不用額外裝非官方的 plugin 就可以完成的。想說來跟大家分享一下。\n難用的官網教學 首先，為了做為參考，可以先看一下官網上的文件有多複雜：\nInternationalizing Flutter apps\nFlutter 後來做的改良 Flutter 應該也聽到了使用者的心聲，知道這整個流程有多麻煩，所以在 2020 年有了改良的版本，文件可以參考這個連結：\nFlutter Internationalization User Guide\n步驟如下：\n在 pubspec.yml 中加入必要的 plugin View gist\n在 Flutter app 的根目錄建立一個文字檔 l10n.yaml，加入下面的資訊。 View gist\narb-dir 是設定你將會把字串相關的檔案放置的目錄，裡頭包含 .arb 格式的檔案。 template-arb-file 設定預設字串檔的檔名，其中需包含這些字串的 meta data。這檔案必須放在 arb-dir 下面。 output-localization-file 設定當 Flutter 試著幫你產生所有需要的資訊並塞進一個 Dart 檔案時，該 Dart 檔案的名稱(app_localizations.dart)。同樣的，這個檔案也是會生成在 arb-dir 下面。 再來就是建立 arb-dir 下面的 app_en.arb 檔案。裡面的寫法很直覺，在第二行寫上這個檔案所代表的 locale，47行則是一個字串的定義。目前測試的結果，在 app_en.arb這個 template arb file中，57行的 description是不可以少的。(但你可以塞空字串進去，如果你不想為 description 傷神的話)。 View gist\n有了主要的 app_en.arb 之後，你可以開始建立其他語系的 app_xx.arb，然後仿造 app_en.arb 的寫法，加入翻譯後的字串，比方說如下： View gist\n再來就是在主要的 Flutter 程式中，加入剛剛說的，Flutter 會幫你產生的 app_localizations.dart。在第2行把它 import 進來，以及13，14行。 View gist\n再來就可以開始使用我們之前建立好的字串了，請看第7行。 View gist\n如果嫌 AppLocaziations.of(context).hello_world 太長的話，可以考慮在一開始 output-localization-file 取個短一點的 class name，或是建立一個簡易的 class 如下：\nclass S { static AppLocalizations of(BuildContext context) =\u0026gt; AppLocalizations.of(context); } 然後在Flutter Widget 被生成後，建立 local variable\nAppLocalizations _str; @override void didChangeDependencies() { super.didChangeDependencies(); ... _str = S.of(context); } 使用字串時，就可以變得很簡潔如 _str.hello_world。\n如果要在 iOS app 中支援多國語系，必須在 iOS 設定檔中設定，可以參考\nInternationalizing Flutter apps\n或是下面這篇文章中的 iOS 的額外設定 這一段。\n讓 Flutter App 支援多國語系的開發流程\n是不是比官網的教學容易多了呢？而且哪天要加入新的語系，只要再加入新的 app_xx.arb 就可以了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%BC%95%E9%AC%86%E5%AE%8C%E6%88%90-flutter-%E4%B8%8A%E7%9A%84-i18n/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/19655dbe7546/1_JYUJ7d6mHBbcqZPYaDvIsw.jpeg\"\u003e\n\u003cem\u003eFlags in Annecy\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e對於 Flutter app 中，字串多國語系化這件事，官網中提供了步驟繁複的文件，深怕大家不知道它有多難搞。所以網路上出現了各種奇奇怪怪 plugin，有的是利用 json 來處理；有的是要你寫 dart class 來放翻譯的內容。這幾天找到了一個還算單純的方式，而且是不用額外裝非官方的 plugin 就可以完成的。想說來跟大家分享一下。\u003c/p\u003e","title":"輕鬆完成 Flutter 上的 i18n"},{"content":"你的 bug 不是你的 bug 寫程式難免要 debug。 de 自己寫出來的 bug，也 de 別人寫出來的 bug。除蟲的探索過程中，總有無盡的苦悶與黑闇，和伴隨著解完 issue 而來的通體舒暢。\n今天要來記錄一下最近經歷的神奇追蟲過程。\n蟲來了 有一天突然被找去一起查一條 crash issue，因為好像我負責的元件有可能是造成問題的原凶之一。稍微看了一下系統上記錄的 crash log，發現全是 native 的 call stack，最後掛在一個 native 的 image library。沒有相關的 map file 的話，native call stack 可以提供的線索其實很少。\n這條 issue 開始大量發生的日期，並不是在 app release 後的那幾天，而是在兩個 release 之間，青黃不接的時期，所以很難去斷定是不是某個特定版本改出來的問題。但 crash 確實是在某一個版本(這裡稱它為 A 版好了)之後才開始有的，這個 crash 只發生在幾家特定品牌作業系比較早期版本的手機上；而且有使用者反應：如果透過刪除會 crash 的 app，再裝回 A 版之前的版本就沒事了。\n追蟲去 追蟲第一步驟：如果 log 看得出來在哪出問題，就直接看 code 去找解法；如果 log 看不出來的話，就要試著去重現問題，然後，再回頭去找有用的 log 來找原因。\n這次遇到的 issue 屬於後者，完全沒有頭緒的 crash，只知道在某些手機上會發生。很幸運地，我從 QA 那邊借來了一台型號一樣的手機，可以讓我試著看能不能做出跟使用者遇到一樣的 crash 。在經過一番努力之後，終於有時可以遇到 crash 的情況，但還是摸不著它 crash 的條件。把相關的 logs 收集了之後，交給比較會看 native crash logs 和 app 中其他元件的同事研究。\n藉由 app logs 和 native crash logs 的時間交叉比對，研判可能是 app 中某個功能在下載圖片時，會造成這個 crash。因為圖片的來源是網路，現有的 app 中用來記錄圖片資訊的 log 並不夠多，所以無法從 log 中得到更詳細的資訊。\n加強版抓蟲器 強人同事，一個晚上的時間，把下載圖片的 http 實作加上了 logging interceptor，build 了一版可以提供更多關於圖片來源的 app。方法類似下面的連結：\nsquare/okhttp\n有了更詳細的 logs 之後，我又再試著去重現 crash，看能不能收集到更多有用的資訊。同時，也試著去把 crash 時，手機上已經有下載好的圖片，檢查看看是不是有破圖。破圖倒是沒有發現，但新增的 log 資訊中，倒是讓我看出了點端倪。\n大部分的圖檔路徑和名稱都很正常，但卻有一張比較像是畫面在做效果時才會取的檔案名稱。抱著不妨一試的心情，我把這張圖的路徑在 app 中試著直接開啟; 果不其然！它 crash 了！\n找到了會讓 app crash 的圖檔之後，再來的調查就順手多了。crash 的源頭在 native image library，而這個 library 是內建在手機裡，不是 app 自帶的。那麼，如果用手機裡的其他 app 來開啟這張圖的話，是不是也一樣會 crash 呢？Bingo！大家都 crash 了！\n看來，解決這條 issue 的方式是：請使用者再買台好一點的手機。\n當然不是這樣子的啦~\n最終的解法是：換一張不會造成 crash 的圖 — 結案！連 app 都不用 release 新版，就解了這條 crash issue。\n雖然 issue 解了，但為了避免往後有類似的問題發生，或是一旦又發生了，要怎麼讓 issue fixing 的時間可以再縮短(從幾天到幾個小時？)，內部還是有了很多後續的檢討和討論。\n以後，如果收到我傳給你的圖檔，記得要打開喔。 :)\n","permalink":"https://plateaukao.github.io/blog/posts/%E4%BD%A0%E7%9A%84-issue-%E4%B8%8D%E6%98%AF%E4%BD%A0%E7%9A%84-issue/","summary":"\u003ch3 id=\"你的-bug-不是你的bug\"\u003e你的 bug 不是你的 bug\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/5732068cae02/1_cDLCXlAq1PESlHt_2qLoTg.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e寫程式難免要 debug。 de 自己寫出來的 bug，也 de 別人寫出來的 bug。除蟲的探索過程中，總有無盡的苦悶與黑闇，和伴隨著解完 issue 而來的通體舒暢。\u003c/p\u003e","title":"你的 issue 不是你的 issue"},{"content":"與 Flutter WebView 奮鬥的那些日子 新北市的天空\n如果 App 完全用 Flutter 開發，是件很愉快的事情，什麼都是在掌控之中。但如果扯上 WebView 時，就完全不是那麼一回事了。\n選擇 在 Flutter 中要套用 WebView 的話，通常有兩個比較主要的選擇：一個是 Flutter team 官方推出的 webview_flutter 。這個套件跟其他的比起來，至少維護性是可以保證的，因為官方會不斷地修正問題和配合新的 Flutter 版本改善其實作。但它的缺點是，彈性不高，很多想要針對 WebView 做的操作，都會因為它沒有開洞讓你去 hook function 而無法達成。要嘛，就是要自己 fork 一份 webview_flutter plugin，然後自己開洞來完成；不然就是要等得天荒地老，看哪一天 Flutter team 佛心大發，幫你將想要的功能實作在新版裡頭。\n第二個主要的選擇是 flutter_inappwebview 。相較於 webview_flutter 的小而美，flutter_inappwebview 則是個包山包海的大補丸。它除了提供一般人比較需要的 inappwebview UI component，讓你可以將WebView Widget 塞到現有的Flutter Widget Tree 裡，跟其他的 Flutter Widget 可以在畫面上互相配合；也提供全畫面，類似 Android 中 Activity的 WebView 模組，可以在不需要跟其他 Flutter Widget 互動的情況下，直接跳個 WebView 畫面出來。\nflutter_inappwebview | Flutter Package\n看一下上面 flutter_inappwebview 的 Readme，內容多到眼花暸亂。它的長度大概是 webview_flutter 的50倍以上吧；就提供的功能來說，說不定也是。如果覺得 Readme 太雜，不好閱讀的話，最近flutter_inappwebview 的作者 Lorenzo Pichilli，很佛心地寫了兩篇 medium 來介紹他開發的 plugin 要怎麼用，有提供那些好用的功能，以及~~如何用他的 plugin 開發出一個 Browser App！\nInAppWebView: The Real Power of WebViews in Flutter\nCreating a Full-Featured Browser using WebViews in Flutter\n看到這麼厲害的文章，可能會想說，連 Browser 都可以自己寫一個出來了，看來在 Flutter 上使用 WebView，應該是一塊蛋糕吧！只要把 WebView Widget build 出來，塞塞參數，然後叫它 loadUrl() 應該就沒有工程師的事了吧。\n但其實魔鬼都是藏在細節裡的。當你大意地踏入整合 WebView 之後，才會發現這其實是一個很大的坑。\nWebView 的生成效能不好 (其實在 native 時本來就沒有好到哪去)，所以如果有 page transition animation 的話，可能會看到卡卡的。 根據 device 的不同，有的 device 在 WebView Widget 生成時，畫面會先黑一下。 Url navigation, 在 Android 和 iOS 的行為有時相同，有時不同。 有些功能在 Android 上可以運作，有些則是在 iOS 上才有作用。 !!! 在 Android 平台上，有些中文輸入法是殘廢的 !!! 這點很重要，所以要畫粗體字。作者是外國人，跟大部分的開發者都是針對歐美國家開發 App 的，所以不見得會遇到輸入法的問題；但對於開發給 CJK 之類語言的 App 來說，就會有很大的影響。Chinese, Japanese, Korean，都是需要組字來完成輸入的。有些輸入法(特別是大量被內建在很多手機中的 gboard)的組字方式，在目前的 Flutter WebView plugin 中都是沒有作用的，不論是 webview_flutter 或是 flutter_inappwebview 都一樣。\n解決問題 關於 Android / iOS 平台的行為不一，只能遇到一個處理一個，但至少還算是可控的。\n效能不好，也可以透過一些偷吃步的方式，讓畫面看起來比較順暢一點。\n畫面會先黑掉再顯示 Web 內容的問題，也可以藉由事先顯示一個畫面中央的載入中 icon來拖點時間，等到 WebView loading progress 來到 20% 或 30%時，再把載入中 icon 移除；這樣子就能很巧妙地隱藏住黑畫面。\n中文輸入法問題 最難解的中文輸入問題，就不是從 app 層可以解決的了，因為這牽扯到 Flutter 底層實作 PlatformView 的方式。 PlatformView 是用來將平台上原生的 View 元件，包成像 Flutter Widget的樣子，讓它可以嵌入在 Flutter Widget Tree 中。關於 PlatformView 的實作詳細介紹，和為什麼它會造成輸入法有問題，可以參考下面這篇介紹：\nFlutter完整開發實戰詳解(二十、AndroidPlatformView和鍵盤問題)\n下面這一篇則是從 flutter 的程式碼來分析\n5分钟彻底搞懂Flutter中PlatFormView与Texture\n既然這是 Flutter framework 的問題，應該很多人也都遇到，而且有發 issue 吧；自己解決不了，總可以時時關注官方的資訊，看何時有被列入開發，何時可以正式 release。\n所以，先上了 flutter github 試著找找相關的 issue。果不其然，很快地就找到了下面這一條 issue (和一堆類似的)。\n[webview_flutter] Keyboard language can\u0026rsquo;t change on Android · Issue #41089 · flutter/flutter\n雖然這條 issue 的對象是 webview_flutter，但因為每個 webview plugin 的作法都是透過 PlatformView，所以問題點其實都是一樣的。看到這條 issue 被提出的時間是在去年(2019)的9月多；到現在還是 open 的狀態，心就涼了一半。\n以後要記得，任何 promotion 式的數據和動人故事，都只會訴說美好的一面，但絕對都不會提到這個平台或架構是不是有數不盡的 issue，萬年臭蟲。一旦其中一條踩到了你的 App 的核心功能，那真是欲哭無淚。\n也不知道是運氣好，還是 Flutter 要出頭天了，在我查到這條 issue 的幾天後，Google Flutter dev Blasten 在這條 issue 上提及了另一條他新開出來的 issue，並留言說，有一大掛的 issue 將會由某個 PR 給一口氣解決掉。\n[webview_flutter] Add new entrypoint that uses hybrid composition on Android · Issue #61133 ·…\n瞧下面這截圖列的 issue 之多，讓人又興起了一絲期待。\n對於這 PR，自然是充滿了好奇，到底是什麼黑魔法，可以一次解決那麼多 PlatformView 帶來的各種問題。進到該 pull request 看了一下：\npull request cotent\n可以看到其實在這 PR 中， SurfaceAndroidWebView 只是把 AndroidWebView 的 build 覆寫掉。\nAndroidWebView 原本是建立一個 AndroidView 。從下面的程式碼可以看出來， build function 中 41 行的 AndroidView ，在 SurfaceAndroidWebView 的 build 中，被覆寫成使用 PlatformViewLink元件。\n原來這個 PR 只是換個元件而已，真正的改動，應該是在其他 flutter framework的 PR 中吧。不過既然有了這支 PR 可以支援 webview_flutter 的中文輸入法；如果我如法炮製，也在 flutter_inappwebview 中也把 AndroidView 換成 PlatformView 的話，是不是也一樣行得通呢？\n發 PR 不落人後\n於是，我 fork 了一份 flutter_inappwebview，參考上面的 PR，把它也改了一下；並且把 flutter 版本升級到當時的 dev channel 1.20.xxx (因為當時的 flutter beta/stable channel 都還不支援 PlatformViewLink)，很開心地發現，中文輸入法就出現了！而且可以用！\n為了希望這個 fix 能趕快也被加入 flutter_inappwebview，不讓官方的 webview_flutter 專美於前，所以我也趕緊發了個 PR 到 flutter_inappwebview去。\nUse PlatformViewLink widget for Android WebView by plateaukao · Pull Request #462 ·…\n作者說，他也在等 hybrid composition 進到 flutter stable channel。在那之後他就要進 code 啦。不知道 hybrid composition 是什麼的話，可以參考下面這個 wiki 說明。\nflutter/flutter\n在我 PR 發完的九天後，也就是昨天(8/5)， Flutter SDK 也正式 release 了 1.20.0！一切看起來都是那麼地順利。\nFlutter SDK releases\n再來，就等 flutter_inappwebview 進版，然後就可以一飛沖天囉！希望以後不需要再跟 WebView 搏鬥了。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%88%87-flutter-webview-%E5%A5%AE%E9%AC%A5%E7%9A%84%E6%95%85%E4%BA%8B/","summary":"\u003ch3 id=\"與-flutter-webview奮鬥的那些日子\"\u003e與 Flutter WebView 奮鬥的那些日子\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/a353f0094734/1_wkDc2HijgBPP8qrAWPTV6w.jpeg\"\u003e\n\u003cem\u003e新北市的天空\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e如果 App 完全用 Flutter 開發，是件很愉快的事情，什麼都是在掌控之中。但如果扯上 WebView 時，就完全不是那麼一回事了。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"選擇\"\u003e選擇\u003c/h3\u003e\n\u003cp\u003e在 Flutter 中要套用 WebView 的話，通常有兩個比較主要的選擇：一個是 Flutter team 官方推出的 \u003ca href=\"https://pub.dev/packages/webview_flutter\"\u003ewebview_flutter\u003c/a\u003e 。這個套件跟其他的比起來，至少維護性是可以保證的，因為官方會不斷地修正問題和配合新的 Flutter 版本改善其實作。但它的缺點是，彈性不高，很多想要針對 WebView 做的操作，都會因為它沒有開洞讓你去 hook function 而無法達成。要嘛，就是要自己 fork 一份 webview_flutter plugin，然後自己開洞來完成；不然就是要等得天荒地老，看哪一天 Flutter team 佛心大發，幫你將想要的功能實作在新版裡頭。\u003c/p\u003e","title":"與 Flutter WebView 奮鬥的故事"},{"content":"Scenario Sometimes you want to give users a glimpse of more info about the item presented on current screen, but you don’t want to bring users to another screen. It’s too heavy and may distract user’s focus.\nInstead of navigating to another detail screen, various UI components can be used to achieve this: to show a popup dialog or to slide in a bottom sheet panel. Either way is good; however, to dismiss the dialog or the bottom sheet panel, it involves user’s extra interaction. Users have to click on an OK button, or to slide the panel off explicitly.\nA better way would be like Instagram’s long press behavior on its photo grid screen. In this case, Instagram uses this implementation to show a bigger photo, allowing users to see the bigger photo directly without leaving the current screen. Once user’s finger leaves the screen, the bigger photo will be disappeared.\nLong Press on Photo Grid UI in Instagram App\nThe behavior flow would be like the diagram below:\nFlow of long press popup dialog\nProblem In Flutter, how could we achieve the same behavior? If you’re a bit familiar with Flutter coding, it’s not hard to know that GestureDetector can be used for the long press behavior handling with its onLongPress callback.\nHowever, when trying to handle the onLongPressEnd callback in the same GestureDetector, you’ll find that this callback is never called. This is because when dialog is displayed, the GestureDetector is interrupted; you’ll no longer be able to get any gesture events.\nIn order to be able to handle both onLongPress and onLongPressEnd , we need to wrap the popup UI in another Flutter Widget — Overlay.\nOverlay \u0026amp; OverlayEntry Overlay is a stack of entries that can be managed independent of the other Widgets that are already arranged on UI. Overlays let its child widgets “float” visual elements on top of other widgets by inserting them into the overlay’s Stack.\nOverlay class\nIn order to use Overlay, we must wrap the widget we want to use into an OverlayEntry. Overlay entries are inserted into an Overlay using the OverlayState.insert or OverlayState.insertAll functions. To find the closest enclosing overlay for a given BuildContext, use the Overlay.of function, e.g.,\nOverlay.of(context).insert(some_created_overlay_entry); OverlayEntry class\nImplementation First, we write a flutter sample app with an image grid view in it. Function _createGridTileWidget(String url) is used to turn an image url into a Widget that will be shown as a grid tile. At line 82–89, you can see that GestureDetector is created. In onLongPress() , a popup dialog is created, and insert into Overlay , so that you can see the dialog is displayed.\nIn order to hide the dialog when user’s finger leaves the screen, at line 89, the dialog is removed from Overlay so that it can be hidden to user.\nTo behave like Instagram, some animations are added into the popup dialog. an AnimatedDialog is written to handle the animation part; and it’s wrapped inside OverlayEntry . The real content is contained inside _createPopupContent(String url) .\nAbout the dialog content, it’s nothing special. You can check the code below to know how it’s constructed.\nDemo Now, here\u0026rsquo;s the result gif of the demo application:\nDemo of the sample app\nReference If you want to see the whole demo application codes, you can refer to the Github project below:\nplateaukao/flutter_long_press_dialog\n","permalink":"https://plateaukao.github.io/blog/posts/implementation-of-instagram-like-long-press-popup-dialog-in-flutter/","summary":"\u003ch3 id=\"scenario\"\u003eScenario\u003c/h3\u003e\n\u003cp\u003eSometimes you want to give users a glimpse of more info about the item presented on current screen, but you don’t want to bring users to another screen. It’s too heavy and may distract user’s focus.\u003c/p\u003e\n\u003cp\u003eInstead of navigating to another detail screen, various UI components can be used to achieve this: to show a popup dialog or to slide in a bottom sheet panel. Either way is good; however, to dismiss the dialog or the bottom sheet panel, it involves user’s extra interaction. Users have to click on an OK button, or to slide the panel off explicitly.\u003c/p\u003e","title":"Implementation of Instagram-like Long Press popup Dialog in Flutter"},{"content":"韓國語線上免費學習資料匯整 首爾車站\n這是一個收藏自己常用的韓語學習資料的地方\n影片 Korean Culture Series \u0026amp; Quick Korean(初) Korean Culture Series \u0026amp; Quick Korean\n這個 Youtube 頻道上，有循序漸進的韓文學習影片，總共分為一到四級。第一級的教學影片有用不同語言(韓、日、中、英、泰、西班牙文)進行的，方便對韓文完全沒有概念的人，用自己的母語或是自己原本就熟悉的語言學習。\n從第二級開始，就全是用韓語教學，讓學習者可以開始沉浸在韓語的環境中。當然一開始無法全部聽得懂，所以它也很貼心地附上了不同語言的字幕。\n每一課的內容都不會太多，一開始會先有段簡短的韓文對話，裡頭會有該集要介紹的文法和句型；講師會先針對會話中的單字和文法做解釋，然後再回到對話，請大家一起跟著唸。最後再教一些延伸的單字和用法。\n目前我還在學習第三級的內容。所以之後會有一篇 medium 記錄一下第三級的影片重點。\nYoutube 中的影片雖然都沒有提供連結下載當集的上課內容，但如果真的想要有份文件參考著看的話，可以前往這整份教學的另一個平台 The Korean Times。在 The Korean times 上的影片教學，就都有附上 pdf 下載的連結。\n[바른 한국어 1급] - Learning Koean\nトリリンガルのトミ【韓国語講座】(初/中) トリリンガルのトミ韓国語講座：無料で勉強したい初心者・独学者向け\nTomi 的教學是少見很有系統的教法。初級韓國語講座中包含了43回的教學，已全部錄製完成。2021年上半年也開始了中級的講座，預計會有60回，目前以一週兩回的進度在推出。雖然一週只有兩回，但每一回平均會講授三個文法，然後要求聽眾再背40個單字。所以認真跟著學習的話，也是會蠻充實的。\n初級講座\n中級講座\n除此之外，還有很多概念性的介紹，比方說，如何透過學習時數來判斷自己的程度；要通過 TOPIK 考試的話，有多少數量的單字需要記憶，跟不同程度的 TOPIK 考試，各需要了解哪些文法。她也在她的網站上，把這些內容公開，供學習者下載。\n韓語除了很多詞彙跟中文很像外，在文法結構上則是跟日文十分相似。許多細微口氣上的表達方法，日文都能找到幾乎是一對一的說法；韓語也有區分類似敬體、常體的用法，用日文解說和翻譯時，比較能一看就意會其差別。所以對原本就有基本日文能力的人來說，透過日文來學習韓文，會更容易掌握韓文說法的語態和口氣。\nおしゃべり韓国語 (初) おしゃべり韓国語\n這是一個由住在釜山的韓國語教師開設的 Youtube 頻道，教學是用日文進行的。早期他的影片有一系列初、中、上級的「(旧)おしゃべり韓国語」，教學的方式很像是在課堂上一樣，他會邊講邊寫白板；不像其他的頻道，現在都是用後製的方式，雖然比較美觀，但其實可能印象就不會那麼深刻。\n每一集裡面會介紹一兩個語尾用法，他會舉很多例子並解釋這用法適用的場景；還會將動詞或形容詞該怎麼變化寫出來。\n相較於書本教條式只用一兩句解釋意義，這樣子的講課方式會讓人更容易體會到每種語尾的實際使用情況。\n最近他還是有繼續在上傳新的教學影片，不過好像就比較不再是用白板的方式教學了。但還是很值得一看。\n另外一提，他也有個人網站，上面有提供付費的線上課程資訊\nおしゃべり韓国語 - みなさんと一緒に韓国語で話す空間\nLearnTeachKorean (初) Learn Teach Korean\n這個 Youtube 頻道(世宗學堂)裡，也是有滿滿的影片供使用者學習。之前有在看的是「Sejong Korean Conversation 1,2,3」，這一系列有點像是教科書，在講課的人都長得像主播似的，讓人有動力持續學習下去。教學是用韓文進行的，但一樣可以打開字幕。每一集中間都會穿插一點小短片，講述一個從荷蘭來的小帥哥，想要找不知道姓名的某個韓國女生的故事。\n同樣的，它也有網站提供線上課程\n세종학당 온라인 학습 플랫폼\n베이직 코리안 Basic Korean (初) 베이직 코리안 Basic Korean\n這個 Youtube Channel 全是用韓文進行的，所以最好先有點基礎再來聽，不過，真的聽不懂的話，其實它是有附英文字幕的，可以打開來看。畫面會是用類似數位黑板的型式呈現，讓聽者可以更專心於學習上。語速很慢，可以一邊學習文法，一邊練習基本的韓文聽力。\nKIIP Study (初/中) KIIP 為 Korean Immigration \u0026amp; Integration Program 的縮寫。這是韓國政府為了讓打算要移民的外國人，或是想要在韓國工作的人所推出的教學。除了韓語外，它還包含了文化介紹和暸解韓國社會的相關課程。\n教學總共有5級，0到4級是韓語本文和一些文化的介紹，學完後大約可到韓檢中級的程度；5級則是專門在提高對韓國社會的了解，不再是文法的解釋，而是滿滿的主題文章。\n如果人住在韓國的話，可以到官網去報名上實體的課程： ，如果只是要透過這一系列的教材來學習韓文和了解韓國文化的話，可以到下面的 https://kiipgrammar.com/resources.html 下載每一級的 課本 pdf 和相關 audio 檔案。\n然後在 Youtube 上有頻道是專門透過這系列教材在上課的，讓你可以跟去課堂上上課一樣，跟著一步步的學習。建議可以下載 pdf，然後跟著 Youtube 上課。但前提是要先把自己程度提升到 TOPIK 2 級左右，才可以聽得懂影片中的韓文。這些影片都是用韓文在講解，而且沒有英文字幕；但韓文字幕也是 Youtube 系統自動生成的，很不可靠。\n另外，這個 Youtube 頻道在錄製時，收音效果很不好，這大概是這系列影片的唯一缺點吧。單純看 pdf 會有點枯燥；但是在 Youtube 影片的前行解釋之後，再去看 pdf 中每課的文章，就會順暢不少，因為他會先把文章中最重要的單字都先用韓文解釋過一次，再帶大家看文章內容。這大概就是去上課和自學的那麼差異感吧。\n사회통합프로그램 study\nTAONの韓國語講座 GOGAKU TAON\n這是個在日本生活的韓國人，錄製了一系列看韓劇學韓文的影片。不想死板地背文法單字的人，或是在有一定文法基礎後，可以看看這個頻道，多了解學習到的用法怎麼應用到實際生活中。影片都是用日文講解的，所以最好有一定的日文聽力程度再觀看。\nちょっと楽しくなる韓国語 (中) Joo [korean Learning channel]\n這是個很有特色的 Youtube 頻道。教學是用日語進行，內容很多元。特色是畫面上會有一隻畫出來的貓跟 Youtuber 互動，常會吐糟 Youtuber。所以雖然這是個一人 Youtuber，卻有種兩個人在對話的感覺。除了基本的文法或單字比較的教學之外，很特別的一點是常會有影片在介紹不同明星的發音差別。也有錄製睡前專用的影片。\n對於一般學習影片膩了的人，可以偶爾來看看這個頻道，會很舒壓。\nWABOM TOPIK 這是最近 (2022/02, 03) 在聽的一個頻道。雖然這是個用韓文專門在講解 TOPIK II 內容，但因為說明的方式很清晰，所以就算文章中有什麼原先不會的字彙，她也會用很淺顯易懂的描述解釋，讓你不用特別再去找字典就能理解。\n而且，除了講解 TOPIK 考試內容外，還會進一步地擴大說明主題相關的內容，讓影片的深度不光侷限在字句的推敲上而已。\n很適合考過 2 級，有 3 級程度，想要再往上學習的人。\nWABOM TOPIK\nPodcast Talk to Me in Korean The List of TTMIK Podcasts - Talk To Me In Korean\nApple Podcasts https://apple.co/2TM9LoE\nSpotify https://spoti.fi/2IFr9oI\nTalk to Me in Korean 是個很老牌的韓文教學單位，通常大家會簡稱之為 TTMIK。最著名的 podcast 是 KOREAN GRAMMAR with TTMIK，這系列總共分為 10 個 level，每一個 level 有 30 課左右的內容；每一課長度都在20分鐘上下，由一男一女透過英文講解該集的文法，並介紹許多例句。愈到後面，課程內容變得更加多元，像是 Word builder，Commonly used phrases，Long sentence building，Grammars related to the same scenario 等單元；讓學習能更加多元，而不只是專注在學習文法上。\n通常聽到第 4 或第 5 level 時，對於韓文文法應該就有了基本的認識，建議這時候要開始輔以其他的教材來加強聽力，寫作，和閱讀等能力。不然可能會跟我一樣，文法學了一堆，但單字量太少，在閱讀上依然無法了解較複雜的內容；或是聽力反而是英文變好了，韓文聽力始終在原地打轉。\nApple Podcasts https://apple.co/39MS6mn\nSpotify https://spoti.fi/39Pv2Ua\n在學習來到 Level 6, 7 時，就可以考慮開始聽 IYAGI 這個系列。IYAGI 在韓文的意思是「會話」。這系列總共有 145 回，每一回裡會由有兩個人針對某個主題用韓文展開討論。前面一開始的回數，說話速度有慢一些，但愈到後面，其實速度有愈來愈快的感覺。如果無法跟上速度理解內容的話，在早期，是可以到 TTMIK 的網站下載每一回的對話內容的 pdf 檔案；不過，在網站改版後，就要成為付費會員才能下載 pdf 了。如果真的很需要 pdf 來看對話內容的話，在網路上應該還是有辦法找到早期其他人整理好的 pdf 內容，不妨找找看。\nIYAGI 這系列除了可以訓練聽力外，還可以藉由這些討論的內容，從各個層面更加了解韓國的文化，算是一舉兩得。對話內容的 pdf，也可以拿來當作練習閱讀的材料，或是學習一些自己還沒記過的單字。\n目前，直接聽 IYAGI podcast，如果主題真的自己比較熟悉的話，我大概可以聽得懂三四成，但大部分情況下，還是要對照著對話內容 pdf 聽，才能了解全部的內容。希望能快點晉級到不看 pdf 就能全部都聽得懂。\nApple Podcasts https://apple.co/33fOhnh\nSoundcloud http://bit.ly/39OhB6J\nSpotify https://spoti.fi/2TNS3kF\nIYAGI 系列是很多年前推出的系列。最近針對聽力新推出的系列是 수다 타임 (Talk to Me in 100% Korean)。內容跟之前的 IYAGI 類似，不過，如果有加入網站會員的話，除了可以看到每一回的 pdf 檔案外，還可以直接觀賞兩個人對話的影片。只從 podcast 聽的話，可能有時不太能夠理解說話者的表情和意思；透過影片學習的話，比較能看到說話者的神態，而且可以直接開啟韓文字幕。在學習上會更方便。\nTalk To Me In Korean - Books \u0026amp; Online Courses for Korean Learning\nTTMIK 的網站不斷有在新增內容，和改善功能。在今年年中的改版時，加入了訂閱的會員制，使用者可以以月或以年為單位成為 Premium 會員。成為會員後，就可以瀏覽所有網站上的教學內容，下載對應的 pdf 文件。前一陣子還有推出訂閱一年送一年的優惠！(可惜這個活動是發生在我剛訂閱完後的一週後…)所以大家如果有興趣的話，可以偶爾上去逛逛，說不定哪天又會有其他的優惠。\n待續…\n線上文字資料 https://kiipgrammar.com/\n待續…\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9F%93%E5%9C%8B%E8%AA%9E%E5%AD%B8%E7%BF%92%E8%B3%87%E6%96%99%E5%8C%AF%E6%95%B4/","summary":"\u003ch3 id=\"韓國語線上免費學習資料匯整\"\u003e韓國語線上免費學習資料匯整\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/958d61f8e9b5/1_lQ_29FE33bt-ig-Yg0UcBQ.jpeg\"\u003e\n\u003cem\u003e首爾車站\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e這是一個收藏自己常用的韓語學習資料的地方\u003c/p\u003e\n\u003ch3 id=\"影片\"\u003e影片\u003c/h3\u003e\n\u003ch4 id=\"korean-culture-series--quick-korean初\"\u003e\u003ca href=\"https://www.youtube.com/channel/UCNjCmUDirLIsGkGkkHt3wWQ\"\u003e\u003cstrong\u003eKorean Culture Series \u0026amp; Quick Korean\u003c/strong\u003e\u003c/a\u003e\u003cstrong\u003e(初)\u003c/strong\u003e\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://www.youtube.com/c/QuickKorean/playlists\"\u003eKorean Culture Series \u0026amp; Quick Korean\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e這個 Youtube 頻道上，有循序漸進的韓文學習影片，總共分為一到四級。第一級的教學影片有用不同語言(韓、日、中、英、泰、西班牙文)進行的，方便對韓文完全沒有概念的人，用自己的母語或是自己原本就熟悉的語言學習。\u003c/p\u003e","title":"韓國語學習資料匯整"},{"content":"\n雖然標題是英文，內文還是用中文撰寫，造福一下看中文資訊的讀者。\n在 flutter 中，如果遇到花時間的 task，小一點的 task 可以直接利用 async function 把這件事延後到 event loop後面；大一點的 task，則是建議利用 isolate，將它交給獨立的 isolate 處理完後，再送回主要的 UI thread。\ndart 本身也知道 isolate 的寫法很煩瑣，所以特地包了一個 compute 的 function 讓開發者可以很快地將想處理的邏輯包在一個 top level function 中，再將它丟給 compute 就行。關於 isolate 和 compute的使用方式，可以參考我之前的一篇文章：\n幻滅，是成長的開始 — Flutter 的 async 與 isolate\ncompute 在使用上相當無腦，卻也不是萬靈丹。它底層還是會去建立新的 isolate，處理完 task 後再把該 isolate 砍掉。每次建立 isolate 的成本是很高的，可能需要花費 50 到 150 ms 的時間。如果同一個畫面中有多張圖片要處理，或是透過 isolate 方式在處理多個網路 request 的 response parsing。將會產生許多 isolate 開開關關造成不必要的資源浪費和時間上的延遲。\n這時，其實不難想到，如果能有個類似 thread pool 的機制就好了：透過固定產生幾個 isolate 在那，由這幾個 isolate 輪流去處理不斷進來的需求。\nisolate 這個 plugin 也為大家準備好了這樣子的solution，稱為 loadBalancer。先來看一下如果是 compute 的話，可以怎麼寫。在第 6 行利用 compute 執行 processImage function。\nView gist\n下面是利用 LoadBalancer 的例子。利用 Loadbalancer 的話，主要是第 2行的建立 LoadBalancer instance，在第 9 行般取得資源，然後透過 loadBalancer.run() 執行 task。\nView gist\n下面是套用了 LoadBalancer 的 Flutter App 的表現。\n相關文章 Flutter 中處理 Image 的方式\n","permalink":"https://plateaukao.github.io/blog/posts/advanced-isolate-usage-in-flutter/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/94b124deacdd/1_TYtiTKgefFO_2lSRwo9M0Q.png\"\u003e\u003c/p\u003e\n\u003cp\u003e雖然標題是英文，內文還是用中文撰寫，造福一下看中文資訊的讀者。\u003c/p\u003e\n\u003cp\u003e在 flutter 中，如果遇到花時間的 task，小一點的 task 可以直接利用 async function 把這件事延後到 event loop後面；大一點的 task，則是建議利用 isolate，將它交給獨立的 isolate 處理完後，再送回主要的 UI thread。\u003c/p\u003e","title":"Advanced isolate usage in flutter"},{"content":"打造 E-ink 專用的瀏覽器 (II): 增加按鈕、桌面模式、改善 UI 開始來改造 Browser 吧\n增加 PageUp / PageDown 按鈕 大部分的電子書閱讀器雖然有 A2 模式可以啟動，加速畫面繪製的速度，但是如果能讓瀏覽器跟看書一樣，是以頁為單位往上或往下前進的話，還是比較方便，而且會造成的殘影也會少一些。\n目前 Onyx Boox 系列內建的瀏覽器有做方向鍵在畫面左下方，但點擊時上或下時，畫面還是以捲動的方式往上或往下一頁。\n所以，第一個要新增的功能是為 FOSS Browser 加上翻頁的按鈕，成果如下圖標示。\nPageUp /PageDown 按鈕\n要更改的程式碼不多，主要是幫 NinjiaWebView 加上 pageDownWithNoAnimation ，然後在翻頁時呼叫它。\npublic void pageDownWithNoAnimation() { scrollTo(0, getScrollY() + getHeight() / 5 * 4); } modify scroll to bottom logic; modify button position; fix injectCSS \u0026hellip; ·…\n往上翻頁的按鈕也比照辦理就行了。\n有了上下翻頁的功能後，另一個很常要用到功能是跳到網頁的最上方。這個功能我把它加在長按 PageUp 按鈕。\npublic void jumpToTop() { scrollTo(0, 0); } 支援 Desktop 模式 目前的 E-ink 設備，6吋，7.8吋，一直到 10 吋，13 吋都有，除了6 吋有點太小，其他尺寸蠻適合用來瀏覽 PC 版的網頁，所以，讓 Browser 可以切換 Desktop 模式是很重要的。\n在 reddit 上看到，在很早期的 FOSS Browser 是有支援這功能的，只是在後來不知道為什麼，就把這功能拿掉了。有了這資訊後，當然是先回去翻翻 commit，找原本的實作在哪裡，看有沒有機會直接再搬到最新版的程式碼中。\n經過一陣搜尋後，發現在是 v5.6 升級到 v5.7 時，將它拿掉的(目前最新版是 v6.9了)。在稍微參考原先的作法後，我把 fast toggle 選單中的「反轉顏色」的功能，改成新版的 Desktop 模式的按鈕。對 E-ink 設備來說，應該不會有人想把底色換成全黑的吧。\n### v 5.6.1 (WIP) · scoute-dich/browser@95ca4aa\n如下圖所示，當長按三個點時，會跳出 fast toggle 的選單，紅色框選的按鈕就是新加的 Desktop 模式。點選後再按 RELOAD 就可以看到 PC 版的網頁了。\n實作其實不難，只是塞一個類似 PC Browser 的 user agent string 給 WebView 而已。如果想離開 Desktop 模式，只要再把原本的 user agent string 重新設定回去就行。\n改善瀏覽器 UI 的配色 FOSS Browser 原本的 icons 設定的顏色都是用 \u0026quot;?android:attr/textColorSecondary\u0026quot;。這是一個近似黑色的顏色，在 E-ink devices 上看起來灰灰的，視覺效果不是很好，所以全部都改成了黑色，讓這些 icon 在電子書上更顯眼。\nchange icon color to pur black or white · plateaukao/browser@621a187\n顯示目前分頁數 FOSS Browser 雖然可以讓使用者開啟多個網頁，而且可以叫出預覽畫面，在不同的網頁間切換，但在正常模式下，看不出來目前有多少分頁；也不曉得點了連結後，是在原本的分頁開啟，還是又開啟了一個新的分頁。\n如果能夠在下方把目前分頁數顯示出來的話，就能避免這樣子的問題。\n修改前的功能列\n修改後的功能列\n這修改也很容易，只要把原本的 icon image 換成隨便一個 checkbox 的圖案，然後在它上頭蓋上一個 TextView ，這個 TextView 在網頁數有增加時，都更新一下數字就可以了。\nadd a tab count icon on bottom bar · plateaukao/browser@3a409e9\n系列文章 打造一個電子書的瀏覽器: Part I\n有用的連結 如果想裝來試試的人，可以安裝 Google Play Store 上的版本，或是下面已經編譯好的版本。\nEinkBro - Apps on Google Play\nplateaukao/browser\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-part-ii/","summary":"\u003ch3 id=\"打造-e-ink-專用的瀏覽器-ii-增加按鈕桌面模式改善-ui\"\u003e打造 E-ink 專用的瀏覽器 (II): 增加按鈕、桌面模式、改善 UI\u003c/h3\u003e\n\u003cp\u003e開始來改造 Browser 吧\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/a7bfb98233f7/1_Wj_38xUoGa66K0P3aQWwPA.png\"\u003e\u003c/p\u003e\n\u003ch3 id=\"增加-pageup--pagedown按鈕\"\u003e增加 PageUp / PageDown 按鈕\u003c/h3\u003e\n\u003cp\u003e大部分的電子書閱讀器雖然有 A2 模式可以啟動，加速畫面繪製的速度，但是\u003cstrong\u003e如果能讓瀏覽器跟看書一樣，是以頁為單位往上或往下前進的話\u003c/strong\u003e，還是比較方便，而且會造成的殘影也會少一些。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器: Part II"},{"content":"打造 E-ink 專用的瀏覽器 (I): Onyx Boox 上的怪現象 這是一個追查為什麼 FOSS browser 在 Onyx Boox 電子書閱讀器上，會莫名奇妙地跳轉網頁的無聊故事\n需求 目前的開放式電子書閱讀器都已經做得很方便，除了可以安裝一般的電子書 App 像是 Kindle, Kobo, Google Play Books, Hyread 之外，還有內建瀏覽器供使用者偶爾上上網，查查資料。如果切到 A2 Mode (註1)，其實使用起來的速度已經可以讓人接受，不會整個畫面閃不停。\n但是，當愈來愈常使用電子書的 browser，還是會覺得在捲動畫面時，畫面更新速度跟不上手滑動的速度。那，我是不是可以把 browser 的瀏覽方式改得類似電子書，能夠一頁一頁往下翻，而不是不斷地捲動網頁呢？\n有了這想法之後，首先是上 Github 看看是不是已經有別人將 WebView 包好的 小而美的 App。畢竟，我要的只是改造瀏覽的體驗而已，而不是從頭開始自己寫一個新的 browser。\n經過一番搜尋和比較(可能一兩分鐘吧)，scoute-dich 的 browser 比較符合我的需求，程式碼數量也不會大到我需要花很多時間才能上手。這個 browser 的前身是從 ninjia 的 browser 來的，目前看來，一直到最近都還有在更新，應該比較不會遇到問題。\nscoute-dich/browser\n經過一兩個小時的研究，快速地把自己想要的翻頁功能做好了。(其實只是在點擊畫面某個地方時，讓內容可以直接往下前進 2/3 個畫面，不要有捲動的效果) 關於功能實作的說明，會留在 Part II。這一篇文章比較想要講功能完成後，放到電子書閱讀器上遇到的靈異現象。\n功能完成後，我將 App 安裝到海信的 A5 (Hisense A5)，和博閱的 Likebook Mars，都可以很正常的運作。但安裝到 Onyx Boox Note Pro 時，就不是那麼一回事了。\nApp 一開始執行時，會正常出現預設的網頁，但隔一兩秒後就會突然跳到下面的畫面，然後開始鬼打牆，不斷地重新更新這個畫面，想要叫它停也停不下來。\n看到 javascript blah blah的，就覺得被注入了！明明預設網頁就是個很單純的網頁而已啊。究竟是哪裡出了錯呢？\n思考的方向有下面幾點：\n預設網頁有問題，會偷偷導到某些不知名的地方 這套 open source browser有問題，從程式碼中偷塞了要注入的 javascript Onyx Boox Note Pro 的 WebView 有問題 要排除 1 的可能性很簡單，我只要把預設網頁網址換掉，重新編譯一版 App 來測試就行。事實證明，不是預設網址的問題。即使換成 www.google.com，畫面還是會回到上面的裡打牆狀態。\n難不成會是 2 嗎？雖然之前常會聽到有很多惡意的 hack，故意藏在 open source code 中，但這套 browser 程式碼可是有 400 多顆星，100 多份 fork。在那麼多人研究後都沒出事，卻偏偏被我遇上了？\n抱著無罪推定原則，如果是 2 的話，我得把它偷塞的javascript 程式碼找出來，才能把問題怪到它頭上。不過找了一輪，並沒有看到可疑的程式碼片段。有可能因為我不專業的 hacker，所以才找不到吧。\n暫時放棄 2 這條線索，一早醒來，開始查第 3 種可能性，會不會是 Boox 的 WebView 有被動過手腳？其實這樣的懷疑也是很合理的，因為在另外兩個牌子的電子書閱讀器上並沒有問題，這現象只在 Onyx Boox 的設備上才會發生。\n從問題的症狀來看，WebView 的 url 其實有被重新載入過，由一開始的 www.google.com，被改成預設的搜尋引擎加上一串 javascript 程式碼。所以，我在程式中跟載入網址有關的地方都加了 breakpoint，期待可以在某個時間點，找到凶手是誰。\n抓到凶手了\n抓到了！果然 WebView 會被塞入新的 url 。從上圖可以看到，將被載入的 url，就是這段可疑的 Javascript 程式碼。從圖左下角的 call stack 可以看出 loadUrl() 是被 injectCSS() 呼叫，而它的執行時間點是在 WebView 的 onPageFinished() 。injectCSS() 點進去看，並看不到對應的程式碼，因為它並不是正規的 WebView source code。看到它所屬的位置是在 android.webkit 的 WebViewClient，應該可以肯定是 Onyx Boox 的內建 WebView 有改過。\n既然程式停了下來，我可以仔細看一下這段 javascript 到底寫了什麼。\nurl value in breakpoint\n以我粗淺的 web 程度，大概可以猜出來它是要找到 html 的 head element，在裡頭塞一個 style 的 element，然後要再塞一段 window.atob() 來的資料。為了確保我的理解沒錯，我把上面的內容拿去餵 Google ，得到這是塞 javascript 或 CSS 到 WebView 網頁中的常用方式 。下面是其中一個網站說明的範例程式；寫法完全一模一樣。\nView gist\n到現在，大概知道了這是 Onyx Boxx 的 WebView 搞的鬼，它會在 onPageFinished() 去呼叫新增的 injectCSS() 函式。 windows.atob() 的作用是把編碼過的 Base64 string 再轉回來，所以，如果我想知道被塞了什麼，就要把裡頭的那串\nKiB7Cgljb2xvcjogIzAwMDAwMCFpbXBvcnRhbnQ7Cglmb250LXdlaWdodDo5MDA... 丟到網路上的 Base64 Decoder 去看看。\nView gist\nVoila~ 就是一堆在改顏色的 CSS codes。\n解決的方法 知道是誰在搞鬼，和怎麼搞鬼之後。再來就是看怎麼對症下藥。\nSequence Diagram\n流程中的 3. → 4. → 5. 只要任何一步斷開就可以避免內建的 WebView 去注入 CSS。要斷開 3. → 4. 的話，可以在 NinjiaWebView 的 WebViewClient override onPageFinished 時，不要呼叫 super.onPageFinished()，這樣子就不會進到 4. 中。\n如果不確定 super.onPageFinished() 做了什麼，想要保留的話，可以在 5. 的時候去判斷目前塞進來的 url 是不是 javascript 開頭的字串，如果是的話就直接 return，不做任何處理。這樣子也可以避開 injectCSS() 帶來的影響。\n目前我的解法是採取後者，如下圖所示，在 205 行加入 url 的判斷，如果為真，就 return ，不做任何事情。(畫面中先 comment 掉是為了要重現錯誤的狀況)\nskip loading javascript\n結論 忙了一晚，都是在找問題，真正的翻頁功能倒是早早就寫完了。接下來可以慢慢改善 UI，讓翻頁的功能更方便使用。 :)\n系列文章 打造 E-ink 專用的瀏覽器: Part II\n註釋 註1\n截自 Hyread 網站\nA2模式是什麼？\n前面提到電子紙的特性會有殘影(電子墨水的殘留畫面)，製造商為了解決殘影的問題，設計了全螢幕刷新，全刷的過程螢幕就會閃一下，像是恢復初始狀態的畫面。Gaze上方系統列兩個逆時針箭頭的符號即為「A2模式」，開啟A2模式能讓你快速刷新螢幕，針對內容進行局部刷新，縮短螢幕刷新的時間，同時產生輕微殘影。因此，A2模式適合用於瀏覽網頁或閱讀漫畫，可增加視覺觀感的流暢度，減少全螢幕刷新的閃爍現象。\n參考資料 使用android中的webview將javascript文件注入我的網站-code log\nWindow atob() Method\nBase64 Decode and Encode - Online\nmodify scroll to bottom logic; modify button position; fix injectCSS \u0026hellip; ·…\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%89%93%E9%80%A0-e-ink-%E5%B0%88%E7%94%A8%E7%9A%84%E7%80%8F%E8%A6%BD%E5%99%A8-part-i/","summary":"\u003ch3 id=\"打造-e-ink-專用的瀏覽器-i-onyx-boox上的怪現象\"\u003e打造 E-ink 專用的瀏覽器 (I): Onyx Boox 上的怪現象\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e這是一個追查為什麼 FOSS browser 在 Onyx Boox 電子書閱讀器上，會莫名奇妙地跳轉網頁的無聊故事\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/e6c3d2e55e82/1_EgEAOM9E5TTNFHa_KWnAIA.jpeg\"\u003e\u003c/p\u003e\n\u003ch3 id=\"需求\"\u003e需求\u003c/h3\u003e\n\u003cp\u003e目前的開放式電子書閱讀器都已經做得很方便，除了可以安裝一般的電子書 App 像是 Kindle, Kobo, Google Play Books, Hyread 之外，還有內建瀏覽器供使用者偶爾上上網，查查資料。如果切到 A2 Mode (註1)，其實使用起來的速度已經可以讓人接受，不會整個畫面閃不停。\u003c/p\u003e","title":"打造 E-ink 專用的瀏覽器: Part I"},{"content":" Taichung.Taiwan\n在學習韓文時，除了背單字的需求外，還希望可以有隨時可以查閱或複習的文法資料。找了一下 Android 平台上的相關 App，大部分都沒有太多內容，不然就是雖然有完整的內容，但操作上不是很符合我的需求，或是 UI 有點簡陋，讓人提不起學習的興趣，有的還會一直在畫面下方閃廣告。\n找資料庫 目前看到兩個資料比較完整 App 分別是下圖中的第一個和第三個 App。\n想說，如果能夠利用它們 App 中的資料庫，自己透過 Flutter 來撰寫 UI 的話，不就兩全齊美了嗎。所以我先去 https://apkpure.com/tw/ 下載了這兩個軟體的好幾個版本，然後用 https://github.com/venshine/decompile-apk/releases 把它們打開來看一下究竟資料是存在伺服器端，還是 App 裡頭的 database。\n經過一番努力，有的 App 隱藏的比較好，找不到 App 中的資料；有的 App 在新版已經把資料上了雲端，但舊的版本倒還是把資料庫塞在 App 當中，讓我有機會大展身手，在它之上建立自己想要的 UI。\n分析資料庫 有了資料庫後，要先研究一下裡頭的 table 長怎麼樣，才可以決定要怎麼利用。在 Mac 上可以安裝 https://sqlitebrowser.org/ 來開啟 sqlite 資料庫。該資料庫中的 table 都還蠻清楚的，而且也有稍微做了正規化，避免重複的訊息造成資料庫太大。\ngrammar: 文法本體，有名稱，難度級別，出現頻率等資料。\ngrammar_category:類別，不同語言的翻譯，也放在了裡頭。\ngrammar_desc: 文法說明，也是包含了不同語言的翻譯。\ngrammar_exam: 跟文法相關的例句。如果在畫面中想要顯示例句的話，就需要來讀取這個 table 的資料。\nFlutter 整合既有的 sqlite database 有了基本了解後，就可以開始寫 Flutter啦。首先是在 pubspec.yaml 中加入 sqlitedb 和 path_provider 的支援。\n# sqlite support sqflite: 1.1.2 path_provider: 1.5.1 需要 path_provider 的原因是，我們得把 asset 中的 db 拷貝一份到 storage 中以後，才可以去讀取 db 。加好 package 後，下面是 db 相關的啟動程式碼，我想應該還蠻好懂的。isDBExisting() 是檢查放 db 的目錄是否存在，不存在的話，就建立它，然後看 db 檔案在不在； initDB() 則是確保 db 已經複製到應該存在的地方，然後 openDatabase()，等著之後的操作。\nView gist\ndb 中撈出來的資料會是 List\u0026lt;Map\u0026gt;，如果要直接拿來用在 UI 上，應該會很痛苦；所以我們先建立 data model，做一層轉換。資料庫中資訊很分散，但我們希望在建 data model 時還是都放在一起比較好。\nView gist\n抓資料 透過又臭又長的 SQL 語法，把上述的幾個 table join 一下，在 22 行呼叫 _grammarDb.rawQuery() 取出必要的資料，然後在 23 行轉換成上面剛定義好的 GrammarItem 清單。除了文法清單外，文法類別也是利用同樣的方式來取得。\nView gist\n畫 UI 資料部分完成，終於進入重頭戲，兜出自己想要的 UI 呈現方式。先把之前的單字本 App 翻出來，在主畫面中，加入下方 tabbar的功能。(如果不知道單字本的故事，可以在下面連結看到我的上一篇文章)。在 Scaffold 的 bottomNavigationBar 塞兩個 BottomNavigationBarItem，然後 body 的部分用 IndexedStack 放原先的 單字本 Page，以及接下來要實作的 GrammarHomeWidget。\nView gist\n再來是把 db 中撈出來的文法類別先呈現出來。這對於 Fluter 來說，是蛋糕一塊，這裡就不多作說明，反正就是一個 ListView，幾行 code 可以完成的功能。點擊分類後，顯示該分類下的文法清單，也是幾行 code 的事。\n換主題 到這邊為止，其實都做得跟原本 App 差不多，而且一樣不好看。接下來才是發揮的地方。換主題先！Flutter 對於 Theme 有很容易的更換方式。不過我個人也是沒有什麼美感的，所以只好參考一下網路上看起來比較順眼的畫面來套一下。修改不多，定義了一個主要 Theme: kBaseThemeData ，把兩個主色換掉，然後在四處的 Widget 中套用。\nView gist\n這就是套完的結果，是不是頓時有質感多了呢？(至少比較不像是 flutter demo app 了)。\n增加功能 剛剛在講資料庫內容時有提到，它包含了每個文法的難易程度。對於還在幼幼班的我來說，如果能先只濾出初級的文法，應該會比較容易學習，所以我在 Scaffold Appbar 的 actions 中加了 level 的 popupMenu，讓它可以切換不同難度的文法。切換的方式很暴力，直接從 db 從新讀取，然後再重新 build ListView。\nView gist\n另外，對於文法詳細內容呈現時，我希望顯示的例句在一開始不要有英文說明。因為很直覺的會看到英文，那就失去了先自己猜意思的機會。所以我把它設計成要手動點擊例句再把英文翻譯顯示出來，然後隔幾秒後又會再消失(雖然好像沒有什麼必要)。\nshow/hide 的功能是透過 Visibility Widget，在 ListTile onTap event 來的時候去把 _SentenceWidgetState 中的 isDisplayDefinition 狀態改變，然後叫 Widget 再重新繪製。三秒後消失的功能則是利用 Timer。\nView gist\n未來展望 目前功能只做到這樣。將來希望能自己擴充例句，畢竟，自己生活中遇到的例子會比較容易記得。針對文法的說明，也希望能自己再加入自己的(中文)見解，比較容易記憶的方式等資料。\n前情提要 用 Flutter + Google Sheets 建立單字本 App\n","permalink":"https://plateaukao.github.io/blog/posts/flutter-%E6%95%B4%E5%90%88-database-%E7%9A%84%E6%87%89%E7%94%A8/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/b9024af88daa/1_aDegaTz8oRMp921bnG_d_w.jpeg\"\u003e\n\u003cem\u003eTaichung.Taiwan\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e在學習韓文時，除了背單字的需求外，還希望可以有隨時可以查閱或複習的文法資料。找了一下 Android 平台上的相關 App，大部分都沒有太多內容，不然就是雖然有完整的內容，但操作上不是很符合我的需求，或是 UI 有點簡陋，讓人提不起學習的興趣，有的還會一直在畫面下方閃廣告。\u003c/p\u003e","title":"Flutter 整合 Database 的應用"},{"content":" Annecy.France\n第一課 너무 太多、非常 너무 커요. 太大\n너무 어려워요. 太難\n너무 맛있어요. 非常好吃\n너무 잘했어요. 幹得好\n너무 在古早之前，只能用在否定句；但在現代，正反都可以使用了。\n第二課 動詞 ＋ -고 代替 그리고 이 책은 재미있고 싸요. 這本書又有趣又便宜。\n어제 친구를 만나고, 영화를 봤어요. 昨天跟朋友碰面，看了電影。\n커피 마시고, 도너츠 먹고, 케익 먹고, 우유 마셨어요. 배불러요.\n喝了咖非、吃了甜甜圈、吃了蛋糕、喝了牛奶，飽了。\n第三課 方位 앞에, 옆에, 위에, 밑에, 뒤에 前面、旁邊、上面、下面、後面\n소파 위에서 자고 있어요. 正睡在沙發上。\n第四課 -(으)ㄹ까요? 要不要一起～？我在想，是不是～？ 帶有不確定性，想知道結果\n내일 비가 올까요? （你覺得／我在想）明天會下雨嗎？\n커피 마실까요? 맥주 마실까요? （我們要不要）喝咖啡呢？喝啤酒呢？\n어제 했을까요? （你覺得她）昨天做了嗎？\n第五課 쯤, 약, 정도 大約，大概 第六課 -(으)ㄹ 거예요 vs -(으)ㄹ게요 未來式比較 前者表示自己的意思，不受其他人／事影響。 後者主要是根據前後文，而下的決定。 -(으)ㄹ게요 使用情況\n* 你根據別人的反應而改變決定。\n* 你想了解其他人的反應或想法。\n* 因為別人說了什麼，所以你決定做什麼。\n내일 4시쯤에 갈게요. 괜찮아요?\n明天４點左右去，沒關係嗎？\n第七課 -아/어/여서 所以，因此 （表原因） 비가 와서 못 가요. 在下雨，所以去不了。\n오늘은 바빠서 영화를 못 봐요. 今天很忙，所以看不了電影。\n공원에 가서 책을 읽을 거예요. 去公園、看書。\n케익을 사서 친구한테 줄 거예요. 買蛋糕、給朋友\n固定用法\n-에 따라서 根據\n계획에 따라서 진행하겠습니다. 我會照著計畫進行。\n예를 들어서 舉例來說\n예를 들어서, 이렇게 할 수 있어요. 舉例來說，你可以像這樣做。\n第八課 같아요 看起來像，似乎是、我覺得… 비슷하다 [bi-seu-ta-da] = 像～\n도쿄는 서울하고 비슷해요? 東京像首爾嗎？\n그 이야기는 거짓말 같아요. 這故事像是謊言。\n第九課 -ㄴ 것 같아요 似乎是（＋動詞）、我覺得… 이상한 것 같아요. 看起來怪怪的。我覺的怪怪的。\n여기 비싼 것 같아요. 我覺得這裡很貴。這地方似乎很貴。\n그런 것 같아요. 我覺得是這樣。\n現在式\n詞幹 + -는 것 말하는 것 [ma-ra-neun geot] = 說話，某個人說的話，說話這件事\n過去式\n詞幹 + -(으)ㄴ 것\n말한 것\n未來式\n詞幹+ -(으)ㄹ 것 말할 것\n第十課 -기 전에 在（做某件事）之前 사기 전에 잘 생각하세요. 買之前好好想清楚。\n돈을 내기 전에 付錢之前。\n第十一課 ㅂ 불규칙 不規則變化 如果在 ㅂ 之前的母音是 오，那把它換成 오\n如果不是的話，就換成 우\n돕다 [dop-da] 幫忙\n도 + 오 + 아요 = 도와요\n어렵다 [eo-ryeop-da] 困難\n어려 + 우 + 어요. = 어려워요.\n第十二課 그래도 仍然 비가 와요. 그래도 갈 거예요? 下雨了，你還是要去嗎？\n저도 돈이 없어요. 그래도 걱정하지 마세요. 我沒錢，但不用擔心。\n第十三課 -ㄴ 將描述性動詞轉為形容詞 작은 집 小的家\n빠른 차 快的車\n비싼 컴퓨터 貴的電腦\n더 작은 가방 있어요? 你有小一點的袋子嗎？\n第十四課 -는/(으)ㄴ/(으)ㄹ + 名詞 將一般動詞轉形容詞 좋다 + 사람 = 좋은 사람 好人\n어려운 게임 困難的遊戲\n노래하는 사람 在唱歌的人\n아는 사람 認識的人\n좋아하는 사람 某人喜歡的人／我喜歡的人\n자주 먹는 한국 음식 있어요? 有你常吃的韓國菜嗎？\n第十五課 그러면, 그럼 如果是這樣的話 그럼 이거는 어때요? 那麼，這個如何呢？\n第十六課 Let’s / -아/어/여요 讓我們一起～ -아/어/여요 [polite/plain]\n시작해요.\n-(으)시죠 [honorific]\n시작하시죠.\n-자 [informal]\n시작하자.\n-(으)ㄹ래요? [polite/casual]\n시작할래요?\n-(으)실래요? [polite/formal]\n시작하실래요?\n第十七課 위해 = 위해서 為了 (很正式) 건강을 위해서 為了健康\n일본어를 배우기 위해서 為了學日文\n第十八課 밖에 + 否定句 nothing but 밖 = 外面\n밖에 = 什麼之外\n콜라밖에 안 마시다. 只喝可樂。\n한국어 조금밖에 못해요. 我只會一點韓文。\n第十九課 다음에 ~之後 -(으)ㄴ + 다음에\n-(으)ㄴ + 후에\n-(으)ㄴ + 뒤에\n편지를 받은 다음에 收到信之後\n편지를 받은 후에\n편지를 받은 뒤에\n점심을 먹은 다음에, 도서관에 갔어요. 吃完午餐後，去了圖書館。\n第二十課 -아/어/여도 即使 택시를 타도, 시간이 오래 걸려요. 即使搭計程中，還是很花時間。\n第二十一課 動詞 + -는데 / 名詞 + -인데, 形容詞 + -ㄴ데 用法\n１。描述背景，接著要給建議，或提要求，或問問題。\n내일 일요일인데, 뭐 할 거예요? 明天是星期日，要做什麼呢？\n２。解釋情況，接著要講發生了什麼事。\n어제 자고 있었는데, 한국에서 전화가 왔어요. 昨天睡覺時，接到韓國來的電話。\n３。顯示結果，跟之前的動作或情況相反。\n아직 9시인데 벌써 졸려요. 已經９點了，但我還是想睡。 준비 많이 했는데(요)… 我準備了很多…（但是…\n４。表示驚喜或驚訝\n멋있는데(요)! 太酷了吧。\n５。問問題\n지금 어디에 있는데(요)? 現在人在哪兒啊？\n６。期待得到回答\n지금(요)? 지금 바쁜데(요). 現在？現在我很忙耶…\n第二十二課 -(으)ㄹ 수도 있어요 或許，可能 알 수도 있다 可能知道\n저 내일 못 올 수도 있어요. 我明天可能不行來。\n第二十三課 학(學) 학생 (學生)\n학교 (學校)\n학원 (學院)\n수학 (數學)\n과학 (科學)\n어학 (語學)\n언어학 (言語學)\n경제학 (經濟學)\n학자 (學者)\n유학 (留學)\n유학생 (留學生)\n전학 (轉學)\n전학생 (轉學生)\n학년 (學年)\n학기 (學期)\n방학 (放學) ＝＞ 放假的意思！\n장학금 (奬學金)\n장학생 (奬學生)\n복학생 (復學生)\n학습 (學習)\n독학 (獨學)\n第二十四課 르 不規則變化 고르다 選擇\n뭐 골랐어요? 你選了什麼？\n모르다\n저도 몰라요. 我不知道。\n빠르다\n비행기는 빨라서 좋아요. 飛機很好，因為它們很快。\n자르다\n누가 케이크 잘랐어요? 誰切了蛋糕？\n기르다 rasie, grow\n토끼를 5년 동안 길렀어요. 我養兔子５年了。\n第二十五課 -네요 ~呢！ 커요 大\n크네요 大！（帶有驚訝)\n아무도 안 왔네요. 誰都還沒來呢！\n第二十六課 ㄷ 不規則變化\n듣다 聽\n듣고 있어요. 我在聽\n걷다 走路\n한 시간 걸었어요. 走了一小時。\n묻다 問\n물으면\n싣다 to load\n실을 거예요\n깨닫다 了解\n깨달았어요\n第二十七課 반말 / 존댓말 -ㅂ니다 最正式的說法\n-(아/어/여)요 [-(a/eo/yeo)yo] 有點正式\n-아/어/여\n詢問是否可以說 반말\n* 말 놔도 돼요?\n* 말 편하게 해도 돼요? * 우리 말 놓을까요?\n말 놓으셔도 돼요.\n말 편하게 하셔도 돼요.\n例子\n존댓말: 이거 뭐예요?\n반말: 이거 뭐야?\n존댓말: 어제 친구 만났어요.\n반말: 어제 친구 만났어.\n第二十八課 Verb stem + -자 親近的 let’s ~ 하자\n먹자\n내일 보자.\n안녕하세요 -\u0026gt; 안녕 [an-nyeong]\n안녕히 가세요 -\u0026gt; 안녕 [an-nyeong] / 잘 가 [jal ga]\n안녕히 계세요. -\u0026gt; 안녕 [an-nyeong] / 잘 있어. [jal i-sseo]\n저 -\u0026gt; 나 [na]\n~ 씨 / You -\u0026gt; 너\n네 / 예 -\u0026gt; 응 / 어\n아니요 -\u0026gt; 아니 / 아니야\n第二十九課 ㅅ 不規則 낫다 恢復，變好\n나아요. please feel better.\n감기 다 나았어요?感冒好了嗎？\n잇다\n이었어요. 連上\n짓다\n이 집을 누가 지었어요? 誰建了這房子。\n例外\n웃다 笑\n웃어요.\n씻다 洗\n씻을 거예요.\n벗다 脫（衣服）\n신발을 벗어 주세요. 請脫鞋。\n第三十課 실(室) 화장실 (化粧室)\n교실 (敎室)\n연습실 (練習室)\n대기실 (待機室)\n회의실 (會議室)\n병실 (病室)\n미용실 (美容室)\n사무실 (事務室)\n교무실 (敎務室)\n실장 (室長)\n실내 (室內)\n실외 (室外)\n","permalink":"https://plateaukao.github.io/blog/posts/talk-to-me-in-korean-%E7%AD%86%E8%A8%98-level-3/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/b46fb707c18d/1_RrKh9dzda-i0pcdQuuVx9w.jpeg\"\u003e\n\u003cem\u003eAnnecy.France\u003c/em\u003e\u003c/p\u003e\n\u003ch4 id=\"第一課-너무太多非常\"\u003e第一課 너무 太多、非常\u003c/h4\u003e\n\u003cp\u003e너무 커요. 太大\u003cbr\u003e\n너무 어려워요. 太難\u003cbr\u003e\n너무 맛있어요. 非常好吃\u003cbr\u003e\n너무 잘했어요. 幹得好\u003c/p\u003e\n\u003cp\u003e너무 在古早之前，只能用在否定句；但在現代，正反都可以使用了。\u003c/p\u003e","title":"Talk to Me In Korean 筆記 Level 3"},{"content":"Flutter 整合 Google Sheets 建立單字本 App Seoul. Korea\n用 Flutter 開發 App 很快速，但遇到需要有後台的應用時，一般的考量都會是採用 Firebase。Firebase 的整合不會太困難，但是前置作業有點多，對於一個只是需要從雲端某處讀取資料，而又希望這份資料可以很方便地透過網路來編輯的話，Flutter + Google Sheets 是個不錯的方案。\n1. 在 Google Sheets 上建立資料表格 Google Sheets 的方便性在此不再多說，既然我們打算把資料放在它上頭，第一步驟自然是建立所需要的表格。\n表格建立好後，需要把 url 中 spreadsheets/d/ 後面的 id 記下來，待會 Flutter 中會需要相關資訊。\n2. 撰寫 Google AppScript 有了資料後，再來是需要寫點 script ，產生 data API 讓 Flutter app 可以隨時取得最新的資料。Google AppScript 可以讓你寫點 script 操作 Google apps (calendar, docs, drive, sheets, gmail, slide) 的資料。關於 Google AppScript 的介紹，可以在下面官網得到更多資訊，這裡也是不多作介紹。\nApps Script | Google Developers\n進入 https://script.google.com/home 後，建立一個新的案子，它會跳出編輯器，讓你可以開始盡情發揮。\n填上案子的名稱，便可以開始 coding. 我的使用情境很單純，只需要讀取資料而已，不用從 app 端新增、更新或刪除資料。所以一支 get API 就很足夠了。下面是如何從 Google Sheets 讀取資料的範例，剛剛記下來的 sheets id 會在此時派上用場。另外，還需要指定 worksheet 的名稱和範圍。當然，這些都是可以隨著資料的定義方式有所調整。\nView gist\n寫好 script 後，可以點上方 menu 的 Deploy as web app，會跳出一個對話框，顯示 web app url，和相關的權限設定。執行時的角色和誰可以取得 access right，可以在下面的連結看到更多說明。\nAuthorization for Google Services | Apps Script | Google Developers\n3. 開發 Flutter App 接下來就是容易的部分了，Flutter 的網路存取以及 UI 撰寫都很直覺。連接網路的部分，我是用 Dio，程式碼如下，帶入在 app script 指定的 sheet id 和 range，然後將 json 結果解析成程式中使用的 data model。\nView gist\nUI 的部分，顯示單字只需要用一般的 ListView 就綽綽有餘了。但有時例句和解釋比較長，可能用整頁來呈現比較好看，所以也加入了 PageView的型式。\nView gist\n完成的畫面長這樣，點擊單字會顯示綠色的意思，三秒後會再自動消失。右上方的兩個 icon，一個是可以 randomize 從 google sheets 抓下來的資料，不然資料量一大，可能看來看去，都是前十幾二十個單字和句子；第二個 icon 則是切換 listview, pageview 用的。\n後續 Flutter app 完成後，電腦常常會把 google sheets 畫面開著，遇到覺得值得記下的單字就會馬上填進去。下次在手機上再開啟 app 時，就能複習了。\n後來其實又追加了文法的內容，這部分就下次再說了。\n有用連結 PatilShreyas/Flutter2GoogleSheets-Demo\n","permalink":"https://plateaukao.github.io/blog/posts/%E7%94%A8-flutter-google-sheets-%E5%BB%BA%E7%AB%8B%E5%96%AE%E5%AD%97%E6%9C%AC-app/","summary":"\u003ch3 id=\"flutter-整合-google-sheets-建立單字本app\"\u003eFlutter 整合 Google Sheets 建立單字本 App\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/c51f198cfbf3/1_lQ_29FE33bt-ig-Yg0UcBQ.jpeg\"\u003e\n\u003cem\u003eSeoul. Korea\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e用 Flutter 開發 App 很快速，但遇到需要有後台的應用時，一般的考量都會是採用 Firebase。Firebase 的整合不會太困難，但是前置作業有點多，對於一個只是需要從雲端某處讀取資料，而又希望這份資料可以很方便地透過網路來編輯的話，Flutter + Google Sheets 是個不錯的方案。\u003c/p\u003e","title":"用 Flutter + Google Sheets 建立單字本 App"},{"content":" 한강\n最近開始比較認真的學習韓文。想說應該要來把一些資料記錄下來，方便之後複習用。Talk to Me in Korean 是一份很不錯的教材，每篇教學大約都 20 分鐘左右。平常上班通勤時間大約 40 分鐘，所以把 podcast 播放速度設定為 1.2 倍的話，都可以聽個兩課。\n12 月就這麼把 Level 2 給聽了一遍。為了避免 1 月時就全忘光，在這兒把每課的教學重點和例句記錄下來。\n第一課 未來式 -ㄹ/을 거예요, 할 거예요 내년에는 일본에서 스노우보드를거예요.\n第二課 受詞助詞 -을, -를 밥을 먹어요.\n第三課 接著，所以 그리고, 그래서 친구들을 만난요. 그리고 함께 영화를 봐요 봐요.\n第四課 和，與 -하고, -(이)랑 第五課 星期幾 요일 월요일 [wo-ryo-il] = 週一 （月）\n화요일 [hwa-yo-il] = 週二 （火）\n수요일 [su-yo-il] = 週三 （水）\n목요일 [mo-gyo-il] = 週四 （木）\n금요일 [geu-myo-il] = 週五 （金）\n토요일 [to-yo-il] = 週六 （土）\n일요일 [i-ryo-il] = 週日\n第六課 但是 그렇지만, 그런데 第七課 給（誰）、從（誰）那兒～ 한테, 한테서 第八課 時間 韓文數字唸法 （小時）\n1 하나 2 둘 3 셋 4 넷\n5 다섯 6 여섯 7 일곱 8 여덟\n9 아홉 10 열 11 열하나 12 열둘\n韓文唸法數字 + 시 [si] = 小時\n中文唸法數字 + 분 [bun] = 分\n現在幾點？\n지금 몇 시예요?\n第九課 數量詞 個／名 개, 명 第十課 現在進行式 -고 있어요 한국어를 배우고 있어요.\n第十一課 自我介紹 자기소개 名字\n제 이름은 다니앨 이에요.\n年紀 나이\n저는 30살이에요.\n住所 사는 곳\n저는 대만에 살아요.\n職業 직장\n나는 소프트웨어 개발자이에요.\n家庭成員 가족\n興趣 취미\n저는 취미가 언어를 배우는 것이에요.\n打招呼\n처음 뵙겠습니다. 你好嗎\n반갑습니다. 很高興見到你\n다음에 또 봬요. 下次見\n이야기 많이 들었어요. 久聞大名\n第十二課 日期 날짜 月 월\n日 일\n哪天 며칠\n몇 월 며칠이에요?\n생일이 언제예요?\n第十三課 也 -도 第一部分 我也看了。 저도 봤어요.\n你今天也上班嗎？ 오늘도 일해요?\n第十四課 也 -도 第二部分 我也（是） 나도\n加動詞的用法：動詞轉成名詞 + -도 하다\n看 보다\n看（的名詞）보 + -기 = 보기\n吃 먹다\n吃（的名詞）먹 + -기 = 먹기\n抓 잡다 → 잡기도 하다\n賣 팔다 → 팔기도 하다\n買 사다 → 사기도 하다\n第十五課 只 -만 加在名詞後面：\n只有這個 이것만\n只有我 저만\n我早上只喝了咖啡 아침에는 커피만 마셔요.\n加在成為名詞的動詞後：\n聽 듣다\n我只有聽（沒有說話） 듣기만 했어요. 我只有看（沒有摸） 보기만 할 거예요.\n我只有今天早來 오늘만 일찍 왔어요.\n我只點了啤酒 맥주만 주문했어요.\n為什麼你只買了這個 왜 이것만 샀어요?\n昨天我（啥都沒做）只有玩 어제 놀기만 했어요.\n我只在家看電影 영화는 집에서만 봐요.\n第十六課 非常，一點，真的，並不完全，完全不 아주, 조금, 진짜, 별로, 전혀 有點貴 조금 비싸요\n真的很快 정말 빨라요.\n真的很怪 정말 이상해요.\n진짜 幾乎同義，比較不正式\n非常好吃 아주 맛있어요.\n非常遠 아주 멀어요.\n並沒有很貴 별로 안 비싸요.\n不是很有趣 별로 재미없어요.\n我一點也不忙 전혀 안 바빠요.\n一點也不熱 전혀 안 더워요.\n口語中，하나도 比 전혀 常用。 저는 돈이 하나도 없어요.\n第十七課 可以／不行 -ㄹ 수 있다/없다 -(으)ㄹ 수 있다 用法中， 수 [su] 字面意思指的是解決問題或完成一件事的點子，或是方法。 -(으)ㄹ 수 없다 的另一種說法是在動詞前面加上 못 。\n不行去 갈 수 없다\n不行看 볼 수 없다\n不行做 할 수 없다\n你會開車嗎 운전 할 수 있어요?\n現在無法見你 지금 못 만나요.\n第十八課 做什麼很在行／不在行 ~을/를 잘하다/못하다 很會唱歌 노래를 잘하다\n煮飯不在行 요리를 못하다\n很會跑步 잘 달리다\n很會寫文章 잘 쓰다\n你很會吃辣的嗎 매운 거 잘 먹어요?\n第十九課 動詞轉名詞 -는 것 動詞詞幹 + -는 것 可以有下列意思\n1. “doing” something\n2. the act of “doing” something\n3. the thing that you “do”\n4. what you “do”\n5. the fact that you do something\n感覺中文都一樣，就不翻了。\n跟 기 的差別是，-는 것 可以接動詞的其他時態。\n表示過去式可以用 -(으)ㄴ 것 表示未來式 -(으)ㄹ 것\n你買了的東西 산 것\n你買的東西 사는 것\n你（將）要買的東西 살 것\n吃了的東西 먹은 것\n吃的東西 먹는 것\n將要吃的東西 먹을 것\n現在在聽的是首歌 지금 듣는 것은 노래예요.\n你很會吃辣的嗎 매운 것 잘 먹어요?\n我的興趣是看電影 제 취미는 영화 보는 거예요.\n最近在學什麼？ 요즘 공부하는 거는 뭐예요?\n我喜歡跟朋友聊天 저는 친구랑 수다떠는 거를 좋아해요.\n第二十課 應該／必須 -아/어/여야 되다/하다 細部解釋\n1. -아/어/여 + -야\n只有當__完成／做了，\n2. 되다 or 하다\n되다 完成，可能\n하다 做\n兩部份合起來，表示：只有做了__，才有作用；只有做了__，才是對的；只有了做__，才是 ｏｋａｙ。\n我得回家 집에 가야 돼요.\n我要在那邊待到幾點 언제까지 여기에 있어야 돼요?\n我應該在哪買？ 어디에서 사야 돼요?\n第二十一課 比較 -보다 더 more 더\n西瓜比蘋果大 수박은 사과보다 더 커요.\n今天比昨天熱 오늘은 어제보다 더워요.\n第二十二課 喜歡 좋다 vs 좋아하다 我喜歡韓國語 한국어 좋아요.\n좋다 前面主詞是喜歡的東西\n좋아하다 前面是放受詞，要加 를\n描述性的動詞 + ~하다 Verb stem + -아/어/여 + -하다\n不喜歡 싫다 / 싫어요. 好看 예쁘다 / 예뻐요. 예뻐하다 / 예뻐해요. 傷心 슬프다 / 슬퍼요. 슬퍼하다 / 슬퍼해요.\n第二十三課 萬一，如果 만약, -(으)면 如果明晚下雨 내일 밤에 비가 오면\n也可以用來表示過去式：加上 -았/었/였 在 -으면 前面\n如果買 사면\n如果買了 샀으면\n也可以用來表示未來： -(으)ㄹ 거면.\n如果你（將）要看的話 볼 거면\n第二十四課 還（沒），已經 아직, 벌써 我還沒做 아직 안 했어요.\n還是早上 아직 아침이에요.\n加 도 有評評論對方的意思在\n你怎麼還不知道 아직도 몰라요?\n已經三點了！ 벌써 세 시예요.\n已經來了？ 벌써 왔어요?\n已經結束了？ 벌써 끝났어요?\n已經在下雨了 벌써 비가 오고 있어요.\n이미 和 벌써 類似。이미 用在你和對方都已經知道那件事。\n第二十五課 某人，某物，某處，某個時候 누군가, 무언가, 어딘가, 언젠가 我將來某天想去美國 언젠가 미국에 가고 싶어요.\n你找到了什麼？ 뭐 찾았어요? 你找到了什麼嗎？ 뭔가 찾았어요?\n第二十六課 命令 -(으)세요 請開始 시작하세요.\n請唸書 공부하세요!\n請小心 조심하세요.\n請慢走 안녕히 가세요.\n第二十七課（為我）做 -아/어/여 주세요 給 주다 來 오다\n請來 오세요. 請（為我）來 와 주세요.\n請看我的作業 숙제 봐 주세요.\n我很害怕，請跟我一起走 무서워요. 같이 가 주세요.\n（我）肚子餓，來點 kimbap 배고파요. 김밥 사 주세요.\n第二十八課 表方法的介系詞 -(으)로 用木頭做 나무로 만들다\n左轉 왼쪽으로 가다\n走這條路 이 길로 가다\n用韓文說 한국어로 말하다\n在事故中受傷\n사고로 다치다 搭計程車來的嗎 오늘 택시로 왔어요?\n我要搭公車去 버스로 갈 거예요.\n第二十九課 多，所有 더, 다 全給我 다 주세요.\n給我多一點 더 주세요. 全做好了 다 했어요.\n讀完書 책을 다 읽다\n哪個比較好？뭐가 더 좋아요?\n再等我１０分鐘 10분 더 기다려 주세요.\n第三十課 不要做 -지 마세요 不做，停止做：말다 不要走 가지 마세요.\n還不要走 아직 가지 마세요.\n別做、算了、forget about it\n하지 마세요.\n別笑 웃지 마세요.\n不要擔心 걱정하지 마세요.\n還不要寄出，我還沒寫完 아직 보내지 마세요. 아직 다 안 썼어요.\nTTMIK Level 2\n","permalink":"https://plateaukao.github.io/blog/posts/talk-to-me-in-korean-%E7%AD%86%E8%A8%98-level-2/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/946850cbdf21/1_zfj7U8DkCo_vmBpX_3tIKw.jpeg\"\u003e\n\u003cem\u003e한강\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e最近開始比較認真的學習韓文。想說應該要來把一些資料記錄下來，方便之後複習用。Talk to Me in Korean 是一份很不錯的教材，每篇教學大約都 20 分鐘左右。平常上班通勤時間大約 40 分鐘，所以把 podcast 播放速度設定為 1.2 倍的話，都可以聽個兩課。\u003cbr\u003e\n \u003cbr\u003e\n 12 月就這麼把 Level 2 給聽了一遍。為了避免 1 月時就全忘光，在這兒把每課的教學重點和例句記錄下來。\u003c/p\u003e","title":"Talk to Me In Korean 筆記 Level 2"},{"content":"\nI used to using this plugin for creating android library release zip file, and manually upload it to bintray website.\nblundell/release-android-library\nRecently, it says it’s no longer being maintained; and another gradle plugin is recommended.\nnovoda/bintray-release\nNovoda’s bintray-release allows you to upload locally built javadoc/source jars, and library aar files to bintray directly, without manual work. What a relief if it really works.\nBy following the instructions on GitHub, within minutes, the configuration is ready for trial.\nHowever, during building local files, some javadoc error might happen with following error logs:\nView gist\nIt happens when javadoc task encounters some errors. This error can be bypassed by adding following code snippet to the gradle file.\nView gist\nAfter configuring all parameters for bintray-release plugin, remember to add dryRun = false in the script, or execute the command as below, to allows it really uploading the files to bintray:\n./gradlew clean build bintrayUpload -PbintrayUser=me -PbintrayKey=key -PdryRun=false ","permalink":"https://plateaukao.github.io/blog/posts/how-to-publish-an-android-library-to-jcenter/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/a31231b47241/1_jYFBXKXr_vrW7Vrb4bF8bQ.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003eI used to using this plugin for creating android library release zip file, and manually upload it to \u003cstrong\u003ebintray\u003c/strong\u003e website.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/blundell/release-android-library\"\u003eblundell/release-android-library\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eRecently, it says it’s no longer being maintained; and another gradle plugin is recommended.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/novoda/bintray-release\"\u003enovoda/bintray-release\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eNovoda’s bintray-release allows you to upload locally built javadoc/source jars, and library aar files to \u003cstrong\u003ebintray\u003c/strong\u003e directly, without manual work. What a relief if it really works.\u003c/p\u003e\n\u003cp\u003eBy following the instructions on GitHub, within minutes, the configuration is ready for trial.\u003c/p\u003e","title":"How to Publish an Android Library to jcenter"},{"content":"對一個 Android app 開發者來說，工作中使用時間最長的軟體，我想應該就是 Android Studio 了吧。\n工欲善其事，必先利其器。除了要花時間熟悉內建的各項功能外，有些時候，根據自己的開發習慣，會希望它再額外多些功能，讓自己的開發流程更加順手。\n下面這是官方提供的教學網頁。做為一個 Hello, World 的示範，還算可以。但如果是真的打算開發一個能用的 plugin，這網頁遠遠不足。大部分的說明都是文字描述，而不是以開發時所需要的步驟來講解；常會有看完某個段落後，似乎多懂了一點點，但卻完全不知道怎麼使用該 Class，因為大都沒有附上沒有相關的程式碼範例。\nCreating Your First Plugin\n相對於開發 Android app 來說，明明只是要做個簡單功能的 plugin，卻得花上許多時間東查西查的，還不見得可以找到對的方式。所以想說應該記錄下包，以防以後想再寫別的 plugin 時，又得再重新來一次。或者，如果也有其他人跟我一樣，遇到各式各樣問題，不得其門而入時，可以剛好看到這幾篇分享。\nGUI Desinger 不產生程式碼的問題 開發 Android Studio Plugin 的過程中，常常會需要產生 UI 的畫面：比方說想要在 Preference 中讓使用者可以更改一些相關的設定，或是有些選項希望可以跳出對話框，讓使用者當下選擇後，就可以繼續運作。\n在 Intellij IDEA 中，建立 Java UI 的方式是使用它自帶的 GUI Designer，透過拖拉元件的方式，讓你可以很方便地產生你想要的畫面，然後 Intellij 再幫你產生所需要的 Java 程式碼，讓你可以在專案中使用。不過，就是這個不過，如果你跟我一樣，利用 Gradle 建立專案的話，會遇到 GUI Designer 怎樣就是不動。\n千辛萬苦查了官網的說明，結果官網只簡短地說了，它在 Gradle 下不會產生對應的程式碼(下圖黃色框框)。\nGUI Designer\n咦，如果不產生的話，官網說明總要給我一些方向吧，不然我怎麼知道要怎麼接下去做。難不成我要重新建一個 plugin 專案，然後把系統改成 maven 嗎？\n試了各種方式，最終還是在某篇 StackOverflowF上的文章找到了符合我的解法。\n在 build.gradle 中加入下面 library 的引用就好啦。 dependencies { implementation \u0026#39;com.intellij:forms_rt:7.0.3\u0026#39; } 在 Preference 中，記得要把設定切為 Generate GUI into Java source code 再 build 一次 plugin 看看。 Intellij Idea 13 UI Designer and automatic Gradle building\n","permalink":"https://plateaukao.github.io/blog/posts/android-studio-plugin-%E9%96%8B%E7%99%BC%E8%B8%A9%E5%9D%91%E8%A8%98-i/","summary":"\u003cp\u003e對一個 Android app 開發者來說，工作中使用時間最長的軟體，我想應該就是 Android Studio 了吧。\u003c/p\u003e\n\u003cp\u003e工欲善其事，必先利其器。除了要花時間熟悉內建的各項功能外，有些時候，根據自己的開發習慣，會希望它再額外多些功能，讓自己的開發流程更加順手。\u003c/p\u003e","title":"Android Studio Plugin 開發踩坑記 (I)"},{"content":"\nIn Flutter, it’s easy to load an Image widget. Flutter provides several different ways: You can use ***Image.network(), Image.asset() or Image.memory()***depending on the source you want to load the image.\nHowever, Flutter is known to be slow at processing image, including decoding image when the source file is relatively large.\nView gist\nThe above code snippet is an example of loading an Image from the internet.\nAt line 8: download the data from the internet;\nAt line 12: an Image Widget is created;\nAt line 14: then ask system to build the Widget again by using setState().\nThis code works, but UI often blinks when the image is replacing the placeholder on screen.\nprecacheImage() Come to Rescue! After searching for a long time on the internet, I finally found the way to fix this problem.\nImage.memory() is just setting up the Image Widget; it does not mean the Widget is ready to be drawn on screen yet. That’s why you would see the short period of blank screen on the animation above.\nTo solve this, all you have to do is wait until the Image Widget is ready, and then call setState() afterward. That’s where precacheImage() comes to rescue. It can help you to cache (or load) the Image in advance. When you really need to draw it, you don’t have to spend time decoding the binary again.\nView gist\nAt line 14: we added the code, and that’s it!\nLook at the no-blink-anymore UI! Hope this tip is useful for you too.\n","permalink":"https://plateaukao.github.io/blog/posts/my-2-cents-on-image-processing-in-flutter/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/a6ab5166f590/1_qKJvABWGL9EyztFfvLlJbQ.png\"\u003e\u003c/p\u003e\n\u003cp\u003eIn Flutter, it’s easy to load an Image widget. Flutter provides several different ways: You can use ***Image.network(), Image.asset() or Image.memory()***depending on the source you want to load the image.\u003c/p\u003e\n\u003cp\u003eHowever, Flutter is known to be slow at processing image, including decoding image when the source file is relatively large.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://gist.github.com/plateaukao/ffc76091e5d6901e220d6ea9bed60840\"\u003eView gist\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eThe above code snippet is an example of loading an Image from the internet.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAt line 8\u003c/strong\u003e: download the data from the internet;\u003c/p\u003e","title":"My 2 cents on Image Processing in Flutter"},{"content":"Flickr Authentication in Flutter (II) Following the steps described in last article, you can have a working version of Flickr authentication module in Flutter. However, it’s very tedious to remember the pin code, and input it in the dialog in step 2.\nBy reading closely the “Getting a request token” part in Flickr website, you’ll find that, in fact, there’s another way to get the verifier we need without user intervention. A parameter called “oauth_callback” is used to specify a url. If it’s defined in the request; after authorization is complete, Flickr will redirect the user back to your application using the oauth_callback specified with your Request Token. It looks like this:\nhttp://www.example.com/ ?oauth_token=72157626737672178-022bbd2f4c2f3432 \u0026amp;oauth_verifier=5d1b96a26b494074 Since the oauth_verifier can be obtained directly in the url, you don’t have to ask user to input the pin code anymore!\nSo, let’s start modify the original implementation.\nOriginally, I use url_launcher package to display the web authentication page, and get the pin code. We have to change to use flutter_webview_plugin instead, since it allows us to intercept the redirect urls.\nView gist\nI used “http://localhost/” as my callback url, and try to check url change on FlutterWebviewPlugin instance. When its host is “localhost”, and it does contain a query parameter “oauth_verifier”, I just feed it into requestToken directly. No more memory tests and displaying dialogs!\nThat’s it! Hope you find this article as useful as I do. Here’s the demo from my app:\nReference: Flickr API integration in Flutter with Dart\n","permalink":"https://plateaukao.github.io/blog/posts/flickr-authentication-in-flutter-part-ii/","summary":"\u003ch3 id=\"flickr-authentication-in-flutterii\"\u003eFlickr Authentication in Flutter (II)\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/c06495127f60/1_pecNyXcm3szOvnyBJH3Xzw.png\"\u003e\u003c/p\u003e\n\u003cp\u003eFollowing the steps described in \u003ca href=\"https://medium.com/@danielkao/flickr-api-integration-in-flutter-with-dart-1c07d4b9e8d6\"\u003elast article\u003c/a\u003e, you can have a working version of Flickr authentication module in Flutter. However, it’s very tedious to remember the pin code, and input it in the dialog in step 2.\u003c/p\u003e\n\u003cp\u003eBy reading closely the \u003cstrong\u003e“\u003c/strong\u003e\u003ca href=\"https://www.flickr.com/services/api/auth.oauth.html#request_token\"\u003e\u003cstrong\u003eGetting a request token\u003c/strong\u003e\u003c/a\u003e\u003cstrong\u003e”\u003c/strong\u003e part in Flickr website, you’ll find that, in fact, there’s another way to get the verifier we need without user intervention. A parameter called “oauth_callback” is used to specify a url. If it’s defined in the request; after authorization is complete, Flickr will redirect the user back to your application using the \u003ccode\u003eoauth_callback\u003c/code\u003e specified with your \u003ca href=\"https://www.flickr.com/services/api/auth.oauth.html#request_token\"\u003eRequest Token\u003c/a\u003e. It looks like this:\u003c/p\u003e","title":"Flickr Authentication in Flutter (Part II)"},{"content":" 上高地。日本\nClean Architecture: A Craftsman\u0026rsquo;s Guide to Software Structure and Design, First Edition\nPart I writing working codes is easy; to write codes that are easy to maintain and extend is really hard.\nCh1 What is Design \u0026amp; Architecture In this book, design and architecture means the same thing: it’s simply a continuum of decisions from the highest to the lowest levels.\nThe goal of software architecture is to minimize the human resources required to build and maintain the required system.\nCost per line increased dramatically if code is a mess.\nTortoise and the Hare racing game Conclusion: the only way to go fast, is to go well.\nCh2 A Tale of Two Values Every software system provides two different values: behavior \u0026amp; structure.\nEisenhower’s Matrix\nI have two kinds of problems, the urgent and the important. The urgent are not important, and the important are never urgent.\nit is the responsibility of the software development team to assert the importance of architecture over the urgency of features.\nPart II Ch3 Paradigm Overview Structured Programming: discovered by Dijkstra. Use if/then/else instead of goto. Discipline on direct transfer of control.\nOO Programming: Discipline on indirect transfer of control.\nFunctional Programming: A functional language has no assignment statement. Discipline upon assignment.\nRemoved goto statements, function pointers, and assignment.\nCh4 Structured Programming Böhm and Jacopini proved that all programs can be constructed from just three structures: sequence, selection, and iteration.\nStructured programming allows modules to be recursively decomposed into provable units, which in turn means that modules can be functionally decomposed.\nDijkstra once said, “Testing shows the presence, not the absence, of bugs.”\nCh5 OO Programming What is OO? One answer to this question is “The combination of data and function.” Another common answer to this question is “A way to model the real world.” ??!!\nThree magic words: encapsulation, inheritance, and polymorphism.\nEncapsulation: programming language C can have good encapsulation too, and C++ broke it.\nInheritance: programming language C can do it too.\nPolymorphism: You guess? Yes, we can do it in C too. The problem with explicitly using pointers to functions to create polymorphic behavior is that pointers to functions are dangerous.\nDependency Inversion\nConclusion: OO is the ability, through the use of polymorphism, to gain absolute control over every source code dependency in the system. It allows the architect to create a plugin architecture, in which modules that contain high-level policies are independent of modules that contain low-level details.\nCh6 Functional Programming Variables in functional languages do not vary.\nWhy would an architect be concerned with the mutability of variables? The answer is absurdly simple: All race conditions, deadlock conditions, and concurrent update problems are due to mutable variables.\nEvent sourcing is a strategy wherein we store the transactions, but not the state. When state is required, we simply apply all the transactions from the beginning of time.\nCRUD to CR only in ideal case. No modifications anymore.\nPART III DESIGN PRINCIPLES SOLID principles are explained.\nSRP: The Single Responsibility Principle\nOCP: The Open-Closed Principle\nLSP: The Liskov Substitution Principle\nISP: The Interface Segregation Principle\nDIP: The Dependency Inversion Principle\nCh7 SRP: Single Responsibility Principle least well understood principle due to naming A module should be responsible to one , and only one, actor\ncohesive (凝聚力): Something that is cohesive consists of parts that fit together well and form a united whole.\nCohesion is the force that binds together the code responsible to a single actor.\nCohesive definition and meaning | Collins English Dictionary\nCase 1: Accidental Duplication\nEmployee is a class\nEmployee violates SRP because these three methods are responsible for different actors. It might affect each other, if one of the functions wants to have some changes.\nThe SRP says to separate the code that different actors depend on.\nCase 2: Modifications on the same class at the same time\nSolution\nSeparate the Data Use facade pattern Use separate classes as facade for lesser functions Conclusion At level of components: Common Closure Principle\nAt architectural level, it becomes the Axis of Change responsible for the creation of Architectural Boundaries.\n","permalink":"https://plateaukao.github.io/blog/posts/summary-of-clean-architecture-i/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/910d1ebdc60b/1_rnOWr47Ytu91MtOhw6qLFw.png\"\u003e\n\u003cem\u003e上高地。日本\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/\"\u003eClean Architecture: A Craftsman\u0026rsquo;s Guide to Software Structure and Design, First Edition\u003c/a\u003e\u003c/p\u003e\n\u003ch3 id=\"part-i\"\u003ePart I\u003c/h3\u003e\n\u003cp\u003ewriting working codes is easy; to write codes that are easy to maintain and extend is \u003cem\u003ereally hard\u003c/em\u003e.\u003c/p\u003e\n\u003ch3 id=\"ch1-what-is-design--architecture\"\u003eCh1 What is Design \u0026amp; Architecture\u003c/h3\u003e\n\u003cp\u003eIn this book, \u003cstrong\u003edesign and architecture means the same thing\u003c/strong\u003e: it’s simply a continuum of decisions from the highest to the lowest levels.\u003c/p\u003e","title":"Summary of Clean Architecture (I)"},{"content":"\nFlickr used to be a cool photo service that gathered many talented photographers. I started uploading all my photos to it since 2005, and now I’ve uploaded more than 160k photos. With so many memories in Flickr, I would like to have an app to randomly show me my own photos in order to refresh good old memories.\nThere are many Flickr API Kits in different programming languages in Flickr official website; however, Dart is not one of them. I either need to implement the API wrapper by referencing its Web API document or find another solution on the internet.\nFlickr Services\nWithout luck, I couldn’t find useful Flickr related implementations for dart. I decided to wrap the Web APIs in dart by myself. But first of all, I need to deal with the authentication process. You can see the authentication process explained below.\nAuthentication difficulties Flickr still uses OAuth version 1.0. It needs 3 API calls to complete the authentication process. Among the flow, a web page interaction is required for users to grant the access, and get the pin code to input into second API call.\nFlickr Services\nIt would take some effort to implement the OAuth1 signature generation, and appending related http query string parameters along with the APIs that needs authentication. Fortunately, someone already implemented the hard work for OAuth1 authentication. We only have to pass in necessary information related to Flickr in order to make it work.\nBy the way, remember to get your own developer API key and API secrete from Flickr.\noauth1 for OAuth authentication oauth1 | Dart Package\nBy following the oauth1 package’s documentation you can initiate the flow like this and call the requestTemporaryCredentials(). It will return the Web url that you need to display to users.\nView gist\nurl_launcher : for WebView For displaying webview, I used url_launcher package. It’s very straightforward too.\nView gist\nThe WebView to ask for user permission.\nAfter getting user permission, it will show the pin code\nurl_launcher | Flutter Package\nAfter getting the pin-code from webview, A dialog will be displayed to ask user input the pin-code, and feed it into requestToken().\nCreate a dialog to get the pin code from user.\nIn the end, requestToken will successfully return an oauth1.Client object, that can be used to call other Flickr API.\nView gist\nVoila! The authentication is done! The rest part is pure dart and flutter implementation which can be easily built up based on sample codes on flutter web site.\nThat’s it! With one night effort, my app is running well in my android phone, and also in the iOS simulator too. :)\nA simple grid view layout to show random page of photos from my own Flickr account\nPS. Now there’s a better way to get pin code! Please check my another article:\nFlickr Authentication in Flutter (Part II)\n","permalink":"https://plateaukao.github.io/blog/posts/flickr-api-integration-in-flutter-with-dart/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1c07d4b9e8d6/1_BR4aqmuc6kDpZ8CbJ9_z0A.png\"\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eFlickr used to be a cool photo service that gathered many talented photographers. I started uploading all my photos to it since 2005, and now I’ve uploaded more than 160k photos. With so many memories in Flickr, I would like to have an app to randomly show me my own photos in order to refresh good old memories.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThere are many Flickr API Kits in different programming languages in Flickr official website; however, Dart is not one of them. I either need to implement the API wrapper by referencing its Web API document or find another solution on the internet.\u003c/p\u003e","title":"Flickr API integration in Flutter with Dart"},{"content":"\n原以為 Flutter 寫起來很開心，有任何耗時比較長的工作，就塞到一個 Future someFunction(…) async {…} 中就好，不需要像 Android 裡還需要用到 rxJava 或是自己建 thread 來處理。但後來發現其實沒有那麼單純。就像下面這篇文章開頭講的一樣：剛接觸 Flutter 的人通常不會管到 asynchronous 的問題，直到 UI 開始變得很卡的時候。\nFlutter Threading\n在 Dart 中 async 和 Future 無法解決所有耗時的工作。Dart 雖然支援 非同步執行，但其實如果是透過 async keyword 的話，只是把工作丟到同一個 event loop 中，讓它暫時不會卡住目前的工作，等到真的輪到它執行時，如果它真的很耗時，那 main isolate 還是會 freeze 住的。Dart 主要的 task 都是在 main isolate 中完成的，isolate 像是個 single thread 的 process。如果真的想要讓某些工作能夠同時進行，不要卡住 main isolate 的話，就得要自己產生新的 isolate 來執行。但 isolate 又不是那麼好寫，必須藉由ReceivePort來傳送資料。下面有個小範例：\nView gist\nDart Fundamentals - Isolates\n好在針對一般需要比較多時間執行的工作，Dart 提供了一個比較容易使用的 compute() function，幫開發者包裝自建 isolate 的繁雜流程。\n以下是個簡單的範例。原先第一行的 processImage() 因為需要針對圖片的每個 pixel 做處理，所以會很花時間，如果只是單純用 async 的話，在執行的時候依然會在 main isolate 做，造成畫面反應很不流暢。將它改成用 compute() 來呼叫後，Dart 會幫忙產生新的 isolate 同步執行。如此一來畫面就不會再卡卡的了。\nView gist\n將現有的 async function 改成呼叫 compute()，只是幾行 code 的事，就可以解決在同一個 event loop 中執行影響其他 task 流暢度的問題，但如同上面的例子所示， compute 呼叫的 function 必須要是top-level function 或是 static 才行，在實作上還要思考怎樣管理這些透過compute 完成的 function。\n參考資料 Asynchronous programming: futures \u0026amp; async-await\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%B9%BB%E6%BB%85%E6%98%AF%E6%88%90%E9%95%B7%E7%9A%84%E9%96%8B%E5%A7%8B-flutter-%E7%9A%84-async-%E8%88%87-isolate/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/2f87321a7ba8/1_yyCGSU6ph3dCpTMk1SfBGA.png\"\u003e\u003c/p\u003e\n\u003cp\u003e原以為 Flutter 寫起來很開心，有任何耗時比較長的工作，就塞到一個 \u003cem\u003e\u003cstrong\u003eFuture\u003cT\u003e someFunction(…) async {…}\u003c/strong\u003e\u003c/em\u003e 中就好，不需要像 Android 裡還需要用到 rxJava 或是自己建 thread 來處理。但後來發現其實沒有那麼單純。就像下面這篇文章開頭講的一樣：剛接觸 Flutter 的人通常不會管到 asynchronous 的問題，直到 UI 開始變得很卡的時候。\u003c/p\u003e","title":"幻滅，是成長的開始 — Flutter 的 async 與 isolate"},{"content":"\n在開發的時候，會注意到畫面的右上角有個 Debug 的標籤。官方的說法是，因為開發的版本開啟了很多協助開發或除錯的功能，所以運行速度會跟正式版差很多。為了避免使用的人以為這就是 flutter app 的速度，所以針對測試中的版本，預設都會加上 debug 標籤，讓人比較不會誤會 (真的這樣就不會誤會嗎？)\n單純在開發的話，右上角的標籤並不會造成困擾，但如果打算要正式發行 app，想要抓一些 app 運行中的畫面時，這就是個很大的問題了。網上到處都有提到怎麼把這標籤拿掉，如下圖 19 行，在 MaterialApp 中，將 debugShowCheckdModeBanner 設成 false 就可以了。\n如果真的那麼簡單，我就不會寫這篇 blog 了。\n對 iOS build 來說，要在 release build 中，才不會看到 debug 標籤，但是 release build 又無法執行於 iOS Emulator 中。也就是說，即使加了 debugShowCheckedModeBanner，還是一樣會在 iOS Emulator 中看到惱人的標籤。\n在網路上找了很久，終於找到了解決這問題的方式：\n先將 app 執行於 iOS Emulator 中。 在 Android Studio 中開啟 Flutter Inspector，從 More Actions 中找到 Hide Debug Mode Banner 的選項，關了它。 大功告成！\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E7%A7%BB%E6%8E%89-flutter-app-%E4%B8%AD%E7%9A%84-debug-label/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/a157ebf2bfbe/1_IXdjHBpDOdbl1P5K01a5zw.png\"\u003e\u003c/p\u003e\n\u003cp\u003e在開發的時候，會注意到畫面的右上角有個 Debug 的標籤。官方的說法是，因為開發的版本開啟了很多協助開發或除錯的功能，所以運行速度會跟正式版差很多。為了避免使用的人以為這就是 flutter app 的速度，所以針對測試中的版本，預設都會加上 debug 標籤，讓人比較不會誤會 (真的這樣就不會誤會嗎？)\u003c/p\u003e","title":"如何移掉 flutter app 中的 debug label"},{"content":" Image processing in flutter (remove white background)\n在 Android 中，把圖片顯示出來前，如果對 Image 做點變化的話，通常會將 Image 先轉成 Bitmap，再針對 Bitmap 中的每個 pixel 去做處理，最終轉成 Drawable，再送給 View 去繪製。作法可以參加 CalliImageView 中的 adjust function。\nplateaukao/CalliImageView\n但是在 Flutter 的世界中，事情就不是那麼單純了。官方提供顯示圖片的 Widget 主要是 Image，可以透過 Image.network(), Image.asset(), Image.file(), Image.memory()，從不同來源載入圖片。但是 Image 並沒有提供取出 raw data 的 public function，即使你繼承它，也一樣找不到類似 onDraw() 的功能。想要更了解 Flutter 的繪製 Widget 架構的話，可以參考掘金上的這篇文章 Flutter中的Image入门讲解 。\n網路上建議如果想要 Flutter Image Processing ，是使用這個套件 https://pub.dartlang.org/packages/image ，但找到的範例都是要先從檔案中把圖片讀成 Bytes 再餵給 Image。\nView gist\n我的使用情境是：圖片會是從網路來的。所以我得先去研究怎麼把拿到的 url 先從網路下載到本地端才行。\n在網路上找到一個比較簡單的 network cache image 範例如下：\nzmqgithub/Save-Server-Image\n仔細研究一下它的 downloadImage() 可以發現，在第12行時它把 request (應該是 response 才對)的資料當成 bytes 讀出來，然後再寫到 file 中。這 bytes 應該就是可以直接用來塞到 Image.decodeImage() 中吧！如此一來，就不需要真的把圖存成圖片了。\nView gist\n我想做的事是把一張圖裡，白色的部分都變成透明的，只留下黑色的部分。接下來就容易了，只要參考 Image 套件中的各種 filter class，也寫一個自己的版本就行。下面是更改過的 downloadImage() 版本，加入了把圖片去背的功能。\nView gist\n最終的效果，大家可以看文章一開始的圖片囉。沒去背之前，書法字旁邊因為有白色背景，所以背景畫的九宮格無法正確的顯示。去背後，一切就正常啦。\n","permalink":"https://plateaukao.github.io/blog/posts/flutter-%E4%B8%AD%E8%99%95%E7%90%86-image-%E7%9A%84%E6%96%B9%E5%BC%8F/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/edef5f47000b/1_q4e7UAFWxHbwBoskSmhAyw.png\"\u003e\n\u003cem\u003eImage processing in flutter (remove white background)\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e在 Android 中，把圖片顯示出來前，如果對 Image 做點變化的話，通常會將 Image 先轉成 Bitmap，再針對 Bitmap 中的每個 pixel 去做處理，最終轉成 Drawable，再送給 View 去繪製。作法可以參加 CalliImageView 中的 adjust function。\u003c/p\u003e","title":"Flutter 中處理 Image 的方式"},{"content":" Hakone.Japan\nIt’s common that an android developer is working on several projects. Sometimes, you just need some existing string translations from another Android project. Usually, the translated strings are scattered in strings.xml file in tens of folders named values-xx. There’s no easy way to copy these string definitions from multiple folders into new project.\nTherefore, I wrote a simple python script to do the job. All you have to do is:\nConfigure input and output folder (usually it would be looked like …/project_name/app/src/main/res/ ) run commands for each string key you wanna copy. View gist\nnew_string_key is optional if you expect using the same string key in new project.\nHere’s the link to Github repository. Happy Coding~\nplateaukao/AndroidExtractTranslatedString\n","permalink":"https://plateaukao.github.io/blog/posts/extract-translated-strings-in-android-project/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/adef05165e23/1_bXZwCYedjvXKLs0P9qPQwg.png\"\u003e\n\u003cem\u003eHakone.Japan\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eIt’s common that an android developer is working on several projects. Sometimes, you just need some existing string translations from another Android project. Usually, the translated strings are scattered in \u003cem\u003e\u003cstrong\u003estrings.xml\u003c/strong\u003e\u003c/em\u003e file in tens of folders named \u003cem\u003e\u003cstrong\u003evalues-xx\u003c/strong\u003e\u003c/em\u003e. There’s no easy way to copy these string definitions from multiple folders into new project.\u003c/p\u003e\n\u003cp\u003eTherefore, I wrote a simple python script to do the job. All you have to do is:\u003c/p\u003e","title":"Extract translated strings in Android project"},{"content":" 青森八甲田樹冰\n滑雪滑這麼多年了，第一次滑到這麼特別的雪場。整個雪場只有一條纜車；雖然一次可以載一百個人，但每次排隊可能要等20分到1個多小時，一天有可能只滑得到三趟。儘管如此，每天前往朝聖的人還是絡繹不絕。究竟它有怎樣的魅力呢，下面會一一道來。我們先來看看怎麼前往這個神奇雪場。\n交通 從台北出發的話，可以搭 JAL (日航)從松山飛羽田，再從羽田轉機飛青森的班機。或是先搭機到東京，然後搭新幹線到新青森再轉車到青森車站。前者比較方便，轉機間隔只有一個多小時，但缺點是日本國內線有可能因為天候不佳而延後起飛或是甚至就停飛。後者雖然比較麻煩一點，但是彈性比較大。\n到達青森車站後，如果是住在車站附近旁的旅館，每天一早可以在青森車站旁的巴士中心買JR Bus 車票：青森前往八甲田纜車站前，然後在 11 號巴士亭等車。巴士最早的班次是 8:00，要滑雪的遊客大都會搭乘這一班，所以為了確保能搭得上巴士，最好是早上 7:30 就先到 11 號巴士亭排隊，用雪板和其他裝備佔個位子，人就可以進到巴士中心休息。\n由於八甲田纜車有可能因為氣候關係停止營業，所以在出發前，最好先到下面的網站看一下當天的營運狀況再決定是否要搭車前往。不然搭了一個多小時的車，卻發現上不了山，是很糗的。\n八甲田ロープウェー株式会社\n青森往八甲田纜車站車票\n排隊中\n青森車站到八甲田纜車站大約需要50分左右的車程，而纜車開始營業的時間剛好是 9 點。理論上沒太多遊客的話，是可以剛好買纜車票，然後搭纜車上山開始滑雪。不過，實際的狀況通常不是這麼一回事：很多住在八甲田附近民宿或旅館的遊客，在八點多時就已經先到纜車站了，所以第一趟上山總是要排隊排很久。介意的人可以考慮直接住在八甲田附近的旅館，省掉第一次漫長的排隊，大概可以多滑一趟吧。\n纜車外排隊盛況\n一個小時後，終於排到纜車了\n滑雪相關資訊 雪票 料金＆営業時間 | 八甲田ロープウェー株式会社\n八甲田雪票( 5 次回數劵)\n單次劵\n八甲田有季票，月票，回數票可以買。要滑多天的人可以考慮買月票，如果只滑個幾天，又不是很確定自己能滑多快，或是排隊的時間會不會很久的話，可以考慮先買回數票。回數票 1 張可以搭纜車上山 5 次。通常 1 天能滑到 4 趟就很不錯了，要滑到 5 趟的話，得要遊客人數不多，排隊時間不長，又沒有在樹林中卡太久才行。回數票 1 張 5050 日圓 (2019 票價)，平均滑一趟 1000 日圓。\n八甲田山滑雪路線圖\n滑道 八甲田纜車右邊的 Direct Course 是比較熱門的路線，從圖上來看，是在纜車站的右手邊。在經過給觀光客看的樹冰區，大約50公尺後，開始會有紅色的桿子標示路線。路線的附近有許多大型樹冰，樹冰旁都會有比較深的凹洞。所以在這一區滑行時要小心不要太靠近樹冰，以免樹旁雪太鬆滑到一半陷進去。\nDirect course 一開始的大型樹冰區\nDirect Course 滑行三四分鐘後，會來到坡度比較緩的部分，這一區是相對上比較好玩的。紅色桿子附近開始有許多樹林可以鑽來鑽去。只要看準在遠遠的前方路線是要再往右轉的，進入右邊樹林就可以安心地不斷往前滑，不怕會愈滑愈遠。\nDirect course 的後半段樹林區\n另外一條能滑的路線(Forest course)在纜車的左手邊，這一條有時候會因為風雪的關係沒有開放。在圖中雖然路線是從山頂公園站開始，但實際上在滑的時候，是可以從纜車下的樹冰區一路從右邊的 direct course 切到左邊的 forest course。不過要這麼做有一定的風險存在，因為在切往 forest course 的路上，有些路段很平緩，skier 可以藉由雪杖一步步前進，snowboarder 就得自求多福了。\nDirect course 斜切 Forest course 經過的樹冰區\nDirect course 斜切 Forest course 經過的樹冰區\n其他的雪道，可能都要有請嚮導帶路會比較好。這次主要都是在滑 direct course，所以沒有什麼可以補充的。\n裝備 八甲田的雪很厚，而且很多地方很平垣，所以即使是 snowboarder 也會帶上以下的裝備，確保自己不會被厚雪卡住，動彈不得。\n滑雪背包：一般雪場可以輕裝上陣，什麼都不用帶；但在八甲田滑雪，強烈建議一定要帶滑雪背包；除了可以掛雪杖、雪鞋、雪板外，還可以放些乾糧，甚至是多一件衣物。如果有在拍照的話，運動相機，備用電池，和自拍棒都可以很安心地塞在裡頭。\n雪杖：snowboarder 很少會需要用到雪杖，所以其實不用買到太貴的。建議到迪卡儂購買一般的登山杖就行，然後再加買雪地用的登山杖擋泥板。整套買下來(兩根登山杖加上擋泥板)不用台幣一千元，卻可以為你節省很多在鬆雪中掙扎的時間，是項很划算的投資。在台灣時，登山杖還可以拿來爬山用。\n迪卡儂登山健行用品與裝備 - 登山杖 - Decathlon\n迪卡儂 登山杖檔泥板 (2入) FORCLAZ\n**雪鞋：**在連雪杖也施不上力的時候，就得考慮脫板穿上雪鞋。比方說不小心滑到小山谷，面前是個小坡得往上爬，雪又過於鬆厚。這時雪鞋會比滑雪板加上雪杖來得實用。雪鞋可以固定在滑雪背包後面(如下圖所示)。\nsnowshoes - Google Search\n住宿 有錢的話，可以直接住八甲田纜車站附近的民宿或旅館，每天可以一早更快到纜車站排隊，但缺點是如果八甲田因為天候不佳停開的話，待在飯店會無處可去。\n另一個選擇是住在青森車站附近。雖然每天早上要坐一個小時左右的巴士，但是行程比較有彈性。如果遇上八甲田停開，可以臨時改去附近的一個小雪場 Moya hills。巴士只要 610 日圓，車程 30 分鐘左右。儘管比不上八甲田的過癮，但對於放假天數不多的人來說，能滑一天是一天。\n青森車站附近的住宿都不貴，一人一天大約是一千到兩千之間。如果想要在運動後泡泡溫泉，可以走到車站附近的青森市民溫泉，一次只要 450 日圓。嫌走來走去太麻煩的話，也可以直接住在青森市民溫泉的樓上(青森中心飯店)。\nAPA Hotel Aomori-Ekihigashi in Aomori\nToyoko Inn Aomori-eki Shomen-guchi in Aomori\n青森センターホテル＜公式＞【最安価格宣言】\n週邊景點 (unfinished) 青森縣立美術館 青森縣立美術館\n青森縣立美術館外景色\n淺虫溫泉 飯店外景色\n淺虫溫泉外景色\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%9D%92%E6%A3%AE%E5%85%AB%E7%94%B2%E7%94%B0%E6%BB%91%E9%9B%AA%E6%94%BB%E7%95%A5/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/b5ca8372f4bb/1_i3NnWz9Ii5JB2QJMKN-mdQ.jpeg\"\u003e\n\u003cem\u003e青森八甲田樹冰\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e滑雪滑這麼多年了，第一次滑到這麼特別的雪場。整個雪場只有一條纜車；雖然一次可以載一百個人，但每次排隊可能要等20分到1個多小時，一天有可能只滑得到三趟。儘管如此，每天前往朝聖的人還是絡繹不絕。究竟它有怎樣的魅力呢，下面會一一道來。我們先來看看怎麼前往這個神奇雪場。\u003c/p\u003e","title":"青森八甲田滑雪攻略"},{"content":"\n有時，雖然部分的 classes 或是 functions 是設為 public 的，但在產生 javadoc 文件時，我們卻希望不要將這些資訊呈現給看文件的使用者。一種可能的情況是，我們在程式碼中已經有實作某些新的功能，卻不希望使用者事前知道這些功能的存在。\n最基本的 javadoc 並不支援這樣子的功能，我們可以透過 doclava 這個 客製化的 javadoc doclet 來幫我們達成。\n首先要先在你的 gradle 文件中加入 dependency，並在 javadoc task 中多幾行設定。基本上就完成了。\n再來就是在你想要隱藏的 class 或 function 的 javadoc comment 上，加上 @hide 的 annotation。像是以下的例子：\nhide class\nhide function at line 17\n我在 github 上放了一個很簡單的 android 範例\n針對他產生出來的 javadoc 文件則可以在下面看得到：\n完整的文件，沒有套用 doclava doclet\n套用過 doclava 的文件：\n可以看得出來，兩份文件的長相其實並不太一樣。但至少，可以達到我們想要的功能了。\nGoogle Code Archive - Long-term storage for Google Code Project Hosting.\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%A6%82%E4%BD%95%E5%9C%A8-javadoc-%E4%B8%AD%E5%81%9A%E5%88%B0%E9%9A%B1%E8%97%8F%E9%83%A8%E5%88%86-public-function-%E5%92%8C-class-%E7%9A%84%E5%AE%9A%E7%BE%A9/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/848119471f66/1_NhLD_BIklQgsLk8jIDqmLA.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e有時，雖然部分的 classes 或是 functions 是設為 \u003ccode\u003epublic\u003c/code\u003e 的，但在產生 javadoc 文件時，我們卻希望不要將這些資訊呈現給看文件的使用者。一種可能的情況是，我們在程式碼中已經有實作某些新的功能，卻不希望使用者事前知道這些功能的存在。\u003c/p\u003e","title":"如何在 javadoc 中做到隱藏部分 public function 和 class 的定義"},{"content":" Chamonix, France\n通常在 Activity 中使用 WebView 時，會習慣在 onPause 裡頭，呼叫 WebView.onPause()，順著 Activity 的生命週期，將 WebView 順手停下來 ；然後在 onResume 來的時候，再呼叫 WebView.onResume() 讓 WebView 可以繼續運作。\n這樣子的實作，在一般情況下是很 Okay 的。但一旦你的應用程式支援 multi-window 模式，上述的實作很有可能會造成你的 WebView 畫面在進入 multi-window 模式時，變成白色畫面。\n觀察結果如下：\nView gist\n如果 WebView.onPause() 在 Activity 的 onPause (第 8 行和第 12 行) 就被呼叫的話，在 2. 裡頭的最後一步，系統會把畫面上方的 Window 又呼叫一次 onPause 。造成它雖然是在畫面上，但實際上狀態是 PAUSE 的。\n目前的解法是，如果你的畫面中有用到 WebView，你要把 WebView.onResume() 的呼叫時機改到 Activity 的 onStart()；然後 WebView.onPause() 延後到 Activity 的 onStop() 才做。\n更多閱讀\nMulti-Window Support | Android Developers\n5 tips for preparing for Multi-window in Android N\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%94%AF%E6%8F%B4-android-multi-window-%E6%A8%A1%E5%BC%8F%E6%99%82%E9%9C%80%E8%A6%81%E6%B3%A8%E6%84%8F%E7%9A%84%E4%BA%8B%E6%83%85/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/674194e67f1/1_ZkBhwEpKnZq5olcjUH1XLg.jpeg\"\u003e\n\u003cem\u003eChamonix, France\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e通常在 Activity 中使用 WebView 時，會習慣在 \u003ccode\u003eonPause\u003c/code\u003e 裡頭，呼叫 \u003ccode\u003eWebView.onPause()\u003c/code\u003e，順著 Activity 的生命週期，將 WebView 順手停下來 ；然後在 \u003ccode\u003eonResume\u003c/code\u003e 來的時候，再呼叫 \u003ccode\u003eWebView.onResume()\u003c/code\u003e 讓 WebView 可以繼續運作。\u003c/p\u003e","title":"支援 Android multi-window 模式時需要注意的事情"},{"content":"這支手機多了一個望遠的鏡頭，雖然像素比較低，畫質也沒有主要鏡頭好，但是跟一般只能拍標準視角的眾多手機比起來，就像是多了一雙能飛但飛不高的翅膀，稍微能帶你看看不太一樣的世界。\n在點擊畫面的 1X 後，會跳至 3X，隔個一兩秒，畫面會從有點模糊切換到比較清晰。這時表示已經啟動了望遠鏡頭。望遠鏡頭可以讓拍照更專注在被攝物，把畫面中不需要存在的雜物排除。比方說下面這張在日本祭典時拍的照片。馬路兩旁擠滿了看熱鬧的路人，正前方則是等待著出發的下個表演團體。如果用傳統手機拍攝，可能免不了會拍到一堆人頭，或是很多高舉的手機。但透過望遠鏡頭，就能輕易地避開這些干擾。\n另一種情況是，拍照的情境不允許你靠太近。比方說下面這張蝴蝶照，再靠近它就會機警地飛走。拍照時只能慢慢靠近，然後用望遠鏡頭多拍幾張，直到它飛走。\n搭飛機時的雲也是我很喜歡拍的題材之一。不過常常礙於機位的選擇，造成拍照時的困難。有時是機翼就在窗前，怎樣都避不掉；有時是窗戶某處太髒，怎麼拍都會有汙點；有時是窗戶反光很嚴重，畫面中總是會有過亮的區塊。\n有望遠鏡頭的話，讓拍照時更有彈性，只針對可以掌握的部分構圖。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%8F%AF%E7%82%BA-p20-pro-%E6%8B%8D%E7%85%A7%E5%BF%83%E5%BE%97-ii-%E6%9C%9B%E9%81%A0%E9%8F%A1%E9%A0%AD/","summary":"\u003cp\u003e這支手機多了一個望遠的鏡頭，雖然像素比較低，畫質也沒有主要鏡頭好，但是跟一般只能拍標準視角的眾多手機比起來，就像是多了一雙能飛但飛不高的翅膀，稍微能帶你看看不太一樣的世界。\u003c/p\u003e","title":"華為 p20 pro 拍照心得 II 望遠鏡頭"},{"content":"\n購買前就很期待它的夜拍功能，也很好奇被評價為 DxoMark 最高分的照片畫質。\n在拿到手機後，還沒摸熟功能就剛好有機會到東京一趟。正好可以試試它的拍照功能。\n首先是夜拍，真的如同各大評測文章說的，相當神奇。傳統夜間拍照通常建議開大光圈，降低曝光秒數，讓手持的晃動不會太過嚴重。但真的到了一定亮度以下，即使手機光圈開到最大，也無法避免手部晃動帶來的模楜感。再者，即使上了腳架，也會因為亮部比暗部亮很多，造成長曝後，亮的地方過亮，暗的地方也不見得夠亮。\n華為的夜間模式似乎是透過多重曝光的方式保留亮部細節，然後打亮小光圈時太暗的部分。最神奇的是，在拍攝的4秒中，儘管手會情不自禁地搖動，最終出來的照片卻是相當清晰。\n對於不愛拍夜景的我來說，現在總是會想在夜裡隨便朝著什麼拍一下。\n除了夜拍很好用之外，搭配三鏡頭的設計也讓 p20 pro 多了一個其他手機相機比較辦不到的功能：光學變焦，拍攝較遠的物體。點擊畫面中的 1X ，就可以快速切換到 3X 和 5X。如果不介意畫質稍差以換取更大的倍率，最多可以放大到 10 倍。\n講完好的地方之後，接下來要說說 p20 pro 不是那麼討喜的地方：顏色。\n雖然濃淡這回事，很見人見智；但我就是覺得它濃過頭了。尤其是在開啟 AI 模式下偵測到照片內容是藍天的時候，比方說下面這張：\n濃到變深藍，想要救回來也很困難。同樣的情況也發生在偵測為草綠時：\n即使沒開 AI Master，還是能很明顯看出色調被調整過的痕跡。只能在拍完後，再試著手動降低照片的彩度，讓它看起來自然一點。\n剩下的功能，就和其他手機差不多，有機會的話下次再分享。\n","permalink":"https://plateaukao.github.io/blog/posts/%E8%8F%AF%E7%82%BA-p20-pro-%E6%8B%8D%E7%85%A7%E5%BF%83%E5%BE%97/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/4e26fcc260a6/1_CE9x2mENUzUDvaF6qfSUQA.jpeg\"\u003e\u003c/p\u003e\n\u003cp\u003e購買前就很期待它的夜拍功能，也很好奇被評價為 DxoMark 最高分的照片畫質。\u003c/p\u003e\n\u003cp\u003e在拿到手機後，還沒摸熟功能就剛好有機會到東京一趟。正好可以試試它的拍照功能。\u003c/p\u003e\n\u003cp\u003e首先是夜拍，真的如同各大評測文章說的，相當神奇。傳統夜間拍照通常建議開大光圈，降低曝光秒數，讓手持的晃動不會太過嚴重。但真的到了一定亮度以下，即使手機光圈開到最大，也無法避免手部晃動帶來的模楜感。再者，即使上了腳架，也會因為亮部比暗部亮很多，造成長曝後，亮的地方過亮，暗的地方也不見得夠亮。\u003c/p\u003e","title":"華為 p20 pro 拍照心得"},{"content":"看到有人把 liff 的 API 包了起來，使用時不用再拿著 Access Token 去寫 curl command 了。但是他用的是 javascript，要用 npm 安裝。基於之前使用 npm 時常出現一大堆問題，所以我決定寫一版 python 版的。\nhttps://github.com/plateaukao/pylineliff/\n使用上很簡單，本來的 curl script 也沒有多複雜就是了。一開始要先呼叫下面指令，把從 https://developers.line.me/console/channel/\u0026lt;channel_id\u0026gt;/basic/ 拿到的 Channel Access Token 存到本地。\n./liff.py init \u0026lt;accessToken\u0026gt; 接下來就可以呼叫其他的功能了。\nlist 列出所有註冊過的 LINE LIFF apps\n$ ./liff.py list add 新增一個 LIFF app\n(type 可以是下列其中一種: full, tall, compact)\n$ ./liff.py add \u0026lt;url\u0026gt; \u0026lt;type\u0026gt; delete 刪除目前的某個 LINE LIFF app\n$ ./liff.py delete \u0026lt;liff-id\u0026gt; update 更新現有的 LINE LIFF app\n$ ./liff.py update \u0026lt;liff-id\u0026gt; \u0026lt;json_string\u0026gt; 參考資料 https://github.com/morugu/liff-cli\nplateaukao/pylineliff\n延伸閱讀 初探 LINE Message API 的新功能 LIFF\n","permalink":"https://plateaukao.github.io/blog/posts/pylineliff/","summary":"\u003cp\u003e看到有人把 liff 的 API 包了起來，使用時不用再拿著 Access Token 去寫 curl command 了。但是他用的是 javascript，要用 npm 安裝。基於之前使用 npm 時常出現一大堆問題，所以我決定寫一版 python 版的。\u003c/p\u003e","title":"LINE LIFF server API wrapper: pylineliff"},{"content":"在使用 Android Room 做 Query 時，有時不見得會找到想要的資料，所以當結果有可能是空的的時候，需要有個回傳值可以做後續處理。\n以下面的 Query 為例子，某個 id 的 Magazine 就不見得找得到。\n@Query(\u0026#34;SELECT * FROM bookmark WHERE id=:id\u0026#34;) fun getMagazine(id: String): Maybe\u0026lt;Magazine\u0026gt; 從 Single換成 Maybe 之後，處理方式就有點不同。Single 的話，只需要處理 onSuccess 和 onError 的情況， Maybe 要多處理沒有資料的情況，方式如下：\ndataSource.getMagazine(id) .doOnSuccess { // 有值的情況 } .doOnError { // 失敗的情況 } .doOnComplete { // Query 不到資料的情況 } .subscribe() ","permalink":"https://plateaukao.github.io/blog/posts/single-vs-maybe-in-rxjava2/","summary":"\u003cp\u003e在使用 Android Room 做 Query 時，有時不見得會找到想要的資料，所以當結果有可能是空的的時候，需要有個回傳值可以做後續處理。\u003c/p\u003e\n\u003cp\u003e以下面的 Query 為例子，某個 id 的 Magazine 就不見得找得到。\u003c/p\u003e","title":"Single vs Maybe in RxJava2"},{"content":"擴充 signature_pad javascript library Sky\n在開發 LINE Message API 的 LIFF 新功能時，藉由 signatue_pad javascript library 在 html canvas上寫字或畫圖。signaure_pad 把這件事包裝得很好，可以很容易地把 canvas 上的圖案輸出成圖片 (png 或 jpeg)，再傳送到自己想要儲存的地點。\n在官網上還有一個範例介紹了怎麼樣提供 Undo 的功能，程式碼如下\nView gist\n這裡的 data 是 Array of IPoingGroup，每 pop() 一次，就是把前一次的線條資料刪掉，再把 data 餵回 signaturePad 重新繪製。\nView gist\n由於 IPointGroup 中只包含了線條的顏色和線條所經過的點的位置，如果在每一次描繪線條時，都改變筆刷的大小，在 data 中其實並沒有記錄到當時的筆刷大小；這樣會導致 Undo 再重畫時，所有的線條全都成了同樣的粗細。\n舉個例子來說，我畫了 5 條線 (A, B, C, D, E)，分別是筆刷大小 1, 2, 3, 4, 5。這時我不想要第 5 條線 E，我執行了 Undo 功能，畫面重繪後只會剩下 A, B, C, D 4 條線，但是筆刷大小全成了 5。(因為最後一刻的筆刷大小設定是 5)\n也就是說， Undo 功能在有更換筆刷大小的情況下，會是有問題的。原本有用粗筆來填充畫面的話，一旦做了 Undo，就很有可能只剩下細細的一堆線。會讓人有種前功盡棄的感覺，需要全部再重新來一次。\n為了讓 Undo 在更換筆刷後，依然能正常運作，我把 signaure_pad 的 IPointerGroup 稍做更動，針對每一筆線條的資訊，加入當時筆刷大小的資訊。這樣子即使是事後重畫，也可以確保每一筆線條的原始大小。\nView gist\n針對這修正，我也發了 pull request 給 signature_pad 作者。只是該 repo 的更新似乎沒有很活躍，不知道要多久之後才會被看到。\n前情提要 初探 LINE Message API 的新功能 LIFF\n","permalink":"https://plateaukao.github.io/blog/posts/%E6%93%B4%E5%85%85-signature_pad-javascript-library/","summary":"\u003ch3 id=\"擴充-signature_pad-javascript-library\"\u003e擴充 signature_pad javascript library\u003c/h3\u003e\n\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/b659f1c2430a/1_rlNmoxbL1qKfmTBLB02YrQ.jpeg\"\u003e\n\u003cem\u003eSky\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e在開發 LINE Message API 的 LIFF 新功能時，藉由 s\u003ca href=\"https://github.com/szimek/signature_pad\"\u003eignatue_pad\u003c/a\u003e javascript library 在 html canvas上寫字或畫圖。signaure_pad 把這件事包裝得很好，可以很容易地把 canvas 上的圖案輸出成圖片 (png 或 jpeg)，再傳送到自己想要儲存的地點。\u003c/p\u003e","title":"擴充 signature_pad javascript library"},{"content":" 花蓮七星潭\n開發 Android app 時，一直是用 command line 模式在操作 adb。沒有什麼不好，但很多參數太久沒用，每次要再使用時都要得看一次說明，久了，會覺得有點煩。\n最近在 medium 上看到有人用 Java 開發了一套方便使用 adb 功能的圖形介面程式，而且是 Open Source 的，連結如下：\nyapplications/ADB-GUI\n安裝方式可以從 Github 上下載 zip 解開透過 java -jar ADB-GUI-Tool.jar 來執行，或是下載程式碼後，透過 Intellij IDEA 自行編譯。\n為了事後可以自己加新功能，我選擇了後者。編譯完啟動的畫面如下；畫面很工程師風格，沒有什麼多餘的美化。雖然有些操作上不是那麼直覺，但習慣之後，倒也沒有什麼大礙。\n主要功能有下面幾項：\n執行批次指令，像是自動登入 app 的帳號，或一次安裝多個 apk 和 copy 檔案。 截取畫面。(不過這功能現在在 Android Studio 中也很方便) 透過 Intent / Broadcasts 測試 deep linking。 從手機上擷取 apk。 查看和匯出 adb logs。 一鍵跑 Monkey。 雖然功能很多，但對我來說比較實用的應該是批次執行和下 Intent / Broadcasts 吧。稍微試了一下批次執行，想要啟動之前開發的書法加 app 起來，並查詢特定字串。\n結果一試便遇到了兩個問題：\n進入 app 後，要輸入字串前，必須先將 focus 設在 EditText，但透過單純的 adb 指令無法達到這件事。 書法加 app 畫面\n退而求其次，我在 ADB-GUI 中，新增了 adb shell touch 的動作。原先的 Commands Wizard 只有 Input text, Power, Back, Tab, Enter, Volume up, Volume down, Home, Recent 等動作。我照著 Input text 的作法，加了 touch position ( commit 在這兒)，然後多試著輸入幾次，就可以正確找到 EditText 的位置，把 focus 設到它身上。\n新增指令\n另一個問題是，adb 功能儘管很強大，但並不支援輸入中文字。書法加是一個用來查詢中文字的書法 app，輸入英文的話什麼也找不到。 為了解決無法透過 adb shell input text 指令輸入中文的問題，在網路上找到了 ADBKeyboard。完成下一段落的步驟後，終於可以自動執行下面的批次指令。\n在書法加 app 中搜尋「書法」的字帖\n這類簡單的操作，透過批次指令來進行，可以省去很多手動的不確定性和緩慢性，也不用特地寫程式。但如果要更進一步的和 app 互動，可能還是利用 espresso 或是 uiautomator 會更恰當。\nGithub 上的 ADBKeyBoard專案 senzhk/ADBKeyBoard\n解決無法透過 adb shell input text 輸入中文的問題：\n下載現成的 apk 或是取得原始碼自行編譯。\n安裝 apk，並進入 Settings -\u0026gt; Languages \u0026amp; Input 的管理輸入法畫面，勾選 ADB Keyboard，或是執行下面指令：\nadb shell ime enable com.android.adbkeyboard/.AdbIME 取得現在的輸入法，並記錄下來， adb shell settings get secure default_input_method --\u0026gt; net.toload.main.hd/.LIMEService 切換成 ADBKeyboard， adb shell ime set com.android.adbkeyboard/.AdbIME 輸入中文 adb shell am broadcast -a ADB_INPUT_TEXT --es msg \u0026#39;可以輸入中文了\u0026#39; 切回原本的輸入法 adb shell ime set net.toload.main.hd/.LIMEService 原始碼很小，只有一個檔案，裡頭實作了 BroadcastReceiver。原本編譯出來是 11 k，後來我把它改成 kotlin，結果 apk 變成了400多 k。好像有點多此一舉。\n","permalink":"https://plateaukao.github.io/blog/posts/%E9%97%9C%E6%96%BC-android-adb-%E4%BA%8C%E4%B8%89%E4%BA%8B/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/1f8627318d6f/1_Ley7dmpL7fUGk4RCNRnDWg.jpeg\"\u003e\n\u003cem\u003e花蓮七星潭\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e開發 Android app 時，一直是用 command line 模式在操作 adb。沒有什麼不好，但很多參數太久沒用，每次要再使用時都要得看一次說明，久了，會覺得有點煩。\u003c/p\u003e\n\u003cp\u003e最近在 medium 上看到有人用 Java 開發了一套方便使用 adb 功能的圖形介面程式，而且是 Open Source 的，連結如下：\u003c/p\u003e","title":"關於 Android adb 二三事"},{"content":" 小烏來\nLIFF 全名是 LINE Front-end Framework，一個可以在 LINE app 內運作的 web app 平台，可以在對話視窗中，不需要另外加入 bot 就直接使用，比 bot 更加方便﹙雖然目前還是有些限制，後面會提到﹚。但由於它才剛推出沒多久，沒有太多官方以外的線上資源，只能參考下面的日文文章，再加上自己摸索。\n昨天 (6/6) 在日本 LINE Engineering blog 看到了一篇在介紹 LIFF 實作範例的文章 (https://engineering.linecorp.com/ja/blog/detail/299)，之前剛好有申請了一個 channel，想說也來照著實作來看。\n簡單來說要建立一個 LIFF web app ，需要下面幾個步驟：\n建立一個具有 Message API 權限的 LINE channel。 取得 channel 的 token，用它來註冊 LIFF web app 取得一組 LIFF app id。 參考官方 Github 的範例，開始寫 code。 關於第一點，在這裡就不詳述了，有了 channel 之後，channel token 可以從 https://developers.line.me/console/channel/your_channel_id/basic/ 中的 Messaging settings 中取得，然後執行以下的指命，拿到 LIFF app id。\ncurl -XPOST \\ -H \u0026#34;Authorization: Bearer YOUR_CHANNEL_TOKEN\u0026#34; \\ -H \u0026#34;Content-Type: application/json\u0026#34; \\ -d \u0026#39;{ \u0026#34;view\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;LIFF_SIZE\u0026#34;, \u0026#34;url\u0026#34;: \u0026#34;URL_OF_YOUR_APPLICATION\u0026#34; } }\u0026#39; \\ https://api.line.me/liff/v1/apps YOUR_CHANNEL_TOKEN 剛剛取得的 channel token。\nLIFF_SIZE這個 web app 可以有三種大小，分別是 compact (佔畫面 50%)，tall (80%), full (100%)。\nURL_OF_YOUR_APPLICATION web app 實際的網址。\n一切順利的話，上面的指令很快會回傳一個 registration id 給你\n{\u0026#34;liffId\u0026#34;:\u0026#34;1234567890-XXXXXXXX\u0026#34;} 有了上面的 id，就可以在一般的 LINE 聊天視窗中輸入下面的連結，點下去就會開啟 LIFF 的畫面。\nline://app/1234567890-XXXXXXXX\n接下來，就可以照著上面的日文網誌一步步建立起 web app，或是直接 clone 文章最下面的官方範例，馬上可以看到 LIFF 所有支援的功能。\n下面就是改良版 painting 的 LIFF web app 畫面。\nsize 為 tall 的畫面大小\n我用的 LIFF_SIZE 是 tall，讓 web app 跳出時畫面夠大，又不致於蓋住整個聊天視窗。再經過一翻改良後，可以如下圖，輸出透明的字跡到聊天視窗中。比起呆板地輸入文字，用手寫的是不是更有誠意呢？\n左：手寫內容，點擊 Share；右：內容會被送出到對話視窗中。\n在文章一開始有提到，儘管 LIFF 可以在聊天室窗中直接使用，不需要事先加入 bot，但有個缺點是它一定要透過 url 被啟動。也就是說，在一個對話視窗中如果要啟動 LIFF，就得先把 LIFF 的 url 輸入，然後再去點擊它才行。隨著聊天視窗中的對話訊息不斷增加，原先輸入的 LIFF url，可能會被推擠到很上方。這時，可以透過 LINE 前一陣子推出的公告置頂功能來快速定位到 LIFF url，方便再次啟動。\n踩雷 手寫功能雖然很新奇，但如果每次想要輸入一樣的內容，都得再寫一次，那就不有趣了。所以我打算在原先的功能裡，增加可以顯示之前寫過的畫面。已經寫過的字，只要用選的就好，不用再重新寫一次，等於是有了自己的手寫圖庫。\n最初的想法是在最下方的按鈕列加上一個 List 按鈕，只要點擊它就會進到另一個網頁，拿著 LIFF javascript library 給我的 userId 去把這個使用者事前寫過的圖片都撈出來。所以我在 List 按鈕被點擊時，呼叫了下面的 javascript，進到另一個實作好的網頁。\nlocation.href = \u0026#39;./list\u0026#39;; 實際測試，畫面是切過去了沒錯，但跟 LIFF 相關的參數卻都抓不到，函式也呼叫不了。LIFF API 中有個 openWindow() 的函式，試了也一樣不行。看來 LIFF 被啟動時，就只能在當下使用 LIFF 相關的功能，一旦換了連結，即使是同一個 domain 下的 url，也無法再使用 LIFF 的 library。\n由於 LIFF javascript library 只有在 LINE 裡被啟動時才有作用，每次要測試時，一定要先把 server 上的程式碼更新好，然後進入 LINE 中實際操作看效果。如果有什麼功能不如預期般運作，不能用 Chrome debugger tool 來除錯，實在是很痛苦。只能不斷地 try and error。\n無奈之下，只好改變作法。點擊 List 按鈕後 ，把用來手寫的 canvas element 隱藏住，然後直接在按鈕列下面顯示之前手寫過的圖片。\n畫面很陽春，不過想要的功能完成了，可以很快速地從之前的圖片中點選一張，送出圖片。如果使用者是個很會畫圖的人，用了這個 LIFF web app，不用上架 LINE stickers 也可以很快地就產生出一組自己專屬的貼圖。\n結論 LIFF 可以讓開發者很快地實作出與使用者互動的 web app，從使用者端得到 input 後，把處理完的結果馬上顯示到當下的聊天視窗中。未來應該陸陸續續會有各種有趣的小應用出現。\n目前開發除錯不是很方便，如果能針對開發環境提供 LIFF dummy js library，讓開發者在沒有 LINE 的情況下，也可以很方便地測試 web app 功能，相信會有更多人願意開發相關的應用。\n補充 在某些朋友的 iPhone 上無法正常運作，後來調查之後，發現有些版本的 mobile safari 會對以下的 javascript 報錯：說程式中宣告了跟 global 一樣的canvas 變數會造成 shadow effetct，造成網頁無法再正常運作。後來把變數名稱改成 canvasElement 之後，朋友的 iPhone 就可以使用了。\n參考連結：\n官方開發文件 官方範例 Signature Pad 延伸閱讀 pylineliff\n","permalink":"https://plateaukao.github.io/blog/posts/%E5%88%9D%E6%8E%A2-line-message-api-%E7%9A%84%E6%96%B0%E5%8A%9F%E8%83%BD-liff/","summary":"\u003cp\u003e\u003cimg loading=\"lazy\" src=\"/images/51d5e7ff1a6a/1_L9UaB0W_2OaGhF71UX60EA.jpeg\"\u003e\n\u003cem\u003e小烏來\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eLIFF 全名是 \u003cstrong\u003eLINE Front-end Framework\u003c/strong\u003e，一個可以在 LINE app 內運作的 web app 平台，可以在對話視窗中，不需要另外加入 bot 就直接使用，比 bot 更加方便﹙雖然目前還是有些限制，後面會提到﹚。但由於它才剛推出沒多久，沒有太多官方以外的線上資源，只能參考下面的日文文章，再加上自己摸索。\u003c/p\u003e","title":"初探 LINE Message API 的新功能 LIFF"}]