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

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View file

@ -0,0 +1,68 @@
package com.linuxcommandlibrary.shared
import android.content.Context
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import com.linuxcommandlibrary.CommandDatabase
import java.io.File
import java.io.InputStream
import java.io.OutputStream
/* 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.
*/
actual var databaseHelper = DatabaseHelper()
fun hasDatabase(context: Context): Boolean {
val file = File(context.dataDir, "databases/database.db")
return file.exists()
}
fun copyDatabase(context: Context, onUpdateStatus: (Int) -> Unit = {}) {
val databaseFolder = File(context.dataDir, "databases")
if (!databaseFolder.exists()) {
databaseFolder.mkdir()
}
val file = File(databaseFolder, "database.db")
val inputStream = context.assets.open("database.db")
inputStream.copyToWithStatus(file.outputStream(), onUpdateStatus)
inputStream.close()
// Delete old realm database
val filesFolder = File(context.dataDir, "files")
if (filesFolder.exists()) {
File(filesFolder, "database.realm").delete()
}
}
fun InputStream.copyToWithStatus(out: OutputStream, onUpdateStatus: (Int) -> Unit = {}): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
var bytes = read(buffer)
val totalSize = this.available().toFloat()
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
onUpdateStatus(bytesCopied.div(totalSize).times(100f).toInt())
}
return bytesCopied
}
fun initDatabase(context: Context) {
val driver: SqlDriver = AndroidSqliteDriver(CommandDatabase.Schema, context, "database.db")
databaseHelper.setupDriver(driver)
}

View file

@ -0,0 +1,25 @@
package com.linuxcommandlibrary.shared
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
/* 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.
*/
@Preview
@Composable
fun AppPreview() {
// App()
}

View file

@ -0,0 +1,33 @@
package com.linuxcommandlibrary.shared
import com.linuxcommandlibrary.CommandDatabase
import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
import java.io.File
/* 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.
*/
actual fun getPlatformName(): String = "Desktop"
actual var databaseHelper = DatabaseHelper()
fun initDatabase() {
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite:assets/database.db")
if (!File("assets/database.db").exists()) {
CommandDatabase.Schema.create(driver)
}
databaseHelper.setupDriver(driver)
}

View file

@ -0,0 +1,127 @@
package com.linuxcommandlibrary.shared
import databases.BasicCategory
import databases.Command
import databases.CommandSection
import java.util.Locale
/* 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.
*/
sealed class CommandElement {
data class Text(val text: String) : CommandElement()
data class Man(val man: String) : CommandElement()
data class Url(val command: String, val url: String) : CommandElement()
}
/**
* Search in name and description and return sorted by priority
*/
fun List<Command>.sortedSearch(phrase: String): List<Command> = this.sortedBy {
val name = it.name.lowercase()
val lowercasePhrase = phrase.lowercase()
when {
!name.contains(lowercasePhrase) -> 30
name == lowercasePhrase -> 0
name.startsWith(lowercasePhrase) -> 10
else -> 20
}
}
/**
* Return a list of sealed Elements for visual representation
*/
fun String.getCommandList(
mans: String,
hasBrackets: Boolean = false,
checkExisting: Boolean = false,
): List<CommandElement> {
var command = " $this"
val list = mutableListOf<CommandElement>()
mans.split(",").filterNot { it.isEmpty() }.map { it.replace("(", "").replace(")", "") }
.forEach {
command = if (it.startsWith("url:")) {
val cmd = it.substring(4).split("|").first()
command.replace(cmd, " ü${it}ä")
} else {
if (hasBrackets) {
val escapedIt = Regex.escape(it) // Escapes special characters, e.g., "pbmto\\*\\*\\*"
val regex = "(?:[\\s,])($escapedIt)".toRegex()
command.replace(regex, " ü${it}ä")
} else {
command.replace(it, " ü${it}ä")
}
}
}
var currentText = ""
var currentCommand = ""
var isCommand = false
command.trim().forEach {
if (it == 'ü') {
list.add(CommandElement.Text(currentText.replace("\n", "")))
currentText = ""
isCommand = true
} else if (it == 'ä') {
if (currentCommand.isNotBlank()) {
when {
currentCommand.startsWith("url:") -> {
val url = currentCommand.split("|").last()
val cmd = currentCommand.substring(4).split("|").first()
list.add(CommandElement.Url(cmd, url))
}
checkExisting && databaseHelper.getCommand(currentCommand) == null -> {
list.add(CommandElement.Text(currentCommand))
}
else -> {
list.add(CommandElement.Man(currentCommand))
}
}
}
currentCommand = ""
isCommand = false
} else {
if (isCommand) {
currentCommand += it
} else {
currentText += it
}
}
}
list.add(CommandElement.Text(currentText.replace("[cmd]", "[command]").replace("\n", "")))
return list.toList()
}
val onlyCharactersRegex = "[^a-z]".toRegex()
/**
* Only allow characters in html file names to guarantee matching on the website and app deep linking
*/
fun BasicCategory.getHtmlFileName(): String = this.title.lowercase(Locale.US).replace(onlyCharactersRegex, "")
/**
* Show TLDR and SYNOPSIS always on the top and SEE ALSO and AUTHOR on the bottom. Everything else in between
*/
fun CommandSection.getSortPriority(): Int = when (this.title) {
"TLDR" -> 0
"SYNOPSIS" -> 10
"SEE ALSO" -> 90
"AUTHOR" -> 100
else -> 50
}
fun String.isLetter(): Boolean = this.firstOrNull() in 'a'..'z' || this.firstOrNull() in 'A'..'Z'

View file

@ -0,0 +1,3 @@
package com.linuxcommandlibrary.shared
class EmptyClass

View file

@ -0,0 +1,61 @@
package com.linuxcommandlibrary.shared
import app.cash.sqldelight.db.SqlDriver
import com.linuxcommandlibrary.CommandDatabase
import databases.BasicCategory
import databases.BasicCommand
import databases.BasicGroup
import databases.Command
import databases.CommandQueries
import databases.CommandSection
import databases.Tip
import databases.TipSection
/* 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.
*/
expect var databaseHelper: DatabaseHelper
class DatabaseHelper {
private lateinit var sqlDriver: SqlDriver
private lateinit var commandQueries: CommandQueries
fun setupDriver(driver: SqlDriver) {
sqlDriver = driver
// println("Setup driver: $sqlDriver")
commandQueries = CommandDatabase(sqlDriver).commandQueries
}
fun getCommand(name: String): Command? = commandQueries.selectCommandByName(name).executeAsOneOrNull()
fun getCommands(): List<Command> = commandQueries.selectCommands().executeAsList().sortedBy { !it.name.isLetter() }
fun getCommandsByQuery(query: String): List<Command> = commandQueries.selectCommandsByQuery(query, query).executeAsList()
fun getBasics(): List<BasicCategory> = commandQueries.selectBasicCategories().executeAsList()
fun getBasicGroupsByQuery(categoryId: Long): List<BasicGroup> = commandQueries.selectBasicGroupByCategory(categoryId).executeAsList()
fun getBasicCommands(groupId: Long): List<BasicCommand> = commandQueries.selectBasicCommandByGroupId(groupId).executeAsList()
fun getBasicGroupsByQuery(query: String): List<BasicGroup> = commandQueries.selectBasicGroupsByQuery(query).executeAsList()
fun getSections(commandId: Long): List<CommandSection> = commandQueries.selectCommandSectionsByCommandId(commandId).executeAsList()
fun getTips(): List<Tip> = commandQueries.selectTips().executeAsList()
fun getTipSections(): List<TipSection> = commandQueries.selectAllTipSections().executeAsList()
}

View file

@ -0,0 +1,141 @@
CREATE TABLE IF NOT EXISTS BasicGroup (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"position" INTEGER NOT NULL,
"description" TEXT NOT NULL,
"category_id" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS BasicCommand (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"command" TEXT NOT NULL,
"mans" TEXT NOT NULL,
"group_id" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS BasicCategory (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"position" INTEGER NOT NULL,
"title" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Command (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"category" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS CommandSection (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"command_id" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS Tip (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"position" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS TipSection (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"tip_id" INTEGER NOT NULL,
"position" INTEGER NOT NULL,
"type" INTEGER NOT NULL,
"data1" TEXT NOT NULL,
"data2" TEXT NOT NULL,
"extra" TEXT NOT NULL
);
selectBasicCategories:
SELECT *
FROM BasicCategory
ORDER BY position;
selectBasicGroupByCategory:
SELECT *
FROM BasicGroup
WHERE category_id = ?
ORDER BY position DESC;
selectBasicGroupsByQuery:
SELECT *
FROM BasicGroup
WHERE UPPER(description) LIKE '%' || UPPER(?) || '%'
AND category_id != 250
AND category_id != 253
AND category_id != 254
AND category_id != 255
AND category_id != 256
AND category_id != 260
ORDER BY position DESC;
selectBasicCommandByGroupId:
SELECT *
FROM BasicCommand
WHERE group_id = ?;
insertCommand:
INSERT INTO Command (category, name, description)
VALUES (?, ?, ?);
insertBasicGroup:
INSERT INTO BasicGroup (position, description, category_id)
VALUES (?, ?, ?);
insertBasicCommand:
INSERT INTO BasicCommand (command, mans, group_id)
VALUES (?, ?, ?);
selectCommands:
SELECT *
FROM Command
ORDER BY name COLLATE NOCASE ASC;
selectCommandsByQuery:
SELECT *
FROM Command
WHERE UPPER(name) LIKE '%' || UPPER(?) || '%'
OR UPPER(description) LIKE '%' || UPPER(?) || '%'
ORDER BY name COLLATE NOCASE ASC;
selectCommandByName:
SELECT *
FROM Command
WHERE name = ?;
deleteCommandSections:
DELETE FROM CommandSection
WHERE command_id = ? AND title != "TLDR";
insertCommandSection:
INSERT INTO CommandSection (title, content, command_id)
VALUES (?, ?, ?);
selectCommandSectionsByCommandId:
SELECT *
FROM CommandSection
WHERE command_id = ? AND title != "NAME"
ORDER BY id;
updateCommandTLDRSectionByCommandId:
UPDATE CommandSection
SET content = ?
WHERE command_id = ? AND title = "TLDR";
selectTips:
SELECT *
FROM Tip
ORDER BY position;
selectTipSections:
SELECT *
FROM TipSection
WHERE tip_id = ?
ORDER BY position;
selectAllTipSections:
SELECT *
FROM TipSection
ORDER BY position;

View file

@ -0,0 +1,63 @@
import com.linuxcommandlibrary.shared.CommandElement
import com.linuxcommandlibrary.shared.getCommandList
import com.linuxcommandlibrary.shared.getHtmlFileName
import com.linuxcommandlibrary.shared.getSortPriority
import com.linuxcommandlibrary.shared.sortedSearch
import databases.BasicCategory
import databases.Command
import databases.CommandSection
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class CommonTests {
@Test
fun testCommandListElements() {
val command = "ps ax | grep firefox"
val elements = command.getCommandList("ps,grep")
assertTrue(elements.count { it is CommandElement.Man } == 2)
}
@Test
fun testCommandListSearch() {
val commands = listOf(
Command(0, 0, "optipng", "convert"),
Command(0, 0, "thumbnail", "take png and do something"),
Command(0, 0, "Pngcheck", "print detailed"),
Command(0, 0, "png", "png"),
)
val filteredCommands = commands.sortedSearch("png")
assert(filteredCommands.size == 4)
assertEquals(filteredCommands[0].name, "png")
assertEquals(filteredCommands[1].name, "Pngcheck")
assertEquals(filteredCommands[2].name, "optipng")
assertEquals(filteredCommands[3].name, "thumbnail")
}
@Test
fun testBasicCategory() {
val category = BasicCategory(0L, 0L, "Users & Groups 2")
assertEquals(category.getHtmlFileName(), "usersgroups")
}
@Test
fun testSectionSorting() {
val sections = listOf(
CommandSection(0L, "SEE ALSO", "", 0L),
CommandSection(0L, "RANDOM", "", 0L),
CommandSection(0L, "TLDR", "", 0L),
CommandSection(0L, "AUTHOR", "", 0L),
CommandSection(0L, "SYNOPSIS", "", 0L),
).sortedBy { it.getSortPriority() }
assertEquals("TLDR", sections[0].title)
assertEquals("SYNOPSIS", sections[1].title)
assertEquals("RANDOM", sections[2].title)
assertEquals("SEE ALSO", sections[3].title)
assertEquals("AUTHOR", sections[4].title)
}
}

View file

@ -0,0 +1,32 @@
package com.linuxcommandlibrary.shared
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
import com.linuxcommandlibrary.CommandDatabase
import java.io.File
/* 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.
*/
actual var databaseHelper = DatabaseHelper()
fun initDatabase() {
val databaseFile = EmptyClass::class.java.classLoader?.getResource("database.db")?.toURI()
val driver: SqlDriver = JdbcSqliteDriver("jdbc:sqlite::resource:$databaseFile")
if (!File("assets/database.db").exists()) {
CommandDatabase.Schema.create(driver)
}
databaseHelper.setupDriver(driver)
}