Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions src/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
z-index: 10;
}

th[data-sort] {
user-select: none;
}

th[data-sort]:after {
content: " ⇅";
font-size: 0.9em;
color: #222121;
}

/* device name column */
tr th:first-child, tr td:first-child {
width: 160px;
Expand Down Expand Up @@ -133,21 +143,21 @@ <h1>Welcome to Olaki.</h1>
<h2 id="olaki-table-header">Table</h2>
<p id="quick-table-stats"></p>
<div id="table-container">
<table id="olaki-table">
<table id="olaki-table" class="sortable">
<thead>
<tr>
<th>Device name</th>
<th>Codename</th>
<th class="os-column-header">crDroid</th>
<th class="os-column-header">/e/OS</th>
<th class="os-column-header">Kali<br>Linux</th>
<th class="os-column-header">Lineage<br>OS</th>
<th class="os-column-header">OmniROM</th>
<th class="os-column-header">postmarket<br>OS</th>
<th class="os-column-header">Ubuntu<br>Touch</th>
<th class="os-column-header">Calyx<br>OS</th>
<th class="os-column-header">Graphene<br>OS</th>
<th class="os-column-header">iodéOS</th>
<th data-sort="natural">Device name</th>
<th data-sort="natural">Codename</th>
<th data-sort="natural" class="os-column-header">crDroid</th>
<th data-sort="text" class="os-column-header">/e/OS</th>
<th data-sort="text" class="os-column-header">Kali<br>Linux</th>
<th data-sort="text" class="os-column-header">Lineage<br>OS</th>
<th data-sort="text" class="os-column-header">OmniROM</th>
<th data-sort="text" class="os-column-header">postmarket<br>OS</th>
<th data-sort="natural" class="os-column-header">Ubuntu<br>Touch</th>
<th data-sort="text" class="os-column-header">Calyx<br>OS</th>
<th data-sort="text" class="os-column-header">Graphene<br>OS</th>
<th data-sort="text" class="os-column-header">iodéOS</th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -355,5 +365,6 @@ <h3>How do I run my phone without a battery?</h3>
// no need for an appendix section for iodéOS

</script>
<script src="table-sorting.js"></script>
</body>
</html>
126 changes: 126 additions & 0 deletions src/public/table-sorting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const TableSort = (function () {
/**
* NaturalSorter helps sort natural text.
* e.g. ['File1', 'File20', 'File10', 'File2'] gets sorted into ['File1', 'File2', 'File10', 'File20']
*/
class NaturalSorter {
/**
* Converts a string like "File12A" into tokens:
* ["file", 12, "a"]
*/
static tokenize(str, locale) {
// Split into numeric and non-numeric parts
const re = /(\d+|\D+)/g;
const parts = [];
let match;

while ((match = re.exec(str)) !== null) {
const part = match[0];
if (/^\d+$/.test(part)) {
// zero-pad numbers so lexicographic compare works correctly
parts.push(part.padStart(10, '0'));
} else {
// locale-aware lowercasing for stable text comparison
parts.push(part.toLocaleLowerCase(locale));
}
}

return parts;
}

/**
* Compares two token lists (e.g., ["a", 12] vs ["a", 2])
*/
static compare(aTokens, bTokens, asc) {
const len = Math.max(aTokens.length, bTokens.length);

for (let i = 0; i < len; i++) {
const a = aTokens[i] || '';
const b = bTokens[i] || '';

if (a === b) continue;

const result = a < b ? -1 : 1;
return asc ? result : -result;
}

return 0;
}
}

function parseValue(text, type, locale) {
const trimmed = text.trim();

switch (type) {
case 'number':
return parseFloat(trimmed) || 0;
case 'date':
return new Date(trimmed).getTime();
case 'natural':
return NaturalSorter.tokenize(trimmed, locale);
default: // text
return trimmed.toLocaleLowerCase(locale);
}
}

function sortTable(table, columnIndex, type, asc, locale) {
const tbody = table.tBodies[0];
const rows = Array.from(tbody.rows);

rows.sort((rowA, rowB) => {
const aVal = parseValue(rowA.cells[columnIndex].textContent, type, locale);
const bVal = parseValue(rowB.cells[columnIndex].textContent, type, locale);

if (type === 'natural') {
return NaturalSorter.compare(aVal, bVal, asc);
}

if (type === 'text') {
const cmp = aVal.localeCompare(bVal, locale);
return asc ? cmp : -cmp;
}

// other types
if (aVal === bVal) return 0;
return asc ? (aVal > bVal ? 1 : -1) : aVal < bVal ? 1 : -1;
});

rows.forEach(row => tbody.appendChild(row));
}

function initSortableTable(table) {
const headers = table.querySelectorAll('thead th');

headers.forEach((th, index) => {
const type = th.dataset.sort;

if (!type) return; // only sort columns with data-sort="..."

th.style.cursor = 'pointer';
th.dataset.asc = 'true';

th.addEventListener('click', () => {
const asc = th.dataset.asc === 'true';
const locale = th.dataset.locale ?? 'en';
sortTable(table, index, type, asc, locale);
th.dataset.asc = (!asc).toString(); // toggle for next click
});
});
}

// add sorting capability to any table with the class 'sortable'
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('table.sortable').forEach(initSortableTable);
});

// public API
return {
sort(table, columnIndex, options = {}) {
const th = table.querySelectorAll('th')[columnIndex];
const type = options.type || th?.dataset.sort;
const asc = options.asc ?? true;
const locale = options.locale || th?.dataset.locale;
sortTable(table, columnIndex, type, asc, locale);
},
};
})();
8 changes: 5 additions & 3 deletions src/ts/device_summary_extractor/publicAssetsBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
const SRC_PUBLIC_DIRECTORY = './src/public';
const DIST_PUBLIC_DIRECTORY = './dist/public';
const JS_RESULT_FILENAME = 'olaki-data.js';
const JS_TABLE_SORTING_FILENAME = 'table-sorting.js';
const INDEX_FILENAME = 'index.html';
const JS_RESULT_FILE_PATH = `${DIST_PUBLIC_DIRECTORY}/${JS_RESULT_FILENAME}`;

Expand Down Expand Up @@ -47,12 +48,13 @@ const createJavaScriptFileInPublicDirectory = (codenameToDeviceSummary: Codename
writeFileSync(JS_RESULT_FILE_PATH, javaScriptFileContent);
};

const copyIndexFileToPublicDirectory = () =>
copyFileSync(`${SRC_PUBLIC_DIRECTORY}/${INDEX_FILENAME}`, `${DIST_PUBLIC_DIRECTORY}/${INDEX_FILENAME}`);
const copyFileToPublicDirectory = (filename: string) =>
copyFileSync(`${SRC_PUBLIC_DIRECTORY}/${filename}`, `${DIST_PUBLIC_DIRECTORY}/${filename}`);

export const buildPublicDirectory = (codenameToDeviceSummary: CodenameToDeviceSummary) => {
createJavaScriptFileInPublicDirectory(codenameToDeviceSummary);
copyIndexFileToPublicDirectory();
copyFileToPublicDirectory(JS_TABLE_SORTING_FILENAME);
copyFileToPublicDirectory(INDEX_FILENAME);

logger.info('[Public Assets Builder] Success.');
};
Loading