この記事は、Pythonista3 Advent Calendar 2022 の25日目の記事です。
Introduction
本記事では、TweetDeckをスマホ版UI/UXで、Pythonista3から閲覧する方法を紹介します。
環境 / 前提
- iOS 16.1.1
- Pythonista3 v3.3
Python自体の文法は扱いませんが、コードの解説は気持ち程度行います。
背景: Twitterのインプレッション可視化問題
2022年12月下旬某日、Twitterのトレンドは「インプレッション」「閲覧数」「可視化」「欲求承認モンスター」etc...とスマホ版Twitterアプリが追加したとある機能で物議を醸していた!!!
先日、Twitterアプリではツイートの左下にインプレッションの数(?)が誰でも見れるようになりました。
これにより大きくは2つの不満がTLでは散見されました。
- いいねの数とインプレッションの差で悲しくなる
- 従来のリプライ, RT, いいねの位置がずれてしまって不便
個人的には後者の方が嫌ですね。
まぁTwitterが要らない機能を加えるのはよくあることなのでどうせ慣れる気はします。
とはいえ、他のクライアントアプリ1を使ってみたいなぁと思い調査してみました。
そこでクライアントアプリの代表ともいえるTweetDeckなるものに出会いました。
Chrome拡張機能のBetter TweetDeck, MultiRow TweetDeckなどと組み合わせると、PCでは色々とできてよい感じがします。
MTDeck - TweetDeckをスマホアプリ化
しかしながらTweetDeck本家は、スマホ版には対応していないとのこと2。
「TweetDeck スマホ」で${}^\dagger$Google 検索${}^\dagger$してみると、次のユーザースクリプトが見つかりました。
こちらのスクリプトをTweetDeckのページで読み込ませることで、スマホアプリのようなUI/UXを実現することができます。
TweetDeckをスマホアプリ化するユーザースクリプト、MTDeckの導入方法を詳しく解説などでは、一例としてAndroidではKiwi Browser, iOSではOhajiki Web BrowserやSafari Snippetsを紹介しています。
TweetDeck + MTDeckを実現するためには、必要な要件は次の2点になります。
- ブラウザでTweetDeckのページが開ける
- 開いていたページで用意したJavaScriptが読み込める
天才ワイ「これPythonista3でオッケーちゃうか?」
続いてPythonista3について軽く紹介します。
Pythonista3
Pythonista3とは、スマホでPythonを動かせる実行環境を兼ね備えたアプリです。
入力補完をはじめとしたコーディング体験, 文字の直感的な1文字単位のスライドなど、大きなところから小さなところまで様々な工夫が凝らされた、「僕が考えた最強のPythonコーディング環境 on スマホ」です。
簡易なアプリ・ゲーム作成, キーボード拡張, Widget作成まで可能とする、もはや意味不明(褒め言葉)なアプリです。
少しでも興味を持ったらタグやアドカレをご覧ください。
uiモジュール
Pythonista3では、uiモジュールという簡単にGUIアプリを作れるフレームワーク的なものが用意されています。
ui.WebView
は指定したhtml, またURLでページを表示するコンポーネントなので、これを使うことで外部サイトの閲覧が可能です。
しかしさすがにJavaScriptを読み込ませるのは...
WebView.eval_js(js)
WebView.evaluate_javascript(js)
Evaluate a snippet of JavaScript in the context of the current page and return the result as a string.
ありました。何気に返り値も取得可能なので、夢が広がります。
というわけで、この機能を用いてTweetDeck on Pythonista3を実現します。
TweetDeck on Pythoniosta3
DEMO
最小構成 (+ ちょっとだけ便利なメソッド )の成果物を示します。
↓ 実際の画面 ↓
↑ 実際の画面 ↑
コード
mtdeck.jsをPythonista3に追加します。
中身はMTdeck/dist/mtdeck.user.jsをコピーして貼り付けます。
TweetDeckOnPythonista3/
|- main.py
|- mtdeck.js
import ui
import time
TweetDeckUrl = 'https://tweetdeck.twitter.com/'
def getJavaScript(path):
with open(path, 'r', encoding='utf-8') as f:
s = ''.join(f.readlines())
return s
class WebviewDelegate:
def webview_should_start_load(self, webview, url, nav_type):
return True
def webview_did_start_load(self, webview):
self.js = getJavaScript('mtdeck.js')
def webview_did_finish_load(self, webview):
webview.eval_js(self.js)
def webview_did_fail_load(self, webview, error_code, error_msg):
pass
class TweetDeckBrowser:
def __init__(self):
self.webview = ui.WebView()
# Delegateを利用して遅延してJSを読み込ませる
self.webview.delegate = WebviewDelegate()
# 拡大縮小の操作をしない
self.webview.scales_page_to_fit = False
self.load()
def load(self, *args):
self.webview.load_url(TweetDeckUrl)
def closeSideMenu(self, *args):
js = '''if(!document.body.classList.contains("mtdeck-close")) { document.body.classList.add("mtdeck-close") }'''
self.webview.eval_js(js)
def goToPreviousColumn(self, *args):
js = 'window.MTD.backColumn()'
self.webview.eval_js(js)
def goToNextColumn(self, *args):
js = 'window.MTD.pushColumn()'
self.webview.eval_js(js)
class TweetDeckApp(ui.View):
def __init__(self):
super().__init__()
self.background_color = 'gray'
self.browser = TweetDeckBrowser()
self.add_subview(self.browser.webview)
# 画面上部のボタン定義
reloadBtn = ui.ButtonItem()
reloadBtn.image = ui.Image.named('iob:ios7_reload_32')
@ui.in_background
def reload(*args):
reloadBtn.enabled = False
self.browser.webview.reload()
time.sleep(1)
reloadBtn.enabled = True
reloadBtn.action = reload
self.right_button_items = [
reloadBtn
]
def layout(self):
_, _, w, h = self.frame
footer_height = h/15
# 全画面使うとサイトの下の方が見切れるので。
self.browser.webview.frame = (0, 0, w, h - footer_height)
if __name__ == '__main__':
app = TweetDeckApp()
app.present('fullscreen')
layout
メソッドとは
こちらが詳しいです。というかui.View
のカスタムViewの入門として最強だと思います。これ読みましょう。
TweetDeckBrowser
について
ui.WebView
をラップするようなクラスです。(サブクラス化させたかったけど禁止されている模様)
ここでポイントとして、JavaScriptの読み込みタイミングです。このスクリプトはサイト(HTML)が読み込まれた後に読み込む必要があります。本記事では、Delegateを用いて解決しています。
delegate
(少なくともuiモジュールでは)Delegateとは処理をお願いするオブジェクトのことで、ui.WebView
の場合は4つのメソッドを持つオブジェクトが該当します。
class WebviewDelegate:
def webview_should_start_load(self, webview, url, nav_type):
return True
def webview_did_start_load(self, webview): # 1
self.js = getJavaScript('mtdeck.js')
def webview_did_finish_load(self, webview): # 2
webview.eval_js(self.js)
def webview_did_fail_load(self, webview, error_code, error_msg):
pass
-
webview_did_start_load
: ページのロードが始まる直前にスクリプトを用意して -
webview_did_finish_load
: ロードが終わったら実行する
という動作を実装しています。
TweetDeckBrowser.goToPreviousColumn, TweetDeckBrowser.goToNextColumn
列を動的に切り替えます。
ここはJavaScriptの話になります。
MTDeckにはドキュメントなどはありませんが、コードの688行目をよく見ると、window.MTD
として外側に露出させているクラスがあります。
このクラスDeck
はDeck.pushColumn(), Deck.backColumn()
を持っており、このメソッドを使うことで画面に表示しているカラムを変えています。つまり、このメソッドをPython側から呼び出すことができれば、GUI側からカラムの変更を行えます。
このメソッドは単なる例であり、今回の実装ではどこからも呼び出していませんが、ui.WebView.eval_js()
のお陰でこのようなことが可能であるというお話です。
TweetDeckBrowser.closeSideMenu
もJSのコードを参考にして、サイドバーを閉じる処理を書いています。
可能性を感じませんか???
右上のボタン追加 : ui.ButtonItem
, right_button_items
よく考えたらこの辺の部品の日本語の説明見たことがないですね。そのうち記事を書くかもしれません。
今回の実装では、画面の右上にボタンを追加しています。
リロードするボタンを追加してみました。
class TweetDeckApp(ui.View):
def __init__(self):
super().__init__()
...
reloadBtn = ui.ButtonItem()
reloadBtn.image = ui.Image.named('iob:ios7_reload_32')
...
self.right_button_items = [
reloadBtn
]
@ui.in_background
については...
(自分の記事より引用)
uiなどのGUI表示部分の処理とは異なるスレッドで処理させるデコレーションです。多分。
ボタンのactionなどのコールバック関数にデコレーションをすると、別スレッドで処理が走ります。
よくわかりゃん!!の場合は、下記のようにアクションを行う部分に@でつけるとだけ覚えておけばよいです。
import ui
@ui.in_background
def btn_action(sender):
# 何か重い処理
pass
... # ui.Buttonのactionに`btn_action`を登録する
デコレータについて詳しくなりたい方は、Pythonのデコレータについてを参照してください。
Conclusion
本記事では、TweetDeck(+MTDeck)をPythonista3で動かす方法を紹介しました。
Pythoista3で動かすことで、次のメリットがあります。
- Python, JavaScript, HTML, CSSの理解が深まる
- uiモジュールの理解が深まる
- GUIからJSを実行することで、挙動を改造できる
- CSSを簡単にinjectできる (今回の実装では触れていません。)
Pythonista3自体が有料ではありますが、Pythonを書く人はもれなく購入すべきアプリですので、これを機会にお試しあれ!
Reference
Appendix
Discord
Pythonista3 の日本語コミュニティーがあります。みなさん優しくて、わからないところも親身に教えてくれるのでこの機会に覗いてみてください。