diff --git a/android/app/detekt-baseline.xml b/android/app/detekt-baseline.xml index 88a28094f..2faf7416e 100644 --- a/android/app/detekt-baseline.xml +++ b/android/app/detekt-baseline.xml @@ -5,10 +5,10 @@ LongMethod:EditorActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun EditorScreen( configuration: EditorConfiguration, dependencies: EditorDependencies? = null, accountId: ULong? = null, enableNativeMediaUpload: Boolean = true, coroutineScope: CoroutineScope, onClose: () -> Unit, onGutenbergViewCreated: (GutenbergView) -> Unit = {} ) LongMethod:MainActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainScreen( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) LongMethod:MediaProxyServerActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun MediaProxyServerScreen(onBack: () -> Unit) - LongMethod:PostsListActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostsListScreen( viewModel: PostsListViewModel, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) - LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) + LongMethod:PostsListActivity.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun PostsListScreen( viewModel: PostsListViewModel, title: String, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) + LongMethod:SitePreparationActivity.kt$@Composable private fun FeatureConfigurationCard( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeOption>, selectedPostType: PostTypeOption?, onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) LongParameterList:MainActivity.kt$( configurations: List<ConfigurationItem>, onConfigurationClick: (ConfigurationItem) -> Unit, onConfigurationLongClick: (ConfigurationItem) -> Boolean, onAddConfiguration: (String) -> Unit, onDeleteConfiguration: (ConfigurationItem) -> Unit, onMediaProxyServer: () -> Unit = {}, isDiscoveringSite: Boolean = false, onDismissDiscovering: () -> Unit = {}, isLoadingCapabilities: Boolean = false, authError: String? = null, onDismissAuthError: () -> Unit = {} ) - LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeDetails>, selectedPostType: PostTypeDetails?, onPostTypeChange: (PostTypeDetails) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) + LongParameterList:SitePreparationActivity.kt$( enableNativeInserter: Boolean, onEnableNativeInserterChange: (Boolean) -> Unit, enableInserterMediaStrip: Boolean, onEnableInserterMediaStripChange: (Boolean) -> Unit, enableNativeMediaUpload: Boolean, onEnableNativeMediaUploadChange: (Boolean) -> Unit, siteSupportsThemeStyles: Boolean, enableThemeStyles: Boolean, onEnableThemeStylesChange: (Boolean) -> Unit, siteSupportsBlockAssets: Boolean, enableThirdPartyBlocks: Boolean, onEnableThirdPartyBlocksChange: (Boolean) -> Unit, isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, postTypes: List<PostTypeOption>, selectedPostType: PostTypeOption?, onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) MaxLineLength:MediaProxyServerActivity.kt$Text("Size", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) MaxLineLength:MediaProxyServerActivity.kt$Text("Throughput", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) MaxLineLength:MediaProxyServerActivity.kt$Text("Time", fontFamily = FontFamily.Monospace, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f)) diff --git a/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt index 933c07c04..efc079b8a 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/PostsListActivity.kt @@ -68,17 +68,20 @@ class PostsListActivity : ComponentActivity() { companion object { const val EXTRA_ACCOUNT_ID = "account_id" const val EXTRA_POST_TYPE = "post_type" + const val EXTRA_TITLE = "title" fun createIntent( context: Context, accountId: ULong, postType: PostTypeDetails, + title: String, configuration: EditorConfiguration, dependencies: EditorDependencies? ): Intent { return Intent(context, PostsListActivity::class.java).apply { putExtra(EXTRA_ACCOUNT_ID, accountId.toLong()) putExtra(EXTRA_POST_TYPE, postType) + putExtra(EXTRA_TITLE, title) putExtra(MainActivity.EXTRA_CONFIGURATION, configuration) if (dependencies != null) { val filePath = EditorDependenciesSerializer.writeToDisk(context, dependencies) @@ -95,6 +98,7 @@ class PostsListActivity : ComponentActivity() { val accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1L).takeIf { it >= 0 }?.toULong() val postType = intent.getParcelableExtra(EXTRA_POST_TYPE, PostTypeDetails::class.java) ?: PostTypeDetails.post + val title = intent.getStringExtra(EXTRA_TITLE) ?: getString(R.string.posts) val configuration = intent.getParcelableExtra(MainActivity.EXTRA_CONFIGURATION, EditorConfiguration::class.java) val dependenciesPath = intent.getStringExtra(EditorActivity.EXTRA_DEPENDENCIES_PATH) @@ -116,6 +120,7 @@ class PostsListActivity : ComponentActivity() { AppTheme { PostsListScreen( viewModel = viewModel, + title = title, onClose = { finish() }, onPostSelected = { post -> launchEditor(post, configuration, dependenciesPath, accountId) @@ -209,6 +214,7 @@ class PostsListViewModel( @Composable fun PostsListScreen( viewModel: PostsListViewModel, + title: String, onClose: () -> Unit, onPostSelected: (AnyPostWithEditContext) -> Unit ) { @@ -222,7 +228,7 @@ fun PostsListScreen( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( - title = { Text(stringResource(R.string.posts)) }, + title = { Text(title) }, navigationIcon = { IconButton(onClick = onClose) { Icon( diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt index c999bbce0..6ae8449a7 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationActivity.kt @@ -70,7 +70,6 @@ import java.util.Locale import org.wordpress.gutenberg.model.EditorConfiguration import org.wordpress.gutenberg.model.EditorDependencies import org.wordpress.gutenberg.model.EditorDependenciesSerializer -import org.wordpress.gutenberg.model.PostTypeDetails class SitePreparationActivity : ComponentActivity() { @@ -197,12 +196,19 @@ class SitePreparationActivity : ComponentActivity() { private fun launchPostsList( accountId: ULong, - postType: PostTypeDetails, + postType: PostTypeOption, configuration: EditorConfiguration, dependencies: EditorDependencies? ) { startActivity( - PostsListActivity.createIntent(this, accountId, postType, configuration, dependencies) + PostsListActivity.createIntent( + this, + accountId, + postType.details, + postType.pluralLabel, + configuration, + dependencies + ) ) } } @@ -214,7 +220,7 @@ fun SitePreparationScreen( accountId: ULong?, onClose: () -> Unit, onStartEditor: (EditorConfiguration, EditorDependencies?, Boolean) -> Unit, - onBrowsePosts: (EditorConfiguration, EditorDependencies?, PostTypeDetails) -> Unit + onBrowsePosts: (EditorConfiguration, EditorDependencies?, PostTypeOption) -> Unit ) { val uiState by viewModel.uiState.collectAsState() @@ -426,9 +432,9 @@ private fun FeatureConfigurationCard( isPreparing: Boolean, enableNetworkLogging: Boolean, onEnableNetworkLoggingChange: (Boolean) -> Unit, - postTypes: List, - selectedPostType: PostTypeDetails?, - onPostTypeChange: (PostTypeDetails) -> Unit, + postTypes: List, + selectedPostType: PostTypeOption?, + onPostTypeChange: (PostTypeOption) -> Unit, showBrowseButton: Boolean = false, onBrowsePosts: () -> Unit = {} ) { @@ -566,7 +572,7 @@ private fun FeatureConfigurationCard( enabled = !isPreparing ) Text( - text = postType.postType.replaceFirstChar { it.uppercase() }, + text = postType.label, modifier = Modifier.padding(start = 8.dp) ) } diff --git a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt index 740f155fc..b71615e04 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/SitePreparationViewModel.kt @@ -28,9 +28,9 @@ data class SitePreparationUiState( val enableThirdPartyBlocks: Boolean = true, val enableNetworkLogging: Boolean = false, /** All viewable post types fetched from the site, or empty while loading. */ - val postTypes: List = emptyList(), + val postTypes: List = emptyList(), /** The post type currently selected in the picker. Null until [postTypes] loads. */ - val selectedPostType: PostTypeDetails? = null, + val selectedPostType: PostTypeOption? = null, val cacheBundleCount: Int? = null, val isLoading: Boolean = false, val error: String? = null, @@ -39,6 +39,18 @@ data class SitePreparationUiState( val loadingProgress: Float? = null ) +/** A post type offered in the picker, with the names shown for it. */ +data class PostTypeOption(val details: PostTypeDetails, val label: String, val pluralLabel: String) { + companion object { + /** Falls back to the slug and REST base when the site's labels are missing or blank. */ + fun from(details: PostTypeDetails, label: String? = null, pluralLabel: String? = null) = PostTypeOption( + details = details, + label = label?.ifBlank { null } ?: details.postType.replaceFirstChar { it.uppercase() }, + pluralLabel = pluralLabel?.ifBlank { null } ?: details.restBase.replaceFirstChar { it.uppercase() } + ) + } +} + class SitePreparationViewModel( application: Application, private val configurationItem: ConfigurationItem @@ -113,7 +125,7 @@ class SitePreparationViewModel( _uiState.update { it.copy(enableNetworkLogging = enabled) } } - fun setPostType(postType: PostTypeDetails) { + fun setPostType(postType: PostTypeOption) { // Discard dependencies prepared for the previous post type. _uiState.update { it.copy(selectedPostType = postType, editorDependencies = null) } } @@ -227,10 +239,11 @@ class SitePreparationViewModel( private fun createBundledConfiguration(): EditorConfiguration { // Bundled offline editor: only the standard `post` type is meaningful. + val post = PostTypeOption.from(PostTypeDetails.post) _uiState.update { it.copy( - postTypes = listOf(PostTypeDetails.post), - selectedPostType = PostTypeDetails.post + postTypes = listOf(post), + selectedPostType = post ) } return EditorConfiguration.builder( @@ -274,8 +287,8 @@ class SitePreparationViewModel( // available (the typical case); otherwise pick the first type in the // list. Falls back to `PostTypeDetails.post` if the call fails so the // editor can still launch with a sensible default. - val postTypes = loadPostTypes(config) ?: listOf(PostTypeDetails.post) - val defaultPostType = postTypes.firstOrNull { it.postType == "post" } ?: postTypes.first() + val postTypes = loadPostTypes(config) ?: listOf(PostTypeOption.from(PostTypeDetails.post)) + val defaultPostType = postTypes.firstOrNull { it.details.postType == "post" } ?: postTypes.first() _uiState.update { it.copy( postTypes = postTypes, @@ -286,7 +299,7 @@ class SitePreparationViewModel( return EditorConfiguration.builder( siteURL = config.siteUrl, siteApiRoot = siteApiRoot, - postType = defaultPostType + postType = defaultPostType.details ) .setPlugins(capabilities.supportsPlugins) .setThemeStyles(capabilities.supportsThemeStyles) @@ -310,7 +323,7 @@ class SitePreparationViewModel( /** * Fetches the site's post types from the WordPress REST API and returns the - * subset relevant to the editor picker. + * subset relevant to the editor picker, each with its singular and plural labels. * * Mirrors the iOS [SitePreparationView.loadPostTypes] filter: * - always include the standard `post` and `page` types @@ -322,7 +335,7 @@ class SitePreparationViewModel( */ private suspend fun loadPostTypes( config: ConfigurationItem.ConfiguredEditor - ): List? { + ): List? { val app = getApplication() val account = app.accountRepository.all().firstOrNull { it.id() == config.accountId } ?: return null @@ -342,13 +355,20 @@ class SitePreparationViewModel( } } .map { (_, details) -> - PostTypeDetails( - postType = details.slug, - restBase = details.restBase, - restNamespace = details.restNamespace + PostTypeOption.from( + details = PostTypeDetails( + postType = details.slug, + restBase = details.restBase, + restNamespace = details.restNamespace + ), + label = details.labels.singularName, + pluralLabel = details.labels.name ) } - .sortedBy { it.postType } + .sortedWith( + compareBy(String.CASE_INSENSITIVE_ORDER) { it.label } + .thenBy { it.details.postType } + ) } /** @@ -372,7 +392,7 @@ class SitePreparationViewModel( .setEnableInserterMediaStrip(_uiState.value.enableInserterMediaStrip) .setThemeStyles(baseConfig.themeStyles && _uiState.value.enableThemeStyles) .setPlugins(baseConfig.plugins && _uiState.value.enableThirdPartyBlocks) - .setPostType(selectedPostType) + .setPostType(selectedPostType.details) .build() } } diff --git a/ios/Demo-iOS/Sources/Views/PostsListView.swift b/ios/Demo-iOS/Sources/Views/PostsListView.swift index f0f3331aa..8e81f5651 100644 --- a/ios/Demo-iOS/Sources/Views/PostsListView.swift +++ b/ios/Demo-iOS/Sources/Views/PostsListView.swift @@ -10,7 +10,16 @@ struct PostsListView: View { @State private var viewModel: PostsListViewModel - init(client: WordPressAPI, postTypeDetails: PostTypeDetails, editorConfiguration: EditorConfiguration, editorDependencies: EditorDependencies?) { + private let title: String + + init( + client: WordPressAPI, + postTypeDetails: PostTypeDetails, + title: String, + editorConfiguration: EditorConfiguration, + editorDependencies: EditorDependencies? + ) { + self.title = title self.viewModel = PostsListViewModel( client: client, postTypeDetails: postTypeDetails, @@ -37,7 +46,7 @@ struct PostsListView: View { } } else if viewModel.posts.isEmpty { ContentUnavailableView { - Label("No Entires", systemImage: "doc.text") + Label("No Entries", systemImage: "doc.text") } description: { Text("No entries found for this post type.") } @@ -63,7 +72,7 @@ struct PostsListView: View { .listStyle(.plain) } } - .navigationTitle(viewModel.postTypeDetails.restBase.capitalized) + .navigationTitle(title) .task { await viewModel.loadPosts() } diff --git a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift index 94a6477d2..1e2fc875e 100644 --- a/ios/Demo-iOS/Sources/Views/SitePreparationView.swift +++ b/ios/Demo-iOS/Sources/Views/SitePreparationView.swift @@ -75,8 +75,8 @@ struct SitePreparationView: View { } } else { Picker("Post Type", selection: $viewModel.selectedPostTypeDetails) { - ForEach(viewModel.postTypes, id: \.self) { postType in - Text(postType.name).tag(postType) + ForEach(viewModel.postTypes, id: \.details) { option in + Text(option.label).tag(option.details) } } .disabled(viewModel.disableButtons) @@ -87,6 +87,7 @@ struct SitePreparationView: View { PostsListView( client: client, postTypeDetails: viewModel.selectedPostTypeDetails, + title: viewModel.selectedPostType.pluralLabel, editorConfiguration: configuration, editorDependencies: viewModel.editorDependencies ) @@ -243,7 +244,12 @@ class SitePreparationViewModel { } } - var postTypes: [PostTypeDetails] = [] + var postTypes: [PostTypeOption] = [] + + var selectedPostType: PostTypeOption { + postTypes.first { $0.details == selectedPostTypeDetails } + ?? PostTypeOption(details: selectedPostTypeDetails) + } var selectedPostTypeDetails: PostTypeDetails { get { editorConfiguration?.postType ?? .post } @@ -288,7 +294,7 @@ class SitePreparationViewModel { switch configurationItem { case .bundledEditor: self.editorConfiguration = Self.applyDemoAppDefaults(to: .bundled) - self.postTypes = [.post, .page] + self.postTypes = PostTypeOption.builtIn case .localWordPress: guard let credentials = LocalWordPressCredentials.load() else { throw AppError(errorDescription: "Local WordPress not configured.\n\nRun 'make wp-env-start' from the project root to set up a local WordPress environment.") @@ -345,7 +351,7 @@ class SitePreparationViewModel { let newConfiguration = try await self.loadConfiguration(for: account) self.editorConfiguration = Self.applyDemoAppDefaults(to: newConfiguration) } catch let error as WpApiError where error.isDeviceOffline { - self.postTypes = [.post, .page] + self.postTypes = PostTypeOption.builtIn let fallback = Self.buildOfflineConfiguration(for: account) self.editorConfiguration = Self.applyDemoAppDefaults(to: fallback) } @@ -524,7 +530,7 @@ class SitePreparationViewModel { @MainActor private func loadPostTypes() async throws { guard let client = self.client else { - self.postTypes = [.post, .page] + self.postTypes = PostTypeOption.builtIn return } @@ -547,17 +553,20 @@ class SitePreparationViewModel { } .values .map { postType in - PostTypeDetails( - postType: postType.slug, - restBase: postType.restBase, - restNamespace: postType.restNamespace + PostTypeOption( + details: PostTypeDetails( + postType: postType.slug, + restBase: postType.restBase, + restNamespace: postType.restNamespace + ), + label: postType.labels.singularName, + pluralLabel: postType.labels.name ) } - .sorted(using: KeyPathComparator(\.postType)) - - if let firstType = postTypes.first { - self.selectedPostTypeDetails = firstType - } + .sorted(using: [ + KeyPathComparator(\.label, comparator: .localizedStandard), + KeyPathComparator(\.details.postType), + ]) } func buildAndLoadConfiguration(navigation: Navigation) { @@ -575,6 +584,27 @@ class SitePreparationViewModel { } } +/// A post type offered in the picker, with the names shown for it. +struct PostTypeOption { + static let builtIn = [PostTypeOption(details: .post), PostTypeOption(details: .page)] + + let details: PostTypeDetails + let label: String + let pluralLabel: String + + /// Falls back to the slug and REST base when the site's labels are missing or blank. + init(details: PostTypeDetails, label: String? = nil, pluralLabel: String? = nil) { + self.details = details + self.label = Self.nonBlank(label) ?? details.postType.capitalized + self.pluralLabel = Self.nonBlank(pluralLabel) ?? details.restBase.capitalized + } + + private static func nonBlank(_ value: String?) -> String? { + guard let value, !value.trimmingCharacters(in: .whitespaces).isEmpty else { return nil } + return value + } +} + struct KeyValueRow: View { enum Value { @@ -613,12 +643,6 @@ struct KeyValueRow: View { } } -extension PostTypeDetails { - var name: String { - postType.capitalized - } -} - #Preview("Bundled Editor") { NavigationStack { SitePreparationView(site: .bundledEditor)