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

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
/build
/captures
/native
/android/build
/android/release
/common/build
/desktop/build
/cli/build
.idea/
.gradle/
local.properties
*.key
*.aab
*.jar
/html

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View file

@ -1,2 +1,62 @@
# linux-cli-lib
## Linux Command Library (Mobile+CLI+Web)
![Icon](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandLibrary/master/art/web_hi_res_144.png)
The app currently has **7680** manual pages, **22+** basic categories and a bunch of general terminal tips. It works 100% offline, doesn't need an internet connection and has no tracking software.
[![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)
[![F-Droid](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/fdroid_badge.png)](https://f-droid.org/en/packages/com.inspiredandroid.linuxcommandbibliotheca/)
[![Web](https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/web_badge.png)](https://linuxcommandlibrary.com)
### Mobile screenshots
<p>
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-1.png" width="200">
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-2-dark.png" width="200">
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-3.png" width="200">
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-4-dark.png" width="200">
</p>
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-1-tablet.png" width="400">
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-2-tablet.png" width="400">
### CLI screenshot
<img src="https://raw.githubusercontent.com/SimonSchubert/LinuxCommandBibliotheca/master/art/screen-cli-1.png" width="300">
Execute `gradle :cli:buildJar` to create jar file for Linux, Windows and Mac.
### Content
#### Categories
One-liners, System information, System control, Users & Groups, Files & Folders, Input, Printing, JSON, Network, Search & Find, GIT, SSH, Video & Audio, Package manager, Hacking tools, Terminal games, Crypto currencies, VIM Texteditor, Emacs Texteditor, Nano Texteditor, Pico Texteditor, Micro Texteditor
#### Tips
Clear and reset the terminal, List of recent commands, Close a frozen window/application, Tab Completion, Temporary aliases, Permanent aliases, Chain commands, Command syntax, Cursor navigation, Redirection, Special characters in commands, View file permissions, Modify file permissions, Set file permissions via binary references
### CI/CD
[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.
### Tests
Android Jetpack Compose screen tests: [ComposeTests.kt](android/src/androidTest/java/com/inspiredandroid/linuxcommandbibliotheca/ComposeTests.kt)
Android Jetpack Compose deeplinking tests: [ComposeDeeplinkTests.kt](android/src/androidTest/java/com/inspiredandroid/linuxcommandbibliotheca/ComposeDeeplinkTests.kt)
Common code unit tests: [CommonTests.kt](common/src/commonTest/kotlin/CommonTests.kt)
### Licensing
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.
### Thanks to
http://letsgokoyo.com - App Icon
https://www.commandlinefu.com - Lots of one-liners
https://icons8.com - Icons
https://tldr.sh - TLDR

78
android/build.gradle.kts Normal file
View file

@ -0,0 +1,78 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.compose.compiler)
}
group = "com.inspiredandroid.linuxcommandbibliotheca"
dependencies {
implementation(project(":common"))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material)
implementation(libs.androidx.navigation.compose)
implementation(libs.accompanist.appcompat.theme)
implementation(libs.accompanist.systemuicontroller)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.preference)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material.icons.core)
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
implementation(libs.androidx.foundation)
implementation(libs.kotlinx.collections.immutable)
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.test.manifest)
debugImplementation(libs.androidx.ui.tooling)
}
android {
compileSdk = 36
defaultConfig {
applicationId = "com.inspiredandroid.linuxcommandbibliotheca"
minSdk = 24
targetSdk = 36
versionCode =
libs.versions.androidVersionCode
.get()
.toInt()
versionName = libs.versions.appVersion.get()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
getByName("debug") {
isMinifyEnabled = false
}
}
sourceSets["main"].assets.setSrcDirs(listOf("../assets"))
buildFeatures {
compose = true
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
lint {
abortOnError = false
}
namespace = "com.inspiredandroid.linuxcommandbibliotheca"
}

View file

@ -0,0 +1,88 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import androidx.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.linuxcommandlibrary.shared.initDatabase
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* Test if deeplinks from/to website open correct screens. If test started the first time
* "Always open urls with app" dialog has to be accepted. Or the app has to be signed with the
* release key for autoVerify to take effect.
*/
@RunWith(AndroidJUnit4::class)
class ComposeDeeplinkTests {
private lateinit var scenario: ActivityScenario<MainActivity>
@get:Rule
val composeTestRule = createEmptyComposeRule()
@Before
fun setUp() {
val context: Context = ApplicationProvider.getApplicationContext()
initDatabase(context)
// Clear bookmarks
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putString("KEY_BOOKMARKS", "").apply()
}
private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
scenario = ActivityScenario.launch(intent)
}
@Test
fun testBasicCategories() {
openUrl("https://linuxcommandlibrary.com/basics")
composeTestRule.onNodeWithContentDescription("TopAppBarTitle")
.assertTextEquals("Basics")
}
@Test
fun testBasicCategory() {
openUrl("https://linuxcommandlibrary.com/basic/usersgroups")
composeTestRule.onNodeWithContentDescription("TopAppBarTitle")
.assertTextEquals("Users & Groups")
}
@Test
fun testTips() {
openUrl("https://linuxcommandlibrary.com/tips")
composeTestRule.onNodeWithContentDescription("TopAppBarTitle")
.assertTextEquals("Tips")
}
@Test
fun testCommandList() {
openUrl("https://linuxcommandlibrary.com/")
composeTestRule.onNodeWithContentDescription("Back").performClick()
composeTestRule.onNodeWithContentDescription("TopAppBarTitle")
.assertTextEquals("Commands")
}
@Test
fun testCommandDetail() {
openUrl("https://linuxcommandlibrary.com/man/2048")
composeTestRule.onNodeWithContentDescription("TopAppBarTitle")
.assertTextEquals("2048")
}
}

View file

@ -0,0 +1,138 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.content.Context
import android.graphics.Bitmap
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.linuxcommandlibrary.shared.copyDatabase
import com.linuxcommandlibrary.shared.initDatabase
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.java.KoinJavaComponent.inject
import java.io.FileOutputStream
/**
* Take screenshots of Phone and Tablet.
* Phone = Pixel 2 1080x1920
* Tablet 7" = Nexus 7 1200x1920
*
* Pull images from device to art folder for readme:
* run pull_screenshots.sh
*/
@RunWith(AndroidJUnit4::class)
class ComposeScreenshots {
private lateinit var scenario: ActivityScenario<MainActivity>
@get:Rule
val composeTestRule = createEmptyComposeRule()
@Before
fun setUp() {
val context: Context = ApplicationProvider.getApplicationContext()
copyDatabase(context)
initDatabase(context)
val dataManager: com.inspiredandroid.linuxcommandbibliotheca.DataManager by inject(com.inspiredandroid.linuxcommandbibliotheca.DataManager::class.java)
dataManager.updateDatabaseVersion()
// Clear bookmarks
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putString("KEY_BOOKMARKS", "").apply()
// Clear files folder
InstrumentationRegistry.getInstrumentation().targetContext.filesDir.listFiles()?.forEach {
it.deleteRecursively()
}
scenario = ActivityScenario.launch(MainActivity::class.java)
}
@Test
fun takeTabletLightAndDarkScreenshots() {
takeTabletScreenshots("")
scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity {
setDefaultNightMode(MODE_NIGHT_YES)
}
takeTabletScreenshots("-dark")
}
private fun takeTabletScreenshots(prefix: String) {
// Tips
composeTestRule.onNodeWithText("Tips").performClick()
composeTestRule.takeScreenshot("screen-1-tablet$prefix.png")
// Basics
composeTestRule.onNodeWithText("Basics").performClick()
composeTestRule.takeScreenshot("screen-2-tablet$prefix.png")
}
@Test
fun takePhoneLightAndDarkScreenshots() {
takePhoneScreenshots("")
scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity {
setDefaultNightMode(MODE_NIGHT_YES)
}
takePhoneScreenshots("-dark")
}
private fun takePhoneScreenshots(prefix: String) {
// Command list
composeTestRule.onNodeWithText("Commands").performClick()
composeTestRule.onNodeWithContentDescription("Search").performClick()
composeTestRule.onNodeWithContentDescription("SearchField")
.performTextInput("mk")
composeTestRule.takeScreenshot("screen-4$prefix.png")
// Command detail
composeTestRule.onNodeWithText("mkdir").performClick()
composeTestRule.onNodeWithText("SYNOPSIS").performClick()
composeTestRule.onNodeWithText("DESCRIPTION").performClick()
composeTestRule.takeScreenshot("screen-1$prefix.png")
// Basics
composeTestRule.onNodeWithText("Basics").performClick()
composeTestRule.onNodeWithText("System information").performClick()
composeTestRule.takeScreenshot("screen-3$prefix.png")
// Tips
composeTestRule.onNodeWithText("Tips").performClick()
composeTestRule.onNodeWithContentDescription("Scroll")
.performScrollToNode(hasText("$ [command] --help"))
composeTestRule.takeScreenshot("screen-2$prefix.png")
}
private fun ComposeTestRule.takeScreenshot(file: String) {
// TODO: Find better way to wait for animations to finish
runBlocking {
delay(1000L)
}
onRoot()
.captureToImage()
.asAndroidBitmap()
.save(file)
}
private fun Bitmap.save(file: String) {
val path = InstrumentationRegistry.getInstrumentation().targetContext.filesDir.canonicalPath
FileOutputStream("$path/$file").use { out ->
compress(Bitmap.CompressFormat.PNG, 100, out)
}
}
}

View file

@ -0,0 +1,124 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.content.Context
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.preference.PreferenceManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.linuxcommandlibrary.shared.copyDatabase
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.initDatabase
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.java.KoinJavaComponent.inject
/**
* Test for navigation, search and booksmarks. More tests to come
*/
@RunWith(AndroidJUnit4::class)
class ComposeTests {
@get:Rule
val composeTestRule = createComposeRule()
@Before
fun setUp() {
val context: Context = ApplicationProvider.getApplicationContext()
copyDatabase(context)
initDatabase(context)
val dataManager: com.inspiredandroid.linuxcommandbibliotheca.DataManager by inject(com.inspiredandroid.linuxcommandbibliotheca.DataManager::class.java)
dataManager.updateDatabaseVersion()
// Clear bookmarks
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit().putString("KEY_BOOKMARKS", "").apply()
composeTestRule.setContent { LinuxApp() }
}
/**
* Click though BottomNavigationBar and assert that TopAppBar titles are correct
*/
@Test
fun testBottomNavigation() {
composeTestRule.onNodeWithText("Tips").performClick()
composeTestRule.onNodeWithContentDescription("TopAppBarTitle").assertTextEquals("Tips")
composeTestRule.onNodeWithText("Commands").performClick()
composeTestRule.onNodeWithContentDescription("Back").performClick()
composeTestRule.onNodeWithContentDescription("TopAppBarTitle").assertTextEquals("Commands")
composeTestRule.onNodeWithText("Basics").performClick()
composeTestRule.onNodeWithContentDescription("TopAppBarTitle").assertTextEquals("Basics")
}
/**
* Test if info is shown when search for a command that doesn't exist and if command description
* is shown when search for an existing command
*/
@Test
fun testSearch() {
composeTestRule.onNodeWithText("Commands").performClick()
composeTestRule.onNodeWithContentDescription("Search").performClick()
// Search for a command that doesn't exist
composeTestRule.onNodeWithContentDescription("SearchField")
.performTextInput("CommandThatDoesn'tExist")
composeTestRule.onNodeWithText("404 command not found").assertIsDisplayed()
composeTestRule.onNodeWithContentDescription("SearchField").performTextClearance()
// Search for an existing command
val firstCommand = databaseHelper.getCommands().last()
composeTestRule.onNodeWithContentDescription("SearchField")
.performTextInput(firstCommand.name)
composeTestRule.onNodeWithText(firstCommand.description).assertIsDisplayed()
}
/**
* Test if bookmarks in command detail and command list are shown correctly
*/
@Test
fun testBookmarks() {
val firstCommand = databaseHelper.getCommands().first()
composeTestRule.onNodeWithText("Commands").performClick()
composeTestRule.onNodeWithContentDescription("Search").performClick()
// Search for first command and go to command detail screen
composeTestRule.onNodeWithContentDescription("SearchField")
.performTextInput(firstCommand.name)
composeTestRule.onNodeWithText(firstCommand.description).performClick()
// Click bookmark icon and check if icon/contentDescription changed
composeTestRule.onNodeWithContentDescription("Add bookmark").performClick()
composeTestRule.mainClock.advanceTimeBy(1000)
composeTestRule.waitForIdle()
composeTestRule.onNodeWithContentDescription("Remove bookmark").assertIsDisplayed()
// Go back to search/list and check if bookmark icon is visible
composeTestRule.onNodeWithContentDescription("Back").performClick()
composeTestRule.onNodeWithContentDescription("Bookmarked").assertIsDisplayed()
}
@Test
fun testBasicsScreen() {
val firstBasicCategory = databaseHelper.getBasics().first()
// Click on first category
composeTestRule.onNodeWithText(firstBasicCategory.title).performClick()
composeTestRule.onNodeWithContentDescription("TopAppBarTitle")
.assertTextEquals(firstBasicCategory.title)
val basicGroup = databaseHelper.getBasicGroupsByQuery(firstBasicCategory.id).first()
// Click on first group
composeTestRule.onNodeWithText(basicGroup.description).performClick()
// Check if commands of group expanded and therefore share icon(s) are visible
composeTestRule.onAllNodesWithContentDescription("Share").assertAny(isEnabled())
}
}

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="preferExternal">
<application
android:name=".LinuxApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="linuxcommandlibrary.com"
android:path="/tips.html"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:path="/tips"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:path="/basics.html"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:path="/basics"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:path="/"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:path="/index.html"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:pathPrefix="/man/"
android:scheme="https" />
<data
android:host="linuxcommandlibrary.com"
android:pathPrefix="/basic/"
android:scheme="https" />
</intent-filter>
</activity>
<activity
android:name=".InitializeDatabaseActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,57 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceManager
class DataManager(context: Context) {
val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val bookmarksIds = getBookmarkIds()
private fun getBookmarkIds(): MutableList<Long> {
val bookmarksChain = prefs.getString(KEY_BOOKMARKS, "") ?: ""
return bookmarksChain.split(",").mapNotNull { it.trim().toLongOrNull() }.toMutableList()
}
private fun saveBookmarkIds() {
val bookmarksChain = bookmarksIds.joinToString(separator = ",")
prefs.edit { putString(KEY_BOOKMARKS, bookmarksChain) }
}
fun addBookmark(id: Long) {
bookmarksIds.add(id)
saveBookmarkIds()
}
fun removeBookmark(id: Long) {
bookmarksIds.remove(id)
saveBookmarkIds()
}
fun hasBookmark(id: Long): Boolean = bookmarksIds.contains(id)
fun isDatabaseUpToDate(): Boolean {
val databaseVersion = prefs.getInt(KEY_DATABASE_VERSION, 0)
return databaseVersion == CURRENT_DATABASE_VERSION
}
fun updateDatabaseVersion() {
prefs.edit { putInt(KEY_DATABASE_VERSION, CURRENT_DATABASE_VERSION) }
}
fun setAutoExpandSections(autoExpand: Boolean) {
prefs.edit { putBoolean(KEY_AUTO_EXPAND_SECTIONS, autoExpand) }
}
fun isAutoExpandSections(): Boolean = prefs.getBoolean(KEY_AUTO_EXPAND_SECTIONS, false)
companion object {
const val KEY_BOOKMARKS = "KEY_BOOKMARKS"
const val KEY_DATABASE_VERSION = "DATABASE_VERSION"
const val KEY_AUTO_EXPAND_SECTIONS = "auto_expand_sections"
const val CURRENT_DATABASE_VERSION = 32
}
}

View file

@ -0,0 +1,30 @@
package com.inspiredandroid.linuxcommandbibliotheca
import androidx.navigation.NavBackStackEntry
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.getHtmlFileName
fun NavBackStackEntry.getCommandId(): Long? {
var commandId =
arguments?.getString("commandId")?.toLongOrNull()
if (commandId == null) {
// get id by command name when opened via deeplink
val commandName = arguments?.getString("commandName") ?: ""
val command = databaseHelper.getCommand(commandName)
commandId = command?.id
}
return commandId
}
fun NavBackStackEntry.getCategoryId(): Long? {
var categoryId =
arguments?.getString("categoryId")?.toLongOrNull()
if (categoryId == null) {
// get id by category name when opened via deeplink
val categoryName =
arguments?.getString("deepLinkCategoryName") ?: ""
val categories = databaseHelper.getBasics()
categoryId = categories.firstOrNull { it.getHtmlFileName() == categoryName }?.id
}
return categoryId
}

View file

@ -0,0 +1,264 @@
package com.inspiredandroid.linuxcommandbibliotheca
import databases.BasicCategory
import databases.BasicGroup
fun BasicGroup.getIconResource(): Int = when (id.toInt()) {
1205 -> R.drawable.ic_info_40dp
1206 -> R.drawable.ic_icons8_restore_window
1207 -> R.drawable.ic_icons8_compass
1208 -> R.drawable.ic_icons8_console
1212 -> R.drawable.ic_icons8_hand_with_pen
1209 -> R.drawable.ic_info_40dp
1211 -> R.drawable.ic_icons8_keyboard
1210 -> R.drawable.ic_icons8_compass
1214 -> R.drawable.ic_info_40dp
1215 -> R.drawable.ic_icons8_keyboard
1213 -> R.drawable.ic_icons8_compass
1199 -> R.drawable.ic_file
1200 -> R.drawable.ic_icons8_compass
1201 -> R.drawable.ic_icons8_copy
1202 -> R.drawable.ic_icons8_hand_with_pen
1203 -> R.drawable.ic_search_40dp
1204 -> R.drawable.ic_icons8_restore_window
1165 -> R.drawable.ic_icons8_copy
1166 -> R.drawable.ic_icons8_new
1167 -> R.drawable.ic_icons8_save
1168 -> R.drawable.ic_info_40dp
1169 -> R.drawable.ic_icons8_plus
1170 -> R.drawable.ic_icons8_numbered_list
1171 -> R.drawable.ic_delete_black_24dp
1172 -> R.drawable.ic_icons8_merge
1173 -> R.drawable.ic_icons8_arrow
1174 -> R.drawable.ic_icons8_arrow
1175 -> R.drawable.ic_icons8_plus
1176 -> R.drawable.ic_delete_black_24dp
1177 -> R.drawable.ic_icons8_save
1178 -> R.drawable.ic_available_updates
1179 -> R.drawable.ic_icons8_plus
1180 -> R.drawable.ic_icons8_user_male_circle
1181 -> R.drawable.ic_icons8_user_male_circle
1182 -> R.drawable.ic_info_40dp
1183 -> R.drawable.ic_icons8_undo
1184 -> R.drawable.ic_icons8_hide
1185 -> R.drawable.ic_icons8_save
1186 -> R.drawable.ic_icons8_save
1187 -> R.drawable.ic_icons8_visible
1188 -> R.drawable.ic_delete_black_24dp
1189 -> R.drawable.ic_delete_black_24dp
1190 -> R.drawable.ic_delete_black_24dp
1191 -> R.drawable.ic_delete_black_24dp
1192 -> R.drawable.ic_delete_black_24dp
125 -> R.drawable.ic_file_download_black_24dp
126 -> R.drawable.ic_file
127 -> R.drawable.ic_delete_black_24dp
128 -> R.drawable.ic_search_40dp
129 -> R.drawable.ic_info_40dp
130 -> R.drawable.ic_icons8_synchronize
131 -> R.drawable.ic_arrow_upward_black_24dp
132 -> R.drawable.ic_add_rule
7, 5, 1236, 1248, 1235, 1247, 1246, 1238, 8, 6, 10, 1241, 1245, 9, 1244, 1243, 1, 4, 1242, 2, 3, 0, 1237, 2630 -> R.drawable.ic_icon_controller
1163 -> R.drawable.ic_vpn_key_black_24dp
1164, 1162, 1161, 1160, 1159 -> R.drawable.ic_icons8_connected
1231, 1232 -> R.drawable.ic_file
1158 -> R.drawable.ic_icons8_circled_pause
26, 91, 92, 93, 94, 95 -> R.drawable.ic_search_40dp
96, 97, 98, 100, 2399, 99 -> R.drawable.ic_file
41 -> R.drawable.ic_electronics
42 -> R.drawable.ic_battery_90_black_24dp
43, 44 -> R.drawable.ic_bluetooth_black_24dp
45 -> R.drawable.ic_icons8_network_card
46 -> R.drawable.ic_memory_slot
56, 65 -> R.drawable.ic_icons8_tv_off
57 -> R.drawable.ic_icons8_tv_on
47 -> R.drawable.ic_icons8_linux
48 -> R.drawable.ic_icons8_root_server
49 -> R.drawable.ic_usb_black_48dp
50 -> R.drawable.ic_icons8_flow_chart
51 -> R.drawable.ic_ip_address
58 -> R.drawable.ic_refresh_black_24dp
59 -> R.drawable.ic_power_settings_new_black_24dp
55 -> R.drawable.ic_icons8_calendar_1
52, 60 -> R.drawable.ic_timer_black_24dp
53 -> R.drawable.ic_icons8_hdd
61 -> R.drawable.ic_stop_bluetooth
62 -> R.drawable.ic_bluetooth_start
63 -> R.drawable.ic_stop_wifi
64 -> R.drawable.ic_wifi_start
28 -> R.drawable.ic_icons8_work
29 -> R.drawable.ic_icons8_undo
31 -> R.drawable.ic_icons8_kitchen_scales
81 -> R.drawable.ic_file_download_white
191 -> R.drawable.ic_vip_lookup_white_48dp
83 -> R.drawable.ic_icons8_ping_pong
189 -> R.drawable.ic_settings_black_24dp
27, 32 -> R.drawable.ic_icons8_show_property
16 -> R.drawable.ic_file_move_white
15 -> R.drawable.ic_file_copy_white_48dp
20 -> R.drawable.ic_change_folder_white
13 -> R.drawable.ic_file_content_white
19 -> R.drawable.ic_folder_list_white
18 -> R.drawable.ic_delete_folder_white_48dp
17 -> R.drawable.ic_create_new_folder_white
12 -> R.drawable.ic_delete_file_white
11 -> R.drawable.ic_create_file_white
14 -> R.drawable.ic_file_edit_white_48dp
21 -> R.drawable.ic_icons8_home
22 -> R.drawable.ic_icons8_mother
23 -> R.drawable.ic_icons8_downloads_folder
25 -> R.drawable.ic_file_link_white_48dp
30 -> R.drawable.ic_icons8_exe
102 -> R.drawable.ic_remove_user_group
101 -> R.drawable.ic_icons8_add_user_group
103 -> R.drawable.ic_edit_group
107 -> R.drawable.ic_user_password
104 -> R.drawable.ic_icons8_add_user
105 -> R.drawable.ic_icons8_remove_user_male
108 -> R.drawable.ic_icons8_moderator_male
106 -> R.drawable.ic_icons8_edit_user
110 -> R.drawable.ic_add_user_to_group_white_48dp
111 -> R.drawable.ic_add_user_to_group_white_48dp
112 -> R.drawable.ic_remove_user_from_group_white_48dp
114 -> R.drawable.ic_list_user_white_48dp
33 -> R.drawable.ic_icons8_reuse
34 -> R.drawable.ic_icons8_delete_trash
35 -> R.drawable.ic_icons8_add_trash
36 -> R.drawable.ic_icons8_file_preview
37 -> R.drawable.ic_file_permission_white_48dp
38 -> R.drawable.ic_icons8_user_folder
24 -> R.drawable.ic_folder_path_white
39 -> R.drawable.ic_icons8_user_male_circle
40 -> R.drawable.ic_icons8_group_foreground_selected
113 -> R.drawable.ic_list_groups_white_48dp
109 -> R.drawable.ic_info_40dp
1193 -> R.drawable.ic_icons8_print_file
1194 -> R.drawable.ic_icons8_visible
1195 -> R.drawable.ic_icons8_cancel
1196 -> R.drawable.ic_info_40dp
1197 -> R.drawable.ic_icons8_circled_play
1198 -> R.drawable.ic_icons8_circled_pause
1230 -> R.drawable.ic_icons8_plus
1229 -> R.drawable.ic_delete_black_24dp
1228 -> R.drawable.ic_icons8_plus
1227 -> R.drawable.ic_icons8_plus
1226 -> R.drawable.ic_icons8_print
80 -> R.drawable.ic_list_interfaces_white_48dp
82 -> R.drawable.ic_vip_lookup_white_48dp
84 -> R.drawable.ic_settings_black_24dp
85 -> R.drawable.ic_icons8_visible
86 -> R.drawable.ic_fingerprint_black_24dp
88 -> R.drawable.ic_dns_black_24dp
89 -> R.drawable.ic_ip_address
90 -> R.drawable.ic_list_sockets_white_48dp
143 -> R.drawable.ic_search_executeable_man_white_48dp
87 -> R.drawable.ic_search_source_man_white_48dp
320 -> R.drawable.ic_icons8_show_property
138 -> R.drawable.ic_fingerprint_black_24dp
116 -> R.drawable.ic_vpn_key_black_24dp
221 -> R.drawable.ic_list_sockets_white_48dp
137 -> R.drawable.ic_file_edit_white_48dp
71 -> R.drawable.ic_volume_off_black_24dp
67 -> R.drawable.ic_webcam_white_48dp
66 -> R.drawable.ic_desktop_windows_black_24dp
68 -> R.drawable.ic_icons8_talk
69, 70 -> R.drawable.ic_volume_up_black_24dp
72, 73, 75, 76, 77, 78, 1239, 1240, 74 -> R.drawable.ic_switch_video_white_48dp
430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440 -> R.drawable.ic_videogame_asset_black_24dp
443 -> R.drawable.ic_vpn_key_black_24dp
450 -> R.drawable.ic_wifi_black_24dp
446, 1495, 457, 1516, 1509 -> R.drawable.ic_info_black_24dp
451 -> R.drawable.ic_report_black_24dp
445 -> R.drawable.ic_loupe_black_24dp
444 -> R.drawable.ic_fingerprint_black_24dp
448 -> R.drawable.ic_public_black_24dp
447 -> R.drawable.ic_healing_black_24dp
441 -> R.drawable.ic_flash_on_black_24dp
449 -> R.drawable.ic_storage_black_24dp
454 -> R.drawable.ic_file
456 -> R.drawable.ic_search_black_24dp
460 -> R.drawable.ic_add_rule
453 -> R.drawable.ic_file_download_black_24dp
458 -> R.drawable.ic_available_updates
459 -> R.drawable.ic_arrow_upward_black_24dp
455, 1519, 1511, 1528, 1529, 1530, 1531, 1532 -> R.drawable.ic_delete_black_24dp
461 -> R.drawable.ic_icons8_show_property
1492 -> R.drawable.ic_icons8_home
1493 -> R.drawable.ic_icons8_mother
1496 -> R.drawable.ic_icons8_work
1497 -> R.drawable.ic_icons8_undo
1494 -> R.drawable.ic_icons8_calendar_1
1503 -> R.drawable.ic_vpn_key_black_24dp
1487, 1488, 1489, 1490, 1491 -> R.drawable.ic_file
1498 -> R.drawable.ic_icons8_cancel
1499, 1500, 1501, 1502, 1504 -> R.drawable.ic_icons8_connected
1505 -> R.drawable.ic_icons8_copy
1515 -> R.drawable.ic_icons8_new
1506, 1525, 1512 -> R.drawable.ic_icons8_save
1517, 1510, 1523 -> R.drawable.ic_icons8_plus
1518 -> R.drawable.ic_icons8_numbered_list
1522 -> R.drawable.ic_icons8_merge
1520, 1521 -> R.drawable.ic_icons8_arrow
1524 -> R.drawable.ic_icons8_synchronize
1507, 1508 -> R.drawable.ic_icons8_user_male_circle
1513 -> R.drawable.ic_icons8_redo
1514 -> R.drawable.ic_icons8_hide
1527 -> R.drawable.ic_icons8_visible
178 -> R.drawable.ic_icons8_show_property
154 -> R.drawable.ic_icons8_exe
141 -> R.drawable.ic_icons8_reuse
144 -> R.drawable.ic_icons8_delete_trash
411 -> R.drawable.ic_icons8_add_trash
247 -> R.drawable.ic_icons8_visible
169 -> R.drawable.ic_icons8_cancel
3731 -> R.drawable.ic_info_black_24dp
186 -> R.drawable.ic_icons8_circled_play
187 -> R.drawable.ic_icons8_circled_pause
226 -> R.drawable.ic_icons8_file_preview
231 -> R.drawable.ic_file_permission_white_48dp
1217 -> R.drawable.ic_icons8_coin_wallet
1218 -> R.drawable.ic_icons8_bot
1216 -> R.drawable.ic_icons8_golden_fever
1233 -> R.drawable.ic_icons8_teacher_hiring
117 -> R.drawable.ic_fingerprint_black_24dp
115 -> R.drawable.ic_flash_on_black_24dp
118 -> R.drawable.ic_loupe_black_24dp
119 -> R.drawable.ic_info_40dp
120 -> R.drawable.ic_healing_black_24dp
121 -> R.drawable.ic_public_black_24dp
122 -> R.drawable.ic_storage_black_24dp
123 -> R.drawable.ic_wifi_black_24dp
124 -> R.drawable.ic_report_black_24dp
79 -> R.drawable.ic_vpn_key_black_24dp
54 -> R.drawable.ic_icons8_document
1222, 1221, 1223 -> R.drawable.ic_icon_mouse
1220 -> R.drawable.ic_icons8_clipboard
1219 -> R.drawable.ic_icons8_treatment
1225, 1224 -> R.drawable.ic_icons8_keyboard
2403 -> R.drawable.ic_icon_mouse
2400 -> R.drawable.ic_icons8_keyboard
2402 -> R.drawable.ic_file
else -> R.drawable.ic_icons8_console
}
fun BasicCategory.getIconResource(): Int = when (title) {
"One-liners" -> R.drawable.ic_icons8_hand_with_pen
"System information" -> R.drawable.ic_icon_system_task
"System control" -> R.drawable.ic_settings_black_40dp
"Users & Groups" -> R.drawable.ic_icon_user
"Files & Folders" -> R.drawable.ic_file
"Printing" -> R.drawable.ic_icons8_print
"Network" -> R.drawable.ic_network_card_40dp
"Search & Find" -> R.drawable.ic_search_40dp
"GIT" -> R.drawable.ic_icon_git
"SSH" -> R.drawable.ic_icons8_console
"Video & Audio" -> R.drawable.ic_video_trimming_40dp
"Package manager" -> R.drawable.ic_package_40
"Hacking tools" -> R.drawable.ic_icon_skull
"Terminal games" -> R.drawable.ic_icon_controller
"VIM Texteditor", "Emacs Texteditor", "Nano Texteditor", "Pico Texteditor", "Micro Texteditor" -> R.drawable.ic_icons8_text
"Crypto currencies" -> R.drawable.ic_icon_bitcoin
"Input" -> R.drawable.ic_icon_mouse
"JSON" -> R.drawable.ic_icon_json
"Fun" -> R.drawable.ic_icon_fun
else -> R.drawable.ic_icon_mouse
}

View file

@ -0,0 +1,39 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.InitializeDatabaseScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
/* 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.
*/
class InitializeDatabaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LinuxTheme {
InitializeDatabaseScreen {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
}
}
}
}

View file

@ -0,0 +1,38 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.app.Application
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basiccategories.BasicCategoriesViewModel
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basicgroups.BasicGroupsViewModel
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commanddetail.CommandDetailViewModel
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commandlist.CommandListViewModel
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.search.SearchViewModel
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.tips.TipsViewModel
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.GlobalContext.startKoin
import org.koin.core.module.dsl.*
import org.koin.dsl.module
class LinuxApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@LinuxApplication)
modules(appModule)
}
}
private val appModule = module {
single { DataManager(androidContext()) }
viewModel { BasicGroupsViewModel(get()) }
viewModel { BasicCategoriesViewModel() }
viewModel { (commandId: Long) -> CommandDetailViewModel(commandId, get()) }
viewModel { TipsViewModel() }
viewModel { CommandListViewModel(get()) }
viewModel { SearchViewModel() }
}
}

View file

@ -0,0 +1,251 @@
package com.inspiredandroid.linuxcommandbibliotheca
import android.content.Intent
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.BottomBar
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.TopBar
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basiccategories.BasicCategoriesScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basicgroups.BasicGroupsScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commanddetail.CommandDetailScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commandlist.CommandListScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.search.SearchScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.tips.TipsScreen
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LocalCustomColors
import com.linuxcommandlibrary.shared.hasDatabase
import com.linuxcommandlibrary.shared.initDatabase
import org.koin.android.ext.android.inject
/* 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.
*/
class MainActivity : AppCompatActivity() {
private val dataManager by inject<DataManager>()
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT), navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT))
super.onCreate(savedInstanceState)
if (!hasDatabase(this) || !dataManager.isDatabaseUpToDate()) {
startActivity(Intent(this, InitializeDatabaseActivity::class.java))
dataManager.updateDatabaseVersion()
finish()
return
}
initDatabase(this)
setContent {
LinuxTheme {
Box(
Modifier
.background(MaterialTheme.colors.primary)
.statusBarsPadding(),
) {
Box(
Modifier
.background(LocalCustomColors.current.navBarBackground)
.systemBarsPadding(),
) {
LinuxApp()
}
}
}
}
}
}
const val DEEPLINK_URI = "https://linuxcommandlibrary.com"
@Composable
fun LinuxApp() {
val navController = rememberNavController()
val navBackStackEntry = navController.currentBackStackEntryAsState()
val searchTextValue = remember {
mutableStateOf(
TextFieldValue(text = "", selection = TextRange(0)),
)
}
val showSearch = remember { mutableStateOf(false) }
val onNavigate: (String) -> Unit = remember(navController) { { route -> navController.navigate(route) } }
Scaffold(
topBar = {
TopBar(
navBackStackEntry = navBackStackEntry,
textFieldValue = searchTextValue,
onNavigateBack = {
navController.popBackStack()
},
isSearchVisible = showSearch,
)
},
bottomBar = {
BottomBar(
navController = navController,
resetSearch = {
searchTextValue.value = TextFieldValue(text = "", selection = TextRange(0))
showSearch.value = false
},
)
},
) { innerPadding ->
Box(
modifier = Modifier.padding(innerPadding),
) {
NavHost(
navController = navController,
startDestination = Screen.Basics.route,
) {
composable(
Screen.Basics.route,
deepLinks = listOf(
navDeepLink { uriPattern = "$DEEPLINK_URI/basics" },
navDeepLink { uriPattern = "$DEEPLINK_URI/basics.html" },
),
) {
BasicCategoriesScreen(
onNavigate = onNavigate,
)
}
composable(
Screen.Commands.route,
deepLinks = listOf(
navDeepLink { uriPattern = "$DEEPLINK_URI/" },
navDeepLink { uriPattern = "$DEEPLINK_URI/index.html" },
),
) {
CommandListScreen(
onNavigate = onNavigate,
)
}
composable(
Screen.Tips.route,
deepLinks = listOf(
navDeepLink { uriPattern = "$DEEPLINK_URI/tips" },
navDeepLink { uriPattern = "$DEEPLINK_URI/tips.html" },
),
) {
TipsScreen(onNavigate)
}
composable(
"basicgroups?categoryId={categoryId}&categoryName={categoryName}",
arguments = listOf(
navArgument("categoryId") { defaultValue = "" },
navArgument("categoryName") {},
),
deepLinks = listOf(
navDeepLink {
uriPattern = "$DEEPLINK_URI/basic/{categoryName}.html"
},
navDeepLink { uriPattern = "$DEEPLINK_URI/basic/{categoryName}" },
),
) { backStackEntry ->
val categoryId = backStackEntry.getCategoryId()
if (categoryId != null) {
BasicGroupsScreen(
categoryId = categoryId,
onNavigate = onNavigate,
)
} else {
// open tips screen on invalid deeplink parameters
TipsScreen(onNavigate)
}
}
composable(
"command?commandId={commandId}&commandName={commandName}",
arguments = listOf(
navArgument("commandId") { defaultValue = "" },
navArgument("commandName") {},
),
deepLinks = listOf(
navDeepLink { uriPattern = "$DEEPLINK_URI/man/{commandName}.html" },
navDeepLink { uriPattern = "$DEEPLINK_URI/man/{commandName}" },
),
) { backStackEntry ->
val commandId = backStackEntry.getCommandId()
if (commandId != null) {
CommandDetailScreen(
commandId = commandId,
onNavigate = onNavigate,
)
} else {
// open tips screen on invalid deeplink parameters
TipsScreen(onNavigate)
}
}
}
val isSearchVisible by remember(navBackStackEntry, searchTextValue) {
derivedStateOf {
searchTextValue.value.text.isNotEmpty() &&
navBackStackEntry.value?.destination?.route?.startsWith("command?") == false
}
}
AnimatedVisibility(
visible = isSearchVisible,
enter = fadeIn(animationSpec = tween(300)),
exit = fadeOut(animationSpec = tween(durationMillis = 300, delayMillis = 300)), // work around for navigation overlaps
) {
SearchScreen(
searchText = searchTextValue.value.text,
onNavigate = remember(navController) { { route -> navController.navigate(route) } },
)
}
}
}
}
sealed class Screen(
val route: String,
@StringRes val resourceId: Int,
@DrawableRes val drawableRes: Int,
) {
data object Commands : Screen("commands", R.string.commands, R.drawable.ic_search_40dp)
data object Basics : Screen("basics", R.string.basics, R.drawable.ic_puzzle)
data object Tips : Screen("tips", R.string.tips, R.drawable.ic_idea)
}

View file

@ -0,0 +1,88 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import androidx.compose.foundation.layout.size
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.inspiredandroid.linuxcommandbibliotheca.Screen
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LocalCustomColors
/* 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.
*/
val bottomBarItems = listOf(
Screen.Basics,
Screen.Tips,
Screen.Commands,
)
@Composable
fun BottomBar(
navController: NavHostController,
resetSearch: () -> Unit,
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigation(
backgroundColor = LocalCustomColors.current.navBarBackground,
elevation = 0.dp,
) {
bottomBarItems.forEach { screen ->
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = screen.drawableRes),
contentDescription = null,
modifier = Modifier.size(24.dp),
)
},
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
selectedContentColor = MaterialTheme.colors.primary,
unselectedContentColor = MaterialTheme.colors.onSurface,
onClick = {
// Pop back stack if current route starts with "command?"
// This specific logic might need adjustment based on detailed navigation graph behavior
// For example, ensure it doesn't pop too much or interfere with expected navigation.
// A more robust way might be to navigate to a specific point before the command details.
while (navController.currentBackStackEntry?.destination?.route?.startsWith("command?") == true) {
navController.popBackStack()
}
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
resetSearch()
},
)
}
}
}

View file

@ -0,0 +1,166 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.R
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
import com.linuxcommandlibrary.shared.CommandElement
import com.linuxcommandlibrary.shared.databaseHelper
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/* 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.
*/
@Composable
fun CommandView(
command: String,
elements: ImmutableList<CommandElement>,
onNavigate: (String) -> Unit = {},
verticalPadding: Dp = 6.dp,
) {
val codeColor = MaterialTheme.colors.primary
val baseAnnotatedString = remember(elements, codeColor) {
buildAnnotatedString {
elements.forEach { element ->
when (element) {
is CommandElement.Text -> {
append(element.text)
}
is CommandElement.Man -> {
val start = this.length
withStyle(style = SpanStyle(color = codeColor)) {
append(element.man)
}
val end = this.length
addLink(
LinkAnnotation.Clickable(
tag = "man:${element.man}",
linkInteractionListener = {
val manCommand = databaseHelper.getCommand(element.man)
if (manCommand != null) {
onNavigate("command?commandId=${manCommand.id}&commandName=${manCommand.name}")
}
},
),
start,
end,
)
}
is CommandElement.Url -> {
val start = this.length
withStyle(style = SpanStyle(color = codeColor)) {
append(element.command)
}
val end = this.length
addLink(
LinkAnnotation.Url(element.url),
start,
end,
)
}
}
}
}
}
val finalAnnotatedString = remember(baseAnnotatedString) {
if (baseAnnotatedString.text.isEmpty()) {
baseAnnotatedString
} else {
buildAnnotatedString {
append(baseAnnotatedString)
addStyle(
style = ParagraphStyle(), // Default ParagraphStyle
start = 0,
end = baseAnnotatedString.text.length,
)
}
}
}
Row(modifier = Modifier.padding(start = 12.dp, end = 4.dp).padding(vertical = verticalPadding)) {
Text(
text = finalAnnotatedString,
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
style = MaterialTheme.typography.subtitle2,
)
val context = LocalContext.current
val shareAction = remember(context, command) { { shareCommand(context, command) } }
IconButton(
modifier = Modifier.align(Alignment.CenterVertically),
onClick = shareAction,
) {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.share),
)
}
}
}
fun shareCommand(context: Context, command: String) {
val intent = Intent(Intent.ACTION_SEND)
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TEXT,
command.dropWhile { it == '$' || it.isWhitespace() }.replace("\\n", ""),
)
try {
context.startActivity(Intent.createChooser(intent, "Share command"))
} catch (e: Exception) {
e.printStackTrace()
}
}
@Composable
@Preview
fun CommandViewPreview() {
LinuxTheme {
CommandView(
command = "$ find ex?mple.txt",
elements = listOf(
CommandElement.Text("$ "),
CommandElement.Man("find"),
CommandElement.Text(" ex?mple.txt"),
).toImmutableList(),
)
}
}

View file

@ -0,0 +1,80 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
/* 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.
*/
@Composable
fun HighlightedText(
text: String,
pattern: String,
maxLines: Int = 1,
) {
if (pattern.isEmpty()) {
Text(
text = text,
maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
)
} else if (text.equals(pattern, ignoreCase = true)) {
Text(
text = text,
maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colors.primary,
)
} else {
val highlightColor = MaterialTheme.colors.primary
val annotatedString = remember(text, pattern, highlightColor) {
buildAnnotatedString {
val splitText = text.split(pattern, ignoreCase = true)
splitText.forEachIndexed { index, s ->
append(s)
if (index != splitText.size - 1) {
withStyle(style = SpanStyle(color = highlightColor)) {
append(pattern)
}
}
}
}
}
Text(
text = annotatedString,
maxLines = maxLines,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
@Preview
fun HighlightedTextPreview() {
LinuxTheme {
HighlightedText(
text = "pacman",
pattern = "cm",
)
}
}

View file

@ -0,0 +1,70 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
import com.linuxcommandlibrary.shared.CommandElement
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/* 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.
*/
@Composable
fun NestedCommandView(
text: String,
command: String,
commandElements: ImmutableList<CommandElement>,
onNavigate: (String) -> Unit,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text,
fontWeight = FontWeight.Bold,
modifier = Modifier.width(40.dp),
textAlign = TextAlign.Center,
)
CommandView(
command = command,
elements = commandElements,
onNavigate = onNavigate,
)
}
}
@Composable
@Preview
fun NestedCommandViewPreview() {
LinuxTheme {
NestedCommandView(
text = "",
command = "$ find ex?mple.txt",
commandElements = listOf(
CommandElement.Text("$ "),
CommandElement.Man("find"),
CommandElement.Text(" ex?mple.txt"),
).toImmutableList(),
onNavigate = {},
)
}
}

View file

@ -0,0 +1,50 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
/* 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.
*/
@Composable
fun NestedText(textLeft: String, textRight: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
textLeft,
fontWeight = FontWeight.Bold,
modifier = Modifier.width(40.dp),
textAlign = TextAlign.Center,
)
Text(textRight, modifier = Modifier.padding(8.dp))
}
}
@Composable
@Preview
fun NestedTextPreview() {
LinuxTheme {
NestedText(textLeft = ">>", textRight = "redirect")
}
}

View file

@ -0,0 +1,46 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
/* 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.
*/
@Composable
fun SectionTitle(modifier: Modifier = Modifier, title: String) {
Text(
text = title,
fontSize = 20.sp,
style = MaterialTheme.typography.subtitle1,
fontWeight = FontWeight.Bold,
modifier = modifier.padding(bottom = 4.dp),
)
}
@Preview
@Composable
fun SectionTitlePreview() {
LinuxTheme {
SectionTitle(title = "List of recent commands")
}
}

View file

@ -0,0 +1,360 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.composables
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavBackStackEntry
import com.inspiredandroid.linuxcommandbibliotheca.R
import com.inspiredandroid.linuxcommandbibliotheca.getCommandId
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.AppInfoDialog
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.BookmarkFeedbackDialog
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commanddetail.CommandDetailViewModel
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.getHtmlFileName
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
/* 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.
*/
@Composable
fun TopBar(
navBackStackEntry: State<NavBackStackEntry?>,
textFieldValue: MutableState<TextFieldValue>,
onNavigateBack: () -> Unit,
isSearchVisible: MutableState<Boolean>,
) {
val route = navBackStackEntry.value?.destination?.route
if (route == "commands" || route == "basics") {
val hideSearchCallback = remember { { isSearchVisible.value = false } }
val showSearchCallback = remember { { isSearchVisible.value = true } }
SearchTopBar(
title = getTitleByRoute(navBackStackEntry.value),
textFieldValue = textFieldValue,
isSearchVisible = isSearchVisible.value,
hideSearch = hideSearchCallback,
showSearch = showSearchCallback,
)
} else if (route?.startsWith("command?") == true) {
DetailTopBar(
commandId = navBackStackEntry.value?.getCommandId() ?: 0,
title = getTitleByRoute(navBackStackEntry.value),
onNavigateBack = onNavigateBack,
)
} else {
val title = getTitleByRoute(navBackStackEntry.value)
val showBackIcon = route != "tips" // Simplified from original when
val showAppInfoIcon = route == "tips"
GenericTopBar(
title = title,
showBackIcon = showBackIcon,
onNavigateBack = onNavigateBack,
showAppInfoIcon = showAppInfoIcon,
)
}
}
@Composable
private fun GenericTopBar(
title: String,
showBackIcon: Boolean,
onNavigateBack: () -> Unit,
showAppInfoIcon: Boolean,
) {
var showDialog by remember { mutableStateOf(false) }
TopAppBar(
title = {
Text(
title,
modifier = Modifier.semantics { contentDescription = "TopAppBarTitle" },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White,
navigationIcon = if (showBackIcon) {
{
IconButton(onClick = { onNavigateBack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
}
} else {
null
},
actions = {
if (showAppInfoIcon) {
IconButton(onClick = {
showDialog = true
}) {
Icon(
imageVector = Icons.Filled.Info,
contentDescription = stringResource(R.string.info),
)
}
}
},
)
if (showDialog) {
AppInfoDialog(onDismiss = { showDialog = false })
}
}
@Composable
private fun DetailTopBar(
commandId: Long,
viewModel: CommandDetailViewModel = koinViewModel(
key = commandId.toString(),
viewModelStoreOwner = LocalActivity.current as ViewModelStoreOwner,
parameters = { parametersOf(commandId) },
),
title: String,
onNavigateBack: () -> Unit,
) {
val uiState by viewModel.state.collectAsStateWithLifecycle()
val isAllExpanded by remember { derivedStateOf { uiState.isAllExpanded() } }
TopAppBar(
title = {
Text(
title,
modifier = Modifier.semantics { contentDescription = "TopAppBarTitle" },
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
backgroundColor = MaterialTheme.colors.primary,
contentColor = Color.White,
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
},
actions = {
IconButton(onClick = {
viewModel.onToggleAllExpanded()
}) {
if (isAllExpanded) {
Icon(
painterResource(R.drawable.ic_collapse_all),
contentDescription = stringResource(R.string.collapse_all),
)
} else {
Icon(
painterResource(R.drawable.ic_expand_all),
contentDescription = stringResource(R.string.expand_all),
)
}
}
IconButton(onClick = {
if (uiState.isBookmarked) {
viewModel.removeBookmark()
} else {
viewModel.addBookmark()
}
}) {
if (uiState.isBookmarked) {
Icon(
painterResource(R.drawable.ic_bookmark_black_24dp),
contentDescription = stringResource(R.string.remove_bookmark),
)
} else {
Icon(
painterResource(R.drawable.ic_bookmark_border_black_24dp),
contentDescription = stringResource(R.string.add_bookmark),
)
}
}
},
)
if (uiState.showBookmarkDialog) {
BookmarkFeedbackDialog(onDismiss = { viewModel.hideBookmarkDialog() })
}
}
@Composable
private fun SearchTopBar(
title: String,
textFieldValue: MutableState<TextFieldValue>,
isSearchVisible: Boolean,
hideSearch: () -> Unit,
showSearch: () -> Unit,
) {
val focusRequester = remember { FocusRequester() }
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primary)
.heightIn(min = 56.dp)
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isSearchVisible) {
IconButton(onClick = {
hideSearch()
textFieldValue.value = TextFieldValue("")
}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.back),
tint = Color.White,
)
}
}
if (isSearchVisible) {
OutlinedTextField(
value = textFieldValue.value,
onValueChange = { textFieldValue.value = it },
modifier = Modifier
.weight(1f)
.focusRequester(focusRequester)
.semantics { contentDescription = "SearchField" }
.padding(start = 8.dp, end = 8.dp),
placeholder = { Text("Search...", color = Color.White.copy(alpha = 0.7f)) },
textStyle = MaterialTheme.typography.subtitle1.copy(color = Color.White),
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = Color.White,
cursorColor = Color.White,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
backgroundColor = Color.Transparent,
trailingIconColor = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
placeholderColor = LocalContentColor.current.copy(alpha = 0.7f),
),
maxLines = 1,
singleLine = true,
)
if (textFieldValue.value.text.isNotEmpty()) {
IconButton(onClick = {
textFieldValue.value = TextFieldValue("")
}) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(id = R.string.reset),
tint = Color.White,
)
}
}
} else {
Text(
title,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp) // Standard title padding when no nav icon
.semantics { contentDescription = "TopAppBarTitle" },
style = MaterialTheme.typography.h6.copy(color = LocalContentColor.current), // Use h6 for title as per Material
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = Color.White,
)
}
if (!isSearchVisible) {
IconButton(onClick = {
showSearch()
}) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = stringResource(R.string.search),
tint = Color.White,
)
}
}
}
LaunchedEffect(isSearchVisible) {
if (isSearchVisible) {
focusRequester.requestFocus()
}
}
}
@Composable
private fun getTitleByRoute(backStackEntry: NavBackStackEntry?): String {
if (backStackEntry == null) {
return "Linux"
}
return when (val route = backStackEntry.destination.route) {
"commands" -> stringResource(R.string.commands)
"basics" -> stringResource(R.string.basics)
"tips" -> stringResource(R.string.tips)
else -> {
if (route?.startsWith("command?") == true) {
backStackEntry.arguments?.getString("commandName") ?: ""
} else if (route?.startsWith("basicgroups?") == true) {
val deeplinkName = backStackEntry.arguments?.getString("categoryName") ?: ""
remember(deeplinkName) {
val categories = databaseHelper.getBasics()
val category = categories.firstOrNull { it.getHtmlFileName() == deeplinkName }
category?.title ?: "Not found"
}
} else {
""
}
}
}
}

View file

@ -0,0 +1,162 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.inspiredandroid.linuxcommandbibliotheca.R
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
import com.linuxcommandlibrary.shared.*
/* 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.
*/
// Todo: Replace with AlertDialog
@Composable
fun AppInfoDialog(
onDismiss: () -> Unit = {},
) {
val uriHandler = LocalUriHandler.current
Dialog(onDismissRequest = { onDismiss() }) {
Card(
elevation = 8.dp,
shape = RoundedCornerShape(6.dp),
) {
Column(modifier = Modifier.fillMaxSize()) {
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(8.dp),
)
Row {
Button(
modifier = Modifier.padding(start = 6.dp),
content = {
Text(
"Rate the app",
color = Color.White,
)
},
onClick = {
uriHandler.openUri("https://play.google.com/store/apps/details?id=com.inspiredandroid.linuxcommandbibliotheca")
},
)
IconButton(onClick = {
uriHandler.openUri("https://github.com/SimonSchubert/LinuxCommandLibrary")
}) {
Icon(
painterResource(R.drawable.ic_icons8_github),
contentDescription = "View on GitHub", // TODO: Use stringResource
modifier = Modifier.size(40.dp),
)
}
}
Text(
"Version: ${Version.appVersion}",
style = MaterialTheme.typography.caption,
modifier = Modifier.padding(8.dp),
)
Column(
modifier = Modifier
.padding(8.dp)
.weight(1f)
.verticalScroll(rememberScrollState()),
) {
Text("Support this project", style = MaterialTheme.typography.h6)
Text("By using my referral links for these amazing products.")
Spacer(Modifier.height(4.dp))
Image(
modifier = Modifier
.fillMaxWidth()
.clickable {
val link = "https://linuxcommandlibrary.com/proton-free-2025"
uriHandler.openUri(link)
},
painter = painterResource(R.mipmap.proton_free_horizontal),
contentDescription = "Proton services promotion", // TODO: Use stringResource
)
Spacer(Modifier.height(8.dp))
Image(
modifier = Modifier
.fillMaxWidth()
.clickable {
val link = "https://linuxcommandlibrary.com/linode-2025"
uriHandler.openUri(link)
},
painter = painterResource(R.mipmap.linode_horizontal),
contentDescription = "Linode cloud hosting promotion", // TODO: Use stringResource
)
Spacer(Modifier.height(8.dp))
Text("Man pages", style = MaterialTheme.typography.h6)
Text("Licence information about the man page is usually specified in the man detail page under the category Author, Copyright or Licence. If there is no information on the page you can find the information in the man page source file on your linux system. If you have questions or can't find what you need, you can contact me at sschubert89@gmail.com.")
Spacer(Modifier.height(8.dp))
Text("TLDR pages", style = MaterialTheme.typography.h6)
Text(
"The MIT License (MIT) Copyright (c) 2014 the TLDR team and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
)
Spacer(Modifier.height(8.dp))
Text(
"Thanks to commandlinefu.com for the one-liners and icons8.com for the icons",
style = MaterialTheme.typography.h6,
)
}
TextButton(
content = { Text("OK") },
modifier = Modifier
.align(Alignment.End)
.padding(end = 6.dp),
onClick = onDismiss,
)
}
}
}
}
@Preview
@Composable
fun AppInfoDialogPreview() {
LinuxTheme {
AppInfoDialog()
}
}

View file

@ -0,0 +1,50 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.inspiredandroid.linuxcommandbibliotheca.R
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.SectionTitle
import kotlinx.coroutines.delay
@Composable
fun BookmarkFeedbackDialog(onDismiss: () -> Unit) {
LaunchedEffect(Unit) {
delay(600)
onDismiss()
}
Dialog(onDismissRequest = onDismiss) {
Card(
elevation = 8.dp,
shape = RoundedCornerShape(6.dp),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp),
) {
Icon(
painterResource(R.drawable.ic_bookmark_black_24dp),
contentDescription = null, // Decorative, as title says "Bookmarked"
modifier = Modifier.size(48.dp),
)
Spacer(Modifier.height(8.dp))
SectionTitle(title = "Bookmarked")
}
}
}
}

View file

@ -0,0 +1,71 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.R
import com.linuxcommandlibrary.shared.copyDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun InitializeDatabaseScreen(onFinish: () -> Unit = {}) {
var status by remember {
mutableIntStateOf(0)
}
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painterResource(R.mipmap.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier
.size(240.dp)
.align(Alignment.CenterHorizontally),
)
Text(
"Initialize database",
color = MaterialTheme.colors.onSurface,
modifier = Modifier.align(Alignment.CenterHorizontally),
)
LinearProgressIndicator(
progress = status.div(100f),
modifier = Modifier.padding(top = 16.dp),
)
}
LaunchedEffect(Unit) {
coroutineScope.launch(Dispatchers.IO) {
copyDatabase(context) { progress ->
status = progress
}
withContext(Dispatchers.Main) {
onFinish()
}
}
}
}

View file

@ -0,0 +1,69 @@
@file:OptIn(ExperimentalMaterialApi::class)
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basiccategories
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.ListItem
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.getIconResource
import com.linuxcommandlibrary.shared.getHtmlFileName
import org.koin.androidx.compose.koinViewModel
/* 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.
*/
@Composable
fun BasicCategoriesScreen(
viewModel: BasicCategoriesViewModel = koinViewModel(),
onNavigate: (String) -> Unit,
) {
LazyVerticalGrid(
modifier = Modifier.fillMaxSize(),
columns = GridCells.Adaptive(minSize = 300.dp),
) {
items(
items = viewModel.basicCategories,
key = { it.id },
contentType = { "basic_category_item" },
) { basicCategory ->
ListItem(
text = { Text(basicCategory.title) },
icon = {
Icon(
painterResource(basicCategory.getIconResource()),
contentDescription = null,
modifier = Modifier.size(40.dp),
)
},
modifier = Modifier.clickable {
onNavigate(
"basicgroups?categoryId=${basicCategory.id}&categoryName=${basicCategory.getHtmlFileName()}",
)
},
)
}
}
}

View file

@ -0,0 +1,27 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basiccategories
import androidx.lifecycle.ViewModel
import com.linuxcommandlibrary.shared.databaseHelper
import databases.BasicCategory
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/* 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.
*/
class BasicCategoriesViewModel : ViewModel() {
var basicCategories: ImmutableList<BasicCategory> = databaseHelper.getBasics().toImmutableList()
}

View file

@ -0,0 +1,114 @@
@file:OptIn(ExperimentalMaterialApi::class)
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basicgroups
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.ListItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.getIconResource
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.CommandView
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.HighlightedText
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.getCommandList
import databases.BasicGroup
import kotlinx.collections.immutable.toImmutableList
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
/* 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.
*/
@Composable
fun BasicGroupsScreen(
categoryId: Long,
viewModel: BasicGroupsViewModel = koinViewModel<BasicGroupsViewModel>(
parameters = { parametersOf(categoryId) },
),
onNavigate: (String) -> Unit = {},
) {
val uiState by viewModel.uiState.collectAsState()
LazyColumn(Modifier.fillMaxSize()) {
items(
items = uiState.basicGroups,
key = { it.id },
contentType = { "basic_group_item" },
) { basicGroup ->
BasicGroupColumn(
basicGroup = basicGroup,
isExpanded = !uiState.collapsedMap.getOrDefault(basicGroup.id, true),
onToggleCollapse = { viewModel.toggleCollapse(basicGroup.id) },
onNavigate = onNavigate,
)
}
}
}
@Composable
fun BasicGroupColumn(
basicGroup: BasicGroup,
searchText: String = "",
isExpanded: Boolean,
onToggleCollapse: () -> Unit,
onNavigate: (String) -> Unit = {},
) {
ListItem(
text = {
HighlightedText(
text = basicGroup.description,
pattern = searchText,
maxLines = 3,
)
},
icon = {
Icon(
painterResource(basicGroup.getIconResource()),
contentDescription = null,
modifier = Modifier.size(40.dp),
)
},
modifier = Modifier.clickable { onToggleCollapse() },
)
if (isExpanded) {
ExpandedGroupContent(basicGroup = basicGroup, onNavigate = onNavigate)
}
}
@Composable
private fun ExpandedGroupContent(basicGroup: BasicGroup, onNavigate: (String) -> Unit) {
val commands = remember(basicGroup.id) {
databaseHelper.getBasicCommands(basicGroup.id).toImmutableList()
}
commands.forEach { basicCommand ->
CommandView(
command = basicCommand.command,
elements = basicCommand.command.getCommandList(basicCommand.mans).toImmutableList(),
onNavigate = onNavigate,
)
}
}

View file

@ -0,0 +1,12 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basicgroups
import databases.BasicGroup
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
data class BasicGroupsUiState(
val basicGroups: ImmutableList<BasicGroup> = persistentListOf(),
val collapsedMap: ImmutableMap<Long, Boolean> = persistentMapOf(),
)

View file

@ -0,0 +1,45 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basicgroups
import androidx.lifecycle.ViewModel
import com.linuxcommandlibrary.shared.databaseHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
/* 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.
*/
class BasicGroupsViewModel(categoryId: Long) : ViewModel() {
private val _uiState = MutableStateFlow(BasicGroupsUiState())
val uiState = _uiState.asStateFlow()
init {
val groups = databaseHelper.getBasicGroupsByQuery(categoryId).toImmutableList()
_uiState.value = BasicGroupsUiState(basicGroups = groups)
}
fun isGroupCollapsed(id: Long): Boolean = _uiState.value.collapsedMap.getOrDefault(id, true)
fun toggleCollapse(id: Long) {
_uiState.update { currentState ->
val newMap = currentState.collapsedMap.toMutableMap()
newMap[id] = !currentState.collapsedMap.getOrDefault(id, true)
currentState.copy(collapsedMap = newMap.toPersistentMap())
}
}
}

View file

@ -0,0 +1,221 @@
@file:OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class)
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commanddetail
import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Chip
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.text.HtmlCompat
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.CommandView
import com.linuxcommandlibrary.shared.CommandElement
import com.linuxcommandlibrary.shared.databaseHelper
import databases.CommandSection
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
/* 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.
*/
@Composable
fun CommandDetailScreen(
commandId: Long,
viewModel: CommandDetailViewModel = koinViewModel(
key = commandId.toString(),
viewModelStoreOwner = LocalActivity.current as ViewModelStoreOwner,
parameters = { parametersOf(commandId) },
),
onNavigate: (String) -> Unit,
) {
val uiState by viewModel.state.collectAsStateWithLifecycle()
LazyColumn(Modifier.fillMaxSize()) {
items(
items = uiState.sections, // This should ideally be ImmutableList from ViewModel
key = { it.id },
contentType = { "command_section_item" },
) { section ->
CommandSectionColumn(
section = section,
isExpanded = uiState.expandedSectionsMap.getOrDefault(section.id, false),
onToggleExpanded = { id -> viewModel.onToggleExpanded(id) },
onNavigate = onNavigate,
)
}
}
}
@Composable
private fun CommandSectionColumn(
section: CommandSection,
isExpanded: Boolean,
onToggleExpanded: (Long) -> Unit,
onNavigate: (String) -> Unit,
) {
ListItem(
text = {
Text(
section.title.uppercase(),
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
},
modifier = Modifier.clickable {
onToggleExpanded(section.id)
},
)
if (isExpanded) {
when (section.title) {
"TLDR" -> TldrSectionContent(content = section.content, onNavigate = onNavigate)
"SEE ALSO" -> SeeAlsoSectionContent(content = section.content, onNavigate = onNavigate)
else -> DefaultSectionContent(content = section.content)
}
}
}
@Composable
private fun TldrSectionContent(content: String, onNavigate: (String) -> Unit) {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
) {
val tldrParts = content.split("<b>")
tldrParts.forEachIndexed { index, s ->
val split = s.split("</b>")
if (split.size > 1) {
Text(
text = split[0],
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f),
)
val command = "$ " + split[1].replace("<br>", "").replace("`", "")
CommandView(
command = command,
elements = listOf(CommandElement.Text(command)).toImmutableList(),
onNavigate = onNavigate,
verticalPadding = 4.dp,
)
}
if (index != tldrParts.lastIndex && split.size > 1) { // Add spacer only if content was added
Spacer(Modifier.height(6.dp))
}
}
}
}
@Composable
private fun SeeAlsoSectionContent(content: String, onNavigate: (String) -> Unit) {
val commands = getCommands(content)
if (commands.isNotEmpty()) {
FlowRow(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
commands.forEach { name ->
Chip(onClick = {
onNavigate("command?commandName=$name")
}) {
Text(
text = name,
color = MaterialTheme.colors.onSurface,
)
}
}
}
} else {
// fallback to default rendering if no commands were parsed (e.g. plain text)
DefaultSectionContent(content = content, isFallback = true)
}
}
@Composable
private fun DefaultSectionContent(content: String, isFallback: Boolean = false) {
Text(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = if (isFallback) 0.dp else 8.dp, bottom = 8.dp),
text = content.toAnnotatedString(),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.7f),
)
}
private fun getCommands(input: String): ImmutableList<String> {
val commands = input.split(",").map { it.trim() }
return commands
.map { command ->
command.replace(Regex("\\(\\d+\\)$"), "").trim()
}.filter {
databaseHelper.getCommand(it) != null
}.toImmutableList()
}
private fun String.toAnnotatedString(): AnnotatedString {
val spanned = HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_LEGACY)
val trimmedText = spanned.toString().trim('\n', ' ')
return buildAnnotatedString {
append(trimmedText)
spanned.getSpans(0, trimmedText.length, Any::class.java).forEach { span ->
val start = spanned.getSpanStart(span)
val end = spanned.getSpanEnd(span)
when (span) {
is StyleSpan -> when (span.style) {
android.graphics.Typeface.BOLD -> {
addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
}
android.graphics.Typeface.ITALIC -> {
addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
}
}
is UnderlineSpan -> {
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
}
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commanddetail
import databases.CommandSection
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
data class CommandDetailUiState(
val sections: ImmutableList<CommandSection>,
val expandedSectionsMap: ImmutableMap<Long, Boolean>,
val isBookmarked: Boolean = false,
val showBookmarkDialog: Boolean = false,
) {
fun isAllExpanded(): Boolean = expandedSectionsMap.all { it.value }
}

View file

@ -0,0 +1,90 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commanddetail
import androidx.lifecycle.ViewModel
import com.inspiredandroid.linuxcommandbibliotheca.DataManager
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.getSortPriority
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
/* 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.
*/
class CommandDetailViewModel(
private val commandId: Long,
private val dataManager: DataManager,
) : ViewModel() {
val state: MutableStateFlow<CommandDetailUiState>
init {
val sectionsData = databaseHelper.getSections(commandId).sortedBy { it.getSortPriority() }
val isAutoExpandEnabled = dataManager.isAutoExpandSections()
state = MutableStateFlow(
CommandDetailUiState(
sections = sectionsData.toImmutableList(),
expandedSectionsMap = sectionsData.associate {
it.id to isAutoExpandEnabled
}.toImmutableMap(),
isBookmarked = dataManager.hasBookmark(commandId),
),
)
}
fun onToggleAllExpanded() {
val isAllExpanded = state.value.isAllExpanded()
state.update {
val currentMap = it.expandedSectionsMap
val updatedMap = currentMap.toMutableMap()
updatedMap.replaceAll { _, _ -> !isAllExpanded }
it.copy(expandedSectionsMap = updatedMap.toImmutableMap())
}
dataManager.setAutoExpandSections(!isAllExpanded)
}
fun onToggleExpanded(id: Long) {
state.update {
val currentMap = it.expandedSectionsMap
val updatedMap = currentMap.toMutableMap()
updatedMap[id] = !updatedMap.getOrDefault(id, false)
it.copy(expandedSectionsMap = updatedMap.toImmutableMap())
}
}
fun removeBookmark() {
state.update {
it.copy(isBookmarked = false)
}
dataManager.removeBookmark(commandId)
}
fun addBookmark() {
state.update {
it.copy(
isBookmarked = true,
showBookmarkDialog = true,
)
}
dataManager.addBookmark(commandId)
}
fun hideBookmarkDialog() {
state.update {
it.copy(showBookmarkDialog = false)
}
}
}

View file

@ -0,0 +1,128 @@
@file:OptIn(
ExperimentalMaterialApi::class,
ExperimentalMaterialApi::class,
ExperimentalMaterialApi::class,
)
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commandlist
import androidx.compose.foundation.clickable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.ListItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.inspiredandroid.linuxcommandbibliotheca.R
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.HighlightedText
import com.inspiredandroid.linuxcommandbibliotheca.ui.theme.LinuxTheme
import databases.Command
import org.koin.androidx.compose.koinViewModel
/* 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.
*/
@Composable
fun CommandListScreen(
viewModel: CommandListViewModel = koinViewModel(),
onNavigate: (String) -> Unit,
) {
val commands by viewModel.commands.collectAsState()
LazyColumn {
items(
items = commands,
key = { it.id },
contentType = { "command_list_item" },
) { command ->
CommandListItem(
command = command,
onNavigate = onNavigate,
isBookmarked = viewModel.hasBookmark(command.id),
)
}
}
}
@Composable
fun CommandListItem(
command: Command,
searchText: String = "",
onNavigate: (String) -> Unit,
isBookmarked: Boolean,
) {
ListItem(
text = {
HighlightedText(
text = command.name,
pattern = searchText,
)
},
trailing = {
if (isBookmarked) {
Icon(
painterResource(R.drawable.ic_bookmark_black_24dp),
contentDescription = stringResource(R.string.remove_bookmark),
)
}
},
secondaryText = {
HighlightedText(
text = command.description,
pattern = searchText,
)
},
modifier = Modifier.clickable(
onClick = remember(command.id, command.name, onNavigate) {
{ onNavigate("command?commandId=${command.id}&commandName=${command.name}") }
},
),
)
}
@Preview
@Composable
fun CommandListItemPreview() {
val command = Command(0L, 0L, "cowsay", "A talking cow says moo.")
LinuxTheme {
CommandListItem(
command = command,
searchText = "cow",
onNavigate = { },
isBookmarked = true,
)
}
}
@Preview
@Composable
fun CommandListScreenPreview() {
// This preview is more complex due to the ViewModel dependency.
// For a proper preview, you'd typically use a fake ViewModel implementation
// or mock data source. For now, this will just show an empty screen
// or potentially crash if the ViewModel koinViewModel() can't be resolved in preview.
// To make it useful, one might need to adjust Koin setup for previews or pass
// a fake ViewModel.
// LinuxTheme {
// CommandListScreen(onNavigate = {})
// }
}

View file

@ -0,0 +1,63 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commandlist
import android.content.SharedPreferences
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.inspiredandroid.linuxcommandbibliotheca.DataManager
import com.linuxcommandlibrary.shared.databaseHelper
import databases.Command
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/* 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.
*/
class CommandListViewModel(private val dataManager: DataManager) : ViewModel() {
private val _commands = MutableStateFlow<ImmutableList<Command>>(persistentListOf())
val commands = _commands.asStateFlow()
private val preferenceListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == DataManager.KEY_BOOKMARKS) {
updateCommands()
}
}
init {
updateCommands()
dataManager.prefs.registerOnSharedPreferenceChangeListener(preferenceListener)
}
private fun updateCommands() {
viewModelScope.launch(Dispatchers.IO) {
_commands.update {
databaseHelper.getCommands().sortedBy { !hasBookmark(it.id) }.toImmutableList()
}
}
}
fun hasBookmark(id: Long): Boolean = dataManager.hasBookmark(id)
override fun onCleared() {
dataManager.prefs.unregisterOnSharedPreferenceChangeListener(preferenceListener)
super.onCleared()
}
}

View file

@ -0,0 +1,97 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.search
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.basicgroups.BasicGroupColumn
import com.inspiredandroid.linuxcommandbibliotheca.ui.screens.commandlist.CommandListItem
import org.koin.androidx.compose.koinViewModel
@Composable
fun SearchScreen(
searchText: String,
viewModel: SearchViewModel = koinViewModel(),
onNavigate: (String) -> Unit,
) {
// Removed the early return for searchText.isEmpty() as ViewModel now handles it by emitting an empty state.
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(searchText) {
viewModel.search(searchText)
}
val keyboardController = LocalSoftwareKeyboardController.current
val lazyListState = rememberLazyListState()
val showEmptyMessage by remember(uiState.filteredCommands, uiState.filteredBasicGroups) {
derivedStateOf {
uiState.filteredCommands.isEmpty() && uiState.filteredBasicGroups.isEmpty()
}
}
// Only show "404" if search text is not empty and results are empty
if (searchText.isNotEmpty() && showEmptyMessage) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable(enabled = false, onClick = {})
.background(MaterialTheme.colors.background),
) {
Text("404 command not found", modifier = Modifier.align(Alignment.Center))
}
} else {
LazyColumn(
Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
state = lazyListState,
) {
items(
items = uiState.filteredBasicGroups,
key = { "basicGroup_${it.id}" },
contentType = { "search_basic_group_item" },
) { basicGroup ->
BasicGroupColumn(
basicGroup = basicGroup,
searchText = searchText,
onNavigate = onNavigate,
isExpanded = !uiState.collapsedMap.getOrDefault(basicGroup.id, false),
onToggleCollapse = { viewModel.toggleCollapse(basicGroup.id) },
)
}
items(
items = uiState.filteredCommands,
key = { "command_${it.id}" },
contentType = { "search_command_item" },
) { command ->
CommandListItem(
command = command,
searchText = searchText,
onNavigate = onNavigate,
isBookmarked = false, // Or fetch actual bookmark status if relevant
)
}
}
}
LaunchedEffect(lazyListState.isScrollInProgress) {
if (lazyListState.isScrollInProgress) {
keyboardController?.hide()
}
}
}

View file

@ -0,0 +1,14 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.search
import databases.BasicGroup
import databases.Command
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
data class SearchUiState(
val filteredCommands: ImmutableList<Command> = persistentListOf(),
val filteredBasicGroups: ImmutableList<BasicGroup> = persistentListOf(),
val collapsedMap: ImmutableMap<Long, Boolean> = persistentMapOf(),
)

View file

@ -0,0 +1,66 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.search
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.sortedSearch
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.coroutines.cancellation.CancellationException
class SearchViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SearchUiState())
val uiState = _uiState.asStateFlow()
fun isGroupCollapsed(id: Long): Boolean = _uiState.value.collapsedMap.getOrDefault(id, false)
fun toggleCollapse(id: Long) {
_uiState.update { currentState ->
val newMap = currentState.collapsedMap.toMutableMap()
newMap[id] = !currentState.collapsedMap.getOrDefault(id, false)
currentState.copy(collapsedMap = newMap.toPersistentMap())
}
}
private var searchJob: Job? = null
fun search(searchText: String) {
searchJob?.cancel()
if (searchText.isBlank()) {
_uiState.update {
it.copy(
filteredCommands = persistentListOf(),
filteredBasicGroups = persistentListOf(),
)
}
return
}
searchJob = viewModelScope.launch(Dispatchers.IO) {
try {
ensureActive()
val commands = databaseHelper.getCommandsByQuery(searchText).sortedSearch(searchText)
val basicGroups = databaseHelper.getBasicGroupsByQuery(searchText)
ensureActive()
_uiState.update { currentState ->
currentState.copy(
filteredCommands = commands.toImmutableList(),
filteredBasicGroups = basicGroups.toImmutableList(),
)
}
} catch (ignore: CancellationException) {
// Preserve previous results on cancellation
}
}
}
}

View file

@ -0,0 +1,111 @@
@file:OptIn(ExperimentalMaterialApi::class)
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.tips
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.CommandView
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.NestedCommandView
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.NestedText
import com.inspiredandroid.linuxcommandbibliotheca.ui.composables.SectionTitle
import kotlinx.collections.immutable.toImmutableList
import org.koin.androidx.compose.koinViewModel
/* 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.
*/
@Composable
fun TipsScreen(
onNavigate: (String) -> Unit = {},
viewModel: TipsViewModel = koinViewModel(),
) {
LazyVerticalStaggeredGrid(
modifier = Modifier
.semantics { contentDescription = "Scroll" },
columns = StaggeredGridCells.Adaptive(minSize = 300.dp),
) {
items(
items = viewModel.tips,
key = { it.tip.id },
contentType = { "tip_item" },
) { tip ->
TipItemCard(tip = tip, onNavigate = onNavigate)
}
}
}
@Composable
private fun TipItemCard(tip: MergedTip, onNavigate: (String) -> Unit) {
Card(
elevation = 4.dp,
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(8.dp)) {
SectionTitle(title = tip.tip.title)
TipSections(sections = tip.sections, onNavigate = onNavigate)
}
}
}
@Composable
private fun TipSections(sections: List<TipSectionElement>, onNavigate: (String) -> Unit) {
// Assuming MergedTip.sections is already ImmutableList from ViewModel refactor
// If not, it should be passed as ImmutableList<TipSectionElement>
sections.forEach { element ->
when (element) {
is TipSectionElement.Text -> {
Text(element.text)
}
is TipSectionElement.Code -> {
CommandView(
command = element.command,
elements = element.elements.toImmutableList(), // elements within TipSectionElement should also be ImmutableList
onNavigate = onNavigate,
)
}
is TipSectionElement.NestedCode -> {
NestedCommandView(
text = element.text,
command = element.command,
commandElements = element.elements.toImmutableList(), // elements within TipSectionElement should also be ImmutableList
onNavigate = onNavigate,
)
}
is TipSectionElement.NestedText -> {
NestedText(
textLeft = element.text,
textRight = element.info,
)
}
}
}
}

View file

@ -0,0 +1,71 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.screens.tips
import androidx.lifecycle.ViewModel
import com.linuxcommandlibrary.shared.CommandElement
import com.linuxcommandlibrary.shared.databaseHelper
import com.linuxcommandlibrary.shared.getCommandList
import databases.Tip
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
sealed class TipSectionElement {
data class Text(val text: String) : TipSectionElement()
data class Code(val command: String, val elements: ImmutableList<CommandElement>) : TipSectionElement()
data class NestedCode(
val text: String,
val command: String,
val elements: ImmutableList<CommandElement>,
) : TipSectionElement()
data class NestedText(val text: String, val info: String) : TipSectionElement()
}
data class MergedTip(val tip: Tip, val sections: ImmutableList<TipSectionElement>)
class TipsViewModel : ViewModel() {
var tips: ImmutableList<MergedTip>
init {
val tipSectionsFromDB = databaseHelper.getTipSections() // Renamed to avoid confusion with MergedTip.sections
tips = databaseHelper.getTips().map { tip ->
MergedTip(
tip,
tipSectionsFromDB.filter { it.tip_id == tip.id }.map { section ->
when (section.type) {
0L -> {
val text =
section.data1.replace("\\n", "").replace("<b>", "").replace("</b>", "")
.replace("\\'", "")
TipSectionElement.Text(text)
}
1L -> {
TipSectionElement.Code(
section.data1,
section.data1.getCommandList(section.extra).toImmutableList(),
)
}
3L -> {
if (section.data2.startsWith("$")) {
TipSectionElement.NestedCode(
section.data1,
section.data2,
section.data2.getCommandList(section.extra).toImmutableList(),
)
} else {
TipSectionElement.NestedText(section.data1, section.data2)
}
}
else -> {
TipSectionElement.Text("")
}
}
}.toImmutableList(), // Convert list of sections for a tip to ImmutableList
)
}.toImmutableList() // Convert final list of MergedTip to ImmutableList
}
}

View file

@ -0,0 +1,88 @@
package com.inspiredandroid.linuxcommandbibliotheca.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Typography
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import com.inspiredandroid.linuxcommandbibliotheca.R
/* 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.
*/
data class CustomColors(
val navBarBackground: Color = Color(0xFF00695C),
)
val LocalCustomColors = compositionLocalOf { CustomColors() }
@Composable
fun LinuxTheme(content: @Composable () -> Unit) {
val darkMode = isSystemInDarkTheme()
val darkColors = darkColors(
primary = Color(0xFFe45151),
secondary = Color.White,
background = Color(0xFF262626),
surface = Color(0xFF262626),
)
val lightColors = lightColors(
primary = Color(0xFFe45151),
secondary = Color.Black,
background = Color.White,
surface = Color.White,
)
val colorSchema = if (darkMode) darkColors else lightColors
val techMonoFont = FontFamily(
Font(R.font.share_tech_mono),
)
val codeTextStyle = TextStyle(
fontFamily = techMonoFont,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
color = colorSchema.secondary,
)
val typography = Typography(
subtitle2 = codeTextStyle,
)
val customColors = if (darkMode) {
CustomColors(
navBarBackground = Color(0xFF2D2D2D),
)
} else {
CustomColors(
navBarBackground = Color(0xFFFAFAFA),
)
}
CompositionLocalProvider(LocalCustomColors provides customColors) {
MaterialTheme(
colors = colorSchema,
typography = typography,
content = content,
)
}
}

View file

@ -0,0 +1,46 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#00000000"
android:pathData="M6,7"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M12,5c0,-1.2 1,-2 2,-2c-2.6,0 -8.8,0 -9.2,0C3.8,3 3,3.9 3,5s0.8,2 1.8,2c0.4,0 6.6,0 9.2,0C13,7 12,6.2 12,5z"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineCap="round" />
<path
android:fillColor="#00000000"
android:pathData="M6,7c0,0 0,7.5 0,10s1.5,4 3,4h5"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M17,17c0,0 0,-7.5 0,-10s-1.5,-4 -3,-4"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M9,11L14,11"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M9,15L14,15"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#ffffff"
android:pathData="M19,16h2v8h-2z" />
<path
android:fillColor="#ffffff"
android:pathData="M16,19h8v2h-8z" />
</vector>

View file

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M1317,1804c-4,-4 -7,-29 -7,-55 0,-42 4,-51 35,-76 102,-84 329,-84 430,0 34,27 36,32 33,81l-3,51 -241,3c-132,1 -243,-1 -247,-4z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1227,1654c-4,-4 -7,-24 -7,-45 0,-37 -1,-38 -42,-41 -42,-3 -43,-4 -43,-38 0,-34 1,-35 42,-38 42,-3 42,-3 45,-45 3,-41 4,-42 38,-42 34,0 35,1 38,42 3,42 3,42 45,45 41,3 42,4 42,38 0,34 -1,35 -42,38 -42,3 -42,3 -45,45 -3,39 -5,42 -33,45 -17,2 -34,0 -38,-4z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1469,1531c-35,-35 -39,-44 -39,-91 0,-47 4,-56 39,-91 35,-35 44,-39 91,-39 47,0 56,4 91,39 35,35 39,44 39,91 0,47 -4,56 -39,91 -35,35 -44,39 -91,39 -47,0 -56,-4 -91,-39z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M77,1183c-10,-10 -8,-238 2,-265 28,-73 162,-157 321,-200 122,-33 357,-33 478,0 155,42 254,98 305,174 27,41 28,44 25,167l-3,126 -561,3c-308,1 -564,-1 -567,-5z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1356,1178c-3,-7 -7,-67 -10,-133 -6,-151 -22,-199 -92,-275 -30,-32 -54,-63 -54,-70 0,-20 228,-7 324,20 150,40 248,97 299,172 27,41 28,44 25,167l-3,126 -243,3c-189,2 -243,0 -246,-10z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M535,531c-50,-23 -109,-84 -130,-133 -21,-52 -19,-153 4,-203 23,-50 84,-109 133,-130 48,-19 148,-19 196,0 49,21 110,80 133,130 23,49 25,151 5,199 -20,49 -85,116 -131,137 -54,25 -157,25 -210,0z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1175,531c-50,-23 -109,-84 -130,-133 -21,-52 -19,-153 4,-203 23,-50 84,-109 133,-130 48,-19 148,-19 196,0 49,21 110,80 133,130 23,49 25,151 5,199 -20,49 -85,116 -131,137 -54,25 -157,25 -210,0z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M4,12l1.41,1.41L11,7.83V20h2V7.83l5.58,5.59L20,12l-8,-8 -8,8z" />
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M14,10l7,-7l0,7z" />
<path
android:fillColor="#ffffff"
android:pathData="M10,14l-7,7l0,-7z" />
<path
android:fillColor="#ffffff"
android:pathData="M12,3c-4.963,0 -9,4.038 -9,9h2c0,-3.86 3.141,-7 7,-7c2.785,0 5.188,1.639 6.315,4h2.16C19.236,5.51 15.91,3 12,3z" />
<path
android:fillColor="#ffffff"
android:pathData="M12,19c-2.785,0 -5.188,-1.639 -6.315,-4h-2.16C4.764,18.49 8.09,21 12,21c4.963,0 9,-4.037 9,-9h-2C19,15.859 15.859,19 12,19z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillAlpha=".3"
android:fillColor="#FFF"
android:pathData="M17,5.33C17,4.6 16.4,4 15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33V8h10V5.33z" />
<path
android:fillColor="#FFF"
android:pathData="M7,8v12.67C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V8H7z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M17.71,7.71L12,2h-1v7.59L6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 11,14.41L11,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,5.83l1.88,1.88L13,9.59L13,5.83zM14.88,16.29L13,18.17v-3.76l1.88,1.88z" />
</vector>

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#00000000"
android:pathData="M8.203,12l0,-8l4,4z"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M8.203,20l0,-8l4,4z"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M3.203,7L9.303,13.1"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M3.203,17L8.903,11.3"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#ffffff"
android:pathData="M15.51,14.21l0,8.894l7.395,-4.447z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M229,1587c-18,-12 -44,-38 -56,-56l-23,-34 0,-537 0,-537 23,-34c12,-18 38,-44 56,-56 33,-23 40,-23 305,-23l271,0 80,80 80,80 346,0c343,0 346,0 380,23 18,12 44,38 56,56l23,34 0,457 0,457 -23,34c-12,18 -38,44 -56,56l-34,23 -697,0 -697,0 -34,-23zM1175,890l-110,-110 -5,82 -5,83 -207,3 -208,2 0,50 0,50 208,2 207,3 3,85 3,84 112,-112 112,-112 -110,-110z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m296,880 l-56,-56 240,-240 240,240 -56,56 -184,-184L296,880ZM480,376L240,136l56,-56 184,184 184,-184 56,56 -240,240Z"
android:fillColor="#1f1f1f"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M389,1747c-18,-12 -44,-38 -56,-56l-23,-34 0,-697 0,-697 23,-34c12,-18 38,-44 56,-56 34,-23 36,-23 385,-23l351,0 243,243 242,242 0,511 0,511 -23,34c-12,18 -38,44 -56,56l-34,23 -537,0 -537,0 -34,-23zM1182,1463l3,-118 118,-3 117,-3 0,-69 0,-69 -117,-3 -118,-3 -3,-117 -3,-118 -69,0 -69,0 -3,118 -3,117 -117,3 -118,3 0,69 0,69 118,3 117,3 3,118 3,117 69,0 69,0 3,-117zM1450,707c0,-1 -90,-92 -200,-202l-200,-200 0,203 0,202 200,0c110,0 200,-1 200,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M229,1587c-18,-12 -44,-38 -56,-56l-23,-34 0,-537 0,-537 23,-34c12,-18 38,-44 56,-56 33,-23 40,-23 305,-23l271,0 80,80 80,80 346,0c343,0 346,0 380,23 18,12 44,38 56,56l23,34 0,457 0,457 -23,34c-12,18 -38,44 -56,56l-34,23 -697,0 -697,0 -34,-23zM1272,1233l3,-118 118,-3 117,-3 0,-69 0,-69 -117,-3 -118,-3 -3,-117 -3,-118 -69,0 -69,0 -3,118 -3,117 -117,3 -118,3 0,69 0,69 118,3 117,3 3,118 3,117 69,0 69,0 3,-117z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M389,1747c-18,-12 -44,-38 -56,-56l-23,-34 0,-697 0,-697 23,-34c12,-18 38,-44 56,-56 34,-23 36,-23 385,-23l351,0 243,243 242,242 0,511 0,511 -23,34c-12,18 -38,44 -56,56l-34,23 -537,0 -537,0 -34,-23zM1420,1270l0,-70 -310,0 -310,0 0,70 0,70 310,0 310,0 0,-70zM1450,707c0,-1 -90,-92 -200,-202l-200,-200 0,203 0,202 200,0c110,0 200,-1 200,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M229,1587c-18,-12 -44,-38 -56,-56l-23,-34 0,-537 0,-537 23,-34c12,-18 38,-44 56,-56 33,-23 40,-23 305,-23l271,0 80,80 80,80 346,0c343,0 346,0 380,23 18,12 44,38 56,56l23,34 0,457 0,457 -23,34c-12,18 -38,44 -56,56l-34,23 -697,0 -697,0 -34,-23zM1510,1040l0,-70 -310,0 -310,0 0,70 0,70 310,0 310,0 0,-70z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M21,2L3,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h7c1.1,0 2,-0.9 2,-2L23,4c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,4h18v12z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M20,13H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-6c0,-0.55 -0.45,-1 -1,-1zM7,19c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM20,3H4c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1V4c0,-0.55 -0.45,-1 -1,-1zM7,9c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z" />
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="m10,9c0,-1.7 1.3,-3 3,-3s3,1.3 3,3c0,1.7 -1.3,3 -3,3s-3,-1.3 -3,-3zM13,14c-4.6,0 -6,3.3 -6,3.3l0,1.7l12,0l0,-1.7c0,0 -1.4,-3.3 -6,-3.3z" />
<path
android:fillColor="#ffffff"
android:pathData="M19.5,8.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
<path
android:fillColor="#ffffff"
android:pathData="m19.5,13c-1.2,0 -2.1,0.3 -2.8,0.8c2.3,1.1 3.2,3 3.2,3.2l0,0.1l4.1,0l0,-1.3c0,-0.1 -1.1,-2.8 -4.5,-2.8z" />
<path
android:fillColor="#ffffff"
android:pathData="m8.82,3.303l1.231,1.22c0.188,0.186 0.188,0.487 0,0.673l-0.817,0.81l-1.911,-1.893l0.816,-0.81c0.188,-0.186 0.493,-0.186 0.68,0zM6.744,4.687l-5.007,4.961l0,1.893l1.911,0l5.007,-4.961l-1.911,-1.893z" />
</vector>

View file

@ -0,0 +1,81 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#00000000"
android:pathData="M8,1L8,4"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M12,1L12,4"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M16,1L16,4"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M8,20L8,23"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M12,20L12,23"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M16,20L16,23"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M23,8L20,8"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M23,12L20,12"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M23,16L20,16"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M4,8L1,8"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M4,12L1,12"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M4,16L1,16"
android:strokeWidth="2"
android:strokeColor="#ffffff"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffffff"
android:pathData="M17,5H7C5.9,5 5,5.9 5,7v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V7C19,5.9 18.1,5 17,5zM16,15H8V9h8V15z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,880 L240,640l57,-57 183,183 183,-183 57,57L480,880ZM298,376l-58,-56 240,-240 240,240 -58,56 -182,-182 -182,182Z"
android:fillColor="#1f1f1f"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM18.5,9H13V3.5L18.5,9z" />
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M389,1747c-18,-12 -44,-38 -56,-56l-23,-34 0,-697 0,-697 23,-34c12,-18 38,-44 56,-56 34,-23 36,-23 385,-23l351,0 243,243 242,242 0,511 0,511 -23,34c-12,18 -38,44 -56,56l-34,23 -537,0 -537,0 -34,-23zM1154,1472c39,-18 90,-49 114,-69 54,-44 122,-146 122,-183 0,-57 -95,-173 -191,-233 -123,-76 -355,-76 -478,0 -96,60 -191,176 -191,233 0,37 68,139 121,181 111,89 177,110 324,107 100,-3 116,-6 179,-36zM1450,707c0,-1 -90,-92 -200,-202l-200,-200 0,203 0,202 200,0c110,0 200,-1 200,-3z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M875,1403c-153,-80 -155,-286 -3,-364 73,-37 179,-18 234,43 93,101 64,256 -58,319 -46,23 -131,25 -173,2zM1041,1301c25,-25 29,-37 29,-81 0,-44 -4,-56 -29,-81 -25,-25 -37,-29 -81,-29 -44,0 -56,4 -81,29 -25,25 -29,37 -29,81 0,44 4,56 29,81 25,25 37,29 81,29 44,0 56,-4 81,-29z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M86,1408c-24,-34 -24,-742 0,-776 15,-21 20,-22 207,-22l192,0 123,123 122,122 0,265c0,224 -2,269 -16,288 -15,22 -16,22 -314,22 -298,0 -299,0 -314,-22zM630,887c0,-1 -40,-43 -90,-92l-90,-90 0,93 0,92 90,0c50,0 90,-1 90,-3z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1207,1402c-27,-28 -27,-29 -27,-175l0,-146 -100,99c-54,55 -103,100 -107,100 -14,0 -53,-30 -53,-41 0,-6 26,-36 57,-67l57,-57 -135,-5 -134,-5 0,-35 0,-35 134,-5 135,-5 -57,-57c-31,-31 -57,-61 -57,-67 0,-11 39,-41 53,-41 4,0 53,45 107,100l100,99 0,-196 0,-197 27,-28 27,-28 181,0 180,0 123,123 122,122 0,260 0,259 -27,28 -27,28 -276,0 -276,0 -27,-28zM1612,1263l3,-58 58,-3c53,-3 57,-5 57,-27 0,-22 -4,-24 -57,-27l-58,-3 -3,-57c-3,-54 -5,-58 -27,-58 -22,0 -24,4 -27,58l-3,57 -57,3c-54,3 -58,5 -58,27 0,22 4,24 58,27l57,3 3,58c3,53 5,57 27,57 22,0 24,-4 27,-57zM1740,887c0,-1 -40,-43 -90,-92l-90,-90 0,93 0,92 90,0c50,0 90,-1 90,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M389,1747c-18,-12 -44,-38 -56,-56l-23,-34 0,-697 0,-697 23,-34c12,-18 38,-44 56,-56 34,-23 36,-23 385,-23l351,0 243,243 242,242 0,511 0,511 -23,34c-12,18 -38,44 -56,56l-34,23 -537,0 -537,0 -34,-23zM1230,1510l0,-30 -270,0 -270,0 0,30 0,30 270,0 270,0 0,-30zM1095,1250l129,-129 -75,-3 -74,-3 -3,-117 -3,-118 -109,0 -109,0 -3,118 -3,117 -74,3 -75,3 129,129c72,72 132,130 135,130 3,0 63,-58 135,-130zM1450,707c0,-1 -90,-92 -200,-202l-200,-200 0,203 0,202 200,0c110,0 200,-1 200,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M389,1747c-18,-12 -44,-38 -56,-56l-23,-34 0,-697 0,-697 23,-34c12,-18 38,-44 56,-56 34,-23 36,-23 385,-23l351,0 243,243 242,242 0,511 0,511 -23,34c-12,18 -38,44 -56,56l-34,23 -537,0 -537,0 -34,-23zM972,1363l218,-218 -72,-72 -73,-73 -217,217 -218,218 0,72 0,73 72,0 73,0 217,-217zM1310,1006c0,-26 -98,-126 -124,-126 -11,0 -37,17 -58,37l-38,37 72,73 72,73 38,-37c21,-20 38,-46 38,-57zM1450,707c0,-1 -90,-92 -200,-202l-200,-200 0,203 0,202 200,0c110,0 200,-1 200,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M181,1616c-51,-28 -50,-23 -51,-593 0,-392 3,-539 12,-558 23,-52 43,-55 343,-55 177,0 275,4 275,10 0,6 7,10 15,10 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 7,15 15,15 8,0 15,7 15,15 0,8 5,15 10,15 15,0 13,781 -2,815 -25,54 -32,55 -480,55 -324,-1 -418,-4 -437,-14zM960,815c0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -8,0 -15,-7 -15,-15 0,-8 -7,-15 -15,-15 -13,0 -15,22 -15,135l0,135 135,0c113,0 135,-2 135,-15z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1270,1612c-126,-57 -149,-214 -43,-297 36,-28 44,-30 141,-33l103,-4 -3,44 -3,43 -88,5c-78,4 -91,8 -108,29 -24,30 -24,72 0,102 17,21 30,25 108,29l88,5 3,43 3,42 -93,-1c-51,0 -100,-3 -108,-7z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1512,1578l3,-43 88,-5c78,-4 91,-8 108,-29 10,-13 19,-36 19,-51 0,-15 -9,-38 -19,-51 -17,-21 -30,-25 -108,-29l-88,-5 -3,-43 -3,-44 103,4c97,3 105,5 141,33 44,34 67,81 67,135 0,54 -23,101 -67,135 -36,28 -44,30 -141,33l-103,4 3,-44z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1354,1486c-3,-8 -4,-29 -2,-48l3,-33 135,0 135,0 0,45 0,45 -133,3c-108,2 -133,0 -138,-12z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1176,1178c-24,-34 -24,-742 0,-776 15,-21 20,-22 207,-22l192,0 123,123 122,122 0,265c0,224 -2,269 -16,288 -15,22 -16,22 -314,22 -298,0 -299,0 -314,-22zM1720,657c0,-1 -40,-43 -90,-92l-90,-90 0,93 0,92 90,0c50,0 90,-1 90,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M97,1402l-27,-28 0,-354 0,-354 27,-28 27,-28 181,0 180,0 123,123 122,122 0,260 0,259 -27,28 -27,28 -276,0 -276,0 -27,-28zM620,1175l0,-25 -145,0 -145,0 0,25 0,25 145,0 145,0 0,-25zM630,887c0,-1 -40,-43 -90,-92l-90,-90 0,93 0,92 90,0c50,0 90,-1 90,-3z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M1196,1408c-13,-18 -16,-53 -16,-175l0,-152 -100,99c-54,55 -103,100 -107,100 -14,0 -53,-30 -53,-41 0,-6 26,-36 57,-67l57,-57 -135,-5 -134,-5 0,-35 0,-35 134,-5 135,-5 -57,-57c-31,-31 -57,-61 -57,-67 0,-11 39,-41 53,-41 4,0 53,45 107,100l100,99 0,-202c0,-167 3,-206 16,-225 15,-21 20,-22 207,-22l192,0 123,123 122,122 0,265c0,224 -2,269 -16,288 -15,22 -16,22 -314,22 -298,0 -299,0 -314,-22zM1740,887c0,-1 -40,-43 -90,-92l-90,-90 0,93 0,92 90,0c50,0 90,-1 90,-3z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M389,1747c-18,-12 -44,-38 -56,-56l-23,-34 0,-697 0,-697 23,-34c12,-18 38,-44 56,-56 34,-23 36,-23 385,-23l351,0 243,243 242,242 0,511 0,511 -23,34c-12,18 -38,44 -56,56l-34,23 -537,0 -537,0 -34,-23zM1254,1614c14,-13 16,-50 16,-252 0,-130 -4,-244 -9,-251 -5,-8 -31,-17 -58,-20l-48,-6 -5,-80c-8,-135 -67,-195 -190,-195 -123,0 -182,60 -190,195l-5,80 -48,6c-27,3 -53,12 -58,20 -5,7 -9,121 -9,251 0,202 2,239 16,252 13,14 54,16 294,16 240,0 281,-2 294,-16zM1450,707c0,-1 -90,-92 -200,-202l-200,-200 0,203 0,202 200,0c110,0 200,-1 200,-3z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M905,1415c-52,-51 -18,-135 54,-135 42,0 81,38 81,80 0,19 -9,40 -25,55 -15,16 -36,25 -55,25 -19,0 -40,-9 -55,-25z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M837,1084c-4,-4 -7,-30 -7,-58 0,-60 25,-107 72,-136 42,-25 74,-25 116,0 51,31 73,75 70,140l-3,55 -121,3c-66,1 -123,-1 -127,-4z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M17.81,4.47c-0.08,0 -0.16,-0.02 -0.23,-0.06C15.66,3.42 14,3 12.01,3c-1.98,0 -3.86,0.47 -5.57,1.41 -0.24,0.13 -0.54,0.04 -0.68,-0.2 -0.13,-0.24 -0.04,-0.55 0.2,-0.68C7.82,2.52 9.86,2 12.01,2c2.13,0 3.99,0.47 6.03,1.52 0.25,0.13 0.34,0.43 0.21,0.67 -0.09,0.18 -0.26,0.28 -0.44,0.28zM3.5,9.72c-0.1,0 -0.2,-0.03 -0.29,-0.09 -0.23,-0.16 -0.28,-0.47 -0.12,-0.7 0.99,-1.4 2.25,-2.5 3.75,-3.27C9.98,4.04 14,4.03 17.15,5.65c1.5,0.77 2.76,1.86 3.75,3.25 0.16,0.22 0.11,0.54 -0.12,0.7 -0.23,0.16 -0.54,0.11 -0.7,-0.12 -0.9,-1.26 -2.04,-2.25 -3.39,-2.94 -2.87,-1.47 -6.54,-1.47 -9.4,0.01 -1.36,0.7 -2.5,1.7 -3.4,2.96 -0.08,0.14 -0.23,0.21 -0.39,0.21zM9.75,21.79c-0.13,0 -0.26,-0.05 -0.35,-0.15 -0.87,-0.87 -1.34,-1.43 -2.01,-2.64 -0.69,-1.23 -1.05,-2.73 -1.05,-4.34 0,-2.97 2.54,-5.39 5.66,-5.39s5.66,2.42 5.66,5.39c0,0.28 -0.22,0.5 -0.5,0.5s-0.5,-0.22 -0.5,-0.5c0,-2.42 -2.09,-4.39 -4.66,-4.39 -2.57,0 -4.66,1.97 -4.66,4.39 0,1.44 0.32,2.77 0.93,3.85 0.64,1.15 1.08,1.64 1.85,2.42 0.19,0.2 0.19,0.51 0,0.71 -0.11,0.1 -0.24,0.15 -0.37,0.15zM16.92,19.94c-1.19,0 -2.24,-0.3 -3.1,-0.89 -1.49,-1.01 -2.38,-2.65 -2.38,-4.39 0,-0.28 0.22,-0.5 0.5,-0.5s0.5,0.22 0.5,0.5c0,1.41 0.72,2.74 1.94,3.56 0.71,0.48 1.54,0.71 2.54,0.71 0.24,0 0.64,-0.03 1.04,-0.1 0.27,-0.05 0.53,0.13 0.58,0.41 0.05,0.27 -0.13,0.53 -0.41,0.58 -0.57,0.11 -1.07,0.12 -1.21,0.12zM14.91,22c-0.04,0 -0.09,-0.01 -0.13,-0.02 -1.59,-0.44 -2.63,-1.03 -3.72,-2.1 -1.4,-1.39 -2.17,-3.24 -2.17,-5.22 0,-1.62 1.38,-2.94 3.08,-2.94 1.7,0 3.08,1.32 3.08,2.94 0,1.07 0.93,1.94 2.08,1.94s2.08,-0.87 2.08,-1.94c0,-3.77 -3.25,-6.83 -7.25,-6.83 -2.84,0 -5.44,1.58 -6.61,4.03 -0.39,0.81 -0.59,1.76 -0.59,2.8 0,0.78 0.07,2.01 0.67,3.61 0.1,0.26 -0.03,0.55 -0.29,0.64 -0.26,0.1 -0.55,-0.04 -0.64,-0.29 -0.49,-1.31 -0.73,-2.61 -0.73,-3.96 0,-1.2 0.23,-2.29 0.68,-3.24 1.33,-2.79 4.28,-4.6 7.51,-4.6 4.55,0 8.25,3.51 8.25,7.83 0,1.62 -1.38,2.94 -3.08,2.94s-3.08,-1.32 -3.08,-2.94c0,-1.07 -0.93,-1.94 -2.08,-1.94s-2.08,0.87 -2.08,1.94c0,1.71 0.66,3.31 1.87,4.51 0.95,0.94 1.86,1.46 3.27,1.85 0.27,0.07 0.42,0.35 0.35,0.61 -0.05,0.23 -0.26,0.38 -0.47,0.38z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M229,1587c-18,-12 -44,-38 -56,-56l-23,-34 0,-537 0,-537 23,-34c12,-18 38,-44 56,-56 33,-23 40,-23 305,-23l271,0 80,80 80,80 346,0c343,0 346,0 380,23 18,12 44,38 56,56l23,34 0,457 0,457 -23,34c-12,18 -38,44 -56,56l-34,23 -697,0 -697,0 -34,-23zM530,1390l0,-110 -80,0c-73,0 -80,2 -80,20 0,16 8,19 58,22 49,3 57,6 57,23 0,14 -8,21 -27,23 -20,2 -28,9 -28,22 0,13 8,20 28,22 19,2 27,9 27,23 0,17 -8,20 -57,23 -50,3 -58,6 -58,22 0,18 7,20 80,20l80,0 0,-110zM1490,1390l0,-50 -410,0 -410,0 0,50 0,50 410,0 410,0 0,-50zM530,1120c0,-17 -7,-20 -45,-20 -30,0 -45,-4 -45,-13 0,-7 20,-35 45,-64 28,-32 45,-61 45,-77 0,-26 -1,-26 -80,-26 -73,0 -80,2 -80,20 0,17 7,20 45,20 30,0 45,4 45,13 0,7 -20,35 -45,64 -28,32 -45,61 -45,77 0,26 1,26 80,26 73,0 80,-2 80,-20zM1490,1030l0,-50 -410,0 -410,0 0,50 0,50 410,0 410,0 0,-50zM470,670l0,-110 -50,0c-43,0 -50,3 -50,19 0,14 8,21 28,23 27,3 27,3 30,91 3,79 5,87 22,87 19,0 20,-7 20,-110zM1490,670l0,-50 -410,0 -410,0 0,50 0,50 410,0 410,0 0,-50z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="1920.0"
android:viewportHeight="1920.0">
<path
android:fillColor="#ffffff"
android:pathData="M229,1587c-18,-12 -44,-38 -56,-56l-23,-34 0,-537 0,-537 23,-34c12,-18 38,-44 56,-56 33,-23 40,-23 305,-23l271,0 80,80 80,80 346,0c343,0 346,0 380,23 18,12 44,38 56,56l23,34 0,457 0,457 -23,34c-12,18 -38,44 -56,56l-34,23 -697,0 -697,0 -34,-23zM1154,1292c39,-18 90,-49 114,-69 54,-44 122,-146 122,-183 0,-57 -95,-173 -191,-233 -123,-76 -355,-76 -478,0 -96,60 -191,176 -191,233 0,37 68,139 121,181 111,89 177,110 324,107 100,-3 116,-6 179,-36z"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="M875,1223c-153,-80 -155,-286 -3,-364 73,-37 179,-18 234,43 93,101 64,256 -58,319 -46,23 -131,25 -173,2zM1041,1121c25,-25 29,-37 29,-81 0,-44 -4,-56 -29,-81 -25,-25 -37,-29 -81,-29 -44,0 -56,4 -81,29 -25,25 -29,37 -29,81 0,44 4,56 29,81 25,25 37,29 81,29 44,0 56,-4 81,-29z"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFF"
android:pathData="M17.73,12.02l3.98,-3.98c0.39,-0.39 0.39,-1.02 0,-1.41l-4.34,-4.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-3.98,3.98L8,2.29C7.8,2.1 7.55,2 7.29,2c-0.25,0 -0.51,0.1 -0.7,0.29L2.25,6.63c-0.39,0.39 -0.39,1.02 0,1.41l3.98,3.98L2.25,16c-0.39,0.39 -0.39,1.02 0,1.41l4.34,4.34c0.39,0.39 1.02,0.39 1.41,0l3.98,-3.98 3.98,3.98c0.2,0.2 0.45,0.29 0.71,0.29 0.26,0 0.51,-0.1 0.71,-0.29l4.34,-4.34c0.39,-0.39 0.39,-1.02 0,-1.41l-3.99,-3.98zM12,9c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM7.29,10.96L3.66,7.34l3.63,-3.63 3.62,3.62 -3.62,3.63zM10,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM12,15c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM14,11c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM16.66,20.34l-3.63,-3.62 3.63,-3.63 3.62,3.62 -3.62,3.63z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:pathData="M12,0C5.3844,0 0,5.3844 0,12C0,18.6156 5.3844,24 12,24C18.6156,24 24,18.6156 24,12C24,5.3844 18.6156,0 12,0zM12,2C17.5347,2 22,6.4653 22,12C22,17.5347 17.5347,22 12,22C6.4653,22 2,17.5347 2,12C2,6.4653 6.4653,2 12,2zM10,4L10,6L8,6L8,18L10,18L10,20L11,20L11,18L12,18L12,20L13,20L13,17.9805C14.2095,17.9306 15.1722,17.6492 15.8613,17.1133C16.6203,16.5223 17,15.6539 17,14.5059C17,13.8409 16.82,13.256 16.459,12.75C16.107,12.256 15.5788,11.9334 14.8828,11.7754C15.6218,11.2264 16,10.43 16,9.375L16,9.2871C16,8.1941 15.6035,7.3732 14.8105,6.8242C14.3272,6.4896 13.7142,6.2695 13,6.1387L13,4L12,4L12,6.0254C11.8332,6.0168 11.6765,6 11.5,6L11,6L11,4L10,4zM10.4102,8L12,8C12.375,8 12.7145,8.0849 13.0645,8.3359C13.4145,8.5869 13.5898,8.974 13.5898,9.5C13.5898,9.98 13.4173,10.4267 13.0703,10.6777C12.7223,10.9277 12.375,11 12,11L10.4102,11.002L10.4102,8zM10.4102,13L12.8145,13C13.4305,13 13.882,13.1353 14.168,13.4043C14.454,13.6733 14.5977,14.0623 14.5977,14.5723C14.5977,15.0433 14.4298,15.3987 14.0938,15.6387C13.7577,15.8797 13.2718,16 12.6328,16L10.4102,16L10.4102,13z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:pathData="M24,14.9C24,9.4 19,6 17,6s-2.4,1 -5,1S9,6 7,6s-7,3.4 -7,8.9c0,0 0,0 0,0c0,0 0,0 0,0.1c0,2.8 2.2,5 5,5c2.1,0 4.6,-2 7,-2s5,2 7,2c2.8,0 5,-2.2 5,-5C24,15 24,15 24,14.9C24,14.9 24,14.9 24,14.9zM10,14H8v2H6v-2H4v-2h2v-2h2v2h2V14zM17,9.7c0.7,0 1.3,0.6 1.3,1.3s-0.6,1.3 -1.3,1.3s-1.3,-0.6 -1.3,-1.3S16.3,9.7 17,9.7zM13.7,13c0,-0.7 0.6,-1.3 1.3,-1.3s1.3,0.6 1.3,1.3s-0.6,1.3 -1.3,1.3S13.7,13.7 13.7,13zM17,16.3c-0.7,0 -1.3,-0.6 -1.3,-1.3s0.6,-1.3 1.3,-1.3s1.3,0.6 1.3,1.3S17.7,16.3 17,16.3zM19,14.3c-0.7,0 -1.3,-0.6 -1.3,-1.3s0.6,-1.3 1.3,-1.3s1.3,0.6 1.3,1.3S19.7,14.3 19,14.3z" />
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillAlpha="0.4034118"
android:fillColor="#211f46"
android:pathData="m488.067,256.694c0,130.061 -104.369,235.497 -233.115,235.497 -128.746,0 -233.115,-105.435 -233.115,-235.497 0,-130.061 104.369,-235.497 233.115,-235.497 128.746,0 233.115,105.435 233.115,235.497z"
android:strokeWidth="8.421117"
android:strokeAlpha="0.40500003"
android:strokeColor="#0a0b1b" />
<path
android:pathData="m488.067,256.694c0,130.061 -104.369,235.497 -233.115,235.497 -128.746,0 -233.115,-105.435 -233.115,-235.497 0,-130.061 104.369,-235.497 233.115,-235.497 128.746,0 233.115,105.435 233.115,235.497z"
android:strokeWidth="13.33816814"></path>
<path
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="m174.829,422.11c0,0 19.739,1.396 45.131,-0.842 10.283,-0.906 49.327,-4.741 78.517,-11.143 0,0 35.59,-7.617 54.63,-14.633 19.923,-7.342 30.764,-13.573 35.643,-22.402 -0.213,-1.809 1.502,-8.224 -7.685,-12.078 -23.489,-9.852 -50.73,-8.07 -104.634,-9.213 -59.777,-2.054 -79.663,-12.06 -90.256,-20.118 -10.158,-8.175 -5.05,-30.793 38.474,-50.715 21.924,-10.609 107.87,-30.187 107.87,-30.187 -28.945,-14.307 -82.919,-39.459 -94.013,-44.89 -9.731,-4.763 -25.303,-11.936 -28.678,-20.614 -3.827,-8.331 9.038,-15.507 16.225,-17.562 23.145,-6.676 55.818,-10.825 85.555,-11.291 14.947,-0.234 17.373,-1.196 17.373,-1.196 20.624,-3.421 34.201,-17.532 28.545,-39.879 -5.078,-22.81 -31.862,-36.214 -57.314,-31.574 -23.968,4.37 -81.738,21.15 -81.738,21.15 71.408,-0.618 83.359,0.574 88.697,8.037 3.152,4.407 -1.432,10.451 -20.476,13.561 -20.733,3.386 -63.831,7.464 -63.831,7.464 -41.345,2.455 -70.468,2.62 -79.203,21.113 -5.707,12.082 6.085,22.763 11.254,29.449 21.841,24.289 53.388,37.389 73.695,47.036 7.641,3.63 30.059,10.484 30.059,10.484 -65.878,-3.623 -113.4,16.605 -141.276,39.896 -31.529,29.163 -17.581,63.924 47.012,85.327 38.152,12.642 57.072,18.587 113.981,13.462 33.52,-1.807 38.804,-0.732 39.138,2.019 0.47,3.872 -37.231,13.492 -47.524,16.461 -26.185,7.553 -94.828,22.804 -95.171,22.878z"
android:strokeWidth="0"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:pathData="M12,2C6.489,2 2,6.489 2,12C2,17.511 6.489,22 12,22C17.511,22 22,17.511 22,12C22,6.489 17.511,2 12,2zM12,4C15.3674,4 18.2326,6.0649 19.416,9L6.0176,9C5.4586,9 5.0076,9.4586 5.0176,10.0176L5.0352,11C5.2062,12.694 6.238,14 8,14C9.933,14 11,11 12,11C13,11 14.067,14 16,14C17.762,14 18.7938,12.694 18.9648,11L19.9316,11C19.9723,11.3281 20,11.6605 20,12C20,16.4301 16.4301,20 12,20C7.5699,20 4,16.4301 4,12C4,7.5699 7.5699,4 12,4zM17,15C17,15 16,16 10,16C10,16 9.67,18 12,18C16,18 17,15 17,15z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="122.52dp"
android:height="122.52dp"
android:viewportWidth="122.52"
android:viewportHeight="122.52">
<path
android:fillColor="#333333"
android:pathData="M120.206,55.806L66.714,2.316c-3.08,-3.08 -8.076,-3.08 -11.16,0l-11.106,11.11 14.09,14.09c3.275,-1.106 7.03,-0.364 9.64,2.246 2.624,2.626 3.36,6.413 2.226,9.7l13.58,13.579c3.287,-1.133 7.076,-0.4 9.7,2.228 3.668,3.666 3.668,9.608 0,13.276a9.389,9.389 0,0 1,-15.322 -10.21L65.695,45.669v33.33a9.454,9.454 0,0 1,2.482 1.774c3.667,3.666 3.667,9.608 0,13.28 -3.667,3.665 -9.612,3.665 -13.276,0 -3.667,-3.672 -3.667,-9.614 0,-13.28a9.375,9.375 0,0 1,3.076 -2.048V45.087a9.305,9.305 0,0 1,-3.076 -2.049c-2.777,-2.776 -3.445,-6.852 -2.02,-10.263L38.99,18.882 2.31,55.559a7.895,7.895 0,0 0,0 11.161l53.495,53.49a7.892,7.892 0,0 0,11.159 0l53.242,-53.242a7.893,7.893 0,0 0,0 -11.162" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:pathData="M6,2C4.895,2 4,2.895 4,4L4,9L3,9C1.895,9 1,9.895 1,11L1,16C1,17.105 1.895,18 3,18L4,18L4,20C4,21.105 4.895,22 6,22L18,22C19.105,22 20,21.105 20,20L20,18C21.105,18 22,17.105 22,16L22,11C22,9.895 21.105,9 20,9L20,7.8281C20,7.2981 19.7891,6.7891 19.4141,6.4141L15.5859,2.5859C15.2109,2.2109 14.7019,2 14.1719,2L6,2zM6,4L14,4L14,7C14,7.552 14.448,8 15,8L18,8L18,9L6,9L6,4zM5,11L6,11L6,14.5C6,15.328 5.328,16 4.5,16C3.672,16 3,15.328 3,14.5L4,14.5C4,14.776 4.224,15 4.5,15C4.776,15 5,14.776 5,14.5L5,11zM8.6445,11C10.0675,11.041 10.1543,12.2829 10.1543,12.5039L9.1875,12.5039C9.1875,12.4009 9.2049,11.8066 8.6289,11.8066C8.4539,11.8066 8.0598,11.8842 8.0898,12.3672C8.1188,12.8102 8.7035,13.0194 8.8105,13.0664C9.0345,13.1484 10.1414,13.6424 10.1504,14.6504C10.1524,14.8644 10.0971,15.985 8.6641,16C7.1051,16.017 7,14.6754 7,14.3984L7.9746,14.3984C7.9746,14.5454 7.9871,15.2562 8.6641,15.2012C9.0711,15.1672 9.1598,14.8743 9.1738,14.6563C9.1968,14.2893 8.8466,14.0686 8.4766,13.8906C7.9566,13.6406 7.1341,13.3334 7.1191,12.3594C7.1061,11.4824 7.7505,10.975 8.6445,11zM19,11L20,11L20,16L18.8457,16L17,12.7363L17,16L16,16L16,11.0234L17.1543,11.0234L19,14.2676L19,11zM13,11.0645C14.864,11.0345 15,12.8414 15,13.1914L15,14C15,14.342 14.9138,16.1015 13.0078,16.0605C10.9668,16.0185 11,14.342 11,14L11,13.1914C11,12.8404 11.167,11.0935 13,11.0645zM13.002,11.8867C12.057,11.9057 12,12.9535 12,13.1855L12,14C12,14.222 12.1547,15.2592 13.0117,15.2422C13.8337,15.2272 14,14.221 14,14L14,13.1855C14,12.9535 13.888,11.8687 13.002,11.8867zM6,18L18,18L18,20L6,20L6,18z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:pathData="M12,2C7.5898,2 4,5.5898 4,10L6,10C6,7.0313 8.168,4.5742 11,4.0938L11,10L13,10L13,4.0938C15.832,4.5703 18,7.0313 18,10L20,10C20,5.5898 16.4102,2 12,2ZM4,11L4,14C4,18.4102 7.5898,22 12,22C16.4102,22 20,18.4102 20,14L20,11ZM6,13L18,13L18,14C18,17.3086 15.3086,20 12,20C8.6914,20 6,17.3086 6,14Z" />
</vector>

View file

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:fillType="evenOdd"
android:pathData="M12,15.2c-0.5,0.1 -1.3,1.4 -1.3,2.1c0,0.3 0.3,0.5 0.5,0.5c0.4,0 0.7,-0.4 0.8,-0.7c0.1,0.3 0.3,0.7 0.8,0.7c0.3,0 0.5,-0.2 0.5,-0.5C13.3,16.6 12.5,15.3 12,15.2z" />
<path
android:fillColor="#00000000"
android:pathData="M20,10c0,-3.9 -3.6,-7 -8,-7s-8,3.1 -8,7c0,1.8 1,4 1,4v1c0,1.1 0.9,2 2,2h0c1.1,0 2,0.9 2,2v0c0,1.1 0.9,2 2,2h2c1.1,0 2,-0.9 2,-2v0c0,-1.1 0.9,-2 2,-2h0c1.1,0 2,-0.9 2,-2v-1C19,14 20,11.8 20,10z"
android:strokeWidth="2"
android:strokeColor="#333333" />
<path
android:fillColor="#333333"
android:fillType="evenOdd"
android:pathData="M15,13m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
<path
android:fillColor="#333333"
android:fillType="evenOdd"
android:pathData="M9,13m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0" />
</vector>

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M4,4h16c0.6,0 1,0.4 1,1v10c0,0.6 -0.4,1 -1,1H4c-0.6,0 -1,-0.4 -1,-1V5C3,4.4 3.4,4 4,4z"
android:strokeWidth="2"
android:strokeColor="#333333" />
<path
android:fillColor="#00000000"
android:pathData="M7,20L17,20"
android:strokeWidth="2"
android:strokeColor="#333333" />
<path
android:fillColor="#00000000"
android:pathData="M12,20L12,18"
android:strokeWidth="4"
android:strokeColor="#333333" />
<path
android:fillColor="#00000000"
android:pathData="M6,10l3,0l2,-2l2,4l2,-2l3,0"
android:strokeWidth="2"
android:strokeColor="#333333" />
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#333333"
android:pathData="M12,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
<path
android:fillColor="#333333"
android:pathData="M12,14c-6.1,0 -8,4 -8,4v2h16v-2C20,18 18.1,14 12,14z" />
</vector>

View file

@ -0,0 +1,132 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="544.17"
android:viewportHeight="544.8642">
<path
android:fillColor="#019833"
android:pathData="M274.305,35.818 L37.516,272.926 273.319,509.046 510.108,271.938 274.305,35.818z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#66fe98"
android:pathData="m273.319,36.806 l0,-20.747 -257.509,257.855 21.706,0 235.803,-237.108z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#45fe02"
android:pathData="m272.727,36.806 l0,-20.747 257.509,257.855 -21.706,0 -235.803,-237.108z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#017d17"
android:pathData="m273.319,510.429 l0,20.747 -257.509,-257.855 21.706,0 235.803,237.108z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#00000000"
android:pathData="m63.582,42.246 l166.04,0 10.116,10.129 0,31.087 -8.023,9.78 -18.139,0 0,154.388 156.273,-154.388 -25.813,0 -9.069,-9.78 0,-32.834 8.372,-7.684 168.133,0 8.372,8.383 0,30.738 -380.218,390.51 -43.254,0 -12.52,-7.238 0,-373.491 -20.967,0 -7.674,-7.684 0,-32.834 8.372,-9.082z"
android:strokeWidth="27.644022"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#005d04"
android:pathData="m272.727,510.429 l0,20.747 257.509,-257.855 -21.706,0 -235.803,237.108z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#00000000"
android:pathData="M272.624,14.147 L14.147,272.971 271.547,530.718 530.024,271.893 272.624,14.147z"
android:strokeWidth="8.293206"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#fefefe"
android:pathData="m231.387,58.047 l9.373,-4.94 -9.62,-9.633 -167.479,0 -8.51,8.521 0,32.232 9.435,9.447 4.501,-9.447 -5.92,-5.928 0,-22.723 4.44,-3.952 159.833,0 3.946,6.422z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#00000000"
android:pathData="m349.794,50.63 l-5.914,5.922 0,21.741 5.18,5.187 27.366,0 0,21.006 -181.033,183.74 0,-204.487 30.086,0 6.173,-6.181 0,-22 -5.698,-4.409 -158.328,0 -5.18,5.187 0,22.476 5.266,5.273 27.539,0 0,376.557 5.18,5.187 31.294,0 379.375,-391.988 0,-17.289 -5.914,-5.922 -155.393,0z"
android:strokeWidth="1"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#fefefe"
android:pathData="m94.976,83.463 l0,377.236 4.884,5.589 -3.83,7.309 -10.821,-10.802 0,-369.552z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#808080"
android:pathData="m67.919,83.463 l-2.791,9.082 20.232,0 11.162,-9.082 -28.604,0z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#fefefe"
android:pathData="m345.836,93.663 l4.44,-9.139 -6.413,-5.928 0,-20.253 7.4,-7.41 154.407,0 5.92,7.904 8.386,-5.928 -8.633,-8.645 -165.999,0 -8.263,8.274 0,32.479 8.571,8.089m-134.15,155.954 l-16.125,39.178 182.032,-182.771 0,-21.735 -165.907,165.327z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#808080"
android:pathData="m231.018,56.916 l8.023,-5.239 0,31.786 -9.244,9.256 -17.964,0 0,156.658 -16.395,38.772 0,-204.686 29.999,0 5.581,-4.541 0,-22.005z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#cccccc"
android:pathData="m349.794,50.63 l-5.914,5.922 0,21.741 5.18,5.187 27.366,0 0,21.006 -181.033,183.74 0,-204.487 30.086,0 6.173,-6.181 0,-22 -5.698,-4.409 -158.328,0 -5.18,5.187 0,22.476 5.266,5.273 27.539,0 0,376.557 5.18,5.187 31.294,0 379.375,-391.988 0,-17.289 -5.914,-5.922 -155.393,0z"
android:strokeWidth="1"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#808080"
android:pathData="m511.588,57.059 l8.426,-4.871 0,30.558 -382.164,391.037 -40.574,0 3.876,-7.713 31.079,0 378.863,-391.722z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#808080"
android:pathData="m376.652,83.987 l-8.546,8.907 -22.325,0 5.232,-8.907c0.174,0 25.639,0 25.639,0z"
android:strokeWidth="0.94571567"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#cccccc"
android:pathData="m285.522,304.454a1.705,1.703 90,0 0,-0.777 0.389l-7.338,6.311A1.705,1.703 90,0 0,276.932 311.888L269.594,332.462a1.705,1.703 90,0 0,0.388 1.772l5.611,5.619a1.705,1.703 90,0 0,1.209 0.475l22.662,0a1.705,1.703 90,0 0,1.209 -0.475l5.914,-5.965a1.705,1.703 90,0 0,0.432 -0.735l6.302,-21.655a1.705,1.703 90,0 0,-0.432 -1.686l-4.878,-4.884A1.705,1.703 90,0 0,306.802 304.454l-20.935,0a1.705,1.703 90,0 0,-0.345 0zM243.997,362.459a1.705,1.703 90,0 0,-1.295 1.253l-2.806,11.151a1.705,1.703 90,0 0,1.64 2.118l13.338,0 -33.064,94.528a1.705,1.703 90,0 0,1.597 2.248l48.129,0a1.705,1.703 90,0 0,1.64 -1.167l3.151,-10.157a1.705,1.703 90,0 0,-1.64 -2.204l-10.878,0 32.719,-95.522a1.705,1.703 90,0 0,-1.597 -2.248l-50.589,0a1.705,1.703 90,0 0,-0.345 0zM397.146,362.805a1.705,1.703 90,0 0,-0.95 0.562l-10.014,11.324 -15.928,0 -10.619,-11.022a1.705,1.703 90,0 0,-1.209 -0.519l-38.028,0A1.705,1.703 90,0 0,318.802 364.274l-3.496,10.46a1.705,1.703 90,0 0,1.597 2.248l10.187,0 -31.683,93.491a1.705,1.703 90,0 0,1.597 2.248l40.489,0a1.705,1.703 90,0 0,1.597 -1.124l3.108,-9.12a1.705,1.703 90,0 0,-1.597 -2.248l-7.079,0 20.201,-63.667 36.561,0 -23.05,73.954a1.705,1.703 90,0 0,1.64 2.204l39.064,0a1.705,1.703 90,0 0,1.554 -1.037l3.496,-8.385a1.705,1.703 90,0 0,-1.554 -2.334l-7.079,0 20.546,-64.748 34.791,0 -23.395,74.3a1.705,1.703 90,0 0,1.64 2.204l42.906,0a1.705,1.703 90,0 0,1.597 -1.081l3.496,-9.12a1.705,1.703 90,0 0,-1.597 -2.291l-8.503,0 25.856,-84.068a1.705,1.703 90,0 0,-0.259 -1.556l-8.029,-10.806a1.705,1.703 90,0 0,-1.381 -0.648l-30.69,0a1.705,1.703 90,0 0,-1.252 0.519l-9.669,10.633 -16.921,0 -10.014,-10.979a1.705,1.703 90,0 0,-1.252 -0.519l-24.733,0a1.705,1.703 90,0 0,-0.345 0z"
android:strokeWidth="11.057609"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#cccccc"
android:pathData="m285.867,306.14 l-7.338,6.311 -7.338,20.574 5.611,5.619 22.662,0 5.914,-5.965 6.302,-21.655 -4.878,-4.884 -20.935,0zM244.342,364.145 L241.537,375.296 257.249,375.296 223.408,472.072 271.536,472.072 274.687,461.915 261.436,461.915 294.932,364.145 244.342,364.145zM397.491,364.49L387.002,376.377l-17.439,0 -11.137,-11.54 -38.028,0 -3.496,10.46 12.561,0 -32.46,95.739 40.489,0 3.108,-9.12 -9.41,0 21.28,-67.039 40.143,0 -23.741,76.159 39.064,0 3.496,-8.385 -9.41,0 21.626,-68.119 38.373,0 -24.086,76.504 42.906,0 3.496,-9.12 -10.834,0 26.546,-86.273 -8.029,-10.806 -30.69,0 -10.144,11.151 -18.475,0 -10.489,-11.497 -24.733,0z"
android:strokeWidth="1.3822011"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M10,2L9,3L4,3L4,5L20,5L20,3L15,3L14,2L10,2zM5,7L5,20C5,21.1 5.9,22 7,22L12.6836,22C12.2506,21.09 12,20.075 12,19C12,17.094 12.764,15.3675 14,14.1055L14,9L16,9L16,12.6836C16.91,12.2506 17.925,12 19,12L19,7L5,7zM8,9L10,9L10,20L8,20L8,9zM19,14C16.239,14 14,16.239 14,19C14,21.761 16.239,24 19,24C21.761,24 24,21.761 24,19C24,16.239 21.761,14 19,14zM18,16L20,16L20,18L22,18L22,20L20,20L20,22L18,22L18,20L16,20L16,18L18,18L18,16z" />
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M15,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
<path
android:fillColor="#ffffff"
android:pathData="M15,14c-6.1,0 -8,4 -8,4v2h16v-2C23,18 21.1,14 15,14z" />
<path
android:fillColor="#00000000"
android:pathData="M5,7L5,15"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M9,11L1,11"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
</vector>

View file

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M10,9c0,-1.7 1.3,-3 3,-3s3,1.3 3,3c0,1.7 -1.3,3 -3,3S10,10.7 10,9zM13,14c-4.6,0 -6,3.3 -6,3.3V19h12v-1.7C19,17.3 17.6,14 13,14z" />
<path
android:fillColor="#ffffff"
android:pathData="M19.5,8.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
<path
android:fillColor="#ffffff"
android:pathData="M19.5,13c-1.2,0 -2.1,0.3 -2.8,0.8c2.3,1.1 3.2,3 3.2,3.2l0,0.1H24v-1.3C24,15.7 22.9,13 19.5,13z" />
<path
android:fillColor="#00000000"
android:pathData="M5,8L5,16"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
<path
android:fillColor="#00000000"
android:pathData="M9,12L1,12"
android:strokeWidth="2"
android:strokeColor="#ffffff" />
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M15,20l0,-16l8,8z" />
<path
android:fillColor="#fff"
android:pathData="M16,15H3c-0.552,0 -1,-0.448 -1,-1v-4c0,-0.552 0.448,-1 1,-1h13V15z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,2A2,2 0,0 0,10 4A2,2 0,0 0,11 5.7305L11,8L8,8C5.8027,8 4,9.8027 4,12L4,16L2,16L2,18L4,18L4,20C4,21.0931 4.9069,22 6,22L18,22C19.0931,22 20,21.0931 20,20L20,18L22,18L22,16L20,16L20,12C20,9.8027 18.1973,8 16,8L13,8L13,5.7285A2,2 0,0 0,14 4A2,2 0,0 0,12 2zM8,10L11,10L13,10L16,10C17.1167,10 18,10.8833 18,12L18,20L15,20L15,18L9,18L9,20L6,20L6,12C6,10.8833 6.8833,10 8,10zM9.5,13A1.5,1.5 0,0 0,8 14.5A1.5,1.5 0,0 0,9.5 16A1.5,1.5 0,0 0,11 14.5A1.5,1.5 0,0 0,9.5 13zM14.5,13A1.5,1.5 0,0 0,13 14.5A1.5,1.5 0,0 0,14.5 16A1.5,1.5 0,0 0,16 14.5A1.5,1.5 0,0 0,14.5 13z" />
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M6,2h2v4h-2z" />
<path
android:fillColor="#ffffff"
android:pathData="M16,2h2v4h-2z" />
<path
android:fillColor="#ffffff"
android:pathData="M19,4H5C3.9,4 3,4.9 3,6v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6C21,4.9 20.1,4 19,4zM19,20H5V9h14V20z" />
<path
android:fillColor="#ffffff"
android:pathData="M13,18h-1.4v-5.4l-1.4,0.5v-1.2l2.6,-1.1H13V18z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M12,2C6.5,2 2,6.5 2,12c0,5.5 4.5,10 10,10s10,-4.5 10,-10C22,6.5 17.5,2 12,2zM16.9,15.5l-1.4,1.4L12,13.4l-3.5,3.5l-1.4,-1.4l3.5,-3.5L7.1,8.5l1.4,-1.4l3.5,3.5l3.5,-3.5l1.4,1.4L13.4,12L16.9,15.5z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M12,2C6.4883,2 2,6.4883 2,12C2,17.5117 6.4883,22 12,22C17.5117,22 22,17.5117 22,12C22,9.5117 21.0703,7.25 19.5625,5.5L22.0625,3L16,3L16,9.0625L18.1563,6.9063C19.3047,8.2852 20,10.0547 20,12C20,16.4297 16.4297,20 12,20C7.5703,20 4,16.4297 4,12C4,7.5703 7.5703,4 12,4ZM12,6C10.3438,6 9,7.3438 9,9C9,10.6563 10.3438,12 12,12C13.6563,12 15,10.6563 15,9C15,7.3438 13.6563,6 12,6ZM12,14C9.3789,14 7.7852,15.0938 6.9063,16.0313C8.0977,17.5273 9.9414,18.5 12,18.5C14.0586,18.5 15.9023,17.5273 17.0938,16.0313C16.2148,15.0938 14.6211,14 12,14Z" />
</vector>

Some files were not shown because too many files have changed in this diff Show more