Nagomi-SugarHeart commited on
Commit
db3347b
·
verified ·
1 Parent(s): 69b1755

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -45
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") # Secretsに移行、フォールバックあり
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("アップロード後、ボタンでエディタに挿入するかHTMLタグをコピーしてください")
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, col_il4 = st.columns([2, 1, 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
  with col_il3:
 
418
  if st.session_state.editor_mode == "rich":
419
- if st.button("📌 エディタに挿入", key=f"insert_btn_{i}"):
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="quill_editor"
 
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: