Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 15 additions & 15 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# OpenAnime Agent Guide
# OpenStream Agent Guide

This file is the operating manual for any agent working in this repository. Follow it strictly.

## Mission

OpenAnime is a native Android app for discovering and streaming anime using Jetpack Compose, Material 3 Expressive, Hilt, Retrofit, Room, and Media3.
OpenStream is a native Android app for discovering and streaming anime using Jetpack Compose, Material 3 Expressive, Hilt, Retrofit, Room, and Media3.

The job is not just to make code compile. The job is to keep the app coherent, expressive, maintainable, and unmistakably premium.

Expand All @@ -15,10 +15,10 @@ When documentation conflicts with code, trust the code.
Some repo docs are stale or use the old product name `OpenStream`. Do not propagate that drift.

For new work:
- Prefer the product name `OpenAnime` in code comments, docs, and user-facing copy unless the user explicitly asks for a rename.
- Prefer the product name `OpenStream` in code comments, docs, and user-facing copy unless the user explicitly asks for a rename.
- Verify dependencies in [gradle/libs.versions.toml](gradle/libs.versions.toml).
- Verify theme behavior in [Theme.kt](app/src/main/java/com/ivor/openanime/ui/theme/Theme.kt).
- Verify navigation in [AppNavigation.kt](app/src/main/java/com/ivor/openanime/presentation/navigation/AppNavigation.kt).
- Verify theme behavior in [Theme.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Theme.kt).
- Verify navigation in [AppNavigation.kt](app/src/main/java/com/ivor/OpenStream/presentation/navigation/AppNavigation.kt).
- Verify rules in [rules.md](rules.md).

## Non-Negotiable Rules
Expand Down Expand Up @@ -49,7 +49,7 @@ For new work:
## Current Codebase Shape

Main app entry:
- [OpenAnimeApp.kt](app/src/main/java/com/ivor/openanime/OpenAnimeApp.kt)
- [OpenStreamApp.kt](app/src/main/java/com/ivor/OpenStream/OpenStreamApp.kt)
- [MainActivity.kt](MainActivity.kt)

Important layers:
Expand Down Expand Up @@ -88,7 +88,7 @@ Important screens:

### Navigation

- Routes live in [AppNavigation.kt](app/src/main/java/com/ivor/openanime/presentation/navigation/AppNavigation.kt).
- Routes live in [AppNavigation.kt](app/src/main/java/com/ivor/OpenStream/presentation/navigation/AppNavigation.kt).
- Reuse the existing route patterns and argument style.
- If a new screen is added, wire the route, typed arguments, and callbacks cleanly.

Expand Down Expand Up @@ -150,7 +150,7 @@ The UI must not feel:

## Material 3 Expressive Rules

1. Start with the theme and component system already present in [Theme.kt](app/src/main/java/com/ivor/openanime/ui/theme/Theme.kt), [Shape.kt](app/src/main/java/com/ivor/openanime/ui/theme/Shape.kt), and [Type.kt](app/src/main/java/com/ivor/openanime/ui/theme/Type.kt).
1. Start with the theme and component system already present in [Theme.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Theme.kt), [Shape.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Shape.kt), and [Type.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Type.kt).
2. Prefer expressive components such as `MaterialExpressiveTheme`, `LoadingIndicator`, floating toolbars, connected button groups, and expressive button shapes when available.
3. Use `MaterialTheme.colorScheme` roles. Do not hardcode random colors into screens unless there is a strong artistic reason and it still harmonizes with the theme.
4. Favor surface containers over naked backgrounds when grouping content.
Expand Down Expand Up @@ -238,7 +238,7 @@ If adding motion:
## Engineering Rules For UI Work

- Before creating a custom composable, check whether a Material 3 or expressive component already solves the problem.
- Reuse existing shared UI pieces such as [AnimeCard.kt](app/src/main/java/com/ivor/openanime/presentation/components/AnimeCard.kt) and [ExpressiveBackButton.kt](app/src/main/java/com/ivor/openanime/presentation/components/ExpressiveBackButton.kt) when appropriate.
- Reuse existing shared UI pieces such as [AnimeCard.kt](app/src/main/java/com/ivor/OpenStream/presentation/components/AnimeCard.kt) and [ExpressiveBackButton.kt](app/src/main/java/com/ivor/OpenStream/presentation/components/ExpressiveBackButton.kt) when appropriate.
- If an existing shared component is weak, improve it rather than cloning variants across screens.
- Keep modifier chains readable.
- Prefer stable, named helper composables when a screen becomes too long.
Expand Down Expand Up @@ -270,7 +270,7 @@ If adding motion:
## Documentation Hygiene

If you touch documentation:
- keep product naming consistent with `OpenAnime`
- keep product naming consistent with `OpenStream`
- remove stale claims rather than stacking new contradictory notes on top
- keep docs shorter and truer rather than broader and vaguer

Expand All @@ -280,11 +280,11 @@ If you touch documentation:
- [README.md](README.md)
- [Material3ExpressiveDesignGuide.md](Material3ExpressiveDesignGuide.md)
- [material-3-expressive-guide.md](material-3-expressive-guide.md)
- [Theme.kt](app/src/main/java/com/ivor/openanime/ui/theme/Theme.kt)
- [Shape.kt](app/src/main/java/com/ivor/openanime/ui/theme/Shape.kt)
- [Type.kt](app/src/main/java/com/ivor/openanime/ui/theme/Type.kt)
- [AppNavigation.kt](app/src/main/java/com/ivor/openanime/presentation/navigation/AppNavigation.kt)
- [PlayerScreen.kt](app/src/main/java/com/ivor/openanime/presentation/player/PlayerScreen.kt)
- [Theme.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Theme.kt)
- [Shape.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Shape.kt)
- [Type.kt](app/src/main/java/com/ivor/OpenStream/ui/theme/Type.kt)
- [AppNavigation.kt](app/src/main/java/com/ivor/OpenStream/presentation/navigation/AppNavigation.kt)
- [PlayerScreen.kt](app/src/main/java/com/ivor/OpenStream/presentation/player/PlayerScreen.kt)

## Final Standard

Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>

</manifest>
27 changes: 27 additions & 0 deletions app/src/main/java/com/ivor/openanime/data/remote/GithubApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ivor.openanime.data.remote

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import retrofit2.http.GET

@Serializable
data class GithubReleaseDto(
@SerialName("tag_name") val tagName: String,
@SerialName("name") val name: String,
@SerialName("body") val body: String,
@SerialName("html_url") val htmlUrl: String,
@SerialName("published_at") val publishedAt: String,
@SerialName("assets") val assets: List<GithubAssetDto> = emptyList()
)

@Serializable
data class GithubAssetDto(
@SerialName("name") val name: String,
@SerialName("browser_download_url") val downloadUrl: String,
@SerialName("size") val size: Long
)

interface GithubApi {
@GET("repos/ivorisnoob/openstream/releases/latest")
suspend fun getLatestRelease(): GithubReleaseDto
}
22 changes: 22 additions & 0 deletions app/src/main/java/com/ivor/openanime/data/remote/TmdbApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,33 @@ import com.ivor.openanime.data.remote.model.AnimeDetailsDto
import com.ivor.openanime.data.remote.model.AnimeDto
import com.ivor.openanime.data.remote.model.SeasonDetailsDto
import com.ivor.openanime.data.remote.model.TmdbResponse
import com.ivor.openanime.data.remote.model.KeywordDto
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface TmdbApi {
@GET("discover/movie")
suspend fun discoverMovie(
@Query("page") page: Int = 1,
@Query("sort_by") sortBy: String? = null,
@Query("with_genres") withGenres: String? = null,
@Query("with_keywords") withKeywords: String? = null
): TmdbResponse<AnimeDto>

@GET("discover/tv")
suspend fun discoverTv(
@Query("page") page: Int = 1,
@Query("sort_by") sortBy: String? = null,
@Query("with_genres") withGenres: String? = null,
@Query("with_keywords") withKeywords: String? = null
): TmdbResponse<AnimeDto>

@GET("search/keyword")
suspend fun searchKeyword(
@Query("query") query: String,
@Query("page") page: Int = 1
): TmdbResponse<KeywordDto>
@GET("discover/tv")
suspend fun getPopularAnime(
@Query("page") page: Int = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ data class AnimeDto(
@SerialName("release_date") val releaseDate: String? = null,
@SerialName("vote_average") val voteAverage: Double? = null,
@SerialName("genre_ids") val genreIds: List<Int>? = null,
@SerialName("media_type") val mediaType: String? = "tv",
@SerialName("original_language") val originalLanguage: String? = null
@SerialName("media_type") val mediaType: String? = "tv",
@SerialName("original_language") val originalLanguage: String? = null,
@SerialName("popularity") val popularity: Double? = null
) {
val name: String
get() = movieTitle ?: tvName ?: ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ivor.openanime.data.remote.model

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class KeywordDto(
@SerialName("id") val id: Int,
@SerialName("name") val name: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,48 @@ class AnimeRepositoryImpl @Inject constructor(
}
}

override suspend fun discoverWithFilters(query: String, page: Int, mediaType: String, sortBy: String): Result<List<AnimeDto>> = runCatching {
var keywordIds: String? = null
if (query.isNotBlank()) {
val keywordResponse = api.searchKeyword(query, 1)
if (keywordResponse.results.isNotEmpty()) {
// Use up to top 3 matching keywords for discovery
keywordIds = keywordResponse.results.take(3).joinToString("|") { it.id.toString() }
}
}

// If a query was entered but no keywords were found, fallback to standard search
if (query.isNotBlank() && keywordIds == null) {
return searchAnime(query, page, mediaType)
}

val movies = mutableListOf<AnimeDto>()
val tvShows = mutableListOf<AnimeDto>()

if (mediaType == "movie" || mediaType == "all") {
val movieRes = api.discoverMovie(page = page, sortBy = sortBy, withKeywords = keywordIds)
movies.addAll(movieRes.results.map { it.copy(mediaType = "movie") })
}

if (mediaType == "tv" || mediaType == "all") {
val tvRes = api.discoverTv(page = page, sortBy = sortBy, withKeywords = keywordIds)
tvShows.addAll(tvRes.results.map { it.copy(mediaType = "tv") })
}

val combined = (movies + tvShows)

// Local sorting logic if "all" is selected since they are combined
when (sortBy) {
"popularity.desc" -> combined.sortedByDescending { it.popularity ?: 0.0 }
"popularity.asc" -> combined.sortedBy { it.popularity ?: 0.0 }
"vote_average.desc" -> combined.sortedByDescending { it.voteAverage ?: 0.0 }
"vote_average.asc" -> combined.sortedBy { it.voteAverage ?: 0.0 }
"first_air_date.desc", "primary_release_date.desc" -> combined.sortedByDescending { it.releaseDate ?: it.firstAirDate ?: "" }
"first_air_date.asc", "primary_release_date.asc" -> combined.sortedBy { it.releaseDate ?: it.firstAirDate ?: "" }
else -> combined
}
}

override suspend fun getAnimeDetails(id: Int): Result<AnimeDetailsDto> = runCatching {
api.getAnimeDetails(id = id)
}
Expand Down
25 changes: 24 additions & 1 deletion app/src/main/java/com/ivor/openanime/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.ivor.openanime.di
import com.ivor.openanime.BuildConfig
import com.ivor.openanime.data.remote.SubtitleApi
import com.ivor.openanime.data.remote.TmdbApi
import com.ivor.openanime.data.remote.GithubApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -109,4 +110,26 @@ object NetworkModule {
.build()
.create(SubtitleApi::class.java)
}
}

@Provides
@Singleton
fun provideGithubApi(json: Json): GithubApi {
val contentType = "application/json".toMediaType()
return Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(
OkHttpClient.Builder()
.addInterceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("Accept", "application/vnd.github+json")
.build()
)
}
.build()
)
.addConverterFactory(json.asConverterFactory(contentType))
.build()
.create(GithubApi::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface AnimeRepository {
suspend fun getAiringTodayAnime(page: Int = 1): Result<List<AnimeDto>>

suspend fun searchAnime(query: String, page: Int, filter: String = "all"): Result<List<AnimeDto>>
suspend fun discoverWithFilters(query: String, page: Int, mediaType: String, sortBy: String): Result<List<AnimeDto>>
suspend fun getAnimeDetails(id: Int): Result<AnimeDetailsDto>
suspend fun getMovieDetails(id: Int): Result<AnimeDetailsDto>
suspend fun getMediaDetails(id: Int, mediaType: String): Result<AnimeDetailsDto>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SystemUpdate
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.TopAppBar
Expand Down Expand Up @@ -62,6 +65,7 @@ fun HomeScreen(
onAnimeClick: (Int) -> Unit,
onSearchClick: () -> Unit,
onHistoryClick: () -> Unit,
onUpdateClick: () -> Unit = {},
viewModel: HomeViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Expand Down Expand Up @@ -91,6 +95,14 @@ fun HomeScreen(
style = MaterialTheme.typography.displaySmall
)
},
actions = {
IconButton(onClick = onUpdateClick) {
Icon(
imageVector = Icons.Default.SystemUpdate,
contentDescription = "Check for updates"
)
}
},
scrollBehavior = scrollBehavior
)
}
Expand Down
Loading