Right after refactoring
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
@@ -41,7 +40,6 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -74,130 +72,8 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
private val AppBackground = Color(0xFF000000)
|
||||
private val PanelPrimary = Color(0xFF121212)
|
||||
private val PanelSecondary = Color(0xFF1E1E1E)
|
||||
private val PanelTertiary = Color(0xFF262626)
|
||||
private val TerminalBackground = Color(0xFF050505)
|
||||
private val TextPrimary = Color(0xFFFFFFFF)
|
||||
private val TextSecondary = Color(0xFFE6E6E6)
|
||||
private val TextMuted = Color(0xFFBDBDBD)
|
||||
private val Accent = Color(0xFF00E5FF)
|
||||
private val Success = Color(0xFF00FF95)
|
||||
|
||||
private val GitHugColorScheme = darkColorScheme(
|
||||
primary = Accent,
|
||||
onPrimary = Color.Black,
|
||||
secondary = TextPrimary,
|
||||
onSecondary = Color.Black,
|
||||
background = AppBackground,
|
||||
onBackground = TextPrimary,
|
||||
surface = PanelPrimary,
|
||||
onSurface = TextPrimary,
|
||||
surfaceVariant = PanelSecondary,
|
||||
onSurfaceVariant = TextSecondary,
|
||||
outline = TextMuted,
|
||||
)
|
||||
|
||||
private enum class PaneId(val title: String) {
|
||||
LEVELS("Levels"),
|
||||
VISUAL("Visual"),
|
||||
EXERCISE("Exercise"),
|
||||
TERMINAL("Terminal"),
|
||||
}
|
||||
|
||||
private data class PaneLayout(
|
||||
val order: List<PaneId>,
|
||||
val collapsed: Set<PaneId>,
|
||||
val weights: Map<PaneId, Float>,
|
||||
)
|
||||
|
||||
private enum class ExerciseDetailPanel {
|
||||
HINT,
|
||||
SUGGESTIONS,
|
||||
}
|
||||
|
||||
private fun defaultPaneLayout(): PaneLayout = PaneLayout(
|
||||
order = listOf(PaneId.EXERCISE, PaneId.TERMINAL, PaneId.VISUAL, PaneId.LEVELS),
|
||||
collapsed = emptySet(),
|
||||
weights = mapOf(
|
||||
PaneId.EXERCISE to 0.95f,
|
||||
PaneId.TERMINAL to 1.45f,
|
||||
PaneId.VISUAL to 1.0f,
|
||||
PaneId.LEVELS to 0.8f,
|
||||
),
|
||||
)
|
||||
|
||||
private class PaneLayoutStore(private val context: Context) {
|
||||
private val dataStore = PreferenceDataStoreFactory.create(
|
||||
produceFile = { context.preferencesDataStoreFile("pane_layout_preferences") }
|
||||
)
|
||||
|
||||
private val orderKey = stringPreferencesKey("pane_order")
|
||||
private val collapsedKey = stringPreferencesKey("pane_collapsed")
|
||||
private val weightsKey = stringPreferencesKey("pane_weights")
|
||||
|
||||
val layoutFlow = dataStore.data
|
||||
.catch { error ->
|
||||
if (error is IOException) emit(emptyPreferences()) else throw error
|
||||
}
|
||||
.map { preferences ->
|
||||
val defaults = defaultPaneLayout()
|
||||
val order = preferences[orderKey]
|
||||
?.split(',')
|
||||
?.mapNotNull { value -> PaneId.entries.firstOrNull { it.name == value } }
|
||||
?.let { parsed ->
|
||||
val missing = PaneId.entries.filterNot { it in parsed }
|
||||
parsed + missing
|
||||
}
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: defaults.order
|
||||
|
||||
val collapsed = preferences[collapsedKey]
|
||||
?.split(',')
|
||||
?.mapNotNull { value -> PaneId.entries.firstOrNull { it.name == value } }
|
||||
?.toSet()
|
||||
?: emptySet()
|
||||
|
||||
val parsedWeights = preferences[weightsKey]
|
||||
?.split(';')
|
||||
?.mapNotNull { item ->
|
||||
val parts = item.split(':', limit = 2)
|
||||
val paneId = PaneId.entries.firstOrNull { it.name == parts.getOrNull(0) }
|
||||
val weight = parts.getOrNull(1)?.toFloatOrNull()
|
||||
if (paneId != null && weight != null) paneId to weight.coerceAtLeast(0.6f) else null
|
||||
}
|
||||
?.toMap()
|
||||
.orEmpty()
|
||||
|
||||
PaneLayout(
|
||||
order = order,
|
||||
collapsed = collapsed,
|
||||
weights = PaneId.entries.associateWith { pane -> parsedWeights[pane] ?: defaults.weights.getValue(pane) },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun save(layout: PaneLayout) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[orderKey] = layout.order.joinToString(",") { it.name }
|
||||
preferences[collapsedKey] = layout.collapsed.joinToString(",") { it.name }
|
||||
preferences[weightsKey] = layout.order.joinToString(";") { pane ->
|
||||
"${pane.name}:${layout.weights[pane] ?: 1f}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GitHugApp() {
|
||||
@@ -1174,94 +1050,3 @@ private fun TerminalKeyButton(
|
||||
}
|
||||
}
|
||||
|
||||
private fun movePane(layout: PaneLayout, paneId: PaneId, delta: Int): PaneLayout {
|
||||
val currentIndex = layout.order.indexOf(paneId)
|
||||
if (currentIndex == -1) return layout
|
||||
val targetIndex = (currentIndex + delta).coerceIn(0, layout.order.lastIndex)
|
||||
if (currentIndex == targetIndex) return layout
|
||||
|
||||
val updatedOrder = layout.order.toMutableList()
|
||||
updatedOrder.removeAt(currentIndex)
|
||||
updatedOrder.add(targetIndex, paneId)
|
||||
return layout.copy(order = updatedOrder)
|
||||
}
|
||||
|
||||
private fun resizePanes(
|
||||
layout: PaneLayout,
|
||||
upper: PaneId,
|
||||
lower: PaneId,
|
||||
dragFraction: Float,
|
||||
): PaneLayout {
|
||||
if (upper in layout.collapsed || lower in layout.collapsed) return layout
|
||||
|
||||
val minWeight = 0.6f
|
||||
val upperWeight = layout.weights[upper] ?: 1f
|
||||
val lowerWeight = layout.weights[lower] ?: 1f
|
||||
val scaledDelta = dragFraction.coerceIn(-0.75f, 0.75f)
|
||||
|
||||
val newUpper = (upperWeight + scaledDelta).coerceAtLeast(minWeight)
|
||||
val newLower = (lowerWeight - scaledDelta).coerceAtLeast(minWeight)
|
||||
|
||||
return layout.copy(
|
||||
weights = layout.weights + mapOf(
|
||||
upper to newUpper,
|
||||
lower to newLower,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun recommendedPaneHeights(
|
||||
level: Level,
|
||||
levelCount: Int,
|
||||
outputLineCount: Int,
|
||||
screenHeightDp: Int,
|
||||
suggestionsVisible: Boolean,
|
||||
visibleHint: String?,
|
||||
): Map<PaneId, Int> {
|
||||
val screenHeight = screenHeightDp.coerceAtLeast(560)
|
||||
val levelsHeight = (56 + levelCount * 44).coerceAtLeast(160)
|
||||
val descriptionLines = (level.description.length / 42).coerceAtLeast(2) + 2
|
||||
val suggestionsLines = if (suggestionsVisible) level.commandSuggestions.size + 1 else 0
|
||||
val hintLines = visibleHint?.let { (it.length / 42).coerceAtLeast(1) + 1 } ?: 0
|
||||
val exerciseHeight = (120 + descriptionLines * 24 + suggestionsLines * 22 + hintLines * 22).coerceAtLeast(180)
|
||||
val terminalHeight = (170 + outputLineCount.coerceAtLeast(1) * 20).coerceAtMost((screenHeight * 0.7f).toInt())
|
||||
val visualHeight = (screenHeight * 0.24f).toInt().coerceAtLeast(170)
|
||||
|
||||
return mapOf(
|
||||
PaneId.EXERCISE to exerciseHeight,
|
||||
PaneId.TERMINAL to terminalHeight,
|
||||
PaneId.VISUAL to visualHeight,
|
||||
PaneId.LEVELS to levelsHeight,
|
||||
)
|
||||
}
|
||||
|
||||
private fun recommendedPaneWeights(
|
||||
heights: Map<PaneId, Int>,
|
||||
): Map<PaneId, Float> {
|
||||
fun weightFor(height: Int): Float = (height / 180f).coerceAtLeast(0.6f)
|
||||
|
||||
return heights.mapValues { (_, height) -> weightFor(height) }
|
||||
}
|
||||
|
||||
private fun recommendedWorkspaceHeight(
|
||||
paneLayout: PaneLayout,
|
||||
recommendedHeights: Map<PaneId, Int>,
|
||||
): androidx.compose.ui.unit.Dp {
|
||||
val dividerHeight = 18 * (paneLayout.order.size - 1)
|
||||
val collapsedPaneHeight = 44 * paneLayout.collapsed.size
|
||||
val expandedHeight = paneLayout.order
|
||||
.filterNot { it in paneLayout.collapsed }
|
||||
.sumOf { pane -> recommendedHeights[pane] ?: 0 }
|
||||
return (expandedHeight + dividerHeight + collapsedPaneHeight).dp
|
||||
}
|
||||
|
||||
private fun commonPrefix(values: List<String>): String {
|
||||
if (values.isEmpty()) return ""
|
||||
var prefix = values.first()
|
||||
values.drop(1).forEach { value ->
|
||||
while (!value.startsWith(prefix) && prefix.isNotEmpty()) {
|
||||
prefix = prefix.dropLast(1)
|
||||
}
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
29
app/src/main/java/com/kawomi/githugandroid/GitHugTheme.kt
Normal file
29
app/src/main/java/com/kawomi/githugandroid/GitHugTheme.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val AppBackground = Color(0xFF000000)
|
||||
val PanelPrimary = Color(0xFF121212)
|
||||
val PanelSecondary = Color(0xFF1E1E1E)
|
||||
val PanelTertiary = Color(0xFF262626)
|
||||
val TerminalBackground = Color(0xFF050505)
|
||||
val TextPrimary = Color(0xFFFFFFFF)
|
||||
val TextSecondary = Color(0xFFE6E6E6)
|
||||
val TextMuted = Color(0xFFBDBDBD)
|
||||
val Accent = Color(0xFF00E5FF)
|
||||
val Success = Color(0xFF00FF95)
|
||||
|
||||
val GitHugColorScheme = darkColorScheme(
|
||||
primary = Accent,
|
||||
onPrimary = Color.Black,
|
||||
secondary = TextPrimary,
|
||||
onSecondary = Color.Black,
|
||||
background = AppBackground,
|
||||
onBackground = TextPrimary,
|
||||
surface = PanelPrimary,
|
||||
onSurface = TextPrimary,
|
||||
surfaceVariant = PanelSecondary,
|
||||
onSurfaceVariant = TextSecondary,
|
||||
outline = TextMuted,
|
||||
)
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun movePane(layout: PaneLayout, paneId: PaneId, delta: Int): PaneLayout {
|
||||
val currentIndex = layout.order.indexOf(paneId)
|
||||
if (currentIndex == -1) return layout
|
||||
val targetIndex = (currentIndex + delta).coerceIn(0, layout.order.lastIndex)
|
||||
if (currentIndex == targetIndex) return layout
|
||||
|
||||
val updatedOrder = layout.order.toMutableList()
|
||||
updatedOrder.removeAt(currentIndex)
|
||||
updatedOrder.add(targetIndex, paneId)
|
||||
return layout.copy(order = updatedOrder)
|
||||
}
|
||||
|
||||
fun resizePanes(
|
||||
layout: PaneLayout,
|
||||
upper: PaneId,
|
||||
lower: PaneId,
|
||||
dragFraction: Float,
|
||||
): PaneLayout {
|
||||
if (upper in layout.collapsed || lower in layout.collapsed) return layout
|
||||
|
||||
val minWeight = 0.6f
|
||||
val upperWeight = layout.weights[upper] ?: 1f
|
||||
val lowerWeight = layout.weights[lower] ?: 1f
|
||||
val scaledDelta = dragFraction.coerceIn(-0.75f, 0.75f)
|
||||
|
||||
val newUpper = (upperWeight + scaledDelta).coerceAtLeast(minWeight)
|
||||
val newLower = (lowerWeight - scaledDelta).coerceAtLeast(minWeight)
|
||||
|
||||
return layout.copy(
|
||||
weights = layout.weights + mapOf(
|
||||
upper to newUpper,
|
||||
lower to newLower,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun recommendedPaneHeights(
|
||||
level: Level,
|
||||
levelCount: Int,
|
||||
outputLineCount: Int,
|
||||
screenHeightDp: Int,
|
||||
suggestionsVisible: Boolean,
|
||||
visibleHint: String?,
|
||||
): Map<PaneId, Int> {
|
||||
val screenHeight = screenHeightDp.coerceAtLeast(560)
|
||||
val levelsHeight = (56 + levelCount * 44).coerceAtLeast(160)
|
||||
val descriptionLines = (level.description.length / 42).coerceAtLeast(2) + 2
|
||||
val suggestionsLines = if (suggestionsVisible) level.commandSuggestions.size + 1 else 0
|
||||
val hintLines = visibleHint?.let { (it.length / 42).coerceAtLeast(1) + 1 } ?: 0
|
||||
val exerciseHeight = (120 + descriptionLines * 24 + suggestionsLines * 22 + hintLines * 22).coerceAtLeast(180)
|
||||
val terminalHeight = (170 + outputLineCount.coerceAtLeast(1) * 20).coerceAtMost((screenHeight * 0.7f).toInt())
|
||||
val visualHeight = (screenHeight * 0.24f).toInt().coerceAtLeast(170)
|
||||
|
||||
return mapOf(
|
||||
PaneId.EXERCISE to exerciseHeight,
|
||||
PaneId.TERMINAL to terminalHeight,
|
||||
PaneId.VISUAL to visualHeight,
|
||||
PaneId.LEVELS to levelsHeight,
|
||||
)
|
||||
}
|
||||
|
||||
fun recommendedPaneWeights(
|
||||
heights: Map<PaneId, Int>,
|
||||
): Map<PaneId, Float> {
|
||||
fun weightFor(height: Int): Float = (height / 180f).coerceAtLeast(0.6f)
|
||||
|
||||
return heights.mapValues { (_, height) -> weightFor(height) }
|
||||
}
|
||||
|
||||
fun recommendedWorkspaceHeight(
|
||||
paneLayout: PaneLayout,
|
||||
recommendedHeights: Map<PaneId, Int>,
|
||||
): Dp {
|
||||
val dividerHeight = 18 * (paneLayout.order.size - 1)
|
||||
val collapsedPaneHeight = 44 * paneLayout.collapsed.size
|
||||
val expandedHeight = paneLayout.order
|
||||
.filterNot { it in paneLayout.collapsed }
|
||||
.sumOf { pane -> recommendedHeights[pane] ?: 0 }
|
||||
return (expandedHeight + dividerHeight + collapsedPaneHeight).dp
|
||||
}
|
||||
|
||||
fun commonPrefix(values: List<String>): String {
|
||||
if (values.isEmpty()) return ""
|
||||
var prefix = values.first()
|
||||
values.drop(1).forEach { value ->
|
||||
while (!value.startsWith(prefix) && prefix.isNotEmpty()) {
|
||||
prefix = prefix.dropLast(1)
|
||||
}
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
enum class PaneId(val title: String) {
|
||||
LEVELS("Levels"),
|
||||
VISUAL("Visual"),
|
||||
EXERCISE("Exercise"),
|
||||
TERMINAL("Terminal"),
|
||||
}
|
||||
|
||||
data class PaneLayout(
|
||||
val order: List<PaneId>,
|
||||
val collapsed: Set<PaneId>,
|
||||
val weights: Map<PaneId, Float>,
|
||||
)
|
||||
|
||||
enum class ExerciseDetailPanel {
|
||||
HINT,
|
||||
SUGGESTIONS,
|
||||
}
|
||||
|
||||
fun defaultPaneLayout(): PaneLayout = PaneLayout(
|
||||
order = listOf(PaneId.EXERCISE, PaneId.TERMINAL, PaneId.VISUAL, PaneId.LEVELS),
|
||||
collapsed = emptySet(),
|
||||
weights = mapOf(
|
||||
PaneId.EXERCISE to 0.95f,
|
||||
PaneId.TERMINAL to 1.45f,
|
||||
PaneId.VISUAL to 1.0f,
|
||||
PaneId.LEVELS to 0.8f,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.kawomi.githugandroid
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.IOException
|
||||
|
||||
class PaneLayoutStore(private val context: Context) {
|
||||
private val dataStore = PreferenceDataStoreFactory.create(
|
||||
produceFile = { context.preferencesDataStoreFile("pane_layout_preferences") }
|
||||
)
|
||||
|
||||
private val orderKey = stringPreferencesKey("pane_order")
|
||||
private val collapsedKey = stringPreferencesKey("pane_collapsed")
|
||||
private val weightsKey = stringPreferencesKey("pane_weights")
|
||||
|
||||
val layoutFlow = dataStore.data
|
||||
.catch { error ->
|
||||
if (error is IOException) emit(emptyPreferences()) else throw error
|
||||
}
|
||||
.map { preferences ->
|
||||
val defaults = defaultPaneLayout()
|
||||
val order = preferences[orderKey]
|
||||
?.split(',')
|
||||
?.mapNotNull { value -> PaneId.entries.firstOrNull { it.name == value } }
|
||||
?.let { parsed ->
|
||||
val missing = PaneId.entries.filterNot { it in parsed }
|
||||
parsed + missing
|
||||
}
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: defaults.order
|
||||
|
||||
val collapsed = preferences[collapsedKey]
|
||||
?.split(',')
|
||||
?.mapNotNull { value -> PaneId.entries.firstOrNull { it.name == value } }
|
||||
?.toSet()
|
||||
?: emptySet()
|
||||
|
||||
val parsedWeights = preferences[weightsKey]
|
||||
?.split(';')
|
||||
?.mapNotNull { item ->
|
||||
val parts = item.split(':', limit = 2)
|
||||
val paneId = PaneId.entries.firstOrNull { it.name == parts.getOrNull(0) }
|
||||
val weight = parts.getOrNull(1)?.toFloatOrNull()
|
||||
if (paneId != null && weight != null) paneId to weight.coerceAtLeast(0.6f) else null
|
||||
}
|
||||
?.toMap()
|
||||
.orEmpty()
|
||||
|
||||
PaneLayout(
|
||||
order = order,
|
||||
collapsed = collapsed,
|
||||
weights = PaneId.entries.associateWith { pane -> parsedWeights[pane] ?: defaults.weights.getValue(pane) },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun save(layout: PaneLayout) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[orderKey] = layout.order.joinToString(",") { it.name }
|
||||
preferences[collapsedKey] = layout.collapsed.joinToString(",") { it.name }
|
||||
preferences[weightsKey] = layout.order.joinToString(";") { pane ->
|
||||
"${pane.name}:${layout.weights[pane] ?: 1f}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user