674 lines
23 KiB
JavaScript
674 lines
23 KiB
JavaScript
(function () {
|
||
'use strict';
|
||
|
||
var VALID_TABS = ['cp', 'brother', 'sister'];
|
||
var LAYER_BASE = './fami/assets/layers/';
|
||
var THEMES = {
|
||
cp: {
|
||
background: 'bg-cp.png',
|
||
tab: 'tab-cp.png',
|
||
card: 'card-cp.png',
|
||
center: 'center-cp.png',
|
||
level: 'level-cp.png',
|
||
},
|
||
brother: {
|
||
background: 'bg-brother.png',
|
||
tab: 'tab-brother.png',
|
||
card: 'card-brother.png',
|
||
center: 'center-brother.png',
|
||
level: 'level-brother.png',
|
||
},
|
||
sister: {
|
||
background: 'bg-sister.png',
|
||
tab: 'tab-sister.png',
|
||
card: 'card-sister.png',
|
||
center: 'center-sister.png',
|
||
level: 'level-sister.png',
|
||
},
|
||
};
|
||
|
||
var page = document.getElementById('famiPage');
|
||
var stage = document.getElementById('famiStage');
|
||
var design = document.getElementById('famiDesign');
|
||
var background = document.getElementById('famiBackground');
|
||
var tabJewel = document.getElementById('famiTabJewel');
|
||
var relationList = document.getElementById('famiRelationList');
|
||
var stateButton = document.getElementById('famiStateButton');
|
||
var content = document.getElementById('famiContent');
|
||
var template = document.getElementById('famiRelationTemplate');
|
||
var backButton = document.getElementById('famiBackButton');
|
||
var helpButton = document.getElementById('famiHelpButton');
|
||
var tabButtons = Array.prototype.slice.call(
|
||
document.querySelectorAll('.fami-tabs [data-tab]')
|
||
);
|
||
var params = new URLSearchParams(window.location.search || '');
|
||
var resizeObserver = null;
|
||
|
||
var state = {
|
||
activeTab: normalizeTab(params.get('tab')) || 'cp',
|
||
mock: params.get('mock') === '1',
|
||
tabs: {
|
||
cp: createTabState(),
|
||
brother: createTabState(),
|
||
sister: createTabState(),
|
||
},
|
||
};
|
||
|
||
function createTabState() {
|
||
return {
|
||
loading: false,
|
||
loaded: false,
|
||
error: '',
|
||
relationships: [],
|
||
requestId: 0,
|
||
};
|
||
}
|
||
|
||
function normalizeTab(value) {
|
||
var tab = String(value || '')
|
||
.trim()
|
||
.toLowerCase();
|
||
return VALID_TABS.indexOf(tab) >= 0 ? tab : '';
|
||
}
|
||
|
||
function readNumber(value, fallback) {
|
||
var number = Number(value);
|
||
return Number.isFinite(number) ? number : fallback || 0;
|
||
}
|
||
|
||
function readString(value) {
|
||
return String(value == null ? '' : value).trim();
|
||
}
|
||
|
||
function firstValue() {
|
||
for (var index = 0; index < arguments.length; index += 1) {
|
||
var value = arguments[index];
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
return value;
|
||
}
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
function hasValue(value) {
|
||
return value !== undefined && value !== null && value !== '';
|
||
}
|
||
|
||
function t(key, fallback) {
|
||
return window.HyAppI18n && window.HyAppI18n.t
|
||
? window.HyAppI18n.t(key, fallback)
|
||
: fallback || key;
|
||
}
|
||
|
||
function formatNumber(value) {
|
||
var number = Math.max(0, Math.floor(readNumber(value, 0)));
|
||
return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||
}
|
||
|
||
function layer(fileName) {
|
||
return LAYER_BASE + fileName;
|
||
}
|
||
|
||
function setSource(node, fileName) {
|
||
if (!node) return;
|
||
var source = layer(fileName);
|
||
if (node.getAttribute('src') !== source) {
|
||
node.setAttribute('src', source);
|
||
}
|
||
}
|
||
|
||
function profileFromData(raw) {
|
||
var profile = raw || {};
|
||
return {
|
||
id: readString(profile.user_id || profile.userId || profile.id),
|
||
displayId: readString(
|
||
profile.display_user_id || profile.displayUserId
|
||
),
|
||
name: readString(
|
||
profile.username ||
|
||
profile.nickname ||
|
||
profile.display_name ||
|
||
profile.name
|
||
),
|
||
avatar: readString(
|
||
profile.avatar ||
|
||
profile.avatar_url ||
|
||
profile.avatarUrl ||
|
||
profile.avatar_path
|
||
),
|
||
};
|
||
}
|
||
|
||
function daysFromTimestamp(value) {
|
||
var formedAt = readNumber(value, 0);
|
||
if (formedAt <= 0) return 0;
|
||
if (formedAt < 100000000000) formedAt *= 1000;
|
||
if (Date.now() <= formedAt) return 1;
|
||
return Math.floor((Date.now() - formedAt) / 86400000) + 1;
|
||
}
|
||
|
||
function normalizeLevel(value) {
|
||
var level = Math.floor(readNumber(value, 1));
|
||
return Math.max(1, Math.min(99, level));
|
||
}
|
||
|
||
function normalizeRelationship(raw, tab) {
|
||
var item = raw || {};
|
||
var level = normalizeLevel(item.level || item.current_level);
|
||
var intimacy = Math.max(
|
||
0,
|
||
readNumber(item.intimacy_value || item.intimacyValue, 0)
|
||
);
|
||
var currentThreshold = Math.max(
|
||
0,
|
||
readNumber(
|
||
item.current_level_threshold || item.currentLevelThreshold,
|
||
0
|
||
)
|
||
);
|
||
var nextThreshold = Math.max(
|
||
currentThreshold,
|
||
readNumber(item.next_level_threshold || item.nextLevelThreshold, 0)
|
||
);
|
||
var currentExp = hasValue(firstValue(item.current_exp, item.currentExp))
|
||
? Math.max(
|
||
0,
|
||
readNumber(firstValue(item.current_exp, item.currentExp))
|
||
)
|
||
: Math.max(0, intimacy - currentThreshold);
|
||
var nextExp = hasValue(firstValue(item.next_exp, item.nextExp))
|
||
? Math.max(0, readNumber(firstValue(item.next_exp, item.nextExp)))
|
||
: Math.max(0, nextThreshold - currentThreshold);
|
||
if (!nextExp && intimacy > 0) nextExp = intimacy;
|
||
var neededExp = hasValue(
|
||
firstValue(item.needed_for_next_level, item.neededExp)
|
||
)
|
||
? Math.max(
|
||
0,
|
||
readNumber(
|
||
firstValue(item.needed_for_next_level, item.neededExp)
|
||
)
|
||
)
|
||
: Math.max(0, nextExp - currentExp);
|
||
var explicitProgress = firstValue(
|
||
item.level_progress_percent,
|
||
item.progress_percent,
|
||
item.progressPercent
|
||
);
|
||
var progressPercent = hasValue(explicitProgress)
|
||
? readNumber(explicitProgress, 0)
|
||
: nextExp > 0
|
||
? (currentExp / nextExp) * 100
|
||
: item.max_level
|
||
? 100
|
||
: 0;
|
||
var days = Math.max(
|
||
0,
|
||
readNumber(
|
||
firstValue(
|
||
item.days,
|
||
item.relationship_days,
|
||
daysFromTimestamp(item.formed_at_ms || item.formedAtMS)
|
||
),
|
||
0
|
||
)
|
||
);
|
||
|
||
return {
|
||
id: readString(
|
||
item.relationship_id || item.relationshipId || item.id
|
||
),
|
||
relationType:
|
||
normalizeTab(item.relation_type || item.relationType) || tab,
|
||
status: readString(item.status || 'active').toLowerCase(),
|
||
level: level,
|
||
nextLevel: normalizeLevel(
|
||
firstValue(item.next_level, item.nextLevel, level + 1)
|
||
),
|
||
days: days,
|
||
togetherDays: Math.max(
|
||
0,
|
||
readNumber(
|
||
firstValue(
|
||
item.together_days,
|
||
item.togetherDays,
|
||
item.time_spent_days,
|
||
days
|
||
),
|
||
days
|
||
)
|
||
),
|
||
currentExp: currentExp,
|
||
nextExp: nextExp,
|
||
neededExp: neededExp,
|
||
progressPercent: Math.max(0, Math.min(100, progressPercent)),
|
||
maxLevel: Boolean(item.max_level || item.maxLevel),
|
||
me: profileFromData(item.me || item.me_profile),
|
||
partner: profileFromData(
|
||
item.partner || item.partner_profile || item.cp_user
|
||
),
|
||
};
|
||
}
|
||
|
||
function activeRelationships(payload, tab) {
|
||
var data = payload || {};
|
||
var rows = Array.isArray(data)
|
||
? data
|
||
: data.relationships || data.items || [];
|
||
return (Array.isArray(rows) ? rows : [])
|
||
.map(function (item) {
|
||
return normalizeRelationship(item, tab);
|
||
})
|
||
.filter(function (item) {
|
||
return (
|
||
item.relationType === tab &&
|
||
(!item.status || item.status === 'active')
|
||
);
|
||
});
|
||
}
|
||
|
||
function mockRelationship(tab) {
|
||
return normalizeRelationship(
|
||
{
|
||
relationship_id: 'figma-' + tab,
|
||
relation_type: tab,
|
||
status: 'active',
|
||
level: 1,
|
||
next_level: 2,
|
||
days: 999,
|
||
together_days: 4,
|
||
current_exp: 1000,
|
||
next_exp: 200000,
|
||
needed_for_next_level: 888,
|
||
level_progress_percent: 22.3175965665,
|
||
me: {
|
||
user_id: 'figma-left',
|
||
username: 'Fami',
|
||
avatar: layer('mock-avatar.png'),
|
||
},
|
||
partner: {
|
||
user_id: 'figma-right',
|
||
username: 'Fami',
|
||
avatar: layer('mock-avatar.png'),
|
||
},
|
||
},
|
||
tab
|
||
);
|
||
}
|
||
|
||
function renderAvatar(node, profile) {
|
||
if (!node) return;
|
||
node.replaceChildren();
|
||
var data = profile || {};
|
||
if (data.avatar) {
|
||
var image = document.createElement('img');
|
||
image.src = data.avatar;
|
||
image.alt = data.name || '';
|
||
image.addEventListener('error', function () {
|
||
image.remove();
|
||
node.textContent = (data.name || data.displayId || '?')
|
||
.charAt(0)
|
||
.toUpperCase();
|
||
});
|
||
node.appendChild(image);
|
||
return;
|
||
}
|
||
node.textContent = (data.name || data.displayId || '?')
|
||
.charAt(0)
|
||
.toUpperCase();
|
||
}
|
||
|
||
function renderRelationship(item, tab) {
|
||
var theme = THEMES[tab];
|
||
var fragment = template.content.cloneNode(true);
|
||
var card = fragment.querySelector('.fami-relation-card');
|
||
var levelText = t('cpSpace.levelValue', 'Lv.{level}').replace(
|
||
'{level}',
|
||
formatNumber(item.level)
|
||
);
|
||
var nextLevel = item.maxLevel ? item.level : item.nextLevel;
|
||
var expAwayText = item.maxLevel
|
||
? t('cpSpace.maxLevelExp', '{exp} EXP').replace(
|
||
'{exp}',
|
||
formatNumber(item.currentExp)
|
||
)
|
||
: t('cpSpace.expAway', '{exp} Exp away from Lv. {level}')
|
||
.replace('{exp}', formatNumber(item.neededExp))
|
||
.replace('{level}', formatNumber(nextLevel));
|
||
|
||
setSource(card.querySelector('[data-layer="card"]'), theme.card);
|
||
setSource(card.querySelector('[data-layer="center"]'), theme.center);
|
||
card.querySelectorAll('[data-layer="level"]').forEach(function (node) {
|
||
setSource(node, theme.level);
|
||
});
|
||
renderAvatar(card.querySelector('[data-avatar="me"]'), item.me);
|
||
renderAvatar(
|
||
card.querySelector('[data-avatar="partner"]'),
|
||
item.partner
|
||
);
|
||
card.querySelector('[data-value="days"]').textContent = formatNumber(
|
||
item.days
|
||
);
|
||
card.querySelector('[data-value="exp-away"]').textContent = expAwayText;
|
||
// 两侧徽标在 Figma 中表示这段关系当前等级,不是“当前/下一等级”;下一等级只出现在经验提示里。
|
||
card.querySelector('[data-value="level-left"]').textContent = levelText;
|
||
card.querySelector('[data-value="level-right"]').textContent =
|
||
levelText;
|
||
card.querySelector('[data-value="progress-text"]').textContent =
|
||
String(Math.max(0, Math.floor(item.currentExp))) +
|
||
'/' +
|
||
String(Math.max(0, Math.floor(item.nextExp)));
|
||
card.querySelector('[data-value="progress"]').style.width =
|
||
Math.max(1, Math.min(233, (item.progressPercent / 100) * 233)) +
|
||
'px';
|
||
card.querySelector('[data-value="time"]').textContent = t(
|
||
'cpSpace.timeTogether',
|
||
'Time spent together: {days} days'
|
||
).replace('{days}', formatNumber(item.togetherDays));
|
||
card.setAttribute(
|
||
'aria-label',
|
||
t('cpSpace.levelCard', 'Best friend level {level}').replace(
|
||
'{level}',
|
||
formatNumber(item.level)
|
||
)
|
||
);
|
||
return fragment;
|
||
}
|
||
|
||
function renderTheme() {
|
||
var tab = state.activeTab;
|
||
var theme = THEMES[tab];
|
||
page.setAttribute('data-tab', tab);
|
||
setSource(background, theme.background);
|
||
setSource(tabJewel, theme.tab);
|
||
tabButtons.forEach(function (button) {
|
||
var selected = button.getAttribute('data-tab') === tab;
|
||
button.setAttribute('aria-selected', String(selected));
|
||
button.tabIndex = selected ? 0 : -1;
|
||
if (selected) content.setAttribute('aria-labelledby', button.id);
|
||
});
|
||
}
|
||
|
||
function setStateMessage(message, retry) {
|
||
stateButton.hidden = !message;
|
||
stateButton.textContent = message || '';
|
||
stateButton.setAttribute('data-retry', String(Boolean(retry)));
|
||
stateButton.disabled = !retry;
|
||
}
|
||
|
||
function updateDesignHeight(cardCount) {
|
||
var count = Math.max(1, Number(cardCount || 0));
|
||
var designHeight = Math.max(
|
||
812,
|
||
169 + count * 199 + (count - 1) * 14 + 40
|
||
);
|
||
design.style.setProperty('--design-height', designHeight + 'px');
|
||
stage.style.setProperty('--design-height', designHeight + 'px');
|
||
syncScale();
|
||
}
|
||
|
||
function render() {
|
||
renderTheme();
|
||
var tabState = state.tabs[state.activeTab];
|
||
content.setAttribute('aria-busy', String(tabState.loading));
|
||
relationList.replaceChildren();
|
||
|
||
if (tabState.loading && !tabState.loaded) {
|
||
setStateMessage(t('cpSpace.loading', 'Loading...'), false);
|
||
updateDesignHeight(0);
|
||
return;
|
||
}
|
||
if (tabState.error) {
|
||
setStateMessage(
|
||
tabState.error ||
|
||
t('cpSpace.loadFailed', 'Load failed. Tap to retry.'),
|
||
true
|
||
);
|
||
updateDesignHeight(0);
|
||
return;
|
||
}
|
||
if (!tabState.relationships.length) {
|
||
setStateMessage(
|
||
t('cpSpace.empty', 'No relationship data yet.'),
|
||
false
|
||
);
|
||
updateDesignHeight(0);
|
||
return;
|
||
}
|
||
|
||
setStateMessage('', false);
|
||
tabState.relationships.forEach(function (item) {
|
||
relationList.appendChild(renderRelationship(item, state.activeTab));
|
||
});
|
||
updateDesignHeight(tabState.relationships.length);
|
||
}
|
||
|
||
function syncScale() {
|
||
if (!stage) return;
|
||
var width = stage.getBoundingClientRect().width || window.innerWidth;
|
||
var scale = Math.max(0.1, width / 375);
|
||
var designHeight = parseFloat(
|
||
stage.style.getPropertyValue('--design-height') || '812'
|
||
);
|
||
stage.style.setProperty('--design-scale', String(scale));
|
||
stage.style.setProperty('--stage-height', designHeight * scale + 'px');
|
||
}
|
||
|
||
function bindScale() {
|
||
syncScale();
|
||
if (window.ResizeObserver) {
|
||
resizeObserver = new ResizeObserver(syncScale);
|
||
resizeObserver.observe(stage);
|
||
return;
|
||
}
|
||
window.addEventListener('resize', syncScale);
|
||
window.addEventListener('orientationchange', syncScale);
|
||
}
|
||
|
||
function syncURL(tab) {
|
||
if (!window.history || !window.history.replaceState) return;
|
||
var url = new URL(window.location.href);
|
||
url.searchParams.set('tab', tab);
|
||
// tab 只属于本页视觉和查询状态;replaceState 避免每次切换都污染 App WebView 的返回栈。
|
||
window.history.replaceState(null, '', url.toString());
|
||
}
|
||
|
||
function loadTab(tab, force) {
|
||
var target = normalizeTab(tab) || 'cp';
|
||
var tabState = state.tabs[target];
|
||
if (!force && (tabState.loading || tabState.loaded)) {
|
||
render();
|
||
return Promise.resolve(tabState.relationships);
|
||
}
|
||
var requestId = ++tabState.requestId;
|
||
tabState.loading = true;
|
||
tabState.error = '';
|
||
render();
|
||
|
||
if (state.mock) {
|
||
tabState.relationships = [mockRelationship(target)];
|
||
tabState.loaded = true;
|
||
tabState.loading = false;
|
||
render();
|
||
return Promise.resolve(tabState.relationships);
|
||
}
|
||
|
||
if (
|
||
!window.HyAppAPI ||
|
||
!window.HyAppAPI.cpSpace ||
|
||
!window.HyAppAPI.cpSpace.relationships
|
||
) {
|
||
tabState.loading = false;
|
||
tabState.error = t(
|
||
'cpSpace.loadFailed',
|
||
'Load failed. Tap to retry.'
|
||
);
|
||
render();
|
||
return Promise.reject(new Error('cp_space_api_unavailable'));
|
||
}
|
||
|
||
// 关系类型和分页值原样交给 gateway;页面不猜测用户关系,也不把一个 tab 的结果复制到另一个 tab。
|
||
return window.HyAppAPI.cpSpace
|
||
.relationships({
|
||
relation_type: target,
|
||
page: 1,
|
||
page_size: target === 'cp' ? 1 : 20,
|
||
})
|
||
.then(function (payload) {
|
||
if (requestId !== tabState.requestId) return [];
|
||
tabState.relationships = activeRelationships(payload, target);
|
||
tabState.loaded = true;
|
||
return tabState.relationships;
|
||
})
|
||
.catch(function (error) {
|
||
if (requestId !== tabState.requestId) return [];
|
||
tabState.error =
|
||
(error && error.message) ||
|
||
t('cpSpace.loadFailed', 'Load failed. Tap to retry.');
|
||
return [];
|
||
})
|
||
.then(function (rows) {
|
||
if (requestId !== tabState.requestId) return rows;
|
||
tabState.loading = false;
|
||
render();
|
||
return rows;
|
||
});
|
||
}
|
||
|
||
function setActiveTab(tab, options) {
|
||
var target = normalizeTab(tab) || 'cp';
|
||
state.activeTab = target;
|
||
render();
|
||
if (!options || options.syncURL !== false) syncURL(target);
|
||
return loadTab(target, Boolean(options && options.force));
|
||
}
|
||
|
||
function hasNativeBridge() {
|
||
return Boolean(
|
||
(window.flutter_inappwebview &&
|
||
window.flutter_inappwebview.callHandler) ||
|
||
(window.HyAppBridgeChannel &&
|
||
window.HyAppBridgeChannel.postMessage) ||
|
||
(window.FlutterBridge && window.FlutterBridge.postMessage) ||
|
||
(window.webkit &&
|
||
window.webkit.messageHandlers &&
|
||
window.webkit.messageHandlers.HyAppBridge)
|
||
);
|
||
}
|
||
|
||
function contextualURL(path) {
|
||
var url = new URL(path, window.location.href);
|
||
['token', 'access_token', 'env', 'language', 'lang', 'locale'].forEach(
|
||
function (key) {
|
||
if (params.has(key)) url.searchParams.set(key, params.get(key));
|
||
}
|
||
);
|
||
url.searchParams.set('app_code', 'fami');
|
||
return url.toString();
|
||
}
|
||
|
||
function applyExternalData(payload) {
|
||
var data = payload || {};
|
||
VALID_TABS.forEach(function (tab) {
|
||
if (!data[tab]) return;
|
||
state.tabs[tab].relationships = activeRelationships(
|
||
Array.isArray(data[tab])
|
||
? { relationships: data[tab] }
|
||
: data[tab],
|
||
tab
|
||
);
|
||
state.tabs[tab].loaded = true;
|
||
state.tabs[tab].loading = false;
|
||
state.tabs[tab].error = '';
|
||
});
|
||
if (normalizeTab(data.activeTab)) state.activeTab = data.activeTab;
|
||
render();
|
||
}
|
||
|
||
function bindEvents() {
|
||
tabButtons.forEach(function (button) {
|
||
button.addEventListener('click', function () {
|
||
setActiveTab(button.getAttribute('data-tab'));
|
||
});
|
||
button.addEventListener('keydown', function (event) {
|
||
var current = VALID_TABS.indexOf(
|
||
button.getAttribute('data-tab')
|
||
);
|
||
var next = current;
|
||
if (event.key === 'ArrowLeft') next = current - 1;
|
||
else if (event.key === 'ArrowRight') next = current + 1;
|
||
else if (event.key === 'Home') next = 0;
|
||
else if (event.key === 'End') next = VALID_TABS.length - 1;
|
||
else return;
|
||
event.preventDefault();
|
||
// 标签视觉顺序固定为从左到右;键盘同样按可见顺序循环,RTL 只翻转返回箭头而不倒置关系类型。
|
||
next = (next + VALID_TABS.length) % VALID_TABS.length;
|
||
var targetButton = tabButtons[next];
|
||
targetButton.focus();
|
||
setActiveTab(targetButton.getAttribute('data-tab'));
|
||
});
|
||
});
|
||
|
||
stateButton.addEventListener('click', function () {
|
||
if (stateButton.getAttribute('data-retry') === 'true') {
|
||
loadTab(state.activeTab, true);
|
||
}
|
||
});
|
||
|
||
backButton.addEventListener('click', function () {
|
||
if (
|
||
window.FlutterPageControl &&
|
||
window.FlutterPageControl.postMessage
|
||
) {
|
||
window.FlutterPageControl.postMessage('close_page');
|
||
return;
|
||
}
|
||
if (hasNativeBridge() && window.HyAppBridge) {
|
||
window.HyAppBridge.back();
|
||
return;
|
||
}
|
||
window.history.back();
|
||
});
|
||
|
||
helpButton.addEventListener('click', function () {
|
||
if (hasNativeBridge() && window.HyAppBridge) {
|
||
window.HyAppBridge.post('cpSpaceHelpTap', {
|
||
page: 'fami-cp-space',
|
||
relation_type: state.activeTab,
|
||
});
|
||
return;
|
||
}
|
||
window.location.href = contextualURL('../../cp-rules/');
|
||
});
|
||
|
||
window.addEventListener('hyapp:i18n-ready', render);
|
||
window.addEventListener('hyapp:cp-space-fami-data', function (event) {
|
||
applyExternalData(event.detail || {});
|
||
});
|
||
window.addEventListener('hyapp:cp-space-data', function (event) {
|
||
applyExternalData(event.detail || {});
|
||
});
|
||
}
|
||
|
||
if (hasNativeBridge() && params.get('figma_status') !== '1') {
|
||
page.classList.add('has-native-status');
|
||
}
|
||
bindScale();
|
||
bindEvents();
|
||
if (window.HyAppCPSpaceFamiData) {
|
||
applyExternalData(window.HyAppCPSpaceFamiData);
|
||
setActiveTab(state.activeTab, { syncURL: false });
|
||
} else {
|
||
setActiveTab(state.activeTab, { syncURL: false });
|
||
}
|
||
|
||
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
||
window.HyAppBridge.ready({
|
||
page: 'fami-cp-space',
|
||
app_code: 'fami',
|
||
tabs: VALID_TABS.slice(),
|
||
mock: state.mock,
|
||
});
|
||
}
|
||
})();
|