StreamlitでPWA

## はじめに
Streamlitで作ったWebアプリをネイティブアプリのようにスマートフォンから気軽に使いたい！
ということで、PWAにしてみました。(正確にはStreamlitではなく、stliteというStreamlit の WebAssembly への移植版を利用しました。タチバナさんという日本人の方が作られています。)

https://github.com/whitphx/stlite

※サンプルアプリ（iPhoneだと「ホーム画面に追加」すると、PWAとして動くと思います。）

https://ppspps824.github.io/stlite_pwa/

## PWAとは（ChatGPTより）

PWA（Progressive Web App）は、ウェブアプリケーションの一形態で、ネイティブアプリのようなユーザー体験を提供するための技術および設計手法のことです。PWAの主な特徴は以下の通りです。

1. **インストール可能**: ユーザーはPWAを自分のデバイスにインストールすることができ、ホーム画面にアイコンを追加してネイティブアプリのように利用することができます。
2. **オフラインサポート**: Service Workerを使用して、ネットワークが利用できない場合でもアプリをオフラインで利用できるようにします。
3. **プッシュ通知**: ユーザーに対してプッシュ通知を送信することができ、再訪問を促進する手助けになります。
4. **レスポンシブデザイン**: さまざまなデバイスや画面サイズに対応したデザインが求められます。
5. **セキュリティ**: HTTPSを使用して、セキュリティを確保します。

PWAは、ウェブとネイティブアプリの利点を組み合わせ、ユーザーにシームレスな体験を提供することを目指しています。

## つくったもの

https://github.com/ppspps824/stlite_pwa

※以下、分けて記載していますが↑からクローンしていただければすぐ使えます。

stliteを読み込む部分を↓のようにするとCDN経由ではなく直接リポジトリから読み込むため、より最新のStreamlitの機能が使えるようです。2024/6/4現在だとver1.35.0まで使えそうです。もはや最新ですね。（前述のstliteのGithubのページには「この URL は壊れる可能性があり、このリソースが将来も利用可能であるという保証がないため、この URL を使用しないことを強くお勧めします。」とありますのでご注意ください。）

```<script src="https://whitphx.github.io/stlite/lib/mountable/stlite.js"></script>```


```html:index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <title>stlite PWA</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.34.0/build/stlite.css" />
    <link rel="manifest" href="manifest.json" />
</head>

<body>
    <div id="root"></div>
    <script src="https://cdn.jsdelivr.net/npm/@stlite/mountable@0.34.0/build/stlite.js"></script>
    <script>

        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/service-worker.js')
                    .then(registration => {
                        console.log('ServiceWorker registration successful with scope: ', registration.scope);
                    }, error => {
                        console.log('ServiceWorker registration failed: ', error);
                    });
            });
        }
        async function mountStreamlitApp() {
            try {
                const response = await fetch("app.py");
                const app = await response.text();
                stlite.mount(
                    {
                        entrypoint: "app.py",  // The target file of the `streamlit run` command
                        files: {
                            "app.py": app
                        }
                    },
                    document.getElementById("root")
                );
            } catch (error) {
                console.error("Failed to fetch or mount the app:", error);
            }
        }

        mountStreamlitApp();


    </script>
</body>

</html>
```



```python:app.py
import streamlit as st

HIDE_ST_STYLE = """
                <style>
                div[data-testid="stToolbar"] {
                visibility: hidden;
                height: 0%;
                position: fixed;
                }
                div[data-testid="stDecoration"] {
                visibility: hidden;
                height: 0%;
                position: fixed;
                }
                div[data-testid="stStatusWidget"] {
                visibility: hidden;
                height: 0%;
                position: fixed;
                }
                #MainMenu {
                visibility: hidden;
                height: 0%;
                }
                header {
                visibility: hidden;
                height: 0%;
                }
                footer {
                visibility: hidden;
                height: 0%;
                }
                .block-container {
                padding-top: 0rem;
                }
                </style>
                """
def init_style():
    st.set_page_config(
        page_title="stlite PWA",
        page_icon="🚀",
        layout="wide",
        initial_sidebar_state="auto",
        menu_items=None,
    )
    st.markdown(HIDE_ST_STYLE,unsafe_allow_html=True)


def main():
    st.title("stlite PWA")
    name=st.text_input("Name")
    if name:
        st.write(f"Hello!{name}.")

if __name__ == "__main__":
    init_style()
    main()

```

```json:manifest.json
{
    "name": "stlite PWA",
    "short_name": "stlite",
    "start_url": ".",
    "display": "standalone",
    "background_color": "#ffffff",
    "theme_color": "#000000",
    "icons": [
        {
            "src": "assets/icon-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "assets/icon-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}
```

```javascript:service-worker.js
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open('static-cache-v1').then(cache => {
            return cache.addAll([
                '/',
                '/app.py',
                '/index.html',
                '/manifest.json',
                '/assets/icon-192x192.png',
                '/assets/icon-512x512.png',
                'https://cdn.jsdelivr.net/npm/@stlite/mountable@0.34.0/build/stlite.css',
                'https://cdn.jsdelivr.net/npm/@stlite/mountable@0.34.0/build/stlite.js'
            ]);
        })
    );
});

self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(response => {
            return response || fetch(event.request);
        })
    );
});

```

## おわりに
Gradioにも似たようなのがありますね。同じ感じで動くと思います。

https://www.gradio.app/guides/gradio-lite