Pith - shim
shim/app/src/main/kotlin/com/vgmlr/shim/MainActivity.kt [17.8 kb]
Modified: 22:39:16 117 026 (14 Jul 026)
11 Days Ago
package com.vgmlr.shim

import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.foundation.background
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.FileOpen
import androidx.compose.material.icons.outlined.History
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.DataArray
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.input.TextFieldValue

class MainActivity : ComponentActivity() {
    private lateinit var db: ShimDatabase
    private lateinit var themeManager: ShimThemeManager
    private var shimListState = mutableStateOf<List<ShimClass>>(emptyList())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        window.setBackgroundDrawableResource(R.color.background_color)
        db = ShimDatabase(this)
        themeManager = ShimThemeManager(this)

        enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT))

        ShimBackUp.schedule(this)
        
        setContent {
            ShimTheme(themeManager) {
                MainScreen()
            }
        }
    }

    override fun onResume() {
        super.onResume()
        themeManager.refresh()
        refreshList()
    }

    private fun refreshList() {
        shimListState.value = db.getAllShims()
    }

    @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
    @Composable
    fun MainScreen() {
        val lifecycleOwner = LocalLifecycleOwner.current
        var searchQuery by remember { mutableStateOf("") }
        var isSearchExpanded by remember { mutableStateOf(false) }
        var showDataDialog by remember { mutableStateOf(false) }
        var showTagsDialog by remember { mutableStateOf(false) }
        
        var showVersionDialog by remember { mutableStateOf(false) }
        var showExportDialog by remember { mutableStateOf(false) }
        var shimText by remember { mutableStateOf("") }

        var hashtagText by remember { mutableStateOf(TextFieldValue("")) }
        val allTags = remember(shimListState.value) {
            shimListState.value.flatMap { it.shimhash.split(" ").filter { tag -> tag.isNotBlank() } }.distinct().sorted()
        }
        val tagAssociations = remember(shimListState.value) {
            val associations = mutableMapOf<String, MutableSet<String>>()
            shimListState.value.forEach { shim ->
                val tags = shim.shimhash.split(" ").filter { it.isNotBlank() }
                tags.forEach { tag ->
                    val associated = associations.getOrPut(tag) { mutableSetOf() }
                    associated.addAll(tags.filter { it != tag })
                }
            }
            associations.mapValues { it.value.sorted() }
        }

        var selectedIds by remember { mutableStateOf<Set<Int>>(emptySet()) }
        val selectionCount = selectedIds.size
        val singleSelected = remember(selectedIds, shimListState.value) {
            if (selectionCount == 1) shimListState.value.firstOrNull { it.id == selectedIds.first() } else null
        }
        val clipboardManager = LocalClipboardManager.current

        DisposableEffect(lifecycleOwner) {
            val observer = LifecycleEventObserver { _, event ->
                if (event == Lifecycle.Event.ON_RESUME) {
                    isSearchExpanded = false
                    searchQuery = ""
                    selectedIds = emptySet()
                }
            }
            lifecycleOwner.lifecycle.addObserver(observer)
            onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
        }

        val exportLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/x-sqlite3")) { uri ->
            uri?.let {
                val dbFile = getDatabasePath("shim.db")
                contentResolver.openOutputStream(it)?.use { output ->
                    dbFile.inputStream().use { input ->
                        input.copyTo(output)
                    }
                }
            }
        }
        
        val csvExportLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/csv")) { uri ->
            uri?.let {
                db.exportCsv(this@MainActivity, it)
            }
        }
        val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
            uri?.let {
                db.importDatabase(this@MainActivity, it)
                refreshList()
            }
        }

        Box(modifier = Modifier.fillMaxSize()) {
            Scaffold(
            topBar = {
                Column {
                    TopAppBar(
                        title = {
                            Box(Modifier.fillMaxWidth().padding(end = 16.dp), contentAlignment = Alignment.CenterEnd) {
                                Text("shim", fontSize = 18.sp, color = colorResource(id = R.color.title_color))
                            }
                        },
                        colors = TopAppBarDefaults.topAppBarColors(containerColor = colorResource(id = R.color.primary_color)),
                        actions = {
                            if (selectedIds.isEmpty()) {
                                IconButton(onClick = {
                                    isSearchExpanded = false
                                    searchQuery = ""
                                    refreshList()
                                }) {
                                    Icon(Icons.Default.Add, contentDescription = "Add", tint = colorResource(id = R.color.title_color))
                                }
                                IconButton(onClick = { showTagsDialog = true }) {
                                    Icon(Icons.Default.Tag, contentDescription = "Tags", tint = colorResource(id = R.color.title_color))
                                }
                            } else {
                                val singleEnabled = selectionCount == 1
                                val singleTint = colorResource(id = R.color.title_color)
                                    .copy(alpha = if (singleEnabled) 1f else 0.3f)
                                IconButton(
                                    enabled = singleEnabled,
                                    onClick = {
                                        singleSelected?.let { clipboardManager.setText(AnnotatedString(it.shimtext)) }
                                        selectedIds = emptySet()
                                    }
                                ) {
                                    Icon(Icons.Default.ContentCopy, contentDescription = "Copy", tint = singleTint)
                                }
                                IconButton(
                                    enabled = singleEnabled,
                                    onClick = {
                                        singleSelected?.let { shim ->
                                            clipboardManager.setText(AnnotatedString(shim.shimtext))
                                            val intent = Intent(Intent.ACTION_SEND).apply {
                                                type = "text/plain"
                                                putExtra(Intent.EXTRA_TEXT, shim.shimtext)
                                            }
                                            startActivity(Intent.createChooser(intent, "Share shim"))
                                        }
                                        selectedIds = emptySet()
                                    }
                                ) {
                                    Icon(Icons.Default.Share, contentDescription = "Share", tint = singleTint)
                                }
                                IconButton(onClick = {
                                    selectedIds.forEach { db.deleteShim(it) }
                                    refreshList()
                                    selectedIds = emptySet()
                                }) {
                                    Icon(Icons.Outlined.Delete, contentDescription = "Delete", tint = colorResource(id = R.color.title_color))
                                }
                            }
                            Box {
                                var expanded by remember { mutableStateOf(false) }
                                IconButton(onClick = { expanded = true }) {
                                    Icon(Icons.Default.MoreVert, contentDescription = "More", tint = colorResource(id = R.color.title_color))
                                }
                                DropdownMenu(
                                    expanded = expanded,
                                    onDismissRequest = { expanded = false },
                                    modifier = Modifier.background(colorResource(id = R.color.menu_bg_color)).padding(horizontal = 6.dp)
                                ) {
                                    DropdownMenuItem(
                                        text = { Text("Import", color = colorResource(id = R.color.menu_color)) },
                                        onClick = {
                                            importLauncher.launch("*/*")
                                            expanded = false
                                        },
                                        leadingIcon = { Icon(Icons.Outlined.FileOpen, null, tint = colorResource(id = R.color.menu_color)) }
                                    )
                                    DropdownMenuItem(
                                        text = { Text("Export", color = colorResource(id = R.color.menu_color)) },
                                        onClick = {
                                            showExportDialog = true
                                            expanded = false
                                        },
                                        leadingIcon = { Icon(Icons.AutoMirrored.Outlined.ExitToApp, null, tint = colorResource(id = R.color.menu_color)) }
                                    )
                                    HorizontalDivider(
                                        modifier = Modifier.padding(vertical = 8.dp),
                                        color = colorResource(id = R.color.hr_color).copy(0.4f),
                                        thickness = 1.dp
                                    )
                                    DropdownMenuItem(
                                        text = { Text("Data", color = colorResource(id = R.color.menu_color)) },
                                        onClick = {
                                            showDataDialog = true
                                            expanded = false
                                        },
                                        leadingIcon = { Icon(Icons.Outlined.DataArray, null, tint = colorResource(id = R.color.menu_color)) }
                                    )
                                    DropdownMenuItem(
                                        text = { Text("Settings", color = colorResource(id = R.color.menu_color)) },
                                        onClick = {
                                            startActivity(Intent(this@MainActivity, ShimSettings::class.java))
                                            expanded = false
                                        },
                                        leadingIcon = { Icon(Icons.Outlined.Settings, null, tint = colorResource(id = R.color.menu_color)) }
                                    )
                                    DropdownMenuItem(
                                        text = { Text("Version", color = colorResource(id = R.color.menu_color)) },
                                        onClick = {
                                            showVersionDialog = true
                                            expanded = false
                                        },
                                        leadingIcon = { Icon(Icons.Outlined.History, null, tint = colorResource(id = R.color.menu_color)) }
                                    )
                                }
                            }
                        }
                    )
                    if (isSearchExpanded) {
                        SearchField(
                            query = searchQuery,
                            onQueryChange = {
                                searchQuery = it
                                shimListState.value = if (it.isEmpty()) db.getAllShims() else db.searchShims(it)
                            },
                            onClose = {
                                isSearchExpanded = false
                                searchQuery = ""
                                refreshList()
                            }
                        )
                    }
                }
            }
            ) { padding ->
                LazyColumn(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(padding)
                        .background(themeManager.backgroundColor.value)
                ) {
                    item { Spacer(Modifier.height(0.dp)) }
                    items(shimListState.value) { shim ->
                    ShimItem(
                        shim = shim,
                        themeManager = themeManager,
                        isSelected = shim.id in selectedIds,
                        isSelectionMode = selectedIds.isNotEmpty(),
                        onHashtagClick = { tag ->
                            searchQuery = tag
                            isSearchExpanded = true
                            shimListState.value = db.searchShims(tag)
                        },
                        onLongClick = {
                            selectedIds = if (shim.id in selectedIds) selectedIds - shim.id else selectedIds + shim.id
                        },
                        onClick = {
                            if (selectedIds.isNotEmpty()) {
                                selectedIds = if (shim.id in selectedIds) selectedIds - shim.id else selectedIds + shim.id
                            }
                        }
                    )
                }
                }
            }

            if (!isSearchExpanded) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(Color.Black.copy(alpha = 0.8f))
                        .clickable { isSearchExpanded = true }
                )
                Box(
                    modifier = Modifier
                        .align(Alignment.BottomCenter)
                        .navigationBarsPadding()
                        .imePadding()
                ) {
                    InputBar(
                        shimText = shimText,
                        onShimTextChange = { shimText = it },
                        hashtagText = hashtagText,
                        allTags = allTags,
                        tagAssociations = tagAssociations,
                        onHashtagTextChange = { hashtagText = it },
                        onAdd = { text, tags ->
                            db.addShim(text, tags.text, OTCClock.getCurrentOTCTime())
                            shimText = ""
                            hashtagText = TextFieldValue("")
                            refreshList()
                        },
                        themeManager = themeManager
                    )
                }
            }

            if (showDataDialog) {
                DataDialog(
                    shims = shimListState.value,
                    onDismiss = { showDataDialog = false }
                )
            }

            if (showTagsDialog) {
                TagsDialog(
                    shims = shimListState.value,
                    onDismiss = { showTagsDialog = false },
                    onTagClick = { tag ->
                        searchQuery = tag
                        isSearchExpanded = true
                        shimListState.value = db.searchShims(tag)
                    }
                )
            }
            
            if (showVersionDialog) {
                VersionDialog(onDismiss = { showVersionDialog = false })
            }
            if (showExportDialog) {
                ExportDialog(
                    onDismiss = { showExportDialog = false },
                    onExportDb = { exportLauncher.launch("shim${OTCClock.getFileNameTimestamp()}.db") },
                    onExportCsv = { csvExportLauncher.launch("shim${OTCClock.getFileNameTimestamp()}.csv") }
                )
            }
        }
    }
}
Updates
Wedge - Android 126.026
Wedge - Linux 124.026
Shim - Android 117.026
Miter - 114.026
Kerf - Android 112.026
Dev
TVShow (227) 'CSA'
TVShow (228) 'APT'
TVProgram (83) 'BXT'
Miter Update(s)
Peen (Messaging)

Menu
Calendar
Project Tin (024/029)
Miter
RSS Feed
User Avatar
@vgmlr
=SUM(parts)