0
0

More than 1 year has passed since last update.

kivyMDチュートリアル其の参什陸 Components - NavigationDrawer(Twitterクローンもどきもあるよ)篇

Posted at

ようこそ、KivyMDの世界へ!

さて、〇〇食堂の始まり方のような感じで始まりました。KivyMDのお時間です。

というか初月ですね。これ書いててふと気づきました。困ったな初月には時候の挨拶
をすることにしてたのに(すごくどうでもよい)。ま、一旦来週にすることとします。

今週もこれといったことはなかったような気もしますが、あるとするならば総裁選の
こととかになるでしょうか。でも、政治の知識が皆無に等しいので話題の提供は出来
ないというのが現状ですが...でも日々生活に根付いて大事なことは間違いありません。
エンジニアは高給だから税導入するよーとかなればえらいこっちゃとなります(ならない)。

ということで、最初のアプリは政治系にしようかな(難しそう)。なので早くアプリを作る
ためにも、KivyMDにガツガツ触っていこうと思います。今日は個人的には待望であった
NavigationDrawer篇となります。それでは、えいえい、えいー。

Twitterクローンでホイホイ来られた方は、このページの1番最後に動かした様子を まとめております。なので、そこだけ見たいんだよという方は途中にマニュアル解説 みたいなことをやってますので、それを吹っ飛ばして最下部まで移動をお願いします。 ※ Twitterクローンもどきと書いているくらいなので完成度は全然ないと先に思ってもらえたら

NavigationDrawer

気合いを入れたところで、マテリアルデザインのリンクは飛ばします。こればかりは譲れ
ません。ですが、ためになることばかりなのでお時間ある方は見てもらえればと思います。

私が言うまでもなく、簡単に言えば折り畳めるサイドバーということになるでしょうか。
マニュアルの方では、以下のように説明がありますね。

Navigation drawers provide access to destinations in your app.

厳密に説明すると、アプリの中で目的地点へ到達するためのウィジェットと言えるでしょ
うか。というか、私の記載したことはなかったことにしてください。

先週とかでも少し触れた、ハンバーガーメニュー(3本線のアイコン)とともに使われること
が多いですね。というか、すでにこのセット(ハンバーガーと言っていてややこしい)は確立
されていると言っても過言ではないでしょうか。

少しこの話題になると、長くなるのでそれほど触れませんが、結構調べてみるとハンバーガー
メニューは古い!みたいなことにもなっているのですね。デザインの最先端ではもう使用しない
といったところもあるでしょうかね。全然その世界にいない私はほぇーとなりましたが。
# 併せてNavigationDrawerは使用しなくBottomNavigationを使用した方がいいみたいな人もいて

ということで色々物議を醸し出しているところですが、本当にそう言われていることが妥当
かどうかも見ていきたいと思います。少し前置きが長くなりましたが、一旦使用方法を。

Usage(架空)

タイトル付けはされていませんが、区切りもよいということで一応付けてみました。
マニュアルの方では続けてこのように記載があります。

When using the class MDNavigationDrawer skeleton of your KV markup should look like this:

Root:

    MDNavigationLayout:

        ScreenManager:

            Screen_1:

            Screen_2:

        MDNavigationDrawer:
            # This custom rule should implement what will be appear in your MDNavigationDrawer
            ContentNavigationDrawer

ここまではシンプルですね。まずルートウィジェットがあり配下にMDNavigationLayout
が配置されています。で、そのまた配下にScreenManagerとMDNavigationDrawerがさら
に配置される形となります。

まず、ScreenManagerですが以前にもさらっと扱っているものがありますので、そちら
もしくはKivy公式マニュアルをご参照してもらえればと思います。簡単に言えば画面の
切り替えということになりますでしょうか。

あとはMDNavigationDrawerですが、こちらが折り畳む・開く動作をするナビゲーション
のウィジェットになります。その中にウィジェットをそれぞれ定義するもしくはカスタムで
定義するウィジェットを配置します。サンプルコードではそれぞれ書き方があります。

この形としては少し似ているとして、Backdropウィジェットが挙げられるでしょうか。
そのときはフロント・バックレイヤーを分けていましたね。え、そんなの知らないよという
方はこちらでもよいですが、公式マニュアルをみた方がいいかもしれません。理由は、はい、
なんとなくわかるよね...?

A simple example:

では、さっそく簡単なサンプルコードを見てみましょう。

xxxvi/simple_navigation_drawer.py
from kivy.lang import Builder

from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.app import MDApp

KV = '''
MDScreen:

    MDNavigationLayout:

        ScreenManager:

            MDScreen:

                BoxLayout:
                    orientation: 'vertical'

                    MDToolbar:
                        title: "Navigation Drawer"
                        elevation: 10
                        left_action_items: [['menu', lambda x: nav_drawer.set_state("open")]]

                    Widget:


        MDNavigationDrawer:
            id: nav_drawer

            ContentNavigationDrawer:
'''


class ContentNavigationDrawer(MDBoxLayout):
    pass


class TestNavigationDrawer(MDApp):
    def build(self):
        return Builder.load_string(KV)


TestNavigationDrawer().run()

ここでも使用方法の通りで特にとりとめもありませんが、まず変わったこととしては
以下の部分が加わったことになりますかね。

BoxLayout:
    orientation: 'vertical'

    MDToolbar:
        title: "Navigation Drawer"
        elevation: 10
        left_action_items: [['menu', lambda x: nav_drawer.set_state("open")]]

    Widget:

こちらはサンプルとして、BoxLayout配下がくっ付いてきたということでしょうか。
配下にはMDToolbarとWidgetがあります。MDToolbarについては以前にも取り扱っ
ているのでそちらもしくは公式マニュアルの方をご参照頂ければと思います。

ずいぶん以前からWidgetがところどころいたりしますが、これは何なんでしょうかね。
いまだによく分かっていない状況です。と思っていたら、いつもお世話になっている
せなさんのブログのページに行き着きました。

たしかにWidgetはウィジェットということは分かるのですが。それだけを配置する
意味などがよく分からないということが厳密な説明となります。KivyMD特有の書き方
というか少しこれは語弊がありそうですが、理由は不明という状況です。私もまだまだ
ですね。

で、あとはKV側ではMDNavigationDrawerにID値が設けられて、これがMDToolbar
のハンバーガーメニューのアクションと結びついている状況です。あとは自明のこと
ですが、ContentNavigationDrawerクラスではMDBoxLayoutを継承しているという
ことも。

結果

さて、インポートなしのkv・クラス側を一気にまとめてみましたが、いかがでしょうか。
使い方としてはこれ以上触れることも出来ないので、ここで一旦動かしてみたいと思い
ます。

169.png

170.png

うん、マニュアルと同じですね。問題ありません。
あとは、忘れてはいけませんがインフォメーションがマニュアルに記載されてあります。

MDNavigationDrawer is an empty MDCard panel.

ほぅ、そう来ましたか。。実質MDCardだったとは。

Extend the ContentNavigationDrawer class

適当な節分けをしたかったので、以下マニュアルの記載から節を無理くり作って見ました。

Let’s extend the ContentNavigationDrawer class from the above example
and create content for our MDNavigationDrawer panel:

ですが、ここのパーツで小分けされて記載されているのに、全体としてはGitHubの方に集約
されている状況となります。先にコードの全体像を見たい方はこちらを見てもらえればと思い
ます。

# Menu item in the DrawerList list.
<ItemDrawer>:
    theme_text_color: "Custom"
    on_release: self.parent.set_color_item(self)

    IconLeftWidget:
        id: icon
        icon: root.icon
        theme_text_color: "Custom"
        text_color: root.text_color
class ItemDrawer(OneLineIconListItem):
    icon = StringProperty()

まずこちらは何を記載しているのかというと、ItemDwawerレイアウト(ウィジェット)・クラス
になりますね。OneLineIconListItemを継承しているので、theme_text_colorだとかon_
releaseイベントなどを使用できています。で、その配下にIconLeftWidgetを配置しています。
なんのこっちゃ分からん!となっている方は以下リンクもしくはマニュアルの方を参照頂ければと
思います。

ちなみにですが、IconLeftWidget - text_colorについては公式アカウントのGitHubの方
では、以下のように定められています。

class ItemDrawer(OneLineIconListItem):
    icon = StringProperty()
    text_color = ListProperty((0, 0, 0, 1))

というかなんでマニュアルの方で書いてないんだろう。。

で、ディレクトリらしき画像あたりに進んで、以下のような記載があります。

Top of ContentNavigationDrawer and DrawerList for menu items:

さらにコードがあり、2つのレイアウト・クラスの抜粋が記載されています。

<ContentNavigationDrawer>:
    orientation: "vertical"
    padding: "8dp"
    spacing: "8dp"

    AnchorLayout:
        anchor_x: "left"
        size_hint_y: None
        height: avatar.height

        Image:
            id: avatar
            size_hint: None, None
            size: "56dp", "56dp"
            source: "kivymd.png"

    MDLabel:
        text: "KivyMD library"
        font_style: "Button"
        size_hint_y: None
        height: self.texture_size[1]

    MDLabel:
        text: "kivydevelopment@gmail.com"
        font_style: "Caption"
        size_hint_y: None
        height: self.texture_size[1]

    ScrollView:

        DrawerList:
            id: md_list
class ContentNavigationDrawer(BoxLayout):
    pass


class DrawerList(ThemableBehavior, MDList):
    def set_color_item(self, instance_item):
        '''Called when tap on a menu item.'''

        # Set the color of the icon and text for the menu item.
        for item in self.children:
            if item.text_color == self.theme_cls.primary_color:
                item.text_color = self.theme_cls.text_color
                break
        instance_item.text_color = self.theme_cls.primary_color

まずそれぞれのクラスは1つないし2つのクラスを継承していますね。さらにContent-
NavigationDrawerレイアウトに関しては、これは何を指しているかというとヘッダー
部分 + 複数リストのところになりますね。大まかに言えば、画像(AnchorLayout配下)
- ラベル × 2 - 複数リストの構成となっています。なんかAnchorLayoutという見慣
れないものがありますね。くそう、やることが積み重なっていく。。
# ちなみにですがContentNavigationDrawerはカスタムドローワーにもなっています

というどうでもよいことは置いておいて、レイアウトに関してはLayout・Label・List
篇もしくはマニュアルを参照頂ければ、すんなり入ってくるかと思います。さっき掲載した
List篇以外のリンクを貼り付けておきます。

で、クラスの方ですが、こちらはそれぞれのメニューアイテムをクリックしたときにメニュー
の色を変更するロジックを埋め込んでいます。いやいや、どんな仕組みよと思われるかもしれ
ませんが、ここは丁寧にいきましょう。

まず、こちらのメソッドですが、自明の通りset_color_itemとあります。これはどこで呼ば
れているかと言われると、先ほどのItemDrawerウィジェットでした。そちらの様子を再掲し
ます。

# Menu item in the DrawerList list.
<ItemDrawer>:
    theme_text_color: "Custom"
    on_release: self.parent.set_color_item(self)

()

ありましたね。なんとItemDrawerのコールバックメソッドで定義をされていました。いや、
なんでこっちで定義するのよ、DrawerList側でいいじゃないという疑問もあるかと思われ
ます。(ないとは言わないで) これは、なんでかと言われるとまずself.perent.~(self)
というところに注目です。ですが、ここは一旦次の抜粋コードを踏まえると分かり易いので
一旦疑問を持ったまま次へいきます。

Create a menu list for ContentNavigationDrawer:

メニューの作り方、と言わんばかりの引用を踏まえながらコードの抜粋をしていきます。

def on_start(self):
    icons_item = {
        "folder": "My files",
        "account-multiple": "Shared with me",
        "star": "Starred",
        "history": "Recent",
        "checkbox-marked": "Shared with me",
        "upload": "Upload",
    }
    for icon_name in icons_item.keys():
        self.root.ids.content_drawer.ids.md_list.add_widget(
            ItemDrawer(icon=icon_name, text=icons_item[icon_name])
        )

もう先週のMenu(続)篇やList篇をご覧戴いていればなんのことはないですよね。見てない
方は以下のMenu(続)篇や先述List篇をご覧いただければと思います。

これで先ほどの疑問が解消されることとなります。まず先ほどの疑問としてはon_release
コールバックメソッドがDrawerList側で定義されていないのかということでしたが、ここ
に鍵があります。まずレイアウトとしてはmd_list(DrawerList)にItemDrawerを入れ
込んでいるので、以下のようになります。

md_list(DrawerList):
    - ItemDrawer1
    - ItemDrawer2
    - ...

ということはどういうことかと言うと、md_list側でこのメソッドを持っていないとどの
アイテムが切り替わったのかということを検知できないことになってしまいます。改めて
set_color_itemメソッドの定義を見てみましょう。

class DrawerList(ThemableBehavior, MDList):
    def set_color_item(self, instance_item):
        '''Called when tap on a menu item.'''

        # Set the color of the icon and text for the menu item.
        for item in self.children:
            if item.text_color == self.theme_cls.primary_color:
                item.text_color = self.theme_cls.text_color
                break
        instance_item.text_color = self.theme_cls.primary_color

self.childrenとあるのはまさにそれで、子アイテムを持っていますね。で、異なる
アイテムをクリックしたときは、もともとプライマリーだったアイテムは元通りに(ifブロ
ック)、クリックしたアイテムはプライマリーと(ifブロック以外)変化させたカラクリと
なりました。わぁー、ぱちぱちぱちと拍手を送られるくらいエレガントなふるまいです。

結果

長すぎましたね。もう疲れた... トホホ。。
いやまだまだ!と気張ってもコードがない以上は仕方がありません。これ以外にも色々と
ありますが、もう先述ですので動かして試したみたいと思います。リンクは先述ですが、
GitHubの公式アカウントの該当部分になり、これを対象として動かしてみます。

# ちなみに当アカウントでも保持しています(extend_navigation_drawer.py)

171.gif

全く問題ありませんね。マニュアルと同じ挙動をしています。

感想を言えばゴイスー(死語)ですね。これだけでアプリの半分くらい出来ていませんか
と言えるくらいの完成度の気がします。

Switching screens in the ScreenManager and using the common MDToolbar and NavigationDrawer with type standard

今までは、メニューアイテムを押しても、テキストカラーが変わるだけでコンテンツが
何か変わることはありませんでした。この節では、いやコンテンツを表示させようよと
いう意図のもと、書かれている気がします。すみません、ただの感想となってしまい。。

あとは、先に言っておくと、standard typeというのも選べるらしいのでそちらも
併せていきたいと思います。では、まずはコードからですが、先述の部分あたりだと
かは端折っていきます。

xxxvi/simple_navigation_drawer.py
()

KV = '''
<ContentNavigationDrawer>:

    ScrollView:

        MDList:

            OneLineListItem:
                text: "Screen 1"
                on_press:
                    root.nav_drawer.set_state("close")
                    root.screen_manager.current = "scr 1"

            OneLineListItem:
                text: "Screen 2"
                on_press:
                    root.nav_drawer.set_state("close")
                    root.screen_manager.current = "scr 2"


MDScreen:

(略)

    MDNavigationLayout:
        x: toolbar.height

        ScreenManager:
            id: screen_manager

            MDScreen:
                name: "scr 1"

                MDLabel:
                    text: "Screen 1"
                    halign: "center"

            MDScreen:
                name: "scr 2"

                MDLabel:
                    text: "Screen 2"
                    halign: "center"

        MDNavigationDrawer:
            id: nav_drawer
            type: "standard"

            ContentNavigationDrawer:
                screen_manager: screen_manager
                nav_drawer: nav_drawer
'''


class ContentNavigationDrawer(MDBoxLayout):
    #screen_manager = ObjectProperty()
    #nav_drawer = ObjectProperty()
    pass

()

まずはkvから触れていきます。ContentNavigationDrawerウィジェットですが、
こちらはシンプルなリストを格納したウィジェットとなります。これがMDNavigation-
Drawerの配下に配置されます。

で、さらにルートウィジェット側に入り、今度はMDNavigationLayoutになります。
x領域を指すxプロパティを保持し、ツールバーの高さ(toolbar.height)を指定して
います。端折っていますが、MDToolbarウィジェットはMDNavigationLayoutの配下
ではなく、今回はMDScreenルートウィジェットの配下に存在する形となりました。

MDNavigationLayoutの配下にはScreenManagerとMDNavigationDrawerウィジェ
ットが配置されていますね。ScreenManagerについては、以前にもScreen篇で触れ込
んでいますので、興味がある方はそちらをご参照頂ければと思います。

さらに、今度はMDNavigationDrawerウィジェットですが、こちらは今日散々やってき
ましたね。type・idプロパティがあることが変わり種で、ContentNavigationDrawer
ウィジェットにもそれぞれscreen_manager・nav_drawerプロパティを保持して扱いや
すくそれぞれ異なるウィジェットを値として指定しています。はて、screen_managerは
分かるけど、なんでnav_drawerは指定しているのだろう。。parentで操作できるはず...
# こういう使い方もできるよ!ということでしょうか

あとはクラス側ですが、今回メインのTestNavigationDrawerクラスでも操作するところ
がないので、screen_managerなどのプロパティは削除してみました。たしかid値で指定
出来ていれば問題なさそうな気はしますが。。クラス側でも適切な取得の仕方をしていれば
と言う前提がつきますが。

結果

ということで、全て触れ込みは終わったのできりのいいところで動かしてみましょう。

172.gif

はい、全然問題ありませんね。色々変更しても問題ないことは分かりました。

現状他に分かっていることと言うと、typeプロパティをstandardに変更するとドローワー
領域以外でクリックなどをしてもドローワー自体が閉じないということくらいでしょうか。
これがstandard以外とかだとちゃんと閉じます。他にも何か見えていない挙動があるかも
しれませんね。

API - kivymd.uix.navigationdrawer.navigationdrawer

まとめに入る前に、本日使用したAPIについて触れていきます。

class kivymd.uix.navigationdrawer.navigationdrawer.MDNavigationLayout(**kwargs)

add_widget(self, widget, index=0, canvas=None)

Only two layouts are allowed: ScreenManager and MDNavigationDrawer.

上記2つのレイアウトしか入れ子にすることはできません。これを使うのであれば、ほと
んどアプリのレイアウトは決まってきそうですね。

class kivymd.uix.navigationdrawer.navigationdrawer.MDNavigationDrawer(**kwargs)

やたらと長い概要文があります。少しこれは自身の方ではなかなか大変なので、依頼を
してみようと思います。

FakeRectangularElevationBehavior is a shadow mockup for widgets.
Improves performance using cached images inside
kivymd.images dir

This class cast a fake Rectangular shadow behaind the widget.

You can either use this behavior to overwrite the elevation of
a prefab widget, or use it directly inside a new widget class definition.

Use this class as follows for new widgets:
コード

With this method each class can draw it’s content in the canvas
in the correct order, avoiding some visual errors.

FakeCircularElevationBehavior will load prefabricated textures
to optimize loading times.

Also, this class allows you to overwrite real time shadows,
in the sence that if you are using a standard widget,
like a button, MDCard or Toolbar, you can include this class
after the base class to optimize the loading times.

As an example of this flexibility:
コード

FakeRectangularElevationBehavio`rは、ウィジェット用の影のモックアップです。
kivymd.images dirにキャッシュされた画像を使用してパフォーマンスを向上させます。

このクラスはウィジェットの後ろにフェイクの長方形の影を落とします。

このビヘイビアを使って,プレハブウィジェットのエレベーションを上書きするか,
新しいウィジェットクラスの定義の中で直接使うことができます.

このクラスは、新しいウィジェットに以下のように使用します。
コード

このメソッドを使うと、各クラスはコンテンツを正しい順序でキャンバスに描くことができ、
視覚的なエラーを避けることができます。

FakeCircularElevationBehavior は、ロード時間を最適化するために、プレハブの
テクスチャをロードします。

また、このクラスでは、リアルタイムシャドウを上書きすることができます。つまり、
ボタン、MDCard、ツールバーなどの標準的なウィジェットを使用している場合は、
ベースクラスの後にこのクラスを含めることで、読み込み時間を最適化することができます。

この柔軟性の一例として
コード

なかなかコアな話で、読み解くのも難しいところですね。ただ、こういったところの責任は
あなたたちにもあるのですよと言われているような気もします(気のせい)。

あとこういったことも記載がありますね。

About rounded corners: be careful, since this behavior is a mockup and will not draw any rounded corners.

あ、まだ試作段階ということなのですね。今後の期待ということでしょうか。

type

Type of drawer. Modal type will be on top of screen.
Standard type will be at left or right of screen.
Also it automatically disables close_on_click and
enable_swiping to prevent closing drawer for standard type.

type is a OptionProperty and defaults to modal.

最後のサンプルコードで指定していましたね。ここも少し長く記載されているので
依頼してみましょう。

ドロワーの種類。モーダルタイプは画面の上に表示されます。標準タイプは画面の
左または右に表示されます。また、標準タイプのドロワーを閉じないように、
close_on_clickとenable_swipingを自動的に無効にします。

ドロワーの種類としては、modalかstandardの2つみたいです。デフォルトはmodal
ですね。モーダルは上記の2つのプロパティを無効化しているとありますね。なので、
動作確認したときにドロワー以外の領域をタッチすると、ドロワーが閉じるといった
ことが起きるというのは合点がいきますね。

close_on_click

Close when click on scrim or keyboard escape.
It automatically sets to False for “standard” type.

close_on_click is a BooleanProperty and defaults to True.

どうやら、standard typeのときに無効化するプロパティの一部はこれみたいですね。
ややこしいところですが、しっかりと把握したいところです。

enable_swiping

Allow to open or close navigation drawer with swipe.
It automatically sets to False for “standard” type.

enable_swiping is a BooleanProperty and defaults to True.

standard typeのときの無効化オプションのもう1つがこれですね。スワイプ
するときに戻るというのも注目ポイントの1つかと思われます。これらの特性を
しっかり把握した上で、typeプロパティの値も決めたいですね。

set_state(self, new_state='toggle', animation=True)

Change state of the side panel.
New_state can be one of “toggle”, “open” or “close”.

リストアイテムのon_pressプロパティで選択していましたね。これを指定しないと、
リストを選んだ際にドローワーが閉じられないという不恰好なさまになってしまいます。

まとめ

さて、いかがだったでしょうか。長大編(おおげさ)となってしまいましたね。

結構このコンポーネントは骨が折れるとは言い過ぎかもしれませんが、なかなか難しかった
のではないでしょうか。なんせ複数のコンポーネントが組み合わさっていますので、前提知識
が結構問われます。一旦改めて、他のウィジェットをリンクしたものをリストアップしてみます。

  • Screen
  • Toolbar
  • List
  • Layout
  • Label
  • Menu
    ※ 順不同

まぁでも、これだけ分かっていれば最低限のアプリは作れるという感じでしょうか。
何も作っていないやつが何を言うかと言われそうですが。。

ということで今日は厳密に言うとまだ終わってないですが、一旦ここまでとしようと
思います。来週は順番通りでNavigationRailに入ろうと思います。ナビゲーション
続きですね。。では、また来週〜。

それでは、ごきげんよう。

参照

Components » NavigationDrawer
https://kivymd.readthedocs.io/en/latest/components/navigationdrawer/

DeepL 翻訳ツール
https://www.deepl.com/ja/translator

番外(Twitterクローンもどき)編

はい、みなさん、おまっとさんです。

ということで、今日のお題を兼ねてナビゲーションドロワーの一部のみTwitterクローンに
挑戦してみました。「はぁ?、そんなことだと思ってなかったんだわ」という方はブラウザ
バックをお願い出来ればと思います。

ということで、そんな前置きいらんのよと言われそうなので動かしてみた様子を載せておき
ます。ご賞味あれ。

173.gif

本当にもどきということを入れただけであって、完成度は恐ろしいくらい低いです。プロフィ
ールの折りたたみはないわ、フォロー・フォロワー数の間隔も気になるし、下部の方にはテー
マの選択ボタンもありません。選択ボタンなどはちょっと諦めていたりしてw

まぁでもいかにクローンを作ることが難しいということが分かっただけでもよいかなと思い
ます。クローン作るよりかはライブラリの特性を優先して、オリジナルのものを作る方が
いいのかなぁなんて思ったり。

あと、今回試したコードについては以下にて格納しております。興味ある方は除いてもらう
と嬉しかったりも。あと、こんなんじゃだめだ!となって完成まで改修頂くのも歓迎します。
ただ、GitHubでチーム開発したことないので、めちゃ対応が遅くなるかも。プルリクもらえ
れば、動かしてみてマージまではこちらでやっておきます。

あとは特に書いておくことでもないですが、アカウントの画像としては以下から拝借しました。
見たことある方が多いのではないでしょうか。みなさんもお気に入りの写真を使ってみてはいか
がでしょうか。

ということで以上です!見てくれてありがとうな!(えらそう)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0