forked from librespeed/speedtest
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
executable file
·468 lines (461 loc) · 25.9 KB
/
index.html
File metadata and controls
executable file
·468 lines (461 loc) · 25.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no" />
<meta charset="UTF-8" />
<!-- TailwindCSS via CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Optional: Tailwind dark mode based on system preference
tailwind.config = {
darkMode: 'media'
};
// Prevent FOUC in some browsers
document.documentElement.classList.add('bg-white','text-slate-900');
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.remove('bg-white','text-slate-900');
document.documentElement.classList.add('bg-slate-900','text-slate-100');
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e)=>{
if(e.matches){
document.documentElement.classList.remove('bg-white','text-slate-900');
document.documentElement.classList.add('bg-slate-900','text-slate-100');
}else{
document.documentElement.classList.remove('bg-slate-900','text-slate-100');
document.documentElement.classList.add('bg-white','text-slate-900');
}
});
// Hide unstyled content until Tailwind loads
document.documentElement.classList.add('min-h-full');
</script>
<script type="text/javascript" src="speedtest.js"></script>
<script>
window.LIBRESPEED_TELEMETRY_SECRET = window.LIBRESPEED_TELEMETRY_SECRET || '';
</script>
<script type="text/javascript">
function I(i){return document.getElementById(i);}
//LIST OF TEST SERVERS. Leave empty if you're doing a standalone installation. See documentation for details
var SPEEDTEST_SERVERS=[
/*{ //this server doesn't actually exist, remove it
name:"Example Server 1", //user friendly name for the server
server:"//test1.mydomain.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
dlURL:"backend/garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"backend/empty.php", //path to upload test on this server (empty.php or replacement)
pingURL:"backend/empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"backend/getIP.php" //path to getIP on this server (getIP.php or replacement)
},
{ //this server doesn't actually exist, remove it
name:"Example Server 2", //user friendly name for the server
server:"//test2.example.com/", //URL to the server. // at the beginning will be replaced with http:// or https:// automatically
dlURL:"garbage.php", //path to download test on this server (garbage.php or replacement)
ulURL:"empty.php", //path to upload test on this server (empty.php or replacement)
pingURL:"empty.php", //path to ping/jitter test on this server (empty.php or replacement)
getIpURL:"getIP.php" //path to getIP on this server (getIP.php or replacement)
}*/
//add other servers here, comma separated
];
//INITIALIZE SPEEDTEST
var s=new Speedtest(); //create speed test object
s.setParameter("telemetry_level","basic"); //enable basic telemetry (for results sharing)
if (window.LIBRESPEED_TELEMETRY_SECRET) {
s.setParameter("telemetry_secret", window.LIBRESPEED_TELEMETRY_SECRET);
}
s.setParameter("telemetry_signature_version", 1);
//SERVER AUTO SELECTION
function initServers(){
if(SPEEDTEST_SERVERS.length==0){ //standalone installation
//just make the UI visible
I("loading").classList.add("hidden");
I("serverArea").style.display="none";
I("testWrapper").classList.remove("hidden");
initUI();
}else{ //multiple servers
var noServersAvailable=function(){
I("message").innerHTML="利用可能なサーバーがありません";
}
var serverSelectBound=false;
var bindServerSelect=function(){
var sel=I("server");
if(!sel||serverSelectBound) return;
sel.addEventListener("change",function(){
try{
var idx=parseInt(this.value,10);
if(isNaN(idx)) return;
if(!SPEEDTEST_SERVERS||!SPEEDTEST_SERVERS[idx]) return;
s.setSelectedServer(SPEEDTEST_SERVERS[idx]);
initUI();
var badge=I('statusBadge');
if(badge){
badge.classList.remove('hidden');
badge.textContent='Ready';
}
}catch(e){
console.error(e);
}
});
serverSelectBound=true;
};
var runServerSelect=function(){
s.selectServer(function(server){
if(server!=null){ //at least 1 server is available
I("loading").classList.add("hidden"); //hide loading message
//populate server list for manual selection
for(var i=0;i<SPEEDTEST_SERVERS.length;i++){
if(SPEEDTEST_SERVERS[i].pingT==-1) continue;
var option=document.createElement("option");
option.value=i;
option.textContent=SPEEDTEST_SERVERS[i].name;
if(SPEEDTEST_SERVERS[i]===server) option.selected=true;
I("server").appendChild(option);
}
bindServerSelect();
//show test UI
I("testWrapper").classList.remove("hidden");
initUI();
}else{ //no servers are available, the test cannot proceed
noServersAvailable();
}
});
}
if(typeof SPEEDTEST_SERVERS === "string"){
//need to fetch list of servers from specified URL
s.loadServerList(SPEEDTEST_SERVERS,function(servers){
if(servers==null){ //failed to load server list
noServersAvailable();
}else{ //server list loaded
SPEEDTEST_SERVERS=servers;
runServerSelect();
}
});
}else{
//hardcoded server list
s.addTestPoints(SPEEDTEST_SERVERS);
runServerSelect();
}
}
}
var meterBk=/Trident.*rv:(\d+\.\d+)/i.test(navigator.userAgent)?"#EAEAEA":"#80808040";
var dlColor="#0284c7", // sky-600
ulColor="#d97706"; // amber-600
var progColor=meterBk;
//CODE FOR GAUGES
function drawMeter(c,amount,bk,fg,progress,prog){
var ctx=c.getContext("2d");
var dp=window.devicePixelRatio||1;
var cw=c.clientWidth*dp, ch=c.clientHeight*dp;
var sizScale=ch*0.0055;
if(c.width==cw&&c.height==ch){
ctx.clearRect(0,0,cw,ch);
}else{
c.width=cw;
c.height=ch;
}
ctx.beginPath();
ctx.strokeStyle=bk;
ctx.lineWidth=12*sizScale;
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,Math.PI*0.1);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle=fg;
ctx.lineWidth=12*sizScale;
ctx.arc(c.width/2,c.height-58*sizScale,c.height/1.8-ctx.lineWidth,-Math.PI*1.1,amount*Math.PI*1.2-Math.PI*1.1);
ctx.stroke();
if(typeof progress !== "undefined"){
ctx.fillStyle=prog;
ctx.fillRect(c.width*0.3,c.height-16*sizScale,c.width*0.4*progress,4*sizScale);
}
}
function mbpsToAmount(s){
return 1-(1/(Math.pow(1.3,Math.sqrt(s))));
}
function format(d){
d=Number(d);
if(d<10) return d.toFixed(2);
if(d<100) return d.toFixed(1);
return d.toFixed(0);
}
//UI CODE
var uiData=null;
function startStop(){
const btn=I("startStopBtn");
if(s.getState()==3){
// Abort
s.abort();
uiData=null;
btn.classList.remove('running','bg-red-600','hover:bg-red-700');
btn.classList.add('bg-slate-800','hover:bg-slate-900','dark:bg-slate-700','dark:hover:bg-slate-600');
var lbl=btn.querySelector('.start-label'); if(lbl){ lbl.textContent='Start'; }
I("server").disabled=false;
initUI();
}else{
// Start
btn.classList.add('running');
btn.classList.remove('bg-slate-800','hover:bg-slate-900','dark:bg-slate-700','dark:hover:bg-slate-600');
btn.classList.add('bg-red-600','hover:bg-red-700');
var lbl=btn.querySelector('.start-label'); if(lbl){ lbl.textContent='Abort'; }
I("shareArea").style.display="none";
I("testServer").textContent="";
I("server").disabled=true;
var badge=I('statusBadge'); if(badge){ badge.textContent='Running'; badge.classList.remove('hidden'); }
s.onupdate=function(data){
uiData=data;
};
s.onend=function(aborted){
btn.classList.remove('running','bg-red-600','hover:bg-red-700');
btn.classList.add('bg-slate-800','hover:bg-slate-900','dark:bg-slate-700','dark:hover:bg-slate-600');
var lbl=btn.querySelector('.start-label'); if(lbl){ lbl.textContent='Start'; }
I("server").disabled=false;
var badge=I('statusBadge'); if(badge){ badge.classList.remove('hidden'); badge.textContent = aborted ? 'Aborted' : 'Finished'; }
updateUI(true);
if(!aborted){
try{
var testId=uiData.testId;
if(testId!=null){
var base=window.location.href.substring(0,window.location.href.lastIndexOf("/"));
var shareURL=base+"/results/?id="+testId;
I("resultsImg").src=shareURL;
I("resultsURL").value=shareURL;
var openBtn=I('openImageBtn'); if(openBtn){ openBtn.href=shareURL; }
// populate embed codes
var html='<img src="'+shareURL+'" alt="Speedtest result" />';
var md='';
var bb='[img]'+shareURL+'[/img]';
if(I('embedHTML')) I('embedHTML').value=html;
if(I('embedMD')) I('embedMD').value=md;
I("testId").innerHTML=testId;
var serverName="";
try{
var selectedServer=s.getSelectedServer();
if(selectedServer&&selectedServer.name){ serverName=selectedServer.name; }
}catch(serverError){}
if(!serverName){
var serverSelect=I("server");
if(serverSelect&&serverSelect.selectedIndex>=0){
serverName=serverSelect.options[serverSelect.selectedIndex].textContent;
}
}
if(!serverName){
try{
if(typeof s._settings!=="undefined"&&typeof s._settings.telemetry_extra==="string"){
var telemetryExtra=JSON.parse(s._settings.telemetry_extra);
if(telemetryExtra&&typeof telemetryExtra.server==="string") serverName=telemetryExtra.server;
}
}catch(parseError){}
}
I("testServer").textContent=serverName||"N/A";
I("shareArea").style.display="";
}
}catch(e){}
}
};
s.start();
}
}
//this function reads the data sent back by the test and updates the UI
function updateUI(forced){
if(!forced&&s.getState()!=3) return;
if(uiData==null) return;
var status=uiData.testState;
I("ip").textContent=uiData.clientIp;
I("dlText").textContent=(status==1&&uiData.dlStatus==0)?"…":format(uiData.dlStatus);
drawMeter(I("dlMeter"),mbpsToAmount(Number(uiData.dlStatus*(status==1?oscillate():1))),meterBk,dlColor,Number(uiData.dlProgress),progColor);
I("ulText").textContent=(status==3&&uiData.ulStatus==0)?"…":format(uiData.ulStatus);
drawMeter(I("ulMeter"),mbpsToAmount(Number(uiData.ulStatus*(status==3?oscillate():1))),meterBk,ulColor,Number(uiData.ulProgress),progColor);
I("pingText").textContent=format(uiData.pingStatus);
I("jitText").textContent=format(uiData.jitterStatus);
// update small progress bars
var dlBar=I("dlBar"), ulBar=I("ulBar"), pingBar=I("pingBar"), jitBar=I("jitBar");
if(dlBar){ dlBar.style.width=(Number(uiData.dlProgress)*100).toFixed(1)+"%"; }
if(ulBar){ ulBar.style.width=(Number(uiData.ulProgress)*100).toFixed(1)+"%"; }
// ping/jitter tests share phase 2; approximate progress from pingProgress
var pingProg = Number(uiData.pingProgress)*100;
if(pingBar){ pingBar.style.width=pingProg.toFixed(1)+"%"; }
if(jitBar){ jitBar.style.width=pingProg.toFixed(1)+"%"; }
// status badge
var badge=I("statusBadge");
if(badge){
// reset to base classes to avoid class accumulation
badge.className = 'px-3 py-1 text-xs font-medium rounded-full';
if(status==4){
badge.textContent='Finished';
badge.classList.add('bg-slate-200','text-slate-700','dark:bg-slate-700','dark:text-slate-100');
}else if(status==5){
badge.textContent='Aborted';
badge.classList.add('bg-red-100','text-red-700','dark:bg-red-800','dark:text-red-200');
}else if(status>=0 && status<4){
badge.textContent='Running';
badge.classList.add('bg-green-100','text-green-700','dark:bg-green-800','dark:text-green-200');
}else{
badge.textContent='Ready';
badge.classList.add('bg-green-100','text-green-700','dark:bg-green-800','dark:text-green-200');
}
}
}
function oscillate(){
return 1+0.02*Math.sin(Date.now()/100);
}
//update the UI every frame
window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame||(function(callback,element){setTimeout(callback,1000/60);});
function frame(){
requestAnimationFrame(frame);
updateUI();
}
frame(); //start frame loop
//function to (re)initialize UI
function initUI(){
drawMeter(I("dlMeter"),0,meterBk,dlColor,0);
drawMeter(I("ulMeter"),0,meterBk,ulColor,0);
I("dlText").textContent="";
I("ulText").textContent="";
I("pingText").textContent="";
I("jitText").textContent="";
I("ip").textContent="";
}
</script>
<title>KuronekoServer SpeedTest</title>
</head>
<body onload="initServers()" class="font-sans antialiased bg-slate-50 dark:bg-slate-900">
<header class="w-full bg-white/90 dark:bg-slate-800/90 backdrop-blur border-b border-slate-200 dark:border-slate-700 text-slate-800 dark:text-slate-100">
<div class="max-w-6xl mx-auto px-4 py-4 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div>
<h1 class="text-2xl md:text-3xl font-semibold tracking-tight flex items-center gap-3">
KuronekoServer SpeedTest
</h1>
</div>
<div class="flex items-center gap-4 text-[11px] md:text-xs">
<a href="https://krnk.org/terms" target="_blank" class="hover:underline text-slate-600 dark:text-slate-300">利用規約</a>
<a href="https://krnk.org/privacy" target="_blank" class="hover:underline text-slate-600 dark:text-slate-300">プライバシー</a>
<a href="https://discord.krnk.org" target="_blank" class="hover:underline text-slate-600 dark:text-slate-300 flex items-center gap-1">Discord</a>
</div>
</div>
</header>
<main class="max-w-6xl mx-auto px-4 py-8">
<div id="loading" class="flex items-center justify-center py-16">
<p id="message" class="flex items-center gap-3 text-slate-600 dark:text-slate-300 text-lg"><svg class="animate-spin h-6 w-6 text-slate-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path></svg> サーバー選択中...</p>
</div>
<div id="testWrapper" class="hidden space-y-10">
<section class="flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 w-full">
<div class="flex items-center gap-3">
<button id="startStopBtn" type="button" class="group px-6 py-3 rounded-md font-medium bg-slate-800 hover:bg-slate-900 dark:bg-slate-700 dark:hover:bg-slate-600 text-white shadow focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition flex items-center gap-2" onclick="startStop()">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
<span class="start-label">Start</span>
</button>
</div>
<div id="serverArea" class="flex items-center gap-2">
<label for="server" class="text-sm font-medium text-slate-700 dark:text-slate-300 flex items-center gap-1"><svg class="w-4 h-4 text-slate-500 dark:text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>Server</label>
<select id="server" class="text-sm px-3 py-2 rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-slate-500 min-w-[12rem]" aria-label="Select test server"></select>
</div>
<div id="ipArea" class="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-300">
<svg class="w-4 h-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8 2 2 4 2 8v8c0 4 6 6 10 6s10-2 10-6V8c0-4-6-6-10-6Z"/><path d="M6 12h12"/><path d="M12 6v12"/></svg>
<span id="ip" class="font-mono"></span>
</div>
</div>
<div class="flex items-center gap-3 mt-4 md:mt-0">
<span id="statusBadge" class="hidden px-3 py-1 text-xs font-medium rounded-full bg-green-100 text-green-700 dark:bg-green-800 dark:text-green-200">Ready</span>
</div>
</section>
<section id="test" class="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4 flex flex-col gap-2">
<h2 class="text-xs font-semibold tracking-wide text-slate-500 dark:text-slate-400 uppercase flex items-center gap-2"><svg class="w-4 h-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v20"/><path d="M4 6h16"/><path d="M4 10h16"/><path d="M4 14h16"/><path d="M4 18h16"/></svg> Ping</h2>
<div class="flex items-end gap-2">
<div id="pingText" class="text-4xl font-semibold tabular-nums text-red-600 dark:text-red-400" aria-label="Ping value"></div>
<div class="text-sm font-medium text-red-500 dark:text-red-300">ms</div>
</div>
<div class="h-1 rounded bg-red-100 dark:bg-red-900/40 overflow-hidden">
<div id="pingBar" class="h-full bg-red-500 dark:bg-red-400 w-0 transition-all"></div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4 flex flex-col gap-2">
<h2 class="text-xs font-semibold tracking-wide text-slate-500 dark:text-slate-400 uppercase flex items-center gap-2"><svg class="w-4 h-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09c0 .7.4 1.34 1 1.51a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.7.16 1.34.76 1.51 1.47H21a2 2 0 1 1 0 4h-.09c-.7 0-1.34.4-1.51 1Z"/></svg> Jitter</h2>
<div class="flex items-end gap-2">
<div id="jitText" class="text-4xl font-semibold tabular-nums text-red-600 dark:text-red-400" aria-label="Jitter value"></div>
<div class="text-sm font-medium text-red-500 dark:text-red-300">ms</div>
</div>
<div class="h-1 rounded bg-red-100 dark:bg-red-900/40 overflow-hidden">
<div id="jitBar" class="h-full bg-red-500 dark:bg-red-400 w-0 transition-all"></div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4 flex flex-col gap-2 col-span-1">
<h2 class="text-xs font-semibold tracking-wide text-slate-600 dark:text-slate-300 uppercase flex items-center gap-2"><svg class="w-4 h-4 text-sky-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M7 10l5 5 5-5"/><path d="M12 15V3"/></svg> Download</h2>
<div class="relative h-40">
<canvas id="dlMeter" class="absolute inset-0 w-full h-full"></canvas>
<div id="dlText" class="absolute bottom-4 left-1/2 -translate-x-1/2 text-4xl font-semibold tabular-nums text-sky-700 dark:text-sky-400"></div>
</div>
<div class="flex items-center gap-2">
<div class="text-sm font-medium text-slate-600 dark:text-slate-300">Mbit/s</div>
<div id="dlBarWrapper" class="flex-1 h-1.5 rounded bg-sky-100 dark:bg-sky-900/40 overflow-hidden">
<div id="dlBar" class="h-full bg-sky-500 dark:bg-sky-400 w-0 transition-all"></div>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-800 rounded-xl shadow p-4 flex flex-col gap-2 col-span-1">
<h2 class="text-xs font-semibold tracking-wide text-slate-600 dark:text-slate-300 uppercase flex items-center gap-2"><svg class="w-4 h-4 text-amber-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 4v16"/></svg> Upload</h2>
<div class="relative h-40">
<canvas id="ulMeter" class="absolute inset-0 w-full h-full"></canvas>
<div id="ulText" class="absolute bottom-4 left-1/2 -translate-x-1/2 text-4xl font-semibold tabular-nums text-amber-700 dark:text-amber-400"></div>
</div>
<div class="flex items-center gap-2">
<div class="text-sm font-medium text-slate-600 dark:text-slate-300">Mbit/s</div>
<div id="ulBarWrapper" class="flex-1 h-1.5 rounded bg-amber-100 dark:bg-amber-900/40 overflow-hidden">
<div id="ulBar" class="h-full bg-amber-500 dark:bg-amber-400 w-0 transition-all"></div>
</div>
</div>
</div>
</section>
<section id="shareArea" style="display:none" class="rounded-xl shadow overflow-hidden border border-indigo-200 dark:border-indigo-900/40">
<div class="bg-indigo-600 dark:bg-indigo-500 text-white px-4 py-2 flex items-center gap-2">
<svg class="w-5 h-5 text-white" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H7a4 4 0 0 1-4-4V7a2 2 0 0 1 2-2h4"/><path d="M18 3v6"/><path d="M15 6h6"/></svg>
<h3 class="text-base font-semibold">結果共有 / Share Results</h3>
</div>
<div class="p-6 bg-indigo-50/60 dark:bg-slate-800 space-y-5">
<p class="text-sm text-slate-600 dark:text-slate-300">このリンクや埋め込みコードをSNS・ブログ・Webサイトに貼り付けできます。</p>
<div class="grid gap-4 md:grid-cols-2">
<div class="space-y-1">
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">Test ID</p>
<p class="font-mono text-sm" id="testId"></p>
</div>
<div class="space-y-1">
<p class="text-xs uppercase tracking-wide text-slate-500 dark:text-slate-400">Server</p>
<p class="font-mono text-sm" id="testServer">-</p>
</div>
</div>
<div class="space-y-2">
<label for="resultsURL" class="text-xs uppercase tracking-wide text-indigo-700 dark:text-indigo-300">共有リンク / Direct Link</label>
<div class="flex flex-col sm:flex-row gap-2">
<input type="text" value="" id="resultsURL" readonly class="flex-1 px-3 py-2 rounded-md bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-900/50 ring-1 ring-indigo-200/60 dark:ring-indigo-900/40 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" onclick="this.select();document.execCommand('copy');"/>
<div class="flex gap-2">
<button type="button" onclick="navigator.clipboard.writeText(I('resultsURL').value)" class="px-3 py-2 rounded-md bg-indigo-600 hover:bg-indigo-700 text-white text-sm">コピー</button>
<a id="openImageBtn" href="#" target="_blank" class="px-3 py-2 rounded-md border border-indigo-200 dark:border-indigo-900/50 text-indigo-700 dark:text-indigo-300 text-sm hover:bg-indigo-50/60 dark:hover:bg-slate-900">画像を開く</a>
</div>
</div>
</div>
<div class="space-y-2">
<label for="embedHTML" class="text-xs uppercase tracking-wide text-indigo-600 dark:text-indigo-400">埋め込み(HTML)</label>
<div class="flex flex-col sm:flex-row gap-2">
<input type="text" value="" id="embedHTML" readonly class="flex-1 px-3 py-2 rounded-md bg-white dark:bg-slate-900 border border-emerald-200 dark:border-emerald-900/40 ring-1 ring-emerald-200/60 dark:ring-emerald-900/40 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-emerald-500" onclick="this.select();document.execCommand('copy');"/>
<button type="button" onclick="navigator.clipboard.writeText(I('embedHTML').value)" class="px-3 py-2 rounded-md bg-emerald-600 hover:bg-emerald-700 text-white text-sm">コピー</button>
</div>
</div>
<div class="space-y-2">
<label for="embedMD" class="text-xs uppercase tracking-wide text-indigo-600 dark:text-indigo-400">埋め込み(Markdown)</label>
<div class="flex flex-col sm:flex-row gap-2">
<input type="text" value="" id="embedMD" readonly class="flex-1 px-3 py-2 rounded-md bg-white dark:bg-slate-900 border border-amber-200 dark:border-amber-900/40 ring-1 ring-amber-200/60 dark:ring-amber-900/40 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-amber-500" onclick="this.select();document.execCommand('copy');"/>
<button type="button" onclick="navigator.clipboard.writeText(I('embedMD').value)" class="px-3 py-2 rounded-md bg-amber-600 hover:bg-amber-700 text-white text-sm">コピー</button>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-3 rounded-md border border-indigo-200/60 dark:border-indigo-900/40">
<img src="" id="resultsImg" class="max-w-full rounded-md" />
</div>
</div>
</section>
<section class="text-sm space-y-2 leading-relaxed">
<p>本Speedtestは <a href="https://krnk.org" target="_blank">KuronekoServer</a> によって運営・管理されており、<a href="https://github.com/librespeed/speedtest" target="_blank">LibreSpeed</a> を使用して運営しています。何か問題があれば <a href="https://discord.krnk.org" target="_blank">Discord Server</a> までお問い合わせくださいませ。</p>
<p>This Speedtest is operated and maintained by <a href="https://krnk.org" target="_blank">KuronekoServer</a> and is run using <a href="https://github.com/librespeed/speedtest" target="_blank">LibreSpeed</a>. If you have any problems, please contact <a href="https://discord.krnk.org" target="_blank">Discord Server</a>.</p>
</section>
</div>
</main>
<footer class="text-center text-xs py-6 text-slate-500 dark:text-slate-400">© 2018 KuronekoServer</footer>
</body>
</html>