h-1.flet.3/app_text_editor.py

427 lines
15 KiB
Python

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)