Skip to content

Commit 5b3d387

Browse files
committed
优化ccw扩展加载
1 parent e2022cd commit 5b3d387

3 files changed

Lines changed: 175 additions & 45 deletions

File tree

src/containers/extension-library.jsx

Lines changed: 165 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ const messages = defineMessages({
7676
description: 'Label for the SharkPool source filter in the extension library',
7777
id: 'tw.extensionLibrary.source.sharkpool'
7878
},
79+
sourceCCW: {
80+
defaultMessage: 'CCW',
81+
description: 'Label for the CCW source filter in the extension library',
82+
id: 'tw.extensionLibrary.source.ccw'
83+
},
7984
sourceOther: {
8085
defaultMessage: 'Other',
8186
description: 'Label for the Other source filter in the extension library',
@@ -175,16 +180,6 @@ const messages = defineMessages({
175180
defaultMessage: 'Click to import',
176181
description: 'Action hint for importing an extension',
177182
id: 'tw.extensionLibrary.action.import'
178-
},
179-
ccwName: {
180-
defaultMessage: '加载CCW扩展',
181-
description: 'Name of the CCW extension loader item',
182-
id: 'tw.extensionLibrary.ccw.name'
183-
},
184-
ccwDescription: {
185-
defaultMessage: '从共创世界加载扩展,可从 https://assets.ccw.site/extensions 获取。',
186-
description: 'Description of the CCW extension loader item',
187-
id: 'tw.extensionLibrary.ccw.description'
188183
}
189184
});
190185

@@ -197,6 +192,7 @@ const SOURCE_KEYS = {
197192
PM: 'pm',
198193
MIST: 'mist',
199194
SHARKPOOL: 'sharkpool',
195+
CCW: 'ccw',
200196
OTHER: 'other',
201197
CUSTOM: 'custom',
202198
SPECIAL: 'special'
@@ -211,11 +207,78 @@ const SOURCE_NAV_ORDER = [
211207
SOURCE_KEYS.MIST,
212208
SOURCE_KEYS.SHARKPOOL,
213209
SOURCE_KEYS.ASTRA,
210+
SOURCE_KEYS.CCW,
214211
SOURCE_KEYS.OTHER
215212
];
216213

217214
const FAVORITES_STORAGE_KEY = 'tw:library-favorites:extensionLibrary';
218-
const CCW_EXTENSION_ID = 'ccw_extension_loader';
215+
const CCW_EXTENSION_API_BASE = 'https://bfs-web.ccw.site/extensions';
216+
217+
const CCW_METADATA_CACHE = {};
218+
219+
const fetchCCWItemMetadata = async (eid) => {
220+
if (CCW_METADATA_CACHE[eid]) {
221+
return CCW_METADATA_CACHE[eid];
222+
}
223+
const response = await fetch(`${CCW_EXTENSION_API_BASE}/${encodeURIComponent(eid)}`);
224+
if (!response.ok) {
225+
throw new Error(`CCW metadata HTTP error! status: ${response.status}`);
226+
}
227+
const data = await response.json();
228+
const metadata = data?.body || data;
229+
if (!metadata || !Array.isArray(metadata.versions) || !metadata.versions.length) {
230+
throw new Error('No versions found for this CCW extension.');
231+
}
232+
if (!metadata.versions[0]?.assetUri) {
233+
throw new Error('The latest version does not include an asset URL.');
234+
}
235+
CCW_METADATA_CACHE[eid] = metadata;
236+
return metadata;
237+
};
238+
239+
// 实时调用CCW API进行搜索/排序,完全参考ccw-ext.html的请求构建逻辑
240+
const fetchCCWExtensions = async (name, sortField, page, perPage) => {
241+
let requestUrl = `${CCW_EXTENSION_API_BASE}?page=${page || 1}&perPage=${perPage || 30}&sortField=${sortField || 'updatedAt'}&sortType=DESC`;
242+
if (name) {
243+
requestUrl += `&name=${encodeURIComponent(name)}`;
244+
}
245+
const response = await fetch(requestUrl);
246+
if (!response.ok) {
247+
throw new Error(`CCW API HTTP error! status: ${response.status}`);
248+
}
249+
const json = await response.json();
250+
if (json?.body?.data) {
251+
return json.body.data;
252+
}
253+
throw new Error('CCW API response format unexpected');
254+
};
255+
256+
// 映射CCW API数据为内部扩展格式,不带标签
257+
const toCCWGalleryItem = (item) => ({
258+
name: item.name || item.eid || '未知扩展',
259+
nameTranslations: {},
260+
description: item.description || '暂无描述',
261+
descriptionTranslations: {},
262+
extensionId: `ccw_${item.eid || item.id}`,
263+
extensionURL: null, // 点击时才通过fetchCCWItemMetadata获取assetUri
264+
iconURL: item.cover || 'https://placehold.co/600x310/f5f5f5/111111?text=No+Cover',
265+
tags: ['ccw'],
266+
credits: [],
267+
docsURI: null,
268+
samples: null,
269+
incompatibleWithScratch: false,
270+
featured: true,
271+
_ccwMeta: {
272+
eid: item.eid,
273+
id: item.id,
274+
publisher: item.publisher,
275+
stats: item.stats,
276+
createdAt: item.createdAt,
277+
updatedAt: item.updatedAt,
278+
versions: item.versions,
279+
activeVersionId: item.activeVersionId
280+
}
281+
});
219282

220283
const getItemSelectionKey = item => item.extensionURL || item.extensionId;
221284
const isBatchSelectableItem = item => {
@@ -558,6 +621,7 @@ class ExtensionLibrary extends React.PureComponent {
558621
super(props);
559622
bindAll(this, [
560623
'executeItemAction',
624+
'fetchAndSetCCWItems',
561625
'getActionLabel',
562626
'getBatchSelectableItems',
563627
'getCardProps',
@@ -594,6 +658,8 @@ class ExtensionLibrary extends React.PureComponent {
594658
gallery: cachedGallery,
595659
galleryError: null,
596660
galleryTimedOut: false,
661+
ccwItems: [],
662+
ccwLoading: false,
597663
query: '',
598664
quickFilters: {
599665
favorites: false,
@@ -605,6 +671,7 @@ class ExtensionLibrary extends React.PureComponent {
605671
selectedItemKeys: [],
606672
selectedSource: SOURCE_KEYS.ALL
607673
};
674+
this._queryTimeout = null;
608675
}
609676
componentDidMount () {
610677
if (!this.state.gallery) {
@@ -630,6 +697,19 @@ class ExtensionLibrary extends React.PureComponent {
630697
clearTimeout(timeout);
631698
});
632699
}
700+
// 初始加载CCW扩展
701+
this.fetchAndSetCCWItems('', 'updatedAt');
702+
}
703+
async fetchAndSetCCWItems (name, sortField) {
704+
this.setState({ccwLoading: true});
705+
try {
706+
const rawItems = await fetchCCWExtensions(name, sortField);
707+
const ccwItems = rawItems.map(item => toCCWGalleryItem(item));
708+
this.setState({ccwItems, ccwLoading: false});
709+
} catch (err) {
710+
log.error(err);
711+
this.setState({ccwItems: [], ccwLoading: false});
712+
}
633713
}
634714
readFavoritesFromStorage () {
635715
let data;
@@ -650,35 +730,27 @@ class ExtensionLibrary extends React.PureComponent {
650730
[SOURCE_KEYS.PM]: messages.sourcePenguinMod,
651731
[SOURCE_KEYS.MIST]: messages.sourceMist,
652732
[SOURCE_KEYS.SHARKPOOL]: messages.sourceSharkPool,
733+
[SOURCE_KEYS.CCW]: messages.sourceCCW,
653734
[SOURCE_KEYS.OTHER]: messages.sourceOther,
654735
[SOURCE_KEYS.CUSTOM]: messages.sourceCustom,
655736
[SOURCE_KEYS.SPECIAL]: messages.sourceBuiltIn
656737
};
657738
return this.props.intl.formatMessage(sourceMessages[sourceKey] || messages.sourceOther);
658739
}
659-
getCCWLoaderItem () {
660-
return {
661-
name: <FormattedMessage {...messages.ccwName} />,
662-
extensionId: CCW_EXTENSION_ID,
663-
iconURL: ccwIcon,
664-
description: <FormattedMessage {...messages.ccwDescription} />,
665-
featured: true,
666-
tags: []
667-
};
668-
}
669740
getLibraryItems () {
670741
const locale = this.props.intl?.locale;
671742
const baseLibrary = extensionLibraryContent
672743
.map(toLibraryItem)
673744
.map(item => translateGalleryItem(item, locale));
674-
baseLibrary.push(toLibraryItem(this.getCCWLoaderItem()));
675745
if (this.state.gallery) {
746+
const ccwItems = this.state.ccwItems.map(toLibraryItem);
676747
return [
677748
...baseLibrary,
678749
toLibraryItem(galleryMore),
679750
...this.state.gallery
680751
.map(toLibraryItem)
681-
.map(item => translateGalleryItem(item, locale))
752+
.map(item => translateGalleryItem(item, locale)),
753+
...ccwItems
682754
];
683755
}
684756
if (this.state.galleryError) {
@@ -717,8 +789,8 @@ class ExtensionLibrary extends React.PureComponent {
717789
let source = SOURCE_KEYS.OTHER;
718790
if (extensionId === 'custom_extension') {
719791
source = SOURCE_KEYS.CUSTOM;
720-
} else if (extensionId === CCW_EXTENSION_ID) {
721-
source = SOURCE_KEYS.OTHER;
792+
} else if (extensionId.startsWith('ccw_')) {
793+
source = SOURCE_KEYS.CCW;
722794
} else if (extensionId === 'procedures_enable_return') {
723795
source = SOURCE_KEYS.SPECIAL;
724796
} else if (item.tags && item.tags.includes('scratch')) {
@@ -735,10 +807,12 @@ class ExtensionLibrary extends React.PureComponent {
735807
source = SOURCE_KEYS.SHARKPOOL;
736808
} else if (item.tags && item.tags.includes('tw')) {
737809
source = SOURCE_KEYS.TW;
810+
} else if (item.tags && item.tags.includes('ccw')) {
811+
source = SOURCE_KEYS.CCW;
738812
}
739813

740814
const isCustomLoad = extensionId === 'custom_extension';
741-
const isCCWLoad = extensionId === CCW_EXTENSION_ID;
815+
const isCCWLoad = source === SOURCE_KEYS.CCW;
742816
const isSpecialAction = extensionId === 'procedures_enable_return';
743817
const isNative = Boolean(extensionId && !item.extensionURL && !item.href && !isCustomLoad && !isCCWLoad && !isSpecialAction);
744818
const candidateValues = new Set([
@@ -840,11 +914,6 @@ class ExtensionLibrary extends React.PureComponent {
840914
return;
841915
}
842916

843-
if (extensionId === CCW_EXTENSION_ID) {
844-
this.props.onOpenCCWExtensionModal();
845-
return;
846-
}
847-
848917
if (extensionId === 'procedures_enable_return') {
849918
this.handleEnableProcedureReturns();
850919
return;
@@ -854,6 +923,49 @@ class ExtensionLibrary extends React.PureComponent {
854923
return;
855924
}
856925

926+
// Handle CCW gallery items: fetch metadata and load extension
927+
if (item._ccwMeta && item._ccwMeta.eid) {
928+
const ccwEid = item._ccwMeta.eid;
929+
fetchCCWItemMetadata(ccwEid)
930+
.then(metadata => {
931+
const versions = Array.isArray(metadata.versions) ? metadata.versions : [];
932+
const selectedVersion = versions[0];
933+
if (!selectedVersion?.assetUri) {
934+
throw new Error('No valid asset URL found for this CCW extension.');
935+
}
936+
const assetUri = selectedVersion.assetUri;
937+
if (this.props.onSetSelectedExtension && this.props.onOpenExtensionImportMethodModal) {
938+
if (this.props.onSetSelectedExtensions) {
939+
this.props.onSetSelectedExtensions([]);
940+
}
941+
this.props.onSetSelectedExtension({
942+
extensionId: ccwEid,
943+
extensionURL: assetUri,
944+
_ccwMeta: metadata
945+
});
946+
this.props.onOpenExtensionImportMethodModal();
947+
} else {
948+
this.props.vm.extensionManager.loadExtensionURL(assetUri)
949+
.then(() => {
950+
if (this.props.onCategorySelected) {
951+
this.props.onCategorySelected(ccwEid);
952+
}
953+
})
954+
.catch(err => {
955+
log.error(err);
956+
// eslint-disable-next-line no-alert
957+
alert(err);
958+
});
959+
}
960+
})
961+
.catch(err => {
962+
log.error(err);
963+
// eslint-disable-next-line no-alert
964+
alert(err || String(err));
965+
});
966+
return;
967+
}
968+
857969
if (extensionId === 'extfind') {
858970
loadExtensionAsText(this.props.vm, extensionURL)
859971
.then(() => {
@@ -933,11 +1045,22 @@ class ExtensionLibrary extends React.PureComponent {
9331045
this.setState({
9341046
selectedSource
9351047
});
1048+
// 切换到CCW时触发实时搜索
1049+
if (selectedSource === SOURCE_KEYS.CCW || selectedSource === SOURCE_KEYS.ALL) {
1050+
this.fetchAndSetCCWItems(this.state.query, 'updatedAt');
1051+
}
9361052
}
9371053
handleQueryChange (query) {
9381054
this.setState({
9391055
query
9401056
});
1057+
// 防抖:用户停止输入500ms后触发CCW搜索
1058+
if (this._queryTimeout) {
1059+
clearTimeout(this._queryTimeout);
1060+
}
1061+
this._queryTimeout = setTimeout(() => {
1062+
this.fetchAndSetCCWItems(query, 'updatedAt');
1063+
}, 500);
9411064
}
9421065
handleToggleQuickFilter (filterKey) {
9431066
this.setState(prevState => ({
@@ -951,6 +1074,7 @@ class ExtensionLibrary extends React.PureComponent {
9511074
this.setState({
9521075
query: ''
9531076
});
1077+
this.fetchAndSetCCWItems('', 'updatedAt');
9541078
}
9551079
handleClearFilters () {
9561080
this.setState({
@@ -964,6 +1088,7 @@ class ExtensionLibrary extends React.PureComponent {
9641088
},
9651089
selectedSource: SOURCE_KEYS.ALL
9661090
});
1091+
this.fetchAndSetCCWItems('', 'updatedAt');
9671092
}
9681093
handleCustomExtensionOpen () {
9691094
this.props.onOpenCustomExtensionModal();
@@ -975,8 +1100,12 @@ class ExtensionLibrary extends React.PureComponent {
9751100
return this.state.selectedItemKeys.includes(getItemSelectionKey(item));
9761101
}
9771102
matchesSource (item, sourceKey) {
1103+
// CCW 扩展只在选中 "CCW" 分类时显示,不出现在 "All" 或 "Other" 中
1104+
if (sourceKey === SOURCE_KEYS.CCW) {
1105+
return item.source === SOURCE_KEYS.CCW;
1106+
}
9781107
if (sourceKey === SOURCE_KEYS.ALL) {
979-
return true;
1108+
return item.source !== SOURCE_KEYS.CCW;
9801109
}
9811110
if (sourceKey === SOURCE_KEYS.OTHER) {
9821111
return ![
@@ -986,7 +1115,8 @@ class ExtensionLibrary extends React.PureComponent {
9861115
SOURCE_KEYS.ASTRA,
9871116
SOURCE_KEYS.PM,
9881117
SOURCE_KEYS.MIST,
989-
SOURCE_KEYS.SHARKPOOL
1118+
SOURCE_KEYS.SHARKPOOL,
1119+
SOURCE_KEYS.CCW
9901120
].includes(item.source);
9911121
}
9921122
return item.source === sourceKey;
@@ -1030,6 +1160,7 @@ class ExtensionLibrary extends React.PureComponent {
10301160
[SOURCE_KEYS.PM]: 0,
10311161
[SOURCE_KEYS.MIST]: 0,
10321162
[SOURCE_KEYS.SHARKPOOL]: 0,
1163+
[SOURCE_KEYS.CCW]: this.state.ccwItems.length,
10331164
[SOURCE_KEYS.OTHER]: 0
10341165
};
10351166
for (const item of items) {
@@ -1286,7 +1417,6 @@ ExtensionLibrary.propTypes = {
12861417
intl: intlShape.isRequired,
12871418
onCategorySelected: PropTypes.func,
12881419
onEnableProcedureReturns: PropTypes.func,
1289-
onOpenCCWExtensionModal: PropTypes.func,
12901420
onOpenCustomExtensionModal: PropTypes.func,
12911421
onOpenExtensionImportMethodModal: PropTypes.func,
12921422
onRequestClose: PropTypes.func,
@@ -1296,4 +1426,4 @@ ExtensionLibrary.propTypes = {
12961426
vm: PropTypes.instanceOf(VM).isRequired
12971427
};
12981428

1299-
export default injectIntl(ExtensionLibrary);
1429+
export default injectIntl(ExtensionLibrary);

translations/messages/node_modules/scratch-paint/src/lib/messages.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)