diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index cd3c2574..250734cd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Report provider bug + - name: Request a new provider or report bug with an existing provider url: https://github.com/recloudstream - about: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. + about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. - name: Discord url: https://discord.gg/5Hus6fM about: Join our discord for faster support on smaller issues. diff --git a/.github/downloads.jpg b/.github/downloads.jpg index 0b671edc..ca14a664 100644 Binary files a/.github/downloads.jpg and b/.github/downloads.jpg differ diff --git a/.github/home.jpg b/.github/home.jpg index 2ccfaff4..72370d3c 100644 Binary files a/.github/home.jpg and b/.github/home.jpg differ diff --git a/.github/player.jpg b/.github/player.jpg index 0580fb03..f6959cf3 100644 Binary files a/.github/player.jpg and b/.github/player.jpg differ diff --git a/.github/results.jpg b/.github/results.jpg index 5e63169f..4dbc9b8d 100644 Binary files a/.github/results.jpg and b/.github/results.jpg differ diff --git a/.github/search.jpg b/.github/search.jpg index 998b7753..784bec89 100644 Binary files a/.github/search.jpg and b/.github/search.jpg differ diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml index 032ea8d0..3c5caad7 100644 --- a/.github/workflows/generate_dokka.yml +++ b/.github/workflows/generate_dokka.yml @@ -39,9 +39,8 @@ jobs: - name: Clean old builds run: | - shopt -s extglob cd $GITHUB_WORKSPACE/dokka/ - rm -rf !(.git) + rm -rf "./-cloudstream" - name: Setup JDK 11 uses: actions/setup-java@v1 diff --git a/.github/workflows/issue-action.yml b/.github/workflows/issue_action.yml similarity index 96% rename from .github/workflows/issue-action.yml rename to .github/workflows/issue_action.yml index bfcb10d0..79e7766c 100644 --- a/.github/workflows/issue-action.yml +++ b/.github/workflows/issue_action.yml @@ -1,63 +1,63 @@ -name: Issue automatic actions - -on: - issues: - types: [opened, edited] - -jobs: - issue-moderator: - runs-on: ubuntu-latest - steps: - - name: Generate access token - id: generate_token - uses: tibdex/github-app-token@v1 - with: - app_id: ${{ secrets.GH_APP_ID }} - private_key: ${{ secrets.GH_APP_KEY }} - - name: Similarity analysis - uses: actions-cool/issues-similarity-analysis@v1 - with: - token: ${{ steps.generate_token.outputs.token }} - filter-threshold: 0.5 - title-excludes: '' - comment-title: | - ### Your issue looks similar to these issues: - Please close if duplicate. - comment-body: '${index}. ${similarity} #${number}' - - uses: actions/checkout@v2 - - name: Automatically close issues that dont follow the issue template - uses: lucasbento/auto-close-issues@v1.0.2 - with: - github-token: ${{ steps.generate_token.outputs.token }} - issue-close-message: | - @${issue.user.login}: hello! :wave: - This issue is being automatically closed because it does not follow the issue template." - closed-issues-label: "invalid" - - name: Check if issue mentions a provider - id: provider_check - env: - GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" - run: | - wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" - pip3 install httpx - RES="$(python3 ./check_issue.py)" - echo "::set-output name=name::${RES}" - - name: Comment if issue mentions a provider - if: steps.provider_check.outputs.name != 'none' - uses: actions-cool/issues-helper@v3 - with: - actions: 'create-comment' - token: ${{ steps.generate_token.outputs.token }} - body: | - Hello ${{ github.event.issue.user.login }}. - Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). - - Found provider name: `${{ steps.provider_check.outputs.name }}` - - name: Add eyes reaction to all issues - uses: actions-cool/emoji-helper@v1.0.0 - with: - type: 'issue' - token: ${{ steps.generate_token.outputs.token }} - emoji: 'eyes' - - +name: Issue automatic actions + +on: + issues: + types: [opened] + +jobs: + issue-moderator: + runs-on: ubuntu-latest + steps: + - name: Generate access token + id: generate_token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.GH_APP_ID }} + private_key: ${{ secrets.GH_APP_KEY }} + - name: Similarity analysis + uses: actions-cool/issues-similarity-analysis@v1 + with: + token: ${{ steps.generate_token.outputs.token }} + filter-threshold: 0.5 + title-excludes: '' + comment-title: | + ### Your issue looks similar to these issues: + Please close if duplicate. + comment-body: '${index}. ${similarity} #${number}' + - uses: actions/checkout@v2 + - name: Automatically close issues that dont follow the issue template + uses: lucasbento/auto-close-issues@v1.0.2 + with: + github-token: ${{ steps.generate_token.outputs.token }} + issue-close-message: | + @${issue.user.login}: hello! :wave: + This issue is being automatically closed because it does not follow the issue template." + closed-issues-label: "invalid" + - name: Check if issue mentions a provider + id: provider_check + env: + GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}" + run: | + wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" + pip3 install httpx + RES="$(python3 ./check_issue.py)" + echo "::set-output name=name::${RES}" + - name: Comment if issue mentions a provider + if: steps.provider_check.outputs.name != 'none' + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-comment' + token: ${{ steps.generate_token.outputs.token }} + body: | + Hello ${{ github.event.issue.user.login }}. + Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). + + Found provider name: `${{ steps.provider_check.outputs.name }}` + - name: Add eyes reaction to all issues + uses: actions-cool/emoji-helper@v1.0.0 + with: + type: 'issue' + token: ${{ steps.generate_token.outputs.token }} + emoji: 'eyes' + + diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 903878ed..37161d6b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,9 +43,7 @@ jobs: echo "::set-output name=key_pwd::$KEY_PWD" - name: Run Gradle run: | - ./gradlew assemblePrerelease - ./gradlew androidSourcesJar - ./gradlew makeJar + ./gradlew assemblePrerelease makeJar androidSourcesJar env: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} diff --git a/app/build.gradle b/app/build.gradle index b80c820f..291e71ff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -206,10 +206,28 @@ task androidSourcesJar(type: Jar) { from android.sourceSets.main.java.srcDirs//full sources } +// this is used by the gradlew plugin task makeJar(type: Copy) { - // after modifying here, you can export. Jar from('build/intermediates/compile_app_classes_jar/debug') - into('build') // output location - include('classes.jar') // the classes file of the imported rack package - dependsOn build + into('build') + include('classes.jar') + dependsOn('build') } + +dokkaHtml { + moduleName.set("Cloudstream") + dokkaSourceSets { + main { + sourceLink { + // Unix based directory relative path to the root of the project (where you execute gradle respectively). + localDirectory.set(file("src/main/java")) + + // URL showing where the source code can be accessed through the web browser + remoteUrl.set(new URL( + "https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java")) + // Suffix which is used to append the line number to the URL. Use #L for GitHub + remoteLineSuffix.set("#L") + } + } + } +} \ No newline at end of file diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png index 3c4e788c..8c374dd9 100644 Binary files a/app/src/debug/ic_launcher-playstore.png and b/app/src/debug/ic_launcher-playstore.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png index bf8e595f..c947f526 100644 Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png index bf8e595f..c947f526 100644 Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png index 935b7108..e841896f 100644 Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png index 935b7108..e841896f 100644 Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_banner.png b/app/src/debug/res/mipmap-xhdpi/ic_banner.png index 16c4fdd1..6e23cfcf 100644 Binary files a/app/src/debug/res/mipmap-xhdpi/ic_banner.png and b/app/src/debug/res/mipmap-xhdpi/ic_banner.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png index d62f3f79..c80f9a10 100644 Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png index d62f3f79..c80f9a10 100644 Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png index 38d6ede0..f0b781bb 100644 Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png index 38d6ede0..f0b781bb 100644 Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png index 81c5621b..d5fa9d70 100644 Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png index 81c5621b..d5fa9d70 100644 Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 460a47ea..216a5f21 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ android:name="android.software.leanback" android:required="false" /> + + + + + + Unit)): Thread.UncaughtExceptionHandler { +class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : + Thread.UncaughtExceptionHandler { override fun uncaughtException(thread: Thread, error: Throwable) { ACRA.errorReporter.handleException(error) try { PrintStream(errorFile).use { ps -> ps.println(String.format("Enabled resource pack: ${ResourcePackManager.activePackId ?: "none"}")) ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) - ps.println(String.format("Fatal exception on thread %s (%d)", thread.name, thread.id)) + ps.println( + String.format( + "Fatal exception on thread %s (%d)", + thread.name, + thread.id + ) + ) error.printStackTrace(ps) } - } catch (ignored: FileNotFoundException) { } + } catch (ignored: FileNotFoundException) { + } try { onError.invoke() - } catch (ignored: Exception) { } + } catch (ignored: Exception) { + } exitProcess(1) } @@ -97,7 +108,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U class AcraApplication : Application() { override fun onCreate() { super.onCreate() - Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){ + Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) }) @@ -185,5 +196,15 @@ class AcraApplication : Application() { fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { context?.openBrowser(url, fallbackWebview, fragment) } + + /** Will fallback to webview if in TV layout */ + fun openBrowser(url: String, activity: FragmentActivity?) { + openBrowser( + url, + isTvSettings(), + activity?.supportFragmentManager?.fragments?.lastOrNull() + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 49143498..32df314f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -10,16 +10,22 @@ import android.util.Log import android.view.* import android.widget.TextView import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.MainThread import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.preference.PreferenceManager import com.google.android.gms.cast.framework.CastSession +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType +import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission @@ -34,6 +40,7 @@ object CommonActivity { return (this as MainActivity?)?.mSessionManager?.currentCastSession } + var canEnterPipMode: Boolean = false var canShowPipMode: Boolean = false var isInPIPMode: Boolean = false @@ -117,7 +124,7 @@ object CommonActivity { setLocale(this, localeCode) } - fun init(act: Activity?) { + fun init(act: ComponentActivity?) { if (act == null) return //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://developer.android.com/guide/topics/ui/picture-in-picture @@ -129,6 +136,22 @@ object CommonActivity { act.updateLocale() act.updateTv() NewPipe.init(DownloaderTestImpl.getInstance()) + + for (resumeApp in resumeApps) { + resumeApp.launcher = + act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val resultCode = result.resultCode + val data = result.data + if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { + val pos = data.getLongExtra(resumeApp.position, -1L) + val dur = data.getLongExtra(resumeApp.duration, -1L) + if (dur > 0L && pos > 0L) + DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur) + removeKey(resumeApp.lastId) + ResultFragment.updateUI() + } + } + } } private fun Activity.enterPIPMode() { @@ -166,6 +189,8 @@ object CommonActivity { "Light" -> R.style.LightMode "Amoled" -> R.style.AmoledMode "AmoledLight" -> R.style.AmoledModeLight + "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + R.style.MonetMode else R.style.AppTheme else -> R.style.AppTheme } @@ -186,6 +211,10 @@ object CommonActivity { "Banana" -> R.style.OverlayPrimaryColorBanana "Party" -> R.style.OverlayPrimaryColorParty "Pink" -> R.style.OverlayPrimaryColorPink + "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal + "Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal else -> R.style.OverlayPrimaryColorNormal } act.theme.applyStyle(currentTheme, true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index d282f6dd..47afbc42 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -40,6 +40,7 @@ object APIHolder { private const val defProvider = 0 + // ConcurrentModificationException is possible!!! val allProviders: MutableList = arrayListOf() fun initAll() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 0262cd5a..409da7c6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -11,10 +11,12 @@ import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.WindowManager +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IdRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy @@ -34,6 +36,8 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.loadThemes import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -52,7 +56,6 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO -import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings @@ -69,10 +72,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos +import com.lagradost.cloudstream3.utils.Event +import com.lagradost.cloudstream3.utils.IOnBackPressed import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite @@ -96,17 +100,65 @@ import java.nio.charset.Charset import kotlin.reflect.KClass -const val VLC_PACKAGE = "org.videolan.vlc" -const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result" -val VLC_COMPONENT: ComponentName = - ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity") -const val VLC_REQUEST_CODE = 42 +//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 +//https://wiki.videolan.org/Android_Player_Intents/ -const val VLC_FROM_START = -1 -const val VLC_FROM_PROGRESS = -2 -const val VLC_EXTRA_POSITION_OUT = "extra_position" -const val VLC_EXTRA_DURATION_OUT = "extra_duration" -const val VLC_LAST_ID_KEY = "vlc_last_open_id" +//https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904 +//https://mpv-android.github.io/mpv-android/intent.html + +// https://www.webvideocaster.com/integrations + +//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 + +const val VLC_PACKAGE = "org.videolan.vlc" +const val MPV_PACKAGE = "is.xyz.mpv" +const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" + +val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") +val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + +//TODO REFACTOR AF +data class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, +) { + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } +} + +val VLC = ResultResume( + VLC_PACKAGE, + "org.videolan.vlc.player.result", + "extra_position", + "extra_duration", +) + +val MPV = ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", +) + +val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + +val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO +) // Short name for requests client to make it nicer to use @@ -152,6 +204,72 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val mainPluginsLoadedEvent = Event() // homepage api, used to speed up time to load for homepage val afterRepositoryLoadedEvent = Event() + + /** + * @return true if the str has launched an app task (be it successful or not) + * @param isWebview does not handle providers and opening download page if true. Can still add repos and login. + * */ + fun handleAppIntentUrl( + activity: FragmentActivity?, + str: String?, + isWebview: Boolean + ): Boolean = + with(activity) { + if (str != null && this != null) { + if (str.startsWith("https://cs.repo")) { + val realUrl = "https://" + str.substringAfter("?") + println("Repository url: $realUrl") + loadRepository(realUrl) + return true + } else if (str.contains(appString)) { + for (api in OAuth2Apis) { + if (str.contains("/${api.redirectUrl}")) { + ioSafe { + Log.i(TAG, "handleAppIntent $str") + val isSuccessful = api.handleRedirect(str) + + if (isSuccessful) { + Log.i(TAG, "authenticated ${api.name}") + } else { + Log.i(TAG, "failed to authenticate ${api.name}") + } + + this@with.runOnUiThread { + try { + showToast( + this@with, + getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( + api.name + ) + ) + } catch (e: Exception) { + logError(e) // format might fail + } + } + } + return true + } + } + } else if (URI(str).scheme == appStringRepo) { + val url = str.replaceFirst(appStringRepo, "https") + loadRepository(url) + return true + } else if (!isWebview) { + if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) { + this.navigate(R.id.navigation_downloads) + return true + } else { + for (api in apis) { + if (str.startsWith(api.mainUrl)) { + loadResult(str, api.name) + return true + } + } + } + } + } + return false + } } override fun onColorSelected(dialogId: Int, color: Int) { @@ -313,31 +431,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == VLC_REQUEST_CODE) { - if (resultCode == RESULT_OK && data != null) { - val pos: Long = - data.getLongExtra( - VLC_EXTRA_POSITION_OUT, - -1 - ) //Last position in media when player exited - val dur: Long = - data.getLongExtra( - VLC_EXTRA_DURATION_OUT, - -1 - ) //Last position in media when player exited - val id = getKey(VLC_LAST_ID_KEY) - println("SET KEY $id at $pos / $dur") - if (dur > 0 && pos > 0) { - setViewPos(id, pos, dur) - } - removeKey(VLC_LAST_ID_KEY) - ResultFragment.updateUI() - } - } - super.onActivityResult(requestCode, resultCode, data) - } - override fun onDestroy() { val broadcastIntent = Intent() broadcastIntent.action = "restart_service" @@ -356,56 +449,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (intent == null) return val str = intent.dataString loadCache() - if (str != null) { - if (str.startsWith("https://cs.repo")) { - val realUrl = "https://" + str.substringAfter("?") - println("Repository url: $realUrl") - loadRepository(realUrl) - } else if (str.contains(appString)) { - for (api in OAuth2Apis) { - if (str.contains("/${api.redirectUrl}")) { - val activity = this - ioSafe { - Log.i(TAG, "handleAppIntent $str") - val isSuccessful = api.handleRedirect(str) - - if (isSuccessful) { - Log.i(TAG, "authenticated ${api.name}") - } else { - Log.i(TAG, "failed to authenticate ${api.name}") - } - - activity.runOnUiThread { - try { - showToast( - activity, - getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( - api.name - ) - ) - } catch (e: Exception) { - logError(e) // format might fail - } - } - } - } - } - } else if (URI(str).scheme == appStringRepo) { - val url = str.replaceFirst(appStringRepo, "https") - loadRepository(url) - } else { - if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) { - this.navigate(R.id.navigation_downloads) - } else { - for (api in apis) { - if (str.startsWith(api.mainUrl)) { - loadResult(str, api.name) - break - } - } - } - } - } + handleAppIntentUrl(this, str, false) } private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean = @@ -453,7 +497,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } // it.hashCode() is not enough to make sure they are distinct - apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } + apis = + allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name } APIHolder.apiMap = null } catch (e: Exception) { logError(e) @@ -478,9 +523,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { lastError = errorFile.readText(Charset.defaultCharset()) errorFile.delete() } - + val settingsForProvider = SettingsJson() - settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false) + settingsForProvider.enableAdult = + settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false) MainAPI.settingsForProvider = settingsForProvider @@ -514,7 +560,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } ioSafe { - if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) { + if (settingsManager.getBoolean( + getString(R.string.auto_update_plugins_key), + true + ) + ) { PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) } else { PluginManager.loadAllOnlinePlugins(this@MainActivity) @@ -558,9 +608,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { for (api in accountManagers) { api.init() } - } - ioSafe { inAppAuths.apmap { api -> try { api.initialize() diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt new file mode 100644 index 00000000..18602664 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt @@ -0,0 +1,39 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.utils.* + +class AStreamHub : ExtractorApi() { + override val name = "AStreamHub" + override val mainUrl = "https://astreamhub.com" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List { + val sources = mutableListOf() + app.get(url).document.selectFirst("body > script").let { script -> + val text = script?.html() ?: "" + Log.i("Dev", "text => $text") + if (text.isNotBlank()) { + val m3link = "(?<=file:)(.*)(?=,)".toRegex().find(text) + ?.groupValues?.get(0)?.trim()?.trim('"') ?: "" + Log.i("Dev", "m3link => $m3link") + if (m3link.isNotBlank()) { + sources.add( + ExtractorLink( + name = name, + source = name, + url = m3link, + isM3u8 = true, + quality = Qualities.Unknown.value, + referer = referer ?: url + ) + ) + } + } + } + return sources + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index 7b087157..63634704 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -3,10 +3,9 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.M3u8Helper -import com.lagradost.cloudstream3.utils.loadExtractor +import com.lagradost.cloudstream3.utils.* +import kotlinx.coroutines.delay +import java.net.URI class VidSrcExtractor2 : VidSrcExtractor() { override val mainUrl = "https://vidsrc.me/embed" @@ -27,6 +26,25 @@ open class VidSrcExtractor : ExtractorApi() { override val mainUrl = "$absoluteUrl/embed" override val requiresReferer = false + companion object { + /** Infinite function to validate the vidSrc pass */ + suspend fun validatePass(url: String) { + val uri = URI(url) + val host = uri.host + + // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/ + val referer = host.split(".").let { + val size = it.size + "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/" + } + + while (true) { + app.get(url, referer = referer) + delay(60_000) + } + } + } + override suspend fun getUrl( url: String, referer: String?, @@ -40,7 +58,10 @@ open class VidSrcExtractor : ExtractorApi() { val datahash = it.attr("data-hash") if (datahash.isNotBlank()) { val links = try { - app.get("$absoluteUrl/src/$datahash", referer = "https://source.vidsrc.me/").url + app.get( + "$absoluteUrl/src/$datahash", + referer = "https://source.vidsrc.me/" + ).url } catch (e: Exception) { "" } @@ -54,11 +75,28 @@ open class VidSrcExtractor : ExtractorApi() { val srcresponse = app.get(server, referer = absoluteUrl).text val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap - M3u8Helper.generateM3u8( - name, - srcm3u8, - absoluteUrl - ).forEach(callback) + val passRegex = Regex("""['"](.*set_pass[^"']*)""") + val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( + Regex("""^//"""), "https://" + ) + + callback.invoke( + ExtractorLink( + this.name, + this.name, + srcm3u8, + this.mainUrl, + Qualities.Unknown.value, + extractorData = pass, + isM3u8 = true + ) + ) + +// M3u8Helper.generateM3u8( +// name, +// srcm3u8, +// absoluteUrl +// ).forEach(callback) } else { loadExtractor(linkfixed, url, subtitleCallback, callback) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 5c3276fa..e5c03d64 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -15,6 +15,7 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!" +const val DEBUG_PRINT = "DEBUG PRINT" class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message") @@ -24,6 +25,12 @@ inline fun debugException(message: () -> String) { } } +inline fun debugPrint(tag: String = DEBUG_PRINT, message: () -> String) { + if (BuildConfig.DEBUG) { + Log.d(tag, message.invoke()) + } +} + inline fun debugWarning(message: () -> String) { if (BuildConfig.DEBUG) { logError(DebugException(message.invoke())) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 29603a5f..3b55a99a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -1,13 +1,17 @@ package com.lagradost.cloudstream3.plugins +import android.app.* import dalvik.system.PathClassLoader import com.google.gson.Gson import android.content.res.AssetManager import android.content.res.Resources import android.os.Environment import android.widget.Toast -import android.app.Activity +import android.content.Context +import android.os.Build import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -22,15 +26,18 @@ import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.resources.ResourcePackManager import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import org.acra.log.debug import java.io.File import java.io.InputStreamReader import java.util.* @@ -39,6 +46,9 @@ import java.util.* const val PLUGINS_KEY = "PLUGINS_KEY" const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL" +const val EXTENSIONS_CHANNEL_ID = "cloudstream3.extensions" +const val EXTENSIONS_CHANNEL_NAME = "Extensions" +const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel" // Data class for internal storage data class PluginData( @@ -79,6 +89,8 @@ object PluginManager { const val TAG = "PluginManager" + private var hasCreatedNotChanel = false + /** * Store data about the plugin for fetching later * */ @@ -113,6 +125,10 @@ object PluginManager { val plugins = getPluginsOnline().filter { !it.filePath.contains(repositoryPath) } + val file = File(repositoryPath) + normalSafeApiCall { + if (file.exists()) file.deleteRecursively() + } setKey(PLUGINS_KEY, plugins) } } @@ -164,8 +180,16 @@ object PluginManager { val onlineData: Pair, ) { val isOutdated = - onlineData.second.version != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE + onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN + + fun validOnlineData(context: Context): Boolean { + return getPluginPath( + context, + savedData.internalName, + onlineData.first + ).absolutePath == savedData.filePath + } } // var allCurrentOutDatedPlugins: Set = emptySet() @@ -211,29 +235,42 @@ object PluginManager { // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated val outdatedPlugins = getPluginsOnline().map { savedData -> - onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName } + onlinePlugins + .filter { onlineData -> savedData.internalName == onlineData.second.internalName } .map { onlineData -> OnlinePluginData(savedData, onlineData) + }.filter { + it.validOnlineData(activity) } }.flatten().distinctBy { it.onlineData.second.url } - debug { + debugPrint { "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" } + val updatedPlugins = mutableListOf() + outdatedPlugins.apmap { pluginData -> if (pluginData.isDisabled) { + //updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name)) unloadPlugin(pluginData.savedData.filePath) } else if (pluginData.isOutdated) { downloadAndLoadPlugin( activity, pluginData.onlineData.second.url, pluginData.savedData.internalName, - pluginData.onlineData.first - ) + File(pluginData.savedData.filePath) + ).let { success -> + if (success) + updatedPlugins.add(pluginData.onlineData.second.name) + } } } + main { + createNotification(activity, updatedPlugins) + } + ioSafe { afterPluginsLoadedEvent.invoke(true) } @@ -396,24 +433,61 @@ object PluginManager { ) + "." + name.hashCode() } + /** + * This should not be changed as it is used to also detect if a plugin is installed! + **/ + fun getPluginPath( + context: Context, + internalName: String, + repositoryUrl: String + ): File { + val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique + val fileName = getPluginSanitizedFileName(internalName) + return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3") + } + + /** + * Used for fresh installs + * */ suspend fun downloadAndLoadPlugin( activity: Activity, pluginUrl: String, internalName: String, repositoryUrl: String ): Boolean { - try { - val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique - val fileName = getPluginSanitizedFileName(internalName) - unloadPlugin("${activity.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3") + val file = getPluginPath(activity, internalName, repositoryUrl) + downloadAndLoadPlugin(activity, pluginUrl, internalName, file) + return true + } - Log.d(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName") + /** + * Used for updates. + * + * Uses a file instead of repository url, as extensions can get moved it is better to directly + * update the files instead of getting the filepath from repo url. + * */ + private suspend fun downloadAndLoadPlugin( + activity: Activity, + pluginUrl: String, + internalName: String, + file: File, + ): Boolean { + try { + unloadPlugin(file.absolutePath) + + Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}") // The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names - val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName) + val newFile = downloadPluginToFile(pluginUrl, file) return loadPlugin( activity, - file ?: return false, - PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET) + newFile ?: return false, + PluginData( + internalName, + pluginUrl, + true, + newFile.absolutePath, + PLUGIN_VERSION_NOT_SET + ) ) } catch (e: Exception) { logError(e) @@ -421,18 +495,13 @@ object PluginManager { } } - /** - * @param isFilePath will treat the pluginUrl as as the filepath instead of url - * */ - suspend fun deletePlugin(pluginIdentifier: String, isFilePath: Boolean): Boolean { - val data = - (if (isFilePath) (getPluginsLocal() + getPluginsOnline()).firstOrNull { it.filePath == pluginIdentifier } - else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false + suspend fun deletePlugin(file: File): Boolean { + val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath } return try { - if (File(data.filePath).delete()) { - unloadPlugin(data.filePath) - deletePluginData(data) + if (File(file.absolutePath).delete()) { + unloadPlugin(file.absolutePath) + list.forEach { deletePluginData(it) } return true } false @@ -440,4 +509,63 @@ object PluginManager { false } } + + private fun Context.createNotificationChannel() { + hasCreatedNotChanel = true + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = EXTENSIONS_CHANNEL_NAME //getString(R.string.channel_name) + val descriptionText = + EXTENSIONS_CHANNEL_DESCRIPT//getString(R.string.channel_description) + val importance = NotificationManager.IMPORTANCE_LOW + val channel = NotificationChannel(EXTENSIONS_CHANNEL_ID, name, importance).apply { + description = descriptionText + } + // Register the channel with the system + val notificationManager: NotificationManager = + this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + private fun createNotification( + context: Context, + extensionNames: List + ): Notification? { + try { + if (extensionNames.isEmpty()) return null + + val content = extensionNames.joinToString(", ") +// main { // DON'T WANT TO SLOW IT DOWN + val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID) + .setAutoCancel(false) + .setColorized(true) + .setOnlyAlertOnce(true) + .setSilent(true) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setColor(context.colorFromAttribute(R.attr.colorPrimary)) + .setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size)) + .setSmallIcon(R.drawable.ic_baseline_extension_24) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(content) + ) + .setContentText(content) + + if (!hasCreatedNotChanel) { + context.createNotificationChannel() + } + + val notification = builder.build() + with(NotificationManagerCompat.from(context)) { + // notificationId is a unique int for each notification that you must define + notify((System.currentTimeMillis() / 1000).toInt(), notification) + } + return notification + } catch (e: Exception) { + logError(e) + return null + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index ee0b60b8..e3203787 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -84,7 +84,7 @@ object RepositoryManager { // Normal parsed function not working? // return response.parsedSafe() tryParseJson>(response.text)?.toList() ?: emptyList() - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) emptyList() } @@ -102,9 +102,27 @@ object RepositoryManager { }.flatten() } + suspend fun downloadPluginToFile( + pluginUrl: String, + file: File + ): File? { + return suspendSafeApiCall { + file.mkdirs() + + // Overwrite if exists + if (file.exists()) { file.delete() } + file.createNewFile() + + val body = app.get(pluginUrl).okhttpResponse.body + write(body.byteStream(), file.outputStream()) + file + } + } + suspend fun downloadPluginToFile( context: Context, pluginUrl: String, + /** Filename without .cs3 */ fileName: String, folder: String ): File? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index ab702d71..f099ad1a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -10,6 +10,9 @@ import java.security.MessageDigest import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" @@ -23,10 +26,10 @@ object VotingApi { // please do not cheat the votes lol private val apiDomain = "https://api.countapi.xyz" private fun transformUrl(url: String): String = // dont touch or all votes get reset - MessageDigest - .getInstance("SHA-256") - .digest("${url}#funny-salt".toByteArray()) - .fold("") { str, it -> str + "%02x".format(it) } + MessageDigest + .getInstance("SHA-256") + .digest("${url}#funny-salt".toByteArray()) + .fold("") { str, it -> str + "%02x".format(it) } suspend fun SitePlugin.getVotes(): Int { return getVotes(url) @@ -53,9 +56,9 @@ object VotingApi { // please do not cheat the votes lol return votesCache[pluginUrl] ?: app.get(url).parsedSafe()?.value?.also { votesCache[pluginUrl] = it } ?: (0.also { - ioSafe { - createBucket(pluginUrl) - } + ioSafe { + createBucket(pluginUrl) + } }) } @@ -64,7 +67,8 @@ object VotingApi { // please do not cheat the votes lol } private suspend fun createBucket(pluginUrl: String) { - val url = "${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0" + val url = + "${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0" Log.d(LOGKEY, "Requesting: $url") app.get(url) } @@ -74,32 +78,46 @@ object VotingApi { // please do not cheat the votes lol return true } + private val voteLock = Mutex() suspend fun vote(pluginUrl: String, requestType: VoteType): Int { - if (!canVote(pluginUrl)) { - main { - Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT).show() + // Prevent multiple requests at the same time. + voteLock.withLock { + if (!canVote(pluginUrl)) { + main { + Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT) + .show() + } + return getVotes(pluginUrl) } - return getVotes(pluginUrl) - } - val savedType: VoteType = getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE - var newType: VoteType = requestType - var changeValue = 0 - if (requestType == savedType) { - newType = VoteType.NONE - changeValue = -requestType.value - } else if (savedType == VoteType.NONE) { - changeValue = requestType.value - } else if (savedType != requestType) { - changeValue = -savedType.value + requestType.value - } - val url = "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}" - Log.d(LOGKEY, "Requesting: $url") - val res = app.get(url).parsedSafe()?.value - if (res != null) { + + val savedType: VoteType = + getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE + + val newType = if (requestType == savedType) VoteType.NONE else requestType + val changeValue = if (requestType == savedType) { + -requestType.value + } else if (savedType == VoteType.NONE) { + requestType.value + } else if (savedType != requestType) { + -savedType.value + requestType.value + } else 0 + + // Pre-emptively set vote key setKey("cs3-votes/${transformUrl(pluginUrl)}", newType) - votesCache[pluginUrl] = res + + val url = + "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}" + Log.d(LOGKEY, "Requesting: $url") + val res = app.get(url).parsedSafe()?.value + + if (res == null) { + // "Refund" key if the response is invalid + setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType) + } else { + votesCache[pluginUrl] = res + } + return res ?: 0 } - return res ?: 0 } private data class Result( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 2bc39b54..825ff673 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -37,7 +37,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val subtitleProviders get() = listOf( openSubtitlesApi, -// indexSubtitlesApi // they got anti scraping measures in place :( + indexSubtitlesApi // they got anti scraping measures in place :( ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt index 0f882f3b..ef74edfc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.syncproviders +import androidx.fragment.app.FragmentActivity + interface OAuth2API : AuthAPI { val key: String val redirectUrl: String suspend fun handleRedirect(url: String) : Boolean - fun authenticate() + fun authenticate(activity: FragmentActivity?) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 606fee97..3140abbc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.syncproviders.providers +import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper @@ -48,9 +49,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { removeAccountKeys() } - override fun authenticate() { + override fun authenticate(activity: FragmentActivity?) { val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token" - openBrowser(request) + openBrowser(request, activity) } override suspend fun handleRedirect(url: String): Boolean { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt index f847e0b2..7ec168da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.syncproviders.providers +import androidx.fragment.app.FragmentActivity import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -15,7 +16,7 @@ class Dropbox : OAuth2API { override val icon: Int get() = TODO("Not yet implemented") - override fun authenticate() { + override fun authenticate(activity: FragmentActivity?) { TODO("Not yet implemented") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt index 5673e868..2fc97477 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt @@ -20,10 +20,9 @@ class IndexSubtitleApi : AbstractSubApi { override fun logOut() {} - private val interceptor = CloudflareKiller() companion object { - const val host = "https://indexsubtitle.com" + const val host = "https://subscene.cyou" const val TAG = "INDEXSUBS" } @@ -126,16 +125,15 @@ class IndexSubtitleApi : AbstractSubApi { epNumber = epNum, seasonNumber = seasonNum, year = yearNum, - headers = interceptor.getCookieHeaders(link).toMap() ) ) } - val document = app.get("$host/?search=$queryText", interceptor = interceptor).document + val document = app.get("$host/?search=$queryText").document document.select("div.my-3.p-3 div.media").map { block -> if (seasonNum > 0) { - val name = block.select("strong.text-primary").text().trim() + val name = block.select("strong.text-primary, strong.text-info").text().trim() val season = getOrdinal(seasonNum) if ((block.selectFirst("a")?.attr("href") ?.contains( @@ -163,7 +161,7 @@ class IndexSubtitleApi : AbstractSubApi { val urlItem = fixUrl( it.selectFirst("a")!!.attr("href") ) - val itemDoc = app.get(urlItem, interceptor = interceptor).document + val itemDoc = app.get(urlItem).document val id = imdbUrlToIdNullable( itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() ?.attr("href") @@ -202,14 +200,14 @@ class IndexSubtitleApi : AbstractSubApi { val results = mutableListOf() urlItems.forEach { url -> - val request = app.get(url, interceptor = interceptor) + val request = app.get(url) if (request.isSuccessful) { request.document.select("div.my-3.p-3 div.media").map { block -> if (block.select("span.d-block span[data-original-title=Language]").text() .trim() .contains("$queryLang") ) { - var name = block.select("strong.text-primary").text().trim() + var name = block.select("strong.text-primary, strong.text-info").text().trim() val link = fixUrl(block.selectFirst("a")!!.attr("href")) if (seasonNum > 0) { when { @@ -235,7 +233,7 @@ class IndexSubtitleApi : AbstractSubApi { val seasonNum = data.seasonNumber val epNum = data.epNumber - val req = app.get(data.data, interceptor = interceptor) + val req = app.get(data.data) if (req.isSuccessful) { val document = req.document @@ -246,7 +244,7 @@ class IndexSubtitleApi : AbstractSubApi { } else { document.select("div.my-3.p-3 div.media").mapNotNull { block -> val name = - block.selectFirst("strong.d-block.text-primary")?.text()?.trim().toString() + block.selectFirst("strong.d-block")?.text()?.trim().toString() if (seasonNum!! > 0) { if (isRightEps(name, seasonNum, epNum)) { fixUrl(block.selectFirst("a")!!.attr("href")) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index ea27720a..c08958ce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Base64 +import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser @@ -281,7 +282,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return false } - override fun authenticate() { + override fun authenticate(activity: FragmentActivity?) { // It is recommended to use a URL-safe string as code_verifier. // See section 4 of RFC 7636 for more details. @@ -294,7 +295,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val codeChallenge = codeVerifier val request = "$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId" - openBrowser(request) + openBrowser(request, activity) } private var requestId = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index cb4bbf37..19e24f74 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -11,12 +11,12 @@ import android.webkit.WebViewClient import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.network.WebViewResolver -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import kotlinx.android.synthetic.main.fragment_webview.* -import java.net.URI class WebviewFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -31,16 +31,8 @@ class WebviewFragment : Fragment() { request: WebResourceRequest? ): Boolean { val requestUrl = request?.url.toString() - val repoUrl = if (requestUrl.startsWith("https://cs.repo")) { - "https://" + requestUrl.substringAfter("?") - } else if (URI(requestUrl).scheme == appStringRepo) { - requestUrl.replaceFirst(appStringRepo, "https") - } else { - null - } - - if (repoUrl != null) { - activity?.loadRepository(repoUrl) + val performedAction = MainActivity.handleAppIntentUrl(activity, requestUrl, true) + if (performedAction) { findNavController().popBackStack() return true } @@ -48,12 +40,15 @@ class WebviewFragment : Fragment() { return super.shouldOverrideUrlLoading(view, request) } } - web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") - web_view.settings.javaScriptEnabled = true - web_view.settings.domStorageEnabled = true WebViewResolver.webViewUserAgent = web_view.settings.userAgentString -// web_view.settings.userAgentString = USER_AGENT + + web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") + web_view.settings.javaScriptEnabled = true + web_view.settings.userAgentString = USER_AGENT + web_view.settings.domStorageEnabled = true +// WebView.setWebContentsDebuggingEnabled(true) + web_view.loadUrl(url) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 0071ab49..520b6b99 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -256,13 +256,14 @@ class HomeFragment : Fragment() { nsfw: MaterialButton?, others: MaterialButton?, ): List>> { + // This list should be same order as home screen to aid navigation return listOf( - Pair(anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)), - Pair(cartoons, listOf(TvType.Cartoon)), - Pair(tvs, listOf(TvType.TvSeries)), - Pair(docs, listOf(TvType.Documentary)), Pair(movies, listOf(TvType.Movie, TvType.Torrent)), + Pair(tvs, listOf(TvType.TvSeries)), + Pair(anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)), Pair(asian, listOf(TvType.AsianDrama)), + Pair(cartoons, listOf(TvType.Cartoon)), + Pair(docs, listOf(TvType.Documentary)), Pair(livestream, listOf(TvType.Live)), Pair(nsfw, listOf(TvType.NSFW)), Pair(others, listOf(TvType.Others)), @@ -352,11 +353,25 @@ class HomeFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } + /** + * Since fire tv is fucked we need to manually define the focus layout. + * Since visible buttons are only known in runtime this is required. + **/ + var lastButton: MaterialButton? = null + for ((button, validTypes) in pairList) { val isValid = validAPIs.any { api -> validTypes.any { api.supportedTypes.contains(it) } } button?.isVisible = isValid if (isValid) { + + // Set focus navigation + button?.let { currentButton -> + lastButton?.nextFocusRightId = currentButton.id + lastButton?.id?.let { currentButton.nextFocusLeftId = it } + lastButton = currentButton + } + fun buttonContains(): Boolean { return preSelectedTypes.any { validTypes.contains(it) } } @@ -506,15 +521,12 @@ class HomeFragment : Fragment() { } } - //Disable Random button, if its toggled off on settings + //Load value for toggling Random button. Hide at startup context?.let { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) toggleRandomButton = settingsManager.getBoolean(getString(R.string.random_button_key), false) - home_random?.isVisible = toggleRandomButton - if (!toggleRandomButton) { - home_random?.visibility = View.GONE - } + home_random?.visibility = View.GONE } observe(homeViewModel.apiName) { apiName -> @@ -611,6 +623,7 @@ class HomeFragment : Fragment() { home_loading_shimmer?.stopShimmer() val d = data.value + val mutableListOfResponse = mutableListOf() listHomepageItems.clear() // println("ITEMCOUNT: ${d.values.size} ${home_master_recycler?.adapter?.itemCount}") @@ -623,6 +636,11 @@ class HomeFragment : Fragment() { home_loading_error?.isVisible = false home_loaded?.isVisible = true if (toggleRandomButton) { + //Flatten list + d.values.forEach { dlist -> + mutableListOfResponse.addAll(dlist.list.list) + } + listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url }) home_random?.isVisible = listHomepageItems.isNotEmpty() } else { home_random?.isGone = true @@ -1016,4 +1034,4 @@ class HomeFragment : Fragment() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 6254cb9b..d8497876 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -276,9 +276,6 @@ class HomeViewModel : ViewModel() { if (preferredApiName == noneApi.name) { setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) loadAndCancel(noneApi) - // If the plugin isn't loaded yet. (Does not set the key) - } else if (api == null) { - loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { val validAPIs = context?.filterProviderByPreferredMedia() if (validAPIs.isNullOrEmpty()) { @@ -289,6 +286,9 @@ class HomeViewModel : ViewModel() { loadAndCancel(apiRandom) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) } + // If the plugin isn't loaded yet. (Does not set the key) + } else if (api == null) { + loadAndCancel(noneApi) } else { setKey(USER_SELECTED_HOMEPAGE_API, api.name) loadAndCancel(api) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 19f2b25b..1ddd752f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -611,6 +611,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player_lock?.isGone = !isShowing //player_media_route_button?.isClickable = !isGone player_go_back_holder?.isGone = isGone + player_sources_btt?.isGone = isGone } private fun updateLockUI() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index c69dc476..e20a6c7b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -58,6 +58,7 @@ import kotlinx.android.synthetic.main.player_select_source_and_subs.* import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings import kotlinx.android.synthetic.main.player_select_tracks.* import kotlinx.coroutines.Job +import kotlinx.coroutines.delay class GeneratorPlayer : FullScreenPlayer() { companion object { @@ -115,10 +116,11 @@ class GeneratorPlayer : FullScreenPlayer() { override fun onTracksInfoChanged() { val tracks = player.getVideoTracks() - player_tracks_btt?.isVisible = tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1 + player_tracks_btt?.isVisible = + tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1 // Only set the preferred language if it is available. // Otherwise it may give some users audio track init failed! - if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }){ + if (tracks.allAudioTracks.any { it.language == preferredAudioTrackLanguage }) { player.setPreferredAudioTrack(preferredAudioTrackLanguage) } } @@ -602,8 +604,20 @@ class GeneratorPlayer : FullScreenPlayer() { subtitleList.setItemChecked(subtitleIndex, true) subtitleList.setOnItemClickListener { _, _, which, _ -> - subtitleIndex = which - subtitleList.setItemChecked(which, true) + if (which > currentSubtitles.size) { + // Since android TV is funky the setOnItemClickListener will be triggered + // instead of setOnClickListener when selecting. To override this we programmatically + // click the view when selecting an item outside the list. + + // Cheeky way of getting the view at that position to click it + // to avoid keeping track of the various footers. + // getChildAt() gives null :( + val child = subtitleList.adapter.getView(which, null, subtitleList) + child?.performClick() + } else { + subtitleIndex = which + subtitleList.setItemChecked(which, true) + } } sourceDialog.cancel_btt?.setOnClickListener { @@ -762,7 +776,8 @@ class GeneratorPlayer : FullScreenPlayer() { ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) // audioArrayAdapter.add(ctx.getString(R.string.no_subtitles)) audioArrayAdapter.addAll(currentAudioTracks.mapIndexed { index, format -> - format.label ?: format.language?.let { fromTwoLettersToLanguage(it) } ?: index.toString() + format.label ?: format.language?.let { fromTwoLettersToLanguage(it) } + ?: index.toString() }) audioList.adapter = audioArrayAdapter diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 3abd827e..e9fbd5f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint +import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,6 +10,7 @@ import android.widget.TextView import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton @@ -53,6 +55,10 @@ const val ACTION_SHOW_DESCRIPTION = 15 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 +const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 +const val ACTION_PLAY_EPISODE_IN_MPV = 17 + + data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -60,7 +66,26 @@ class EpisodeAdapter( private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.Adapter() { - private var cardList: MutableList = mutableListOf() + companion object { + /** + * @return ACTION_PLAY_EPISODE_IN_PLAYER, ACTION_PLAY_EPISODE_IN_BROWSER or ACTION_PLAY_EPISODE_IN_VLC_PLAYER depending on player settings. + * See array.xml/player_pref_values + **/ + fun getPlayerAction(context: Context): Int { + + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + return when (settingsManager.getInt(context.getString(R.string.player_pref_key), 1)) { + 1 -> ACTION_PLAY_EPISODE_IN_PLAYER + 2 -> ACTION_PLAY_EPISODE_IN_VLC_PLAYER + 3 -> ACTION_PLAY_EPISODE_IN_BROWSER + 4 -> ACTION_PLAY_EPISODE_IN_WEB_VIDEO + 5 -> ACTION_PLAY_EPISODE_IN_MPV + else -> ACTION_PLAY_EPISODE_IN_PLAYER + } + } + } + + var cardList: MutableList = mutableListOf() private val mBoundViewHolders: HashSet = HashSet() private fun getAllBoundViewHolders(): Set? { @@ -239,7 +264,6 @@ class EpisodeAdapter( itemView.setOnLongClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) - return@setOnLongClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 5fc61146..a173f1c1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -38,6 +38,7 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.EasyDownloadButton import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment +import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.* @@ -95,6 +96,7 @@ import kotlinx.android.synthetic.main.fragment_result.result_vpn import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_tv.* import kotlinx.android.synthetic.main.result_sync.* +import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking @@ -293,7 +295,7 @@ open class ResultFragment : ResultTrailerPlayer() { result_reload_connection_open_in_browser?.isVisible = true } 2 -> { - result_bookmark_fab?.isGone = isTvSettings() + result_bookmark_fab?.isGone = isTrueTvSettings() result_bookmark_fab?.extend() //if (result_bookmark_button?.context?.isTrueTvSettings() == true) { // when { @@ -412,7 +414,39 @@ open class ResultFragment : ResultTrailerPlayer() { is ResourceSome.Success -> { result_episodes?.isVisible = true result_episode_loading?.isVisible = false + + /* + * Okay so what is this fuckery? + * Basically Android TV will crash if you request a new focus while + * the adapter gets updated. + * + * This means that if you load thumbnails and request a next focus at the same time + * the app will crash without any way to catch it! + * + * How to bypass this? + * This code basically steals the focus for 500ms and puts it in an inescapable view + * then lets out the focus by requesting focus to result_episodes + */ + + // Do not use this.isTv, that is the player + val isTv = isTvSettings() + val hasEpisodes = + !(result_episodes?.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() + + if (isTv && hasEpisodes) { + // Make it impossible to focus anywhere else! + temporary_no_focus?.isFocusable = true + temporary_no_focus?.requestFocus() + } + (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value) + + if (isTv && hasEpisodes) main { + delay(500) + temporary_no_focus?.isFocusable = false + // This might make some people sad as it changes the focus when leaving an episode :( + result_episodes?.requestFocus() + } } } } @@ -422,7 +456,8 @@ open class ResultFragment : ResultTrailerPlayer() { val apiName: String, val showFillers: Boolean, val dubStatus: DubStatus, - val start: AutoResume? + val start: AutoResume?, + val playerAction: Int ) private fun getStoredData(context: Context): StoredData? { @@ -436,6 +471,8 @@ open class ResultFragment : ResultTrailerPlayer() { ) DubStatus.Dubbed else DubStatus.Subbed val startAction = arguments?.getInt(START_ACTION_BUNDLE) + val playerAction = getPlayerAction(context) + val start = startAction?.let { action -> val startValue = arguments?.getInt(START_VALUE_BUNDLE) val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE) @@ -450,7 +487,7 @@ open class ResultFragment : ResultTrailerPlayer() { season = resumeSeason ) } - return StoredData(url, apiName, showFillers, dubStatus, start) + return StoredData(url, apiName, showFillers, dubStatus, start, playerAction) } private fun reloadViewModel(success: Boolean = false) { @@ -458,7 +495,14 @@ open class ResultFragment : ResultTrailerPlayer() { val storedData = getStoredData(activity ?: context ?: return) ?: return //viewModel.clear() - viewModel.load(activity, storedData.url ?: return, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start) + viewModel.load( + activity, + storedData.url ?: return, + storedData.apiName, + storedData.showFillers, + storedData.dubStatus, + storedData.start + ) } } @@ -734,7 +778,8 @@ open class ResultFragment : ResultTrailerPlayer() { viewModel.handleAction( activity, EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, value.result + storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER, + value.result ) ) } @@ -916,7 +961,14 @@ open class ResultFragment : ResultTrailerPlayer() { if (storedData?.url != null) { result_reload_connectionerror.setOnClickListener { - viewModel.load(activity, storedData.url, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start) + viewModel.load( + activity, + storedData.url, + storedData.apiName, + storedData.showFillers, + storedData.dubStatus, + storedData.start + ) } result_reload_connection_open_in_browser?.setOnClickListener { @@ -952,7 +1004,14 @@ open class ResultFragment : ResultTrailerPlayer() { if (restart || !viewModel.hasLoaded()) { //viewModel.clear() - viewModel.load(activity, storedData.url, storedData.apiName, storedData.showFillers, storedData.dubStatus, storedData.start) + viewModel.load( + activity, + storedData.url, + storedData.apiName, + storedData.showFillers, + storedData.dubStatus, + storedData.start + ) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 48919308..906b652d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -1,14 +1,13 @@ package com.lagradost.cloudstream3.ui.result import android.app.Activity -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent +import android.content.* import android.net.Uri +import android.os.Bundle import android.util.Log import android.widget.Toast import androidx.core.content.FileProvider +import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -16,6 +15,7 @@ import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.IGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.getNameFull @@ -43,7 +44,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason @@ -52,9 +52,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason -import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.UIHelper.requestRW import kotlinx.coroutines.* import java.io.File import java.lang.Math.abs @@ -615,7 +613,7 @@ class ResultViewModel2 : ViewModel() { val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let // SET VISUAL KEYS - AcraApplication.setKey( + setKey( DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( @@ -629,7 +627,7 @@ class ResultViewModel2 : ViewModel() { ) ) - AcraApplication.setKey( + setKey( DataStore.getFolderName( DOWNLOAD_EPISODE_CACHE, parentId.toString() @@ -956,70 +954,155 @@ class ResultViewModel2 : ViewModel() { return LinkLoadingResult(sortUrls(links), sortSubs(subs)) } - private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe { - if (act == null) return@ioSafe - if (data.links.isEmpty()) { - showToast(act, R.string.no_links_found_toast, Toast.LENGTH_SHORT) - return@ioSafe + private fun launchActivity( + activity: Activity?, + resumeApp: ResultResume, + id: Int? = null, + work: suspend (Intent.(Activity) -> Unit) + ): Job? { + val act = activity ?: return null + return CoroutineScope(Dispatchers.IO).launch { + try { + resumeApp.launch(id) { + work(act) + } + } catch (t: Throwable) { + logError(t) + main { + if (t is ActivityNotFoundException) { + showToast(activity, txt(R.string.app_not_found_error), Toast.LENGTH_LONG) + } else { + showToast(activity, t.toString(), Toast.LENGTH_LONG) + } + } + } } - try { - if (!act.checkWrite()) { - act.requestRW() - if (act.checkWrite()) return@ioSafe - } + } - val outputDir = act.cacheDir - val outputFile = withContext(Dispatchers.IO) { - File.createTempFile("mirrorlist", ".m3u8", outputDir) + private fun playInWebVideo( + activity: Activity?, + link: ExtractorLink, + title: String?, + posterUrl: String?, + subtitles: List + ) = launchActivity(activity, WEB_VIDEO) { + setDataAndType(Uri.parse(link.url), "video/*") + + putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray()) + title?.let { putExtra("title", title) } + posterUrl?.let { putExtra("poster", posterUrl) } + val headers = Bundle().apply { + if (link.referer.isNotBlank()) + putString("Referer", link.referer) + putString("User-Agent", USER_AGENT) + for ((key, value) in link.headers) { + putString(key, value) } + } + putExtra("android.media.intent.extra.HTTP_HEADERS", headers) + putExtra("secure_uri", true) + } + + private fun playWithMpv( + activity: Activity?, + id: Int, + link: ExtractorLink, + subtitles: List, + resume: Boolean = true, + ) = launchActivity(activity, MPV, id) { + putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray()) + putExtra("subs.name", subtitles.map { it.name }.toTypedArray()) + putExtra("subs.filename", subtitles.map { it.name }.toTypedArray()) + setDataAndType(Uri.parse(link.url), "video/*") + component = MPV_COMPONENT + putExtra("secure_uri", true) + putExtra("return_result", true) + val position = getViewPos(id)?.position + if (resume && position != null) + putExtra("position", position.toInt()) + } + + // https://wiki.videolan.org/Android_Player_Intents/ + private fun playWithVlc( + activity: Activity?, + data: LinkLoadingResult, + id: Int, + resume: Boolean = true, + // if it is only a single link then resume works correctly + singleFile: Boolean? = null + ) = launchActivity(activity, VLC, id) { act -> + addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + + val outputDir = act.cacheDir + + if (singleFile ?: (data.links.size == 1)) { + setDataAndType(data.links.first().url.toUri(), "video/*") + } else { + val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir) + var text = "#EXTM3U" - for (sub in data.subs) { - text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\"" - } + + // With subtitles it doesn't work for no reason :( +// for (sub in data.subs) { +// text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\"" +// } for (link in data.links) { text += "\n#EXTINF:, ${link.name}\n${link.url}" } outputFile.writeText(text) - val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT) - - vlcIntent.setPackage(VLC_PACKAGE) - vlcIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - vlcIntent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) - vlcIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - vlcIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - - vlcIntent.setDataAndType( + setDataAndType( FileProvider.getUriForFile( act, act.applicationContext.packageName + ".provider", outputFile ), "video/*" ) - - val startId = VLC_FROM_PROGRESS - - var position = startId - if (startId == VLC_FROM_START) { - position = 1 - } else if (startId == VLC_FROM_PROGRESS) { - position = 0 - } - - vlcIntent.putExtra("position", position) - - vlcIntent.component = VLC_COMPONENT - act.setKey(VLC_LAST_ID_KEY, id) - act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE) - } catch (e: Exception) { - logError(e) - showToast(act, e.toString(), Toast.LENGTH_LONG) } + + val position = if (resume) { + getViewPos(id)?.position ?: 0L + } else { + 1L + } + + component = VLC_COMPONENT + + putExtra("from_start", !resume) + putExtra("position", position) } - fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launchSafe { - handleEpisodeClickEvent(activity, click) - } + + fun handleAction(activity: Activity?, click: EpisodeClickEvent) = + viewModelScope.launchSafe { + handleEpisodeClickEvent(activity, click) + } + + data class ExternalApp( + val packageString: String, + val name: Int, + val action: Int, + ) + + private val apps = listOf( + ExternalApp( + VLC_PACKAGE, + R.string.player_settings_play_in_vlc, + ACTION_PLAY_EPISODE_IN_VLC_PLAYER + ), ExternalApp( + WEB_VIDEO_CAST_PACKAGE, + R.string.player_settings_play_in_web, + ACTION_PLAY_EPISODE_IN_WEB_VIDEO + ), + ExternalApp( + MPV_PACKAGE, + R.string.player_settings_play_in_mpv, + ACTION_PLAY_EPISODE_IN_MPV + ) + ) private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) { when (click.action) { @@ -1035,9 +1118,17 @@ class ResultViewModel2 : ViewModel() { } options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) - if (activity?.isAppInstalled(VLC_PACKAGE) == true) { - options.add(txt(R.string.episode_action_play_in_vlc) to ACTION_PLAY_EPISODE_IN_VLC_PLAYER) + for (app in apps) { + if (activity?.isAppInstalled(app.packageString) == true) { + options.add( + txt( + R.string.episode_action_play_in_format, + txt(app.name) + ) to app.action + ) + } } + options.addAll( listOf( txt(R.string.episode_action_play_in_browser) to ACTION_PLAY_EPISODE_IN_BROWSER, @@ -1073,9 +1164,10 @@ class ResultViewModel2 : ViewModel() { click.copy(action = ACTION_CHROME_CAST_EPISODE) ) } else { + val action = getPlayerAction(ctx) handleEpisodeClickEvent( activity, - click.copy(action = ACTION_PLAY_EPISODE_IN_PLAYER) + click.copy(action = action) ) } } @@ -1212,6 +1304,11 @@ class ResultViewModel2 : ViewModel() { } ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { loadLinks(click.data, isVisible = true, isCasting = true) { links -> + if (links.links.isEmpty()) { + showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT) + return@loadLinks + } + playWithVlc( activity, links, @@ -1219,6 +1316,37 @@ class ResultViewModel2 : ViewModel() { ) } } + ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( + click.data, + isCasting = true, + txt( + R.string.episode_action_play_in_format, + txt(R.string.player_settings_play_in_web) + ) + ) { (result, index) -> + playInWebVideo( + activity, + result.links[index], + click.data.name ?: click.data.headerName, + click.data.poster, + result.subs + ) + } + ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( + click.data, + isCasting = true, + txt( + R.string.episode_action_play_in_format, + txt(R.string.player_settings_play_in_mpv) + ) + ) { (result, index) -> + playWithMpv( + activity, + click.data.id, + result.links[index], + result.subs + ) + } ACTION_PLAY_EPISODE_IN_PLAYER -> { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = @@ -1284,7 +1412,11 @@ class ResultViewModel2 : ViewModel() { }, { if (this !is AnimeLoadResponse) return@argamap val map = - Kitsu.getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false) + Kitsu.getEpisodesDetails( + getMalId(), + getAniListId(), + isResponseRequired = false + ) if (map.isNullOrEmpty()) return@argamap updateEpisodes = DubStatus.values().map { dubStatus -> val current = @@ -1304,8 +1436,10 @@ class ResultViewModel2 : ViewModel() { val currentBack = this this.description = this.description ?: node.description?.en this.name = this.name ?: node.titles?.canonical - this.episode = this.episode ?: node.num ?: episodeNumbers[index] - this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url + this.episode = + this.episode ?: node.num ?: episodeNumbers[index] + this.posterUrl = + this.posterUrl ?: node.thumbnail?.original?.url } } } @@ -1592,7 +1726,9 @@ class ResultViewModel2 : ViewModel() { val idIndex = ep.key.id for ((index, i) in ep.value.withIndex()) { val episode = i.episode ?: (index + 1) - val id = mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) ?: 0) + val id = + mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000) + ?: 0) if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) val seasonData = loadResponse.seasonNames.getSeason(i.season) @@ -1888,7 +2024,10 @@ class ResultViewModel2 : ViewModel() { if (ep.getWatchProgress() > 0.9) continue handleAction( activity, - EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, ep) + EpisodeClickEvent( + getPlayerAction(activity), + ep + ) ) break } @@ -1905,7 +2044,10 @@ class ResultViewModel2 : ViewModel() { ?: return@launchSafe handleAction( activity, - EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, episode) + EpisodeClickEvent( + getPlayerAction(activity), + episode + ) ) } } @@ -1983,7 +2125,7 @@ class ResultViewModel2 : ViewModel() { preferStartEpisode = getResultEpisode(mainId) preferStartSeason = getResultSeason(mainId) - AcraApplication.setKey( + setKey( DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 2554d6ee..f9627e46 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,8 +1,5 @@ package com.lagradost.cloudstream3.ui.settings -import android.app.Activity -import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.View import android.view.View.* @@ -12,8 +9,10 @@ import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError @@ -39,7 +38,11 @@ import kotlinx.android.synthetic.main.add_account_input.* class SettingsAccount : PreferenceFragmentCompat() { companion object { /** Used by nginx plugin too */ - fun showLoginInfo(activity: Activity?, api: AccountManager, info: AuthAPI.LoginInfo) { + fun showLoginInfo( + activity: FragmentActivity?, + api: AccountManager, + info: AuthAPI.LoginInfo + ) { val builder = AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) .setView(R.layout.account_managment) @@ -62,9 +65,13 @@ class SettingsAccount : PreferenceFragmentCompat() { dialog.dismissSafe(activity) showAccountSwitch(activity, api) } + + if (isTvSettings()) { + dialog.account_switch_account?.requestFocus() + } } - fun showAccountSwitch(activity: Activity, api: AccountManager) { + fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) { val accounts = api.getAccounts() ?: return val builder = @@ -98,11 +105,11 @@ class SettingsAccount : PreferenceFragmentCompat() { } @UiThread - fun addAccount(activity: Activity?, api: AccountManager) { + fun addAccount(activity: FragmentActivity?, api: AccountManager) { try { when (api) { is OAuth2API -> { - api.authenticate() + api.authenticate(activity) } is InAppAuthAPI -> { val builder = @@ -144,13 +151,11 @@ class SettingsAccount : PreferenceFragmentCompat() { dialog.login_username_input?.isVisible = api.requiresUsername dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() dialog.create_account?.setOnClickListener { - val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse(api.createAccountUrl) - try { - activity.startActivity(i) - } catch (e: Exception) { - logError(e) - } + openBrowser( + api.createAccountUrl ?: return@setOnClickListener, + activity + ) + dialog.dismissSafe() } dialog.text1?.text = api.name @@ -181,9 +186,10 @@ class SettingsAccount : PreferenceFragmentCompat() { try { showToast( activity, - activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( - api.name - ) + activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail) + .format( + api.name + ) ) } catch (e: Exception) { logError(e) // format might fail diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 125fadec..33d41934 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -113,6 +113,22 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.player_pref_key)?.setOnPreferenceClickListener { + val prefNames = resources.getStringArray(R.array.player_pref_names) + val prefValues = resources.getIntArray(R.array.player_pref_values) + val current = settingsManager.getInt(getString(R.string.player_pref_key), 1) + + activity?.showBottomDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.player_pref), + true, + {}) { + settingsManager.edit().putInt(getString(R.string.player_pref_key), prefValues[it]).apply() + } + return@setOnPreferenceClickListener true + } + getPref(R.string.subtitle_settings_key)?.setOnPreferenceClickListener { SubtitlesFragment.push(activity, false) return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 61a5b130..10daacda 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.settings +import android.os.Build import android.os.Bundle import android.view.View import androidx.preference.PreferenceFragmentCompat @@ -86,8 +87,20 @@ class SettingsUI : PreferenceFragmentCompat() { } getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { - val prefNames = resources.getStringArray(R.array.themes_names) - val prefValues = resources.getStringArray(R.array.themes_names_values) + val prefNames = resources.getStringArray(R.array.themes_names).toMutableList() + val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + val toRemove = prefValues + .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } + .filterNotNull() + var offset = 0 + toRemove.forEach { idx -> + prefNames.removeAt(idx - offset) + prefValues.removeAt(idx - offset) + offset += 1 + } + } val currentLayout = settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) @@ -110,8 +123,20 @@ class SettingsUI : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { - val prefNames = resources.getStringArray(R.array.themes_overlay_names) - val prefValues = resources.getStringArray(R.array.themes_overlay_names_values) + val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList() + val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + val toRemove = prefValues + .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } + .filterNotNull() + var offset = 0 + toRemove.forEach { idx -> + prefNames.removeAt(idx - offset) + prefValues.removeAt(idx - offset) + offset += 1 + } + } val currentLayout = settingsManager.getString(getString(R.string.primary_color_key), prefValues.first()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt index b5f82ae8..897e7366 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsViewModel.kt @@ -3,21 +3,19 @@ package com.lagradost.cloudstream3.ui.settings.extensions import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.debugAssert -import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.txt -import kotlinx.coroutines.launch +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe data class RepositoryData( @JsonProperty("name") val name: String, @@ -46,7 +44,8 @@ class ExtensionsViewModel : ViewModel() { val pluginStats: LiveData> = _pluginStats //TODO CACHE GET REQUESTS - fun loadStats() = viewModelScope.launchSafe { + // DO not use viewModelScope.launchSafe, it will ANR on slow internet + fun loadStats() = ioSafe { val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES @@ -61,6 +60,7 @@ class ExtensionsViewModel : ViewModel() { PluginManager.OnlinePluginData(savedData, onlineData) } }.flatten().distinctBy { it.onlineData.second.url } + val total = onlinePlugins.count() val disabled = outdatedPlugins.count { it.isDisabled } val downloadedTotal = outdatedPlugins.count() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index 7b944f62..e4435fff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -147,7 +147,7 @@ class PluginsFragment : Fragment() { pluginViewModel.updatePluginListLocal() tv_types_scroll_view?.isVisible = false } else { - pluginViewModel.updatePluginList(url) + pluginViewModel.updatePluginList(context, url) tv_types_scroll_view?.isVisible = true // 💀💀💀💀💀💀💀 Recyclerview when diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index 0a71c17a..6d94f91e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.app.Activity +import android.content.Context import android.util.Log import android.widget.Toast import androidx.lifecycle.LiveData @@ -13,6 +14,7 @@ import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.plugins.PluginData import com.lagradost.cloudstream3.plugins.PluginManager +import com.lagradost.cloudstream3.plugins.PluginManager.getPluginPath import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.SitePlugin import com.lagradost.cloudstream3.ui.result.txt @@ -45,8 +47,8 @@ class PluginsViewModel : ViewModel() { private val repositoryCache: MutableMap> = mutableMapOf() const val TAG = "PLG" - private fun isDownloaded(plugin: Plugin, data: Set? = null): Boolean { - return (data ?: getDownloads()).contains(plugin.second.internalName) + private fun isDownloaded(context: Context, pluginName: String, repositoryUrl: String): Boolean { + return getPluginPath(context, pluginName, repositoryUrl).exists() } private suspend fun getPlugins( @@ -63,24 +65,15 @@ class PluginsViewModel : ViewModel() { ?.also { repositoryCache[repositoryUrl] = it } ?: emptyList() } - private fun getStoredPlugins(): Array { - return PluginManager.getPluginsOnline() - } - - private fun getDownloads(): Set { - return getStoredPlugins().map { it.internalName }.toSet() - } - /** * @param viewModel optional, updates the plugins livedata for that viewModel if included * */ fun downloadAll(activity: Activity?, repositoryUrl: String, viewModel: PluginsViewModel?) = ioSafe { if (activity == null) return@ioSafe - val stored = getDownloads() val plugins = getPlugins(repositoryUrl) - plugins.filter { plugin -> !isDownloaded(plugin, stored) }.also { list -> + plugins.filter { plugin -> !isDownloaded(activity, plugin.second.internalName, repositoryUrl) }.also { list -> main { showToast( activity, @@ -103,7 +96,7 @@ class PluginsViewModel : ViewModel() { PluginManager.downloadAndLoadPlugin( activity, metadata.url, - metadata.name, + metadata.internalName, repo ) }.main { list -> @@ -117,7 +110,7 @@ class PluginsViewModel : ViewModel() { ), Toast.LENGTH_SHORT ) - viewModel?.updatePluginListPrivate(repositoryUrl) + viewModel?.updatePluginListPrivate(activity, repositoryUrl) } else if (list.isNotEmpty()) { showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT) } @@ -140,11 +133,10 @@ class PluginsViewModel : ViewModel() { if (activity == null) return@ioSafe val (repo, metadata) = plugin - val (success, message) = if (isDownloaded(plugin) || isLocal) { - PluginManager.deletePlugin( - metadata.url, - isLocal - ) to R.string.plugin_deleted + val file = getPluginPath(activity, plugin.second.internalName, plugin.first) + + val (success, message) = if (file.exists() || isLocal) { + PluginManager.deletePlugin(file) to R.string.plugin_deleted } else { PluginManager.downloadAndLoadPlugin( activity, @@ -165,14 +157,13 @@ class PluginsViewModel : ViewModel() { if (isLocal) updatePluginListLocal() else - updatePluginListPrivate(repositoryUrl) + updatePluginListPrivate(activity, repositoryUrl) } - private suspend fun updatePluginListPrivate(repositoryUrl: String) { - val stored = getDownloads() + private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { val plugins = getPlugins(repositoryUrl) val list = plugins.map { plugin -> - PluginViewData(plugin, isDownloaded(plugin, stored)) + PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } this.plugins = list @@ -211,9 +202,10 @@ class PluginsViewModel : ViewModel() { _filteredPlugins.postValue(false to plugins.filterTvTypes().filterLang().sortByQuery(currentQuery)) } - fun updatePluginList(repositoryUrl: String) = viewModelScope.launchSafe { + fun updatePluginList(context: Context?, repositoryUrl: String) = viewModelScope.launchSafe { + if (context == null) return@launchSafe Log.i(TAG, "updatePluginList = $repositoryUrl") - updatePluginListPrivate(repositoryUrl) + updatePluginListPrivate(context, repositoryUrl) } fun search(query: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index b1d82b76..b97468e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -135,7 +135,8 @@ class SubtitlesFragment : Fragment() { it.mkdir() } return fontDir.list()?.mapNotNull { - if (it.endsWith(".ttf")) { + // No idea which formats are supported, but these should be. + if (it.endsWith(".ttf") || it.endsWith(".otf")) { File(fontDir.absolutePath + "/" + it) } else null } ?: listOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 1c7bb214..cf3fbfde 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -31,6 +31,7 @@ import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import androidx.core.text.toSpanned import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -415,7 +416,7 @@ object AppUtils { } } - fun AppCompatActivity.loadResult( + fun FragmentActivity.loadResult( url: String, apiName: String, startAction: Int = 0, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index b5c2cd44..199f0398 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -344,6 +344,7 @@ val extractorApis: MutableList = arrayListOf( VidSrcExtractor(), VidSrcExtractor2(), PlayLtXyz(), + AStreamHub(), ) diff --git a/app/src/main/res/drawable/example_poster.jpg b/app/src/main/res/drawable/example_poster.jpg index 6d6e04ee..f5d7345b 100644 Binary files a/app/src/main/res/drawable/example_poster.jpg and b/app/src/main/res/drawable/example_poster.jpg differ diff --git a/app/src/main/res/drawable/subtitles_preview_background.jpg b/app/src/main/res/drawable/subtitles_preview_background.jpg index c7cd5f2e..c140e9b3 100644 Binary files a/app/src/main/res/drawable/subtitles_preview_background.jpg and b/app/src/main/res/drawable/subtitles_preview_background.jpg differ diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 7c5b4edd..a428b80f 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -420,14 +420,14 @@ + android:minWidth="250dp" + android:nextFocusRight="@id/result_bookmark_button"> @@ -753,6 +753,16 @@ android:orientation="horizontal" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/result_episode" /> + + diff --git a/app/src/main/res/layout/home_select_mainpage.xml b/app/src/main/res/layout/home_select_mainpage.xml index 6f86d40c..ca9fa517 100644 --- a/app/src/main/res/layout/home_select_mainpage.xml +++ b/app/src/main/res/layout/home_select_mainpage.xml @@ -52,6 +52,11 @@ android:id="@+id/home_select_none" style="@style/RoundedSelectableButtonIcon"/>--> + + حلقة كروم كاست مرآة كروم كاست تشغيل في التطبيق - VLC تشغيل في + %s تشغيل في تشغيل في الويب نسخ الرابط التحميل التلقائي diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index d4cb4caa..bffaf804 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -279,7 +279,7 @@ Episódio pelo Chromecast Alternativa pelo Chromecast Assistir no App - Assistir no VLC + Assistir no %s Assistir no navegador Copiar link Auto download diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index aa840760..9e00f17b 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -268,7 +268,7 @@ Chromecastovat epizodu Chromecast jako zrcadlo Přehrát v aplikace - Přehrát ve VLC + Přehrát ve %s Přehrát v prohlížeči Zkopírovat odkaz Automaticky stáhnout diff --git a/app/src/main/res/values-de/strings-de.xml b/app/src/main/res/values-de/strings-de.xml index 621080b5..e1d657c7 100644 --- a/app/src/main/res/values-de/strings-de.xml +++ b/app/src/main/res/values-de/strings-de.xml @@ -281,7 +281,7 @@ Chromecast-Episode Chromecastmirror In App wiedergeben - In VLC wiedergeben + In %s wiedergeben In Browser wiedergeben Link kopieren Auto Download diff --git a/app/src/main/res/values-es/array.xml b/app/src/main/res/values-es/array.xml index 658ba7ae..d1e5be2f 100644 --- a/app/src/main/res/values-es/array.xml +++ b/app/src/main/res/values-es/array.xml @@ -14,19 +14,6 @@ @id/cast_button_type_forward_30_seconds - - Todos - Películas y TV - Anime - Documental - - - 0 - 1 - 2 - 3 - - @string/resolution_and_title @string/title @@ -169,7 +156,7 @@ @string/episode_action_chromecast_episode @string/episode_action_chromecast_mirror @string/episode_action_play_in_app - @string/episode_action_play_in_vlc + @string/episode_action_play_in_format @string/episode_action_play_in_browser @string/episode_action_copy_link @string/episode_action_auto_download @@ -222,6 +209,8 @@ Banana Fiesta Dolor rosa + Material You + Material You (Secondary) Normal @@ -240,19 +229,24 @@ Banana Party Pink + Monet + Monet2 + Oscuro Gris Amoled Destello + Material You AmoledLight Black Amoled Light + Monet diff --git a/app/src/main/res/values-es/strings-es.xml b/app/src/main/res/values-es/strings-es.xml index 9b739b39..172d079c 100644 --- a/app/src/main/res/values-es/strings-es.xml +++ b/app/src/main/res/values-es/strings-es.xml @@ -269,7 +269,7 @@ Episodio Chromecast Espejo Chromecast Reproducir en la app - Reproducir en VLC + Reproducir en %s Reproducir en el navegador Copiar enlace Descarga automática diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7d259b31..c98173ce 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -164,7 +164,7 @@ Episode Chromecast Miroir Chromecast Lecture dans l\'application - Lecture dans VLC + Lecture dans %s Lecture dans le navigateur Copier le lien Téléchargement Automatique diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e3f0a233..a6ad0af7 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -136,7 +136,7 @@ क्रोमकास्ट एपिसोड कक्रोमकास्ट मिरर एप्प मैं चलाये - VLC में चलाए + %s में चलाए Browser में चलाए लिंक कॉपी करें डाउनलोड करे diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index bc2a7098..2a7bff1c 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -299,7 +299,7 @@ Chromecast epizoda Chromecast mirror Pokreni u aplikaciji - Pokreni u VLC-u + Pokreni u %s Pokreni u pregledniku Kopiraj poveznicu Automatsko preuzimanje diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e8f7e9e9..31b84bfd 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -264,7 +264,7 @@ Episode Chromecast Mirror Chromecast Putar di aplikasi - Putar di VLC + Putar di %s Putar di browser Salin tautan Download otomatis diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index db72b31d..86206213 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -271,7 +271,7 @@ Chromecast Chromecast mirror Riproduci in app - Riproduci in VLC + Riproduci in %s Riproduci nel browser Copia link Download diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 96044dc7..85aee997 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -190,7 +190,7 @@ Епизода на Chromecast Огледало на Chromecastr Пушти во апликацијата - Пушти на VLC + Пушти на %s Пушти на прелистувач Копирај линк Авто превземање diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d8fab674..78f45e26 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -175,7 +175,7 @@ ആപ്പിൽ പ്ലേയ് ചെയ്യുക - VLCയിൽ പ്ലേയ് ചെയ്യുക + %sയിൽ പ്ലേയ് ചെയ്യുക ബ്രൗസറിൽ പ്ലേയ് ചെയ്യുക ലിങ്ക് പകർത്തുക ഡൌൺലോഡ് ചെയ്യൂ diff --git a/app/src/main/res/values-mo/string.xml b/app/src/main/res/values-mo/string.xml index 340428b9..361aaf56 100644 --- a/app/src/main/res/values-mo/string.xml +++ b/app/src/main/res/values-mo/string.xml @@ -145,7 +145,7 @@ aauugghhooo-ahah ohaaauugghh aoohaaahhu ahouuhhh ooo-ahahaauuh aaahhu - ooo-ahah ohaauuh + ooo-ahah ohaauuh ahoha ooo-ahahohoohah oooohh aauugghhahhaauugghh aaaghhoooohh aaahhu ahooo diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ae774f03..7daca143 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -20,7 +20,7 @@ Snelheid (%.2fx) - Beoordeeld: %.Als + Beoordeeld: %.1fAls Nieuwe update gevonden!\n%s -> %s Filler %d min @@ -274,7 +274,7 @@ Chromecast aflevering Chromecast mirror Speel in app - Speel in VLC + Speel in %s Speel in browser Kopieer link Automatisch downloaden diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index fc7cbbe2..ebd6ee49 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -196,7 +196,7 @@ Støpt Episode Støpt Speil Spill i appen - Spill i VLC + Spill i %s Spill i nettleseren Kopier link Automatisk nedlasting diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index c40b7f87..30b6f4a1 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -165,7 +165,7 @@ @string/episode_action_chromecast_episode @string/episode_action_chromecast_mirror @string/episode_action_play_in_app - @string/episode_action_play_in_vlc + @string/episode_action_play_in_format @string/episode_action_play_in_browser @string/episode_action_copy_link @string/episode_action_auto_download @@ -218,6 +218,8 @@ Bananowy Łososiowy Świnko peppowy + Material You + Material You (drugorzędny) Normal @@ -236,19 +238,24 @@ Banana Party Pink + Monet + Monet2 + Ciemny Szary Amoled Flashbang + Material You AmoledLight Black Amoled Light + Monet diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ea6eb140..6da1cc8f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -252,7 +252,7 @@ Chromecast odcinka Chromecast mirroru Odtwórz w aplikacji - Odtwórz w VLC + Odtwórz w %s Odtwórz w przeglądarce Kopiuj link Automatyczne pobieranie @@ -455,4 +455,5 @@ Wspierane Język Najpierw zainstaluj rozszerzenie + Zaaktualizowano %d rozszerzeń diff --git a/app/src/main/res/values-pt/strings-pt.xml b/app/src/main/res/values-pt/strings-pt.xml index de70f746..375c3193 100644 --- a/app/src/main/res/values-pt/strings-pt.xml +++ b/app/src/main/res/values-pt/strings-pt.xml @@ -268,7 +268,7 @@ Episódio pelo Chromecast Alternativa pelo Chromecast Reproduzir na app - Reproduzir no VLC + Reproduzir no %s Reproduzir no navegador Copiar link Transferência Automática diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 7d1f3458..ce8f328c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -267,7 +267,7 @@ Chromecast Chromecast alternativ Redă în Aplicație - Redă în VLC + Redă în %s Redă în Browser Copiază link-ul Auto-descărcare diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 3f3cead1..58ff060e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -167,7 +167,7 @@ Chromecasta ett Avsnitt Chromecasta en Länk Spela upp i appen - Spela upp i VLC + Spela upp i %s Spela upp i webbläsaren Kopiera länk Automatisk nerladdning diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 9d0e0e02..dfe922ee 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -204,7 +204,7 @@ Chromecast Episode Chromecast Mirror I-play sa App - I-play sa VLC + I-play sa %s I-play sa browser Kopyahin ang Link Awtomatiking i-download diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index ec9acd0e..177be03b 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -14,19 +14,6 @@ @id/cast_button_type_forward_30_seconds - - Hepsi - Film ve Dizi - Anime - Belgesel - - - 0 - 1 - 2 - 3 - - @string/resolution_and_title @string/title @@ -169,7 +156,7 @@ @string/episode_action_chromecast_episode @string/episode_action_chromecast_mirror @string/episode_action_play_in_app - @string/episode_action_play_in_vlc + @string/episode_action_play_in_format @string/episode_action_play_in_browser @string/episode_action_copy_link @string/episode_action_auto_download @@ -222,6 +209,8 @@ Muz Parti Pembe + Material You + Material You (Secondary) Normal @@ -240,19 +229,24 @@ Banana Party Pink + Monet + Monet2 + Koyu Gri Amoled Flaş Bombası + Material You AmoledLight Black Amoled Light + Monet diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 14db2f6b..48e36013 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -272,7 +272,7 @@ Bölümü Chromecast ile yayınla Bağlantıyı Chromecast ile yayınla Uygulamada oynat - VLC\'de oynat + %s\'de oynat Tarayıcıda oynat Linki kopyala Otomatik indir diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index 5e70223b..a5145c9e 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -14,18 +14,6 @@ @id/cast_button_type_forward_30_seconds - - Tất cả - Phim lẻ và Phim bộ - Anime - Phim tài liệu - - - 0 - 1 - 2 - 3 - @string/resolution_and_title @@ -169,7 +157,7 @@ @string/episode_action_chromecast_episode @string/episode_action_chromecast_mirror @string/episode_action_play_in_app - @string/episode_action_play_in_vlc + @string/episode_action_play_in_format @string/episode_action_play_in_browser @string/episode_action_copy_link @string/episode_action_auto_download @@ -222,6 +210,8 @@ Vàng Hồng Hồng đậm + Material You + Material You (Secondary) Normal @@ -240,19 +230,24 @@ Banana Party Pink + Monet + Monet2 + Tối Xám Amoled Sáng + Material You AmoledLight Black Amoled Light + Monet diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 302c13b5..ce8358cf 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -292,7 +292,7 @@ Tập Chromecast Chiếu Chromecast Xem với trình phát mặc định - Xem với trình phát VLC + Xem với trình phát %s Xem tại trình duyệt Sao chép liên kết Tự động tải xuống diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d3ff6e1e..a6557990 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -303,7 +303,7 @@ 投屏剧集 投屏镜像 在应用中播放 - 在 VLC 中播放 + 在 %s 中播放 在浏览器中播放 复制链接 自动下载 diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 13d4f2dc..3554beb2 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -33,6 +33,22 @@ 6 + + @string/player_settings_play_in_app + @string/player_settings_play_in_vlc + @string/player_settings_play_in_mpv + @string/player_settings_play_in_web + @string/player_settings_play_in_browser + + + + 1 + 2 + 5 + 4 + 3 + + @string/resolution_and_title @string/title @@ -175,7 +191,7 @@ @string/episode_action_chromecast_episode @string/episode_action_chromecast_mirror @string/episode_action_play_in_app - @string/episode_action_play_in_vlc + @string/episode_action_play_in_format @string/episode_action_play_in_browser @string/episode_action_copy_link @string/episode_action_auto_download @@ -228,6 +244,8 @@ Banana Party Pink Pain + Material You + Material You (Secondary) Normal @@ -246,6 +264,8 @@ Banana Party Pink + Monet + Monet2 @@ -253,12 +273,14 @@ Gray Amoled Flashbang + Material You AmoledLight Black Amoled Light + Monet diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0008cd1..fdb4b943 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ subtitle_settings_key subtitle_settings_chromecast_key quality_pref_key + player_pref_key prefer_limit_title_key prefer_limit_title_rez_key video_buffer_size_key @@ -256,7 +257,7 @@ Search Accounts Updates and backup - + Info Advanced Search Gives you the search results separated by provider @@ -364,7 +365,7 @@ Chromecast episode Chromecast mirror Play in app - Play in VLC + Play in %s Play in browser Copy link Auto download @@ -602,6 +603,7 @@ Downloaded: %d Disabled: %d Not downloaded: %d + Updated %d plugins Add a repository to install site extensions View community repositories Public list @@ -627,6 +629,14 @@ Supported Language Install the extension first - + HLS Playlist + + Preferred video player + Internal player + VLC + MPV + Web Video Cast + Browser + App not found diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 2fb9b5b4..9840cb80 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -95,6 +95,16 @@ #000 + + + + + +