diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 5a19730b7ab9..faca76dc50d1 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -395,6 +395,7 @@ dependencies { } } implementation project(":libs:login") + implementation project(":libs:posttypes") implementation("$gradle.ext.aboutAutomatticBinaryPath:${libs.versions.automattic.about.get()}") implementation("$gradle.ext.gutenbergKitBinaryPath:${libs.versions.gutenberg.kit.get()}") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index c352d755f64d..4e377f495206 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -97,6 +97,8 @@ import org.wordpress.android.ui.posts.PostUtils.EntryPoint; import org.wordpress.android.ui.posts.PostsListActivity; import org.wordpress.android.ui.posts.RemotePreviewLogicHelper.RemotePreviewType; +import org.wordpress.android.posttypes.CptPostTypesActivity; +import org.wordpress.android.posttypes.bridge.SiteReference; import org.wordpress.android.ui.prefs.AccountSettingsActivity; import org.wordpress.android.ui.prefs.AppSettingsActivity; import org.wordpress.android.ui.prefs.BlogPreferencesActivity; @@ -705,6 +707,16 @@ public static void viewCurrentBlogPages(@NonNull Context context, @NonNull SiteM AnalyticsUtils.trackWithSiteDetails(AnalyticsTracker.Stat.OPENED_PAGES, site); } + public static void viewPostTypes(@NonNull Context context, @NonNull SiteModel site) { + SiteReference siteRef = SiteReference.Companion.create( + site.getSiteId(), + site.getName() != null ? site.getName() : "", + site.getUrl() != null ? site.getUrl() : "" + ); + Intent intent = CptPostTypesActivity.Companion.createIntent(context, siteRef); + context.startActivity(intent); + } + public static void viewCurrentBlogPagesOfType(Context context, SiteModel site, PageListType pageListType) { if (pageListType == null) { Intent intent = new Intent(context, PagesActivity.class); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt index 2ff48c4be568..45252320e38e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt @@ -616,6 +616,8 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), is SiteNavigationAction.OpenPlan -> ActivityLauncher.viewBlogPlans(activity, action.site) is SiteNavigationAction.OpenPosts -> ActivityLauncher.viewCurrentBlogPosts(requireActivity(), action.site) is SiteNavigationAction.OpenPages -> ActivityLauncher.viewCurrentBlogPages(requireActivity(), action.site) + is SiteNavigationAction.OpenPostTypes -> + ActivityLauncher.viewPostTypes(requireActivity(), action.site) is SiteNavigationAction.OpenHomepage -> ActivityLauncher.editLandingPageForResult( this, action.site, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt index 755ce359110f..573224ef3d8c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt @@ -24,6 +24,7 @@ sealed class SiteNavigationAction { data class OpenPlan(val site: SiteModel) : SiteNavigationAction() data class OpenPosts(val site: SiteModel) : SiteNavigationAction() data class OpenPages(val site: SiteModel) : SiteNavigationAction() + data class OpenPostTypes(val site: SiteModel) : SiteNavigationAction() data class OpenHomepage( val site: SiteModel, val homepageLocalId: Int, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt index b359ca8c3d90..362e16cff0a8 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt @@ -28,6 +28,7 @@ class ListItemActionHandler @Inject constructor( ListItemAction.PLAN -> SiteNavigationAction.OpenPlan(selectedSite) ListItemAction.POSTS -> SiteNavigationAction.OpenPosts(selectedSite) ListItemAction.PAGES -> SiteNavigationAction.OpenPages(selectedSite) + ListItemAction.POST_TYPES -> SiteNavigationAction.OpenPostTypes(selectedSite) ListItemAction.ADMIN -> SiteNavigationAction.OpenAdmin(selectedSite) ListItemAction.SUBSCRIBERS -> SiteNavigationAction.OpenSubscribers(selectedSite) ListItemAction.PEOPLE -> SiteNavigationAction.OpenPeople(selectedSite) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/ListItemAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/ListItemAction.kt index a3bf1cac40b6..0235c689b8c3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/ListItemAction.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/ListItemAction.kt @@ -7,6 +7,7 @@ enum class ListItemAction (val trackingLabel: String) { PLAN("plan"), POSTS("posts"), PAGES("pages"), + POST_TYPES("post_types"), ADMIN("admin"), PEOPLE("people"), SUBSCRIBERS("subscribers"), diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/SiteItemsBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/SiteItemsBuilder.kt index 6d79da7e1472..488126d01066 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/SiteItemsBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/items/listitem/SiteItemsBuilder.kt @@ -13,6 +13,9 @@ import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.mysite.items.listitem.ListItemAction.COMMENTS import org.wordpress.android.ui.mysite.items.listitem.ListItemAction.MEDIA import org.wordpress.android.ui.mysite.items.listitem.ListItemAction.POSTS +import org.wordpress.android.ui.mysite.items.listitem.ListItemAction.POST_TYPES +import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures +import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures.Feature import org.wordpress.android.ui.utils.ListItemInteraction import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -21,7 +24,8 @@ import javax.inject.Inject class SiteItemsBuilder @Inject constructor( private val siteListItemBuilder: SiteListItemBuilder, private val quickStartRepository: QuickStartRepository, - private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil + private val jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil, + private val experimentalFeatures: ExperimentalFeatures ) { @Suppress("LongMethod") fun build(params: SiteItemsBuilderParams): List { @@ -49,6 +53,7 @@ class SiteItemsBuilder @Inject constructor( listItemAction = POSTS ), siteListItemBuilder.buildPagesItemIfAvailable(params.site, params.onClick, showPagesFocusPoint), + buildPostTypesItemIfEnabled(params.onClick), ListItem( R.drawable.ic_media_white_24dp, UiStringRes(R.string.media), @@ -65,6 +70,19 @@ class SiteItemsBuilder @Inject constructor( ) } + private fun buildPostTypesItemIfEnabled(onClick: (ListItemAction) -> Unit): ListItem? { + return if (experimentalFeatures.isEnabled(Feature.EXPERIMENTAL_POST_TYPES)) { + ListItem( + R.drawable.ic_posts_white_24dp, + UiStringRes(R.string.post_types_screen_title), + onClick = ListItemInteraction.create(POST_TYPES, onClick), + listItemAction = POST_TYPES + ) + } else { + null + } + } + private fun getTrafficSiteItems( params: SiteItemsBuilderParams ): List { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/menu/MenuActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/menu/MenuActivity.kt index 3942e3058861..35204ff125d6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/menu/MenuActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/menu/MenuActivity.kt @@ -133,6 +133,7 @@ class MenuActivity : BaseAppCompatActivity() { is SiteNavigationAction.OpenPlan -> ActivityLauncher.viewBlogPlans(this, action.site) is SiteNavigationAction.OpenPosts -> ActivityLauncher.viewCurrentBlogPosts(this, action.site) is SiteNavigationAction.OpenPages -> ActivityLauncher.viewCurrentBlogPages(this, action.site) + is SiteNavigationAction.OpenPostTypes -> ActivityLauncher.viewPostTypes(this, action.site) is SiteNavigationAction.OpenAdmin -> ActivityLauncher.viewBlogAdmin(this, action.site) is SiteNavigationAction.OpenPeople -> ActivityLauncher.viewCurrentBlogPeople(this, action.site) is SiteNavigationAction.OpenSharing -> ActivityLauncher.viewBlogSharing(this, action.site) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeatures.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeatures.kt index 78cfc432e4a8..5dc842c719cc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeatures.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeatures.kt @@ -44,6 +44,11 @@ class ExperimentalFeatures @Inject constructor( "network_debugging", R.string.experimental_network_debugging, R.string.experimental_network_debugging_description + ), + EXPERIMENTAL_POST_TYPES( + "experimental_post_types", + R.string.experimental_post_types, + R.string.experimental_post_types_description ); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt index b32d05747bec..cd2857373546 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/experimentalfeatures/ExperimentalFeaturesViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.wordpress.android.BuildConfig import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.fluxc.utils.AppLogWrapper @@ -50,7 +51,10 @@ internal class ExperimentalFeaturesViewModel @Inject constructor( } private fun shouldShowFeature(feature: Feature): Boolean { - return if (gutenbergKitFeature.isEnabled()) { + // Only show Post Types feature in debug builds + return if (feature == Feature.EXPERIMENTAL_POST_TYPES) { + BuildConfig.DEBUG + } else if (gutenbergKitFeature.isEnabled()) { feature != Feature.EXPERIMENTAL_BLOCK_EDITOR } else { feature != Feature.DISABLE_EXPERIMENTAL_BLOCK_EDITOR diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 2527913b9534..173cb30f55b1 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1003,6 +1003,9 @@ Disable Tracking? Existing logs will expire based on the retention period. To clear logs now, tap \"View Network Requests\" and use the trash icon before disabling. Disable + Post Types + Enable experimental post types screen + Post Types Debug Settings diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/items/SiteItemsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/items/SiteItemsBuilderTest.kt index af5235dce627..12e356afa946 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/items/SiteItemsBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/items/SiteItemsBuilderTest.kt @@ -16,6 +16,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.SiteItemsB import org.wordpress.android.ui.mysite.cards.quickstart.QuickStartRepository import org.wordpress.android.ui.mysite.items.listitem.SiteItemsBuilder import org.wordpress.android.ui.mysite.items.listitem.SiteListItemBuilder +import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures import org.wordpress.android.ui.quickstart.QuickStartType @RunWith(MockitoJUnitRunner::class) @@ -35,6 +36,9 @@ class SiteItemsBuilderTest { @Mock lateinit var jetpackFeatureRemovalOverlayUtil: JetpackFeatureRemovalOverlayUtil + @Mock + lateinit var experimentalFeatures: ExperimentalFeatures + private lateinit var siteItemsBuilder: SiteItemsBuilder @Before @@ -45,7 +49,8 @@ class SiteItemsBuilderTest { siteItemsBuilder = SiteItemsBuilder( siteListItemBuilder, quickStartRepository, - jetpackFeatureRemovalOverlayUtil + jetpackFeatureRemovalOverlayUtil, + experimentalFeatures ) } diff --git a/libs/posttypes/README.md b/libs/posttypes/README.md new file mode 100644 index 000000000000..321eb33f1183 --- /dev/null +++ b/libs/posttypes/README.md @@ -0,0 +1,151 @@ +# Post Types Module + +This module contains the experimental Custom Post Types (CPT) feature implementation. + +## Purpose + +The Post Types module is **intentionally isolated** from the main WordPress app module to: + +1. **Prevent accidental FluxC usage** - By not depending on FluxC, the compiler catches any + accidental imports of legacy patterns. This is a key goal during wordpress-rs integration. + +2. **Enable clean wordpress-rs integration** - Forces deliberate decisions about data models and + API integration from the start, rather than falling back to familiar FluxC patterns. + +3. **Establish new architectural patterns** - Acts as a sandbox for the new service layer before + patterns are propagated to the rest of the codebase. + +## Module Structure + +``` +libs/posttypes/ +├── build.gradle +├── README.md +└── src/main/ + ├── AndroidManifest.xml + ├── java/org/wordpress/android/posttypes/ + │ ├── bridge/ # ⚠️ Temporary bridging code + │ │ ├── package-info.kt # Documentation for bridge package + │ │ ├── ActivitySetup.kt # CptActivity interface + applyBaseSetup() + │ │ ├── BridgeConstants.kt # Intent keys mirroring main app + │ │ ├── BridgeTheme.kt # Standalone Material3 theme + │ │ └── SiteReference.kt # Minimal site representation + │ ├── CptPostTypesActivity.kt + │ ├── CptPostTypesViewModel.kt + │ ├── CptFlatPostListActivity.kt + │ ├── CptFlatPostListViewModel.kt + │ └── compose/ + │ ├── CptPostTypesScreen.kt + │ └── CptFlatPostListScreen.kt + └── res/values/ + ├── strings.xml + └── styles.xml # Cpt.NoActionBar theme +``` + +## The Bridge Package + +The `bridge` package contains **temporary coupling code** that connects this isolated module to +the main WordPress app. Everything in this package is explicitly marked as "needs attention when +merging back or integrating wordpress-rs properly." + +### Bridge Components + +| Component | Purpose | Main App Equivalent | +|-----------|---------|---------------------| +| `CptActivity` + `applyBaseSetup()` | Activity setup (edge-to-edge, etc.) | `BaseAppCompatActivity` | +| `@style/Cpt.NoActionBar` | Activity theme (no action bar) | `@style/WordPress.NoActionBar` | +| `BridgeConstants.EXTRA_SITE` | Intent extra key for site data | `WordPress.SITE` | +| `CptTheme` | Standalone Material3 Compose theme | `AppThemeM3` | +| `SiteReference` | Minimal site data model | `SiteModel` (FluxC) | + +### Why Composition Over Inheritance? + +Activities implement `CptActivity` interface and call `applyBaseSetup()` instead of extending +a base class. This approach: + +- Avoids "is-a" inheritance problems (fragile base class, tight coupling) +- Makes the setup explicit and discoverable +- Easier to migrate - just remove interface and inline or replace the setup +- Can be extended with additional lifecycle hooks if needed + +## Migration Guide + +### Option A: Full wordpress-rs Integration (Recommended) + +When wordpress-rs integration is complete: + +1. Replace `SiteReference` with wordpress-rs site models +2. Replace `CptTheme` with `AppThemeM3` (or keep module theme if preferred) +3. Replace `BridgeConstants.EXTRA_SITE` with `WordPress.SITE` +4. For activities: either extend `BaseAppCompatActivity` or keep `CptActivity` pattern +5. Delete the entire `bridge` package +6. Keep module structure for continued isolation + +### Option B: Merge Back to Main Module + +If reverting to the main module structure: + +1. Move all non-bridge files to: + ``` + WordPress/src/main/java/org/wordpress/android/ui/posttypes/ + ``` + +2. Replace bridge imports with main app equivalents: + - `CptActivity` + `applyBaseSetup()` → extend `BaseAppCompatActivity` + - `@style/Cpt.NoActionBar` → `@style/WordPress.NoActionBar` (in AndroidManifest) + - `CptTheme` → `AppThemeM3` + - `SiteReference` → `SiteModel` + - `BridgeConstants.EXTRA_SITE` → `WordPress.SITE` + +3. Update `ActivityLauncher.viewPostTypes()` to pass `SiteModel` directly + +4. Remove module from `settings.gradle`: + ```diff + - include ':libs:posttypes' + ``` + +5. Remove dependency from `WordPress/build.gradle`: + ```diff + - implementation project(":libs:posttypes") + ``` + +6. Add activities back to `WordPress/src/main/AndroidManifest.xml` + +7. Delete `libs/posttypes/` directory + +## Dependencies + +This module intentionally has minimal dependencies: + +- **AndroidX AppCompat** - Base activity class +- **Jetpack Compose** - UI framework +- **Hilt** - Dependency injection +- **Kotlin Parcelize** - For `SiteReference` parcelable + +**Not included (by design):** +- FluxC - Networking/data layer +- WordPress utils - App-specific utilities +- Any other main app dependencies + +## Adding New Features + +When adding new functionality to this module: + +1. **Do not add FluxC dependencies** - If you need site/post data, integrate with wordpress-rs + or add to the bridge package with clear documentation + +2. **Document any new bridge items** - If you must add bridging code, add it to the `bridge` + package with migration notes in both the code and this README + +3. **Keep the module self-contained** - The goal is isolation; dependencies should flow inward, + not outward + +## Testing + +```bash +# Build the module +./gradlew :libs:posttypes:assembleDebug + +# Build the full app (verifies integration) +./gradlew assembleWordPressVanillaDebug +``` diff --git a/libs/posttypes/build.gradle b/libs/posttypes/build.gradle new file mode 100644 index 000000000000..ec731c6e2b94 --- /dev/null +++ b/libs/posttypes/build.gradle @@ -0,0 +1,54 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.google.dagger.hilt) + alias(libs.plugins.ksp) + alias(libs.plugins.dependency.analysis) +} + +android { + namespace "org.wordpress.android.posttypes" + + defaultConfig { + compileSdk rootProject.compileSdkVersion + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + } + + compileOptions { + sourceCompatibility JvmTarget.fromTarget(libs.versions.java.get()).target + targetCompatibility JvmTarget.fromTarget(libs.versions.java.get()).target + } + + buildFeatures { + compose true + } +} + +dependencies { + // AndroidX + implementation libs.androidx.appcompat.main + + // Compose + implementation platform(libs.androidx.compose.bom) + implementation libs.androidx.compose.ui.main + implementation libs.androidx.compose.material3 + implementation libs.androidx.compose.material.icons.extended + implementation libs.androidx.compose.ui.tooling.preview + implementation libs.androidx.activity.compose + + // Lifecycle & ViewModel + implementation libs.androidx.lifecycle.viewmodel.compose + implementation libs.androidx.lifecycle.runtime + + // Hilt + implementation libs.google.dagger.hilt.android.main + ksp libs.google.dagger.hilt.compiler + + // Debug + debugImplementation libs.androidx.compose.ui.tooling.main +} diff --git a/libs/posttypes/src/main/AndroidManifest.xml b/libs/posttypes/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b69d73104e70 --- /dev/null +++ b/libs/posttypes/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptFlatPostListActivity.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptFlatPostListActivity.kt new file mode 100644 index 000000000000..f7c358cd474c --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptFlatPostListActivity.kt @@ -0,0 +1,55 @@ +package org.wordpress.android.posttypes + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.posttypes.bridge.BridgeConstants +import org.wordpress.android.posttypes.bridge.CptActivity +import org.wordpress.android.posttypes.bridge.CptTheme +import org.wordpress.android.posttypes.bridge.SiteReference +import org.wordpress.android.posttypes.bridge.applyBaseSetup +import org.wordpress.android.posttypes.compose.CptFlatPostListScreen + +@AndroidEntryPoint +class CptFlatPostListActivity : AppCompatActivity(), CptActivity { + private val viewModel: CptFlatPostListViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + applyBaseSetup() + super.onCreate(savedInstanceState) + setContent { + CptTheme { + val uiState by viewModel.uiState.collectAsState() + CptFlatPostListScreen( + uiState = uiState, + onBackClick = { onBackPressedDispatcher.onBackPressed() }, + onPostClick = viewModel::onPostClick + ) + } + } + } + + companion object { + const val EXTRA_POST_TYPE_SLUG = "post_type_slug" + const val EXTRA_POST_TYPE_LABEL = "post_type_label" + + fun createIntent( + context: Context, + site: SiteReference, + postTypeSlug: String, + postTypeLabel: String + ): Intent { + return Intent(context, CptFlatPostListActivity::class.java).apply { + putExtra(BridgeConstants.EXTRA_SITE, site) + putExtra(EXTRA_POST_TYPE_SLUG, postTypeSlug) + putExtra(EXTRA_POST_TYPE_LABEL, postTypeLabel) + } + } + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptFlatPostListViewModel.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptFlatPostListViewModel.kt new file mode 100644 index 000000000000..1205dc1a8ce3 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptFlatPostListViewModel.kt @@ -0,0 +1,67 @@ +package org.wordpress.android.posttypes + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.wordpress.android.posttypes.bridge.BridgeConstants +import org.wordpress.android.posttypes.bridge.SiteReference +import javax.inject.Inject + +data class CptPostListItem( + val id: Long, + val title: String, + val excerpt: String, + val status: String +) + +data class CptFlatPostListUiState( + val postTypeLabel: String = "", + val posts: List = emptyList(), + val isLoading: Boolean = false +) + +@HiltViewModel +class CptFlatPostListViewModel @Inject constructor( + savedStateHandle: SavedStateHandle +) : ViewModel() { + @Suppress("unused") // Will be used to fetch posts from wordpress-rs + private val site: SiteReference? = savedStateHandle.get(BridgeConstants.EXTRA_SITE) + + @Suppress("unused") // Will be used to fetch posts from wordpress-rs + private val postTypeSlug: String = savedStateHandle.get( + CptFlatPostListActivity.EXTRA_POST_TYPE_SLUG + ) ?: "" + + private val postTypeLabel: String = savedStateHandle.get( + CptFlatPostListActivity.EXTRA_POST_TYPE_LABEL + ) ?: "" + + private val _uiState = MutableStateFlow( + CptFlatPostListUiState( + postTypeLabel = postTypeLabel, + posts = generateMockPosts() + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + @Suppress("MagicNumber") // Mock data - will be replaced with real data from wordpress-rs + private fun generateMockPosts(): List { + val statuses = listOf("Published", "Draft", "Scheduled") + return (1..5).map { id -> + CptPostListItem( + id = id.toLong(), + title = "Post $id", + excerpt = "This is the post $id excerpt...", + status = statuses[id % statuses.size] + ) + } + } + + @Suppress("UnusedParameter") // Will navigate to post editor with wordpress-rs integration + fun onPostClick(post: CptPostListItem) { + // No-op: navigation will be implemented with wordpress-rs integration + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptHierarchicalPostListActivity.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptHierarchicalPostListActivity.kt new file mode 100644 index 000000000000..105067ce103c --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptHierarchicalPostListActivity.kt @@ -0,0 +1,55 @@ +package org.wordpress.android.posttypes + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.posttypes.bridge.BridgeConstants +import org.wordpress.android.posttypes.bridge.CptActivity +import org.wordpress.android.posttypes.bridge.CptTheme +import org.wordpress.android.posttypes.bridge.SiteReference +import org.wordpress.android.posttypes.bridge.applyBaseSetup +import org.wordpress.android.posttypes.compose.CptHierarchicalPostListScreen + +@AndroidEntryPoint +class CptHierarchicalPostListActivity : AppCompatActivity(), CptActivity { + private val viewModel: CptHierarchicalPostListViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + applyBaseSetup() + super.onCreate(savedInstanceState) + setContent { + CptTheme { + val uiState by viewModel.uiState.collectAsState() + CptHierarchicalPostListScreen( + uiState = uiState, + onBackClick = { onBackPressedDispatcher.onBackPressed() }, + onPostClick = viewModel::onPostClick + ) + } + } + } + + companion object { + const val EXTRA_POST_TYPE_SLUG = "post_type_slug" + const val EXTRA_POST_TYPE_LABEL = "post_type_label" + + fun createIntent( + context: Context, + site: SiteReference, + postTypeSlug: String, + postTypeLabel: String + ): Intent { + return Intent(context, CptHierarchicalPostListActivity::class.java).apply { + putExtra(BridgeConstants.EXTRA_SITE, site) + putExtra(EXTRA_POST_TYPE_SLUG, postTypeSlug) + putExtra(EXTRA_POST_TYPE_LABEL, postTypeLabel) + } + } + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptHierarchicalPostListViewModel.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptHierarchicalPostListViewModel.kt new file mode 100644 index 000000000000..7683bb891f18 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptHierarchicalPostListViewModel.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.posttypes + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.wordpress.android.posttypes.bridge.BridgeConstants +import org.wordpress.android.posttypes.bridge.SiteReference +import javax.inject.Inject + +data class CptHierarchicalPostItem( + val id: Long, + val title: String, + val status: String, + val indent: Int = 0 +) + +data class CptHierarchicalPostListUiState( + val postTypeLabel: String = "", + val posts: List = emptyList(), + val isLoading: Boolean = false +) + +@HiltViewModel +class CptHierarchicalPostListViewModel @Inject constructor( + savedStateHandle: SavedStateHandle +) : ViewModel() { + @Suppress("unused") // Will be used to fetch posts from wordpress-rs + private val site: SiteReference? = savedStateHandle.get(BridgeConstants.EXTRA_SITE) + + @Suppress("unused") // Will be used to fetch posts from wordpress-rs + private val postTypeSlug: String = savedStateHandle.get( + CptHierarchicalPostListActivity.EXTRA_POST_TYPE_SLUG + ) ?: "" + + private val postTypeLabel: String = savedStateHandle.get( + CptHierarchicalPostListActivity.EXTRA_POST_TYPE_LABEL + ) ?: "" + + private val _uiState = MutableStateFlow( + CptHierarchicalPostListUiState( + postTypeLabel = postTypeLabel, + posts = generateMockHierarchy() + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + @Suppress("MagicNumber") // Mock data - will be replaced with real data from wordpress-rs + private fun generateMockHierarchy(): List { + // Simulates a page hierarchy: Home > About > Team, Contact + return listOf( + CptHierarchicalPostItem(1, "Home", "Published", indent = 0), + CptHierarchicalPostItem(2, "About", "Published", indent = 1), + CptHierarchicalPostItem(3, "Team", "Published", indent = 2), + CptHierarchicalPostItem(4, "Contact", "Published", indent = 1), + CptHierarchicalPostItem(5, "Blog", "Draft", indent = 0) + ) + } + + @Suppress("UnusedParameter") // Will navigate to post editor with wordpress-rs integration + fun onPostClick(post: CptHierarchicalPostItem) { + // No-op: navigation will be implemented with wordpress-rs integration + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptPostTypesActivity.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptPostTypesActivity.kt new file mode 100644 index 000000000000..3de6f90fb369 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptPostTypesActivity.kt @@ -0,0 +1,76 @@ +package org.wordpress.android.posttypes + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.posttypes.bridge.BridgeConstants +import org.wordpress.android.posttypes.bridge.CptActivity +import org.wordpress.android.posttypes.bridge.CptTheme +import org.wordpress.android.posttypes.bridge.SiteReference +import org.wordpress.android.posttypes.bridge.applyBaseSetup +import org.wordpress.android.posttypes.compose.CptPostTypesScreen + +@AndroidEntryPoint +class CptPostTypesActivity : AppCompatActivity(), CptActivity { + private val viewModel: CptPostTypesViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + applyBaseSetup() + super.onCreate(savedInstanceState) + setContent { + CptTheme { + val uiState by viewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + viewModel.navigation.collect { action -> + handleNavigation(action) + } + } + + CptPostTypesScreen( + uiState = uiState, + onBackClick = { onBackPressedDispatcher.onBackPressed() }, + onPostTypeClick = viewModel::onPostTypeClick + ) + } + } + } + + private fun handleNavigation(action: CptNavigationAction) { + when (action) { + is CptNavigationAction.OpenPostTypeList -> { + val intent = if (action.hierarchical) { + CptHierarchicalPostListActivity.createIntent( + context = this, + site = action.site, + postTypeSlug = action.postTypeSlug, + postTypeLabel = action.postTypeLabel + ) + } else { + CptFlatPostListActivity.createIntent( + context = this, + site = action.site, + postTypeSlug = action.postTypeSlug, + postTypeLabel = action.postTypeLabel + ) + } + startActivity(intent) + } + } + } + + companion object { + fun createIntent(context: Context, site: SiteReference): Intent { + return Intent(context, CptPostTypesActivity::class.java).apply { + putExtra(BridgeConstants.EXTRA_SITE, site) + } + } + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptPostTypesViewModel.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptPostTypesViewModel.kt new file mode 100644 index 000000000000..85627f199029 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/CptPostTypesViewModel.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.posttypes + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import org.wordpress.android.posttypes.bridge.BridgeConstants +import org.wordpress.android.posttypes.bridge.SiteReference +import javax.inject.Inject + +data class CptPostTypeItem( + val slug: String, + val label: String, + val hierarchical: Boolean = false +) + +data class CptPostTypesUiState( + val postTypes: List = emptyList() +) + +sealed class CptNavigationAction { + data class OpenPostTypeList( + val site: SiteReference, + val postTypeSlug: String, + val postTypeLabel: String, + val hierarchical: Boolean + ) : CptNavigationAction() +} + +@HiltViewModel +class CptPostTypesViewModel @Inject constructor( + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val site: SiteReference? = savedStateHandle.get(BridgeConstants.EXTRA_SITE) + + private val _uiState = MutableStateFlow( + CptPostTypesUiState( + postTypes = listOf( + CptPostTypeItem(slug = "post", label = "Posts", hierarchical = false), + CptPostTypeItem(slug = "page", label = "Pages", hierarchical = true) + ) + ) + ) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _navigation = MutableSharedFlow(extraBufferCapacity = 1) + val navigation: SharedFlow = _navigation.asSharedFlow() + + fun onPostTypeClick(postType: CptPostTypeItem) { + site?.let { + _navigation.tryEmit( + CptNavigationAction.OpenPostTypeList( + site = it, + postTypeSlug = postType.slug, + postTypeLabel = postType.label, + hierarchical = postType.hierarchical + ) + ) + } + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/BridgeConstants.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/BridgeConstants.kt new file mode 100644 index 000000000000..e10731f89122 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/BridgeConstants.kt @@ -0,0 +1,23 @@ +package org.wordpress.android.posttypes.bridge + +/** + * Constants used for bridging with the main WordPress app module. + * + * ## Purpose + * This file contains constants that mirror values from the main app module to maintain + * compatibility while keeping this module isolated from FluxC and other legacy dependencies. + * + * ## Migration Notes + * When merging this module back into the main app: + * - Replace [EXTRA_SITE] usages with `WordPress.SITE` + * - Remove this file entirely + * + * @see org.wordpress.android.posttypes.bridge package documentation + */ +object BridgeConstants { + /** + * Intent extra key for passing site data between activities. + * Mirrors `WordPress.SITE` from the main app module. + */ + const val EXTRA_SITE = "SITE" +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/BridgeTheme.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/BridgeTheme.kt new file mode 100644 index 000000000000..6872291abcd0 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/BridgeTheme.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.posttypes.bridge + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +/** + * Temporary theme wrapper for the Post Types module. + * + * ## Purpose + * Provides a standalone Material3 theme that doesn't depend on the main app's theming system. + * This allows the module to be compiled and tested independently. + * + * ## Migration Notes + * When merging this module back into the main app: + * - Replace [CptTheme] usages with `AppThemeM3` + * - Remove this file entirely + * + * @see org.wordpress.android.posttypes.bridge package documentation + */ +@Composable +fun CptTheme( + isDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (isDarkTheme) DarkColorScheme else LightColorScheme + + MaterialTheme( + colorScheme = colorScheme + ) { + Surface(color = MaterialTheme.colorScheme.background) { + content() + } + } +} + +// Basic color schemes - these approximate the WordPress/Jetpack themes +private val LightColorScheme = lightColorScheme( + primary = Color(0xFF0675C4), + secondary = Color(0xFF0675C4), + background = Color.White, + surface = Color.White, + error = Color(0xFFD63638), + onPrimary = Color.White, + onSecondary = Color.White, + onBackground = Color.Black, + onSurface = Color.Black, + onError = Color.White +) + +private val DarkColorScheme = darkColorScheme( + primary = Color(0xFF6EC2FB), + secondary = Color(0xFF6EC2FB), + background = Color(0xFF1E1E1E), + surface = Color(0xFF1E1E1E), + error = Color(0xFFFF8085), + onPrimary = Color.Black, + onSecondary = Color.White, + onBackground = Color.White, + onSurface = Color.White, + onError = Color.Black +) diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/CptActivity.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/CptActivity.kt new file mode 100644 index 000000000000..37ed040c4ad4 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/CptActivity.kt @@ -0,0 +1,47 @@ +package org.wordpress.android.posttypes.bridge + +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity + +/** + * Marker interface for activities in the Post Types module. + * + * Implement this interface to gain access to [applyBaseSetup] and other + * module-specific activity extensions. + * + * ## Purpose + * Uses composition over inheritance to avoid extending a base activity class. + * This makes the module more portable and easier to migrate. + * + * ## Migration Notes + * When merging back into the main app: + * - Option A: Remove interface, have activities extend `BaseAppCompatActivity` + * - Option B: Keep the pattern if preferred over inheritance + * + * @see applyBaseSetup + */ +interface CptActivity + +/** + * Applies base activity setup for Post Types module activities. + * + * Call this in `onCreate()` before `super.onCreate()`: + * ``` + * override fun onCreate(savedInstanceState: Bundle?) { + * applyBaseSetup() + * super.onCreate(savedInstanceState) + * // ... + * } + * ``` + * + * ## What it does + * - Enables edge-to-edge display (handles status bar properly) + * + * ## Migration Notes + * When merging back into the main app: + * - Replace with `BaseAppCompatActivity` inheritance, or + * - Inline the setup if preferred + */ +fun T.applyBaseSetup() where T : AppCompatActivity, T : CptActivity { + enableEdgeToEdge() +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/SiteReference.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/SiteReference.kt new file mode 100644 index 000000000000..88244ceeced1 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/SiteReference.kt @@ -0,0 +1,46 @@ +package org.wordpress.android.posttypes.bridge + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Minimal site reference for the Post Types module. + * + * ## Purpose + * Provides a lightweight, module-internal representation of a site that doesn't depend on + * FluxC's SiteModel. This enforces isolation and prepares for wordpress-rs integration. + * + * ## Migration Notes + * When integrating wordpress-rs: + * - This class should be replaced with the wordpress-rs site model + * - Or converted to a domain model that maps from wordpress-rs types + * + * When merging back into the main app (if not using wordpress-rs): + * - Replace with FluxC SiteModel + * - Update [fromParcelable] callers to pass SiteModel directly + * + * @see org.wordpress.android.posttypes.bridge package documentation + */ +@Parcelize +data class SiteReference( + val id: Long, + val name: String, + val url: String +) : Parcelable { + companion object { + /** + * Creates a [SiteReference] from a Parcelable site object. + * + * This is the bridge point where the main app's SiteModel is converted to our + * module-internal representation. The main app module should call this when + * launching Post Types activities. + * + * @param siteId The site's local or remote ID + * @param siteName The site's display name + * @param siteUrl The site's URL + */ + fun create(siteId: Long, siteName: String, siteUrl: String): SiteReference { + return SiteReference(id = siteId, name = siteName, url = siteUrl) + } + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/package-info.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/package-info.kt new file mode 100644 index 000000000000..fb94d3f183c4 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/bridge/package-info.kt @@ -0,0 +1,48 @@ +/** + * # Bridge Package + * + * This package contains temporary bridging code that connects the isolated Post Types module + * to the main WordPress app module. + * + * ## Purpose + * + * The Post Types module is intentionally isolated to: + * 1. **Prevent accidental FluxC usage** - By not depending on FluxC, the compiler will catch + * any accidental imports of legacy patterns + * 2. **Enable clean wordpress-rs integration** - Forces deliberate decisions about data models + * and API integration from the start + * 3. **Establish new architectural patterns** - Acts as a sandbox for the new service layer + * + * ## Contents + * + * - [BridgeConstants] - Intent extra keys and other constants that mirror main app values + * - [BridgeTheme] - Standalone Material3 theme (temporary replacement for AppThemeM3) + * - [SiteReference] - Minimal site representation (temporary replacement for SiteModel) + * + * ## Migration Guide + * + * When this module is merged back into the main app or fully integrated with wordpress-rs: + * + * ### If keeping wordpress-rs integration: + * 1. Replace [SiteReference] with wordpress-rs site models + * 2. Replace [BridgeTheme] with `AppThemeM3` + * 3. Replace [BridgeConstants.EXTRA_SITE] with `WordPress.SITE` + * 4. Delete this entire bridge package + * + * ### If reverting to main module (no wordpress-rs): + * 1. Move all non-bridge files to `WordPress/src/main/java/org/wordpress/android/ui/posttypes/` + * 2. Replace bridge imports with main app equivalents: + * - `BridgeTheme` → `AppThemeM3` + * - `SiteReference` → `SiteModel` + * - `BridgeConstants.EXTRA_SITE` → `WordPress.SITE` + * 3. Delete this module from `settings.gradle` + * 4. Remove the module dependency from `WordPress/build.gradle` + * + * ## Adding New Bridge Items + * + * If you need to add new bridging code: + * 1. Add it to this package + * 2. Document the equivalent main app type/constant in KDoc + * 3. Add migration notes explaining how to replace it + */ +package org.wordpress.android.posttypes.bridge diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptFlatPostListScreen.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptFlatPostListScreen.kt new file mode 100644 index 000000000000..5ca99c18d581 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptFlatPostListScreen.kt @@ -0,0 +1,99 @@ +package org.wordpress.android.posttypes.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.wordpress.android.posttypes.CptFlatPostListUiState +import org.wordpress.android.posttypes.CptPostListItem +import org.wordpress.android.posttypes.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CptFlatPostListScreen( + uiState: CptFlatPostListUiState, + onBackClick: () -> Unit, + onPostClick: (CptPostListItem) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(uiState.postTypeLabel) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.cpt_back) + ) + } + } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + items(uiState.posts) { post -> + CptPostListItemRow( + post = post, + onClick = { onPostClick(post) } + ) + HorizontalDivider() + } + } + } +} + +@Composable +private fun CptPostListItemRow( + post: CptPostListItem, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + Text( + text = post.title, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = post.excerpt, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 4.dp) + ) + Text( + text = post.status, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(top = 4.dp) + ) + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptHierarchicalPostListScreen.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptHierarchicalPostListScreen.kt new file mode 100644 index 000000000000..40c79ac48de4 --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptHierarchicalPostListScreen.kt @@ -0,0 +1,104 @@ +package org.wordpress.android.posttypes.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.wordpress.android.posttypes.CptHierarchicalPostItem +import org.wordpress.android.posttypes.CptHierarchicalPostListUiState +import org.wordpress.android.posttypes.R + +private const val INDENT_WIDTH_DP = 16 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CptHierarchicalPostListScreen( + uiState: CptHierarchicalPostListUiState, + onBackClick: () -> Unit, + onPostClick: (CptHierarchicalPostItem) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(uiState.postTypeLabel) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.cpt_back) + ) + } + } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + items(uiState.posts) { post -> + CptHierarchicalPostItemRow( + post = post, + onClick = { onPostClick(post) } + ) + HorizontalDivider() + } + } + } +} + +@Composable +private fun CptHierarchicalPostItemRow( + post: CptHierarchicalPostItem, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Indent based on hierarchy level + if (post.indent > 0) { + Spacer(modifier = Modifier.width((INDENT_WIDTH_DP * post.indent).dp)) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = post.title, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = post.status, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(top = 4.dp) + ) + } + } +} diff --git a/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptPostTypesScreen.kt b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptPostTypesScreen.kt new file mode 100644 index 000000000000..5060fcb475cc --- /dev/null +++ b/libs/posttypes/src/main/java/org/wordpress/android/posttypes/compose/CptPostTypesScreen.kt @@ -0,0 +1,82 @@ +package org.wordpress.android.posttypes.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.wordpress.android.posttypes.CptPostTypeItem +import org.wordpress.android.posttypes.CptPostTypesUiState +import org.wordpress.android.posttypes.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CptPostTypesScreen( + uiState: CptPostTypesUiState, + onBackClick: () -> Unit, + onPostTypeClick: (CptPostTypeItem) -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.cpt_post_types_screen_title)) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.cpt_back) + ) + } + } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + items(uiState.postTypes) { postType -> + CptPostTypeListItem( + postType = postType, + onClick = { onPostTypeClick(postType) } + ) + HorizontalDivider() + } + } + } +} + +@Composable +private fun CptPostTypeListItem( + postType: CptPostTypeItem, + onClick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 16.dp) + ) { + Text( + text = postType.label, + style = MaterialTheme.typography.bodyLarge + ) + } +} diff --git a/libs/posttypes/src/main/res/values/strings.xml b/libs/posttypes/src/main/res/values/strings.xml new file mode 100644 index 000000000000..44f71f7b2242 --- /dev/null +++ b/libs/posttypes/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Post Types + Back + diff --git a/libs/posttypes/src/main/res/values/styles.xml b/libs/posttypes/src/main/res/values/styles.xml new file mode 100644 index 000000000000..5d2a614f03eb --- /dev/null +++ b/libs/posttypes/src/main/res/values/styles.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/settings.gradle b/settings.gradle index eddc1a7dd4ca..f613c71afd66 100644 --- a/settings.gradle +++ b/settings.gradle @@ -105,6 +105,7 @@ include ':libs:mocks' include ':libs:fluxc', ':libs:fluxc-annotations', ':libs:fluxc-processor' include ':libs:login' +include ':libs:posttypes' apply from: './config/gradle/included_builds.gradle' apply from: './config/gradle/gradle_build_scan.gradle'