NotionのデスクトップアプリをオリジナルのJavaScriptで改造する (2021年4月版)
2年ぶりくらいにやってみたら少しだけやりかたが変わっていた(簡単になっていた)のでまとめ直し。
MacOS向けですが、Windowsとかでもたぶん同じです。
①Notionアプリの内容を開く
ApplicationフォルダにあるNotionアプリの内容を開きます。
②preload.jsを開く
場所は以下のスクショのとおり。
③inPageSearchのstopのところにからオリジナルスクリプトを呼べるようにする
以前(1~2年前)と変わらずここに仕込むとページ遷移ごとに呼ばれていい感じにユーザースクリプトを注入できます。
④好きなところにオリジナルスクリプトを書く
例えばタイムラインのアイテムに"🔥"マークがあったら赤くする」としたらこんなコード。
これコードなぐり書きでめっちゃ汚いですけど、初心者のために貼ります。
上級者はもうここまでで十分だと思うのでコード読まないでください。
ていうか5分で書いたし自分用だしめっちゃ適当です。本気出したらもっとエレガントになります。
// ===================================================================================== // ↓ MY NOTION HACK ↓ // ===================================================================================== var myNotionHack_onPageChange = function(){}; var TITLE_DIV_HACK_CLASS = "TITLE_DIV_TO_HACK"; var INJECTION_STYLE_TAG = (function() {/* <style id="myHackStyle"> .TL_ITEM_LEVEL_FIRE { background: red!important; } </style> */}).toString().replace(/(\n)/g, '').split('*')[1]; // タイムラインのハック function appendOriginalClassesToTimelineItems() { console.log("appendOriginalClassesToTimelineItems"); var getTimelines = function () { return document.querySelectorAll(".notion-timeline-view"); } var getRows = function () { return document.querySelectorAll(".notion-timeline-item-row"); } var appendLevelClassToItem = function (rowDiv) { var itemDiv = rowDiv.querySelector(".notion-timeline-item"); var propDivs = rowDiv.querySelectorAll(".notion-timeline-item-properties > div > div"); isAppendded = false; propDivs.forEach(function(propDiv) { if (propDiv.innerText.indexOf("🔥") >= 0) { if(!itemDiv.className || itemDiv.className.indexOf("TL_ITEM_LEVEL_FIRE") == -1) { itemDiv.classList.add("TL_ITEM_LEVEL_FIRE"); isAppendded = true; } } }); return isAppendded; } // MAIN var timelines = getTimelines(); if (timelines.length == 0) { console.log("timeline is not found") return false; } var isAppendded = false; var rows = getRows(); rows.forEach(function(row) { if (appendLevelClassToItem(row)) { isAppendded = true; } }); return isAppendded; } (function MY_NOTION_HACK_SCRIPT() { function us_injectStyleTag() { if (document.querySelector("#myHackStyle") == null) { document.body.insertAdjacentHTML('afterbegin', INJECTION_STYLE_TAG); return true; } return false; } function polling(times, action) { for (var i=0; i<times.length; i++) { setTimeout(action, times[i]); } } myNotionHack_onPageChange = function() { var isCompleteStyle = false; var isCompleteTimelineHack = false; polling([100, 200, 300, 350, 500, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 150000], function() { if (!isCompleteStyle) { isCompleteStyle = us_injectStyleTag(); } if (!isCompleteTimelineHack) { isCompleteTimelineHack = appendOriginalClassesToTimelineItems(); } }); } })(); // ===================================================================================== // ↑ MY NOTION HACK ↑ // =====================================================================================
④結果
例えば結果はこんな感じ。 デバッグツールはChromeとかと同様にアプリ内で cmd + opt + i とかで開けます。
Notionのカレンダーを見やすくハックする
先日、このような記事を書きましたが、これを生かして新たにNotionの個人的な課題を解決してみました。
個人的に困っていたのは、 タスクボードのカレンダーViewで強弱がつけづらく、すべての予定が均等の強さに見えてしまうことでした。
小さなタスクも重たいタスクと同様に表示されてしまうので、ぱっとみですごく焦ります。
以前はRealtimeBoardで1週間の予定を管理していたためそのあたりの表現は自由自在だったのですが、Notionに切り替えてから毎日ソワソワしてカレンダーViewを見つめてしまうようになり、とても気になりました。
そこで、こんなふうにLevelプロパティを追加し、レベルを選択することでそのタスクの重さを表現できないかと考えました。
Notionはほとんどの要素がdivでクラス名も無いため少し大変でしたが、 先日の記事と同じ手法を使ってこんなふうに強弱をつけるユーザースクリプトを実装できました。
ハック後はだいぶ見やすくなったように思います。
NotionのDesktopアプリのUIをカスタマイズするハック
最近Notionというツールを[ノートアプリ + タスク管理アプリ]として乗り換えてみたところ、めちゃくちゃ便利で毎日使い倒しています。
ただNotionってタイトルと絵文字がめっちゃでかく表示されておしゃれなのですが、職場だとそれがなんだか恥ずかしかったので小さくしたくなりましたw
そこで、Notionのデスクトップ版にカスタムのJavaScriptを当てる方法を模索してみました。
Electronを触るのは初めてでしたが、結果的にはユーザースクリプトを流し込み、常に自分好みのUIに調節することに成功しました!
※Macでのやり方を説明します。
1. NotionアプリからDevToolを開き、preload.jsの場所を確認する
Electronが提供しているwebviewタグというものがあり、
その中にWeb版のNotionを表示しているようです。
属性要素をよく見ると、preload.jsというものが読み込まれています。
(おそらくこれもElectronではデフォルトの方法でしょうか)
僕の場合は
file:///Applications/Notion.app/Contents/Resources/app.asar/renderer/preload.js
でした。
2. preload.jsを編集するため、app.asarファイルを解凍する
app.asarファイルを解凍するためには、はasarというコマンドラインツールのインストールをしておきます。
npm install -g asar
以下のように解凍できます。
asar e app.asar myApp
3. preload.jsにユーザースクリプトを追記する
そこにJavaScriptを書けばDOMにアクセス可能なので、UIの変更など基本的になんでもできます。
Notionのページ遷移イベントのハック
Notionの場合はページ遷移時のイベントがわからなかったので、以下のようなスクリプトでElectronのipcメッセージを確認したところ、それに近いイベントが見つかったので、それを利用することにしました。
webview.addEventListener('ipc-message', function(event) { console.log(event); });
4. preload.jsを圧縮し、置き換える
asar pack myApp app.asar
5. アプリにリロードをかけて確認
アプリを⌘Rなどで更新すれば変更が反映されているはずです。
※ WebviewのDevToolを開く方法
以下のようなコードをNotionデスクトップアプリのDevToolで実行することでWebviewのDevToolを開くことができます。
document.querySelector('webview').openDevTools()
ハック後のUI
Embedded Frameworkの導入の検討 / firefox-iosの例
個人的なアプリでは導入してみていますが、途中から入った現在のプロジェクトでも導入を検討しているので、色々と整理してみました。
Embedded Frameworkを取り入れる利点
ビルドパフォーマンスの改善
EmbeddedFrameworkを導入することで差分コンパイルが効きやすくなるため、ビルドの時間を減らすことに大きく貢献します。
そのためビルド時間短縮の有効な手段の1つとしてよく取り上げられています。
本格的なiOS開発では、ビルドパフォーマンスを意識せずに開発を続けていくと大抵の場合フルビルドに8分~15分くらいかかるようになっていきます。しかもメソッドを1つ追加するだけでも全体をビルドし直しになったりします。
Swiftはコードが増えてくると凄く生産性が悪くなるのでビルド時間の短縮化は開発者の間でよく取り扱われるテーマの1つとなっています。
ウィジェットなどとの間で機能を共通化できる
iOSアプリ本体に加えて、ウィジェットやシェア拡張などのAppExtensionを開発する場合やiPad向けアプリやAppleTV向けアプリやWatch向けアプリを開発する際、ビルドターゲットはアプリとは別になります。
ターゲットの切り分けをしていない場合、 TodayウィジェットからAPIを叩く必要があるときは、アプリのコードを使用することができず、結果、二重にAPIへのリクエスト部分を実装することになってしまいますが、 機能がEmedded Frameworkとして切り離されていればアプリとウィジェット間で共通の機能を使うことが出来ます。
コードがキレイになる
依存がはっきりとわかれて、名前空間も別れるので簡潔なコードが書きやすくなります。
ターゲットを分けずにアプリ開発している場合はアクセス修飾子のinternal / public の違いがなくなりますが、 フレームワーク内ではinternal/publicが意味を持つようになり、より安全に分かりやすく開発していくことが可能です。 ターゲット内でのみ使えるinternalなクラスを作ったり、外部に公開するクラスやメソッドを限定的にしてモジュールを使いやすく分かりやすいインターフェイスにしていけます。
Embedded Frameworkの作り方
参考になりそうな記事を貼っておきます。
Embedded Frameworkを導入してXcodeのビルドパフォーマンスをあげる - VASILY DEVELOPERS BLOG
Embedded Frameworkの分け方の例
以下のように層ごとで分けることが多いかと思います。
メインターゲット
- iOSアプリ
- ウィジェットなど
- Apple Watchアプリ
Embed Framework
- Util (全体で使う汎用的な機能やExtensionなど)
- APIClient
- Model
- in-App-Purchase機能
mozilla-mobile/firefox-ios の例
embed frameworkを取り入れている例として firefox-iosのオープンソースプロジェクトをざっくり観察してみました。
ターゲットのわけ方
メインターゲット
- Client(iOSアプリ)
- ShareTo(ShareExtension)
- Today(TodayExtension)
- NotificationService(NotificationExtension)
- SendTo(ActionExtension)
- ViewLater(ActionExtension)
Embed Frameworks
- Shared: 汎用的なextensionやUtil系クラス
- Storage: ブックマークなどの永続化層?
- Account: アカウント管理?
- Sync: ブックマークなどの同期?
- ReadingList: リーディングリストの管理?
- Telemetry: 効果測定?
Model,APIClient…というふうに層で分けるのもいいですが、firefoxのように機能ごとに分けるのもよさそうですね。
ライブラリの管理をどうしているのか
Carthageで取得してきたframeworkたちが一番上の階層のFrameworksフォルダにまとめられており、各ターゲットがおのおの必要なフレームワークを取り入れていました。
具体的には各ターゲットの”Linked Framewoks and Libraries”, ”Linked Binary with Libraries” に使用するframeworkを追加して、使用しているファイル内でimport文を追加しています。
EmbedFrameworkを使いすぎると起動が遅くなる件について
EmbedFrameworkを使いすぎると起動が遅くなるそうです。 (参考資料: More Swift App Startup Time)
つまりEmbedFrameworkを使用してビルド時間を短縮することとアプリの起動時間が伸びるのはトレードオフとのこと。 (参考資料: iOSプロジェクトのBuildを高速化する)
しかし実際 firefoxのアプリの起動時間を体感してみたところそこまで起動の遅さが感じられなくて安心しました。 むしろ平均以上の速さのように感じました。
firefoxのEmbeded Frameworkの数
- 外部framework 23個
- 内部framework 6個
このframework数でiPhone6sで250msほどで起動していました。
firefoxのEmbeded Frameworkの数は少なくもなく多くもなく通常のアプリ開発はこのくらいの数になると思います。
これで充分に起動時間が速いので、過剰な数のEmbeded Frameworkを使いさえしなければ起動時間の伸びについては特に問題になることはなさそうだと思いました。
まとめ
Embeded Frameworkは利点が多く、ウィジェットの開発を視野に入れていたりビルド時間を肥大化させたくない場合は早めに導入できると良さそうだと思いました。
firefox-iosは Embed Frameworkを活用している例としては貴重で参考になりました。 大手のプロジェクトでEmbed Frameworkを活用した例があると安心感があります。 僕の会社でも最近のiOSプロジェクトはEmbed Frameworkを導入いる例がいくつかあるので参考にしてみたいと思いました。
導入時期の検討
Embed Frameworkの導入はアプリ開発が進むほどにコストが高まっていきます。
僕のアプリの場合はリリースが近くなってしまったので、今導入するとデグレの危険性があるためリリース直後くらいに行えたら良いなと思っています。
JavaScriptで定期的にWi-FiをOFF➜ONする
僕は普段はSwiftを書いているのですがインスタントな雑スクリプトはJavaScriptで書きます。
カフェでドヤマックしてたのですがWifiの調子が悪くて定期的にon/offしていたので、それを自動化したいと思い雑なnodeJSを書きなぐりました。
Wifiを「切」にして3秒後に「入」にする`を60秒ごとに行うスクリプトです。 Shell Commandをタイマー実行しているだけですが。
toggleWifi.js
const exec = require('child_process').exec; var intervalSec = 60; function main() { console.log("start to toggle wifi"); setInterval(function() { toggleWifi(); }, 1000 * intervalSec) toggleWifi(); } function toggleWifi() { console.log("toggle..."); changeWifiPower(false); setTimeout(function() { changeWifiPower(true); }, 1000 * 3); } function changeWifiPower(isOn) { var nextState = isOn ? "on" : "off"; var command = "networksetup -setairportpower en0 " + nextState; doShellCommand(command); console.log("wifi turn " + nextState); } function doShellCommand(command) { exec(command, (err, stdout, stderr) => { if (err) { console.log(err); } console.log(stdout); }); } main();
使い方
$ node toggleWifi.js
WKWebViewのリダイレクト時の挙動
WKWebViewのリダイレクト周りの挙動が分からなかったので調べてみました。 (iOS10 / iOS9) (Swift3.2)
リダイレクトはWKNavigationDelegateで検知するしかなさそう
自分の実験の中ではWKWebViewではリダイレクトのステータスコード(300系)を観測できなかった。 どうやらWKWebViewが内部的にリダイレクトは処理してしまっているようだったので、 WKNavigationDelegateによってそのリダイレクトによる遷移を検知した。
主要なWKNavigationDelegateのメソッド
func webView(WKWebView, decidePolicyFor: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void)
Decides whether to allow or cancel a navigation.
- リクエストを開始するときに呼ばれる。そのまま遷移をさせるか、キャンセルするかを決める。
- リダイレクトが起これば連続してよばれる。
- navigationTypeを見るとクリックよる遷移か、リダイレクトによる遷移かがわかる。
func webView(WKWebView, decidePolicyFor: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void)
Decides whether to allow or cancel a navigation after its response is known.
- レスポンスヘッダが返ってきたときに呼ばれる。そのまま遷移をさせるか、キャンセルするかを決める。
- このあとにリダイレクトでまたnavigationActionが呼ばれることもあるので、ここで終わりとは限らない。
func webView(WKWebView, didCommit: WKNavigation!)
Called when the web view begins to receive web content.
- リダイレクトが終わってコンテンツのロードが始まった時に呼ばれる。
リダイレクトの流れ
リダイレクトが終わるまで
webView(WKWebView, decidePolicyFor: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void)
と
webView(WKWebView, decidePolicyFor: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void)
が
連続で呼ばれ続け、
リダイレクトが終わってコンテンツのロードが始まると
webView(WKWebView, didCommit: WKNavigation!)
が呼ばれる。
気をつけること
- decidePolicyFor: WKNavigationResponseが呼ばれても終わりとは限らない。さらにそのままdecidePolicyFor: WKNavigationActionが呼ばれることがあり、didCommitが呼ばれるまで終わりとは限らないので気をつける。
- didCommitが呼ばれて初めてリダイレクトが終了したとわかるが、Webコンテンツのロードが始まらない限り最後のURLがわからない。
P.S.
その他のWKNavigationDelegateのメソッド
didStartProvisionalNavigation
とdidReceiveServerRedirectForProvisionalNavigation
は呼ばれない事もあったのでよく分かっていない・・・
func webView(WKWebView, didStartProvisionalNavigation: WKNavigation!)
Called when web content begins to load in a web view.
これが呼ばれるとリダイレクトがおわりURLが決定しているようだった。これのあとに最後のリクエストかレスポンスがきた。
これのあとにリクエストが来た場合はそれが最後のリダイレクトなのか、そのあとにdidReceiveServerRedirectForProvisionalNavigationが呼ばれ、レスポンスヘッダがきた。
※ なぜか、呼ばれないこともあった…
func webView(WKWebView, didReceiveServerRedirectForProvisionalNavigation: WKNavigation!)
Called when a web view receives a server redirect.
リダイレクトされたときの最後のリクエストのあとで呼ばれ、そのあとにレスポンスがきた。
※ なぜか呼ばれないこともあった…
発表しました。「Swiftがだいたい読めるようになるセッション」
先日、社内勉強会でエンジニアがiOSを雰囲気で読めるようになるのを目指すLTをさせていただきました。
Swiftはとても読みやすい言語なので、普段読んだことのないエンジニアの人でも要所さえ抑えてしまえば言語自体は簡単に読めると思います。
加えてiOSアプリケーション開発の全体像を掴めば、iOSプロジェクトがだいたい雰囲気読めるようになるのではないかと思いこの発表をしてみました。
リーディングの題材としてはこちらのオープンソースプロジェクトをお借りしました。 少しリファクタしましたが、手頃なサンプルプロジェクトを1から作る時間が無かったので助かりました。 github.com
最近はAndroid界隈でKotlinというSwiftにとても似てる言語が普及している流れも有りAndroidエンジニアとiOSエンジニアでお互いにコードを読みやすくなっているので、僕もそろそろKotolinのAndroidを雰囲気で読めるようになっておきたいところです。