""" テキストエディタコンポーネント 再利用可能なテキスト編集機能を提供 """ import flet as ft import sqlite3 import logging from datetime import datetime from typing import List, Dict, Optional, Callable class TextEditor: """再利用可能なテキストエディタコンポーネント""" def __init__( self, page: ft.Page, title: str = "テキストエディタ", placeholder: str = "テキストを入力してください", min_lines: int = 10, max_lines: int = 50, width: int = 600, height: int = 400, on_save: Optional[Callable] = None, on_load: Optional[Callable] = None, on_delete: Optional[Callable] = None ): self.page = page self.title_text = title self.placeholder = placeholder self.min_lines = min_lines self.max_lines = max_lines self.width = width self.height = height self.on_save = on_save self.on_load = on_load self.on_delete = on_delete # UI部品 self.title = ft.Text(title, size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900) # テキストエリア self.text_area = ft.TextField( label=placeholder, multiline=True, min_lines=min_lines, max_lines=max_lines, value="", autofocus=True, width=width, height=height ) # ボタン群 self.save_btn = ft.Button( "保存", on_click=self.save_text, 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_latest, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE ) self.delete_btn = ft.Button( "削除", on_click=self.delete_latest, bgcolor=ft.Colors.RED, color=ft.Colors.WHITE ) # テキストリスト self.text_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_texts() 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.text_list ], expand=True, spacing=15) def save_text(self, e): """テキストを保存""" if self.text_area.value.strip(): try: # データベースに保存 conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS text_storage ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') cursor.execute(''' INSERT INTO text_storage (category, title, content) VALUES (?, ?, ?) ''', (self.title_text, f"{self.title_text}_{datetime.now().strftime('%Y%m%d_%H%M%S')}", self.text_area.value)) conn.commit() conn.close() # コールバック実行 if self.on_save: self.on_save(self.text_area.value) # 成功メッセージ self._show_snackbar("テキストを保存しました", ft.Colors.GREEN) logging.info(f"テキスト保存: {len(self.text_area.value)} 文字") # リスト更新 self.load_texts() except Exception as ex: logging.error(f"テキスト保存エラー: {ex}") self._show_snackbar("保存エラー", ft.Colors.RED) def clear_text(self, e): """テキストをクリア""" self.text_area.value = "" self.page.update() logging.info("テキストをクリア") def load_latest(self, e): """最新のテキストを読み込み""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS text_storage ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, 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 text_storage WHERE category = ? ORDER BY created_at DESC LIMIT 1 ''', (self.title_text,)) result = cursor.fetchone() conn.close() if result: text_id, title, content, created_at = result self.text_area.value = content # コールバック実行 if self.on_load: self.on_load(content) self._show_snackbar(f"テキストを読み込みました (ID: {text_id})", ft.Colors.GREEN) logging.info(f"テキスト読込: ID={text_id}") else: self._show_snackbar("保存済みテキストがありません", ft.Colors.ORANGE) except Exception as ex: logging.error(f"テキスト読込エラー: {ex}") self._show_snackbar("読込エラー", ft.Colors.RED) def delete_latest(self, e): """最新のテキストを削除""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' DELETE FROM text_storage WHERE id = (SELECT id FROM text_storage WHERE category = ? ORDER BY created_at DESC LIMIT 1) ''', (self.title_text,)) conn.commit() conn.close() # コールバック実行 if self.on_delete: self.on_delete() # リスト更新 self.load_texts() self._show_snackbar("テキストを削除しました", ft.Colors.GREEN) logging.info("テキスト削除完了") except Exception as ex: logging.error(f"テキスト削除エラー: {ex}") self._show_snackbar("削除エラー", ft.Colors.RED) def load_texts(self): """テキストリストを読み込む""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS text_storage ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, 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 text_storage WHERE category = ? ORDER BY created_at DESC LIMIT 10 ''', (self.title_text,)) texts = cursor.fetchall() conn.close() # リストを更新 self.text_list.controls.clear() for text in texts: text_id, title, content, created_at = text # テキストカード text_card = ft.Card( content=ft.Container( content=ft.Column([ ft.Row([ ft.Text(f"ID: {text_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 _, tid=text_id: self.load_specific_text(text_id), bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE, width=80 ) self.text_list.controls.append( ft.Row([text_card, load_btn], alignment=ft.MainAxisAlignment.SPACE_BETWEEN) ) self.page.update() except Exception as e: logging.error(f"テキストリスト読込エラー: {e}") def load_specific_text(self, text_id): """特定のテキストを読み込む""" try: conn = sqlite3.connect('sales.db') cursor = conn.cursor() cursor.execute(''' SELECT content FROM text_storage WHERE id = ? ''', (text_id,)) result = cursor.fetchone() conn.close() if result: self.text_area.value = result[0] # コールバック実行 if self.on_load: self.on_load(result[0]) self._show_snackbar(f"テキスト #{text_id} を読み込みました", ft.Colors.GREEN) logging.info(f"テキスト読込: ID={text_id}") else: self._show_snackbar("テキストが見つかりません", ft.Colors.RED) except Exception as ex: logging.error(f"テキスト読込エラー: {ex}") self._show_snackbar("読込エラー", ft.Colors.RED) def get_text(self) -> str: """現在のテキストを取得""" return self.text_area.value def set_text(self, text: str): """テキストを設定""" self.text_area.value = text self.page.update() def _show_snackbar(self, message: str, color: ft.Colors): """SnackBarを表示""" try: self.page.snack_bar = ft.SnackBar( content=ft.Text(message), bgcolor=color ) self.page.snack_bar.open = True self.page.update() except: pass # 使用例 def create_draft_editor(page: ft.Page) -> TextEditor: """下書きエディタを作成""" return TextEditor( page=page, title="下書きエディタ", placeholder="下書き内容を入力してください", min_lines=15, max_lines=50, width=600, height=400 ) def create_memo_editor(page: ft.Page) -> TextEditor: """メモエディタを作成""" return TextEditor( page=page, title="メモ", placeholder="メモを入力してください", min_lines=10, max_lines=30, width=500, height=300 ) def create_note_editor(page: ft.Page) -> TextEditor: """ノートエディタを作成""" return TextEditor( page=page, title="ノート", placeholder="ノートを入力してください", min_lines=20, max_lines=100, width=700, height=500 )