stable #1
5 changed files with 344 additions and 0 deletions
298
public/assets/js/index.js
Normal file
298
public/assets/js/index.js
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
/*
|
||||||
|
Search
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Android versions
|
||||||
|
[
|
||||||
|
'1.0', '1.1', '1.5', '1.6',
|
||||||
|
'2.0', '2.0.1', '2.1', '2.2', '2.2.1', '2.2.2', '2.2.3', '2.3', '2.3.1', '2.3.2', '2.3.3', '2.3.4', '2.3.5', '2.3.6', '2.3.7',
|
||||||
|
'3.0', '3.1', '3.2', '3.2.1', '3.2.2', '3.2.3', '3.2.4', '3.2.5', '3.2.6',
|
||||||
|
'4.0', '4.0.1', '4.0.2', '4.0.3', '4.0.4', '4.1', '4.1.1', '4.1.2', '4.2', '4.2.1', '4.2.2', '4.3', '4.3.1', '4.4', '4.4.1', '4.4.2', '4.4.3', '4.4.4',
|
||||||
|
'5.0', '5.0.1', '5.0.2', '5.1', '5.1.1',
|
||||||
|
'6.0', '6.0.1',
|
||||||
|
'7.0', '7.1', '7.1.1', '7.1.2',
|
||||||
|
'8.0', '8.1',
|
||||||
|
'9.0'
|
||||||
|
].forEach(v => document.querySelector('#minAndroidVersion').innerHTML += `<option value="${v}">${v}</option>`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search request
|
||||||
|
* @param query
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
let search = (query, callback) => {
|
||||||
|
socket.emit('search', query);
|
||||||
|
socket.on('res', res => {
|
||||||
|
let r = [];
|
||||||
|
for(let i = 0; i < res.length; i++){
|
||||||
|
let _c = [];
|
||||||
|
for(let j = 0; j < query.conditions.length; j++){
|
||||||
|
_c.push(query.conditions[j](res[i]));
|
||||||
|
}
|
||||||
|
if(_c.every(c => c === true) || _c.length === 0){
|
||||||
|
r.push(res[i]);
|
||||||
|
}
|
||||||
|
for(let key in res[i]){
|
||||||
|
if(res[i].hasOwnProperty(key)){
|
||||||
|
if(['size', 'version', 'androidVersion'].indexOf(key) !== -1 && res[i][key].search(/var[iy]/i) !== -1){
|
||||||
|
res[i][key] = '-';
|
||||||
|
}
|
||||||
|
if(key === 'offersIAP'){
|
||||||
|
res[i][key] = res[i][key].toString().replace('true', 'yes').replace('false', 'no');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(r);
|
||||||
|
socket.removeListener('res');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Conditions checking functions
|
||||||
|
*/
|
||||||
|
let _c = {
|
||||||
|
maxPrice: (app, n) => app.price <= n,
|
||||||
|
adFree: app => !app.adSupported,
|
||||||
|
inAppFree: app => !app.offersIAP,
|
||||||
|
minInstalls: (app, n) => app.minInstalls >= n,
|
||||||
|
minScore: (app, n) => app.score.toFixed(1) >= n,
|
||||||
|
maxSize: (app, n) => parseFloat(app.size) <= n,
|
||||||
|
maxUpdatedDays: (app, n) => Math.floor((Date.now() - new Date(app.updated)) / 86400000) <= n,
|
||||||
|
minAndroidVersion: (app, n) => [app.androidVersion, n].sort()[0] === app.androidVersion
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global search settings
|
||||||
|
let Query_Conditions, Query_Price, Query_Number;
|
||||||
|
// Default search text display (for reset)
|
||||||
|
let Query_Text = [...document.querySelectorAll('.search__settings span')].map(el => el.textContent);
|
||||||
|
let Query_Set = [...document.querySelectorAll('.search__setting')].map(el => el.classList.contains('search__setting--set'));
|
||||||
|
|
||||||
|
let resetSettings = () => {
|
||||||
|
// Reset search settings
|
||||||
|
Query_Conditions = [_c.adFree, _c.inAppFree];
|
||||||
|
Query_Price = 'free';
|
||||||
|
Query_Number = 25;
|
||||||
|
document.querySelector('.search__settings').reset();
|
||||||
|
// Reset default text display
|
||||||
|
document.querySelectorAll('.search__settings span').forEach((el, index) => el.textContent = Query_Text[index]);
|
||||||
|
document.querySelectorAll('.search__setting').forEach((el, index) => {
|
||||||
|
if(Query_Set[index]){
|
||||||
|
el.classList.add('search__setting--set');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.classList.remove('search__setting--set');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Init default query settings
|
||||||
|
resetSettings();
|
||||||
|
|
||||||
|
document.querySelector('.resetSettings').onclick = resetSettings;
|
||||||
|
|
||||||
|
// Reset arrow sort symbols in results table
|
||||||
|
let resetSorting = () => document.querySelectorAll('.output__header th').forEach(el => el.classList.remove('output__header--sortUp', 'output__header--sortDown'));
|
||||||
|
|
||||||
|
let searchInput = document.querySelector('.search__input');
|
||||||
|
let searchButton = document.querySelector('.search__submit');
|
||||||
|
|
||||||
|
searchButton.onclick = () => {
|
||||||
|
// Empty query won't work
|
||||||
|
if(!searchInput.value) return;
|
||||||
|
// Time search
|
||||||
|
let startTime = Date.now();
|
||||||
|
let outputMessage = document.querySelector('.output__message');
|
||||||
|
// Query settings
|
||||||
|
let query = { name: searchInput.value, number: Query_Number, price: Query_Price, conditions: Query_Conditions };
|
||||||
|
// Let user know that I'm searching
|
||||||
|
outputMessage.innerHTML = '<div class="spinner" uk-spinner="ratio: 0.75"></div></div>Searching<span class="loading-dots"></span>';
|
||||||
|
// Reset and hide previous search elements
|
||||||
|
resetSorting();
|
||||||
|
document.querySelectorAll('.output__tabs, .output__tables').forEach(el => el.style.display = 'none');
|
||||||
|
document.querySelector('.output__data').innerHTML = '';
|
||||||
|
document.querySelector('.output2__header').innerHTML = '<tr><th>Permissions \\ Apps</th></tr>';
|
||||||
|
document.querySelector('.output2__data').innerHTML = '';
|
||||||
|
// Send query
|
||||||
|
search(query, res => {
|
||||||
|
if(res.length === 0){
|
||||||
|
return outputMessage.textContent = 'No result.';
|
||||||
|
}
|
||||||
|
// Calculate elapsed time
|
||||||
|
let elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
/*
|
||||||
|
Show results
|
||||||
|
*/
|
||||||
|
// Message
|
||||||
|
outputMessage.textContent = `Results : ${res.length} items (${(res.length / query.number * 100).toFixed(2)}% of ${query.number} apps), elapsed time : ${elapsedTime}s`;
|
||||||
|
let displayedPermissions = [];
|
||||||
|
res.forEach(app => {
|
||||||
|
// Show all permissions
|
||||||
|
app.permissions.forEach(permission => {
|
||||||
|
let p = permission.permission;
|
||||||
|
let d = permission.description;
|
||||||
|
if(displayedPermissions.indexOf(p) === -1){
|
||||||
|
displayedPermissions.push(p);
|
||||||
|
document.querySelector('.output2__data').innerHTML += `<tr class="output2__permission"><th class="output2__permission__name" uk-tooltip="title: ${d}; pos: right">${p}</th></tr>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.querySelector('.output2__header tr').innerHTML += `<th class="output2__app">${app.title}</th>`;
|
||||||
|
// Show apps details
|
||||||
|
document.querySelector('.output__data').innerHTML += `<tr class="output__item" data-link="${app.url}"><td class="output__data__name"><img class="output__icon" src="${app.icon}" alt="${app.title}">${app.title}</td><td class="output__data__description">${app.summary}</td><td class="output__data__installs">${app.minInstalls.toLocaleString()}</td><td class="output__data__score">${app.score.toFixed(1)}</td><td class="output__data__size">${app.size.endsWith('k') ? `0.${app.size.slice(0, -1)}M` : app.size}</td><td class="output__data__version">${app.version}</td><td class="output__data__last-update" data-date=${app.updated}">${new Date(app.updated).toDateString()}</td><td class="output__data__price">${app.priceText}</td><td class="output__data__ads">${app.adSupported}</td><td class="output__data__IAP">${app.offersIAP}</td><td class="output__data__android-version">${app.androidVersion}</td><td class="output__data__permissions">${app.permissions.length}</td></tr>`;
|
||||||
|
});
|
||||||
|
res.forEach(app => {
|
||||||
|
// Show permissions per app
|
||||||
|
for(let i = 0; i < displayedPermissions.length; i++){
|
||||||
|
document.querySelectorAll('.output2__permission')[i].innerHTML += `<td>${app.permissions.map(p => p.permission).indexOf(displayedPermissions[i]) !== -1 ? '•' : ''}</td>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Display tables
|
||||||
|
document.querySelectorAll('.output__tabs, .output__tables').forEach(el => el.style.display = '');
|
||||||
|
// Open link on click
|
||||||
|
document.querySelectorAll('.output__item').forEach(el => {
|
||||||
|
let d = false;
|
||||||
|
el.onclick = () => {
|
||||||
|
if(window.getSelection().toString().length > 0) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
if(!d) window.open(el.getAttribute('data-link'));
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
el.ondblclick = () => d = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
searchInput.onkeydown = e => {
|
||||||
|
if(e.key === 'Enter') searchButton.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
searchInput.focus();
|
||||||
|
|
||||||
|
let inputs = document.querySelectorAll('.search__settings input, .search__settings select');
|
||||||
|
inputs.forEach(el => el.oninput = () => {
|
||||||
|
/*
|
||||||
|
Set query conditions from user input
|
||||||
|
*/
|
||||||
|
let filter = el.id;
|
||||||
|
Query_Conditions = [];
|
||||||
|
inputs.forEach(el => {
|
||||||
|
if([el.min, 'on', ''].indexOf(el.value) === -1 || el.checked){
|
||||||
|
let filter = el.id;
|
||||||
|
switch(filter){
|
||||||
|
case 'maxPrice':
|
||||||
|
case 'minScore':
|
||||||
|
case 'maxSize':
|
||||||
|
case 'maxUpdatedDays':
|
||||||
|
case 'minAndroidVersion':
|
||||||
|
Query_Conditions.push(app => _c[filter](app, parseFloat(el.value)));
|
||||||
|
break;
|
||||||
|
case 'adFree':
|
||||||
|
case 'inAppFree':
|
||||||
|
Query_Conditions.push(_c[filter]);
|
||||||
|
break;
|
||||||
|
case 'minInstalls':
|
||||||
|
let range = [0, 1, 1000, 10000, 100000, 1000000, 10000000];
|
||||||
|
Query_Conditions.push(app => _c[filter](app, range[parseInt(el.value)]));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
el.parentElement.classList.add('search__setting--set');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.parentElement.classList.remove('search__setting--set')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
Display proper conditions text from user input
|
||||||
|
*/
|
||||||
|
Query_Number = Math.floor(Query_Conditions.length * 7.5);
|
||||||
|
if(el.type === 'range'){
|
||||||
|
let text = document.querySelector(`.${filter}-text`);
|
||||||
|
let suffix = '';
|
||||||
|
let minText = 'min';
|
||||||
|
let maxText = 'max';
|
||||||
|
if(filter === 'maxSize') suffix = 'M';
|
||||||
|
if(filter === 'maxPrice'){
|
||||||
|
minText = 'free';
|
||||||
|
suffix = '$';
|
||||||
|
}
|
||||||
|
if(filter === 'maxPrice'){
|
||||||
|
if(el.value === '0')
|
||||||
|
Query_Price = 'free';
|
||||||
|
else
|
||||||
|
Query_Price = 'all';
|
||||||
|
}
|
||||||
|
if(filter === 'minInstalls')
|
||||||
|
maxText = '10M+';
|
||||||
|
if(el.value === el.min)
|
||||||
|
text.textContent = minText;
|
||||||
|
else if(el.value === el.max)
|
||||||
|
text.textContent = maxText;
|
||||||
|
else {
|
||||||
|
if(filter === 'minInstalls'){
|
||||||
|
let range = ['0', '1', '1k', '10k', '100k', '1M', '10M'];
|
||||||
|
return text.textContent = range[parseInt(el.value)];
|
||||||
|
}
|
||||||
|
text.textContent = `${el.value}${suffix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Sort
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.querySelectorAll('.output__header th').forEach(el => el.onclick = () => {
|
||||||
|
// App description and version cannot be sorted
|
||||||
|
if(['output__header__description', 'output__header__version'].indexOf(el.className) !== -1) return;
|
||||||
|
// Reset and re-sort
|
||||||
|
resetSorting();
|
||||||
|
let items = [...document.querySelectorAll('.output__item')];
|
||||||
|
let sortedItems = items.slice(0);
|
||||||
|
let getItem = item => item.querySelector(`.output__data__${el.className.split('__').slice(-1)[0]}`).textContent;
|
||||||
|
if(el.className === 'output__header__last-update'){
|
||||||
|
// Use UNIX timestamp instead of interpreted date string
|
||||||
|
getItem = item => item.querySelector(`.output__data__${el.className.split('__').slice(-1)[0]}`).getAttribute('data-date');
|
||||||
|
}
|
||||||
|
switch(el.className){
|
||||||
|
// Different comparison function for each data type
|
||||||
|
case 'output__header__name':
|
||||||
|
sortedItems.sort((a, b) => getItem(a) < getItem(b) ? -1 : 1);
|
||||||
|
break;
|
||||||
|
case 'output__header__installs':
|
||||||
|
sortedItems.sort((a, b) => parseInt(getItem(a).replace(/\s/g, '')) - parseInt(getItem(b).replace(/\s/g, '')));
|
||||||
|
break;
|
||||||
|
case 'output__header__score':
|
||||||
|
case 'output__header__permissions':
|
||||||
|
sortedItems.sort((a, b) => getItem(a) - getItem(b));
|
||||||
|
break;
|
||||||
|
case 'output__header__size':
|
||||||
|
case 'output__header__android-version':
|
||||||
|
sortedItems.sort((a, b) => getItem(a).slice(0, -1) - getItem(b).slice(0, -1));
|
||||||
|
break;
|
||||||
|
case 'output__header__last-update':
|
||||||
|
sortedItems.sort((a, b) => parseInt(getItem(a)) - parseInt(getItem(b)));
|
||||||
|
break;
|
||||||
|
case 'output__header__price':
|
||||||
|
sortedItems = [].concat(items.filter(el => getItem(el) === 'Free'),
|
||||||
|
items.filter(el => getItem(el) !== 'Free').sort((a, b) => [getItem(a), getItem(b)].sort()[0] === getItem(a) ? -1 : 1));
|
||||||
|
break;
|
||||||
|
case 'output__header__ads':
|
||||||
|
case 'output__header__IAP':
|
||||||
|
sortedItems.sort(a => getItem(a) === 'yes' ? 1 : -1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Display proper sort symbol
|
||||||
|
if(JSON.stringify(items.map(item => getItem(item))) === JSON.stringify(sortedItems.map(item => getItem(item)))){
|
||||||
|
sortedItems.reverse();
|
||||||
|
el.classList.add('output__header--sortUp');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.classList.add('output__header--sortDown');
|
||||||
|
}
|
||||||
|
// Hide all items
|
||||||
|
items.forEach(item => item.parentNode.removeChild(item));
|
||||||
|
// Unhide all items in proper sort order
|
||||||
|
for(let item of sortedItems){
|
||||||
|
document.querySelector('.output__data').appendChild(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
8
public/assets/js/js.cookie.min.js
vendored
Normal file
8
public/assets/js/js.cookie.min.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Minified by jsDelivr using UglifyJS v3.4.4.
|
||||||
|
* Original file: /npm/js-cookie@2.2.0/src/js.cookie.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
!function(e){var n=!1;if("function"==typeof define&&define.amd&&(define(e),n=!0),"object"==typeof exports&&(module.exports=e(),n=!0),!n){var o=window.Cookies,t=window.Cookies=e();t.noConflict=function(){return window.Cookies=o,t}}}(function(){function g(){for(var e=0,n={};e<arguments.length;e++){var o=arguments[e];for(var t in o)n[t]=o[t]}return n}return function e(l){function C(e,n,o){var t;if("undefined"!=typeof document){if(1<arguments.length){if("number"==typeof(o=g({path:"/"},C.defaults,o)).expires){var r=new Date;r.setMilliseconds(r.getMilliseconds()+864e5*o.expires),o.expires=r}o.expires=o.expires?o.expires.toUTCString():"";try{t=JSON.stringify(n),/^[\{\[]/.test(t)&&(n=t)}catch(e){}n=l.write?l.write(n,e):encodeURIComponent(String(n)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),e=(e=(e=encodeURIComponent(String(e))).replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent)).replace(/[\(\)]/g,escape);var i="";for(var c in o)o[c]&&(i+="; "+c,!0!==o[c]&&(i+="="+o[c]));return document.cookie=e+"="+n+i}e||(t={});for(var a=document.cookie?document.cookie.split("; "):[],s=/(%[0-9A-Z]{2})+/g,f=0;f<a.length;f++){var p=a[f].split("="),d=p.slice(1).join("=");this.json||'"'!==d.charAt(0)||(d=d.slice(1,-1));try{var u=p[0].replace(s,decodeURIComponent);if(d=l.read?l.read(d,u):l(d,u)||d.replace(s,decodeURIComponent),this.json)try{d=JSON.parse(d)}catch(e){}if(e===u){t=d;break}e||(t[u]=d)}catch(e){}}return t}}return(C.set=C).get=function(e){return C.call(C,e)},C.getJSON=function(){return C.apply({json:!0},[].slice.call(arguments))},C.defaults={},C.remove=function(e,n){C(e,"",g(n,{expires:-1}))},C.withConverter=e,C}(function(){})});
|
||||||
|
//# sourceMappingURL=/sm/31d5cd1b58ce5e6231e4ea03a69b2801a53e76e98152bc29dc82a494ed0a1ee6.map
|
||||||
32
public/assets/js/main.js
Normal file
32
public/assets/js/main.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Socket.IO connection
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
// Animated loading dots
|
||||||
|
setInterval(() => document.querySelectorAll('.loading-dots').forEach(el => el.textContent.length < 3 ? el.textContent += '.' : el.textContent = ''), 500);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dark mode
|
||||||
|
*/
|
||||||
|
let toggleDark = () => {
|
||||||
|
// Toggle UIkit properties
|
||||||
|
['uk-light', 'uk-background-secondary'].forEach(Class => [document.body, document.querySelector('.uk-modal-body')].forEach(el => el.classList.toggle(Class)));
|
||||||
|
// Toggle dark button text
|
||||||
|
let darkButton = document.querySelector('.dark');
|
||||||
|
darkButton.textContent = darkButton.textContent === 'Dark' ? 'White' : 'Dark';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector('.dark').onclick = () => {
|
||||||
|
// Save white/dark state
|
||||||
|
if(Cookies.get('dark') === 'true'){
|
||||||
|
Cookies.set('dark', 'false');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Cookies.set('dark', 'true');
|
||||||
|
}
|
||||||
|
// Toggle
|
||||||
|
toggleDark();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restore saved dark style
|
||||||
|
if(Cookies.get('dark') === 'true') toggleDark();
|
||||||
3
public/assets/js/uikit-icons.min.js
vendored
Normal file
3
public/assets/js/uikit-icons.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/assets/js/uikit.min.js
vendored
Normal file
3
public/assets/js/uikit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in a new issue