Mise à jour de @master

This commit is contained in:
xdrm-brackets 2015-10-22 14:06:49 +02:00
parent bc084b068e
commit 3590a9f09a
17 changed files with 782 additions and 0 deletions

77
API.js Executable file
View File

@ -0,0 +1,77 @@
/* classe API */
function APIClass(){};
APIClass.prototype = {
xhr: [], // tableau d'objets pour les requêtes ajax
/* transaction avec le serveur (API.php)
*
* @param pRequest<Object> l'objet passé en JSON à API.php
* @param pHandler<Function> fonction qui s'éxécutera lors de la réponse (1 argument -> réponse<Object>)
*
* @return answer<Object> l'objet retourné par API.php via pHandler (1er argument)
*
***************************************************************************************************
*
* @usecase
* 1. var answerObject = sendRequest(
* 2. { var1: "exemple", var2: 198294 },
* 3. function(rep){ alert(rep); }
* 4. );
* @explain
* 1. on appelle la fonction <=> on créé la requête
* 2. on passe l'objet qui sera envoyé
* 3. on passe une fonction qui utilise un argument (sera la réponse de API.php) (sous forme d'objet)
*
*/
send: function(pRequest, pHandler){
// on efface les requêtes qui sont terminées (toutes celles de this.xhr)
for( var i = 0 ; i < this.xhr.length ; i++ ){
if( this.xhr[i].readyState == 4 ) // si terminée
this.xhr = this.xhr.slice(0,i-1).concat(this.xhr.slice(i,this.xhr.length-1)); // suppression entrée
}
// on créé une nouvelle entrée
this.xhr.push(null);
i = this.xhr.length-1;
// création de l'objet AJAX
if(window.XMLHttpRequest) // IE7+, Firefox, Chrome, Opera, Safari
this.xhr[i] = new XMLHttpRequest();
else // IE5, IE6
this.xhr[i] = new ActiveXObject('Microsoft.XMLHttpRequest');
console.log(pRequest);
var ptrAPI = this;
this.xhr[i].onreadystatechange = function(){
if( ptrAPI.xhr[i].readyState == 4 ){ // si la requête est terminée
/* DEBUG : affiche la réponse BRUTE de API.php */
// console.log('API.php => '+ptrAPI.xhr[i].responseText);
console.log(JSON.parse(ptrAPI.xhr[i].responseText) );
/* si success de requête */
if( [0,200].indexOf(ptrAPI.xhr[i].status) > -1 ){ // si fichier existe et reçu
try{ pHandler( JSON.parse(ptrAPI.xhr[i].responseText) ); } // si on peut parser, on envoie
catch(e){ pHandler({request:'corrupted'}); } // sinon on envoie obj.request = 'corrupted'
}
/* sinon retourne obj.request = 'unreachable' */
else
pHandler({request: 'unreachable'});
}
}
// on créé un formulaire POST (virtuel)
var form = new FormData();
form.append('json', JSON.stringify(pRequest) ); // on créé la variable $_POST['json']=>request
this.xhr[i].open('POST', 'API.php', true);
this.xhr[i].send( form );
}
};

99
API.php Executable file
View File

@ -0,0 +1,99 @@
<?php
/* GESTION DE L'AUTHENTIFICATION - SI L'UTILISATEUR EST CONNECTÉ */
// A faire
/* si l'utilisateur est connecté */
if( true ){
$answer = new stdClass(); // on initialise la réponse (Objet vide)
/* si $_POST['json'] existe */
if( !empty($_POST) && isset($_POST['json']) ){
$request = json_decode( $_POST['json'] ); // on décode la requête
/* si le JSON n'est pas corrompu (décodable) */
if( $request != null ){ // si le json n'est pas corrompu
/* ROUTAGE (niveau 0) */
switch( $request->level_0 ){
/***************/
/* UTILISATEUR */
/***************/
case 'user':
if( isset($request->level_1) ){ include 'manager/user.php'; user_switch_level_1($request, $answer); }
else { $answer->request = 'missing_level_1'; }
break;
/***********/
/* GROUPES */
/***********/
case 'groups':
$answer->type = "group";
break;
/******/
/* UE */
/******/
case 'ues':
break;
/**********/
/* MODULE */
/**********/
case 'modules':
break;
/************/
/* CONTRÔLE */
/************/
case 'tests':
break;
/**************/
/* PARAMETRES */
/**************/
case 'settings':
break;
/***********/
/* DEFAULT */
/***********/
default:
$answer->request = 'unknown_level_0';
break;
}
if( $answer == null )
$answer->request = 'no_level_0';
}else // si json corrompu (undécodable)
$answer->request = 'jsoncorrupted';
}else // $_POST vide [OU] $_POST['json'] pas défini
$answer->request = 'nopost';
// on envoie (affiche) l'objet en JSON
echo json_encode($answer);
}
?>

237
js/pageManager.js Executable file
View File

@ -0,0 +1,237 @@
function pageManager(){};
var ptrPageManager; // pointeur global pour l'utilisation de fonctions de fonctions
pageManager.prototype = {
depJS: null, // la dépendance javascript
depCSS: null, // la dépendance css
xhr: [], // tableau d'objets pour les requêtes ajax
page: null, // l'indice de la page courante dans pagelist
vars: [], // les variables suivant le nom de la page dans l'URL
path: '', // le chemin du dossier contenant les pages (.php)
pagelist: null, // la liste des pages pouvant être chargées
container: null, // élément DOM qui contiendra le contenu des pages à charger
/* =======================================================================
Cette fonction effectue une requête Ajax (compatible à partir de IE5)
PARAMETRES:
- pLink<string> le lien à charger
- pHandler<function> une fonction qui s'éxécutera avec la réponse de la requête passée en paramètre (voir exemples dessous pour pHandler)
- pMethod<string> type de méthode, vaut 'POST' ou 'GET' et vaut 'POST' par défaut ou s'il n'est pas renseigné
- pForm<FormData> formulaire de type FormData() contenant les données à envoyer (uniquement en POST), si pForm vaut GET les données doivent être passées dans l'URL
========================================================================== */
ajax: function(pLink, pHandler, pMethod, pForm){
// on efface les requêtes qui sont terminées et on push une nouvelle
for( var i = 0 ; i < this.xhr.length ; i++ ){
if( this.xhr[i].readyState == 4 ) // si terminée
this.xhr = this.xhr.slice(0,i-1).concat(this.xhr.slice(i,this.xhr.length-1)); // suppression entrée
}
this.xhr.push(true);
i = this.xhr.length-1;
if(window.XMLHttpRequest) // IE7+, Firefox, Chrome, Opera, Safari
this.xhr[i] = new XMLHttpRequest();
else // IE5, IE6
this.xhr[i] = new ActiveXObject('Microsoft.XMLHttpRequest');
var ptrPageManager = this;
this.xhr[i].onreadystatechange = function(){
if( ptrPageManager.xhr[i].readyState == 4 ) // si la requête est terminée
if( [0,200].indexOf(ptrPageManager.xhr[i].status) > -1 ) // si fichier existe et reçu
pHandler(ptrPageManager.xhr[i].responseText);
else // si code d'erreur retourne null
pHandler();
}
// gestion de la méthode
var method = ( typeof pMethod == 'string' && /^POST|GET$/i.test(pMethod) ) ? pMethod.toUpperCase() : 'POST';
// gestion du formulaire si la méthode est POST
var form = ( method == 'POST' && typeof pForm == 'object' && pForm instanceof FormData ) ? pForm : null;
this.xhr[i].open( method, pLink, true);
this.xhr[i].send( form );
},
/***************************************************** [APPLICATION] Ajax() ******************************************************/
// EXEMPLES DE FONCTIONS POUR pHandler //
// 1. var a = function(param){ alert(param); } // les deux notations 1 et 2 sont équivalents
// 2. function a(param){ alert(param); } // les deux notations 1 et 2 sont équivalents
// ajax( 'index.php', a ); // utilisation d'une fonction définie
// ajax( 'index.php', alert ); // utilisation d'une fonction prédéfinie
// ajax( 'index.php', alert, 'GET' ); // utilisation de méthode
// var fd = new FormData(); // création d'un formulaire
// fd.append('var', 100); // ajout de la variable VAR qui vaut 100
// ajax( 'index.php', alert, null, fd ); // saut de paramètre avec null + envoi formulaire
// ajax( 'index.php?var=10', alert, 'GET' ); // envoi formulaire en GET (dans l'url)
// ajax( 'index.php?var=10', alert, 'POST', fd ); // envoi formulaire en GET (dans l'url) + en POST via le formulaire FD
/* =======================================================================
Cette fonction effectue une décomposition de l'URL sur le shéma spécifié dessous
Renvoie pour http://www.exemple.com/dirA/dirB/#/NOMPAGE/VARPAGE
- null si la page n'est pas référencée dans le tableau PAGELIST
- null si le lien ne contient pas /#/NOMPAGE à la fin
- null si NOMPAGE ne contient pas uniquement : lettres, chiffres, underscore
- null si VARPAGE ne contient pas uniquement : lettres, chiffres, underscore
- un objet contenant {page: valeur, var: valeur}
========================================================================== */
explodeURL: function(url_data){
url_data = (arguments.length >= 1) ? url_data : document.URL;
// si pageList est correct et que l'URL correspond à un schéma de page => continue [sinon] return null
if( this.pagelist != null && /^(?:.+)\/#\/([a-z0-9_]+)\/?(?:\/((?:[a-z0-9_]+\/)+)\/?)?$/i.test(url_data) ){
// si la page récupérée dans l'url est dans la liste => renvoi de l'objet [sinon] null
var vars = RegExp.$2.split('/');
while( vars[vars.length-1] == '' ) // on supprime les dernières entrées vides
vars.pop();
return ( this.pagelist.indexOf(RegExp.$1) > -1 ) ? {page: RegExp.$1, var: vars} : null;
}else
return null;
},
/* =======================================================================
Cette fonction ajoute des dépendances (un js et un css) situés dans le répertoire des pages.
pageDir/
_JS/
page1.js
page2.js
_CSS/
page1.css
page2.css
========================================================================== */
loadDependencies: function(){
// si depCSS est un élément du DOM c'est à dire qu'il contient le fichier de la page précédente et qu'il est enfant de <head>, on le détruit
if( typeof this.depCSS == 'object' && this.depCSS instanceof Element && this.depCSS.parentNode == document.head )
document.head.removeChild( this.depCSS );
// si depJS est un élément du DOM c'est à dire qu'il contient le fichier de la page précédente, on le détruit
if( typeof this.depJS == 'object' && this.depJS instanceof Element && this.depJS.parentNode == document.head )
document.head.removeChild( this.depJS );
ptrPageManager = this;
// si le fichier css existe
this.ajax(this.path+'/'+'_CSS'+'/'+this.page+'.css', function(e){
if( e != null ){ // on charge la dépendance CSS si le fichier existe
ptrPageManager.depCSS = document.createElement('link');
ptrPageManager.depCSS.rel = 'stylesheet';
ptrPageManager.depCSS.type = 'text/css';
ptrPageManager.depCSS.href = ptrPageManager.path+'/'+'_CSS'+'/'+ptrPageManager.page+'.css';
document.head.appendChild(ptrPageManager.depCSS);
}else
console.log('[loadDependencies_Error] - ('+ptrPageManager.path+'/'+'_CSS'+'/'+ptrPageManager.page+'.css'+')');
});
// si le fichier js existe
this.ajax(this.path+'/'+'_JS'+'/'+this.page+'.js', function(e){
if( e != null ){ // on charge la dépendance JS si le fichier existe
ptrPageManager.depJS = document.createElement('script');
ptrPageManager.depJS.type = 'text/javascript';
ptrPageManager.depJS.src = ptrPageManager.path+'/'+'_JS'+'/'+ptrPageManager.page+'.js';
document.head.appendChild(ptrPageManager.depJS);
}else
console.log('[loadDependencies_Error] - ('+ptrPageManager.path+'/'+'_JS'+'/'+ptrPageManager.page+'.js'+')');
});
},
/* =======================================================================
Cette fonction est celle qui gère les 2 autres et celle que l'utilisateur utilisera
PARAMETRES:
- pName<string> le nom de la page à charger (lettres, chiffres, underscore) (*)
- pPath<string> chemin (relatif ou absolu) du dossier contenant les pages de même nom de fichier que le nom (extension .php)
- pContainer<Element> l'élément du DOM qui contiendra la page chargée (**)
- pPageList<Array<string>> tableau contenant la liste des pages sous forme de chaînes de caractères (**) (***)
* Le chemin du dossier sans le '/' final si c'est le dossier actuel le chemin est une chaîne vide
Si le dossier est 'page' et que l'on cherche la page 'accUe1l', la requête sera vers 'page/accUe1l.php'
le nom de la page est sensible à la casse
** 1. pPageList et pContainer doivent être mis en paramètres uniquement à la première utilisation
et la première utilisation doit se faire au chargement de la page car elle permetra
de mettre l'URL à jour et/ou charger la page de l'URL
*** la première page du tableau est la page par défaut (qui est chargée si l'URL ne contient
pas la page ou si la page de l'URL ne correspond à aucune page de la liste)
========================================================================== */
setPage: function(pName, pPath, pContainer, pPageList){
// liste de pages si c'est un tableau
var pageList = ( typeof pPageList == 'object' && pPageList instanceof Array ) ? pPageList : null; // si this.pagelist n'est pas overwrite il vaut null
if( pageList != null ){ // si c'est un tableau
for( var i = 0 ; i < pageList.length ; i++ ){ // on parcourt tout les éléments pour vérifier que chaque élément ne contient que : lettres, chiffres, underscore [non]> pageList = null
pageList = ( typeof pageList[i] == 'string' && /^[a-z0-9_]+$/i.test(pageList[i]) ) ? pageList : null;
if( pageList == null ) break; // si le tableau est null stoppe la boucle
}
}
/* on attribue la variable temporaire pageList à l'attribut de l'objet si la variable pageList temporaire n'est pas nulle */
this.pagelist = ( pageList != null ) ? pageList : this.pagelist;
// affecte à l'attribut page la page par défaut (premier élément de pagelist)
this.page = this.pagelist[0];
// affecte pPath à l'attribut path s'il est renseigné
this.path = ( typeof pPath == 'string' ) ? pPath : this.path;
/* on attribue le paramètre pContainer à l'attribut si il est spécifié */
this.container = ( typeof pContainer == 'object' && pContainer instanceof Element ) ? pContainer : this.container;
// si this.pagelist && this.container ne sont pas null &&
if( this.pagelist != null && this.container != null ){
// si le pName est renseigné et qu'il est dans pagelist
if( typeof pName == 'string' && this.pagelist.indexOf(pName) > -1 ){
// affecte pName à l'attribut page
this.page = pName;
// charge le contenu de la page dans le container
var ptrPageManager = this;
// formulaire POST
var fd = new FormData();
for( var i = 0 ; i < this.vars.length ; i++ )
fd.append(this.vars[i], null);
this.ajax(this.path+'/'+this.page+'.php', function(e){
ptrPageManager.container.innerHTML = e;
ptrPageManager.loadDependencies();
}, 'POST', fd);
// change l'URL en conséquences(stateObj, titre, url)
if( this.vars.length > 0 ) // si il y a des variables
window.history.pushState(null, this.page, '#/'+this.page+'/'+this.vars.join('/')+'/');
else // s'il n'y en a pas
window.history.pushState(null, this.page, '#/'+this.page+'/');
}else{ // si la page n'est pas spécifiée ou qu'elle n'est pas dans la liste des pages
var urlGet = this.explodeURL();
// si on a récupéré le numéro de la page dans l'URL et qu'elle fait partie de la liste des pages
if( urlGet != null ){
this.page = urlGet.page;
// charge le contenu de la page dans le container
var ptrThis = this;
// formulaire POST
var fd = new FormData();
this.vars.length = 0;
for( var i = 0 ; i < urlGet.var.length ; i++ ){ // replacing object variables with explodeURL variables
this.vars[i] = urlGet.var[i];
fd.append(this.vars[i], null);
}
this.ajax(this.path+'/'+this.page+'.php', function(e){
ptrThis.container.innerHTML = e;
ptrThis.loadDependencies();
}, 'POST', fd);
// change l'URL en conséquences(stateObj, titre, url)
if( this.vars.length > 0 ) // si il y a des variables
window.history.pushState(null, this.page, '#/'+this.page+'/'+this.vars.join('/')+'/');
else // s'il n'y en a pas
window.history.pushState(null, this.page, '#/'+this.page+'/');
}else // si l'url ne contient rien, on charge la page par défaut
this.setPage(this.pagelist[0]);
}
}else
console.log('pagelist et container manquant');
}
}

145
js/shortcut.js Executable file
View File

@ -0,0 +1,145 @@
/* Retourne le keyCode correspondant à la chaîne
*
* @param keyStore enchaînement de touches sous forme de string
* @param handler function qui s'éxécute lors du raccourci
*
* return keyCode le code de la touche correspondante
*/
function strToKeyCode(str){
// on enregistre le keyCode du premier caractère
var keyCode = str.toUpperCase().charCodeAt(0);
// s'il s'agit d'un caractère uniquement (entre "a" et "z")
if( str.length == 1 && keyCode >= 65 && keyCode <= 90 )
return keyCode; // on retourne le keyCode associé
else
switch( str ){
case 'ctrl': return 17; break;
case 'maj': return 16; break;
case 'alt': return 18; break;
case 'tab': return 9; break;
}
return null;
}
var shortcutList = []; // contient les combinaisons de touches
var shortcutStep = []; // contient l'avancée d'un raccourcis
/* Gestion des raccourcis claviers
*
* @param keyStore enchaînement de touches sous forme de string
* @param handler function qui s'éxécute lors du raccourci
*
*/
function Shortcut(keyStore, handler){
var splittedString = keyStore.toLowerCase().split('+'), // découpe la chaîne (en minuscule) par "+"
splittedKeyCode = new Array(); // contiendra les keyCode de chaque touche
// pour chaque touche, on récupère le keyCode
for( var i = 0 ; i < splittedString.length ; i++ )
splittedKeyCode[i] = strToKeyCode( splittedString[i] ); // on enregistre le keyCode correspondant
// on ajout à la liste globale
eventIndex = shortcutList.length;
shortcutList.push( splittedKeyCode );
// on initialise l'avancement
shortcutStep[eventIndex] = 0;
// creation de la fonction d'évènement
shortcutList[eventIndex].push( function(k, f, h){ /* k<keyCode> ; f<eventIndex> ; h<handler()> */
// on cherche l'avancée
var step = shortcutStep[f];
// on regarde si la touche est bonne
if( shortcutList[f][step] == k ){ // si c'est la touche suivante
if( step >= shortcutList[f].length-2 ){ // si c'était la dernière touche
// on initialise le tableau
for( var i = 0 ; i < shortcutStep[f].length ; i++ )
shortcutStep[f][i] = 0;
h(); // EXECUTION DE : handler();
}else // sinon on incrémente l'avancée
shortcutStep[f]++;
}else // si c'est pas la bonne touche, on réinitialise le tableau
shortcutStep[f] = 0;
});
console.log( shortcutList );
// création de l'évènement
window.addEventListener(
'keydown',
function(e){ e.preventDefault(); shortcutList[eventIndex][shortcutList[eventIndex].length-1](e.keyCode, eventIndex, handler); },
false
);
}
/* quand on lâche une touche, tout les raccourcis s'effacent */
window.addEventListener('keyup', function(){
for( var i = 0 ; i < shortcutStep.length ; i++ )
shortcutStep[i] = 0;
}, false);
/*** UTILISATION ***/
// Shortcut(
// 'ctrl+s',
// function(){ alert('sauvegardé'); }
// );

18
manager/database.php Executable file
View File

@ -0,0 +1,18 @@
<?php
class DataBase{
/* ATTRIBUTS */
private $host;
private $dbname;
private $username;
private $password;
private $connection;
public function __construct($host, $dbname, $username, $password){
this->connection = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
}
}
?>

80
page/_JS/auth.js Executable file
View File

@ -0,0 +1,80 @@
/***********************************************************
* *
* SCRIPT LOCAL DE LA PAGE D'AUTHENTIFICATION *
* *
************************************************************
* *
* [0] Variables *
* [1] Gestion des formulaires *
* [a] Gestion des réponses *
* [b] Initialisation des formulaires *
* *
* *
* *
* *
* *
* *
* *
* *
* *
***********************************************************/
/* [0] Variables
==============================================================*/
var subSections = document.querySelectorAll('hgroup');
/* [1] Gestion des formulaires
==============================================================*/
/* [a] Gestion des réponses
==============================================================*/
/* GESTION DU COMPORTEMENT EN FONCTION DE LA REPONSE POUR LE [LOGIN]
*
* @param response
*
* Gestion de toutes les réponse possibles avec une "messageBox" ou de redirection
*
*/
function manageAuthentificationResponse(response){
switch( response.request ){
case 'success':
messageBox('Vous êtes maintenant connecté', 'success'); // on affiche le message
selectSection( document.querySelector('#MENU li:first-child') ); // on redirige vers la page d'accueil
break;
// case 'missing_param': messageBox('Un des champs requis n\'est pas présent', 'warning'); break;
// case 'empty_param': messageBox('Un des champs requis est vide', 'warning'); break;
// case 'unknown_user': messageBox('Nom d\'utilisateur inconnu', 'error'); break;
// case 'wrong_password': messageBox('Mot de passe incorrect', 'error'); break;
case 'empty_param': case 'missing_param': case 'unknown_user': case 'wrong_password':
messageBox('Identifiants incorrects', 'error');
break;
default:
messageBox('Erreur interne', 'error');
break;
}
}
/* [b] Initialisation des formulaires
==============================================================*/
initForm( // initialisation du formulaire de connection
document.querySelector('#user'), // formulaire (élément DOM)
function(request){ // handler
// ajout d'informations à la requête
request.level_0 = 'user';
request.level_1 = 'authentification';
API.send(request, function(response){ manageAuthentificationResponse(response); });
}
);

43
page/_JS/groups.js Executable file
View File

@ -0,0 +1,43 @@
var subSections = document.querySelectorAll('hgroup');
// si aucune sous-partie n'est active, on active la première
if( document.querySelector('#CONTAINER hgroup.active') == null )
selectSubSection( document.querySelector('#CONTAINER hgroup') );
/*************************************************/
/****************** EXEMPLE API ******************/
/*************************************************/
/* objet envoyé à API.php */
var request = {
level_0: 'groups',
level_1: 'visualiser',
group : 'ego'
};
// console.log( request );
// envoi de la requête
// @ on envoie l'objet
// @ quand réception: affichage de l'objet reçu
//
API.send(request, function(){} );

42
page/auth.php Executable file
View File

@ -0,0 +1,42 @@
<?php require('../manager/security.php'); session_init();
/****************************/
/* FORMULAIRE DE CONNECTION */
/****************************/
?>
<hgroup class='active'>Connection</hgroup>
<section>
<form id='user' style='width: 20em; margin-left: calc(50% - 20em/2);'>
<input type='text' name='username' placeholder='username' class='user' style='width:calc( 100% - 2*1.8em );'>
<input type='password' name='password' placeholder='password' style='width:calc( 100% - 2*1.8em );'>
<input type='button' name='submit' value='Connection' style='width:50%;margin-left: 25%;'>
</form>
</section>
<?php
/***************************/
/* VISUALISATION DU PROFIL */
/***************************/
if( isset($_SESSION['permissions']) && $_SESSION['permissions'] != null ){ ?>
<hgroup>Mon Profil</hgroup>
<section>
username = <?php echo $_SESSION['username']; ?><br>
droits = <?php echo $_SESSION['permissions']; ?>
</section>
<?php } ?>

1
page/career.php Executable file
View File

@ -0,0 +1 @@
career.php

1
page/marks.php Executable file
View File

@ -0,0 +1 @@
Notes ici !!!

4
page/modules.php Executable file
View File

@ -0,0 +1,4 @@
<?php require('../manager/security.php'); session_init(); ?>
modules.php

1
page/semestre.php Executable file
View File

@ -0,0 +1 @@
semestre.php

1
page/settings.php Executable file
View File

@ -0,0 +1 @@
settings.php

1
page/ue.php Executable file
View File

@ -0,0 +1 @@
ue.php

32
src/userlist.json Executable file
View File

@ -0,0 +1,32 @@
{
"eleve1": {
"permissions": "student",
"password" : "eleve1password"
},
"eleve2": {
"permissions": "student",
"password" : "eleve2password"
},
"prof1": {
"permissions": "teacher",
"password" : "prof1password"
},
"prof2": {
"permissions": "master",
"password" : "prof2password"
},
"admin1": {
"permissions": "admin",
"password" : "admin1password"
},
"admin2": {
"permissions": "admin",
"password" : "admin2password"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
xdoc/final2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB