|
53 | 53 | ) |
54 | 54 | from .logging_config import get_logger |
55 | 55 | from .media_helpers import ( |
56 | | - _convert_silk_to_wav, |
| 56 | + _convert_silk_to_browser_audio, |
57 | 57 | _detect_image_media_type, |
58 | 58 | _fallback_search_media_by_file_id, |
59 | 59 | _read_and_maybe_decrypt_media, |
@@ -121,9 +121,10 @@ def _resolve_ui_public_dir() -> Optional[Path]: |
121 | 121 | if ui_dir_env: |
122 | 122 | candidates.append(Path(ui_dir_env)) |
123 | 123 |
|
124 | | - # Repo default: `frontend/.output/public` after `npm --prefix frontend run generate`. |
| 124 | + # Repo defaults: generated Nuxt output or checked-in desktop UI assets. |
125 | 125 | repo_root = Path(__file__).resolve().parents[2] |
126 | 126 | candidates.append(repo_root / "frontend" / ".output" / "public") |
| 127 | + candidates.append(repo_root / "desktop" / "resources" / "ui") |
127 | 128 |
|
128 | 129 | for p in candidates: |
129 | 130 | try: |
@@ -622,6 +623,68 @@ def _download_remote_image_to_zip( |
622 | 623 | .wce-audio-actions a { font-size: 0.75rem; color: #07c160; text-decoration: none; } |
623 | 624 | .wce-audio-actions a:hover { text-decoration: underline; } |
624 | 625 |
|
| 626 | +/* Voice message fallback styles (keep close to `frontend/pages/chat/[[username]].vue`). */ |
| 627 | +.wechat-voice-wrapper { display: flex; width: 100%; position: relative; } |
| 628 | +.wechat-voice-bubble { |
| 629 | + border-radius: var(--message-radius); |
| 630 | + position: relative; |
| 631 | + transition: opacity 0.15s ease; |
| 632 | + min-width: 80px; |
| 633 | + max-width: 200px; |
| 634 | + cursor: pointer; |
| 635 | +} |
| 636 | +.wechat-voice-bubble:hover { opacity: 0.85; } |
| 637 | +.wechat-voice-bubble:active { opacity: 0.7; } |
| 638 | +.wechat-voice-sent { background: #95EC69; } |
| 639 | +.wechat-voice-sent::after { |
| 640 | + content: ''; |
| 641 | + position: absolute; |
| 642 | + top: 50%; |
| 643 | + right: -4px; |
| 644 | + transform: translateY(-50%) rotate(45deg); |
| 645 | + width: 10px; |
| 646 | + height: 10px; |
| 647 | + background: #95EC69; |
| 648 | + border-radius: 2px; |
| 649 | +} |
| 650 | +.wechat-voice-received { background: #fff; } |
| 651 | +.wechat-voice-received::before { |
| 652 | + content: ''; |
| 653 | + position: absolute; |
| 654 | + top: 50%; |
| 655 | + left: -4px; |
| 656 | + transform: translateY(-50%) rotate(45deg); |
| 657 | + width: 10px; |
| 658 | + height: 10px; |
| 659 | + background: #fff; |
| 660 | + border-radius: 2px; |
| 661 | +} |
| 662 | +.wechat-voice-content { display: flex; align-items: center; padding: 8px 12px; gap: 8px; } |
| 663 | +.wechat-voice-icon { width: 18px; height: 18px; flex-shrink: 0; color: #1a1a1a; } |
| 664 | +.wechat-quote-voice-icon { width: 14px; height: 14px; color: inherit; } |
| 665 | +.voice-icon-sent { transform: scaleX(-1); } |
| 666 | +.wechat-voice-icon.voice-playing .voice-wave-2 { animation: voice-wave-2 1s infinite; } |
| 667 | +.wechat-voice-icon.voice-playing .voice-wave-3 { animation: voice-wave-3 1s infinite; } |
| 668 | +@keyframes voice-wave-2 { |
| 669 | + 0%, 33% { opacity: 0; } |
| 670 | + 34%, 100% { opacity: 1; } |
| 671 | +} |
| 672 | +@keyframes voice-wave-3 { |
| 673 | + 0%, 66% { opacity: 0; } |
| 674 | + 67%, 100% { opacity: 1; } |
| 675 | +} |
| 676 | +.wechat-voice-duration { font-size: 14px; color: #1a1a1a; } |
| 677 | +.wechat-voice-unread { |
| 678 | + position: absolute; |
| 679 | + top: 50%; |
| 680 | + right: -20px; |
| 681 | + transform: translateY(-50%); |
| 682 | + width: 8px; |
| 683 | + height: 8px; |
| 684 | + border-radius: 50%; |
| 685 | + background: #e75e58; |
| 686 | +} |
| 687 | +
|
625 | 688 | /* Index page helpers. */ |
626 | 689 | .wce-index { min-height: 100vh; background: #EDEDED; } |
627 | 690 | .wce-index-container { max-width: 880px; margin: 0 auto; padding: 24px; } |
@@ -4958,40 +5021,38 @@ def _mark_exported() -> None: |
4958 | 5021 | tw.write(f' <div class="{esc_attr(bubble_base_cls + " " + bubble_dir_cls)}">{render_text_with_emojis(msg.get("content") or "")}</div>\n') |
4959 | 5022 | elif rt == "voice": |
4960 | 5023 | voice = offline_path(msg, "voice") |
| 5024 | + duration_ms = msg.get("voiceLength") |
| 5025 | + width = get_voice_width(duration_ms) |
| 5026 | + seconds = get_voice_duration_in_seconds(duration_ms) |
| 5027 | + voice_dir_cls = "wechat-voice-sent" if is_sent else "wechat-voice-received" |
| 5028 | + content_dir_cls = " flex-row-reverse" if is_sent else "" |
| 5029 | + icon_dir_cls = "voice-icon-sent" if is_sent else "voice-icon-received" |
| 5030 | + voice_id = str(msg.get("id") or "").strip() |
| 5031 | + |
| 5032 | + tw.write(' <div class="wechat-voice-wrapper">\n') |
| 5033 | + tw.write( |
| 5034 | + f' <div class="wechat-voice-bubble msg-radius {esc_attr(voice_dir_cls)}" style="width: {esc_attr(width)}" data-voice-id="{esc_attr(voice_id)}">\n' |
| 5035 | + ) |
| 5036 | + tw.write(f' <div class="wechat-voice-content{esc_attr(content_dir_cls)}">\n') |
| 5037 | + tw.write( |
| 5038 | + f' <svg class="wechat-voice-icon {esc_attr(icon_dir_cls)}" viewBox="0 0 32 32" fill="currentColor">\n' |
| 5039 | + ) |
| 5040 | + tw.write( |
| 5041 | + ' <path d="M10.24 11.616l-4.224 4.192 4.224 4.192c1.088-1.056 1.76-2.56 1.76-4.192s-0.672-3.136-1.76-4.192z"></path>\n' |
| 5042 | + ) |
| 5043 | + tw.write( |
| 5044 | + ' <path class="voice-wave-2" d="M15.199 6.721l-1.791 1.76c1.856 1.888 3.008 4.48 3.008 7.328s-1.152 5.44-3.008 7.328l1.791 1.76c2.336-2.304 3.809-5.536 3.809-9.088s-1.473-6.784-3.809-9.088z"></path>\n' |
| 5045 | + ) |
| 5046 | + tw.write( |
| 5047 | + ' <path class="voice-wave-3" d="M20.129 1.793l-1.762 1.76c3.104 3.168 5.025 7.488 5.025 12.256s-1.921 9.088-5.025 12.256l1.762 1.76c3.648-3.616 5.887-8.544 5.887-14.016s-2.239-10.432-5.887-14.016z"></path>\n' |
| 5048 | + ) |
| 5049 | + tw.write(" </svg>\n") |
| 5050 | + tw.write(f' <span class="wechat-voice-duration">{esc_text(seconds)}"</span>\n') |
| 5051 | + tw.write(" </div>\n") |
| 5052 | + tw.write(" </div>\n") |
4961 | 5053 | if voice: |
4962 | | - duration_ms = msg.get("voiceLength") |
4963 | | - width = get_voice_width(duration_ms) |
4964 | | - seconds = get_voice_duration_in_seconds(duration_ms) |
4965 | | - voice_dir_cls = "wechat-voice-sent" if is_sent else "wechat-voice-received" |
4966 | | - content_dir_cls = " flex-row-reverse" if is_sent else "" |
4967 | | - icon_dir_cls = "voice-icon-sent" if is_sent else "voice-icon-received" |
4968 | | - voice_id = str(msg.get("id") or "").strip() |
4969 | | - |
4970 | | - tw.write(' <div class="wechat-voice-wrapper">\n') |
4971 | | - tw.write( |
4972 | | - f' <div class="wechat-voice-bubble msg-radius {esc_attr(voice_dir_cls)}" style="width: {esc_attr(width)}" data-voice-id="{esc_attr(voice_id)}">\n' |
4973 | | - ) |
4974 | | - tw.write(f' <div class="wechat-voice-content{esc_attr(content_dir_cls)}">\n') |
4975 | | - tw.write( |
4976 | | - f' <svg class="wechat-voice-icon {esc_attr(icon_dir_cls)}" viewBox="0 0 32 32" fill="currentColor">\n' |
4977 | | - ) |
4978 | | - tw.write( |
4979 | | - ' <path d="M10.24 11.616l-4.224 4.192 4.224 4.192c1.088-1.056 1.76-2.56 1.76-4.192s-0.672-3.136-1.76-4.192z"></path>\n' |
4980 | | - ) |
4981 | | - tw.write( |
4982 | | - ' <path class="voice-wave-2" d="M15.199 6.721l-1.791 1.76c1.856 1.888 3.008 4.48 3.008 7.328s-1.152 5.44-3.008 7.328l1.791 1.76c2.336-2.304 3.809-5.536 3.809-9.088s-1.473-6.784-3.809-9.088z"></path>\n' |
4983 | | - ) |
4984 | | - tw.write( |
4985 | | - ' <path class="voice-wave-3" d="M20.129 1.793l-1.762 1.76c3.104 3.168 5.025 7.488 5.025 12.256s-1.921 9.088-5.025 12.256l1.762 1.76c3.648-3.616 5.887-8.544 5.887-14.016s-2.239-10.432-5.887-14.016z"></path>\n' |
4986 | | - ) |
4987 | | - tw.write(" </svg>\n") |
4988 | | - tw.write(f' <span class="wechat-voice-duration">{esc_text(seconds)}"</span>\n') |
4989 | | - tw.write(" </div>\n") |
4990 | | - tw.write(" </div>\n") |
4991 | 5054 | tw.write(f' <audio src="{esc_attr(voice)}" preload="none" class="hidden"></audio>\n') |
4992 | | - tw.write(" </div>\n") |
4993 | | - else: |
4994 | | - tw.write(f' <div class="{esc_attr(bubble_base_cls + " " + bubble_dir_cls)}">{render_text_with_emojis(msg.get("content") or "")}</div>\n') |
| 5055 | + tw.write(" </div>\n") |
4995 | 5056 | elif rt == "file": |
4996 | 5057 | fsrc = offline_path(msg, "file") |
4997 | 5058 | title = str(msg.get("title") or msg.get("content") or "文件").strip() |
@@ -5982,13 +6043,9 @@ def _materialize_voice( |
5982 | 6043 | if not isinstance(data, (bytes, bytearray)): |
5983 | 6044 | data = bytes(data) |
5984 | 6045 |
|
5985 | | - wav = _convert_silk_to_wav(data) |
5986 | | - if wav != data and wav[:4] == b"RIFF": |
5987 | | - ext = "wav" |
5988 | | - payload = wav |
5989 | | - else: |
5990 | | - ext = "silk" |
5991 | | - payload = data |
| 6046 | + payload, ext, _media_type = _convert_silk_to_browser_audio(data, preferred_format="mp3") |
| 6047 | + if not payload: |
| 6048 | + return "", False |
5992 | 6049 |
|
5993 | 6050 | arc = f"media/voices/voice_{int(server_id)}.{ext}" |
5994 | 6051 | zf.writestr(arc, payload) |
|
0 commit comments