Compare commits

...

10 Commits

Author SHA1 Message Date
Ivan Agosto
cbbc15d9fb Save progress 2025-02-07 18:25:23 -06:00
Ivan Agosto
10836f538d Finished videos view 2025-02-06 22:53:02 -06:00
Ivan Agosto
012fdd5b8e release v0.8.4 2025-02-06 22:32:49 -06:00
Ivan Agosto
71a6b9a31f Add menu on topbar 2025-02-04 21:49:21 -06:00
Ivan Agosto
ac9004fe36 Add implement for infinite scroll 2025-02-04 21:48:36 -06:00
Ivan Agosto
b3d8347200 Dependencies upgraded 2025-02-04 21:47:42 -06:00
Ivan Agosto
b27bfbd01d release version v0.8.3 2025-02-01 19:03:17 -06:00
Ivan Agosto
8004d14e64 Fix play private videos 2025-02-01 18:52:47 -06:00
Ivan Agosto
a530f4c059 Fix session fetch 2025-02-01 17:40:54 -06:00
Ivan Agosto
e5cbedb4ef Fist commit including jetpack compose and new Main view 2025-01-26 21:38:22 -06:00
28 changed files with 1493 additions and 35 deletions

View File

@ -8,9 +8,9 @@ android {
applicationId "org.libre.agosto.p2play"
compileSdk 35
minSdkVersion 26
targetSdkVersion 32
versionCode 13
versionName "0.8.2"
targetSdkVersion 33
versionCode 15
versionName "0.8.4"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
@ -22,6 +22,7 @@ android {
}
buildFeatures {
viewBinding = true
compose true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_18
@ -35,19 +36,29 @@ android {
kotlinOptions {
jvmTarget = '18'
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.4'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.activity:activity:1.8.0'
implementation 'androidx.activity:activity-ktx:1.10.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
implementation 'androidx.activity:activity-compose:1.10.0'
implementation platform('androidx.compose:compose-bom:2025.01.01')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.navigation:navigation-compose:2.8.6'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@ -56,5 +67,20 @@ dependencies {
implementation 'androidx.media3:media3-ui:1.3.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.3.1'
implementation "androidx.media3:media3-session:1.3.1"
implementation 'com.google.code.gson:gson:2.8.8'
implementation 'com.google.code.gson:gson:2.10.1'
implementation "androidx.compose.material3:material3:1.3.1"
androidTestImplementation platform('androidx.compose:compose-bom:2025.01.01')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
implementation "androidx.compose.runtime:runtime"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.foundation:foundation"
implementation "androidx.compose.foundation:foundation-layout"
implementation "androidx.compose.material3:material3"
implementation "androidx.compose.runtime:runtime-livedata"
implementation "androidx.compose.ui:ui-tooling"
implementation "androidx.constraintlayout:constraintlayout-compose:1.1.0"
implementation "io.coil-kt.coil3:coil-compose:3.0.4"
implementation "io.coil-kt.coil3:coil-network-okhttp:3.0.4"
}

View File

@ -13,7 +13,10 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.P2play">
<activity
android:name=".activities.MainActivity"
android:exported="false"
android:theme="@style/Theme.P2play" />
<activity
android:name=".AccountActivity"
android:exported="false"
@ -65,6 +68,11 @@
android:name=".AboutActivity"
android:exported="false" />
<activity
android:name=".activities.HostActivityV2"
android:exported="true"
android:theme="@style/Theme.P2play" />
<service
android:name=".services.PlaybackService"
android:exported="true"

View File

@ -66,11 +66,6 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
binding.content.mini.miniPlayerAuthor.setOnClickListener { this.resumeVideo() }
// binding.content.mini.setOnClickListener { this.resumeVideo() }
binding.content.mini.miniPlayPause.setOnClickListener { this.playPausePlayer() }
Handler().postDelayed({
// Title for nav_bar
binding.navView.getHeaderView(0).findViewById<TextView>(R.id.side_emailTxt).text = getString(R.string.nav_header_subtitle) + " " + this.packageManager.getPackageInfo(this.packageName, 0).versionName
}, 2000)
}
// Generic function for set data to RecyclerView
@ -374,10 +369,10 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
private fun setSideData() {
val headerView = binding.navView.getHeaderView(0)
if (ManagerSingleton.user.status == 1) {
binding.navView.menu.findItem(R.id.ml).isVisible = true
val headerView = binding.navView.getHeaderView(0)
headerView.findViewById<TextView>(R.id.side_usernameTxt).text = ManagerSingleton.user.username
headerView.findViewById<TextView>(R.id.side_emailTxt).text = ManagerSingleton.user.email
if (ManagerSingleton.user.avatar != "" && headerView.findViewById<ImageView>(R.id.side_imageView) != null) {
@ -394,6 +389,7 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte
}
} else {
binding.navView.menu.findItem(R.id.ml).isVisible = false
headerView.findViewById<TextView>(R.id.side_emailTxt).text = getString(R.string.nav_header_subtitle) + " " + this.packageManager.getPackageInfo(this.packageName, 0).versionName
}
}

View File

@ -13,6 +13,7 @@ object ManagerSingleton {
var videosCount: Int = 0
lateinit var settings: SharedPreferences
lateinit var db: Database
fun toast(text: String?, context: Context) {
android.widget.Toast.makeText(context, text, android.widget.Toast.LENGTH_SHORT).show()
}
@ -21,6 +22,8 @@ object ManagerSingleton {
db.logout()
user = UserModel()
token = TokenModel()
// TODO: Close the session in the user instance
}
fun reloadSettings() {
@ -36,4 +39,8 @@ object ManagerSingleton {
nfsw = settings.getBoolean("show_nsfw", false)
videosCount = settings.getString("videos_count", "15")!!.toInt()
}
fun isLogged (): Boolean {
return token.token !== ""
}
}

View File

@ -10,6 +10,7 @@ import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.Looper
import android.util.Log
import android.view.View
import android.view.WindowManager
import android.webkit.WebChromeClient
@ -139,7 +140,10 @@ class ReproductorActivity : AppCompatActivity() {
}
AsyncTask.execute {
videoPlayback = this.clientVideo.getVideo(this.video.uuid)
videoPlayback = if (ManagerSingleton.token.status == 1)
this.clientVideo.getVideo(this.video.uuid, ManagerSingleton.token.token)
else
this.clientVideo.getVideo(this.video.uuid)
// TODO: Make this configurable
// val bufferSize = 1024 * 1024 // 1mb
// val allocator = DefaultAllocator(true, bufferSize)

View File

@ -2,13 +2,10 @@ package org.libre.agosto.p2play
import android.content.Intent
import android.content.SharedPreferences
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import org.libre.agosto.p2play.activities.MainActivity
import org.libre.agosto.p2play.ajax.Auth
import org.libre.agosto.p2play.helpers.TaskManager
import org.libre.agosto.p2play.models.TokenModel
@ -46,7 +43,6 @@ class SplashActivity : AppCompatActivity() {
}
private fun checkUser() {
Log.d("was", "Checked")
try {
val token = db.getToken()
val user = db.getUser()
@ -56,21 +52,24 @@ class SplashActivity : AppCompatActivity() {
val clientSecret = settings.getString("client_secret", "")!!
val task = TaskManager<TokenModel>()
task.runTask({client.refreshToken(token, clientId, clientSecret)}, {
when (token.status.toString()) {
"1" -> {
db.newToken(it)
ManagerSingleton.token = it
ManagerSingleton.user = user
task.runTask(
{
client.refreshToken(token, clientId, clientSecret)
}, {
when (it.status.toString()) {
"1" -> {
db.newToken(it)
ManagerSingleton.token = it
ManagerSingleton.user = user
}
else -> ManagerSingleton.logout()
}
else -> ManagerSingleton.logout()
}
startApp()
})
} else {
ManagerSingleton.logout()
startApp()
}
startApp()
} catch (err: Exception) {
err.printStackTrace()
Thread.sleep(2000)

View File

@ -0,0 +1,69 @@
package org.libre.agosto.p2play.activities
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ChainStyle
import androidx.constraintlayout.compose.ConstraintLayout
import org.libre.agosto.p2play.MainActivity
import org.libre.agosto.p2play.activities.ui.theme.P2playTheme
class HostActivityV2: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
P2playTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ConstraintLayout(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
val (textLabel, emailField, passwordField) = createRefs()
Text("Iniciar session", modifier = Modifier.constrainAs(textLabel) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(emailField.top)
})
TextField("", {}, label = { Text("Email") }, modifier = Modifier.padding(10.dp).constrainAs(emailField) {
top.linkTo(textLabel.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(passwordField.top)
})
TextField("", {}, label = { Text("Password") }, modifier = Modifier.constrainAs(passwordField) {
top.linkTo(emailField.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
})
createVerticalChain(textLabel, emailField, passwordField, chainStyle = ChainStyle.Packed)
}
}
}
}
}
private fun instanceSelected () {
}
private fun startApp() {
runOnUiThread {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
this.finish()
}
}
}

View File

@ -0,0 +1,58 @@
package org.libre.agosto.p2play.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import org.libre.agosto.p2play.ui.Routes
import org.libre.agosto.p2play.viewModels.VideosViewModel
import org.libre.agosto.p2play.activities.ui.theme.P2playTheme
import org.libre.agosto.p2play.ui.bars.MainNavigationBar
import org.libre.agosto.p2play.ui.bars.MainTopAppBar
import org.libre.agosto.p2play.ui.bars.SearchTopBar
import org.libre.agosto.p2play.ui.views.SearchView
import org.libre.agosto.p2play.ui.views.SubscriptionsView
import org.libre.agosto.p2play.ui.views.VideosView
import org.libre.agosto.p2play.viewModels.SubscriptionsViewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val videoViewModel: VideosViewModel = viewModel()
val subscriptionsViewModel: SubscriptionsViewModel = viewModel()
val navController = rememberNavController()
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
P2playTheme {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
when(currentRoute) {
Routes.Videos.route -> MainTopAppBar(navController)
Routes.Search.route -> SearchTopBar()
else -> MainTopAppBar(navController)
}
},
bottomBar = { MainNavigationBar(navController) }
) { innerPadding ->
NavHost(navController, startDestination = Routes.Videos.route, modifier = Modifier.padding(innerPadding)) {
composable(Routes.Videos.route) { VideosView(videoViewModel) }
composable(Routes.Subscriptions.route) { SubscriptionsView(subscriptionsViewModel) }
composable(Routes.Search.route) { SearchView() }
}
}
}
}
}
}

View File

@ -0,0 +1,226 @@
package org.libre.agosto.p2play.activities.ui.theme
import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF8A5022)
val onPrimaryLight = Color(0xFFFFFFFF)
val primaryContainerLight = Color(0xFFFFDCC6)
val onPrimaryContainerLight = Color(0xFF6D390C)
val secondaryLight = Color(0xFF755845)
val onSecondaryLight = Color(0xFFFFFFFF)
val secondaryContainerLight = Color(0xFFFFDCC6)
val onSecondaryContainerLight = Color(0xFF5B412F)
val tertiaryLight = Color(0xFF5F6135)
val onTertiaryLight = Color(0xFFFFFFFF)
val tertiaryContainerLight = Color(0xFFE4E6AE)
val onTertiaryContainerLight = Color(0xFF47491F)
val errorLight = Color(0xFFBA1A1A)
val onErrorLight = Color(0xFFFFFFFF)
val errorContainerLight = Color(0xFFFFDAD6)
val onErrorContainerLight = Color(0xFF93000A)
val backgroundLight = Color(0xFFFFF8F5)
val onBackgroundLight = Color(0xFF221A15)
val surfaceLight = Color(0xFFFFF8F5)
val onSurfaceLight = Color(0xFF221A15)
val surfaceVariantLight = Color(0xFFF3DED2)
val onSurfaceVariantLight = Color(0xFF52443B)
val outlineLight = Color(0xFF84746A)
val outlineVariantLight = Color(0xFFD7C3B7)
val scrimLight = Color(0xFF000000)
val inverseSurfaceLight = Color(0xFF382F29)
val inverseOnSurfaceLight = Color(0xFFFEEDE4)
val inversePrimaryLight = Color(0xFFFFB784)
val surfaceDimLight = Color(0xFFE7D7CE)
val surfaceBrightLight = Color(0xFFFFF8F5)
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
val surfaceContainerLowLight = Color(0xFFFFF1EA)
val surfaceContainerLight = Color(0xFFFBEBE2)
val surfaceContainerHighLight = Color(0xFFF6E5DC)
val surfaceContainerHighestLight = Color(0xFFF0DFD6)
val primaryLightMediumContrast = Color(0xFF582900)
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
val primaryContainerLightMediumContrast = Color(0xFF9C5E2F)
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val secondaryLightMediumContrast = Color(0xFF493120)
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
val secondaryContainerLightMediumContrast = Color(0xFF856753)
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryLightMediumContrast = Color(0xFF363810)
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightMediumContrast = Color(0xFF6E7042)
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
val errorLightMediumContrast = Color(0xFF740006)
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
val errorContainerLightMediumContrast = Color(0xFFCF2C27)
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
val backgroundLightMediumContrast = Color(0xFFFFF8F5)
val onBackgroundLightMediumContrast = Color(0xFF221A15)
val surfaceLightMediumContrast = Color(0xFFFFF8F5)
val onSurfaceLightMediumContrast = Color(0xFF17100B)
val surfaceVariantLightMediumContrast = Color(0xFFF3DED2)
val onSurfaceVariantLightMediumContrast = Color(0xFF40342B)
val outlineLightMediumContrast = Color(0xFF5E5047)
val outlineVariantLightMediumContrast = Color(0xFF7A6A60)
val scrimLightMediumContrast = Color(0xFF000000)
val inverseSurfaceLightMediumContrast = Color(0xFF382F29)
val inverseOnSurfaceLightMediumContrast = Color(0xFFFEEDE4)
val inversePrimaryLightMediumContrast = Color(0xFFFFB784)
val surfaceDimLightMediumContrast = Color(0xFFD3C3BB)
val surfaceBrightLightMediumContrast = Color(0xFFFFF8F5)
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightMediumContrast = Color(0xFFFFF1EA)
val surfaceContainerLightMediumContrast = Color(0xFFF6E5DC)
val surfaceContainerHighLightMediumContrast = Color(0xFFEADAD1)
val surfaceContainerHighestLightMediumContrast = Color(0xFFDECFC6)
val primaryLightHighContrast = Color(0xFF492100)
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
val primaryContainerLightHighContrast = Color(0xFF703B0E)
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
val secondaryLightHighContrast = Color(0xFF3D2717)
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
val secondaryContainerLightHighContrast = Color(0xFF5E4432)
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
val tertiaryLightHighContrast = Color(0xFF2C2E07)
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
val tertiaryContainerLightHighContrast = Color(0xFF494C22)
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
val errorLightHighContrast = Color(0xFF600004)
val onErrorLightHighContrast = Color(0xFFFFFFFF)
val errorContainerLightHighContrast = Color(0xFF98000A)
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
val backgroundLightHighContrast = Color(0xFFFFF8F5)
val onBackgroundLightHighContrast = Color(0xFF221A15)
val surfaceLightHighContrast = Color(0xFFFFF8F5)
val onSurfaceLightHighContrast = Color(0xFF000000)
val surfaceVariantLightHighContrast = Color(0xFFF3DED2)
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
val outlineLightHighContrast = Color(0xFF362A22)
val outlineVariantLightHighContrast = Color(0xFF54463E)
val scrimLightHighContrast = Color(0xFF000000)
val inverseSurfaceLightHighContrast = Color(0xFF382F29)
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
val inversePrimaryLightHighContrast = Color(0xFFFFB784)
val surfaceDimLightHighContrast = Color(0xFFC5B6AD)
val surfaceBrightLightHighContrast = Color(0xFFFFF8F5)
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
val surfaceContainerLowLightHighContrast = Color(0xFFFEEDE4)
val surfaceContainerLightHighContrast = Color(0xFFF0DFD6)
val surfaceContainerHighLightHighContrast = Color(0xFFE1D1C9)
val surfaceContainerHighestLightHighContrast = Color(0xFFD3C3BB)
val primaryDark = Color(0xFFFFB784)
val onPrimaryDark = Color(0xFF4F2500)
val primaryContainerDark = Color(0xFF6D390C)
val onPrimaryContainerDark = Color(0xFFFFDCC6)
val secondaryDark = Color(0xFFE4BFA8)
val onSecondaryDark = Color(0xFF422B1B)
val secondaryContainerDark = Color(0xFF5B412F)
val onSecondaryContainerDark = Color(0xFFFFDCC6)
val tertiaryDark = Color(0xFFC8CA94)
val onTertiaryDark = Color(0xFF30330B)
val tertiaryContainerDark = Color(0xFF47491F)
val onTertiaryContainerDark = Color(0xFFE4E6AE)
val errorDark = Color(0xFFFFB4AB)
val onErrorDark = Color(0xFF690005)
val errorContainerDark = Color(0xFF93000A)
val onErrorContainerDark = Color(0xFFFFDAD6)
val backgroundDark = Color(0xFF19120D)
val onBackgroundDark = Color(0xFFF0DFD6)
val surfaceDark = Color(0xFF19120D)
val onSurfaceDark = Color(0xFFF0DFD6)
val surfaceVariantDark = Color(0xFF52443B)
val onSurfaceVariantDark = Color(0xFFD7C3B7)
val outlineDark = Color(0xFF9F8D83)
val outlineVariantDark = Color(0xFF52443B)
val scrimDark = Color(0xFF000000)
val inverseSurfaceDark = Color(0xFFF0DFD6)
val inverseOnSurfaceDark = Color(0xFF382F29)
val inversePrimaryDark = Color(0xFF8A5022)
val surfaceDimDark = Color(0xFF19120D)
val surfaceBrightDark = Color(0xFF413731)
val surfaceContainerLowestDark = Color(0xFF140D08)
val surfaceContainerLowDark = Color(0xFF221A15)
val surfaceContainerDark = Color(0xFF261E18)
val surfaceContainerHighDark = Color(0xFF312822)
val surfaceContainerHighestDark = Color(0xFF3C332D)
val primaryDarkMediumContrast = Color(0xFFFFD4B8)
val onPrimaryDarkMediumContrast = Color(0xFF3F1C00)
val primaryContainerDarkMediumContrast = Color(0xFFC5814F)
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
val secondaryDarkMediumContrast = Color(0xFFFBD5BC)
val onSecondaryDarkMediumContrast = Color(0xFF362111)
val secondaryContainerDarkMediumContrast = Color(0xFFAB8A75)
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
val tertiaryDarkMediumContrast = Color(0xFFDEE0A8)
val onTertiaryDarkMediumContrast = Color(0xFF262802)
val tertiaryContainerDarkMediumContrast = Color(0xFF929462)
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
val errorDarkMediumContrast = Color(0xFFFFD2CC)
val onErrorDarkMediumContrast = Color(0xFF540003)
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
val backgroundDarkMediumContrast = Color(0xFF19120D)
val onBackgroundDarkMediumContrast = Color(0xFFF0DFD6)
val surfaceDarkMediumContrast = Color(0xFF19120D)
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkMediumContrast = Color(0xFF52443B)
val onSurfaceVariantDarkMediumContrast = Color(0xFFEDD8CC)
val outlineDarkMediumContrast = Color(0xFFC1AEA3)
val outlineVariantDarkMediumContrast = Color(0xFF9E8D82)
val scrimDarkMediumContrast = Color(0xFF000000)
val inverseSurfaceDarkMediumContrast = Color(0xFFF0DFD6)
val inverseOnSurfaceDarkMediumContrast = Color(0xFF312822)
val inversePrimaryDarkMediumContrast = Color(0xFF6F3A0D)
val surfaceDimDarkMediumContrast = Color(0xFF19120D)
val surfaceBrightDarkMediumContrast = Color(0xFF4D423C)
val surfaceContainerLowestDarkMediumContrast = Color(0xFF0C0603)
val surfaceContainerLowDarkMediumContrast = Color(0xFF241C17)
val surfaceContainerDarkMediumContrast = Color(0xFF2F2620)
val surfaceContainerHighDarkMediumContrast = Color(0xFF3A312B)
val surfaceContainerHighestDarkMediumContrast = Color(0xFF463C35)
val primaryDarkHighContrast = Color(0xFFFFECE2)
val onPrimaryDarkHighContrast = Color(0xFF000000)
val primaryContainerDarkHighContrast = Color(0xFFFEB17B)
val onPrimaryContainerDarkHighContrast = Color(0xFF180700)
val secondaryDarkHighContrast = Color(0xFFFFECE2)
val onSecondaryDarkHighContrast = Color(0xFF000000)
val secondaryContainerDarkHighContrast = Color(0xFFE0BBA4)
val onSecondaryContainerDarkHighContrast = Color(0xFF170700)
val tertiaryDarkHighContrast = Color(0xFFF2F4BB)
val onTertiaryDarkHighContrast = Color(0xFF000000)
val tertiaryContainerDarkHighContrast = Color(0xFFC4C690)
val onTertiaryContainerDarkHighContrast = Color(0xFF0B0C00)
val errorDarkHighContrast = Color(0xFFFFECE9)
val onErrorDarkHighContrast = Color(0xFF000000)
val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
val onErrorContainerDarkHighContrast = Color(0xFF220001)
val backgroundDarkHighContrast = Color(0xFF19120D)
val onBackgroundDarkHighContrast = Color(0xFFF0DFD6)
val surfaceDarkHighContrast = Color(0xFF19120D)
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
val surfaceVariantDarkHighContrast = Color(0xFF52443B)
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
val outlineDarkHighContrast = Color(0xFFFFECE2)
val outlineVariantDarkHighContrast = Color(0xFFD2BFB3)
val scrimDarkHighContrast = Color(0xFF000000)
val inverseSurfaceDarkHighContrast = Color(0xFFF0DFD6)
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
val inversePrimaryDarkHighContrast = Color(0xFF6F3A0D)
val surfaceDimDarkHighContrast = Color(0xFF19120D)
val surfaceBrightDarkHighContrast = Color(0xFF594E47)
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
val surfaceContainerLowDarkHighContrast = Color(0xFF261E18)
val surfaceContainerDarkHighContrast = Color(0xFF382F29)
val surfaceContainerHighDarkHighContrast = Color(0xFF433933)
val surfaceContainerHighestDarkHighContrast = Color(0xFF4F453E)

View File

@ -0,0 +1,278 @@
package org.libre.agosto.p2play.activities.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val lightScheme = lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
private val darkScheme = darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
surfaceDim = surfaceDimDark,
surfaceBright = surfaceBrightDark,
surfaceContainerLowest = surfaceContainerLowestDark,
surfaceContainerLow = surfaceContainerLowDark,
surfaceContainer = surfaceContainerDark,
surfaceContainerHigh = surfaceContainerHighDark,
surfaceContainerHighest = surfaceContainerHighestDark,
)
private val mediumContrastLightColorScheme = lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
private val highContrastLightColorScheme = lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
private val mediumContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
private val highContrastDarkColorScheme = darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
@Immutable
data class ColorFamily(
val color: Color,
val onColor: Color,
val colorContainer: Color,
val onColorContainer: Color
)
val unspecified_scheme = ColorFamily(
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
)
@Composable
fun P2playTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable() () -> Unit
) {
val colorScheme = when {
// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
// val context = LocalContext.current
// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
//}
darkTheme -> darkScheme
else -> lightScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}

View File

@ -0,0 +1,9 @@
package org.libre.agosto.p2play.activities.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val AppTypography = Typography()

View File

@ -9,7 +9,7 @@ import java.net.HttpURLConnection
import java.net.URL
open class Client {
protected fun newCon(uri: String, method: String, token: String = ""): HttpURLConnection {
protected fun newCon(uri: String, method: String, token: String? = null): HttpURLConnection {
val url = URL("https://${ManagerSingleton.url}/api/v1/$uri")
val con = url.openConnection() as HttpURLConnection
@ -17,7 +17,7 @@ open class Client {
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
con.setRequestProperty("Accept", "*/*")
if (token != "") {
if (token !== null) {
con.setRequestProperty("Authorization", "Bearer $token")
}

View File

@ -201,8 +201,8 @@ class Videos : Client() {
return this.getVideos(start, "-likes")
}
fun getVideo(uuid: String): VideoModel {
val con = this.newCon("videos/$uuid", "GET")
fun getVideo(uuid: String, token: String? = null): VideoModel {
val con = this.newCon("videos/$uuid", "GET", token)
val video = VideoModel()
try {
if (con.responseCode == 200) {

View File

@ -0,0 +1,10 @@
package org.libre.agosto.p2play.domain.data
enum class VideoFilterEnum(val filter: String) {
TRENDING("trending"),
HOT("hot"),
LOCAL("local"),
RECENT("recent"),
LIKES("likes"),
POPULAR("popular")
}

View File

@ -0,0 +1,23 @@
package org.libre.agosto.p2play.helpers
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
@Composable
fun InfiniteScrollHandler(
lazyState: LazyListState,
buffer: Int = 1,
onLoadMore: () -> Unit
) {
LaunchedEffect(lazyState) {
snapshotFlow { lazyState.layoutInfo.visibleItemsInfo }
.collect { visibleItems ->
val items = lazyState.layoutInfo.totalItemsCount
if (visibleItems.isNotEmpty() && items > buffer && visibleItems.last().index >= items - 1) {
onLoadMore()
}
}
}
}

View File

@ -0,0 +1,9 @@
package org.libre.agosto.p2play.ui
import org.libre.agosto.p2play.R
sealed class Routes (val route: String, val title: Int?) {
data object Videos: Routes("videos", R.string.app_name)
data object Subscriptions: Routes("subscriptions", R.string.title_subscriptions)
data object Search: Routes("search", null)
}

View File

@ -0,0 +1,80 @@
package org.libre.agosto.p2play.ui.bars
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import org.libre.agosto.p2play.R
import org.libre.agosto.p2play.ui.Routes
@Composable
fun MainNavigationBar (navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
NavigationBar {
NavigationBarItem(
icon = { Icon(Icons.Filled.Home, "home") },
label = { Text(text = stringResource(R.string.nav_menu_videos), fontSize = 11.sp) },
selected = currentDestination?.route == Routes.Videos.route,
onClick = {
navController.navigate(Routes.Videos.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
NavigationBarItem(
icon = { Icon(painterResource(R.drawable.ic_live_tv_black_24dp), "home") },
label = { Text(stringResource(R.string.nav_subscriptions), fontSize = 11.sp) },
selected = currentDestination?.route == Routes.Subscriptions.route,
onClick = {
navController.navigate(Routes.Subscriptions.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
// NavigationBarItem(
// icon = { Icon(painterResource(R.drawable.ic_menu_slideshow), "home") },
// label = { Text(stringResource(R.string.playlists), fontSize = 11.sp) },
// selected = false,
// onClick = {}
// )
NavigationBarItem(
icon = { Icon(Icons.Filled.AccountCircle, "home") },
label = { Text(stringResource(R.string.you), fontSize = 11.sp) },
selected = false,
onClick = {}
)
}
}

View File

@ -0,0 +1,89 @@
package org.libre.agosto.p2play.ui.bars
import android.content.Intent
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import org.libre.agosto.p2play.AboutActivity
import org.libre.agosto.p2play.LoginActivity
import org.libre.agosto.p2play.ManagerSingleton
import org.libre.agosto.p2play.R
import org.libre.agosto.p2play.ui.Routes
import org.libre.agosto.p2play.SettingsActivity2
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainTopAppBar(navController: NavController, modifier: Modifier = Modifier) {
var isMenuOpen by remember { mutableStateOf(false) }
val context = LocalContext.current
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val title = when (currentDestination?.route) {
Routes.Subscriptions.route -> stringResource(Routes.Subscriptions.title!!)
else -> stringResource(R.string.app_name)
}
TopAppBar(
{ Text(title) },
modifier,
actions = {
IconButton({
navController.navigate(Routes.Search.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}) { Icon(Icons.Default.Search, "More") }
// IconButton({}) { Icon(Icons.Default.Notifications, "More") }
IconButton({ isMenuOpen = true }) {
Icon(Icons.Default.MoreVert, "More")
DropdownMenu(isMenuOpen, { isMenuOpen = false }) {
if (ManagerSingleton.isLogged()) {
DropdownMenuItem({ Text(stringResource(R.string.action_logout))}, {
isMenuOpen = false
ManagerSingleton.logout()
})
} else {
DropdownMenuItem({ Text(stringResource(R.string.action_login))}, {
isMenuOpen = false
val intent = Intent(context, LoginActivity::class.java)
context.startActivity(intent)
})
}
DropdownMenuItem({ Text(stringResource(R.string.action_settings))}, {
isMenuOpen = false
val intent = Intent(context, SettingsActivity2::class.java)
context.startActivity(intent)
})
DropdownMenuItem({ Text(stringResource(R.string.nav_about))}, {
isMenuOpen = false
val intent = Intent(context, AboutActivity::class.java)
context.startActivity(intent)
})
}
}
}
)
}

View File

@ -0,0 +1,52 @@
package org.libre.agosto.p2play.ui.bars
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchTopBar () {
var searchText by remember { mutableStateOf("") }
var active by remember { mutableStateOf(true) }
SearchBar(
query = searchText,
onQueryChange = { searchText = it },
onSearch = { active = false },
active = active,
onActiveChange = { active = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Buscar...") },
leadingIcon = {
IconButton (onClick = {}) {
Icon(Icons.Default.ArrowBack, contentDescription = "Volver")
}
},
trailingIcon = {
if (searchText.isNotEmpty()) {
IconButton(onClick = { searchText = "" }) {
Icon(Icons.Default.Close, contentDescription = "Limpiar")
}
}
}) {
if (searchText.isNotEmpty()) {
Text("Resultados para: $searchText")
} else {
Text("Ingresa un término de búsqueda")
}
}
}

View File

@ -0,0 +1,47 @@
package org.libre.agosto.p2play.ui.components.molecules
import android.graphics.drawable.Icon
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
interface ChipValues<T> {
val text: String
val value: T
val icon: ImageVector?
}
@Composable
fun <T> ChipSelector (values: ArrayList<ChipValues<T>>, value: T? = null, modifier: Modifier = Modifier, callback: (T) -> Unit) {
Row (modifier.horizontalScroll(rememberScrollState())) {
Spacer(modifier.width(10.dp))
for (v in values) {
val isSelected = value == v.value
FilterChip(
selected = isSelected,
{ callback(v.value) },
label = { Text(v.text) },
leadingIcon = {
if (isSelected) {
Icon(Icons.Default.Check, "", Modifier.height(20.dp))
} else if (v.icon !== null) {
Icon(v.icon!!, "", Modifier.height(20.dp))
}
}
)
Spacer(modifier.width(10.dp))
}
}
}

View File

@ -0,0 +1,113 @@
package org.libre.agosto.p2play.ui.components.organisms
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import coil3.compose.AsyncImage
import org.libre.agosto.p2play.ChannelActivity
import org.libre.agosto.p2play.ManagerSingleton
import org.libre.agosto.p2play.R
import org.libre.agosto.p2play.ReproductorActivity
import org.libre.agosto.p2play.activities.ui.theme.AppTypography
import org.libre.agosto.p2play.helpers.mapSeconds
import org.libre.agosto.p2play.models.VideoModel
@Composable
fun VideoItem (
videoData: VideoModel
) {
val context = LocalContext.current
val userImgSource = if (videoData.userImageUrl !== "") {
"https://${ManagerSingleton.url}${videoData.userImageUrl}"
} else {
R.drawable.default_avatar
}
Card({
val intent = Intent(context, ReproductorActivity::class.java)
intent.putExtra("video", videoData)
context.startActivity(intent)
}, modifier = Modifier.padding(vertical = 10.dp)) {
ConstraintLayout() {
val (thumbailImg, durationTxt, titleTxt, infoBox, userImg) = createRefs()
Box(modifier = Modifier.fillMaxWidth().aspectRatio(16f / 9f).constrainAs(thumbailImg) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}) {
AsyncImage(
"https://${ManagerSingleton.url}${videoData.thumbUrl}",
videoData.name,
contentScale = ContentScale.Crop,
modifier = Modifier.matchParentSize()
)
}
Text(mapSeconds(videoData.duration.toInt()), color = Color.Black, modifier = Modifier.alpha(0.5F).padding(5.dp).background(Color.White).constrainAs(durationTxt) {
end.linkTo(thumbailImg.end)
bottom.linkTo(thumbailImg.bottom)
})
Box(modifier = Modifier.constrainAs(userImg) {
top.linkTo(thumbailImg.bottom)
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}.clickable {
val intent = Intent(context, ChannelActivity::class.java)
intent.putExtra("channel", videoData.getChannel())
context.startActivity(intent)
}) {
AsyncImage(
userImgSource,
"Profile photo",
contentScale = ContentScale.Fit,
modifier = Modifier.padding(10.dp).size(40.dp).clip(CircleShape),
)
}
Text(
videoData.name,
style = AppTypography.titleMedium,
overflow = TextOverflow.Ellipsis,
lineHeight = 19.sp,
textAlign = TextAlign.Left,
modifier = Modifier.constrainAs(titleTxt) {
top.linkTo(thumbailImg.bottom)
start.linkTo(userImg.end)
}.padding(top = 12.dp, end = 70.dp)
)
Row (modifier = Modifier.constrainAs(infoBox) {
top.linkTo(titleTxt.bottom)
start.linkTo(userImg.end)
}.padding(bottom = 5.dp)) {
Text(videoData.nameChannel, style = AppTypography.labelSmall)
Text(" - ", style = AppTypography.labelSmall)
Text("${videoData.views.toString()} ${stringResource(R.string.view_text)}", style = AppTypography.labelSmall)
}
// createVerticalChain(thumbailImg, titleTxt, infoBox, chainStyle = ChainStyle.Packed)
}
}
}

View File

@ -0,0 +1,77 @@
package org.libre.agosto.p2play.ui.lists
import androidx.compose.foundation.layout.Box
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.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import org.libre.agosto.p2play.ui.components.organisms.VideoItem
import org.libre.agosto.p2play.models.VideoModel
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.libre.agosto.p2play.helpers.InfiniteScrollHandler
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoList (videos: ArrayList<VideoModel>, header: @Composable (() -> Unit)? = null, isLoading: Boolean = false, onRefresh: (() -> Unit)? = null, onLoadMore: (() -> Unit)? = null) {
var isRefreshing by remember { mutableStateOf(false) }
val lazyState = rememberLazyListState()
PullToRefreshBox(
isRefreshing,
{
if (onRefresh !== null) {
onRefresh()
}
}
) {
LazyColumn(state = lazyState) {
if (header !== null) {
item(key = "header") { header() }
}
items(videos, key = { it.id }) {
VideoItem(it)
}
if (isLoading) {
item(key = "loading") {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 20.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
}
if (onLoadMore !== null) {
LaunchedEffect (isLoading) {
snapshotFlow { isLoading }
.collect {
if (!it) {
isRefreshing = false
}
}
}
InfiniteScrollHandler(lazyState, 2) {
onLoadMore()
}
}
}

View File

@ -0,0 +1,15 @@
package org.libre.agosto.p2play.ui.views
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun SearchView (modifier: Modifier = Modifier) {
Box(modifier = modifier.background(Color.Red).width(100.dp).height(100.dp))
}

View File

@ -0,0 +1,60 @@
package org.libre.agosto.p2play.ui.views
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import kotlinx.coroutines.flow.distinctUntilChanged
import org.libre.agosto.p2play.R
import org.libre.agosto.p2play.ajax.Videos
import org.libre.agosto.p2play.ui.lists.VideoList
import org.libre.agosto.p2play.ui.components.molecules.ChipSelector
import org.libre.agosto.p2play.ui.components.molecules.ChipValues
import org.libre.agosto.p2play.helpers.TaskManager
import org.libre.agosto.p2play.models.VideoModel
import org.libre.agosto.p2play.viewModels.SubscriptionsViewModel
import java.util.ArrayList
@SuppressLint("MutableCollectionMutableState")
@Composable
fun SubscriptionsView (subscriptionsViewModel: SubscriptionsViewModel, modifier: Modifier = Modifier) {
val videos by subscriptionsViewModel.videos.observeAsState(initial = listOf())
val isLoading: Boolean by subscriptionsViewModel.isLoading.observeAsState(initial = false)
LaunchedEffect(Unit) {
if (videos.isEmpty()) {
subscriptionsViewModel.loadVideos()
}
}
Column (modifier) {
VideoList(
ArrayList(videos),
isLoading = isLoading,
onRefresh = {
if (!isLoading) {
subscriptionsViewModel.refresh()
}
},
onLoadMore = {
if (!isLoading) {
subscriptionsViewModel.loadVideos()
}
}
)
}
}

View File

@ -0,0 +1,83 @@
package org.libre.agosto.p2play.ui.views
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import org.libre.agosto.p2play.R
import org.libre.agosto.p2play.viewModels.VideosViewModel
import org.libre.agosto.p2play.ui.lists.VideoList
import org.libre.agosto.p2play.ui.components.molecules.ChipSelector
import org.libre.agosto.p2play.ui.components.molecules.ChipValues
import org.libre.agosto.p2play.domain.data.VideoFilterEnum
import java.util.ArrayList
@SuppressLint("MutableCollectionMutableState")
@Composable
fun VideosView (videosViewModel: VideosViewModel,modifier: Modifier = Modifier) {
val chips = arrayListOf(
object : ChipValues<VideoFilterEnum> {
override val text = stringResource(R.string.nav_trending)
override val value = VideoFilterEnum.TRENDING
override val icon = ImageVector.vectorResource(R.drawable.ic_trending_up_black_24dp)
},
object : ChipValues<VideoFilterEnum> {
override val text = stringResource(R.string.nav_likes)
override val value = VideoFilterEnum.LIKES
override val icon = ImageVector.vectorResource(R.drawable.ic_thumb_up_black_24dp)
},
object : ChipValues<VideoFilterEnum> {
override val text = stringResource(R.string.nav_popular)
override val value = VideoFilterEnum.POPULAR
override val icon = Icons.Filled.Star
},
object : ChipValues<VideoFilterEnum> {
override val text = stringResource(R.string.nav_recent)
override val value = VideoFilterEnum.RECENT
override val icon = ImageVector.vectorResource(R.drawable.ic_add_circle_black_24dp)
},
object : ChipValues<VideoFilterEnum> {
override val text = stringResource(R.string.nav_local)
override val value = VideoFilterEnum.LOCAL
override val icon = ImageVector.vectorResource(R.drawable.ic_home_black_24dp)
}
)
val videoFilter by videosViewModel.videoFilter.observeAsState(initial = VideoFilterEnum.TRENDING)
val videos by videosViewModel.videos.observeAsState(initial = listOf())
val isLoading: Boolean by videosViewModel.isLoading.observeAsState(initial = false)
LaunchedEffect(Unit) {
if (videos.isEmpty()) {
videosViewModel.loadVideos()
}
}
Column (modifier) {
VideoList(
ArrayList(videos),
header = {
ChipSelector(chips, videoFilter) {
videosViewModel.changeCategory(it)
}
},
isLoading = isLoading,
onRefresh = {
videosViewModel.refresh()
},
onLoadMore = {
if (!isLoading) {
videosViewModel.loadVideos()
}
}
)
}
}

View File

@ -0,0 +1,44 @@
package org.libre.agosto.p2play.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.libre.agosto.p2play.ManagerSingleton
import org.libre.agosto.p2play.ajax.Videos
import org.libre.agosto.p2play.models.VideoModel
class SubscriptionsViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val client = Videos()
private val _videos = MutableLiveData<List<VideoModel>>()
val videos: LiveData<List<VideoModel>> = _videos
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
fun loadVideos() {
_isLoading.value = true
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
client.videoSubscriptions(ManagerSingleton.token.token, videos.value?.size ?: 0)
}
val data = if (videos.value !== null)
ArrayList(videos.value!!)
else
ArrayList()
data.addAll(result)
_videos.postValue(data)
_isLoading.value = false
}
}
fun refresh() {
_videos.value = arrayListOf()
loadVideos()
}
}

View File

@ -0,0 +1,76 @@
package org.libre.agosto.p2play.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.libre.agosto.p2play.ajax.Videos
import org.libre.agosto.p2play.domain.data.VideoFilterEnum
import org.libre.agosto.p2play.models.VideoModel
class VideosViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
val client = Videos()
private val _videos = MutableLiveData<List<VideoModel>>()
val videos: LiveData<List<VideoModel>> = _videos
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _videoFilter = MutableLiveData<VideoFilterEnum>()
val videoFilter: LiveData<VideoFilterEnum> = _videoFilter
fun loadVideos() {
_isLoading.value = true
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
getVideoResource()
}
val data = if (videos.value !== null)
ArrayList(videos.value!!)
else
ArrayList()
data.addAll(result)
_videos.postValue(data)
_isLoading.value = false
}
}
fun changeCategory(category: VideoFilterEnum) {
_videoFilter.value = category
refresh()
}
fun refresh() {
_videos.value = arrayListOf()
loadVideos()
}
private fun getVideoResource(): ArrayList<VideoModel> {
val skip = videos.value?.size ?: 0
return when (videoFilter.value) {
VideoFilterEnum.TRENDING -> {
return client.getTrendingVideos(skip)
}
VideoFilterEnum.POPULAR -> {
client.getPopularVideos(skip)
}
VideoFilterEnum.LIKES -> {
client.getMostLikedVideos(skip)
}
VideoFilterEnum.RECENT -> {
client.getLastVideos(skip)
}
VideoFilterEnum.LOCAL -> {
client.getLocalVideos(skip)
}
else -> {
client.getTrendingVideos(skip)
}
}
}
}

View File

@ -128,7 +128,7 @@
<!-- Start Prompt string -->
<string name="reportDialog">Reason to report this video:</string>
<string name="reportDialogMsg">You reported the video</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<!-- End Prompt strings -->
<string name="playlists">Playlists</string>
<string name="you">You</string>
</resources>