Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
821e013
Extract root dialog and update hosts
kalzEOS Apr 13, 2026
2384ac3
Extract root hosts and fix settings focus
kalzEOS Apr 13, 2026
7089e18
Restore transient root UI state
kalzEOS Apr 13, 2026
2f4789d
Move browse and sync state into holders
kalzEOS Apr 13, 2026
d2030c0
Move player recovery state into view model
kalzEOS Apr 13, 2026
6caba90
Group root dialog state into a holder
kalzEOS Apr 13, 2026
e2ed737
Move movie info state into a holder
kalzEOS Apr 13, 2026
d820f0c
Fold startup sync state into root holder
kalzEOS Apr 13, 2026
ce2ad2b
Move playback subtitle state into player view model
kalzEOS Apr 13, 2026
8d688ad
Update refactor checklist progress
kalzEOS Apr 13, 2026
dae6a4a
Migrate update state to StateFlow
kalzEOS Apr 13, 2026
5f438bb
Mark state flow migration complete
kalzEOS Apr 13, 2026
3efb339
Extract Xtream API routing helpers
kalzEOS Apr 13, 2026
216b432
Extract Xtream API parsing helpers
kalzEOS Apr 13, 2026
455da19
Extract content repository key helpers
kalzEOS Apr 13, 2026
a14bf9e
Extract content repository sizing helpers
kalzEOS Apr 13, 2026
7becddf
Extract content repository cache policy helpers
kalzEOS Apr 13, 2026
21d31c3
Extract content repository search helper
kalzEOS Apr 13, 2026
f27f741
Extract content repository list helper
kalzEOS Apr 13, 2026
17d40d4
Extract content repository retry policy
kalzEOS Apr 13, 2026
fac0926
Fix update and sync wiring regressions
kalzEOS Apr 13, 2026
4b1863d
Document repository split boundaries
kalzEOS Apr 13, 2026
da0779f
Extract series content repository
kalzEOS Apr 13, 2026
413865f
Extract live content repository
kalzEOS Apr 13, 2026
c5bebf7
Fix update host compile regressions
kalzEOS Apr 13, 2026
f9b1376
Extract search index repository
kalzEOS Apr 13, 2026
649ab5b
Extract VOD content repository
kalzEOS Apr 13, 2026
cf186c3
Extract sync maintenance repository
kalzEOS Apr 13, 2026
c33a069
Apply build and settings fixes
kalzEOS Apr 13, 2026
e8847da
Fix duplicate refactor checklist item
kalzEOS Apr 13, 2026
3ae0606
Extract series info repository logic
kalzEOS Apr 13, 2026
1c6cbab
Extract search content repository
kalzEOS Apr 13, 2026
9cebf27
Extract category content repository
kalzEOS Apr 13, 2026
0ca1668
Fix search extraction bridge
kalzEOS Apr 13, 2026
1cd7429
Fix playback focus restore
kalzEOS Apr 13, 2026
048065a
Fix browser focus restore across tabs
kalzEOS Apr 13, 2026
cb9c60c
Stabilize all-tab movie focus restore
kalzEOS Apr 14, 2026
1e3e523
Merge remote-tracking branch 'origin/main' into review/project-code-r…
kalzEOS Apr 14, 2026
6455166
Bump app version to 3.4.6
kalzEOS Apr 14, 2026
1d7e314
Finalize release navigation fixes
kalzEOS Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ keystore.properties
# Logs
*.log

# Local refactor checklist
XTREAM_REFACTOR_PLAN.md

# OS files
.DS_Store
Thumbs.db
Expand Down
115 changes: 115 additions & 0 deletions XTREAM_REFACTOR_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# XtreamPlayer Refactor Plan

Status legend:
- `[ ]` not started
- `[~]` in progress
- `[x]` done

## What is already done

- `[x]` `MainActivity` no longer injects and forwards repositories into the UI.
- `[x]` `RootScreen` now resolves app singletons through a Hilt entry point.
- `[x]` `SettingsRepository` is no longer manually instantiated inside `RootScreen`.
- `[x]` update-check state has been moved out of local composable state into `UpdateViewModel`.
- `[x]` `:app:compileDebugKotlin` passes after the refactor.

## Goal

Reduce the size and coupling of `MainActivityUi.kt` without changing behavior, then move the remaining app state out of the root composable and into focused view models or feature controllers.

## Phase 1: Split the root UI into smaller composables

- `[x]` Extract startup/update logic into `UpdateSection` or `UpdateHost`.
- `[x]` Extract top-level navigation shell into a dedicated composable file.
- `[x]` Extract dialog orchestration into a separate `DialogsHost`.
- `[x]` Extract playback/player wiring into a dedicated `PlayerHost`.
- `[x]` Extract settings and sync coordination into a `SettingsAndSyncHost`.
- `[x]` Keep each extracted host behavior-identical at first.
- `[x]` Re-run `:app:compileDebugKotlin` after each extraction or small batch.

Acceptance criteria:
- `MainActivityUi.kt` still compiles.
- No user-visible behavior changes.
- Each extracted block has a narrow set of inputs and outputs.

## Phase 2: Move root state into view models

- `[x]` Identify the state that currently belongs to the screen, not the component tree.
- `[x]` Identify the state that currently belongs to the screen, not the component tree.
- `[x]` Add `UiState` data classes for major sections instead of many `mutableStateOf` fields.
- `[x]` Migrate state into `StateFlow` or `MutableStateFlow` where appropriate.
- `[x]` Keep Compose state only for ephemeral UI details that are truly local.
- `[x]` Migrate browse, update, and playback orchestration first.

Suggested targets:
- `[x]` `selectedSection`
- `[x]` `navExpanded`
- `[x]` update dialog state
- `[x]` startup update check flags
- `[x]` sync progress and sync pause state
- `[x]` player retry / recovery state

Acceptance criteria:
- Screen state can be reasoned about from a small number of immutable state objects.
- ViewModels become the source of truth for long-lived UI state.

## Phase 3: Break up the repository layer

- `[ ]` Split `XtreamApi` into a transport client and parsing helpers.
- `[ ]` Split `ContentRepository` into feature-specific repositories or services.
- `[ ]` Keep cache/index logic isolated from fetch/parsing logic.
- `[ ]` Move sync coordination into a dedicated manager.

Suggested boundaries:
- `[x]` live content
- `[x]` VOD content
- `[x]` series/episode content
- `[x]` search/indexing
- `[x]` sync/cache maintenance

Acceptance criteria:
- No single repository owns networking, parsing, caching, and sync orchestration together.
- The code becomes easier to test in isolation.

Boundary map:
- `LiveContent` for live now/next fetching and live-specific cache policy.
- `VodContent` for movie info and VOD-specific loaders.
- `SeriesContent` for series episodes, season counts, and season-full loading.
- `SearchIndex` for search-page assembly and index/search readiness helpers.
- `SyncMaintenance` for cache keys, refresh handling, validation, and sync orchestration.

Suggested first split:
- `SeriesContent` or `SearchIndex`, because both have clear seams and are already close to the current method clusters.

## Phase 4: Tighten dependency injection

- `[ ]` Verify all repositories and controllers come from Hilt.
- `[ ]` Remove any remaining manual construction in composables.
- `[ ]` Make sure activities only host the UI and release lifecycle-bound resources.
- `[ ]` Prefer entry points only where Compose or lifecycle boundaries require them.

Acceptance criteria:
- No new direct instantiation of app singletons in UI code.
- `MainActivity` stays thin.

## Phase 5: Verify and stabilize

- `[ ]` Run `:app:compileDebugKotlin`
- `[ ]` Run `:app:testDebugUnitTest` if it is practical in the current branch state
- `[ ]` Smoke-test navigation, playback, update checks, and settings flows
- `[ ]` Check for regressions in startup and activity recreation

## Working rules for the next chat

- `[ ]` Make one behavior-preserving change at a time.
- `[ ]` Prefer extraction over logic rewrites.
- `[ ]` Keep each refactor shippable.
- `[ ]` If a change requires a bigger redesign, stop and split it.

## Suggested order

1. Extract dialog and update hosts.
2. Extract navigation shell and player host.
3. Move remaining long-lived state into view models.
4. Split repository/service responsibilities.
5. Re-run compile and tests after each batch.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import java.util.Properties
import org.gradle.api.provider.Property
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

val appVersionCode = 141
val appVersionName = "3.4.5"
val appVersionCode = 142
val appVersionName = "3.4.6"

plugins {
alias(libs.plugins.android.application)
Expand Down
61 changes: 51 additions & 10 deletions app/src/main/java/com/example/xtreamplayer/BrowseScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -296,23 +296,25 @@ internal fun BrowseScreen(
val filteredContinueWatchingItems by
filteredContinueWatchingFlow.collectAsStateWithLifecycle(initialValue = emptyList())

var categoryContentEnterTrigger by remember { mutableIntStateOf(0) }

LaunchedEffect(navMoveToContentTrigger) {
if (navMoveToContentTrigger <= 0) return@LaunchedEffect
val useDeterministicContentEntry =
selectedSection == Section.SETTINGS || selectedSection == Section.CATEGORIES
if (useDeterministicContentEntry) {
// These sections should always open at their top focus target.
// CATEGORIES uses its own trigger path to avoid FocusRequester-not-initialized
// warnings from calling requestFocus() on lazy-grid items that may be off-screen.
if (selectedSection == Section.CATEGORIES) {
categoryContentEnterTrigger++
return@LaunchedEffect
}
if (selectedSection == Section.SETTINGS) {
// SETTINGS always opens at its top focus target via contentItemFocusRequester.
val focusedNow =
runCatching { contentItemFocusRequester.requestFocus() }.getOrDefault(false)
if (focusedNow) {
return@LaunchedEffect
}
if (focusedNow) return@LaunchedEffect
withFrameNanos {}
val focusedAfterFrame =
runCatching { contentItemFocusRequester.requestFocus() }.getOrDefault(false)
if (focusedAfterFrame) {
return@LaunchedEffect
}
if (focusedAfterFrame) return@LaunchedEffect
focusToContentTrigger++
return@LaunchedEffect
}
Expand All @@ -329,6 +331,44 @@ internal fun BrowseScreen(
focusToContentTrigger++
}

LaunchedEffect(focusToContentTrigger) {
if (focusToContentTrigger <= 0) return@LaunchedEffect
if (selectedSection == Section.CATEGORIES) {
categoryContentEnterTrigger++
} else {
withFrameNanos {}
// moveFocus(Right) works when focus is on MenuButton (content is directly to
// its right) and doesn't require any FocusRequester to be attached.
// Fall back to contentItemFocusRequester for cases where moveFocus fails
// (e.g. the legacy nav-item fallback path).
if (!focusManager.moveFocus(FocusDirection.Right)) {
runCatching { contentItemFocusRequester.requestFocus() }
}
}
}

LaunchedEffect(moveFocusToNav) {
if (!moveFocusToNav) return@LaunchedEffect
val requester =
when (selectedSection) {
Section.ALL -> allNavItemFocusRequester
Section.CONTINUE_WATCHING -> continueWatchingNavItemFocusRequester
Section.FAVORITES -> favoritesNavItemFocusRequester
Section.MOVIES -> moviesNavItemFocusRequester
Section.SERIES -> seriesNavItemFocusRequester
Section.LIVE -> liveNavItemFocusRequester
Section.CATEGORIES -> categoriesNavItemFocusRequester
Section.LOCAL_FILES -> localFilesNavItemFocusRequester
Section.SETTINGS -> settingsNavItemFocusRequester
}
val focusedNow = runCatching { requester.requestFocus() }.getOrDefault(false)
if (!focusedNow) {
withFrameNanos {}
runCatching { requester.requestFocus() }
}
moveFocusToNav = false
}

Row(modifier = Modifier.fillMaxSize()) {
BrowseSideNavRail(
selectedSection = selectedSection,
Expand Down Expand Up @@ -605,6 +645,7 @@ Row(modifier = Modifier.fillMaxSize()) {
contentItemFocusRequester,
resumeFocusId = resumeFocusId,
resumeFocusRequester = resumeFocusRequester,
contentEnterTrigger = categoryContentEnterTrigger,
onItemFocused = onItemFocused,
onPlay = onPlay,
onPlayWithPosition = onPlayWithPositionAndQueue,
Expand Down
Loading
Loading