diff --git a/.directory b/.directory new file mode 100644 index 0000000..46de82d --- /dev/null +++ b/.directory @@ -0,0 +1,6 @@ +[Dolphin] +Timestamp=2022,1,4,18,4,55.221 +Version=4 + +[Settings] +HiddenFilesShown=true diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 76aae83..a55641d 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -1,18 +1,18 @@ - + - + - - + + - - + + diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 526b4c2..a2d7c21 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -13,7 +13,6 @@ - diff --git a/.idea/misc.xml b/.idea/misc.xml index 5197594..a43e999 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -51,6 +51,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 2f7de43..b6ba446 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,7 @@ plugins { android { - compileSdk 31 + compileSdk 33 defaultConfig { applicationId "eu.toldi.balazs.caster" @@ -82,6 +82,7 @@ dependencies { implementation 'com.google.android.material:material:1.6.1' implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.material3:material3:1.0.0-beta01" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation "androidx.compose.material:material-icons-extended:$compose_version" implementation "androidx.compose.runtime:runtime-livedata:$compose_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95a1393..c2d88e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,13 +10,18 @@ + + + + + + + @@ -42,11 +53,10 @@ android:name=".ChromecastManagerActivity" android:exported="true" android:label="@string/title_activity_chromecast_manager" - android:theme="@style/Theme.Caster.NoActionBar" > + android:theme="@style/Theme.Caster.NoActionBar"> - + - - + + - \ No newline at end of file + diff --git a/app/src/main/ic_launcher_adaptive-playstore.png b/app/src/main/ic_launcher_adaptive-playstore.png new file mode 100644 index 0000000..23b9459 Binary files /dev/null and b/app/src/main/ic_launcher_adaptive-playstore.png differ diff --git a/app/src/main/ic_new_caster-playstore.png b/app/src/main/ic_new_caster-playstore.png new file mode 100644 index 0000000..3075d78 Binary files /dev/null and b/app/src/main/ic_new_caster-playstore.png differ diff --git a/app/src/main/ic_new_caster_monochrome-playstore.png b/app/src/main/ic_new_caster_monochrome-playstore.png new file mode 100644 index 0000000..3075d78 Binary files /dev/null and b/app/src/main/ic_new_caster_monochrome-playstore.png differ diff --git a/app/src/main/java/eu/toldi/balazs/caster/ChromeCastHelper.kt b/app/src/main/java/eu/toldi/balazs/caster/ChromeCastHelper.kt index ea89933..b7a61fd 100644 --- a/app/src/main/java/eu/toldi/balazs/caster/ChromeCastHelper.kt +++ b/app/src/main/java/eu/toldi/balazs/caster/ChromeCastHelper.kt @@ -132,6 +132,7 @@ object ChromeCastHelper { } if (_streamInfo == null) { exitStatus = false + Log.e("Caster_ChromecastHelper","Failed to fetch stream!") callBack.invoke() } else { try { diff --git a/app/src/main/java/eu/toldi/balazs/caster/ChromecastManagerActivity.kt b/app/src/main/java/eu/toldi/balazs/caster/ChromecastManagerActivity.kt index 0aced4e..578e1c7 100644 --- a/app/src/main/java/eu/toldi/balazs/caster/ChromecastManagerActivity.kt +++ b/app/src/main/java/eu/toldi/balazs/caster/ChromecastManagerActivity.kt @@ -1,33 +1,43 @@ package eu.toldi.balazs.caster -import android.annotation.SuppressLint +import android.content.Context import android.content.Intent -import android.database.Cursor +import android.content.SharedPreferences import android.net.Uri import android.os.Build import android.os.Bundle -import android.provider.OpenableColumns import android.util.Log import android.view.KeyEvent -import android.webkit.MimeTypeMap import androidx.activity.ComponentActivity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* +import androidx.compose.material3.ColorScheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import coil.compose.AsyncImage import coil.compose.rememberImagePainter import com.yausername.ffmpeg.FFmpeg import com.yausername.youtubedl_android.YoutubeDL @@ -36,25 +46,111 @@ import eu.toldi.balazs.caster.helpers.FileCacheHelper import eu.toldi.balazs.caster.model.ChromecastManageViewmodel import eu.toldi.balazs.caster.services.ChromecastManagerService import eu.toldi.balazs.caster.ui.theme.CasterTheme +import eu.toldi.balazs.caster.ui.theme.getColorScheme import su.litvak.chromecast.api.v2.ChromeCast import su.litvak.chromecast.api.v2.Media import su.litvak.chromecast.api.v2.MediaStatus -import java.io.BufferedInputStream -import java.io.BufferedOutputStream -import java.io.FileOutputStream +import java.io.File +import java.util.regex.Matcher +import java.util.regex.Pattern +data class BottomNavItem( + val label: String, + val icon: ImageVector, + val route: String, +) + +object Constants { + val BottomNavItems = listOf( + BottomNavItem( + label = "Home", + icon = Icons.Filled.Home, + route = "home" + ), + BottomNavItem( + label = "Gallery", + icon = Icons.Filled.Image, + route = "gallery" + ), + BottomNavItem( + label = "Settings", + icon = Icons.Filled.Settings, + route = "settings" + ) + ) + +} + class ChromecastManagerActivity : ComponentActivity() { companion object { var chromeCast_: ChromeCast? = null const val METADATA_THUMB = "thumb" } + val pattern = + "https?:\\/\\/(?:[0-9A-Z-]+\\.)?(?:youtu\\.be\\/|youtube\\.com\\S*[^\\w\\-\\s])([\\w\\-]{11})(?=[^\\w\\-]|$)(?![?=&+%\\w]*(?:['\"][^<>]*>|<\\/a>))[?=&+%\\w]*" + + val compiledPattern: Pattern = + Pattern.compile(pattern, Pattern.CASE_INSENSITIVE) + + lateinit var colorScheme: ColorScheme + lateinit var prefs: SharedPreferences + + @Composable + fun BottomNavigationBar(navController: NavHostController) { + + BottomNavigation( + backgroundColor = colorScheme.secondary, + contentColor = colorScheme.onSecondary + ) { + // observe the backstack + val navBackStackEntry by navController.currentBackStackEntryAsState() + + // observe current route to change the icon + // color,label color when navigated + val currentRoute = navBackStackEntry?.destination?.route + + // Bottom nav items we declared + Constants.BottomNavItems.forEach { navItem -> + + // Place the bottom nav items + BottomNavigationItem( + + // it currentRoute is equal then its selected route + selected = currentRoute == navItem.route, + + // navigate on click + onClick = { + navController.navigate(navItem.route) + }, + + // Icon of navItem + icon = { + Icon(imageVector = navItem.icon, contentDescription = navItem.label) + }, + + // label + label = { + Text(text = navItem.label) + }, + alwaysShowLabel = false + ) + } + } + } + private lateinit var chromeCast: ChromeCast - private lateinit var viewModel : ChromecastManageViewmodel + private lateinit var viewModel: ChromecastManageViewmodel + + @RequiresApi(Build.VERSION_CODES.O) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + prefs = getSharedPreferences( + "eu.toldi.Caster", Context.MODE_PRIVATE + ) if (chromeCast_ == null) finish() chromeCast = chromeCast_ as ChromeCast @@ -73,56 +169,74 @@ class ChromecastManagerActivity : ComponentActivity() { } setContent { CasterTheme { - viewModel = ViewModelProvider(this).get(ChromecastManageViewmodel::class.java) - viewModel.chromeCast = chromeCast - // A surface container using the 'background' color from the theme - Surface(color = MaterialTheme.colors.background) { - Column { - //viewModel.fetchMediaStatus() - MenuBar() - Column( - modifier = Modifier - .padding(6.dp) - .fillMaxWidth() - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - var text by remember { mutableStateOf("") } + colorScheme = getColorScheme() + // remember navController so it does not + // get recreated on recomposition + val navController = rememberNavController() - OutlinedTextField( - value = text, - onValueChange = { - text = it - }, - label = { Text(stringResource(id = R.string.cast_url)) }, - modifier = Modifier.padding(vertical = 4.dp) - ) - var castEnabled by remember { mutableStateOf(true) } - Button( - enabled = castEnabled, - onClick = { - castEnabled = false - viewModel.castLink(text) { - castEnabled = true - } - }, - modifier = Modifier.padding(vertical = 4.dp) - ) { - Text(text = stringResource(id = R.string.cast)) - } - if (castEnabled.not()) { - CircularProgressIndicator() - } - PlayBackControl() - mediaStatus() + Surface(color = colorScheme.background, contentColor = colorScheme.onBackground) { + + // Scaffold Component + Scaffold( + backgroundColor = colorScheme.background, + contentColor = colorScheme.onBackground, + // Bottom navigation + bottomBar = { + BottomNavigationBar(navController = navController) + }, + content = { padding -> + // Navhost: where screens are placed + NavHostContainer(navController = navController, padding = padding) } - } + ) } } + } } + @Composable + fun NavHostContainer( + navController: NavHostController, + padding: PaddingValues + ) { + + NavHost( + navController = navController, + + // set the start destination as home + startDestination = "home", + + // Set the padding provided by scaffold + modifier = Modifier.padding(paddingValues = padding), + + builder = { + + // route : Home + composable("home") { + Column { + MenuBar() + HomeScreen() + } + } + + // route : search + composable("gallery") { + Column { + MenuBar() + ImageScreen() + } + } + + // route : profile + composable("settings") { + SettingsScreen() + } + }) + + } + @Composable fun mediaStatus() { val mediaStatus = viewModel.mediaStatus.value @@ -157,13 +271,13 @@ class ChromecastManagerActivity : ComponentActivity() { val image = mediaStatus.media.metadata[METADATA_THUMB] as String Log.e("Caster", image) - Image( - painter = rememberImagePainter(image), + AsyncImage( + model = image, contentDescription = null, modifier = Modifier.fillMaxWidth() ) } - if(mediaStatus.media.contentType.startsWith("video/")) { + if (mediaStatus.media.contentType.startsWith("video/")) { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween @@ -198,13 +312,11 @@ class ChromecastManagerActivity : ComponentActivity() { @Composable fun MenuBar() { val pickPictureLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.OpenDocument() + ActivityResultContracts.OpenDocumentTree() ) { fileUri -> if (fileUri != null) { - val cacheHelper = FileCacheHelper(applicationContext) - cacheHelper.cacheThis(listOf(fileUri)) - viewModel.castFromCache(cacheHelper.tryFileName(fileUri)) - + DocumentFile.fromTreeUri(applicationContext, fileUri) + ?.let { viewModel.addFolder(it) } } } TopAppBar( @@ -222,7 +334,7 @@ class ChromecastManagerActivity : ComponentActivity() { actions = { Row { IconButton(onClick = { - pickPictureLauncher.launch(arrayOf("image/*", "video/*")) + pickPictureLauncher.launch(Uri.EMPTY) }) { Icon( Icons.Filled.Folder, @@ -230,7 +342,9 @@ class ChromecastManagerActivity : ComponentActivity() { ) } } - } + }, + backgroundColor = colorScheme.primary, + contentColor = colorScheme.onPrimary ) } @@ -361,4 +475,172 @@ class ChromecastManagerActivity : ComponentActivity() { } return super.onKeyDown(keyCode, event) } + + @Composable + fun HomeScreen() { + // Column Composable, + Column( + modifier = Modifier + .fillMaxSize(), + // Parameters set to place the items in center + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + viewModel = + ViewModelProvider(this@ChromecastManagerActivity).get(ChromecastManageViewmodel::class.java) + viewModel.chromeCast = chromeCast + // A surface container using the 'background' color from the theme + //viewModel.fetchMediaStatus() + + Column( + modifier = Modifier + .padding(6.dp) + .fillMaxWidth() + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + var text by remember { mutableStateOf("") } + + OutlinedTextField( + value = text, + onValueChange = { + text = it + }, + label = { Text(stringResource(id = R.string.cast_url)) }, + modifier = Modifier.padding(vertical = 4.dp), + colors = TextFieldDefaults.outlinedTextFieldColors( + cursorColor = colorScheme.primary, + focusedBorderColor = + colorScheme.primary.copy(alpha = ContentAlpha.high), + focusedLabelColor = colorScheme.primary + ) + ) + var castEnabled by remember { mutableStateOf(true) } + Button( + enabled = castEnabled, + colors = ButtonDefaults.buttonColors( + backgroundColor = colorScheme.primary, + contentColor = colorScheme.onPrimary + ), + onClick = { + castEnabled = false + val link = text + viewModel.castLink(link) { + if (prefs.getBoolean("sponsorblock_enabled", false)) { + val matcher: Matcher = compiledPattern.matcher(link) + if (matcher.find()) { + val id = matcher.group(1) + Log.e("Caster_SB", id) + val intent = Intent( + applicationContext, + ChromecastManagerService::class.java + ).apply { + action = ChromecastManagerService.ACTION_SET_YT_ID + putExtra("yt_id", id) + putExtra("media_url", link) + } + application.startService(intent); + } + } + castEnabled = true + + } + }, + modifier = Modifier.padding(vertical = 4.dp) + ) { + Text(text = stringResource(id = R.string.cast)) + } + if (castEnabled.not()) { + CircularProgressIndicator(color = colorScheme.secondary) + } + PlayBackControl() + mediaStatus() + } + } + } + + + @Composable + fun SettingsScreen() { + // Column Composable, + Column( + modifier = Modifier + .fillMaxSize(), + // parameters set to place the items in center + ) { + Row() { + Text("Enable SponsorBlock") + var switched by remember { + mutableStateOf( + prefs.getBoolean( + "sponsorblock_enabled", + false + ) + ) + } + Switch(checked = switched, onCheckedChange = { + switched = !switched + prefs.edit().putBoolean("sponsorblock_enabled", switched).apply() + }) + } + } + } + + @Composable + fun ImageScreen() { + // Column Composable, + Column( + modifier = Modifier + .fillMaxSize(), + ) { + + val folders = viewModel.folderListState.value + val cacheHelper = FileCacheHelper(applicationContext) + LazyColumn() { + items(folders.size) { index -> + Card(modifier = Modifier + .fillMaxWidth() + .clickable { + val intent = Intent(applicationContext, FolderViewActivity::class.java) + FolderViewActivity.chromecastRecv = chromeCast + FolderViewActivity.folderRecv = folders[index] + startActivity(intent) + }) { + Row( + modifier = Modifier + .fillMaxSize(), + // parameters set to place the items in center + verticalAlignment = Alignment.CenterVertically + ) { + val image = folders[index].listFiles() + .firstOrNull { + it.name?.endsWith(".png") ?: false || it.name?.endsWith( + ".jpg" + ) ?: false + } + if (image != null) { + cacheHelper.cacheThis(listOf(image.uri)) + + val imageFile = File(cacheDir, cacheHelper.tryFileName(image.uri)) + Column( + modifier = Modifier + .height(80.dp) + .width(80.dp) + .padding(10.dp) + ) { + Log.e(null, imageFile.name.toString()) + Image( + painter = rememberImagePainter(imageFile), + contentDescription = null, + modifier = Modifier.fillMaxSize() + ) + } + } + Text(text = folders[index].name!!) + } + } + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/toldi/balazs/caster/FolderViewActivity.kt b/app/src/main/java/eu/toldi/balazs/caster/FolderViewActivity.kt new file mode 100644 index 0000000..846064d --- /dev/null +++ b/app/src/main/java/eu/toldi/balazs/caster/FolderViewActivity.kt @@ -0,0 +1,201 @@ +package eu.toldi.balazs.caster + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role.Companion.Image +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.documentfile.provider.DocumentFile +import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage +import coil.compose.rememberImagePainter +import coil.size.OriginalSize + +import coil.transform.CircleCropTransformation +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.rememberPagerState +import eu.toldi.balazs.caster.helpers.FileCacheHelper +import eu.toldi.balazs.caster.services.ChromecastManagerService +import eu.toldi.balazs.caster.ui.theme.CasterTheme +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import su.litvak.chromecast.api.v2.ChromeCast +import java.io.File + +class FolderViewActivity : ComponentActivity() { + + companion object { + var chromecastRecv: ChromeCast? = null + var folderRecv: DocumentFile? = null + } + + @SuppressLint("CoroutineCreationDuringComposition") + @OptIn(ExperimentalPagerApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (chromecastRecv == null || folderRecv == null) finish() + val chromeCast: ChromeCast = chromecastRecv!! + val folder: DocumentFile = folderRecv!! + val cacheHelper = FileCacheHelper(applicationContext) + setContent { + CasterTheme { + // A surface container using the 'background' color from the theme + Surface(color = MaterialTheme.colors.background) { + Column(modifier = Modifier.fillMaxSize()) { + MenuBar(title = folder.name!!) + val images = folder.listFiles() + .filter { it.name?.endsWith(".png") ?: false || it.name?.endsWith(".jpg") ?: false } + //cacheHelper.cacheThis(images.map { it.uri }) + Log.e("CastFile", images.size.toString()) + val row_count = images.size / 3 + 1 + /*LazyColumn(modifier = Modifier.fillMaxWidth()) { + items(row_count) { index -> + Row(modifier = Modifier.fillMaxWidth()){ + for (i in 0 until 3){ + if(index+i >= images.size) break + val imageFile = if (images[index+i].exists()) { + cacheHelper.cacheThis(listOf(images[index+i].uri)) + File(cacheDir, cacheHelper.tryFileName(images[index+i].uri)) + } else { + File(cacheDir, cacheHelper.tryFileName(images[index+i].uri)) + } + Card(modifier = Modifier + .padding(all = 5.dp) + .weight(1f)) { + Column { + Column( + modifier = Modifier + .height(100.dp) + .width(100.dp) + .padding(10.dp) + ) { + Image( + painter = rememberImagePainter( + data = imageFile + + ), + contentDescription = null, + modifier = Modifier.fillMaxWidth() + ) + } + //Text(imageFile.name) + } + } + } + } + } + }*/ + val pagerState = rememberPagerState() + if (!pagerState.isScrollInProgress) { + val currentPage = pagerState.currentPage + Log.e("Caster_IMG", currentPage.toString()) + GlobalScope.launch(Dispatchers.IO) { + Thread.sleep(1000) + if (pagerState.currentPage == currentPage && !pagerState.isScrollInProgress) { + val imageFile = if (images[currentPage].exists()) { + cacheHelper.cacheThis(listOf(images[currentPage].uri)) + File(cacheDir, cacheHelper.tryFileName(images[currentPage].uri)) + } else { + File(cacheDir, cacheHelper.tryFileName(images[currentPage].uri)) + } + val link = "http:/${ChromeCastHelper.getIPv4Address()}:${ChromecastManagerService.PORT}/assets/${ + imageFile.name + }" + Log.e("Caster_IMG","Casting image with url: $link") + val result = ChromeCastHelper.castLink( + link + ){ Log.e("Caster_IMG","Casting done")} + } + } + } + HorizontalPager( + modifier = Modifier.fillMaxSize(), + count = images.size, + state = pagerState + + ) { index -> + val imageFile = if (images[index].exists()) { + cacheHelper.cacheThis(listOf(images[index].uri)) + File(cacheDir, cacheHelper.tryFileName(images[index].uri)) + } else { + File(cacheDir, cacheHelper.tryFileName(images[index].uri)) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(all = 5.dp) + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .height(240.dp) + // .padding(10.dp) + ) { + Log.e( + "Caster_IMG", + "http:/${ChromeCastHelper.getIPv4Address()}:${ChromecastManagerService.PORT}/assets/${imageFile.name}" + ) + SubcomposeAsyncImage( + model = imageFile, + contentDescription = imageFile.name, + loading = { + CircularProgressIndicator() + }, + modifier = Modifier.fillMaxWidth() + ) + + } + Text(imageFile.name) + } + + } + } + } + } + } + } + } + + @Composable + fun MenuBar(title: String) { + TopAppBar( + title = { + Text(title) + }, + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon( + Icons.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.back) + ) + } + } + ) + } + + @Preview(showBackground = true) + @Composable + fun DefaultPreview() { + CasterTheme { + + } + } +} + diff --git a/app/src/main/java/eu/toldi/balazs/caster/MainActivity.kt b/app/src/main/java/eu/toldi/balazs/caster/MainActivity.kt index 0f072f8..84e0d2e 100644 --- a/app/src/main/java/eu/toldi/balazs/caster/MainActivity.kt +++ b/app/src/main/java/eu/toldi/balazs/caster/MainActivity.kt @@ -2,24 +2,39 @@ package eu.toldi.balazs.caster import android.annotation.SuppressLint import android.content.Intent +import android.graphics.ColorSpace.adapt import android.net.wifi.WifiManager import android.net.wifi.WifiManager.MulticastLock +import android.os.Build import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -29,14 +44,17 @@ import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import eu.toldi.balazs.caster.model.ChromeCastViewModel import eu.toldi.balazs.caster.ui.theme.CasterTheme +import eu.toldi.balazs.caster.ui.theme.getColorScheme import su.litvak.chromecast.api.v2.ChromeCast + + open class MainActivity : ComponentActivity() { protected lateinit var viewModel: ChromeCastViewModel protected var multicastLock: MulticastLock? = null - + protected lateinit var colorScheme: ColorScheme protected fun isViewModelInitialised() = ::viewModel.isInitialized override fun onStart() { @@ -67,13 +85,13 @@ open class MainActivity : ComponentActivity() { setContent { CasterTheme { - + colorScheme = getColorScheme() viewModel = ViewModelProvider(this).get(ChromeCastViewModel::class.java) val chromeCastState = viewModel.chromeCasts.observeAsState(initial = emptyList()) val chromeCasts = chromeCastState.value Log.e(null, chromeCasts.toString()) // A surface container using the 'background' color from the theme - Surface(color = MaterialTheme.colors.background) { + Surface(color = colorScheme.background, contentColor = colorScheme.onBackground) { Column(modifier = Modifier .fillMaxWidth() .fillMaxHeight()) { @@ -123,14 +141,14 @@ open class MainActivity : ComponentActivity() { ) { item { if (chromeCasts.isNotEmpty()) - Text(text = stringResource(id = R.string.available_chromecasts)) + Text(text = stringResource(id = R.string.available_chromecasts),color =colorScheme.onBackground) else { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { - Text(stringResource(id = R.string.looking_for_devices)) - CircularProgressIndicator() + Text(text = stringResource(id = R.string.looking_for_devices),color= colorScheme.onBackground) + CircularProgressIndicator(color = colorScheme.secondary) } } } @@ -152,7 +170,7 @@ open class MainActivity : ComponentActivity() { } AlertDialog(onDismissRequest = dismiss, title = { - Text(text = stringResource(id = R.string.add_chromecast)) + Text(text = stringResource(id = R.string.add_chromecast),color= colorScheme.onBackground) }, text = { OutlinedTextField( @@ -160,7 +178,7 @@ open class MainActivity : ComponentActivity() { onValueChange = { ipaddress = it }, - label = { Text(stringResource(id = R.string.ip_address)) }, + label = { Text(stringResource(id = R.string.ip_address),color= colorScheme.onBackground) }, modifier = Modifier.padding(vertical = 4.dp) ) }, buttons = { @@ -173,11 +191,16 @@ open class MainActivity : ComponentActivity() { add(ipaddress) dismiss() }, - modifier = Modifier.padding(all = 8.dp) + colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, contentColor = colorScheme.onPrimary), + modifier = Modifier.padding(all = 8.dp), + ) { Text(text = stringResource(id = R.string.add_chromecast)) } - Button(onClick = dismiss, modifier = Modifier.padding(all = 8.dp)) { + Button( + onClick = dismiss, + colors = ButtonDefaults.buttonColors(backgroundColor = colorScheme.primary, contentColor = colorScheme.onPrimary), + modifier = Modifier.padding(all = 8.dp)) { Text(stringResource(id = R.string.cancel)) } } @@ -221,7 +244,9 @@ open class MainActivity : ComponentActivity() { ) } - Column(modifier = Modifier.fillMaxWidth().height(80.dp), + Column(modifier = Modifier + .fillMaxWidth() + .height(80.dp), verticalArrangement = Arrangement.Center) { Text( text = "Name: " + when { @@ -265,7 +290,9 @@ open class MainActivity : ComponentActivity() { ) } } - } + }, + backgroundColor = colorScheme.primary, + contentColor = colorScheme.onPrimary ) } diff --git a/app/src/main/java/eu/toldi/balazs/caster/model/ChromecastManageViewmodel.kt b/app/src/main/java/eu/toldi/balazs/caster/model/ChromecastManageViewmodel.kt index 2850bb1..95950f2 100644 --- a/app/src/main/java/eu/toldi/balazs/caster/model/ChromecastManageViewmodel.kt +++ b/app/src/main/java/eu/toldi/balazs/caster/model/ChromecastManageViewmodel.kt @@ -4,6 +4,7 @@ import android.net.Uri import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import eu.toldi.balazs.caster.ChromeCastHelper @@ -20,6 +21,9 @@ class ChromecastManageViewmodel : ViewModel() { private val _mediaState = mutableStateOf(null) val mediaStatus: State get() = _mediaState + private val _folderListState = mutableStateOf>(listOf()) + val folderListState : State> + get() = _folderListState lateinit var chromeCast: ChromeCast @@ -107,6 +111,12 @@ class ChromecastManageViewmodel : ViewModel() { } } + fun addFolder(folder: DocumentFile){ + if(folder.isDirectory){ + _folderListState.value += folder + } + } + fun getIPv4Address(): InetAddress? { NetworkInterface.getNetworkInterfaces().toList().forEach { interf -> interf.inetAddresses.toList().forEach { inetAddress -> diff --git a/app/src/main/java/eu/toldi/balazs/caster/model/SponsorBlock.kt b/app/src/main/java/eu/toldi/balazs/caster/model/SponsorBlock.kt new file mode 100644 index 0000000..ed41f41 --- /dev/null +++ b/app/src/main/java/eu/toldi/balazs/caster/model/SponsorBlock.kt @@ -0,0 +1,32 @@ +package eu.toldi.balazs.caster.model + +import com.beust.klaxon.* +import java.net.URL + +private val klaxon = Klaxon() + +class SponsorBlock(elements: Collection) : ArrayList(elements) { + fun toJson() = klaxon.toJsonString(this) + + + companion object { + fun fromJson(json: String) = SponsorBlock(klaxon.parseArray(json)!!) + fun fromID(id: String) = SponsorBlock(klaxon.parseArray(URL(url+id).readText())!!) + private const val url = "https://sponsor.ajay.app/api/skipSegments?videoID=" + } +} + +data class SponsorBlockElement ( + val category: String, + val actionType: String, + val segment: List, + + @Json(name = "UUID") + val uuid: String, + + val locked: Long, + val votes: Long, + val videoDuration: Double, + val userID: String, + val description: String +) \ No newline at end of file diff --git a/app/src/main/java/eu/toldi/balazs/caster/services/ChromecastManagerService.kt b/app/src/main/java/eu/toldi/balazs/caster/services/ChromecastManagerService.kt index 90db981..26b0335 100644 --- a/app/src/main/java/eu/toldi/balazs/caster/services/ChromecastManagerService.kt +++ b/app/src/main/java/eu/toldi/balazs/caster/services/ChromecastManagerService.kt @@ -4,6 +4,7 @@ import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.app.Service +import android.content.Context import android.content.Intent import android.media.MediaPlayer import android.os.Build @@ -15,7 +16,9 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import eu.toldi.balazs.caster.App.Companion.CHANNEL_ID import eu.toldi.balazs.caster.ChromeCastHelper +import eu.toldi.balazs.caster.ChromecastManagerActivity import eu.toldi.balazs.caster.R +import eu.toldi.balazs.caster.model.SponsorBlock import io.ktor.application.* import io.ktor.features.* import io.ktor.http.* @@ -40,6 +43,7 @@ class ChromecastManagerService : Service() { const val ACTION_PAUSE = "action_pause" const val ACTION_REWIND = "action_rewind" const val ACTION_FAST_FORWARD = "action_fast_foward" + const val ACTION_SET_YT_ID = "action_set_yt_id" const val ACTION_NEXT = "action_next" const val ACTION_PREVIOUS = "action_previous" const val ACTION_STOP = "action_stop" @@ -54,6 +58,7 @@ class ChromecastManagerService : Service() { private lateinit var pendingIntent : PendingIntent private var mediaStatus: MediaStatus? = null private var file : File? = null + private var yt_video :YoutubeVideo? = null override fun onBind(p0: Intent?): IBinder? = null private lateinit var chromeCast: ChromeCast @@ -89,11 +94,25 @@ class ChromecastManagerService : Service() { ACTION_PAUSE -> mController.transportControls.pause() ACTION_FAST_FORWARD -> mController.transportControls.fastForward() ACTION_REWIND -> mController.transportControls.rewind() + ACTION_SET_YT_ID -> setYTID(intent) } return START_REDELIVER_INTENT } + private fun setYTID(intent: Intent) { + CoroutineScope(IO).launch { + try { + val id = intent.getStringExtra("yt_id")!! + val url = intent.getStringExtra("media_url")!! + yt_video = YoutubeVideo(id,url, SponsorBlock.fromID(id)) + Log.e(null,yt_video.toString()) + }catch (e: Exception) { + Log.e("CasterService",e.stackTraceToString()) + } + } + } + private fun initAction(intent: Intent?){ initMediaSessions() @@ -104,42 +123,91 @@ class ChromecastManagerService : Service() { Log.e(null, address) chromeCast = ChromeCast(address) chromeCast.name = name + ChromecastManagerActivity.chromeCast_ = chromeCast val notificationIntent = Intent(this, ChromecastManagerService::class.java) pendingIntent = - PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_MUTABLE) + PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Caster for ${chromeCast.name}@${address}") - .setSmallIcon(R.drawable.ic_caster_logo) + .setSmallIcon(R.drawable.ic_new_caster_monochrome_foreground) .setContentIntent(pendingIntent).build() startForeground(1, notification) CoroutineScope(IO).launch { - server.start(wait = true) + var started = false + while (!started) { + try { + server.start(wait = true) + started = true + Thread.sleep(1000) + } catch (e: Exception) { + Log.e(null,e.stackTraceToString()) + } + } } + val prefs = getSharedPreferences( + "eu.toldi.Caster", Context.MODE_PRIVATE) CoroutineScope(IO).launch { - + var errorCount = 0 while (true) { try { ChromeCastHelper.chromeCast = chromeCast var status = chromeCast.status Log.d(null, status.applications.toString()) - + if(status.runningApp.id == ChromeCastHelper.APP_BACKDROP) + errorCount++ val status_message = if (status.runningApp != null) { ChromeCastHelper.getApplicationDisplayName(status.runningApp.id) } else { "${chromeCast.name} is ready for casting" } mediaStatus = ChromeCastHelper.fetchMediaStatus() + Log.e("Caster_Service","Sb enabled: "+prefs.getBoolean("sponsorblock_enabled",false).toString()+ " yt: ${yt_video.toString()}") + if(prefs.getBoolean("sponsorblock_enabled",false) && yt_video != null){ + try { + if(yt_video!!.url.startsWith("https://youtu.be/")){ + yt_video!!.url = mediaStatus!!.media.url + } + if (mediaStatus!!.media.url != yt_video!!.url) { + yt_video = null + } + checkForSponsorBlock() + } catch (e:Exception){ + //yt_video = null + } + }else { + yt_video = null + } + + errorCount = 0 buildNotification() } catch (e: Exception) { Log.e(null,e.stackTraceToString()) + errorCount++ } - Thread.sleep(10500) + if(errorCount > 120){ + stopForeground(true) + break + } + Thread.sleep(1000) } } } + private fun checkForSponsorBlock() { + try { + + + yt_video!!.sponsorBlock.forEach { + if (mediaStatus!!.currentTime >= it.segment[0] && mediaStatus!!.currentTime <= it.segment[1]) + chromeCast.seek(it.segment[1]) + } + }catch (e: Exception) { + Log.e(null,e.stackTraceToString()) + } + } + private fun generateAction( icon: Int, title: String, @@ -147,7 +215,7 @@ class ChromecastManagerService : Service() { ): NotificationCompat.Action { val intent = Intent(applicationContext, ChromecastManagerService::class.java) intent.action = intentAction - val pendingIntent = PendingIntent.getService(applicationContext, 1, intent, 0) + val pendingIntent = PendingIntent.getService(applicationContext, 1, intent, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Action(icon,title,pendingIntent) } @@ -313,4 +381,10 @@ class ChromecastManagerService : Service() { } ) } + + data class YoutubeVideo( + val id: String, + var url: String, + val sponsorBlock: SponsorBlock + ) } \ No newline at end of file diff --git a/app/src/main/java/eu/toldi/balazs/caster/ui/theme/ColorScheme.kt b/app/src/main/java/eu/toldi/balazs/caster/ui/theme/ColorScheme.kt new file mode 100644 index 0000000..4f2478e --- /dev/null +++ b/app/src/main/java/eu/toldi/balazs/caster/ui/theme/ColorScheme.kt @@ -0,0 +1,35 @@ +package eu.toldi.balazs.caster.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material3.* + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +fun getColorScheme(): ColorScheme { + val LightColorScheme = lightColorScheme( + primary = MaterialTheme.colors.primary, + secondary = MaterialTheme.colors.secondary, + tertiary = MaterialTheme.colors.secondaryVariant, + // error, primaryContainer, onSecondary, etc. + ) + val DarkColorScheme = darkColorScheme( + primary = MaterialTheme.colors.primary, + secondary = MaterialTheme.colors.secondary, + tertiary = MaterialTheme.colors.secondaryVariant, + // error, primaryContainer, onSecondary, etc. + ) + + val darkTheme = isSystemInDarkTheme() + + val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + return when { + dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current) + dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current) + darkTheme -> DarkColorScheme + else -> LightColorScheme + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_new_caster_foreground.xml b/app/src/main/res/drawable/ic_new_caster_foreground.xml new file mode 100644 index 0000000..d7f2538 --- /dev/null +++ b/app/src/main/res/drawable/ic_new_caster_foreground.xml @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_new_caster_monochrome_foreground.xml b/app/src/main/res/drawable/ic_new_caster_monochrome_foreground.xml new file mode 100644 index 0000000..0ec7927 --- /dev/null +++ b/app/src/main/res/drawable/ic_new_caster_monochrome_foreground.xml @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_new_caster.xml b/app/src/main/res/mipmap-anydpi-v26/ic_new_caster.xml new file mode 100644 index 0000000..12d6403 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_new_caster.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_new_caster_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_new_caster_round.xml new file mode 100644 index 0000000..12d6403 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_new_caster_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_new_caster.png b/app/src/main/res/mipmap-hdpi/ic_new_caster.png new file mode 100644 index 0000000..d6e561f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_new_caster.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_new_caster_round.png b/app/src/main/res/mipmap-hdpi/ic_new_caster_round.png new file mode 100644 index 0000000..99e8096 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_new_caster_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_new_caster.png b/app/src/main/res/mipmap-mdpi/ic_new_caster.png new file mode 100644 index 0000000..47a0b6a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_new_caster.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_new_caster_round.png b/app/src/main/res/mipmap-mdpi/ic_new_caster_round.png new file mode 100644 index 0000000..6f6914a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_new_caster_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_new_caster.png b/app/src/main/res/mipmap-xhdpi/ic_new_caster.png new file mode 100644 index 0000000..273aef9 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_new_caster.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_new_caster_round.png b/app/src/main/res/mipmap-xhdpi/ic_new_caster_round.png new file mode 100644 index 0000000..6b11b18 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_new_caster_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_new_caster.png b/app/src/main/res/mipmap-xxhdpi/ic_new_caster.png new file mode 100644 index 0000000..9ac071f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_new_caster.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_new_caster_round.png b/app/src/main/res/mipmap-xxhdpi/ic_new_caster_round.png new file mode 100644 index 0000000..7d61bec Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_new_caster_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_new_caster.png b/app/src/main/res/mipmap-xxxhdpi/ic_new_caster.png new file mode 100644 index 0000000..14fb869 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_new_caster.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_new_caster_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_new_caster_round.png new file mode 100644 index 0000000..b8f0c53 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_new_caster_round.png differ diff --git a/app/src/main/res/values/ic_launcher_adaptive_background.xml b/app/src/main/res/values/ic_launcher_adaptive_background.xml new file mode 100644 index 0000000..88dcf97 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_adaptive_background.xml @@ -0,0 +1,4 @@ + + + #AAD400 + \ No newline at end of file diff --git a/app/src/main/res/values/ic_new_caster_background.xml b/app/src/main/res/values/ic_new_caster_background.xml new file mode 100644 index 0000000..478e0e5 --- /dev/null +++ b/app/src/main/res/values/ic_new_caster_background.xml @@ -0,0 +1,4 @@ + + + #AAD400 + \ No newline at end of file diff --git a/app/src/main/res/values/ic_new_caster_monochrome_background.xml b/app/src/main/res/values/ic_new_caster_monochrome_background.xml new file mode 100644 index 0000000..cc62054 --- /dev/null +++ b/app/src/main/res/values/ic_new_caster_monochrome_background.xml @@ -0,0 +1,4 @@ + + + #AAD400 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f379844..b6c82ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,5 +18,6 @@ Back Now playing Cast URL + FolderViewActivity \ No newline at end of file