|
2 | 2 | 'use strict'; |
3 | 3 |
|
4 | 4 | const API_BASE = 'https://community-api-axuaystczl.cn-hangzhou.fcapp.run'; |
| 5 | + const COMMUNITY_API_BASE = 'https://community-api.pythonide.xin/v2/community'; |
5 | 6 | const APP_STORE_URL = 'https://apps.apple.com/app/id6753987304'; |
6 | 7 | const SITE_ORIGIN = 'https://link.pythonide.xin'; |
7 | 8 | const DEFAULT_TITLE = 'Python IDE 社区作品'; |
|
74 | 75 | community: '社区作品', |
75 | 76 | communityPost: '社区帖子', |
76 | 77 | communityPostSummary: '在 PythonIDE 中查看完整内容、评论和互动。', |
| 78 | + communityProfile: '社区个人主页', |
| 79 | + communityProfileSummary: '在 PythonIDE 中查看创作者的作品、回复和社区动态。', |
77 | 80 | targetComment: '目标评论', |
| 81 | + comments: '评论', |
| 82 | + followers: '粉丝', |
| 83 | + following: '关注', |
| 84 | + works: '作品', |
78 | 85 | remoteImport: '远程导入', |
79 | 86 | importRemoteProject: '导入远程项目', |
80 | 87 | missingProjectURL: '缺少项目地址', |
|
161 | 168 | community: 'Community work', |
162 | 169 | communityPost: 'Community post', |
163 | 170 | communityPostSummary: 'View the full post, comments, and interactions in PythonIDE.', |
| 171 | + communityProfile: 'Community profile', |
| 172 | + communityProfileSummary: 'View this creator’s work, replies, and community activity in PythonIDE.', |
164 | 173 | targetComment: 'Target comment', |
| 174 | + comments: 'Comments', |
| 175 | + followers: 'Followers', |
| 176 | + following: 'Following', |
| 177 | + works: 'Works', |
165 | 178 | remoteImport: 'Remote import', |
166 | 179 | importRemoteProject: 'Import remote project', |
167 | 180 | missingProjectURL: 'Project URL missing', |
|
248 | 261 | function parseCommunityRoute(url) { |
249 | 262 | if (url.hash) return null; |
250 | 263 | const pathParts = url.pathname.split('/'); |
251 | | - if (pathParts.length !== 3 || pathParts[0] !== '' || pathParts[1] !== 'community') return null; |
| 264 | + if (pathParts[0] !== '' || pathParts[1] !== 'community') return null; |
| 265 | + |
| 266 | + if (pathParts.length === 4 && pathParts[2] === 'user') { |
| 267 | + const userID = normalizedCommunityIdentifier(decodeSegment(pathParts[3])); |
| 268 | + if (!userID) return null; |
| 269 | + |
| 270 | + const allowedQueryNames = new Set(['lang', 'v']); |
| 271 | + for (const name of url.searchParams.keys()) { |
| 272 | + if (!allowedQueryNames.has(name) || url.searchParams.getAll(name).length !== 1) return null; |
| 273 | + } |
| 274 | + if (url.searchParams.has('lang') && !['zh', 'en'].includes(url.searchParams.get('lang'))) return null; |
| 275 | + |
| 276 | + return { |
| 277 | + type: 'profile', |
| 278 | + userID, |
| 279 | + path: `/community/user/${encodeURIComponent(userID)}`, |
| 280 | + }; |
| 281 | + } |
| 282 | + |
| 283 | + if (pathParts.length !== 3 || pathParts[2] === 'user') return null; |
252 | 284 |
|
253 | 285 | const postID = normalizedCommunityIdentifier(decodeSegment(pathParts[2])); |
254 | 286 | if (!postID) return null; |
|
306 | 338 | } |
307 | 339 |
|
308 | 340 | function customURLFor(route) { |
| 341 | + if (route.type === 'profile') { |
| 342 | + return `pythonide://community/user/${encodeURIComponent(route.userID)}`; |
| 343 | + } |
309 | 344 | if (route.type === 'community') { |
310 | 345 | const path = `pythonide://community/post/${encodeURIComponent(route.postID)}`; |
311 | 346 | return route.commentID |
|
534 | 569 | let toastTimer = null; |
535 | 570 | let launchTimer = null; |
536 | 571 | let currentScript = null; |
| 572 | + let currentCommunityPost = null; |
| 573 | + let currentProfile = null; |
537 | 574 | let currentView = route.type; |
538 | 575 |
|
539 | 576 | function setLoading(isLoading) { |
|
783 | 820 | } |
784 | 821 | } |
785 | 822 |
|
| 823 | + function communityAvatarURL(user) { |
| 824 | + const explicitAvatar = safeImageURL( |
| 825 | + user?.avatarURL || user?.avatarUrl || user?.avatar_url || user?.imageURL, |
| 826 | + ); |
| 827 | + const avatarSeed = String(user?.avatarSeed || user?.avatarID || '').trim(); |
| 828 | + return explicitAvatar || (avatarSeed |
| 829 | + ? `https://api.dicebear.com/9.x/adventurer/png?seed=${encodeURIComponent(avatarSeed)}&size=160` |
| 830 | + : ''); |
| 831 | + } |
| 832 | + |
| 833 | + function renderCommunityAuthor(user, timestamp = '') { |
| 834 | + const fallbackAuthor = language === 'en' ? 'Community creator' : '社区创作者'; |
| 835 | + const author = String(user?.name || fallbackAuthor).trim() || fallbackAuthor; |
| 836 | + const handle = String(user?.handle || '').trim(); |
| 837 | + const date = formatDate(timestamp); |
| 838 | + const detail = [handle ? `@${handle}` : '', date].filter(Boolean).join(' · ') |
| 839 | + || fallbackAuthor; |
| 840 | + |
| 841 | + setText(el.authorName, author); |
| 842 | + setText(el.authorDetail, detail); |
| 843 | + setText(el.authorInitial, Array.from(author)[0] || 'P'); |
| 844 | + el.authorRow.hidden = false; |
| 845 | + el.authorAvatar.hidden = true; |
| 846 | + el.authorInitial.hidden = false; |
| 847 | + |
| 848 | + const avatarURL = communityAvatarURL(user); |
| 849 | + if (avatarURL) { |
| 850 | + el.authorAvatar.onload = () => { |
| 851 | + el.authorAvatar.hidden = false; |
| 852 | + el.authorInitial.hidden = true; |
| 853 | + }; |
| 854 | + el.authorAvatar.onerror = () => { |
| 855 | + el.authorAvatar.hidden = true; |
| 856 | + el.authorInitial.hidden = false; |
| 857 | + }; |
| 858 | + el.authorAvatar.src = avatarURL; |
| 859 | + el.authorAvatar.alt = language === 'en' ? `${author}'s avatar` : `${author}的头像`; |
| 860 | + } |
| 861 | + } |
| 862 | + |
786 | 863 | function renderProject(script, presentation) { |
787 | 864 | const facts = [ |
788 | 865 | script.entry_file ? `${language === 'en' ? 'Entry' : '入口'} ${script.entry_file}` : '', |
|
915 | 992 | } |
916 | 993 | } |
917 | 994 |
|
| 995 | + function initialCommunityData(type, identifier) { |
| 996 | + const node = document.getElementById('initial-community-data'); |
| 997 | + if (!node) return null; |
| 998 | + try { |
| 999 | + const payload = JSON.parse(node.textContent || '{}'); |
| 1000 | + if (payload?.type !== type || !payload?.data) return null; |
| 1001 | + const value = payload.data; |
| 1002 | + return String(value.id || '') === String(identifier || '') ? value : null; |
| 1003 | + } catch { |
| 1004 | + return null; |
| 1005 | + } |
| 1006 | + } |
| 1007 | + |
918 | 1008 | async function loadScript(initialScript = null) { |
919 | 1009 | currentView = 'script-loading'; |
920 | 1010 | hideStatus(); |
|
984 | 1074 | setLoading(false); |
985 | 1075 | } |
986 | 1076 |
|
987 | | - function loadCommunity() { |
| 1077 | + function communityPostDescription(post) { |
| 1078 | + return String(post?.translatedBody || post?.body || tr('communityPostSummary')) |
| 1079 | + .replace(/\s+/g, ' ') |
| 1080 | + .trim() |
| 1081 | + .slice(0, 280) || tr('communityPostSummary'); |
| 1082 | + } |
| 1083 | + |
| 1084 | + function renderCommunityPost(post) { |
988 | 1085 | currentView = 'community'; |
989 | 1086 | currentScript = null; |
| 1087 | + currentProfile = null; |
| 1088 | + currentCommunityPost = post; |
| 1089 | + el.openApp.disabled = false; |
| 1090 | + hideStatus(); |
| 1091 | + setText(el.openAppLabel, tr('openApp')); |
| 1092 | + |
| 1093 | + const description = communityPostDescription(post); |
| 1094 | + const title = String(post?.translatedTitle || post?.title || description || tr('communityPost')) |
| 1095 | + .trim() |
| 1096 | + .slice(0, 120) || tr('communityPost'); |
| 1097 | + setText(el.eyebrow, `PythonIDE · ${tr('communityPost')}`); |
| 1098 | + setText(el.title, title); |
| 1099 | + setText(el.summary, description); |
| 1100 | + renderCommunityAuthor(post?.author, post?.updatedAt || post?.createdAt); |
| 1101 | + renderGeneric(`PythonIDE · ${tr('communityPost')}`, '#'); |
| 1102 | + |
| 1103 | + const locale = language === 'en' ? 'en-US' : 'zh-CN'; |
| 1104 | + renderStats([ |
| 1105 | + { value: formatCount(post?.likeCount, locale), label: tr('likes') }, |
| 1106 | + { value: formatCount(post?.commentCount, locale), label: tr('comments') }, |
| 1107 | + { value: formatCount(post?.runCount, locale), label: tr('runs') }, |
| 1108 | + { value: formatCount(post?.viewCount, locale), label: tr('views') }, |
| 1109 | + ]); |
| 1110 | + setText(el.actionTitle, tr('openApp')); |
| 1111 | + setText(el.actionDescription, tr('communityPostSummary')); |
| 1112 | + updatePageMetadata(title, description, DEFAULT_SHARE_IMAGE); |
| 1113 | + setLoading(false); |
| 1114 | + } |
| 1115 | + |
| 1116 | + function renderCommunityPostError() { |
| 1117 | + currentView = 'community-error'; |
| 1118 | + currentCommunityPost = null; |
| 1119 | + currentProfile = null; |
990 | 1120 | el.authorRow.hidden = true; |
991 | 1121 | el.openApp.disabled = false; |
992 | 1122 | setText(el.openAppLabel, tr('openApp')); |
993 | | - hideStatus(); |
994 | 1123 | setText(el.eyebrow, `PythonIDE · ${tr('community')}`); |
995 | 1124 | setText(el.title, tr('communityPost')); |
996 | 1125 | setText(el.summary, tr('communityPostSummary')); |
997 | 1126 | renderGeneric(`PythonIDE · ${tr('communityPost')}`, '#'); |
998 | 1127 | renderStats([ |
999 | | - { value: tr('readOnlyValue'), label: tr('preview') }, |
1000 | | - { value: 'App', label: tr('openMethod') }, |
1001 | | - { value: route.commentID ? '1' : '—', label: tr('targetComment') }, |
1002 | | - { value: tr('safeValue'), label: tr('linkStructure') }, |
| 1128 | + { value: '—', label: tr('likes') }, { value: '—', label: tr('comments') }, |
| 1129 | + { value: '—', label: tr('runs') }, { value: '—', label: tr('views') }, |
1003 | 1130 | ]); |
1004 | | - setText(el.actionTitle, tr('openApp')); |
1005 | | - setText(el.actionDescription, tr('communityPostSummary')); |
1006 | 1131 | updatePageMetadata(tr('communityPost'), tr('communityPostSummary'), DEFAULT_SHARE_IMAGE); |
| 1132 | + showStatus(tr('statusUnavailable'), tr('statusUnavailableBody'), true); |
1007 | 1133 | setLoading(false); |
1008 | 1134 | } |
1009 | 1135 |
|
| 1136 | + async function loadCommunity(initialPost = null) { |
| 1137 | + currentView = 'community-loading'; |
| 1138 | + hideStatus(); |
| 1139 | + const hasInitialPost = Boolean(initialPost); |
| 1140 | + if (hasInitialPost) renderCommunityPost(initialPost); |
| 1141 | + else setLoading(true); |
| 1142 | + retryAction = () => loadCommunity(initialPost); |
| 1143 | + |
| 1144 | + const controller = new AbortController(); |
| 1145 | + const timeout = setTimeout(() => controller.abort(), 20000); |
| 1146 | + try { |
| 1147 | + const response = await fetch( |
| 1148 | + `${COMMUNITY_API_BASE}/posts/${encodeURIComponent(route.postID)}`, |
| 1149 | + { headers: { Accept: 'application/json' }, signal: controller.signal }, |
| 1150 | + ); |
| 1151 | + if (!response.ok) throw new Error(`HTTP ${response.status}`); |
| 1152 | + const payload = await response.json(); |
| 1153 | + const post = payload?.data; |
| 1154 | + if (!post || String(post.id || '') !== route.postID) throw new Error('Missing post'); |
| 1155 | + renderCommunityPost(post); |
| 1156 | + } catch { |
| 1157 | + if (!hasInitialPost) renderCommunityPostError(); |
| 1158 | + } finally { |
| 1159 | + clearTimeout(timeout); |
| 1160 | + } |
| 1161 | + } |
| 1162 | + |
| 1163 | + function renderProfile(user) { |
| 1164 | + currentView = 'profile'; |
| 1165 | + currentScript = null; |
| 1166 | + currentCommunityPost = null; |
| 1167 | + currentProfile = user; |
| 1168 | + el.openApp.disabled = false; |
| 1169 | + hideStatus(); |
| 1170 | + setText(el.openAppLabel, tr('openApp')); |
| 1171 | + |
| 1172 | + const fallbackName = tr('communityProfile'); |
| 1173 | + const name = String(user?.name || fallbackName).trim().slice(0, 120) || fallbackName; |
| 1174 | + const handle = String(user?.handle || '').trim(); |
| 1175 | + const description = String(user?.translatedBio || user?.bio || tr('communityProfileSummary')) |
| 1176 | + .replace(/\s+/g, ' ') |
| 1177 | + .trim() |
| 1178 | + .slice(0, 280) || tr('communityProfileSummary'); |
| 1179 | + setText(el.eyebrow, `PythonIDE · ${tr('communityProfile')}`); |
| 1180 | + setText(el.title, name); |
| 1181 | + setText(el.summary, description); |
| 1182 | + renderCommunityAuthor(user, user?.updatedAt || user?.createdAt); |
| 1183 | + renderGeneric(`PythonIDE · ${tr('communityProfile')}`, '@'); |
| 1184 | + |
| 1185 | + const locale = language === 'en' ? 'en-US' : 'zh-CN'; |
| 1186 | + renderStats([ |
| 1187 | + { value: formatCount(user?.followerCount, locale), label: tr('followers') }, |
| 1188 | + { value: formatCount(user?.followingCount, locale), label: tr('following') }, |
| 1189 | + { value: formatCount(user?.workCount, locale), label: tr('works') }, |
| 1190 | + { value: user?.isPro ? 'PRO' : '—', label: handle ? `@${handle}` : tr('community') }, |
| 1191 | + ]); |
| 1192 | + setText(el.actionTitle, tr('openApp')); |
| 1193 | + setText(el.actionDescription, tr('communityProfileSummary')); |
| 1194 | + updatePageMetadata(name, description, communityAvatarURL(user) || DEFAULT_SHARE_IMAGE); |
| 1195 | + setLoading(false); |
| 1196 | + } |
| 1197 | + |
| 1198 | + function renderProfileError() { |
| 1199 | + currentView = 'profile-error'; |
| 1200 | + currentProfile = null; |
| 1201 | + currentCommunityPost = null; |
| 1202 | + el.authorRow.hidden = true; |
| 1203 | + el.openApp.disabled = false; |
| 1204 | + setText(el.openAppLabel, tr('openApp')); |
| 1205 | + setText(el.eyebrow, `PythonIDE · ${tr('community')}`); |
| 1206 | + setText(el.title, tr('communityProfile')); |
| 1207 | + setText(el.summary, tr('communityProfileSummary')); |
| 1208 | + renderGeneric(`PythonIDE · ${tr('communityProfile')}`, '@'); |
| 1209 | + renderStats([ |
| 1210 | + { value: '—', label: tr('followers') }, { value: '—', label: tr('following') }, |
| 1211 | + { value: '—', label: tr('works') }, { value: '—', label: tr('community') }, |
| 1212 | + ]); |
| 1213 | + updatePageMetadata(tr('communityProfile'), tr('communityProfileSummary'), DEFAULT_SHARE_IMAGE); |
| 1214 | + showStatus(tr('statusUnavailable'), tr('statusUnavailableBody'), true); |
| 1215 | + setLoading(false); |
| 1216 | + } |
| 1217 | + |
| 1218 | + async function loadProfile(initialProfile = null) { |
| 1219 | + currentView = 'profile-loading'; |
| 1220 | + hideStatus(); |
| 1221 | + const hasInitialProfile = Boolean(initialProfile); |
| 1222 | + if (hasInitialProfile) renderProfile(initialProfile); |
| 1223 | + else setLoading(true); |
| 1224 | + retryAction = () => loadProfile(initialProfile); |
| 1225 | + |
| 1226 | + const controller = new AbortController(); |
| 1227 | + const timeout = setTimeout(() => controller.abort(), 20000); |
| 1228 | + try { |
| 1229 | + const response = await fetch( |
| 1230 | + `${COMMUNITY_API_BASE}/users/${encodeURIComponent(route.userID)}`, |
| 1231 | + { headers: { Accept: 'application/json' }, signal: controller.signal }, |
| 1232 | + ); |
| 1233 | + if (!response.ok) throw new Error(`HTTP ${response.status}`); |
| 1234 | + const payload = await response.json(); |
| 1235 | + const user = payload?.data?.user; |
| 1236 | + if (!user || String(user.id || '') !== route.userID) throw new Error('Missing user'); |
| 1237 | + renderProfile(user); |
| 1238 | + } catch { |
| 1239 | + if (!hasInitialProfile) renderProfileError(); |
| 1240 | + } finally { |
| 1241 | + clearTimeout(timeout); |
| 1242 | + } |
| 1243 | + } |
| 1244 | + |
1010 | 1245 | function loadHome() { |
1011 | 1246 | currentView = 'home'; |
1012 | 1247 | currentScript = null; |
|
1097 | 1332 | if (currentView === 'script-error') renderScriptError(); |
1098 | 1333 | else if (currentView === 'import') loadImport(); |
1099 | 1334 | else if (currentView === 'short') loadShort(); |
1100 | | - else if (currentView === 'community') loadCommunity(); |
| 1335 | + else if (currentView === 'community' && currentCommunityPost) { |
| 1336 | + renderCommunityPost(currentCommunityPost); |
| 1337 | + } else if (currentView === 'community-error') renderCommunityPostError(); |
| 1338 | + else if (currentView === 'profile' && currentProfile) renderProfile(currentProfile); |
| 1339 | + else if (currentView === 'profile-error') renderProfileError(); |
1101 | 1340 | else if (currentView === 'home') loadHome(); |
1102 | 1341 | } |
1103 | 1342 |
|
|
1135 | 1374 | if (route.type === 'script') loadScript(initialScriptData(route.id)); |
1136 | 1375 | else if (route.type === 'import') loadImport(); |
1137 | 1376 | else if (route.type === 'short') loadShort(); |
1138 | | - else if (route.type === 'community') loadCommunity(); |
| 1377 | + else if (route.type === 'community') { |
| 1378 | + loadCommunity(initialCommunityData('community', route.postID)); |
| 1379 | + } else if (route.type === 'profile') { |
| 1380 | + loadProfile(initialCommunityData('profile', route.userID)); |
| 1381 | + } |
1139 | 1382 | else loadHome(); |
1140 | 1383 | }(typeof globalThis !== 'undefined' ? globalThis : this)); |
0 commit comments