Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 14:25:43 +01:00
parent ef295a34d2
commit bbab4c6180
352 changed files with 14422 additions and 1 deletions

22
desktop/build.gradle.kts Normal file
View file

@ -0,0 +1,22 @@
plugins {
kotlin("jvm")
}
group = "com.linuxcommandlibrary"
version = "1.0"
dependencies {
implementation(project(":common"))
implementation(libs.kotlinx.html.jvm)
implementation(libs.json)
implementation(libs.sqldelight.sqlite.driver)
implementation(libs.kotlinx.coroutines.core)
}
kotlin {
compilerOptions {
sourceSets["main"].apply {
resources.srcDirs("../assets")
}
}
}

View file

@ -0,0 +1,60 @@
package com.linuxcommandlibrary.desktop
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.initDatabase
import java.io.File
import java.io.PrintStream
/* Copyright 2022 Simon Schubert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
fun main() {
initDatabase()
val builder = FdroidInfoBuilder()
builder.buildFullDescription()
builder.buildShortDescription()
}
class FdroidInfoBuilder {
fun buildFullDescription() {
val file = File("fastlane/metadata/android/en-US/full_description.txt")
val stream = PrintStream(file)
stream.appendLine("The app currently has <b>${databaseHelper.getCommands().size}</b> manual pages, <b>${databaseHelper.getBasics().size}</b> basic categories and a bunch of general terminal tips. It works 100% offline, doesn't need an internet connection and has no tracking software.")
stream.appendLine()
stream.appendLine("<b>Categories</b>")
stream.appendLine()
databaseHelper.getBasics().forEach { category ->
stream.appendLine("* ${category.title}")
}
stream.appendLine()
stream.appendLine("<b>Tips</b>")
stream.appendLine()
databaseHelper.getTips().forEach { tip ->
stream.appendLine("* ${tip.title}")
}
stream.close()
}
fun buildShortDescription() {
val file = File("fastlane/metadata/android/en-US/short_description.txt")
val stream = PrintStream(file)
stream.appendLine("${databaseHelper.getCommands().size} manual pages, ${databaseHelper.getBasics().size} basic categories and a bunch of general terminal tips.")
stream.close()
}
}

View file

@ -0,0 +1,114 @@
package com.linuxcommandlibrary.desktop
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.initDatabase
import java.io.File
import java.io.PrintStream
/* Copyright 2022 Simon Schubert
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
fun main() {
initDatabase()
val markdownBuilder = MarkdownBuilder()
markdownBuilder.build()
}
class MarkdownBuilder {
fun build() {
val file = File("README.md")
val stream = PrintStream(file)
stream.appendLine("## Linux Command Library (Mobile+CLI+Web)")
stream.appendLine()
stream.appendLine("![Icon](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandLibrary/master/art/web_hi_res_144.png)")
stream.appendLine()
stream.appendLine("The app currently has **${databaseHelper.getCommands().size}** manual pages, **${databaseHelper.getBasics().size}+** basic categories and a bunch of general terminal tips. It works 100% offline, doesn't need an internet connection and has no tracking software.")
stream.appendLine()
stream.appendLine("[![Play Store](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/play_store_badge.png)](https://play.google.com/store/apps/details?id=com.inspiredandroid.linuxcommandbibliotheca)")
stream.appendLine("[![F-Droid](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/fdroid_badge.png)](https://f-droid.org/en/packages/com.inspiredandroid.linuxcommandbibliotheca/)")
stream.appendLine("[![Web](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/web_badge.png)](https://linuxcommandlibrary.com)")
stream.appendLine()
stream.appendLine("### Mobile screenshots")
stream.appendLine()
stream.appendLine("<p>")
val mobileScreenshotFiles =
listOf("screen-1.png", "screen-2-dark.png", "screen-3.png", "screen-4-dark.png")
mobileScreenshotFiles.forEach { fileName ->
stream.appendLine("<img src=\"https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/${fileName}\" width=\"200\">")
}
stream.appendLine("</p>")
val tabletScreenshotFiles = listOf("screen-1-tablet.png", "screen-2-tablet.png")
tabletScreenshotFiles.forEach { fileName ->
stream.appendLine("<img src=\"https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/${fileName}\" width=\"400\">")
}
stream.appendLine()
stream.appendLine("### CLI screenshot")
stream.appendLine()
stream.appendLine("<img src=\"https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-cli-1.png\" width=\"300\">")
stream.appendLine()
stream.appendLine("Execute `gradle :cli:buildJar` to create jar file for Linux, Windows and Mac.")
stream.appendLine()
stream.appendLine("### Content")
stream.appendLine()
stream.appendLine("#### Categories")
stream.appendLine()
stream.appendLine(
databaseHelper.getBasics().joinToString { category ->
category.title
},
)
stream.appendLine()
stream.appendLine("#### Tips")
stream.appendLine()
stream.appendLine(
databaseHelper.getTips().joinToString { tip ->
tip.title
},
)
stream.appendLine()
stream.appendLine("### CI/CD")
stream.appendLine()
stream.appendLine("[Github Action](.github/workflows/android.yml) to automatically create a new Github release with APK and JAR and upload an AAB to the Play Store.")
stream.appendLine()
stream.appendLine("### Tests")
stream.appendLine()
stream.appendLine("Android Jetpack Compose screen tests: [ComposeTests.kt](android/src/androidTest/java/com/inspiredandroid/linuxcommandbibliotheca/ComposeTests.kt)")
stream.appendLine()
stream.appendLine("Android Jetpack Compose deeplinking tests: [ComposeDeeplinkTests.kt](android/src/androidTest/java/com/inspiredandroid/linuxcommandbibliotheca/ComposeDeeplinkTests.kt)")
stream.appendLine()
stream.appendLine("Common code unit tests: [CommonTests.kt](common/src/commonTest/kotlin/CommonTests.kt)")
stream.appendLine()
stream.appendLine("### Licensing")
stream.appendLine()
stream.appendLine("The source code is licensed under the Apache 2.0 license and the copyright of the man pages in the `database.db` file are copyrighted by their respective authors.")
stream.appendLine()
stream.appendLine("### Thanks to")
stream.appendLine()
stream.appendLine("http://letsgokoyo.com - App Icon")
stream.appendLine()
stream.appendLine("https://www.commandlinefu.com - Lots of one-liners")
stream.appendLine()
stream.appendLine("https://icons8.com - Icons")
stream.appendLine()
stream.appendLine("https://tldr.sh - TLDR")
stream.close()
}
}

View file

@ -0,0 +1,62 @@
package com.linuxcommandlibrary.desktop
import java.io.File
import java.nio.file.Files
class Minifier {
fun minifyScriptsAndSheets(isRelease: Boolean) {
val scriptsDir = File("html/scripts")
scriptsDir.mkdir()
val scripts = File("desktop/src/main/resources/scripts")
scripts.listFiles()?.forEach {
if (it.isFile) {
val file = File(scriptsDir, it.name)
file.delete()
if (isRelease) {
val minified = minifyJS(it.readText())
file.writeText(minified)
} else {
Files.createLink(file.toPath(), it.toPath())
}
}
}
val styleSheetsDir = File("html/stylesheets")
styleSheetsDir.mkdir()
val stylesheets = File("desktop/src/main/resources/stylesheets")
stylesheets.listFiles()?.forEach {
if (it.isFile) {
val file = File(styleSheetsDir, it.name)
file.delete()
if (isRelease) {
val minified = minifyCSS(it.readText())
file.writeText(minified)
} else {
Files.createLink(file.toPath(), it.toPath())
}
}
}
}
private fun minifyCSS(css: String): String = css.replaceWhiteSpacesBeforeAndAfter(";")
.replaceWhiteSpacesBeforeAndAfter("}")
.replaceWhiteSpacesBeforeAndAfter("\\{")
.replaceWhiteSpacesBeforeAndAfter(":")
.replaceWhiteSpacesBeforeAndAfter(",")
private fun minifyJS(js: String): String = js.replace("[\\n\\s].?//.*\\n".toRegex(), "") // will break if comment is after code
.replaceWhiteSpacesBeforeAndAfter(";")
.replaceWhiteSpacesBeforeAndAfter("}")
.replaceWhiteSpacesBeforeAndAfter("\\{")
.replaceWhiteSpacesBeforeAndAfter("=")
.replaceWhiteSpacesBeforeAndAfter("<")
.replaceWhiteSpacesBeforeAndAfter("-")
.replaceWhiteSpacesBeforeAndAfter(",")
.replaceWhiteSpacesBeforeAndAfter("\\+")
.replaceWhiteSpacesBeforeAndAfter("\\(")
.replaceWhiteSpacesBeforeAndAfter("\\)")
.replaceWhiteSpacesBeforeAndAfter("\\&")
.replaceWhiteSpacesBeforeAndAfter("\\>")
private fun String.replaceWhiteSpacesBeforeAndAfter(value: String): String = replace("\\s*$value\\s*".toRegex(), value)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
var timeout;
function copy(text) {
var inp = document.createElement('input');
document.body.appendChild(inp);
inp.value = unEscapeHtml(text);
inp.select();
document.execCommand('copy', false);
inp.remove();
clearTimeout(timeout);
hide();
timeout = setTimeout(function() {
hide();
}, 1500);
setTimeout(function() {
show();
}, 100);
}
function show() {
var element = document.getElementsByClassName("tooltip")[0];
element.classList.remove("hidden");
element.classList.add("visible");
}
function hide() {
var element = document.getElementsByClassName("tooltip")[0];
element.classList.remove("visible");
element.classList.add("hidden");
}
function unEscapeHtml(unsafe) {
return unsafe
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, "\"")
.replace(/&#039;/g, "'")
.replace(/&#47;/g, "index.html")
.replace(/\\/g, "\\\\");
}

View file

@ -0,0 +1,65 @@
var accordionButtons = document.getElementsByClassName("accordion-button");
var toggleAllButton = document.getElementsByClassName("toggle-all-button")[0];
function togglePanel(param) {
param.classList.toggle("active");
updatePanel(param);
if (allCollapsed()) {
toggleAllButton.classList.remove("active");
toggleAllButton.classList.add("active");
toggleAllButton.innerText = "EXPAND ALL";
} else if (isAnyExpanded()) {
toggleAllButton.classList.remove("active");
toggleAllButton.innerText = "COLLAPSE ALL";
}
}
function toggleAll(param) {
param.classList.toggle("active");
if (param.classList.contains("active")) {
param.innerText = "EXPAND ALL";
updateList(true);
} else {
param.innerText = "COLLAPSE ALL";
updateList(false);
}
}
function updateList(collapse) {
for (var i = 0; i < accordionButtons.length; i++) {
accordionButtons[i].classList.remove("active");
if (!collapse) {
accordionButtons[i].classList.add("active");
}
updatePanel(accordionButtons[i]);
}
}
function updatePanel(button) {
var panel = button.nextElementSibling;
if (button.classList.contains("active")) {
panel.style.display = "block";
} else {
panel.style.display = "none";
}
}
function isAnyExpanded() {
for (var i = 0; i < accordionButtons.length; i++) {
if (accordionButtons[i].classList.contains("active")) {
return true;
}
}
return false;
}
function allCollapsed() {
for (var i = 0; i < accordionButtons.length; i++) {
if (accordionButtons[i].classList.contains("active")) {
return false;
}
}
return true;
}

View file

@ -0,0 +1,83 @@
var input, filter, ul, li, a, i, headers, value, index, lastSearch = "";
window.onload = (event) => {
input = document.getElementById('search');
ul = document.getElementById("commandlist");
li = ul.getElementsByTagName('a');
headers = ul.getElementsByTagName('div');
};
document.addEventListener('keyup', (e) => {
if (e.keyCode == 38) {
focusPreviousTabStop();
} else if (e.keyCode == 40) {
focusNextTabStop();
}
});
function getVisibleTabs() {
var universe = document.querySelectorAll('#commandlist a');
return Array.prototype.filter.call(universe, function(item) {return item.parentNode.style.display !== "none"});
}
function focusNextTabStop() {
var tabs = getVisibleTabs();
var index = tabs.indexOf(document.activeElement);
return (tabs[index + 1] || tabs[0]).focus();
}
function focusPreviousTabStop() {
var tabs = getVisibleTabs();
var index = tabs.indexOf(document.activeElement);
if(index == 0) {
input.focus();
} else {
(tabs[index - 1] || tabs[0]).focus();
}
}
function search(){
return Promise.resolve()
.then(function() {
setTimeout(function() {
filter = input.value.toLowerCase();
if(lastSearch === filter) {
return
}
if(lastSearch === "") {
for (i = 0; i < headers.length; i++) {
headers[i].style.display = "none";
}
}
if(filter === "") {
for (i = 0; i < headers.length; i++) {
headers[i].style.display = "";
}
}
lastSearch = filter;
var numberOfResults = 0;
for (i = 0; i < li.length; i++) {
value = li[i].getAttribute('data-c');
index = value.indexOf(filter);
if (index > -1) {
value = value.substring(0, index) + "<span class='highlight'>" + value.substring(index, index + filter.length) + "</span>" + value.substring(index + filter.length);
li[i].innerHTML = value;
numberOfResults++;
li[i].style.display = "";
} else {
li[i].style.display = "none";
}
}
var noresults = document.getElementById("no-results");
if(numberOfResults == 0) {
noresults.innerHTML = "No commands found for '" + filter + "'";
noresults.style.display = "block";
ul.style.display = "none";
} else {
noresults.style.display = "none";
ul.style.display = "block";
}
}, 0);
});
}

View file

@ -0,0 +1,555 @@
body {
margin: 0px;
padding: 0px;
color: #151515;
font-family:sans-serif;
background-color: #f9f7f6;
display: flex;
flex-direction: column;
min-height: 100vh;
}
a {
outline-color: #F44336;
}
nav {
display: flex;
background: #e45151;
height: 44px;
width: 100%;
display: flex;
justify-content: center;
}
nav ul {
display: flex;
padding: 0;
margin: 0;
font-size: 22px;
text-transform: uppercase;
font-weight: bold;
margin: auto;
max-width: 900px;
width: 900px;
}
nav li {
display: block;
margin-top: auto;
margin-bottom: auto;
}
nav li a {
padding-left: 8px;
padding-right: 8px;
height: 44px;
line-height: 44px;
display: inline-block;
}
nav li a:hover {
background-color: #f9f7f6;
}
nav li .selected:hover {
background-color: #161616;
}
nav li .selected {
color: #f9f7f6 !important;
}
#top-border {
height: 60px;
width: 100%;
background: #161616;
display: flex;
justify-content: center;
}
#top-border > div {
display: flex;
flex-wrap: wrap;
align-items: center;
height: 60px;
margin: auto;
max-width: 900px;
width: 900px;
padding-left: 8px;
padding-right: 8px;
}
#filler {
flex-grow: 1;
}
footer {
text-align: center;
padding: 8px;
background: #e45151;
color: #151515;
}
#content, .grid-container, .masonry {
align-self: center;
width: auto;
}
#content-wrapper {
display: flex;
flex-flow: row;
justify-content: center;
}
.grid-container, .masonry {
max-width: 900px;
}
#content {
max-width: 900px;
}
@media (max-width: 1000px) {
.side-panel {
display: none;
}
}
@media (min-width: 1000px) {
.bottom-panel {
display: none;
}
}
.bottom-panel {
text-align: center;
background-color: #e4751a;
}
.bottom-panel a {
display: block;
width: 100%;
}
.side-panel a {
display: block;
height: 100%;
}
.title {
font-size: 16px;
padding-left: 10px;
padding-right: 24px;
font-weight: bold;
color: #dbdbdb;
display: inline-flex;
flex: auto;
text-transform: uppercase;
}
.title span::first-letter {
color: #e45151;
}
.title span {
margin-left: 8px;
}
a:link, a:visited, a:hover, a:active {
color: #151515;
text-decoration: none;
}
h1 {
text-align: center;
background: #f0f0f0;
width: max-content;
margin-left: auto;
margin-right: auto;
padding: 4px 8px;
margin-top: 16px;
margin-top: 16px;
margin-bottom: 0px;
}
.download-icon {
margin-top: 8px;
}
.logo-icon {
height: 48px;
width: 48px;
background-size: 48px 48px;
display: block;
}
.server-button {
text-align: center;
margin-top: 12px;
margin-bottom: 12px;
}
.server-button img {
border: black 2px solid;
max-width: calc(100% - 20px);
width: 500px;
}
.server-button img:hover {
border-color: #F44336;
}
.project img {
border-radius: 5px;
}
.project {
font-size: 12px;
text-align: center;
}
.masonry {
-webkit-column-width: 300px;
-moz-column-width: 300px;
column-width: 300px;
-webkit-column-gap: 10px;
-moz-column-gap: 10px;
column-gap: 12px;
margin: 12px;
}
.grid-container {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin: 6px;
padding: 0;
list-style: none;
width: auto;
}
.grid-item {
background-color: #eee;
color: #444;
width: 128px;
height: 128px;
text-align: center;
margin: 6px;
min-width: 128px;
flex: 1 1 128px;
max-width: 180px;
}
.grid-item h2 {
padding: 0;
margin: 0;
margin-left: 14px;
margin-right: 14px;
font-size: 18px;
font-weight: bold;
}
.grid-item:hover {
background-color: #e45151;
}
.grid-item div {
width: 100%;
height: 100%
}
.grid-item i {
width: 40px;
height: 40px;
display: block;
background-size: 40px 40px;
margin-left: auto;
margin-right: auto;
margin-bottom: 4px;
background-repeat: no-repeat;
background-position: bottom;
padding-top: 28px;
}
.highlight {
color: #F44336;
}
#search {
background-image: url(/images/icon-search.svg);
background-position: 10px 12px;
background-repeat: no-repeat;
font-size: 16px;
padding: 12px 20px 12px 40px;
border: 2px solid #ddd;
border-radius: 2px;
outline-color: #F44336;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
}
#search-wrapper {
width: 100%;
display: inline-block;
}
#no-results {
display: none;
padding: 24px;
text-align: center;
font-size: 20px;
}
#commandlist {
list-style-type: none;
padding: 0;
margin: 0;
margin-top: 12px;
border: solid #ddd;
border-width: 0px 0px 1px 0px;
}
#commandlist a, #commandlist div.headline {
border: solid #ddd;
background-color: #f6f6f6;
padding: 6px;
text-decoration: none;
font-size: 1.4em;
display: block;
border-width: 1px 1px 0 1px;
}
#commandlist div.headline {
background-color: #e2e2e2;
}
#commandlist a:hover:not(.headline) {
background-color: #F44336;
}
#commandlist a:hover:not(.headline) span {
color: #000;
}
.accordion-button {
background-color: #dfdfdf;
color: #444;
cursor: pointer;
padding: 12px;
font-weight: bold;
font-size: 1.3em;
margin: 0px;
font-family: Sans-serif;
}
.toggle-all-button {
background-color: #f9f9f9;
color: #444;
cursor: pointer;
padding: 12px;
position: fixed;
bottom: 10px;
right: 10px;
width: 190px;
outline: none;
font-weight: bold;
font-size: 1.3em;
text-align: center;
border: 2px #f9f9f9 solid;
}
.accordion-button:hover, .toggle-all-button:hover {
background-color: #e45151;
}
.panel {
padding: 0 18px;
display: flow-root;
background-color: #eee;
}
.panel h3 {
text-align: left;
margin: 0px;
margin-bottom: 6px;
font-size: 17px;
}
.panel h3 a {
color: black !important;
}
.panel a {
font-weight: bold;
color: #F44336;
}
.subtitle {
text-align: center;
font-size: 20px;
}
pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
.code {
color: #fff;
background-color: #000;
padding: 2px;
display: inline-block;
white-space: normal;
word-break: break-word;
margin-bottom: 5px;
max-width: 550px;
padding: 4px;
}
.code a:link, .code a:visited {
color: #e45151;
text-decoration: none;
target-new: none;
}
.code-wrapper {
display: flex;
}
.code-group {
background-color: #eee;
padding-left: 8px;
padding-right: 8px;
padding-bottom: 4px;
width: 100%;
box-sizing: border-box;
margin: 0;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid-column;
display: inline-grid;
margin-bottom: 15px;
}
.code-group h2 {
padding-top: 6px;
margin-bottom: 8px;
margin-top: 2px;
font-size: 20px;
}
.copy-button img {
margin-left: 4px;
margin-top: 2px;
cursor: pointer;
}
.copy-button {
display: inline-block;
margin-top: auto;
margin-bottom: auto;
}
.visible {
visibility: visible !important;
opacity: 1;
transition: opacity 0.3s linear;
}
.hidden {
visibility: hidden !important;
opacity: 0;
transition: visibility 0s 0.3s, opacity 0.3s linear;
}
.tooltip {
position: fixed;
background: #bbddab;
padding: 4px;
left: 50%;
border: #bbddab 2px solid;
border-radius: 2px;
font-size: 18px;
transform: translateY(-50%) translateX(-50%);
top: 50%;
visibility: hidden;
}
td:first-child {
white-space: nowrap;
font-weight: bold;
vertical-align: top;
}
table {
border-spacing: 4px 1px;
}
@media screen and (max-width: 410px) {
#logo-icon-wrapper {
display: none !important;
}
}
@media (prefers-color-scheme: dark) {
body {
background-color: #383838;
color: #bebebe;
}
nav a:link,nav a:visited,nav a:hover,nav a:active {
color: #151515;
text-decoration: none;
}
h1 {
background: #282828;
color: #ffffff;
}
a:link, a:visited, a:hover, a:active {
color: #ffffff;
}
.copy-button img, .invert-color {
filter: invert(100%) sepia(0%) saturate(2968%) hue-rotate(17deg) brightness(114%) contrast(87%);
}
.grid-item {
background-color: #1e1e1e;
border-color: #444;
}
#search {
border-color: #444;
background-color: #d3d2d2;
}
#commandlist {
border-color: #444;
}
#commandlist a {
background-color: #282828;
border-color: #444;
}
#commandlist div.headline {
background-color: #585858;
border-color: #444;
}
.panel h3 a {
color: #ffffff !important;
}
.accordion-button, .toggle-all-button {
background-color: #282828;
border-color: #444;
}
.panel {
background-color: #1e1e1e;
}
.toggle-all-button {
color: #ffffff;
}
.code-group {
background: #1e1e1e;
}
.tooltip {
color: #000000;
}
}