Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,7 +5,6 @@ import requests
|
|
| 5 |
import os
|
| 6 |
import re
|
| 7 |
import io
|
| 8 |
-
import json
|
| 9 |
import time
|
| 10 |
from PIL import Image, ImageChops
|
| 11 |
|
|
@@ -13,12 +12,11 @@ from PIL import Image, ImageChops
|
|
| 13 |
GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
|
| 14 |
CMS_SERVICE_ID = os.environ.get("MICROCMS_SERVICE_ID")
|
| 15 |
CMS_API_KEY = os.environ.get("MICROCMS_API_KEY")
|
| 16 |
-
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "1234")
|
| 17 |
AUTHOR_LIST = ["にき", "かん", "なごみ", "蜜柑餅"]
|
| 18 |
SPACE_URL = "https://nagomi-sugarheart-wasemas9thblogs.hf.space"
|
| 19 |
ARTICLES_PER_PAGE = 10
|
| 20 |
|
| 21 |
-
# --- テンプレート定義 ---
|
| 22 |
TEMPLATES = {
|
| 23 |
"テンプレートなし": "",
|
| 24 |
"📅 イベントレポート": """<h2>イベント概要</h2>
|
|
@@ -63,18 +61,13 @@ def render_html(html, height=600):
|
|
| 63 |
wrapped = f"""
|
| 64 |
<html><head><style>
|
| 65 |
body {{
|
| 66 |
-
margin: 0;
|
| 67 |
-
padding: 0;
|
| 68 |
font-family: sans-serif;
|
| 69 |
font-size: 16px;
|
| 70 |
color: #111;
|
| 71 |
word-break: break-word;
|
| 72 |
}}
|
| 73 |
-
img {{
|
| 74 |
-
max-width: 100%;
|
| 75 |
-
height: auto;
|
| 76 |
-
display: block;
|
| 77 |
-
}}
|
| 78 |
p {{ margin: 0.5em 0; }}
|
| 79 |
h1,h2,h3 {{ margin: 0.8em 0 0.4em; }}
|
| 80 |
</style></head>
|
|
@@ -82,6 +75,26 @@ def render_html(html, height=600):
|
|
| 82 |
"""
|
| 83 |
components.html(wrapped, height=height, scrolling=True)
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def upload_image_to_microcms(image_file, max_width=1200):
|
| 86 |
try:
|
| 87 |
img = Image.open(image_file)
|
|
@@ -122,7 +135,6 @@ def upload_image_to_microcms(image_file, max_width=1200):
|
|
| 122 |
st.error(f"画像アップロードエラー: {e}")
|
| 123 |
return None
|
| 124 |
|
| 125 |
-
# --- ページネーション対応の記事取得 ---
|
| 126 |
def get_articles(limit=ARTICLES_PER_PAGE, offset=0):
|
| 127 |
if not CMS_SERVICE_ID or not CMS_API_KEY:
|
| 128 |
return [], 0
|
|
@@ -137,7 +149,6 @@ def get_articles(limit=ARTICLES_PER_PAGE, offset=0):
|
|
| 137 |
except:
|
| 138 |
return [], 0
|
| 139 |
|
| 140 |
-
# --- 全件取得(管理画面用)---
|
| 141 |
def get_all_articles():
|
| 142 |
if not CMS_SERVICE_ID or not CMS_API_KEY:
|
| 143 |
return []
|
|
@@ -171,11 +182,10 @@ if "id" in params:
|
|
| 171 |
st.title(article.get("title", "無題"))
|
| 172 |
st.caption(f"👤 {article.get('author')} | 📅 {article.get('publishedAt')[:10]}")
|
| 173 |
st.divider()
|
| 174 |
-
|
| 175 |
content = clean_quill_html(article.get("content", ""))
|
| 176 |
render_html(content, height=800)
|
| 177 |
-
|
| 178 |
st.divider()
|
|
|
|
| 179 |
share_url = f"{SPACE_URL}?id={article_id}"
|
| 180 |
st.write("🔗 固定リンク:")
|
| 181 |
st.code(share_url)
|
|
@@ -197,9 +207,6 @@ else:
|
|
| 197 |
|
| 198 |
tab_view, tab_manage = st.tabs(["📖 ブログを読む", "🛠️ 記事の管理・投稿"])
|
| 199 |
|
| 200 |
-
# =====================
|
| 201 |
-
# 閲覧タブ
|
| 202 |
-
# =====================
|
| 203 |
with tab_view:
|
| 204 |
st.title("サークル活動ブログ")
|
| 205 |
search_col1, search_col2 = st.columns(2)
|
|
@@ -209,11 +216,8 @@ else:
|
|
| 209 |
search_author_list = ["全員"] + AUTHOR_LIST
|
| 210 |
author_filter = st.selectbox("👤 著者で絞り込み", search_author_list, key="search_author")
|
| 211 |
|
| 212 |
-
# ページネーション用セッション
|
| 213 |
if "view_page" not in st.session_state:
|
| 214 |
st.session_state.view_page = 0
|
| 215 |
-
|
| 216 |
-
# 検索時はページリセット
|
| 217 |
if "last_search" not in st.session_state:
|
| 218 |
st.session_state.last_search = ""
|
| 219 |
if search_query != st.session_state.last_search:
|
|
@@ -221,13 +225,12 @@ else:
|
|
| 221 |
st.session_state.last_search = search_query
|
| 222 |
|
| 223 |
offset = st.session_state.view_page * ARTICLES_PER_PAGE
|
| 224 |
-
articles, total_count = get_articles(limit=ARTICLES_PER_PAGE, offset=offset)
|
| 225 |
|
| 226 |
-
# 検索・フィルタが入っている場合は全件取得に切り替え
|
| 227 |
if search_query or author_filter != "全員":
|
| 228 |
articles, _ = get_articles(limit=100, offset=0)
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
displayed = 0
|
| 231 |
for a in articles:
|
| 232 |
title = a.get("title", "無題")
|
| 233 |
content_html = a.get("content", "")
|
|
@@ -254,11 +257,9 @@ else:
|
|
| 254 |
st.query_params.id = a["id"]
|
| 255 |
st.rerun()
|
| 256 |
st.divider()
|
| 257 |
-
displayed += 1
|
| 258 |
|
| 259 |
-
# ページネーションボタン(検索なしの場合のみ)
|
| 260 |
if not search_query and author_filter == "全員":
|
| 261 |
-
total_pages = -(-total_count // ARTICLES_PER_PAGE)
|
| 262 |
if total_pages > 1:
|
| 263 |
st.caption(f"{total_count}件中 {offset+1}〜{min(offset+ARTICLES_PER_PAGE, total_count)}件を表示")
|
| 264 |
col_prev, col_page, col_next = st.columns([1, 2, 1])
|
|
@@ -275,9 +276,6 @@ else:
|
|
| 275 |
st.session_state.view_page += 1
|
| 276 |
st.rerun()
|
| 277 |
|
| 278 |
-
# =====================
|
| 279 |
-
# 管理タブ
|
| 280 |
-
# =====================
|
| 281 |
with tab_manage:
|
| 282 |
st.title("秘密の管理画面")
|
| 283 |
password = st.text_input("合言葉", type="password")
|
|
@@ -285,7 +283,6 @@ else:
|
|
| 285 |
if password == ADMIN_PASSWORD:
|
| 286 |
st.success("ログイン成功!")
|
| 287 |
|
| 288 |
-
# セッションステート初期化
|
| 289 |
if "edit_id" not in st.session_state: st.session_state.edit_id = None
|
| 290 |
if "edit_data" not in st.session_state: st.session_state.edit_data = {"title": "", "author": AUTHOR_LIST[0], "content": ""}
|
| 291 |
if "editor_mode" not in st.session_state: st.session_state.editor_mode = "rich"
|
|
@@ -296,12 +293,13 @@ else:
|
|
| 296 |
if "autosave_html" not in st.session_state: st.session_state.autosave_html = ""
|
| 297 |
if "autosave_title" not in st.session_state: st.session_state.autosave_title = ""
|
| 298 |
if "autosave_time" not in st.session_state: st.session_state.autosave_time = None
|
|
|
|
|
|
|
| 299 |
|
| 300 |
st.divider()
|
| 301 |
mode_label = f"📝 記事の編集 (ID: {st.session_state.edit_id})" if st.session_state.edit_id else "✍️ 新規記事の投稿"
|
| 302 |
st.subheader(mode_label)
|
| 303 |
|
| 304 |
-
# --- 自動保存の復元バナー ---
|
| 305 |
if st.session_state.autosave_html and not st.session_state.working_html:
|
| 306 |
saved_time = st.session_state.autosave_time or "不明"
|
| 307 |
st.info(f"💾 自動保存された下書きがあります({saved_time})")
|
|
@@ -310,6 +308,7 @@ else:
|
|
| 310 |
if st.button("📂 復元する"):
|
| 311 |
st.session_state.working_html = st.session_state.autosave_html
|
| 312 |
st.session_state.edit_data["title"] = st.session_state.autosave_title
|
|
|
|
| 313 |
st.rerun()
|
| 314 |
with col_r2:
|
| 315 |
if st.button("🗑️ 破棄する"):
|
|
@@ -318,7 +317,6 @@ else:
|
|
| 318 |
st.session_state.autosave_time = None
|
| 319 |
st.rerun()
|
| 320 |
|
| 321 |
-
# --- タイトル・著者 ---
|
| 322 |
col_t, col_a = st.columns([2, 1])
|
| 323 |
with col_t:
|
| 324 |
title_input = st.text_input("記事タイトル", value=st.session_state.edit_data["title"])
|
|
@@ -329,7 +327,7 @@ else:
|
|
| 329 |
def_auth_idx = 0
|
| 330 |
author_select = st.selectbox("著者", AUTHOR_LIST, index=def_auth_idx)
|
| 331 |
|
| 332 |
-
# --- テンプレート
|
| 333 |
st.divider()
|
| 334 |
st.subheader("📋 テンプレート")
|
| 335 |
template_choice = st.selectbox("テンプレートを選択", list(TEMPLATES.keys()), key="template_select")
|
|
@@ -337,6 +335,8 @@ else:
|
|
| 337 |
selected_html = TEMPLATES[template_choice]
|
| 338 |
if selected_html:
|
| 339 |
st.session_state.working_html = selected_html
|
|
|
|
|
|
|
| 340 |
st.rerun()
|
| 341 |
else:
|
| 342 |
st.warning("テンプレートなしが選択されています")
|
|
@@ -374,7 +374,7 @@ else:
|
|
| 374 |
|
| 375 |
# --- 差し込み画像 ---
|
| 376 |
st.subheader("📷 本文内差し込み画像")
|
| 377 |
-
st.caption("アップロード後、ボタンでエディタに挿入
|
| 378 |
|
| 379 |
inline_files = st.file_uploader(
|
| 380 |
"差し込み画像(複数選択可)",
|
|
@@ -408,20 +408,22 @@ else:
|
|
| 408 |
for i, img_info in enumerate(st.session_state.inline_image_urls):
|
| 409 |
max_w = img_info.get("max_w", 900)
|
| 410 |
img_tag = f'<img src="{img_info["url"]}" alt="{img_info["name"]}" style="width:{max_w}px;max-width:100%;height:auto;display:block;" />'
|
| 411 |
-
col_il1, col_il2, col_il3
|
| 412 |
with col_il1:
|
| 413 |
st.caption(img_info["name"])
|
| 414 |
st.image(img_info["url"], width=120)
|
| 415 |
with col_il2:
|
| 416 |
st.text_area("HTMLタグ", value=img_tag, height=80, key=f"img_tag_{i}")
|
|
|
|
|
|
|
| 417 |
with col_il3:
|
|
|
|
| 418 |
if st.session_state.editor_mode == "rich":
|
| 419 |
-
if st.button("📌
|
| 420 |
st.session_state.working_html += img_tag
|
|
|
|
|
|
|
| 421 |
st.rerun()
|
| 422 |
-
else:
|
| 423 |
-
st.caption("HTMLタグをコピーして\nエディタに貼り付けてください")
|
| 424 |
-
with col_il4:
|
| 425 |
if st.button("🗑️", key=f"del_inline_{i}"):
|
| 426 |
st.session_state.inline_image_urls.pop(i)
|
| 427 |
st.rerun()
|
|
@@ -457,7 +459,8 @@ else:
|
|
| 457 |
["link"],
|
| 458 |
["clean"]
|
| 459 |
],
|
| 460 |
-
key
|
|
|
|
| 461 |
)
|
| 462 |
if rich_html is not None:
|
| 463 |
st.session_state.working_html = rich_html
|
|
@@ -513,21 +516,21 @@ else:
|
|
| 513 |
except Exception as e:
|
| 514 |
st.error(f"AIエラー: {e}")
|
| 515 |
|
| 516 |
-
#
|
| 517 |
if st.session_state.working_html and title_input:
|
| 518 |
st.session_state.autosave_html = st.session_state.working_html
|
| 519 |
st.session_state.autosave_title = title_input
|
| 520 |
st.session_state.autosave_time = time.strftime("%H:%M")
|
| 521 |
st.caption(f"💾 自動保存済み({st.session_state.autosave_time})")
|
| 522 |
|
| 523 |
-
#
|
| 524 |
if st.session_state.working_html:
|
| 525 |
st.divider()
|
| 526 |
st.subheader("👀 プレビュー")
|
| 527 |
preview_html = clean_quill_html(st.session_state.working_html)
|
| 528 |
render_html(preview_html, height=400)
|
| 529 |
|
| 530 |
-
#
|
| 531 |
st.divider()
|
| 532 |
col_save1, col_save2 = st.columns(2)
|
| 533 |
with col_save1:
|
|
@@ -542,6 +545,7 @@ else:
|
|
| 542 |
st.session_state.eyecatch_url = ""
|
| 543 |
st.session_state.inline_image_urls = []
|
| 544 |
st.session_state.editor_mode = "rich"
|
|
|
|
| 545 |
st.rerun()
|
| 546 |
|
| 547 |
if st.button("💾 この内容で保存する", use_container_width=True):
|
|
@@ -572,7 +576,6 @@ else:
|
|
| 572 |
|
| 573 |
if res.status_code in [200, 201]:
|
| 574 |
st.success("保存完了!")
|
| 575 |
-
# 自動保存もクリア
|
| 576 |
st.session_state.autosave_html = ""
|
| 577 |
st.session_state.autosave_title = ""
|
| 578 |
st.session_state.autosave_time = None
|
|
@@ -582,13 +585,14 @@ else:
|
|
| 582 |
st.session_state.eyecatch_url = ""
|
| 583 |
st.session_state.inline_image_urls = []
|
| 584 |
st.session_state.editor_mode = "rich"
|
|
|
|
| 585 |
st.rerun()
|
| 586 |
else:
|
| 587 |
st.error(f"エラー: {res.text}")
|
| 588 |
except Exception as e:
|
| 589 |
st.error(f"通信エラー: {e}")
|
| 590 |
|
| 591 |
-
#
|
| 592 |
st.divider()
|
| 593 |
st.subheader("📋 既存記事の管理")
|
| 594 |
manage_articles = get_all_articles()
|
|
@@ -612,6 +616,7 @@ else:
|
|
| 612 |
st.session_state.eyecatch_url = ec.get("url", "") if isinstance(ec, dict) else (ec or "")
|
| 613 |
st.session_state.inline_image_urls = []
|
| 614 |
st.session_state.editor_mode = "rich"
|
|
|
|
| 615 |
st.rerun()
|
| 616 |
with col_m3:
|
| 617 |
if st.session_state.delete_confirm_id == ma_id:
|
|
|
|
| 5 |
import os
|
| 6 |
import re
|
| 7 |
import io
|
|
|
|
| 8 |
import time
|
| 9 |
from PIL import Image, ImageChops
|
| 10 |
|
|
|
|
| 12 |
GEMINI_KEY = os.environ.get("GEMINI_API_KEY")
|
| 13 |
CMS_SERVICE_ID = os.environ.get("MICROCMS_SERVICE_ID")
|
| 14 |
CMS_API_KEY = os.environ.get("MICROCMS_API_KEY")
|
| 15 |
+
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "1234")
|
| 16 |
AUTHOR_LIST = ["にき", "かん", "なごみ", "蜜柑餅"]
|
| 17 |
SPACE_URL = "https://nagomi-sugarheart-wasemas9thblogs.hf.space"
|
| 18 |
ARTICLES_PER_PAGE = 10
|
| 19 |
|
|
|
|
| 20 |
TEMPLATES = {
|
| 21 |
"テンプレートなし": "",
|
| 22 |
"📅 イベントレポート": """<h2>イベント概要</h2>
|
|
|
|
| 61 |
wrapped = f"""
|
| 62 |
<html><head><style>
|
| 63 |
body {{
|
| 64 |
+
margin: 0; padding: 0;
|
|
|
|
| 65 |
font-family: sans-serif;
|
| 66 |
font-size: 16px;
|
| 67 |
color: #111;
|
| 68 |
word-break: break-word;
|
| 69 |
}}
|
| 70 |
+
img {{ max-width: 100%; height: auto; display: block; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
p {{ margin: 0.5em 0; }}
|
| 72 |
h1,h2,h3 {{ margin: 0.8em 0 0.4em; }}
|
| 73 |
</style></head>
|
|
|
|
| 75 |
"""
|
| 76 |
components.html(wrapped, height=height, scrolling=True)
|
| 77 |
|
| 78 |
+
# --- クリップボードコピーボタン ---
|
| 79 |
+
def copy_button(text, key):
|
| 80 |
+
escaped = text.replace('`', '\\`').replace('\\', '\\\\')
|
| 81 |
+
components.html(f"""
|
| 82 |
+
<button onclick="navigator.clipboard.writeText(`{escaped}`).then(()=>{{
|
| 83 |
+
this.innerText='✅ コピー済み';
|
| 84 |
+
setTimeout(()=>this.innerText='📋 コピー', 1500);
|
| 85 |
+
}})"
|
| 86 |
+
style="
|
| 87 |
+
background:#4A90E2;
|
| 88 |
+
color:white;
|
| 89 |
+
border:none;
|
| 90 |
+
border-radius:6px;
|
| 91 |
+
padding:6px 12px;
|
| 92 |
+
cursor:pointer;
|
| 93 |
+
font-size:13px;
|
| 94 |
+
width:100%;
|
| 95 |
+
">📋 コピー</button>
|
| 96 |
+
""", height=40)
|
| 97 |
+
|
| 98 |
def upload_image_to_microcms(image_file, max_width=1200):
|
| 99 |
try:
|
| 100 |
img = Image.open(image_file)
|
|
|
|
| 135 |
st.error(f"画像アップロードエラー: {e}")
|
| 136 |
return None
|
| 137 |
|
|
|
|
| 138 |
def get_articles(limit=ARTICLES_PER_PAGE, offset=0):
|
| 139 |
if not CMS_SERVICE_ID or not CMS_API_KEY:
|
| 140 |
return [], 0
|
|
|
|
| 149 |
except:
|
| 150 |
return [], 0
|
| 151 |
|
|
|
|
| 152 |
def get_all_articles():
|
| 153 |
if not CMS_SERVICE_ID or not CMS_API_KEY:
|
| 154 |
return []
|
|
|
|
| 182 |
st.title(article.get("title", "無題"))
|
| 183 |
st.caption(f"👤 {article.get('author')} | 📅 {article.get('publishedAt')[:10]}")
|
| 184 |
st.divider()
|
|
|
|
| 185 |
content = clean_quill_html(article.get("content", ""))
|
| 186 |
render_html(content, height=800)
|
|
|
|
| 187 |
st.divider()
|
| 188 |
+
|
| 189 |
share_url = f"{SPACE_URL}?id={article_id}"
|
| 190 |
st.write("🔗 固定リンク:")
|
| 191 |
st.code(share_url)
|
|
|
|
| 207 |
|
| 208 |
tab_view, tab_manage = st.tabs(["📖 ブログを読む", "🛠️ 記事の管理・投稿"])
|
| 209 |
|
|
|
|
|
|
|
|
|
|
| 210 |
with tab_view:
|
| 211 |
st.title("サークル活動ブログ")
|
| 212 |
search_col1, search_col2 = st.columns(2)
|
|
|
|
| 216 |
search_author_list = ["全員"] + AUTHOR_LIST
|
| 217 |
author_filter = st.selectbox("👤 著者で絞り込み", search_author_list, key="search_author")
|
| 218 |
|
|
|
|
| 219 |
if "view_page" not in st.session_state:
|
| 220 |
st.session_state.view_page = 0
|
|
|
|
|
|
|
| 221 |
if "last_search" not in st.session_state:
|
| 222 |
st.session_state.last_search = ""
|
| 223 |
if search_query != st.session_state.last_search:
|
|
|
|
| 225 |
st.session_state.last_search = search_query
|
| 226 |
|
| 227 |
offset = st.session_state.view_page * ARTICLES_PER_PAGE
|
|
|
|
| 228 |
|
|
|
|
| 229 |
if search_query or author_filter != "全員":
|
| 230 |
articles, _ = get_articles(limit=100, offset=0)
|
| 231 |
+
else:
|
| 232 |
+
articles, total_count = get_articles(limit=ARTICLES_PER_PAGE, offset=offset)
|
| 233 |
|
|
|
|
| 234 |
for a in articles:
|
| 235 |
title = a.get("title", "無題")
|
| 236 |
content_html = a.get("content", "")
|
|
|
|
| 257 |
st.query_params.id = a["id"]
|
| 258 |
st.rerun()
|
| 259 |
st.divider()
|
|
|
|
| 260 |
|
|
|
|
| 261 |
if not search_query and author_filter == "全員":
|
| 262 |
+
total_pages = -(-total_count // ARTICLES_PER_PAGE)
|
| 263 |
if total_pages > 1:
|
| 264 |
st.caption(f"{total_count}件中 {offset+1}〜{min(offset+ARTICLES_PER_PAGE, total_count)}件を表示")
|
| 265 |
col_prev, col_page, col_next = st.columns([1, 2, 1])
|
|
|
|
| 276 |
st.session_state.view_page += 1
|
| 277 |
st.rerun()
|
| 278 |
|
|
|
|
|
|
|
|
|
|
| 279 |
with tab_manage:
|
| 280 |
st.title("秘密の管理画面")
|
| 281 |
password = st.text_input("合言葉", type="password")
|
|
|
|
| 283 |
if password == ADMIN_PASSWORD:
|
| 284 |
st.success("ログイン成功!")
|
| 285 |
|
|
|
|
| 286 |
if "edit_id" not in st.session_state: st.session_state.edit_id = None
|
| 287 |
if "edit_data" not in st.session_state: st.session_state.edit_data = {"title": "", "author": AUTHOR_LIST[0], "content": ""}
|
| 288 |
if "editor_mode" not in st.session_state: st.session_state.editor_mode = "rich"
|
|
|
|
| 293 |
if "autosave_html" not in st.session_state: st.session_state.autosave_html = ""
|
| 294 |
if "autosave_title" not in st.session_state: st.session_state.autosave_title = ""
|
| 295 |
if "autosave_time" not in st.session_state: st.session_state.autosave_time = None
|
| 296 |
+
# ✅ Quillの強制再初期化用カウンター
|
| 297 |
+
if "quill_key" not in st.session_state: st.session_state.quill_key = 0
|
| 298 |
|
| 299 |
st.divider()
|
| 300 |
mode_label = f"📝 記事の編集 (ID: {st.session_state.edit_id})" if st.session_state.edit_id else "✍️ 新規記事の投稿"
|
| 301 |
st.subheader(mode_label)
|
| 302 |
|
|
|
|
| 303 |
if st.session_state.autosave_html and not st.session_state.working_html:
|
| 304 |
saved_time = st.session_state.autosave_time or "不明"
|
| 305 |
st.info(f"💾 自動保存された下書きがあります({saved_time})")
|
|
|
|
| 308 |
if st.button("📂 復元する"):
|
| 309 |
st.session_state.working_html = st.session_state.autosave_html
|
| 310 |
st.session_state.edit_data["title"] = st.session_state.autosave_title
|
| 311 |
+
st.session_state.quill_key += 1
|
| 312 |
st.rerun()
|
| 313 |
with col_r2:
|
| 314 |
if st.button("🗑️ 破棄する"):
|
|
|
|
| 317 |
st.session_state.autosave_time = None
|
| 318 |
st.rerun()
|
| 319 |
|
|
|
|
| 320 |
col_t, col_a = st.columns([2, 1])
|
| 321 |
with col_t:
|
| 322 |
title_input = st.text_input("記事タイトル", value=st.session_state.edit_data["title"])
|
|
|
|
| 327 |
def_auth_idx = 0
|
| 328 |
author_select = st.selectbox("著者", AUTHOR_LIST, index=def_auth_idx)
|
| 329 |
|
| 330 |
+
# --- テンプレート ---
|
| 331 |
st.divider()
|
| 332 |
st.subheader("📋 テンプレート")
|
| 333 |
template_choice = st.selectbox("テンプレートを選択", list(TEMPLATES.keys()), key="template_select")
|
|
|
|
| 335 |
selected_html = TEMPLATES[template_choice]
|
| 336 |
if selected_html:
|
| 337 |
st.session_state.working_html = selected_html
|
| 338 |
+
# ✅ Quillを強制再初期化して即反映
|
| 339 |
+
st.session_state.quill_key += 1
|
| 340 |
st.rerun()
|
| 341 |
else:
|
| 342 |
st.warning("テンプレートなしが選択されています")
|
|
|
|
| 374 |
|
| 375 |
# --- 差し込み画像 ---
|
| 376 |
st.subheader("📷 本文内差し込み画像")
|
| 377 |
+
st.caption("アップロード後、ボタンでエディタに挿入またはHTMLタグをコピーしてください")
|
| 378 |
|
| 379 |
inline_files = st.file_uploader(
|
| 380 |
"差し込み画像(複数選択可)",
|
|
|
|
| 408 |
for i, img_info in enumerate(st.session_state.inline_image_urls):
|
| 409 |
max_w = img_info.get("max_w", 900)
|
| 410 |
img_tag = f'<img src="{img_info["url"]}" alt="{img_info["name"]}" style="width:{max_w}px;max-width:100%;height:auto;display:block;" />'
|
| 411 |
+
col_il1, col_il2, col_il3 = st.columns([2, 2, 1])
|
| 412 |
with col_il1:
|
| 413 |
st.caption(img_info["name"])
|
| 414 |
st.image(img_info["url"], width=120)
|
| 415 |
with col_il2:
|
| 416 |
st.text_area("HTMLタグ", value=img_tag, height=80, key=f"img_tag_{i}")
|
| 417 |
+
# ✅ コピーボタン
|
| 418 |
+
copy_button(img_tag, key=f"copy_{i}")
|
| 419 |
with col_il3:
|
| 420 |
+
st.write("")
|
| 421 |
if st.session_state.editor_mode == "rich":
|
| 422 |
+
if st.button("📌 挿入", key=f"insert_btn_{i}"):
|
| 423 |
st.session_state.working_html += img_tag
|
| 424 |
+
# ✅ Quillを強制再初期化して即反映
|
| 425 |
+
st.session_state.quill_key += 1
|
| 426 |
st.rerun()
|
|
|
|
|
|
|
|
|
|
| 427 |
if st.button("🗑️", key=f"del_inline_{i}"):
|
| 428 |
st.session_state.inline_image_urls.pop(i)
|
| 429 |
st.rerun()
|
|
|
|
| 459 |
["link"],
|
| 460 |
["clean"]
|
| 461 |
],
|
| 462 |
+
# ✅ keyを動的に変えることでQuillを強制再初期化
|
| 463 |
+
key=f"quill_editor_{st.session_state.quill_key}"
|
| 464 |
)
|
| 465 |
if rich_html is not None:
|
| 466 |
st.session_state.working_html = rich_html
|
|
|
|
| 516 |
except Exception as e:
|
| 517 |
st.error(f"AIエラー: {e}")
|
| 518 |
|
| 519 |
+
# 自動保存
|
| 520 |
if st.session_state.working_html and title_input:
|
| 521 |
st.session_state.autosave_html = st.session_state.working_html
|
| 522 |
st.session_state.autosave_title = title_input
|
| 523 |
st.session_state.autosave_time = time.strftime("%H:%M")
|
| 524 |
st.caption(f"💾 自動保存済み({st.session_state.autosave_time})")
|
| 525 |
|
| 526 |
+
# プレビュー
|
| 527 |
if st.session_state.working_html:
|
| 528 |
st.divider()
|
| 529 |
st.subheader("👀 プレビュー")
|
| 530 |
preview_html = clean_quill_html(st.session_state.working_html)
|
| 531 |
render_html(preview_html, height=400)
|
| 532 |
|
| 533 |
+
# 保存
|
| 534 |
st.divider()
|
| 535 |
col_save1, col_save2 = st.columns(2)
|
| 536 |
with col_save1:
|
|
|
|
| 545 |
st.session_state.eyecatch_url = ""
|
| 546 |
st.session_state.inline_image_urls = []
|
| 547 |
st.session_state.editor_mode = "rich"
|
| 548 |
+
st.session_state.quill_key += 1
|
| 549 |
st.rerun()
|
| 550 |
|
| 551 |
if st.button("💾 この内容で保存する", use_container_width=True):
|
|
|
|
| 576 |
|
| 577 |
if res.status_code in [200, 201]:
|
| 578 |
st.success("保存完了!")
|
|
|
|
| 579 |
st.session_state.autosave_html = ""
|
| 580 |
st.session_state.autosave_title = ""
|
| 581 |
st.session_state.autosave_time = None
|
|
|
|
| 585 |
st.session_state.eyecatch_url = ""
|
| 586 |
st.session_state.inline_image_urls = []
|
| 587 |
st.session_state.editor_mode = "rich"
|
| 588 |
+
st.session_state.quill_key += 1
|
| 589 |
st.rerun()
|
| 590 |
else:
|
| 591 |
st.error(f"エラー: {res.text}")
|
| 592 |
except Exception as e:
|
| 593 |
st.error(f"通信エラー: {e}")
|
| 594 |
|
| 595 |
+
# 既存記事の管理
|
| 596 |
st.divider()
|
| 597 |
st.subheader("📋 既存記事の管理")
|
| 598 |
manage_articles = get_all_articles()
|
|
|
|
| 616 |
st.session_state.eyecatch_url = ec.get("url", "") if isinstance(ec, dict) else (ec or "")
|
| 617 |
st.session_state.inline_image_urls = []
|
| 618 |
st.session_state.editor_mode = "rich"
|
| 619 |
+
st.session_state.quill_key += 1
|
| 620 |
st.rerun()
|
| 621 |
with col_m3:
|
| 622 |
if st.session_state.delete_confirm_id == ma_id:
|