386 lines
13 KiB
Python
386 lines
13 KiB
Python
"""
|
|
テキストエディタコンポーネント
|
|
再利用可能なテキスト編集機能を提供
|
|
"""
|
|
|
|
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
|
|
)
|