Skip to content

Commit

Permalink
Merge pull request #17 from matejsemancik/housekeep/reformat-code
Browse files Browse the repository at this point in the history
Fix formatting issues
  • Loading branch information
matejsemancik authored Mar 1, 2025
2 parents 152d25f + b90fbf4 commit 12672df
Show file tree
Hide file tree
Showing 56 changed files with 120 additions and 99 deletions.
2 changes: 1 addition & 1 deletion .fleet/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"fuzzyResolve.enabled": false
"fuzzyResolve.enabled": false
}
2 changes: 1 addition & 1 deletion .github/workflows/on_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
timeout-minutes: 15 # Windows is such a special snowflake 🙂
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
permissions:
contents: write
Expand Down
31 changes: 18 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
# Development Guidelines

## Build Commands

- Build package: `./gradlew packageDistributionForCurrentOS`
- Run app: `./gradlew runDistributable`
- Run desktop app: `./gradlew :desktopApp:run`

## Code Style

- **Naming**: Classes = PascalCase, Functions/Variables = camelCase
- **Architecture**: MVVM pattern with UI, State (Model), and Data layers
- **State Management**: Kotlin Flows (StateFlow for UI state)
- **Visibility Modifiers**:
- Use the most restrictive visibility modifier possible
- Prefer `internal` over `public` for implementation classes
- Use `private` for all properties and functions that don't need wider visibility
- Use `public` only for APIs that need to be accessed outside the module
- Use the most restrictive visibility modifier possible
- Prefer `internal` over `public` for implementation classes
- Use `private` for all properties and functions that don't need wider visibility
- Use `public` only for APIs that need to be accessed outside the module
- **DI**: Koin for dependency injection
- Use constructor injection whenever possible instead of property injection
- Constructor parameters make dependencies explicit and improve testability
- Use constructor injection whenever possible instead of property injection
- Constructor parameters make dependencies explicit and improve testability
- **Imports**: Grouped by purpose, explicit imports preferred over wildcards
- **Error Handling**: Repository pattern for data operations, proper error propagation through Flows
- **Expressive Kotlin Features**:
- Use `runCatching` instead of try-catch blocks
- Use `.use` extension for closeables
- Prefer `kotlin.io` extensions over Java IO streams
- Use scope functions (`let`, `apply`, `run`, `with`, `also`) appropriately
- Leverage extension functions for better readability
- Use property delegates (`by lazy`, `by Delegates`) where appropriate
- Use Elvis operator (`?:`) for fallback values
- Use `runCatching` instead of try-catch blocks
- Use `.use` extension for closeables
- Prefer `kotlin.io` extensions over Java IO streams
- Use scope functions (`let`, `apply`, `run`, `with`, `also`) appropriately
- Leverage extension functions for better readability
- Use property delegates (`by lazy`, `by Delegates`) where appropriate
- Use Elvis operator (`?:`) for fallback values

## Architecture Guidelines

- Network models (from APIs) should not be propagated to the presentation layer
- Always map network models to domain models in the repository layer
- Service layer handles API communication, Repository layer handles domain translation
Expand Down Expand Up @@ -68,12 +71,14 @@
```

## Project Structure

- `feature/` - App features organized by domain (tracker, search, settings)
- `data/` - Repository interfaces, database models, network services
- `design/` - UI components, theme definitions
- `arch/` - Base architectural components
- `injection/` - Dependency injection modules

## Testing

- Unit tests should follow the same structure as the feature they're testing
- Model tests should verify state transitions and event handling
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,48 @@

**Track your time effortlessly.**

tempo-timer is a simple desktop app designed for logging time on Jira issues directly into Tempo Timesheets. It’s perfect for users who prefer real-time tracking (like me!) and find existing web UI and/or browser extension very slow or clunky (also, like me!). Currently, tracking is limited to "today"—for other dates, use the Tempo web interface.
tempo-timer is a simple desktop app designed for logging time on Jira issues directly into Tempo Timesheets. It’s
perfect for users who prefer real-time tracking (like me!) and find existing web UI and/or browser extension very slow
or clunky (also, like me!). Currently, tracking is limited to "today"—for other dates, use the Tempo web interface.

The app is tested on macOS. It supports Jira Cloud only.

## Features

-**Track time** directly in Tempo Timesheets
-**Edit worklogs** before submitting (time & description)
- 🔍 **Search Jira issues** quickly
-**Save favorite issues** for fast access
- 🎛 **Manage multiple timers** simultaneously
- 🌙 **Dark mode support**

Interested in Kotlin Multiplatform? Learn more [here](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html).
Interested in Kotlin Multiplatform? Learn
more [here](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html).

## Download

[![GitHub Release](https://img.shields.io/github/v/release/matejsemancik/tempo-timer?include_prereleases&sort=semver&display_name=release&style=flat&link=https%3A%2F%2Fgithub.com%2Fmatejsemancik%2Ftempo-timer%2Freleases%2Flatest)](https://github.com/matejsemancik/tempo-timer/releases/latest)

Download latest release [here](https://github.com/matejsemancik/tempo-timer/releases/latest).

### macOS-specific

The app is not signed and notarized on macOS and probably will never be (I refuse to pay Apple Developer fee for this), so system will greet you with this lovely dialog after first opening the app:
The app is not signed and notarized on macOS and probably will never be (I refuse to pay Apple Developer fee for this),
so system will greet you with this lovely dialog after first opening the app:

![](docs/gatekeeper.png)

Considering you inspected source code and you trust it, you can

- manually run `xattr -d -r com.apple.quarantine <path_to_Tempo Timer.app>`, or
- navigate to `System Settings -> Privacy & Security` and allow the app to run, or
- build it from source code 👇 - apps built locally do not trigger Gatekeeper

## Getting started

1. Create a Jira API token → [Generate here](https://id.atlassian.com/manage-profile/security/api-tokens)
2. Create a Tempo API token → Follow [these steps](https://apidocs.tempo.io/#section/Authentication) (Using the REST API as an individual user). Token must have permissions to access Worklogs.
2. Create a Tempo API token → Follow [these steps](https://apidocs.tempo.io/#section/Authentication) (Using the REST API
as an individual user). Token must have permissions to access Worklogs.
3. Log in: Open the app -> Settings and enter your Jira cloud instance name, Jira account email, and both tokens.

That’s it!
Expand Down
1 change: 0 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx2048M

#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ markdownRenderer = { module = "com.mikepenz:multiplatform-markdown-renderer-m3",
[plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hotReload"}
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hotReload" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
Expand Down
18 changes: 11 additions & 7 deletions shared/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<string name="close">Close</string>
<string name="delete">Delete</string>
<string name="remove">Remove</string>

<!-- App -->
<string name="app_name">tempo-timer</string>
<string name="toggle_dark_mode">Toggle dark mode</string>
Expand All @@ -13,32 +13,36 @@
<string name="pick_issue">Pick an issue</string>
<string name="timer">Timer</string>
<string name="new_version_banner_md">✨ New version available! [Download on GitHub](%1$s)</string>

<!-- Commit/Timer -->
<string name="duration_label">⏳ Duration</string>
<string name="duration_placeholder">e.g. \"1h 20m\"</string>
<string name="description_label">📜 Description</string>
<string name="delete_timer">Delete timer</string>
<string name="log_time">Log Time</string>

<!-- Settings -->
<string name="credentials_title">🔐 Credentials</string>
<string name="credentials_description">Sign In by providing necessary credentials.</string>
<string name="credentials_instructions_md">Jira API token is used to sync your profile and search issues. Tempo API token is used to synchronize worklogs with Tempo. [Instructions here](https://github.com/matejsemancik/tempo-timer?tab=readme-ov-file#getting-started).</string>
<string name="credentials_instructions_md">Jira API token is used to sync your profile and search issues. Tempo API
token is used to synchronize worklogs with Tempo. [Instructions
here](https://github.com/matejsemancik/tempo-timer?tab=readme-ov-file#getting-started).
</string>
<string name="jira_url">Jira URL</string>
<string name="jira_email">Atlassian account e-mail</string>
<string name="jira_email_placeholder">[email protected]</string>
<string name="jira_api_token">Jira API token</string>
<string name="tempo_api_token">Tempo API token</string>
<string name="sign_in">Sign In</string>
<string name="sign_out">Sign Out</string>

<!-- Search -->
<string name="search_placeholder">Issue key, or summary, or try your luck...</string>

<!-- Tracker -->
<string name="favorites_section">⭐ Favourites</string>
<string name="timers_section">⏳ Timers</string>
<string name="no_timers">No running timers</string>
<string name="start_timer_instructions">Start a new timer by clicking on Favourite,\nor from menu bar down there 👇</string>
<string name="start_timer_instructions">Start a new timer by clicking on Favourite,\nor from menu bar down there 👇
</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ data class TimerComplete(
parentColumn = "jira_issue_id",
entityColumn = "id"
)
val issue: JiraIssue
val issue: JiraIssue,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface ApplicationPersistence {
}

internal class ApplicationPersistenceImpl(
private val handler: JsonPersistenceHandler
private val handler: JsonPersistenceHandler,
) : ApplicationPersistence {

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ data class Credentials(
val jiraApiToken: String,

@SerialName("tempo_token")
val tempoApiToken: String
val tempoApiToken: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ interface GitHubRepo {
}

class GitHubRepoImpl(
private val gitHubApiManager: GitHubApiManager
private val gitHubApiManager: GitHubApiManager,
) : GitHubRepo {

override suspend fun getLatestAppVersion(): AppVersion {
return gitHubApiManager.getLatestRelease().toAppVersion()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ interface TimerRepo {

internal class TimerRepoImpl(
private val timerDao: TimerDao,
private val clock: Clock
private val clock: Clock,
) : TimerRepo {

override fun getTimers(): Flow<List<Timer>> =
timerDao.getTimers().map { timerList -> timerList.map { dbTimber -> dbTimber.toDomainModel() } }

override suspend fun createTimerForIssue(issue: Issue) {
val newTimer = Timer_Database(jiraIssueId = issue.id, accumulationMs = 0L, lastStartedAt = clock.now(), createdAt = clock.now())
val newTimer = Timer_Database(
jiraIssueId = issue.id,
accumulationMs = 0L,
lastStartedAt = clock.now(),
createdAt = clock.now()
)
timerDao.addOrUpdateTimer(newTimer, issue.toDbModel())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ data class Credentials(
val jiraDomain: String,
val email: String,
val jiraApiToken: String,
val tempoApiToken: String
val tempoApiToken: String,
) {
val jiraApiUrl: String
get() = Constants.JiraApiUrl(jiraDomain)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import kotlin.random.Random

data class SearchResult(
val issue: Issue,
val isFavourite: Boolean
val isFavourite: Boolean,
)

val MockSearchResults = MockIssues.map { SearchResult(it, isFavourite = Random.nextBoolean()) }
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ data class Timer(
val id: Int,
val issue: Issue,
val createdAt: Instant,
val state: TimerState = TimerState()
val state: TimerState = TimerState(),
)

/**
Expand All @@ -20,7 +20,7 @@ data class Timer(
*/
data class TimerState(
val finishedDuration: Duration = 0.seconds,
val lastStartedAt: Instant? = null
val lastStartedAt: Instant? = null,
) {
val isRunning: Boolean
get() = lastStartedAt != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ data class User(
val accountId: String,
val email: String,
val displayName: String,
val avatarUrl: String
val avatarUrl: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import de.jensklingenberg.ktorfit.http.GET
import dev.matsem.bpm.data.service.github.model.Release

interface GitHubApi {

@GET("repos/matejsemancik/tempo-timer/releases/latest")
suspend fun getLatestRelease(): Release

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ interface JiraApi {
@Query("query") query: String,
@Query("currentJQL") currentJql: String,
@Query("showSubTasks") showSubTasks: Boolean,
@Query("showSubTaskParents") showSubTaskParents: Boolean
@Query("showSubTaskParents") showSubTaskParents: Boolean,
): IssuePickerResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface JiraApiManager {
query: String,
currentJql: String,
showSubTasks: Boolean,
showSubTaskParents: Boolean
showSubTaskParents: Boolean,
): IssuePickerResponse
}

Expand All @@ -30,7 +30,7 @@ internal class JiraApiManagerImpl(
query: String,
currentJql: String,
showSubTasks: Boolean,
showSubTaskParents: Boolean
showSubTaskParents: Boolean,
): IssuePickerResponse = sessionScoped { api ->
api.searchIssues(query, currentJql, showSubTasks, showSubTaskParents)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable
data class IssuePickerResponse(

@SerialName("sections")
val sections: List<IssuePickerSection>
val sections: List<IssuePickerSection>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ data class IssuePickerSection(
val sub: String? = null,

@SerialName("issues")
val issues: List<SuggestedIssue>
val issues: List<SuggestedIssue>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ data class SuggestedIssue(
val key: String,

@SerialName("summaryText")
val summary: String
val summary: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import kotlinx.serialization.Serializable
data class AvatarUrlsBean(

@SerialName("48x48")
val largestUrl: String
val largestUrl: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ data class JiraUser(
val avatarUrls: AvatarUrlsBean,

@SerialName("displayName")
val displayName: String
val displayName: String,
)
Loading

0 comments on commit 12672df

Please sign in to comment.