From ae20b7d98ea986aa127080d8fc16ddcc2f5ac49b Mon Sep 17 00:00:00 2001 From: Wing <44992537+wingio@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:57:22 -0400 Subject: [PATCH] Add code and markdown file viewing --- .../gql/fragments/NodeId.fragment.graphql | 4 + .../gql/fragments/RepoFile.fragment.graphql | 40 +++++++ .../gloom/gql/queries/RepoFile.query.graphql | 12 ++ .../gloom/api/repository/GraphQLRepository.kt | 4 + .../gloom/api/service/GraphQLService.kt | 8 ++ gradle/libs.versions.toml | 1 + ui/build.gradle.kts | 1 + .../materiiapps/gloom/di/ViewModelModule.kt | 2 + .../gloom/ui/components/CodeText.kt | 72 ++++++++++++ .../components/{ => toolbar}/LargeToolbar.kt | 3 +- .../ui/components/toolbar/SmallToolbar.kt | 24 ++++ .../explorer/DirectoryListingScreen.kt | 18 ++- .../ui/screens/explorer/FileViewerScreen.kt | 106 ++++++++++++++++++ .../explorer/viewers/MarkdownFileViewer.kt | 28 +++++ .../explorer/viewers/TextFileViewer.kt | 18 +++ .../gloom/ui/screens/home/HomeScreen.kt | 2 +- .../ui/screens/list/base/BaseListScreen.kt | 2 +- .../screens/settings/AccountSettingsScreen.kt | 2 +- .../settings/AppearanceSettingsScreen.kt | 2 +- .../ui/screens/settings/SettingsScreen.kt | 2 +- .../ui/screens/settings/about/AboutScreen.kt | 2 +- .../screens/settings/about/LibrariesScreen.kt | 2 +- .../settings/developer/AlertTestingScreen.kt | 2 +- .../developer/DeveloperSettingsScreen.kt | 2 +- .../materiiapps/gloom/ui/theme/CodeTheme.kt | 86 ++++++++++++++ .../materiiapps/gloom/ui/utils/CodeUtils.kt | 20 ++++ .../explorer/FileViewerViewModel.kt | 44 ++++++++ .../gloom/ui/widgets/code/CodeViewer.kt | 91 +++++++++++++++ 28 files changed, 588 insertions(+), 12 deletions(-) create mode 100644 api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/NodeId.fragment.graphql create mode 100644 api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/RepoFile.fragment.graphql create mode 100644 api/src/commonMain/graphql/com/materiiapps/gloom/gql/queries/RepoFile.query.graphql create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/CodeText.kt rename ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/{ => toolbar}/LargeToolbar.kt (86%) create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/SmallToolbar.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/FileViewerScreen.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/MarkdownFileViewer.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/TextFileViewer.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/theme/CodeTheme.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/utils/CodeUtils.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/viewmodels/explorer/FileViewerViewModel.kt create mode 100644 ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/widgets/code/CodeViewer.kt diff --git a/api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/NodeId.fragment.graphql b/api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/NodeId.fragment.graphql new file mode 100644 index 00000000..04995c5c --- /dev/null +++ b/api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/NodeId.fragment.graphql @@ -0,0 +1,4 @@ +fragment NodeId on Node { + id + __typename +} \ No newline at end of file diff --git a/api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/RepoFile.fragment.graphql b/api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/RepoFile.fragment.graphql new file mode 100644 index 00000000..700b0c00 --- /dev/null +++ b/api/src/commonMain/graphql/com/materiiapps/gloom/gql/fragments/RepoFile.fragment.graphql @@ -0,0 +1,40 @@ +fragment RepoFile on Repository { + id + gitObject: object(expression: $branch) { + __typename + ...NodeId + ... on Commit { + file(path: $path) { + extension + path + fileType { + __typename + ... on MarkdownFileType { + contentHTML + } + ... on ImageFileType { + url + } + ... on PdfFileType { + url + } + ... on TextFileType { + contentRaw + } + } + } + id + } + } + viewerCanPush + ref(qualifiedName: $branch) { + id + viewerCanCommitToBranch + target { + id + oid + } + __typename + } + __typename +} \ No newline at end of file diff --git a/api/src/commonMain/graphql/com/materiiapps/gloom/gql/queries/RepoFile.query.graphql b/api/src/commonMain/graphql/com/materiiapps/gloom/gql/queries/RepoFile.query.graphql new file mode 100644 index 00000000..80057906 --- /dev/null +++ b/api/src/commonMain/graphql/com/materiiapps/gloom/gql/queries/RepoFile.query.graphql @@ -0,0 +1,12 @@ +query RepoFile( + $owner: String! + $name: String! + $branch: String! + $path: String! +) { + repository(owner: $owner, name: $name) { + __typename + ...RepoFile + id + } +} \ No newline at end of file diff --git a/api/src/commonMain/kotlin/com/materiiapps/gloom/api/repository/GraphQLRepository.kt b/api/src/commonMain/kotlin/com/materiiapps/gloom/api/repository/GraphQLRepository.kt index 44c4317d..b0b26258 100644 --- a/api/src/commonMain/kotlin/com/materiiapps/gloom/api/repository/GraphQLRepository.kt +++ b/api/src/commonMain/kotlin/com/materiiapps/gloom/api/repository/GraphQLRepository.kt @@ -94,6 +94,10 @@ class GraphQLRepository( ?: emptyList() } + suspend fun getRepoFile(owner: String, name: String, branch: String, path: String) = + service.getRepoFile(owner, name, branch, path) + .transform { it.repository?.repoFile } + suspend fun getDefaultBranch(owner: String, name: String) = service.getDefaultBranch(owner, name).transform { it.repository?.defaultBranchRef?.name diff --git a/api/src/commonMain/kotlin/com/materiiapps/gloom/api/service/GraphQLService.kt b/api/src/commonMain/kotlin/com/materiiapps/gloom/api/service/GraphQLService.kt index d61ea76b..cb0c72de 100644 --- a/api/src/commonMain/kotlin/com/materiiapps/gloom/api/service/GraphQLService.kt +++ b/api/src/commonMain/kotlin/com/materiiapps/gloom/api/service/GraphQLService.kt @@ -20,6 +20,7 @@ import com.materiiapps.gloom.gql.ProfileQuery import com.materiiapps.gloom.gql.ReactMutation import com.materiiapps.gloom.gql.ReleaseDetailsQuery import com.materiiapps.gloom.gql.RepoDetailsQuery +import com.materiiapps.gloom.gql.RepoFileQuery import com.materiiapps.gloom.gql.RepoFilesQuery import com.materiiapps.gloom.gql.RepoIssuesQuery import com.materiiapps.gloom.gql.RepoListQuery @@ -222,6 +223,13 @@ class GraphQLService( .response() } + suspend fun getRepoFile(owner: String, name: String, branch: String, path: String) = + withContext(Dispatchers.IO) { + client.query(RepoFileQuery(owner, name, branch, path)) + .addToken() + .response() + } + suspend fun getDefaultBranch(owner: String, name: String) = withContext(Dispatchers.IO) { client.query(DefaultBranchQuery(owner, name)) .addToken() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a10be47..c08b0c9c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ compose-material = { group = "androidx.compose.material", name = "material", ver compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose" } compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" } compose-imageloader = { group = "io.github.qdsfdhvh", name = "image-loader", version = "1.6.4" } +highlights = { group = "dev.snipme", name = "highlights", version = "0.7.1" } koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version = "3.4.0" } koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index d971bfb3..a4a557d2 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -44,6 +44,7 @@ kotlin { api(libs.androidx.paging.compose) api(libs.compose.imageloader) + api(libs.highlights) api(libs.koin.core) api(libs.koin.androidx.compose) api(libs.moko.resources.compose) diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/di/ViewModelModule.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/di/ViewModelModule.kt index 92625b85..82d90547 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/di/ViewModelModule.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/di/ViewModelModule.kt @@ -2,6 +2,7 @@ package com.materiiapps.gloom.di import com.materiiapps.gloom.ui.viewmodels.auth.LandingViewModel import com.materiiapps.gloom.ui.viewmodels.explorer.DirectoryListingViewModel +import com.materiiapps.gloom.ui.viewmodels.explorer.FileViewerViewModel import com.materiiapps.gloom.ui.viewmodels.home.HomeViewModel import com.materiiapps.gloom.ui.viewmodels.list.OrgListViewModel import com.materiiapps.gloom.ui.viewmodels.list.RepositoryListViewModel @@ -42,6 +43,7 @@ fun viewModelModule() = module { factoryOf(::RepoDetailsViewModel) factoryOf(::RepoCodeViewModel) factoryOf(::DirectoryListingViewModel) + factoryOf(::FileViewerViewModel) factoryOf(::RepoIssuesViewModel) factoryOf(::RepoPullRequestsViewModel) factoryOf(::RepoReleasesViewModel) diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/CodeText.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/CodeText.kt new file mode 100644 index 00000000..005f1016 --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/CodeText.kt @@ -0,0 +1,72 @@ +package com.materiiapps.gloom.ui.components + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import com.materiiapps.gloom.ui.utils.fromExtension +import dev.snipme.highlights.Highlights +import dev.snipme.highlights.model.BoldHighlight +import dev.snipme.highlights.model.ColorHighlight +import dev.snipme.highlights.model.SyntaxLanguage +import dev.snipme.highlights.model.SyntaxTheme + +@Composable +fun CodeText( + text: String, + extension: String, + theme: SyntaxTheme, + softWrap: Boolean = false, + fontSize: TextUnit = 13.sp, + modifier: Modifier = Modifier +) { + val hl = remember(text, theme) { + Highlights + .default() + .getBuilder() + .code(text) + .language(SyntaxLanguage.fromExtension(extension)) + .theme(theme) + .build() + } + val highlights = remember(hl) { hl.getHighlights() } + + Text( + text = buildAnnotatedString { + append(hl.getCode()) + + if(!(extension.isBlank() || extension == ".txt")) { + highlights + .filterIsInstance() + .forEach { + addStyle( + SpanStyle(color = Color(it.rgb).copy(alpha = 1f)), + start = it.location.start, + end = it.location.end + ) + } + + highlights + .filterIsInstance() + .forEach { + addStyle( + SpanStyle(fontWeight = FontWeight.Bold), + start = it.location.start, + end = it.location.end + ) + } + } + }, + fontFamily = FontFamily.Monospace, + softWrap = softWrap, + fontSize = fontSize, + modifier = modifier + ) +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/LargeToolbar.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/LargeToolbar.kt similarity index 86% rename from ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/LargeToolbar.kt rename to ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/LargeToolbar.kt index 6b3d4ea3..637f5096 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/LargeToolbar.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/LargeToolbar.kt @@ -1,4 +1,4 @@ -package com.materiiapps.gloom.ui.components +package com.materiiapps.gloom.ui.components.toolbar import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.ExperimentalMaterial3Api @@ -6,6 +6,7 @@ import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import com.materiiapps.gloom.ui.components.BackButton @Composable @OptIn(ExperimentalMaterial3Api::class) diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/SmallToolbar.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/SmallToolbar.kt new file mode 100644 index 00000000..e702dcb2 --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/components/toolbar/SmallToolbar.kt @@ -0,0 +1,24 @@ +package com.materiiapps.gloom.ui.components.toolbar + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import com.materiiapps.gloom.ui.components.BackButton + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun SmallToolbar( + title: String, + actions: @Composable RowScope.() -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { BackButton() }, + actions = actions, + scrollBehavior = scrollBehavior + ) +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/DirectoryListingScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/DirectoryListingScreen.kt index 196041d8..75d84283 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/DirectoryListingScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/DirectoryListingScreen.kt @@ -23,6 +23,7 @@ import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.benasher44.uuid.uuid4 +import com.materiiapps.gloom.ui.utils.navigate import com.materiiapps.gloom.ui.viewmodels.explorer.DirectoryListingViewModel import org.koin.core.parameter.parametersOf @@ -61,14 +62,27 @@ class DirectoryListingScreen( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .clickable { - if (entry.type == "tree") - nav.push( + when(entry.type) { + "tree" -> nav.push( DirectoryListingScreen( owner, name, branchAndPath + "${entry.name}/" ) ) + "blob" -> { + val (branch, path) = branchAndPath.split(":") + nav.navigate( + FileViewerScreen( + owner, + name, + branch, + "$path${entry.name}" + ) + ) + } + } + } .padding(22.dp) .fillMaxWidth() diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/FileViewerScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/FileViewerScreen.kt new file mode 100644 index 00000000..65b68660 --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/FileViewerScreen.kt @@ -0,0 +1,106 @@ +package com.materiiapps.gloom.ui.screens.explorer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.getScreenModel +import com.materiiapps.gloom.gql.fragment.RepoFile +import com.materiiapps.gloom.ui.components.RefreshIndicator +import com.materiiapps.gloom.ui.components.toolbar.SmallToolbar +import com.materiiapps.gloom.ui.screens.explorer.viewers.MarkdownFileViewer +import com.materiiapps.gloom.ui.screens.explorer.viewers.TextFileViewer +import com.materiiapps.gloom.ui.viewmodels.explorer.FileViewerViewModel +import org.koin.core.parameter.parametersOf + +class FileViewerScreen( + private val owner: String, + private val name: String, + private val branch: String, + private val path: String +): Screen { + + @Composable + @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) + override fun Content() { + val viewModel: FileViewerViewModel = getScreenModel { parametersOf(FileViewerViewModel.Input(owner, name, branch, path)) } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val pullRefreshState = rememberPullRefreshState(viewModel.isLoading, onRefresh = { viewModel.getRepoFile() }) + val file = viewModel.file?.gitObject?.onCommit?.file + + Scaffold( + topBar = { Toolbar(scrollBehavior, file) }, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + contentWindowInsets = WindowInsets(0, 0, 0, 0) + ) { pv -> + Box( + modifier = Modifier + .padding(pv) + .fillMaxSize() + .pullRefresh(pullRefreshState) + ) { + when(viewModel.hasError) { + true -> { + Icon( + Icons.Filled.Cancel, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.align(Alignment.Center) + ) + } + + false -> FileContent(file) + } + + RefreshIndicator(pullRefreshState, viewModel.isLoading) + } + } + } + + @Composable + private fun FileContent(file: RepoFile.File?) { + when(file?.fileType?.__typename) { + "MarkdownFileType" -> MarkdownFileViewer(file.fileType?.onMarkdownFileType!!) + "ImageFileType" -> Box(Modifier.fillMaxSize()) + "PdfFileType" -> Box(Modifier.fillMaxSize()) + "TextFileType" -> TextFileViewer(file.fileType?.onTextFileType!!, file.extension ?: "") + else -> {} + } + } + + @Composable + private fun RowScope.FileActions(file: RepoFile.File?) { + // TODO: Different actions for each file type + } + + @Composable + @OptIn(ExperimentalMaterial3Api::class) + private fun Toolbar( + scrollBehavior: TopAppBarScrollBehavior, + file: RepoFile.File? + ) { + SmallToolbar( + title = path.split("/").lastOrNull() ?: "File", + actions = { FileActions(file) }, + scrollBehavior = scrollBehavior + ) + } + +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/MarkdownFileViewer.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/MarkdownFileViewer.kt new file mode 100644 index 00000000..ea9d3b1b --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/MarkdownFileViewer.kt @@ -0,0 +1,28 @@ +package com.materiiapps.gloom.ui.screens.explorer.viewers + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.materiiapps.gloom.gql.fragment.RepoFile +import com.materiiapps.gloom.ui.widgets.Markdown + +@Composable +fun MarkdownFileViewer( + markdownFile: RepoFile.OnMarkdownFileType +) { + markdownFile.contentHTML?.let { + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Markdown( + text = it, + modifier = Modifier.fillMaxSize() + ) + } + } +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/TextFileViewer.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/TextFileViewer.kt new file mode 100644 index 00000000..9b3be1b4 --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/explorer/viewers/TextFileViewer.kt @@ -0,0 +1,18 @@ +package com.materiiapps.gloom.ui.screens.explorer.viewers + +import androidx.compose.runtime.Composable +import com.materiiapps.gloom.gql.fragment.RepoFile +import com.materiiapps.gloom.ui.widgets.code.CodeViewer + +@Composable +fun TextFileViewer( + textFile: RepoFile.OnTextFileType, + extension: String, +) { + val content = textFile.contentRaw ?: return + + CodeViewer( + code = content, + extension = extension + ) +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/home/HomeScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/home/HomeScreen.kt index d4ab3d19..7cc1976b 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/home/HomeScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/home/HomeScreen.kt @@ -28,7 +28,7 @@ import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.materiiapps.gloom.Res -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.RefreshIndicator import com.materiiapps.gloom.ui.viewmodels.home.HomeViewModel import com.materiiapps.gloom.ui.widgets.feed.CreatedRepoItem diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/list/base/BaseListScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/list/base/BaseListScreen.kt index 0ecf437b..bca52430 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/list/base/BaseListScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/list/base/BaseListScreen.kt @@ -23,7 +23,7 @@ import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey import cafe.adriel.voyager.core.screen.Screen import com.apollographql.apollo3.api.Query -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.RefreshIndicator import com.materiiapps.gloom.ui.viewmodels.list.base.BaseListViewModel import dev.icerock.moko.resources.StringResource diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AccountSettingsScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AccountSettingsScreen.kt index 5ee0d254..894325fe 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AccountSettingsScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AccountSettingsScreen.kt @@ -32,7 +32,7 @@ import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.materiiapps.gloom.Res -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.RefreshIndicator import com.materiiapps.gloom.ui.components.settings.SettingsButton import com.materiiapps.gloom.ui.screens.auth.LandingScreen diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AppearanceSettingsScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AppearanceSettingsScreen.kt index d540e97d..5ca8df66 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AppearanceSettingsScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/AppearanceSettingsScreen.kt @@ -16,7 +16,7 @@ import cafe.adriel.voyager.koin.getScreenModel import com.materiiapps.gloom.Res import com.materiiapps.gloom.domain.manager.PreferenceManager import com.materiiapps.gloom.domain.manager.Theme -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.settings.SettingsItemChoice import com.materiiapps.gloom.ui.utils.getString import com.materiiapps.gloom.ui.viewmodels.settings.AppearanceSettingsViewModel diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/SettingsScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/SettingsScreen.kt index 08cf0b43..ec98e62e 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/SettingsScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/SettingsScreen.kt @@ -21,7 +21,7 @@ import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.materiiapps.gloom.Res -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.settings.SettingsButton import com.materiiapps.gloom.ui.components.settings.SettingsCategory import com.materiiapps.gloom.ui.screens.auth.LandingScreen diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/AboutScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/AboutScreen.kt index e5235507..b623e6b9 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/AboutScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/AboutScreen.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import cafe.adriel.voyager.core.screen.Screen import com.materiiapps.gloom.Res -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.settings.SettingsCategory import dev.icerock.moko.resources.compose.stringResource diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/LibrariesScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/LibrariesScreen.kt index a9a09872..b6df111b 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/LibrariesScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/about/LibrariesScreen.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import cafe.adriel.voyager.core.screen.Screen import com.materiiapps.gloom.Res import com.materiiapps.gloom.domain.manager.LibraryManager -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.ThinDivider import com.materiiapps.gloom.ui.widgets.about.LibraryItem import dev.icerock.moko.resources.compose.stringResource diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/AlertTestingScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/AlertTestingScreen.kt index 6f542f30..9addc4d0 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/AlertTestingScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/AlertTestingScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import com.materiiapps.gloom.Res -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.settings.SettingsButton import com.materiiapps.gloom.ui.components.settings.SettingsItemChoice import com.materiiapps.gloom.ui.components.settings.SettingsSwitch diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/DeveloperSettingsScreen.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/DeveloperSettingsScreen.kt index 387a4b87..d72d3095 100644 --- a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/DeveloperSettingsScreen.kt +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/screens/settings/developer/DeveloperSettingsScreen.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import cafe.adriel.voyager.core.screen.Screen import com.materiiapps.gloom.Res -import com.materiiapps.gloom.ui.components.LargeToolbar +import com.materiiapps.gloom.ui.components.toolbar.LargeToolbar import com.materiiapps.gloom.ui.components.settings.SettingsCategory import dev.icerock.moko.resources.compose.stringResource diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/theme/CodeTheme.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/theme/CodeTheme.kt new file mode 100644 index 00000000..8158a2cd --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/theme/CodeTheme.kt @@ -0,0 +1,86 @@ +package com.materiiapps.gloom.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.dp +import com.materiiapps.gloom.domain.manager.PreferenceManager +import com.materiiapps.gloom.domain.manager.Theme +import dev.snipme.highlights.model.SyntaxTheme +import dev.snipme.highlights.model.SyntaxThemes +import org.koin.androidx.compose.get + +data class CodeTheme( + val background: Color, + val linesBackground: Color, + val linesContent: Color, + val code: Color, + val keyword: Color, + val string: Color, + val literal: Color, + val comment: Color, + val metadata: Color, + val multilineComment: Color, + val punctuation: Color, + val mark: Color +) { + + constructor( + background: Color, + linesBackground: Color, + linesContent: Color, + syntaxTheme: SyntaxTheme + ): this( + background = background, + linesBackground = linesBackground, + linesContent = linesContent, + code = Color(syntaxTheme.code), + keyword = Color(syntaxTheme.keyword), + string = Color(syntaxTheme.string), + literal = Color(syntaxTheme.literal), + comment = Color(syntaxTheme.comment), + metadata = Color(syntaxTheme.metadata), + multilineComment = Color(syntaxTheme.multilineComment), + punctuation = Color(syntaxTheme.punctuation), + mark = Color(syntaxTheme.mark) + ) + + val syntaxTheme = SyntaxTheme( + key = hashCode().toString(), + code = code.toArgb(), + keyword = keyword.toArgb(), + string = string.toArgb(), + literal = literal.toArgb(), + comment = comment.toArgb(), + metadata = metadata.toArgb(), + multilineComment = multilineComment.toArgb(), + punctuation = punctuation.toArgb(), + mark = mark.toArgb() + ) + + companion object { + + @Composable + fun getDefault(): CodeTheme { + val prefs: PreferenceManager = get() + val isSystemInDarkTheme = isSystemInDarkTheme() + val darkMode = remember(prefs.theme, isSystemInDarkTheme) { + (prefs.theme == Theme.DARK && prefs.theme != Theme.LIGHT) || (prefs.theme == Theme.SYSTEM && isSystemInDarkTheme) + } + val syntaxTheme = remember(darkMode) { SyntaxThemes.pastel(darkMode = darkMode) } + + return CodeTheme( + background = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp).copy(alpha = 0.2f), + linesBackground = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp), + linesContent = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + syntaxTheme = syntaxTheme + ) + } + + } + +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/utils/CodeUtils.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/utils/CodeUtils.kt new file mode 100644 index 00000000..a964aaaf --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/utils/CodeUtils.kt @@ -0,0 +1,20 @@ +package com.materiiapps.gloom.ui.utils + +import dev.snipme.highlights.model.SyntaxLanguage + +fun SyntaxLanguage.Companion.fromExtension(ext: String): SyntaxLanguage = when(ext.replaceFirst(".", "").lowercase()) { + "c" -> SyntaxLanguage.C + "coffee", "litcoffee" -> SyntaxLanguage.COFFEESCRIPT + "h", "o", "cc", "cpp" -> SyntaxLanguage.CPP + "cs" -> SyntaxLanguage.CSHARP + "java" -> SyntaxLanguage.JAVA + "js", "mjs", "ts", "jsx", "tsx" -> SyntaxLanguage.JAVASCRIPT + "kt", "kts" -> SyntaxLanguage.KOTLIN + "pl" -> SyntaxLanguage.PERL + "py" -> SyntaxLanguage.PYTHON + "rs" -> SyntaxLanguage.RUST + "rb" -> SyntaxLanguage.RUBY + "sh", "zsh", "fsh" -> SyntaxLanguage.SHELL + "swift" -> SyntaxLanguage.SWIFT + else -> SyntaxLanguage.DEFAULT +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/viewmodels/explorer/FileViewerViewModel.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/viewmodels/explorer/FileViewerViewModel.kt new file mode 100644 index 00000000..b2584682 --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/viewmodels/explorer/FileViewerViewModel.kt @@ -0,0 +1,44 @@ +package com.materiiapps.gloom.ui.viewmodels.explorer + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import com.materiiapps.gloom.api.repository.GraphQLRepository +import com.materiiapps.gloom.api.utils.fold +import com.materiiapps.gloom.gql.fragment.RepoFile +import kotlinx.coroutines.launch + +class FileViewerViewModel( + private val input: Input, + private val gqlRepo: GraphQLRepository +): ScreenModel { + + data class Input(val owner: String, val name: String, val branch: String, val path: String) + + var file by mutableStateOf(null) + + var isLoading by mutableStateOf(false) + var hasError by mutableStateOf(false) + + init { + getRepoFile() + } + + fun getRepoFile() { + with(input) { + isLoading = true + coroutineScope.launch { + gqlRepo.getRepoFile(owner, name, branch, path).fold( + onSuccess = { + file = it + }, + onError = { hasError = true } + ) + isLoading = false + } + } + } + +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/widgets/code/CodeViewer.kt b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/widgets/code/CodeViewer.kt new file mode 100644 index 00000000..a492a47d --- /dev/null +++ b/ui/src/commonMain/kotlin/com/materiiapps/gloom/ui/widgets/code/CodeViewer.kt @@ -0,0 +1,91 @@ +package com.materiiapps.gloom.ui.widgets.code + +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.materiiapps.gloom.ui.components.CodeText +import com.materiiapps.gloom.ui.theme.CodeTheme +import com.materiiapps.gloom.ui.utils.DimenUtils +import com.materiiapps.gloom.ui.utils.toImmutableList + +@Composable +fun CodeViewer( + code: String, + extension: String, + theme: CodeTheme = CodeTheme.getDefault(), +) { + val lazyListState = rememberLazyListState() + val scrollState = rememberScrollState() + val canScroll = lazyListState.canScrollForward || lazyListState.canScrollBackward + val lines = remember(code) { + code.split("\n").toImmutableList() + } + + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = if(canScroll) DimenUtils.navBarPadding else 0.dp), + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(scrollState) + .background(theme.background) + ) { + itemsIndexed(lines) { i, code -> + Box( + contentAlignment = Alignment.CenterStart + ) { + CodeText( + text = code, + extension = extension, + theme = theme.syntaxTheme, + modifier = Modifier + .padding(start = (38 + 16).dp, end = 16.dp) + ) + Row( + modifier = Modifier + .offset { IntOffset(scrollState.value, 0) } + .background(theme.linesBackground) + ) { + Text( + (i + 1).toString(), + fontFamily = FontFamily.Monospace, + fontSize = 13.sp, + textAlign = TextAlign.End, + color = theme.linesContent, + fontWeight = FontWeight.Bold, + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 3.dp) + .width(24.dp) + ) + Box( + modifier = Modifier + .background(theme.linesContent) + .width(2.dp) + .fillMaxHeight() + ) + } + } + } + } +} \ No newline at end of file