6 Commits 2d81910b04 ... 55f237a688

Auteur SHA1 Message Date
  pezcurrel 55f237a688 Aggiunti un po' di file da ignorare il y a 4 ans
  pezcurrel 059d2f540c Ri-compatibilizzato con *nix il y a 4 ans
  pezcurrel 88bf4cdbce ... il y a 4 ans
  pongrebio 1d0fc20677 Merge branch 'redglow-langs' of RedGlow/MastodonStartpage into master il y a 4 ans
  pezcurrel 0af19d9cc9 ... il y a 4 ans
  RedGlow aa143ed793 Aggiunta deduzione lingua dell'istanza dai toot. il y a 4 ans

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+web/admin/crawler/crawler.log
+web/admin/crawler/currinst.job
+web/admin/crawler/instances.job
+web/admin/crawler/instances.json
+vendor
+composer.lock
+appunti.txt
+web/admin/crawler/infojsonexample.txt
+web/admin/zzz-estemp/
+web/admin/zzz-materiali/mastostart_struttura_e_dati_pro_altervista.sql.gz
+web/admin/zzz-oldcrawler/
+web/blocks/
+web/zzz-materiali/

+ 23 - 0
.vscode/launch.json

@@ -0,0 +1,23 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Listen for XDebug",
+            "type": "php",
+            "request": "launch",
+            "port": 9000
+        },
+        {
+            "name": "Launch currently open script",
+            "type": "php",
+            "request": "launch",
+            "program": "${file}",
+            "cwd": "${fileDirname}",
+            "port": 9000,
+            "runtimeExecutable": "C:\\wamp64\\bin\\php\\php7.3.12\\php.exe"
+        }
+    ]
+}

+ 232 - 49
web/admin/crawler/crawler.php

@@ -16,34 +16,44 @@
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
+require __DIR__ . "/../../vendor/autoload.php";
+use LanguageDetection\Language;
+
 define('N',"\n");
 
+if (strtoupper(substr(PHP_OS,0,3))==='WIN')
+	$iswin=true;
+else
+    $iswin=false;
+
 $link=false;
 $logf=false;
 $jsonf=false;
 
 declare(ticks=1);
-pcntl_signal(SIGTERM,'signalHandler');// Termination ('kill' was called)
-pcntl_signal(SIGHUP,'signalHandler');// Terminal log-out
-pcntl_signal(SIGINT,'signalHandler');// Interrupted (Ctrl-C is pressed)
-function signalHandler($signal) {
-	global $link, $logf, $jsonf;
-	lecho(N.'Sono stato interrotto.'.N);
-	if ($link) {
-		lecho('La connessione MySQL è aperta, la chiudo.'.N);
-		mysqli_close($link);
-	}
-	if ($jsonf) {
-		lecho('Il file di dump json è aperto, lo chiudo.'.N);
-// qui no, altrimenti "riprendi" fa poi casino
-//		fwrite($jsonf,'"Fine?": true'.N.'}'.N);
-		fclose($jsonf);
-	}
-	if ($logf) {
-		lecho('Il file di log è aperto, lo chiudo.'.N);
-		fclose($logf);
+if(defined("pcntl_signal")) {
+	pcntl_signal(SIGTERM,'signalHandler');// Termination ('kill' was called)
+	pcntl_signal(SIGHUP,'signalHandler');// Terminal log-out
+	pcntl_signal(SIGINT,'signalHandler');// Interrupted (Ctrl-C is pressed)
+	function signalHandler($signal) {
+		global $link, $logf, $jsonf;
+		lecho(N.'Sono stato interrotto.'.N);
+		if ($link) {
+			lecho('La connessione MySQL è aperta, la chiudo.'.N);
+			mysqli_close($link);
+		}
+		if ($jsonf) {
+			lecho('Il file di dump json è aperto, lo chiudo.'.N);
+	// qui no, altrimenti "riprendi" fa poi casino
+	//		fwrite($jsonf,'"Fine?": true'.N.'}'.N);
+			fclose($jsonf);
+		}
+		if ($logf) {
+			lecho('Il file di log è aperto, lo chiudo.'.N);
+			fclose($logf);
+		}
+		exit(2);
 	}
-	exit(2);
 }
 
 $opts=array(
@@ -256,7 +266,9 @@ function flushtronc($id) {
 }
 
 function truncs($str,$tab,$col,$ctx) {
-	global $tables, $tronconi;
+	global $tables, $tronconi, $iswin;
+	if ($iswin)
+		$tab=strtolower($tab);
 	$size=$tables[$tab][$col];
 	$len=mb_strlen($str,'UTF-8');
 	if ($len>$size) {
@@ -267,7 +279,9 @@ function truncs($str,$tab,$col,$ctx) {
 }
 
 function truncn($num,$tab,$col,$ctx) {
-	global $tables;
+	global $tables, $iswin;
+	if ($iswin)
+		$tab=strtolower($tab);
 	if (is_numeric($num)) {
 		if ($num>$tables[$tab][$col]['max']) {
 			notify($ctx.': ho dovuto troncare «'.$num.'» al valore massimo «'.$tables[$tab][$col]['max'].'» che può avere nella colonna «'.$col.'» della tabella «'.$tab.'»).',2);
@@ -400,7 +414,9 @@ if (!$riprendi) {
 }
 
 function willtrunc($str,$tab,$col) {
-	global $tables;
+	global $tables, $iswin;
+	if ($iswin)
+		$tab=strtolower($tab);
 	if (mb_strlen($str,'UTF-8')>$tables[$tab][$col])
 		return(true);
 	else
@@ -448,37 +464,204 @@ function subarimp($glue,$key,&$arr) {
 }
 
 function notify($msg,$sev) {
-	global $link, $tables;
+	global $link, $tables, $iswin;
 	lecho('NOTIFICAZIÒ: '.strip_tags($msg).N);
-	mysqli_query($link,'INSERT INTO Notifications (ID, Notification, Severity, Microtime, Seen) VALUES (NULL, \''.myesc($link,mb_substr($msg,0,$tables['Notifications']['Notification'],'UTF-8')).'\', '.$sev.', \''.microtime(true).'\', 0)')
+	$tab='Notifications';
+	if ($iswin)
+		$tab='notifications';
+	mysqli_query($link,'INSERT INTO Notifications (ID, Notification, Severity, Microtime, Seen) VALUES (NULL, \''.myesc($link,mb_substr($msg,0,$tables[$tab]['Notification'],'UTF-8')).'\', '.$sev.', \''.microtime(true).'\', 0)')
 		or mexit(mysqli_error($link).N,3);
 }
 
-function langs($instid) {
+/** <LANGUAGE MANAGEMENT> */
+/**
+ * Effettua una chiamata alla API di Mastodon.
+ *
+ * @param  string $host     L'host da chiamare (e.g.: "mastodon.bida.im")
+ * @param  string $path     Il path della API (e.g.: "/api/v1/timelines/public?local=true")
+ * @return mixed            L'oggetto ritornato dalla chiamata, già parsato da json_decode, o NULL se la chiamata fallisce
+ */
+function get_api($host, $path) {
+    global $context;
+    try {
+        $buf = @file_get_contents('https://' . $host . $path, false, $context);
+    } catch(Exception $e) {
+        echo "error:";
+        echo $e;
+        return NULL;
+    }
+    if ($buf!==false) {
+        $data = json_decode($buf, true);
+        return $data;
+    } else {
+        return NULL;
+    }
+}
+
+/**
+ * Torna un elenco di linguaggi riconosciuti nel toot fornito con relativa probabilità.
+ *
+ * @param  mixed $toot  Il toot da analizzare, come ritornato dalle API
+ * @return array        Mappa tra codice lingua e probabilità che il toot sia in quella lingua.
+ */
+function get_toot_languages($toot) {
+    $l = $toot['language'];
+    $res = [];
+    if($l !== NULL) {
+        // la lingua è specificata già nel toot: usa quella
+        $langs[$l] = 1;
+    } else {
+        // la lingua non è specificata: deducila
+        $text = strip_tags($toot['content']);
+        $ld = new Language;
+        $langs = $ld->detect($text)->bestResults()->close();
+    }
+    // raggruppa le lingue derivate, e.g.: "zh" e "zh-CN"
+    $grouped_langs = array();
+    foreach($langs as $key => $value) {
+        $l = explode("-", $key)[0];
+        if(array_key_exists($l, $grouped_langs)) {
+            $grouped_langs[$l] = max($grouped_langs[$l], $value);
+        } else {
+            $grouped_langs[$l] = $value;
+        }
+    }
+    return $grouped_langs;
+}
+
+/**
+ * Date le probabilità di lingua per ogni toot, calcola la media.
+ *
+ * @param  array $detected_langs    Array di mappe tra lingua e probabilità
+ * @return array                    Mappa tra lingua e probabilità
+ */
+function summary($detected_langs) {
+    $res = Array();
+    foreach($detected_langs as $langs) {
+        foreach($langs as $l => $weight) {
+            if(!array_key_exists($l, $res)) {
+                $res[$l] = 0;
+            }
+            $res[$l] += $weight;
+        }
+    }
+    foreach($res as $l => $sumweight) {
+        $res[$l] = $sumweight / count($detected_langs);
+    }
+    return $res;
+}
+
+/**
+ * Helper function per usort: compara due array usando il primo elemento.
+ *
+ * @param  array $entry1    Primo array da comparare
+ * @param  array $entry2    Secondo array da comparare
+ * @return number           -1, 0 o 1 a seconda che $entry1[0] sia minore, uguale o superiore a $entry2[0]
+ */
+function sort_weights($entry1, $entry2) {
+    $w1 = $entry1[0];
+    $w2 = $entry2[0];
+    if ($w1 < $w2)
+		$ret=1;
+	elseif ($w1 == $w2)
+		$ret=0;
+	else
+		$ret=-1;
+    return $ret;
+}
+
+/**
+ * Data una mappa di lingue, ritorna una lista di linguaggi considerati probabili.
+ *
+ * @param  array $summary   Mappa tra lingue e probabilità
+ * @return string[]         Elenco di lingue considerate probabili
+ */
+function get_languages($summary) {
+    $lst = [];
+    foreach($summary as $code => $weight) {
+        $lst[] = [$weight, $code];
+    }
+    usort($lst, 'sort_weights');
+    $languages = [];
+    $lastweight = 0;
+    foreach($lst as $entry) {
+        $l = $entry[1];
+        $weight = $entry[0];
+        if($weight < $lastweight * 2 / 3) {
+            break;
+        }
+        $languages[] = $l;
+        $lastweight = $weight;
+    }
+    return $languages;
+}
+
+/**
+ * Ritorna una lista di lingue probabili per la data istanza.
+ *
+ * @param  string $host     Hostname dell'istanza (e.g.: "mastodon.bida.im")
+ * @return string[]         Lista di lingue probabili
+ */
+function get_instance_langs($host) {
+    $data = get_api($host, '/api/v1/timelines/public?local=true');
+    if($data == NULL) {
+        return [];
+    }
+    $detected_langs = array_map('get_toot_languages', $data);
+    $summary = summary($detected_langs);
+    $languages = get_languages($summary);
+    return $languages;
+}
+/** </LANGUAGE MANAGEMENT> */
+
+/**
+ * ucfirst UTF-8 aware function
+ *
+ * @param string $string
+ * @return string
+ * @see http://ca.php.net/ucfirst
+ */
+function my_ucfirst($string, $e ='utf-8') {
+	if (function_exists('mb_strtoupper') && function_exists('mb_substr') && !empty($string)) {
+		$string = mb_strtolower($string, $e);
+		$upper = mb_strtoupper($string, $e);
+		preg_match('#(.)#us', $upper, $matches);
+		$string = $matches[1] . mb_substr($string, 1, mb_strlen($string, $e), $e);
+	} else {
+		$string = ucfirst($string);
+	}
+	return $string;
+}
+
+function langs($instid, $uri) {
 	global $info, $instrow, $link;
 	$instlangs=array();
-	if (akeavinn('languages',$info)) {
-		$pos=0;
-		foreach ($info['languages'] as $lang) {
-			$res=mysqli_query($link,'SELECT * FROM Languages WHERE Code=\''.myesc($link,$lang).'\'')
+	$languages = get_instance_langs($uri);
+	if(count($languages) == 0 && akeavinn('languages',$info)) {
+		$languages = $info['languages'];
+	}
+	lecho('Lingue trovate: '.implode(', ',$languages).N);
+	$pos=0;
+	foreach($languages as $lang) {
+		$res=mysqli_query($link,'SELECT * FROM Languages WHERE Code=\''.myesc($link,$lang).'\'')
+			or mexit(mysqli_error($link).N,3);
+		if (mysqli_num_rows($res)<1) {
+			$NameIt=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'it')),'Languages','NameIT','«'.$instrow['URI'].'»'));
+			$NameEn=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'en')),'Languages','NameEN','«'.$instrow['URI'].'»'));
+			$NameFr=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'fr')),'Languages','NameFR','«'.$instrow['URI'].'»'));
+			$NameEs=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,'es')),'Languages','NameES','«'.$instrow['URI'].'»'));
+			$NameOrig=myesc($link,truncs(my_ucfirst(locale_get_display_name($lang,$lang)),'Languages','NameES','«'.$instrow['URI'].'»'));
+			$q = 'INSERT INTO Languages (ID, Code, NameIT, NameEN, NameFR, NameES, NameOrig) VALUES (NULL, \''.myesc($link,truncs($lang,'Languages','Code','«'.$instrow['URI'].'»')).'\', \''.$NameIt.'\', \''.$NameEn.'\', \''.$NameFr.'\', \''.$NameEs.'\', \''.$NameOrig.'\')';
+			mysqli_query($link, $q)
 				or mexit(mysqli_error($link).N,3);
-			if (mysqli_num_rows($res)<1) {
-				$NameIt=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'it')),'Languages','NameIT','«'.$instrow['URI'].'»'));
-				$NameEn=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'en')),'Languages','NameEN','«'.$instrow['URI'].'»'));
-				$NameFr=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'fr')),'Languages','NameFR','«'.$instrow['URI'].'»'));
-				$NameEs=myesc($link,truncs(ucfirst(locale_get_display_name($lang,'es')),'Languages','NameES','«'.$instrow['URI'].'»'));
-				$NameOrig=myesc($link,truncs(ucfirst(locale_get_display_name($lang,$lang)),'Languages','NameOrig','«'.$instrow['URI'].'»'));
-				mysqli_query($link,'INSERT INTO Languages (ID, Code, NameIT, NameEN, NameFR, NameES, NameOrig) VALUES (NULL, \''.myesc($link,truncs($lang,'Languages','Code','«'.$instrow['URI'].'»')).'\', \''.$NameIt.'\', \''.$NameEn.'\', \''.$NameFr.'\', \''.$NameEs.'\', \''.$NameOrig.'\')')
-					or mexit(mysqli_error($link).N,3);
-				$langid=mysqli_insert_id($link);
-				flushtronc($langid);
-			} else {
-				$row=mysqli_fetch_assoc($res);
-				$langid=$row['ID'];
-			}
-			$pos++;
-			$instlangs[]=array('InstID'=>$instid,'LangID'=>$langid,'Pos'=>$pos,'Code'=>$lang);
+			$langid=mysqli_insert_id($link);
+			flushtronc($langid);
+		} else {
+			$row=mysqli_fetch_assoc($res);
+			$langid=$row['ID'];
 		}
+		$pos++;
+		$instlangs[]=array('InstID'=>$instid,'LangID'=>$langid,'Pos'=>$pos,'Code'=>$lang);
 	}
 	return($instlangs);
 }
@@ -597,7 +780,7 @@ while ($i<$cinsts) {
 		$ok=false;
 		lecho('ERRORE :-('.N);
 // questo è anche il limbo delle istanze che non rispondono, perciò controlliamo se già esistono nel db e, nel caso, aggiorniamo InstChecks
-		$res=mysqli_query($link,'SELECT * FROM Instances WHERE URI=\''.myesc($link,mb_substr($dom,0,$tables['Instances']['URI'],'UTF-8')).'\'')
+		$res=mysqli_query($link,'SELECT * FROM Instances WHERE URI=\''.myesc($link,mb_substr($dom,0,$tables[$iswin ? 'instances' : 'Instances']['URI'],'UTF-8')).'\'')
 			or mexit(mysqli_error($link).N,3);
 		if (mysqli_num_rows($res)>0) {
 			lecho('«'.$dom.'» non risponde, ma è presente nel database; aggiorno InstChecks.'.N);
@@ -742,7 +925,7 @@ while ($i<$cinsts) {
 			$oldinstlangs=array();
 			while ($row=mysqli_fetch_assoc($res))
 				$oldinstlangs[]=$row;
-			$instlangs=langs($instrow['ID']);
+			$instlangs=langs($instrow['ID'], $instrow['URI']);
 			if ($instlangs!=$oldinstlangs) {
 				notify('La lista delle lingue utilizzate dichiarate dall’istanza «<a href="editinst.php?id='.$instrow['ID'].'">'.$instrow['URI'].'</a>» è cambiata da «'.subarimp(', ','Code',$oldinstlangs).'» a «'.subarimp(', ','Code',$instlangs).'».',1);
 				mysqli_query($link,'DELETE FROM InstLangs WHERE InstID='.$instrow['ID'])
@@ -773,7 +956,7 @@ while ($i<$cinsts) {
 
 			flushtronc($instid);
 
-			$instlangs=langs($instid);
+			$instlangs=langs($instid, $instrow['URI']);
 			foreach ($instlangs as $row) {
 				mysqli_query($link,'INSERT INTO InstLangs (InstID, LangID, Pos) VALUES ('.$row['InstID'].', '.$row['LangID'].', '.$row['Pos'].')')
 					or mexit(mysqli_error($link).N,3);

+ 9 - 7
web/admin/edinst.php

@@ -39,6 +39,7 @@ mysqli_close($link);
 <link rel="icon" type="image/png" href="imgs/icona-192.png" sizes="192x192">
 <link rel="icon" type="image/png" href="imgs/icona-512.png" sizes="512x512">
 <link rel="apple-touch-icon-precomposed" href="imgs/icona-180.png">
+<script language="JavaScript" src="js/menu.js?v=<?php echo($cjrand); ?>"></script>
 <script language="JavaScript" src="js/confirma.js?v=<?php echo($cjrand); ?>"></script>
 <script language="JavaScript" src="js/alerta.js?v=<?php echo($cjrand); ?>"></script>
 <link rel="stylesheet" type="text/css" href="theme.css?v=<?php echo($cjrand); ?>">
@@ -118,12 +119,12 @@ function ulsh(el,sh) {
 <option value="1">Inglese</option>
 <option value="2">Esperanto</option>
 </select>
-<img src="imgs/carica.svg" style="float:left;"><img src="imgs/salva.svg" style="float:right;">
+<img src="imgs/fresu.svg" style="float:left;"><img src="imgs/fregiu.svg" style="float:right;">
 </td>
 <td style="width:1%;">
-<div style="width:22px;"><img src="imgs/carica.svg"></div>
-<div style="width:22px;"><img src="imgs/carica.svg"></div>
-<div style="width:22px;"><img src="imgs/carica.svg"></div>
+<div class="butdiv"><img src="imgs/fresu.svg"></div>
+<div class="butdiv"><img src="imgs/plus.svg"></div>
+<div class="butdiv"><img src="imgs/minus.svg"></div>
 </td>
 </tr>
 <tr>
@@ -134,9 +135,10 @@ function ulsh(el,sh) {
 </select>
 </td>
 <td style="width:1%;">
-<div style="width:22px;"><img src="imgs/carica.svg"></div>
-<div style="width:22px;"><img src="imgs/carica.svg"></div>
-<div style="width:22px;"><img src="imgs/carica.svg"></div>
+<div class="butdiv"><img src="imgs/fresucim.svg" title="Sposta in cima"></div>
+<div class="butdiv"><img src="imgs/fresu.svg" title="Sposta su"></div>
+<div class="butdiv"><img src="imgs/fregiu.svg" title="Sposta giù"></div>
+<div class="butdiv"><img src="imgs/fregiufon.svg" title="Sposta in fondo"></div>
 </td>
 </tr>
 </table>

+ 0 - 0
web/admin/imgs/salva.svg → web/admin/imgs/fregiu.svg


+ 119 - 0
web/admin/imgs/fregiufon.svg

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="22"
+   height="22"
+   viewBox="0 0 5.8208332 5.8208335"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14"
+   sodipodi:docname="fregiubot.svg">
+  <defs
+     id="defs2">
+    <marker
+       style="overflow:visible"
+       id="DistanceEnd"
+       refX="0.0"
+       refY="0.0"
+       orient="auto"
+       inkscape:stockid="DistanceEnd"
+       inkscape:isstock="true">
+      <g
+         id="g2301"
+         style="stroke:#000000;stroke-opacity:1;fill:#ffffff;fill-opacity:1">
+        <path
+           style="fill:#ffffff;stroke:#000000;stroke-width:1.15;stroke-linecap:square;stroke-opacity:1;fill-opacity:1"
+           d="M 0,0 L -2,0"
+           id="path2316" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-opacity:1;fill-opacity:1"
+           d="M 0,0 L -13,4 L -9,0 -13,-4 L 0,0 z "
+           id="path2312" />
+        <path
+           style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-opacity:1;fill-opacity:1"
+           d="M 0,-4 L 0,40"
+           id="path2314" />
+      </g>
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="45.254834"
+     inkscape:cx="7.7527772"
+     inkscape:cy="9.9873119"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:snap-page="true"
+     inkscape:bbox-nodes="true"
+     inkscape:window-width="3840"
+     inkscape:window-height="2037"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Livello 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-291.17916)">
+    <rect
+       style="opacity:1;vector-effect:none;fill:#3088d4;fill-opacity:1;stroke:none;stroke-width:0.37526914;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       id="rect861"
+       width="5.8208332"
+       height="5.8208265"
+       x="-1.110223e-16"
+       y="-297"
+       ry="0.78214943"
+       rx="0.78214943"
+       transform="scale(1,-1)" />
+    <rect
+       style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       id="rect4521"
+       width="1.5827367"
+       height="4.2721367"
+       x="2.0668476"
+       y="-295.75919"
+       ry="0.4608396"
+       rx="0.4608396"
+       transform="scale(1,-1)" />
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.23701932;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 2.9026735,296.69205 c -0.051063,-0.002 -0.097206,-0.0195 -0.1224421,-0.0473 l -1.87802118,-2.0275 c -0.0584756,-0.0623 0.0135137,-0.1407 0.12934038,-0.14082 h 3.7577667 c 0.1157814,1.3e-4 0.1877481,0.0784 0.1293403,0.14078 l -1.8780211,2.0275 c -0.027985,0.0307 -0.08135,0.049 -0.137963,0.0473 z"
+       id="path4529"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccccc" />
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.23701932;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 2.9026734,295.38534 c -0.051063,-0.002 -0.097206,-0.0195 -0.1224421,-0.0473 l -1.87802109,-2.0275 c -0.0584756,-0.0623 0.0135137,-0.1407 0.12934039,-0.14082 h 3.7577666 c 0.1157814,1.3e-4 0.1877481,0.0784 0.1293403,0.14078 l -1.8780211,2.0275 c -0.027985,0.0307 -0.08135,0.049 -0.137963,0.0473 z"
+       id="path4529-6"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccccc" />
+  </g>
+</svg>

+ 0 - 0
web/admin/imgs/salvacome.svg → web/admin/imgs/fregiuplus.svg


+ 0 - 0
web/admin/imgs/carica.svg → web/admin/imgs/fresu.svg


+ 115 - 0
web/admin/imgs/fresucim.svg

@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="22"
+   height="22"
+   viewBox="0 0 5.8208332 5.8208335"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 5da689c313, 2019-01-14"
+   sodipodi:docname="fresucim.svg">
+  <defs
+     id="defs2">
+    <marker
+       style="overflow:visible"
+       id="DistanceEnd"
+       refX="0.0"
+       refY="0.0"
+       orient="auto"
+       inkscape:stockid="DistanceEnd"
+       inkscape:isstock="true">
+      <g
+         id="g2301"
+         style="stroke:#000000;stroke-opacity:1;fill:#ffffff;fill-opacity:1">
+        <path
+           style="fill:#ffffff;stroke:#000000;stroke-width:1.15;stroke-linecap:square;stroke-opacity:1;fill-opacity:1"
+           d="M 0,0 L -2,0"
+           id="path2316" />
+        <path
+           style="fill:#ffffff;fill-rule:evenodd;stroke:#000000;stroke-opacity:1;fill-opacity:1"
+           d="M 0,0 L -13,4 L -9,0 -13,-4 L 0,0 z "
+           id="path2312" />
+        <path
+           style="fill:#ffffff;stroke:#000000;stroke-width:1;stroke-linecap:square;stroke-opacity:1;fill-opacity:1"
+           d="M 0,-4 L 0,40"
+           id="path2314" />
+      </g>
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="45.254834"
+     inkscape:cx="7.7527772"
+     inkscape:cy="9.9873119"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:snap-page="true"
+     inkscape:bbox-nodes="true"
+     inkscape:window-width="3840"
+     inkscape:window-height="2037"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Livello 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-291.17916)">
+    <rect
+       style="opacity:1;vector-effect:none;fill:#3088d4;fill-opacity:1;stroke:none;stroke-width:0.37526914;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       id="rect861"
+       width="5.8208332"
+       height="5.8208265"
+       x="-1.110223e-16"
+       y="291.17917"
+       ry="0.78214943" />
+    <rect
+       style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       id="rect4521"
+       width="1.5827367"
+       height="4.2721367"
+       x="2.0668476"
+       y="292.41998"
+       ry="0.4608396" />
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.23701932;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 2.9026735,291.48711 c -0.051063,0.002 -0.097206,0.0195 -0.1224421,0.0473 l -1.87802118,2.0275 c -0.0584756,0.0623 0.0135137,0.1407 0.12934038,0.14082 h 3.7577667 c 0.1157814,-1.3e-4 0.1877481,-0.0784 0.1293403,-0.14078 l -1.8780211,-2.0275 c -0.027985,-0.0307 -0.08135,-0.049 -0.137963,-0.0473 z"
+       id="path4529"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccccc" />
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.23701932;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 2.9026734,292.79382 c -0.051063,0.002 -0.097206,0.0195 -0.1224421,0.0473 l -1.87802109,2.0275 c -0.0584756,0.0623 0.0135137,0.1407 0.12934039,0.14082 h 3.7577666 c 0.1157814,-1.3e-4 0.1877481,-0.0784 0.1293403,-0.14078 l -1.8780211,-2.0275 c -0.027985,-0.0307 -0.08135,-0.049 -0.137963,-0.0473 z"
+       id="path4529-6"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccccc" />
+  </g>
+</svg>

+ 3 - 3
web/admin/index.html

@@ -9,7 +9,7 @@
 <link rel="icon" type="image/png" href="imgs/icona-192.png" sizes="192x192">
 <link rel="icon" type="image/png" href="imgs/icona-512.png" sizes="512x512">
 <link rel="apple-touch-icon-precomposed" href="imgs/icona-180.png">
-<link rel="stylesheet" type="text/css" href="theme.css?v=1">
+<link rel="stylesheet" type="text/css" href="theme.css?v=2">
 </head>
 <body>
 <div id="fullscreen">
@@ -17,8 +17,8 @@
 <div id="intro">
 <p>Ciao,<br>
 <a href="index.php">qui</a>, con nome utente “bida” e password uguale, puoi provare quel che c’è della sezione di admin per il “suggeritore d’istanze mastodon” della <a href="https://rame.altervista.org/mastostart">Mastodon Startpage</a>.<br>
-Quel che c’è, al momento, è il “browser di istanze lato admin”, uno strumento che permette di “sfogliare” e cercare-ordinare secondo tanti criteri un database di metadati relativi a tante istanze mastodon (per visualizzare il pannello di ricerca e ordinamento clicca sull’icona con la lente in alto a destra).<br>
-Se vuoi segnalare bachi e/o collaborare in altro modo al progetto della Mastodon Startpage, <a href="https://git.lattuga.net/pongrebio/MastodonStartpage">qui c’è il repo git del progetto</a>; nel wiki trovi una spiegazione abbastanza dettagliata di come si pensa di svilupparlo, quel che c’è e quel che manca.</p>
+Quel che c’è, al momento, è il “browser di istanze lato admin”, uno strumento che permette di “sfogliare” e cercare-ordinare secondo tanti criteri un database di metadati relativi a tante istanze mastodon (per visualizzare il pannello di ricerca e ordinamento, una volta entrat@ nella sezione di admin clicca sull’icona con la lente in alto a destra).<br>
+Se vuoi segnalare bachi o collaborare in altro modo sull’aspetto tecnico (non contenutistico) del progetto della Mastodon Startpage, <a href="https://git.lattuga.net/pongrebio/MastodonStartpage">qui c’è il repo git del progetto</a>; nel wiki trovi una spiegazione abbastanza dettagliata di come si pensa di svilupparlo, quel che c’è e quel che manca.</p>
 </div>
 </div>
 </div>

+ 30 - 45
web/admin/instances.php

@@ -645,23 +645,12 @@ mysqli_close($link);
 <link rel="icon" type="image/png" href="imgs/icona-192.png" sizes="192x192">
 <link rel="icon" type="image/png" href="imgs/icona-512.png" sizes="512x512">
 <link rel="apple-touch-icon-precomposed" href="imgs/icona-180.png">
+<script language="JavaScript" src="js/menu.js?v=<?php echo($cjrand); ?>"></script>
 <script language="JavaScript" src="js/confirma.js?v=<?php echo($cjrand); ?>"></script>
 <script language="JavaScript" src="js/alerta.js?v=<?php echo($cjrand); ?>"></script>
 <link rel="stylesheet" type="text/css" href="theme.css?v=<?php echo($cjrand); ?>">
 <script language="JavaScript">
 <!--
-function chulsh(el,sh) {
-	if (sh)
-		el.querySelector('ul').style.display='block';
-	else
-		el.querySelector('ul').style.display='none';
-}
-function ulsh(el,sh) {
-	if (sh)
-		el.style.display='block';
-	else
-		el.style.display='none';
-}
 function shideplancia() {
 	var plancia=document.getElementById('plancia');
 	var plctrl=document.getElementById('lente');
@@ -683,10 +672,6 @@ function truncsel(el) {
 	for (i=len-1; i>=0; i--)
 		sel.remove(i);
 }
-function addselopt(el,val,txt) {
-	option=new Option(txt,val);
-	el.add(option);
-}
 function inpdisif(index) {
 	console.log('inpdisif index: '+index);
 	var source=document.getElementById('condsel-'+index);
@@ -715,12 +700,12 @@ function popusels(index,valselval) {
 		type=subtype;
 	if (type=='bool') {
 		truncsel('condsel-'+index);
-		addselopt(condsel,'IS','è');
+		condsel.add(new Option('è','IS'));
 		truncsel('valuesel-'+index);
-		addselopt(valsel,'TRUE','vero');
-		addselopt(valsel,'FALSE','falso');
-		addselopt(valsel,'NOT NULL','definit@');
-		addselopt(valsel,'NULL','non definit@');
+		valsel.add(new Option('vero','TRUE'));
+		valsel.add(new Option('falso','FALSE'));
+		valsel.add(new Option('definit@','NOT NULL'));
+		valsel.add(new Option('non definit@','NULL'));
 		if (valselval!==false)
 			selind('valuesel-'+index,valselval);
 		valinp.style.display='none';
@@ -729,40 +714,40 @@ function popusels(index,valselval) {
 		valsel.disabled=false;
 	} else if (type=='text') {
 		truncsel('condsel-'+index);
-		addselopt(condsel,'LIKE','contiene');
-		addselopt(condsel,'NOT LIKE','non contiene');
-		addselopt(condsel,'=','è uguale a');
-		addselopt(condsel,'!=','è divers@ da');
-		addselopt(condsel,'>=','è maggiore o uguale a');
-		addselopt(condsel,'<=','è minore o uguale a');
-		addselopt(condsel,'>','è maggiore di');
-		addselopt(condsel,'<','è minore di');
-		addselopt(condsel,'IS NOT NULL','è definit@');
-		addselopt(condsel,'IS NULL','non è definit@');
+		condsel.add(new Option('contiene','LIKE'));
+		condsel.add(new Option('non contiene','NOT LIKE'));
+		condsel.add(new Option('è uguale a','='));
+		condsel.add(new Option('è divers@ da','!='));
+		condsel.add(new Option('è maggiore o uguale a','>='));
+		condsel.add(new Option('è minore o uguale a','<='));
+		condsel.add(new Option('è maggiore di','>'));
+		condsel.add(new Option('è minore di','<'));
+		condsel.add(new Option('è definit@','IS NOT NULL'));
+		condsel.add(new Option('non è definit@','IS NULL'));
 		valsel.style.display='none';
 		valsel.disabled=true;
 		valinp.style.display='block';
 		valinp.disabled=false;
 	} else if (type=='int' || type=='time') {
 		truncsel('condsel-'+index);
-		addselopt(condsel,'>=','è maggiore o uguale a');
-		addselopt(condsel,'<=','è minore o uguale a');
-		addselopt(condsel,'>','è maggiore di');
-		addselopt(condsel,'<','è minore di');
-		addselopt(condsel,'=','è uguale a');
-		addselopt(condsel,'!=','è divers@ da');
-		addselopt(condsel,'IS NOT NULL','è definit@');
-		addselopt(condsel,'IS NULL','non è definit@');
+		condsel.add(new Option('è maggiore o uguale a','>='));
+		condsel.add(new Option('è minore o uguale a','<='));
+		condsel.add(new Option('è maggiore di','>'));
+		condsel.add(new Option('è minore di','<'));
+		condsel.add(new Option('è uguale a','='));
+		condsel.add(new Option('è divers@ da','!='));
+		condsel.add(new Option('è definit@','IS NOT NULL'));
+		condsel.add(new Option('non è definit@','IS NULL'));
 		valsel.style.display='none';
 		valsel.disabled=true;
 		valinp.style.display='block';
 		valinp.disabled=false;
 	} else if (type=='join') {
 		truncsel('condsel-'+index);
-		addselopt(condsel,'=','è uguale a');
-		addselopt(condsel,'!=','è divers@ da');
-		addselopt(condsel,'IS NOT NULL','è definit@');
-		addselopt(condsel,'IS NULL','non è definit@');
+		condsel.add(new Option('è uguale a','='));
+		condsel.add(new Option('è divers@ da','!='));
+		condsel.add(new Option('è definit@','IS NOT NULL'));
+		condsel.add(new Option('non è definit@','IS NULL'));
 		valsel.style.display='block';
 		valsel.disabled=false;
 		valinp.style.display='none';
@@ -776,7 +761,7 @@ function popusels(index,valselval) {
 		xhr.onload=function() {
 			let jarr=xhr.response;
 			for (i=0; i<jarr.length; i++)
-				addselopt(valsel,jarr[i][0],jarr[i][1]);
+				valsel.add(new Option(jarr[i][1],jarr[i][0]));
 			console.log('valsel pronto!');
 			if (valselval!==false)
 				selind('valuesel-'+index,valselval);
@@ -1261,7 +1246,7 @@ function gotopage(pi) {
 <table id="planciaothers" class="planciatab">
 <tr><td><input type="button" id="subbut" value="Applica" class="ctrlbut" onclick="ckf();"></td></tr>
 </table>
-<table class="planciatab"><tr><td style="width:1%">Preset: </td><td style="width:95%"><select name="presets" id="presets" class="presets"><?php echo($presopts); ?></select></td><td style="width:1%"><img src="imgs/carica.svg" class="lilbut" onclick="loadpres();" title="Carica i criteri dal preset selezionato"></td><td style="width:1%"><img src="imgs/salva.svg" class="lilbut" onclick="pupsavepres(false);" title="Salva i criteri impostati nel preset selezionato sovrascrivendolo (permette inoltre di rinominarlo) ..."></td><td style="width:1%"><img src="imgs/salvacome.svg" class="lilbut" onclick="pupsavepres(true);" title="Salva i criteri impostati come nuovo preset ..."></td><td style="width:1%"><img src="imgs/minus.svg" class="lilbut" onclick="puprempres();" title="Elimina il preset selezionato"></td></tr></table>
+<table class="planciatab"><tr><td style="width:1%">Preset: </td><td style="width:95%"><select name="presets" id="presets" class="presets"><?php echo($presopts); ?></select></td><td style="width:1%"><img src="imgs/fresu.svg" class="lilbut" onclick="loadpres();" title="Carica i criteri dal preset selezionato"></td><td style="width:1%"><img src="imgs/fregiu.svg" class="lilbut" onclick="pupsavepres(false);" title="Salva i criteri impostati nel preset selezionato sovrascrivendolo (permette inoltre di rinominarlo) ..."></td><td style="width:1%"><img src="imgs/fregiuplus.svg" class="lilbut" onclick="pupsavepres(true);" title="Salva i criteri impostati come nuovo preset ..."></td><td style="width:1%"><img src="imgs/minus.svg" class="lilbut" onclick="puprempres();" title="Elimina il preset selezionato"></td></tr></table>
 <input type="hidden" name="p" id="p" value="...">
 </form>
 </div>

+ 12 - 0
web/admin/js/menu.js

@@ -0,0 +1,12 @@
+function chulsh(el,sh) {
+	if (sh)
+		el.querySelector('ul').style.display='block';
+	else
+		el.querySelector('ul').style.display='none';
+}
+function ulsh(el,sh) {
+	if (sh)
+		el.style.display='block';
+	else
+		el.style.display='none';
+}

+ 8 - 3
web/admin/theme.css

@@ -530,17 +530,22 @@ input {
 .edtab label {
 	font-weight: bold;
 }
-.ruler {
+.edtab .ruler {
 	width: 100%;
 	height: 12px;
 }
-.tit {
+.edtab .tit {
 	background-color: #6f916f;
 	color: white;
 	text-align: center;
 	border-radius: 3px;
 	margin-bottom: 6px;
 }
+.edtab .butdiv {
+	width: 22px;
+	margin-bottom: 3px;
+	margin-left: 3px;
+}
 
 .cbtab {
 	border-spacing: 0;
@@ -598,6 +603,6 @@ input {
 		min-width: 244px;
 	}
 	#intro {
-		width: 360px;
+		width: 340px;
 	}
 }

+ 5 - 0
web/composer.json

@@ -0,0 +1,5 @@
+{
+    "require": {
+        "patrickschur/language-detection": "^3.4"
+    }
+}