/** * ShareManager.js * Minimal share manager - only basic share functionality. * Social fission features have been removed in monetization-lite. */ class ShareManager { constructor() { // Default share content this._shareContent = { title: '坦克大战 - 一起来战斗吧!', imageUrl: '', query: '', }; // Cached temp file path from last canvas capture this._cachedImageUrl = ''; // Register share menu and callback ONCE at startup. // The callback reads this._shareContent dynamically so it always // returns the latest share data without needing re-registration. try { if (typeof wx !== 'undefined') { if (wx.showShareMenu) { wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'], }); } if (wx.onShareAppMessage) { wx.onShareAppMessage(() => { console.log('[ShareManager] onShareAppMessage callback, query:', this._shareContent.query); return { title: this._shareContent.title || '坦克大战 - 一起来战斗吧!', imageUrl: this._shareContent.imageUrl || this._cachedImageUrl || '', query: this._shareContent.query || '', }; }); } } } catch (e) { console.warn('[ShareManager] constructor share setup failed:', e); } } /** * Generate a share image from the current game canvas. * Outputs a 5:4 portrait-ratio image so WeChat's share card shows * the full screen without cropping. * * Strategy: * 1. Try offscreen canvas redraw (ideal — contain-fit into 5:4) * 2. Fallback: direct capture with portrait dest dimensions * * @param {function} [callback] - Called with (tempFilePath) on success. */ generateShareImage(callback) { var self = this; try { const srcCanvas = GameGlobal && GameGlobal.canvas; if (!srcCanvas || typeof wx === 'undefined' || !wx.canvasToTempFilePath) { if (callback) callback(); return; } const DPR = GameGlobal.DEVICE_PIXEL_RATIO || 1; const SHARE_W = 500; const SHARE_H = 625; // 5:4 portrait for WeChat share card const srcW = srcCanvas.width / DPR; const srcH = srcCanvas.height / DPR; // --- Try offscreen canvas approach first --- var offCanvas = null; try { offCanvas = wx.createCanvas && wx.createCanvas(); } catch (e2) { offCanvas = null; } if (offCanvas) { try { offCanvas.width = SHARE_W; offCanvas.height = SHARE_H; const offCtx = offCanvas.getContext('2d'); if (offCtx) { // Dark background offCtx.fillStyle = '#0a0e1a'; offCtx.fillRect(0, 0, SHARE_W, SHARE_H); // Contain-fit source into portrait frame const srcRatio = srcW / srcH; const dstRatio = SHARE_W / SHARE_H; var drawW, drawH, dx, dy; if (srcRatio > dstRatio) { drawW = SHARE_W; drawH = SHARE_W / srcRatio; dx = 0; dy = (SHARE_H - drawH) / 2; } else { drawH = SHARE_H; drawW = SHARE_H * srcRatio; dx = (SHARE_W - drawW) / 2; dy = 0; } offCtx.drawImage(srcCanvas, dx, dy, drawW, drawH); // Export offscreen canvas wx.canvasToTempFilePath({ canvas: offCanvas, width: SHARE_W, height: SHARE_H, destWidth: SHARE_W, destHeight: SHARE_H, fileType: 'png', quality: 0.92, success: function(res) { if (res.tempFilePath) { console.log('[ShareManager] Share image (offscreen):', res.tempFilePath); self._cachedImageUrl = res.tempFilePath; if (callback) callback(res.tempFilePath); } else { self._fallbackDirectCapture(srcCanvas, srcW, srcH, SHARE_W, SHARE_H, callback); } }, fail: function() { self._fallbackDirectCapture(srcCanvas, srcW, srcH, SHARE_W, SHARE_H, callback); }, }); return; // done via offscreen path } } catch (e3) { console.warn('[ShareManager] Offscreen canvas draw failed:', e3); } } // --- Fallback: direct canvas capture with portrait output --- this._fallbackDirectCapture(srcCanvas, srcW, srcH, SHARE_W, SHARE_H, callback); } catch (e) { console.warn('[ShareManager] generateShareImage error:', e); if (callback) callback(); } } /** * Fallback: export main canvas directly with portrait dest dimensions. */ _fallbackDirectCapture(canvas, srcW, srcH, outW, outH, callback) { try { wx.canvasToTempFilePath({ canvas: canvas, width: srcW, height: srcH, destWidth: outW, destHeight: outH, fileType: 'png', quality: 0.92, success: function(res) { if (res.tempFilePath) { console.log('[ShareManager] Share image (direct):', res.tempFilePath); this._cachedImageUrl = res.tempFilePath; } if (callback) callback(res && res.tempFilePath ? res.tempFilePath : undefined); }.bind(this), fail: function(err) { console.warn('[ShareManager] Direct canvasToTempFilePath failed:', err); if (callback) callback(); }, }); } catch (e) { console.warn('[ShareManager] _fallbackDirectCapture error:', e); if (callback) callback(); } } /** * Update open data for friend ranking. * @param {number} score * @param {number} level */ updateOpenData(score, level) { try { if (typeof wx !== 'undefined' && wx.setUserCloudStorage) { wx.setUserCloudStorage({ KVDataList: [ { key: 'score', value: String(score) }, { key: 'level', value: String(level) }, ], }); } } catch (e) { console.warn('[ShareManager] updateOpenData failed:', e); } } /** * Re-register the onShareAppMessage callback so the latest * this._shareContent is captured. Called internally whenever * share content changes. */ _refreshShareCallback() { try { if (typeof wx !== 'undefined' && wx.onShareAppMessage) { wx.onShareAppMessage(() => { console.log('[ShareManager] onShareAppMessage callback fired, query:', this._shareContent.query); return { title: this._shareContent.title || '坦克大战 - 一起来战斗吧!', imageUrl: this._shareContent.imageUrl || '', query: this._shareContent.query || '', }; }); } } catch (e) { console.warn('[ShareManager] _refreshShareCallback failed:', e); } } /** * Set share content for the passive share callback (right-corner ··· menu). * Also re-registers the onShareAppMessage callback to guarantee the * latest content is used when the user taps the share button. * @param {object} opts - { title, imageUrl, query } */ setShareContent(opts) { this._shareContent = opts || {}; console.log('[ShareManager] Share content updated:', JSON.stringify(this._shareContent)); // Auto-generate canvas screenshot as share image if none provided if (!this._shareContent.imageUrl && !this._cachedImageUrl) { this.generateShareImage(); } // Re-register callback to ensure WeChat picks up the new content this._refreshShareCallback(); } /** * Trigger a share action (e.g. team invite). * MUST be called within a user-initiated touch event call stack so that * wx.shareAppMessage() is allowed by WeChat policy. * Also updates the passive share callback as a fallback for the ··· menu. * @param {object} opts - { title, imageUrl, query } */ triggerShare(opts) { const data = opts || {}; // Update passive share callback (right-corner ··· menu fallback) this.setShareContent(data); // Ensure we have a share image — generate synchronously-style via cache const imageUrl = data.imageUrl || this._cachedImageUrl || ''; // Directly invoke wx.shareAppMessage() to open the friend-picker panel. // This is permitted because triggerShare is called from a touchstart handler. try { if (typeof wx !== 'undefined' && wx.shareAppMessage) { console.log('[ShareManager] Calling wx.shareAppMessage with query:', data.query); wx.shareAppMessage({ title: data.title || '', imageUrl: imageUrl, query: data.query || '', }); } } catch (e) { console.warn('[ShareManager] wx.shareAppMessage failed, falling back to toast:', e); // Fallback: prompt user to use the ··· menu try { if (typeof wx !== 'undefined' && wx.showToast) { wx.showToast({ title: '请点击右上角「···」转发给好友', icon: 'none', duration: 2500, }); } } catch (e2) { console.warn('[ShareManager] triggerShare fallback failed:', e2); } } } /** * Reset share content to default (clear team invite data). * Called when leaving the team room. */ resetShareContent() { this._shareContent = { title: '坦克大战 - 一起来战斗吧!', imageUrl: '', query: '', }; console.log('[ShareManager] Share content reset to default'); this._refreshShareCallback(); } /** * Share a challenge to friends. * Sets the share content and prompts the user to share via the menu. * @param {number} level * @param {number} score */ shareChallenge(level, score) { this.setShareContent({ title: `我在坦克大战第${level}关拿了${score}分!你能超过我吗?`, imageUrl: '', query: '', }); try { if (typeof wx !== 'undefined' && wx.showToast) { wx.showToast({ title: '请点击右上角「···」分享给好友', icon: 'none', duration: 2500, }); } } catch (e) { console.warn('[ShareManager] shareChallenge failed:', e); } } } module.exports = ShareManager;