```
# src/tabs/main_tab_explorer.py
import os
import platform
import subprocess
from pathlib import Path
from src.debug.timer import timer
import flet as ft

from configs.path_config import IMAGES_DIR
from utils.thumbnails import get_thumbnail


def create_subdir_card(item_path: Path, on_click: callable) -> ft.Card:
    return ft.Card(
        content=ft.Container(
            content=ft.Column([
                ft.Icon(
                    ft.Icons.FOLDER,
                    size=60,
                    color=ft.Colors.BLUE_400
                ),
                ft.Text(
                    item_path.name,
                    size=14,
                    text_align=ft.TextAlign.CENTER,
                    overflow=ft.TextOverflow.ELLIPSIS
                )
            ],
                alignment=ft.MainAxisAlignment.CENTER,
                horizontal_alignment=ft.CrossAxisAlignment.CENTER,
                spacing=10
            ),
            padding=10,
            on_click=on_click,
        ),
        elevation=2
    )


def create_image_card(item_path: Path, on_click: callable) -> ft.Card:
    return ft.Card(
        content=ft.Container(
            content=ft.Stack([
                ft.Image(
                    src=str(get_thumbnail(item_path)),
                    fit=ft.ImageFit.COVER,
                    border_radius=8,
                    left=0, right=0, top=0, bottom=0,
                ),
                ft.Container(  # タイトル部分に透明背景をつける
                    height=28,
                    bgcolor=ft.Colors.with_opacity(0.6, ft.Colors.BLACK),
                    border_radius=8,
                    left=0, right=0, bottom=0,
                ),
                ft.Container(
                    content=ft.Text(
                        item_path.name,
                        size=14,
                        color=ft.Colors.WHITE,
                        weight=ft.FontWeight.BOLD,
                        text_align=ft.TextAlign.CENTER,
                        overflow=ft.TextOverflow.ELLIPSIS,
                    ),
                    alignment=ft.alignment.bottom_center,  # 下中央配置
                    padding=4,
                    # 横方向最大幅制限（親から10px余白）
                    margin=ft.margin.symmetric(horizontal=10),
                    # 角丸処理
                    border_radius=8,
                )
            ]),
            on_click=on_click,
        ),
        elevation=2,
        shape=ft.RoundedRectangleBorder(radius=8),
    )


class ImageBrowser:
    def __init__(self):
        self.root_dir = IMAGES_DIR
        self.current_dir = self.root_dir
        self.history = []

        # サポートする画像拡張子
        self.image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}

    def is_image(self, path):
        return path.suffix.lower() in self.image_extensions

    @staticmethod
    def open_image(image_path):
        """OSのデフォルトアプリケーションで画像を開く"""
        system = platform.system()
        if system == 'Darwin':  # macOS
            subprocess.run(['open', str(image_path)])
        elif system == 'Windows':
            os.startfile(str(image_path))
        else:  # Linux
            subprocess.run(['xdg-open', str(image_path)])

    def get_items(self):
        """現在のディレクトリのサブディレクトリと画像を取得"""
        items = []
        try:
            for item in sorted(self.current_dir.iterdir()):
                if item.is_dir() and not item.name.startswith('.'):
                    items.append(('dir', item))
                elif item.is_file() and self.is_image(item):
                    items.append(('image', item))
        except PermissionError:
            pass
        return items

    def navigate_to(self, path):
        """指定されたパスに移動"""
        if path.is_dir():
            self.history.append(self.current_dir)
            self.current_dir = path

    def go_back(self):
        """前のディレクトリに戻る"""
        if self.history:
            self.current_dir = self.history.pop()

    def go_to_root(self):
        """ルートディレクトリに戻る"""
        self.history.clear()
        self.current_dir = self.root_dir

    def build_tab(self, page: ft.Page):
        """ImageBrowserのUIをタブとして構築して返す"""
        browser = self

        # パスを表示するテキスト
        path_text = ft.Text(
            value=str(browser.current_dir),
            size=16,
            weight=ft.FontWeight.BOLD
        )

        # カードを表示するグリッド
        grid = ft.GridView(
            expand=True,
            runs_count=5,
            max_extent=200,
            child_aspect_ratio=1.0,
            spacing=10,
            run_spacing=10,
        )

        def update_view():
            """ビューを更新"""
            path_text.value = str(browser.current_dir)
            grid.controls.clear()

            items = browser.get_items()
            for item_type, item_path in items:
                if item_type == 'dir':
                    # ディレクトリカード
                    card = create_subdir_card(item_path=item_path, on_click=lambda e, p=item_path: navigate_to_dir(p))
                else:
                    # 画像カード
                    card = create_image_card(item_path=item_path, on_click=lambda e, p=item_path: open_image(p))

                grid.controls.append(card)

            # ナビゲーションボタンの表示/非表示
            back_button.disabled = browser.current_dir == browser.root_dir
            root_button.disabled = browser.current_dir == browser.root_dir

            page.update()

        def navigate_to_dir(path):
            """ディレクトリに移動"""
            browser.navigate_to(path)
            with timer(prefix=f"navigate to {path}: "):
                update_view()

        def open_image(path):
            """画像を開く"""
            ImageBrowser.open_image(path)

        def go_back(e):
            """前に戻る"""
            browser.go_back()
            update_view()

        def go_to_root(e):
            """ルートに戻る"""
            browser.go_to_root()
            update_view()

        # ナビゲーションボタン
        back_button = ft.IconButton(
            icon=ft.Icons.ARROW_BACK,
            on_click=go_back,
            tooltip="前に戻る",
            disabled=True
        )

        root_button = ft.IconButton(
            icon=ft.Icons.HOME,
            on_click=go_to_root,
            tooltip="ルートに戻る",
            disabled=True
        )

        # ヘッダー
        header = ft.Row([
            back_button,
            root_button,
            path_text
        ])

        # タブの内容
        tab_content = ft.Column([
            header,
            grid
        ], expand=True)

        # 初期表示
        update_view()

        return ft.Tab(
            text="Browser",
            content=tab_content
        )


def main(page: ft.Page):
    page.title = "タブ付き画像ブラウザ"
    page.padding = 20

    # ImageBrowserのインスタンスを作成
    image_browser = ImageBrowser()

    # タブを作成
    tabs = ft.Tabs(
        selected_index=0,
        animation_duration=300,
        tabs=[
            image_browser.build_tab(page),
        ],
        expand=True
    )

    # ページにタブを追加
    page.add(tabs)


if __name__ == "__main__":
    ft.app(target=main)
```

```
2025-06-17 00:31:08,002 flet:240 on_session_created [INFO]: App session started
2025-06-17 00:31:08,011 utils.async_task_manager:51 start_worker [INFO]: Worker task started.
2025-06-17 00:31:10,209 src.debug.timer:26 timer [INFO]: navigate to C:\Users\faker\Desktop\lora_test\images\midori_2: 111.26 ms
2025-06-17 00:31:12,678 utils.async_task_manager:98 _worker [INFO]: Worker task cancelled
2025-06-17 00:31:12,679 utils.async_task_manager:102 _worker [INFO]: Worker task finished
2025-06-17 00:31:12,682 watchcat:60 watch_images [INFO]: Observer stopped
```

Python 3.12, Flet 0.28.3.
画像ブラウザ。画像が100枚あるタブに移動したとき、111msの遅延が生じている。
これは100枚のサムネイルのキャッシュをロードしている時間で、その間はGUIの動作がブロックされている。
ページが遷移してから、ページの上からサムネイルを順次ロードするにはどうしたらいい？
なお、`get_thumbnail_async`は用意してある。