427 lines
15 KiB
Python
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)
|