choiminyeong c397fbeea9 Implement TASK-008: pull-request and issue read-only UI
Separate issue and PR list/detail tabs, paginated, with comments
(issues) and reviews+comments (PRs). Deleted/unavailable authors
render a fixed placeholder instead of crashing or inventing identity;
every status is a text label, never color-only.

Reuses TASK-010 directly; no shared/domain file touched. Both list
types dedupe by number per page. Entry point is a new button on the
repository references screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 15:08:26 +09:00

209 lines
9.8 KiB
Kotlin

package com.ubisam.ubgitea
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import com.ubisam.ubgitea.domain.model.RepositoryRef
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserScreen
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserTarget
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserUiState
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserViewModel
import com.ubisam.ubgitea.feature.codebrowser.CodeReferenceKind
import com.ubisam.ubgitea.feature.history.HistoryScreen
import com.ubisam.ubgitea.feature.history.HistoryTarget
import com.ubisam.ubgitea.feature.history.HistoryUiState
import com.ubisam.ubgitea.feature.history.HistoryViewModel
import com.ubisam.ubgitea.feature.repositories.ReferenceKind
import com.ubisam.ubgitea.feature.repositories.RepositoryListScreen
import com.ubisam.ubgitea.feature.repositories.RepositoryListViewModel
import com.ubisam.ubgitea.feature.workitems.WorkItemsScreen
import com.ubisam.ubgitea.feature.workitems.WorkItemsTarget
import com.ubisam.ubgitea.feature.workitems.WorkItemsUiState
import com.ubisam.ubgitea.feature.workitems.WorkItemsViewModel
import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
class MainActivity : ComponentActivity() {
private val viewModel: RepositoryListViewModel by viewModels {
RepositoryListViewModel.Factory(application)
}
private val codeBrowserViewModel: CodeBrowserViewModel by viewModels {
CodeBrowserViewModel.Factory(application)
}
private val historyViewModel: HistoryViewModel by viewModels {
HistoryViewModel.Factory(application)
}
private val workItemsViewModel: WorkItemsViewModel by viewModels {
WorkItemsViewModel.Factory(application)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val state by viewModel.state.collectAsState()
val codeBrowserState by codeBrowserViewModel.state.collectAsState()
val historyState by historyViewModel.state.collectAsState()
val workItemsState by workItemsViewModel.state.collectAsState()
var browsingCode by rememberSaveable {
mutableStateOf(codeBrowserState !is CodeBrowserUiState.Idle)
}
var browsingHistory by rememberSaveable {
mutableStateOf(historyState !is HistoryUiState.Idle)
}
var browsingWorkItems by rememberSaveable {
mutableStateOf(workItemsState !is WorkItemsUiState.Idle)
}
UBGiteaTheme {
if (browsingWorkItems) {
WorkItemsScreen(
state = workItemsState,
onShowIssues = workItemsViewModel::showIssues,
onShowPullRequests = workItemsViewModel::showPullRequests,
onIssueSelected = workItemsViewModel::openIssue,
onPullRequestSelected = workItemsViewModel::openPullRequest,
onNextPage = workItemsViewModel::nextPage,
onPreviousPage = workItemsViewModel::previousPage,
onRetryList = workItemsViewModel::retryList,
onRetryDetail = workItemsViewModel::retryDetail,
onBackToList = workItemsViewModel::backToList,
onBack = {
workItemsViewModel.close()
browsingWorkItems = false
},
onAuthenticationExpired = {
workItemsViewModel.close()
browsingWorkItems = false
viewModel.logout()
},
)
} else if (browsingHistory) {
HistoryScreen(
state = historyState,
onCommitSelected = historyViewModel::openCommit,
onNextPage = historyViewModel::nextPage,
onPreviousPage = historyViewModel::previousPage,
onRetryList = historyViewModel::retryList,
onRetryDetail = historyViewModel::retryDetail,
onRetryPatch = historyViewModel::retryPatch,
onBackToList = historyViewModel::backToList,
onBack = {
historyViewModel.close()
browsingHistory = false
},
onCompare = historyViewModel::compareAgainstCurrentRef,
onCompareNextPage = historyViewModel::compareNextPage,
onComparePreviousPage = historyViewModel::comparePreviousPage,
onRetryCompare = historyViewModel::retryCompare,
onLoadComparePatch = historyViewModel::loadComparePatch,
onAuthenticationExpired = {
historyViewModel.close()
codeBrowserViewModel.close()
browsingHistory = false
browsingCode = false
viewModel.logout()
},
)
} else if (browsingCode) {
CodeBrowserScreen(
state = codeBrowserState,
onEntrySelected = codeBrowserViewModel::openEntry,
onBreadcrumbSelected = codeBrowserViewModel::openBreadcrumb,
onRetry = codeBrowserViewModel::retry,
onBack = {
if (!codeBrowserViewModel.navigateUp()) {
codeBrowserViewModel.close()
browsingCode = false
}
},
onAuthenticationExpired = {
codeBrowserViewModel.close()
browsingCode = false
viewModel.logout()
},
onViewHistory = { target ->
historyViewModel.open(HistoryTarget(target.repository, target.ref))
browsingHistory = true
},
)
} else {
RepositoryListScreen(
state = state,
onSignIn = viewModel::signIn,
onCancelSignIn = viewModel::cancelSignIn,
onRetry = viewModel::retry,
onLogout = viewModel::logout,
onRepositorySelected = viewModel::selectRepository,
onReferenceSelected = { repository, name, kind ->
viewModel.selectReference(name, kind)
codeBrowserViewModel.open(
CodeBrowserTarget(
repository = repository.coordinates,
ref = RepositoryRef.of(name),
kind = if (kind == ReferenceKind.BRANCH) {
CodeReferenceKind.BRANCH
} else {
CodeReferenceKind.TAG
},
),
)
browsingCode = true
},
onReferencesRefresh = viewModel::refreshReferences,
onBackToRepositories = viewModel::backToRepositories,
onNextPage = viewModel::nextPage,
onPreviousPage = viewModel::previousPage,
onRefreshRepositories = viewModel::refreshRepositories,
onRetryLastPushedBranch = viewModel::retryRowLastPushedBranch,
onShowAllRepositories = viewModel::showAllRepositories,
onShowFavorites = viewModel::showFavorites,
onToggleFavorite = viewModel::toggleFavorite,
onViewWorkItems = { repository ->
workItemsViewModel.open(WorkItemsTarget(repository.coordinates))
browsingWorkItems = true
},
)
}
LaunchedEffect(viewModel) {
viewModel.browserRequests.collect(::openSystemBrowser)
}
}
}
consumeCallback(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
consumeCallback(intent)
}
private fun openSystemBrowser(uri: String) {
try {
startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(uri)).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
},
)
} catch (_: ActivityNotFoundException) {
viewModel.browserLaunchFailed()
}
}
private fun consumeCallback(intent: Intent?) {
val callback = intent?.dataString ?: return
setIntent(Intent(intent).setData(null))
viewModel.handleCallback(callback)
}
}