Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/kotlin/com/gitvantage/app/GitHub.kt
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ object GitHub {
val reviewIsNewest = lastReview != null &&
lastReview.submittedAt.toMillis() >= lastComment?.createdAt.toMillis()
val reviewedMine = viewerDidAuthor && reviewIsNewest && !lastReviewBy.isLogin(viewer) &&
lastReviewBy.isNotEmpty() && lastReview?.reactionGroups.orEmpty().none { it.viewerHasReacted }
lastReviewBy.isNotEmpty() && lastReview.reactionGroups.orEmpty().none { it.viewerHasReacted }

val mentioned = lastComment != null && !acknowledged && mentionsMe(lastComment.bodyText, viewer)
// "Someone replied on my own thread and I haven't answered." Only counts when the last
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/gitvantage/git/WorktreeOps.kt
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ object WorktreeOps {
branchMerged = merged,
branchEmpty = contained && !merged,
tipBranch = tipBranch,
deletableUpstream = if (merged && own != null) BranchOps.deletableUpstream(dir, own) else null,
deletableUpstream = if (merged) BranchOps.deletableUpstream(dir, own) else null,
)
}
}
Expand Down
33 changes: 25 additions & 8 deletions src/main/kotlin/com/gitvantage/ui/Components.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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
Expand All @@ -50,8 +51,10 @@ import androidx.compose.ui.input.pointer.isMetaPressed
import androidx.compose.ui.input.pointer.isShiftPressed
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
Expand All @@ -65,12 +68,14 @@ import com.gitvantage.app.MonoFont
import com.gitvantage.app.Tokens
import com.gitvantage.app.UiFont
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
import androidx.compose.foundation.Canvas
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import kotlin.math.cos
import kotlin.math.sin
import java.awt.datatransfer.StringSelection

/** A 1px bottom hairline, like CSS `border-bottom`. */
fun Modifier.drawBottomBorder(color: Color): Modifier = this.drawBehind {
Expand Down Expand Up @@ -393,6 +398,15 @@ fun InfoTip(text: String, modifier: Modifier = Modifier) {
}
}

/**
* Put [text] on the system clipboard as plain text.
*
* [Clipboard] only takes a [ClipEntry], and on desktop the one way to build that is around an AWT
* `Transferable` — a constructor Compose still marks experimental, hence the opt-in.
*/
@OptIn(ExperimentalComposeUiApi::class)
suspend fun Clipboard.setText(text: String) = setClipEntry(ClipEntry(StringSelection(text)))

/**
* A pill that puts [value] on the clipboard and confirms it in place — the label swaps to
* "✓ Copied" for a moment. The confirmation is local rather than a toast because the toast
Expand All @@ -405,7 +419,8 @@ fun InfoTip(text: String, modifier: Modifier = Modifier) {
*/
@Composable
fun CopyPill(value: String, label: String = "Copy", modifier: Modifier = Modifier) {
val clipboard = LocalClipboardManager.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
var copied by remember(value) { mutableStateOf(false) }
LaunchedEffect(copied) { if (copied) { delay(1200.milliseconds); copied = false } }
val shape = RoundedCornerShape(6.dp)
Expand All @@ -414,7 +429,7 @@ fun CopyPill(value: String, label: String = "Copy", modifier: Modifier = Modifie
.background(if (copied) Tokens.tintGreen else Tokens.tintBlue, shape)
.border(1.dp, if (copied) Tokens.okBorder else Tokens.accentBorder, shape)
.pointerHoverIcon(PointerIcon.Hand)
.onTap { clipboard.setText(AnnotatedString(value)); copied = true }
.onTap { scope.launch { clipboard.setText(value); copied = true } }
.padding(horizontal = 9.dp, vertical = 2.dp),
contentAlignment = Alignment.Center,
) {
Expand Down Expand Up @@ -492,14 +507,15 @@ fun RowAction(label: String, danger: Boolean = false, onClick: () -> Unit) {
*/
@Composable
fun CopyAction(value: String, label: String = "Copy") {
val clipboard = LocalClipboardManager.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
var copied by remember(value) { mutableStateOf(false) }
LaunchedEffect(copied) { if (copied) { delay(1200.milliseconds); copied = false } }
Txt(
if (copied) "✓ Copied" else label, 11.5.sp,
if (copied) Tokens.addFg else Tokens.accent, FontWeight.Medium,
modifier = Modifier.pointerHoverIcon(PointerIcon.Hand)
.onTap { clipboard.setText(AnnotatedString(value)); copied = true },
.onTap { scope.launch { clipboard.setText(value); copied = true } },
)
}

Expand All @@ -524,13 +540,14 @@ fun CopyTarget(
modifier: Modifier = Modifier,
content: @Composable (copied: Boolean) -> Unit,
) {
val clipboard = LocalClipboardManager.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
var copied by remember(value) { mutableStateOf(false) }
LaunchedEffect(copied) { if (copied) { delay(1200.milliseconds); copied = false } }
HoverTip(
tip,
modifier.pointerHoverIcon(PointerIcon.Hand)
.onTap { clipboard.setText(AnnotatedString(value)); copied = true },
.onTap { scope.launch { clipboard.setText(value); copied = true } },
) { content(copied) }
}

Expand Down
23 changes: 20 additions & 3 deletions src/main/kotlin/com/gitvantage/ui/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toComposeImageBitmap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
Expand All @@ -56,6 +58,7 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlin.time.Duration.Companion.milliseconds
import org.jetbrains.skia.Image as SkiaImage

/**
* `--smoke-test` runs the subsystem checks in [runSmokeChecks] and exits, without opening a window.
Expand Down Expand Up @@ -97,6 +100,20 @@ private fun adoptContextClassLoader() {
}
}

/**
* The app icon, for both the window and the title bar.
*
* Decoded straight from the classpath rather than through Compose's `painterResource`, which is
* deprecated in favour of the Compose resources library — a generated `Res` class and a
* `composeResources` folder, for one PNG. Read through our own class, so it doesn't depend on the
* thread's context class loader either (see [adoptContextClassLoader]).
*/
private val appIcon: Painter by lazy {
val bytes = checkNotNull(AppState::class.java.getResourceAsStream("/app-icon.png")) { "app-icon.png missing" }
.use { it.readAllBytes() }
BitmapPainter(SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap())
}

@OptIn(FlowPreview::class)
private fun runApp() = nucleusApplication(backend = NucleusBackend.Tao) {
adoptContextClassLoader()
Expand Down Expand Up @@ -131,7 +148,7 @@ private fun runApp() = nucleusApplication(backend = NucleusBackend.Tao) {
state = windowState,
visible = windowVisible,
title = "GitVantage",
icon = painterResource("app-icon.png"), // taskbar / window icon (X11/Win/macOS; Wayland uses app_id)
icon = appIcon, // taskbar / window icon (X11/Win/macOS; Wayland uses app_id)
onPreviewKeyEvent = ::zoomShortcut,
) {
// Everything the window draws — the Nucleus-drawn title bar included — sizes itself
Expand All @@ -153,7 +170,7 @@ private fun runApp() = nucleusApplication(backend = NucleusBackend.Tao) {
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Image(
painter = painterResource("app-icon.png"),
painter = appIcon,
contentDescription = "GitVantage",
modifier = Modifier.size(22.dp).clip(RoundedCornerShape(6.dp)),
)
Expand Down
7 changes: 3 additions & 4 deletions src/main/kotlin/com/gitvantage/ui/Popups.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ import com.gitvantage.app.UiFont
import com.gitvantage.app.UiScale
import com.gitvantage.model.Meta
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import com.gitvantage.app.DiagLog
Expand Down Expand Up @@ -513,7 +512,7 @@ private fun DiagnosticsSection(state: AppState) {
*/
@Composable
private fun CopyReportPill(state: AppState) {
val clipboard = LocalClipboardManager.current
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
var copied by remember { mutableStateOf(false) }
LaunchedEffect(copied) { if (copied) { delay(1200.milliseconds); copied = false } }
Expand All @@ -525,7 +524,7 @@ private fun CopyReportPill(state: AppState) {
.pointerHoverIcon(PointerIcon.Hand)
.onTap {
scope.launch {
clipboard.setText(AnnotatedString(state.diagnosticsReport()))
clipboard.setText(state.diagnosticsReport())
copied = true
}
}
Expand Down
Loading