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
1 change: 1 addition & 0 deletions feature/screens/sessionAnalysis/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ moduleImpl {
projects.core.theme.jvmImpl
projects.core.ui.jvmImpl
projects.core.utils.jvmImpl
projects.feature.screens.chooser.jvmImpl
projects.games.telemetry.ac.api.jvmImpl

lib.metro.metrox.viewmodel.compose.jvmImpl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@
<string name="session_analysis_session_latest">Latest</string>
<string name="session_analysis_no_selection">--</string>
<string name="session_analysis_action_open">Analysis</string>
<string name="session_analysis_action_share">Share Analysis</string>
<string name="session_analysis_action_more">More analysis actions</string>
<string name="session_analysis_action_more_close">Close analysis actions</string>
<string name="session_analysis_lap_label">Lap %1$d</string>
<string name="session_analysis_header_refresh">Refresh</string>
<string name="session_analysis_header_meta">%1$s • %2$s</string>
Expand Down Expand Up @@ -470,4 +473,14 @@
</string>
<string name="session_analysis_coach_corner_prefix">Corner %1$d: %2$s</string>
<string name="session_analysis_coach_this_section">this section</string>
<string name="session_analysis_share_preview">Share preview</string>
<string name="session_analysis_share_formats">Choose how to share this analysis</string>
<string name="session_analysis_share_export_name">Export filename</string>
<string name="session_analysis_share_copy">Copy Chat Summary</string>
<string name="session_analysis_share_copy_description">Copies clean plain text to the clipboard for Discord, Telegram or chat.</string>
<string name="session_analysis_share_export">Save Markdown Report</string>
<string name="session_analysis_share_export_description">Writes a readable .md report to the folder you choose.</string>
<string name="session_analysis_share_raw_files">Open Source Session Files</string>
<string name="session_analysis_share_raw_files_description">Only if you need the original recorded folder, not the share-ready summary.</string>
<string name="session_analysis_share_close">Close</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.analyzer.session.analysis.di
import androidx.lifecycle.ViewModel
import com.analyzer.session.analysis.data.repository.SessionAnalysisRepositoryImpl
import com.analyzer.session.analysis.domain.repository.SessionAnalysisRepository
import com.analyzer.session.analysis.domain.usecase.SessionAnalysisShareResultsUseCase
import com.analyzer.session.analysis.domain.usecase.SessionAnalysisShareResultsUseCaseImpl
import com.analyzer.session.analysis.domain.usecase.SessionAnalysisUseCase
import com.analyzer.session.analysis.domain.usecase.SessionAnalysisUseCaseImpl
import com.analyzer.session.analysis.presentation.SessionAnalysisViewModel
Expand All @@ -29,6 +31,11 @@ interface SessionAnalysisBindings {
@Provides
private fun provideSessionAnalysisUseCase(impl: SessionAnalysisUseCaseImpl): SessionAnalysisUseCase = impl

@Provides
private fun provideSessionAnalysisShareResultsUseCase(
impl: SessionAnalysisShareResultsUseCaseImpl,
): SessionAnalysisShareResultsUseCase = impl

@Provides
@IntoMap
@ViewModelKey(SessionAnalysisViewModel::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.analyzer.session.analysis.domain.usecase

internal sealed interface SessionAnalysisShareResults {

data object CopiedSummary : SessionAnalysisShareResults

data class ExportedReport(val filePath: String) : SessionAnalysisShareResults

data object OpenedSessionFiles : SessionAnalysisShareResults

data object OpenedSessionFilesAndCopiedPath : SessionAnalysisShareResults

data object CopiedSessionFilesPath : SessionAnalysisShareResults

data class Failure(val reason: String) : SessionAnalysisShareResults
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.analyzer.session.analysis.domain.usecase

internal interface SessionAnalysisShareResultsUseCase {

suspend fun copySummary(summaryText: String): SessionAnalysisShareResults

suspend fun exportReport(
directoryPath: String,
reportFileName: String,
summaryText: String,
): SessionAnalysisShareResults

suspend fun openSessionFiles(sessionId: Long): SessionAnalysisShareResults
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.analyzer.session.analysis.domain.usecase

import com.project.analyzer.api.di.ScreenScope
import com.project.analyzer.telemetry.recording.api.session.RecordedTelemetrySessionLocation
import com.project.analyzer.telemetry.recording.api.session.RecordedTelemetrySessionStorage
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import java.awt.Desktop
import java.awt.Toolkit
import java.awt.datatransfer.StringSelection
import java.io.File
import java.nio.charset.StandardCharsets

@Inject
@SingleIn(ScreenScope::class)
internal class SessionAnalysisShareResultsUseCaseImpl(private val storage: RecordedTelemetrySessionStorage,) :
SessionAnalysisShareResultsUseCase {

override suspend fun copySummary(summaryText: String): SessionAnalysisShareResults {
val copied = runCatching {
Toolkit.getDefaultToolkit().systemClipboard.setContents(
StringSelection(summaryText),
null,
)
}.isSuccess
return if (copied) {
SessionAnalysisShareResults.CopiedSummary
} else {
SessionAnalysisShareResults.Failure("Couldn't copy the analysis summary to the clipboard.")
}
}

override suspend fun exportReport(
directoryPath: String,
reportFileName: String,
summaryText: String,
): SessionAnalysisShareResults {
val directory = File(directoryPath)
if (!directory.exists() || !directory.isDirectory) {
return SessionAnalysisShareResults.Failure("The selected export folder is no longer available.")
}

return runCatching {
val target = directory.resolveUniqueReportFile(reportFileName)
target.writeText(summaryText, StandardCharsets.UTF_8)
SessionAnalysisShareResults.ExportedReport(target.absolutePath)
}.getOrElse { error ->
SessionAnalysisShareResults.Failure(
error.message ?: "Couldn't export the analysis report to the selected folder.",
)
}
}

override suspend fun openSessionFiles(sessionId: Long): SessionAnalysisShareResults {
val bundle = storage.findBundle(sessionId = sessionId)
?: return SessionAnalysisShareResults.Failure("No recorded files were found for this session.")
val directories = bundle.locations.map(RecordedTelemetrySessionLocation::dir).distinct()
val target = directories.resolveShareTarget()
?: return SessionAnalysisShareResults.Failure("The recorded session folder is no longer available.")

val openedFolder = runCatching {
check(Desktop.isDesktopSupported()) { "Desktop integration is not available on this system." }
Desktop.getDesktop().open(target)
}.isSuccess
val copiedPath = runCatching {
Toolkit.getDefaultToolkit().systemClipboard.setContents(
StringSelection(target.absolutePath),
null,
)
}.isSuccess

return when {
openedFolder && copiedPath -> SessionAnalysisShareResults.OpenedSessionFilesAndCopiedPath
openedFolder -> SessionAnalysisShareResults.OpenedSessionFiles
copiedPath -> SessionAnalysisShareResults.CopiedSessionFilesPath
else -> SessionAnalysisShareResults.Failure("Couldn't open the session folder or copy its path.")
}
}
}

private fun List<File>.resolveShareTarget(): File? = when {
isEmpty() -> null

size == 1 -> first().takeIf { it.exists() }

else -> {
val parents = mapNotNull(File::getParentFile)
.filter { it.exists() }
.distinctBy { it.absolutePath }
when {
parents.size == 1 -> parents.single()
else -> maxByOrNull(File::lastModified)?.takeIf { it.exists() }
}
}
}

private fun File.resolveUniqueReportFile(reportFileName: String): File {
val extension = reportFileName.substringAfterLast('.', "")
val baseName = if (extension.isBlank()) {
reportFileName
} else {
reportFileName.removeSuffix(".$extension")
}
var candidate = File(this, reportFileName)
var index = 2
while (candidate.exists()) {
val suffix = if (extension.isBlank()) {
"$baseName-$index"
} else {
"$baseName-$index.$extension"
}
candidate = File(this, suffix)
index += 1
}
return candidate
}
Loading
Loading