import flet as ft import sqlite3 import signal import sys import logging import random from datetime import datetime from typing import List, Dict, Optional class ErrorHandler: """グローバルエラーハンドラ""" @staticmethod def handle_error(error: Exception, context: str = ""): """エラーを一元処理""" error_msg = f"{context}: {str(error)}" logging.error(error_msg) print(f"❌ {error_msg}") try: if hasattr(ErrorHandler, 'current_page'): ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED) except: pass @staticmethod def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED): """SnackBarを表示""" try: page.snack_bar = ft.SnackBar( content=ft.Text(message), bgcolor=color ) page.snack_bar.open = True page.update() except: pass class TextEditor: """テキストエディタ""" def __init__(self, page: ft.Page): self.page = page # UI部品 self.title = ft.Text("下書きエディタ", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) # テキストエリア self.text_area = ft.TextField( label="下書き内容", multiline=True, min_lines=10, max_lines=50, value="", autofocus=True, width=600, height=400 ) # ボタン群 self.save_btn = ft.Button( "保存", on_click=self.save_draft, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE ) self.clear_btn = ft.Button( "クリア", on_click=self.clear_text, bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE ) self.load_btn = ft.Button( "読込", on_click=self.load_draft, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE ) self.delete_btn = ft.Button( "削除", on_click=self.delete_draft, bgcolor=ft.Colors.RED, color=ft.Colors.WHITE ) # 下書きリスト self.draft_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=200) # 操作説明 self.instructions = ft.Text( "操作方法: TABでフォーカス移動、SPACE/ENTERで保存、マウスクリックでもボタン操作可能", size=12, color=ft.Colors.GREY_600 ) # データ読み込み self.load_drafts() def build(self): """UIを構築して返す""" return ft.Column([ self.title, ft.Divider(), ft.Row([self.save_btn, self.clear_btn, self.load_btn, self.delete_btn], spacing=10), ft.Divider(), self.text_area, ft.Divider(), ft.Text("下書きリスト", size=18, weight=ft.FontWeight.BOLD), self.instructions, self.draft_list ], expand=True, spacing=15) def save_draft(self, e): """下書きを保存""" if self.text_area.value.strip(): try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS drafts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' INSERT INTO drafts (title, content) VALUES (?, ?) ''', (f"下書き_{datetime.now().strftime('%Y%m%d_%H%M%S')}", self.text_area.value)) conn.commit() conn.close() # 成功メッセージ ErrorHandler.show_snackbar(self.page, "下書きを保存しました", ft.Colors.GREEN) logging.info(f"下書き保存: {len(self.text_area.value)} 文字") # リスト更新 self.load_drafts() except Exception as ex: ErrorHandler.handle_error(ex, "下書き保存エラー") def clear_text(self, e): """テキストをクリア""" self.text_area.value = "" self.page.update() logging.info("テキストをクリア") def load_draft(self, e): """下書きを読み込み""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS drafts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' SELECT id, title, content, created_at FROM drafts ORDER BY created_at DESC LIMIT 10 ''') drafts = cursor.fetchall() conn.close() if drafts: # テキストエリアに設定 self.text_area.value = drafts[0][2] # content # リスト更新 self.draft_list.controls.clear() for draft in drafts: draft_id, title, content, created_at = draft # 下書きカード draft_card = ft.Card( content=ft.Container( content=ft.Column([ ft.Row([ ft.Text(f"ID: {draft_id}", size=12, color=ft.Colors.GREY_600), ft.Text(f"作成: {created_at}", size=12, color=ft.Colors.GREY_600) ], spacing=5), ft.Text(title, weight=ft.FontWeight.BOLD, size=14), ft.Text(content[:100] + "..." if len(content) > 100 else content, size=12), ]), padding=10, width=400 ), margin=ft.margin.only(bottom=5) ) # 読込ボタン load_btn = ft.Button( "読込", on_click=lambda _, did=draft_id: self.load_specific_draft(draft_id), bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE, width=80 ) self.draft_list.controls.append( ft.Row([draft_card, load_btn], alignment=ft.MainAxisAlignment.SPACE_BETWEEN) ) self.page.update() logging.info(f"下書き読込完了: {len(drafts)} 件") else: ErrorHandler.show_snackbar(self.page, "下書きがありません", ft.Colors.ORANGE) except Exception as ex: ErrorHandler.handle_error(ex, "下書き読込エラー") def load_specific_draft(self, draft_id): """特定の下書きを読み込む""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' SELECT content FROM drafts WHERE id = ? ''', (draft_id,)) result = cursor.fetchone() conn.close() if result: self.text_area.value = result[0] ErrorHandler.show_snackbar(self.page, f"下書き #{draft_id} を読み込みました", ft.Colors.GREEN) logging.info(f"下書き読込: ID={draft_id}") else: ErrorHandler.show_snackbar(self.page, "下書きが見つかりません", ft.Colors.RED) except Exception as ex: ErrorHandler.handle_error(ex, "下書き読込エラー") def delete_draft(self, e): """下書きを削除""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' DELETE FROM drafts WHERE id = (SELECT id FROM drafts ORDER BY created_at DESC LIMIT 1) ''') conn.commit() conn.close() # リスト更新 self.load_drafts() ErrorHandler.show_snackbar(self.page, "下書きを削除しました", ft.Colors.GREEN) logging.info("下書き削除完了") except Exception as ex: ErrorHandler.handle_error(ex, "下書き削除エラー") def load_drafts(self): """下書きリストを読み込む""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS drafts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' SELECT id, title, content, created_at FROM drafts ORDER BY created_at DESC LIMIT 10 ''') drafts = cursor.fetchall() conn.close() # リストを更新 self.draft_list.controls.clear() for draft in drafts: draft_id, title, content, created_at = draft # 下書きカード draft_card = ft.Card( content=ft.Container( content=ft.Column([ ft.Row([ ft.Text(f"ID: {draft_id}", size=12, color=ft.Colors.GREY_600), ft.Text(f"作成: {created_at}", size=12, color=ft.Colors.GREY_600) ], spacing=5), ft.Text(title, weight=ft.FontWeight.BOLD, size=14), ft.Text(content[:100] + "..." if len(content) > 100 else content, size=12), ]), padding=10, width=400 ), margin=ft.margin.only(bottom=5) ) # 読込ボタン load_btn = ft.Button( "読込", on_click=lambda _, did=draft_id: self.load_specific_draft(draft_id), bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE, width=80 ) self.draft_list.controls.append( ft.Row([draft_card, load_btn], alignment=ft.MainAxisAlignment.SPACE_BETWEEN) ) self.page.update() except Exception as e: ErrorHandler.handle_error(e, "下書きリスト読込エラー") class SimpleApp: """シンプルなアプリケーション""" def __init__(self, page: ft.Page): self.page = page ErrorHandler.current_page = page # ログ設定 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('app.log'), logging.StreamHandler() ] ) # シグナルハンドラ設定 def signal_handler(signum, frame): print(f"\nシグナル {signum} を受信しました") print("✅ 正常終了処理完了") logging.info("アプリケーション正常終了") sys.exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) # データベース初期化 self._init_database() # テキストエディタ作成 self.text_editor = TextEditor(page) # ページ構築 page.add( self.text_editor.build() ) logging.info("テキストエディタ起動完了") print("🚀 テキストエディタ起動完了") def _init_database(self): """データベース初期化""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() # 下書きテーブル作成 cursor.execute(''' CREATE TABLE IF NOT EXISTS drafts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') conn.commit() conn.close() logging.info("データベース初期化完了") except Exception as e: ErrorHandler.handle_error(e, "データベース初期化エラー") def main(page: ft.Page): """メイン関数""" try: # ウィンドウ設定 page.title = "テキストエディタ" page.window_width = 800 page.window_height = 600 page.theme_mode = ft.ThemeMode.LIGHT # ウィンドウクローズイベント page.on_window_close = lambda _: SimpleApp(page)._cleanup_resources() # アプリケーション起動 app = SimpleApp(page) except Exception as e: ErrorHandler.handle_error(e, "アプリケーション起動エラー") if __name__ == "__main__": ft.run(main)