diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index 992298cc1..4518be726 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -20,6 +20,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + when (state.peekContent()) { // go to login fragment @@ -71,8 +89,113 @@ class MainActivity : AppCompatActivity() { } } }) + + // check if the app has been opened by clicking on torrents/magnet on sharing links + getIntentData() + + // observe for torrents downloaded + registerReceiver(getDownloadCompleteReceiver(), IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + } + + private fun getDownloadCompleteReceiver(): BroadcastReceiver { + return object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + intent?.let { + viewModel.checkDownload(it.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)) + } + } + } } + private fun getIntentData() { + + when (intent?.action) { + Intent.ACTION_SEND -> { + if (intent.type == "text/plain") + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { text -> + when { + text.isMagnet() -> { + // check auth state before loading it + viewModel.authenticationState.observeOnce(this, { auth -> + when (auth.peekContent()) { + AuthenticationState.AUTHENTICATED -> processLinkIntent(text) + AuthenticationState.AUTHENTICATED_NO_PREMIUM -> baseContext.showToast( + R.string.premium_needed_torrent + ) + else -> showToast(R.string.please_login) + } + }) + } + text.isTorrent() -> { + viewModel.authenticationState.observeOnce(this, { auth -> + when (auth.peekContent()) { + AuthenticationState.AUTHENTICATED -> processLinkIntent(text) + AuthenticationState.AUTHENTICATED_NO_PREMIUM -> baseContext.showToast( + R.string.premium_needed_torrent + ) + else -> showToast(R.string.please_login) + } + }) + } + else -> { + // we do not have other cases + } + } + } + + } + Intent.ACTION_VIEW -> { + /* Implicit intent with path to torrent file or magnet link */ + + val data = intent.data + // check uri content + if (data != null) { + + when (data.scheme) { + //clicked on a torrent file or a magnet link + SCHEME_MAGNET, SCHEME_CONTENT, SCHEME_FILE -> { + // check auth state before loading it + viewModel.authenticationState.observeOnce(this, { auth -> + when (auth.peekContent()) { + AuthenticationState.AUTHENTICATED -> processLinkIntent(data) + AuthenticationState.AUTHENTICATED_NO_PREMIUM -> baseContext.showToast( + R.string.premium_needed_torrent + ) + else -> showToast(R.string.please_login) + } + }) + } + SCHEME_HTTP, SCHEME_HTTPS -> { + showToast("You activated the http/s scheme somehow") + } + } + } + } + null -> { // app opened directly by the user. Do nothing. + } + else -> { + + } + } + } + + override fun onDestroy() { + super.onDestroy() + //todo: test if this works (probably not) + unregisterReceiver(getDownloadCompleteReceiver()) + } + + private fun processLinkIntent(uri: Uri) { + // simulate click on new download tab + val bottomNav = findViewById(R.id.bottom_nav_view) + if (bottomNav.selectedItemId != R.id.navigation_download) { + bottomNav.selectedItemId = R.id.navigation_download + } + viewModel.addLink(uri) + } + + private fun processLinkIntent(text: String) = processLinkIntent(Uri.parse(text)) + private fun openSettings() { val intent = Intent(this, SettingsActivity::class.java) startActivity(intent) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt index 447afb115..df0af65a1 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/view/DownloadDetailsFragment.kt @@ -50,7 +50,7 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener { override fun onCopyClick(text: String) { copyToClipboard("Real-Debrid Download Link", text) - showToast(R.string.link_copied) + context?.showToast(R.string.link_copied) } override fun onOpenClick(url: String) { @@ -62,7 +62,7 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener { if (activityViewModel.isTokenPrivate()) { viewModel.fetchStreamingInfo(id) } else - showToast(R.string.api_needs_private_token) + context?.showToast(R.string.api_needs_private_token) } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt index cfc5543a0..6eb746f96 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/view/ListsTabFragment.kt @@ -167,7 +167,7 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL val action = ListsTabFragmentDirections.actionListsTabToDownloadDetails(item) findNavController().navigate(action) } else - showToast(R.string.premium_needed) + context?.showToast(R.string.premium_needed) } override fun onClick(item: TorrentItem) { @@ -176,13 +176,13 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL if (item.status == "downloaded") { // if the item has many links to download, show a toast if (item.links.size>2) - showToast(R.string.downloading_torrent) + context?.showToast(R.string.downloading_torrent) viewModel.downloadTorrent(item) } else - showToast(R.string.torrent_not_downloaded) + context?.showToast(R.string.torrent_not_downloaded) } else - showToast(R.string.premium_needed) + context?.showToast(R.string.premium_needed) } companion object { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt index aab719b0a..8f591c02e 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/view/NewDownloadFragment.kt @@ -1,8 +1,13 @@ package com.github.livingwithhippos.unchained.newdownload.view +import android.app.DownloadManager import android.content.ContentResolver +import android.content.ContentResolver.SCHEME_CONTENT +import android.content.ContentResolver.SCHEME_FILE +import android.content.Context import android.net.Uri import android.os.Bundle +import android.os.Environment import android.os.ParcelFileDescriptor import android.util.Log import android.view.LayoutInflater @@ -11,6 +16,7 @@ import android.view.ViewGroup import android.widget.TextView import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toUri import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController @@ -21,16 +27,22 @@ import com.github.livingwithhippos.unchained.base.UnchainedFragment import com.github.livingwithhippos.unchained.data.model.AuthenticationState import com.github.livingwithhippos.unchained.databinding.NewDownloadFragmentBinding import com.github.livingwithhippos.unchained.newdownload.viewmodel.NewDownloadViewModel -import com.github.livingwithhippos.unchained.start.viewmodel.MainActivityViewModel import com.github.livingwithhippos.unchained.utilities.REMOTE_TRAFFIC_ON +import com.github.livingwithhippos.unchained.utilities.SCHEME_HTTP +import com.github.livingwithhippos.unchained.utilities.SCHEME_HTTPS +import com.github.livingwithhippos.unchained.utilities.SCHEME_MAGNET import com.github.livingwithhippos.unchained.utilities.extension.getClipboardText import com.github.livingwithhippos.unchained.utilities.extension.isMagnet import com.github.livingwithhippos.unchained.utilities.extension.isWebUrl import com.github.livingwithhippos.unchained.utilities.extension.runRippleAnimation import com.github.livingwithhippos.unchained.utilities.extension.showToast import dagger.hilt.android.AndroidEntryPoint +import java.io.File import java.io.FileDescriptor import java.io.FileInputStream +import java.nio.file.Path +import java.util.regex.Matcher +import java.util.regex.Pattern /** * A simple [UnchainedFragment] subclass. @@ -73,53 +85,54 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { viewModel.apiErrorLiveData.observe(viewLifecycleOwner, Observer { it.getContentIfNotHandled()?.let { error -> + //todo: move this code outside onCreateView when (error.errorCode) { - -1 -> showToast(R.string.internal_error) - 1 -> showToast(R.string.missing_parameter) - 2 -> showToast(R.string.bad_parameter_value) - 3 -> showToast(R.string.unknown_method) - 4 -> showToast(R.string.method_not_allowed) + -1 -> context?.showToast(R.string.internal_error) + 1 -> context?.showToast(R.string.missing_parameter) + 2 -> context?.showToast(R.string.bad_parameter_value) + 3 -> context?.showToast(R.string.unknown_method) + 4 -> context?.showToast(R.string.method_not_allowed) // what is this error for? - 5 -> showToast(R.string.slow_down) - 6 -> showToast(R.string.resource_unreachable) - 7 -> showToast(R.string.resource_not_found) + 5 -> context?.showToast(R.string.slow_down) + 6 -> context?.showToast(R.string.resource_unreachable) + 7 -> context?.showToast(R.string.resource_not_found) //todo: check these 8 -> { - showToast(R.string.bad_token) + context?.showToast(R.string.bad_token) activityViewModel.setUnauthenticated() } - 9 -> showToast(R.string.permission_denied) - 10 -> showToast(R.string.tfa_needed) - 11 -> showToast(R.string.tfa_pending) + 9 -> context?.showToast(R.string.permission_denied) + 10 -> context?.showToast(R.string.tfa_needed) + 11 -> context?.showToast(R.string.tfa_pending) 12 -> { - showToast(R.string.invalid_login) + context?.showToast(R.string.invalid_login) activityViewModel.setUnauthenticated() } - 13 -> showToast(R.string.invalid_password) + 13 -> context?.showToast(R.string.invalid_password) 14 -> { - showToast(R.string.account_locked) + context?.showToast(R.string.account_locked) activityViewModel.setUnauthenticated() } - 15 -> showToast(R.string.account_not_activated) - 16 -> showToast(R.string.unsupported_hoster) - 17 -> showToast(R.string.hoster_in_maintenance) - 18 -> showToast(R.string.hoster_limit_reached) - 19 -> showToast(R.string.hoster_temporarily_unavailable) - 20 -> showToast(R.string.hoster_not_available_for_free_users) - 21 -> showToast(R.string.too_many_active_downloads) - 22 -> showToast(R.string.ip_Address_not_allowed) - 23 -> showToast(R.string.traffic_exhausted) - 24 -> showToast(R.string.file_unavailable) - 25 -> showToast(R.string.service_unavailable) - 26 -> showToast(R.string.upload_too_big) - 27 -> showToast(R.string.upload_error) - 28 -> showToast(R.string.file_not_allowed) - 29 -> showToast(R.string.torrent_too_big) - 30 -> showToast(R.string.torrent_file_invalid) - 31 -> showToast(R.string.action_already_done) - 32 -> showToast(R.string.image_resolution_error) - 33 -> showToast(R.string.torrent_already_active) - else -> showToast(R.string.error_unrestricting_download) + 15 -> context?.showToast(R.string.account_not_activated) + 16 -> context?.showToast(R.string.unsupported_hoster) + 17 -> context?.showToast(R.string.hoster_in_maintenance) + 18 -> context?.showToast(R.string.hoster_limit_reached) + 19 -> context?.showToast(R.string.hoster_temporarily_unavailable) + 20 -> context?.showToast(R.string.hoster_not_available_for_free_users) + 21 -> context?.showToast(R.string.too_many_active_downloads) + 22 -> context?.showToast(R.string.ip_Address_not_allowed) + 23 -> context?.showToast(R.string.traffic_exhausted) + 24 -> context?.showToast(R.string.file_unavailable) + 25 -> context?.showToast(R.string.service_unavailable) + 26 -> context?.showToast(R.string.upload_too_big) + 27 -> context?.showToast(R.string.upload_error) + 28 -> context?.showToast(R.string.file_not_allowed) + 29 -> context?.showToast(R.string.torrent_too_big) + 30 -> context?.showToast(R.string.torrent_file_invalid) + 31 -> context?.showToast(R.string.action_already_done) + 32 -> context?.showToast(R.string.image_resolution_error) + 33 -> context?.showToast(R.string.torrent_already_active) + else -> context?.showToast(R.string.error_unrestricting_download) } // re enable buttons to let the user take other actions //todo: this needs to be done also for other errors. maybe throw another error from the ViewModel @@ -159,16 +172,16 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { viewModel.fetchAddedMagnet(link) } link.isBlank()-> { - showToast(R.string.please_insert_url) + context?.showToast(R.string.please_insert_url) } else -> { - showToast(R.string.invalid_url) + context?.showToast(R.string.invalid_url) } } } else - showToast(R.string.premium_needed) + context?.showToast(R.string.premium_needed) } downloadBinding.bPasteLink.setOnClickListener { @@ -177,7 +190,7 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { if (pasteText.isWebUrl() || pasteText.isMagnet()) downloadBinding.tiLink.setText(pasteText, TextView.BufferType.EDITABLE) else - showToast(R.string.invalid_url) + context?.showToast(R.string.invalid_url) } downloadBinding.bPastePassword.setOnClickListener { @@ -192,6 +205,38 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { downloadBinding.bUnrestrict.runRippleAnimation() } + activityViewModel.externalLinkLiveData.observe(viewLifecycleOwner, { + it.getContentIfNotHandled()?.let { link -> + when (link.scheme) { + SCHEME_MAGNET -> { + context?.showToast(R.string.loading_magnet_link) + // set as text input text + downloadBinding.tiLink.setText( + link.toString(), + TextView.BufferType.EDITABLE + ) + // simulate button click + downloadBinding.bUnrestrict.performClick() + } + SCHEME_CONTENT, SCHEME_FILE -> { + context?.showToast(R.string.loading_torrent_file) + loadTorrent(requireContext().contentResolver, link) + } + SCHEME_HTTP, SCHEME_HTTPS -> { + context?.showToast(R.string.loading_torrent_file) + downloadTorrent(link) + } + } + } + }) + + activityViewModel.downloadedTorrentLiveData.observe(viewLifecycleOwner, { + it.getContentIfNotHandled()?.let { fileName -> + val torrentFile = File(requireContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),fileName) + loadTorrent(requireContext().contentResolver, torrentFile.toUri()) + } + }) + return downloadBinding.root } @@ -200,7 +245,7 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { if (uri != null) { loadTorrent(requireContext().contentResolver, uri) } else { - showToast(R.string.error_loading_torrent) + context?.showToast(R.string.error_loading_torrent) } } @@ -209,7 +254,7 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { if (authState == AuthenticationState.AUTHENTICATED) getTorrent.launch("application/x-bittorrent") else - showToast(R.string.premium_needed) + context?.showToast(R.string.premium_needed) } private fun loadTorrent(contentResolver: ContentResolver, uri: Uri) { @@ -231,6 +276,22 @@ class NewDownloadFragment : UnchainedFragment(), NewDownloadListener { } } + + private fun downloadTorrent(uri: Uri) { + val nameRegex= "/([^/]+.torrent)\$" + val m: Matcher = Pattern.compile(nameRegex).matcher(uri.toString()) + val torrentName = if(m.find())m.group(1) else null + if (!torrentName.isNullOrBlank() && context!=null) { + val manager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request: DownloadManager.Request = DownloadManager.Request(uri) + .setTitle(getString(R.string.unchained_torrent_download)) + .setDescription(getString(R.string.temporary_torrent_download)) + .setDestinationInExternalFilesDir(requireContext(), Environment.DIRECTORY_DOWNLOADS,torrentName) + val downloadID = manager.enqueue(request) + activityViewModel.setDownload(downloadID, torrentName) + } + + } } interface NewDownloadListener { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt index 17e48ec2f..83fc845ec 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/start/viewmodel/MainActivityViewModel.kt @@ -1,8 +1,11 @@ package com.github.livingwithhippos.unchained.start.viewmodel import android.annotation.SuppressLint +import android.net.Uri +import androidx.hilt.Assisted import androidx.hilt.lifecycle.ViewModelInject import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.livingwithhippos.unchained.data.model.AuthenticationState @@ -20,6 +23,7 @@ import kotlinx.coroutines.launch * Shared between the fragments to observe the authentication status and update it. */ class MainActivityViewModel @ViewModelInject constructor( + @Assisted private val savedStateHandle: SavedStateHandle, private val authRepository: AuthenticationRepository, private val credentialRepository: CredentialsRepository, private val userRepository: UserRepository @@ -29,6 +33,10 @@ class MainActivityViewModel @ViewModelInject constructor( val userLiveData = MutableLiveData() + val externalLinkLiveData = MutableLiveData>() + + val downloadedTorrentLiveData = MutableLiveData>() + // fixme: this is here because userLiveData.postValue(user) is throwing an unsafe error // but auto-correcting it changes the value of val authenticationState = MutableLiveData>() to a nullable one @SuppressLint("NullSafeMutableLiveData") @@ -129,4 +137,27 @@ class MainActivityViewModel @ViewModelInject constructor( } } + fun addLink(uri: Uri) { + externalLinkLiveData.postValue(Event(uri)) + } + + fun setDownload(downloadID: Long, filePath: String) { + savedStateHandle.set(KEY_TORRENT_DOWNLOAD_ID, downloadID) + savedStateHandle.set(KEY_TORRENT_PATH, filePath) + } + + fun checkDownload(downloadID: Long) { + val id = savedStateHandle.get(KEY_TORRENT_DOWNLOAD_ID) + if (id == downloadID) { + val fileName = savedStateHandle.get(KEY_TORRENT_PATH) + if (fileName != null) + downloadedTorrentLiveData.postValue(Event(fileName)) + } + } + + companion object { + const val KEY_TORRENT_DOWNLOAD_ID = "torrent_download_id_key" + const val KEY_TORRENT_PATH = "torrent_path_key" + } + } \ No newline at end of file diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/Constants.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/Constants.kt index 3fefad431..792e05b1a 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/Constants.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/Constants.kt @@ -16,10 +16,15 @@ const val REMOTE_TRAFFIC_OFF: Int = 0 const val REMOTE_TRAFFIC_ON: Int = 1 const val MAGNET_PATTERN: String = "magnet:\\?xt=urn:btih:[a-zA-Z0-9]{32}" +const val TORRENT_PATTERN: String = "https?://[^\\s]{7,}.torrent" const val FEEDBACK_URL="https://github.com/LivingWithHippos/unchained-android" const val GPLV3_URL="https://www.gnu.org/licenses/gpl-3.0.en.html" +const val SCHEME_MAGNET="magnet" +const val SCHEME_HTTP="http" +const val SCHEME_HTTPS="https" + val errorMap = mapOf( -1 to "Internal error", 1 to "Missing parameter", diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt index 19c56506d..d7217218c 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/Extension.kt @@ -11,9 +11,13 @@ import android.net.Uri import android.util.Log import android.widget.Toast import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.Observer import com.github.livingwithhippos.unchained.BuildConfig import com.github.livingwithhippos.unchained.R -import java.util.* +import java.util.Locale /** * Show a toast message @@ -21,17 +25,15 @@ import java.util.* * @param length How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} Defaults to short */ -fun Fragment.showToast(stringResource: Int, length: Int = Toast.LENGTH_SHORT) { - Toast.makeText(requireContext(), getString(stringResource), length).show() -} +fun Context.showToast(stringResource: Int, length: Int = Toast.LENGTH_SHORT) = this.showToast(getString(stringResource, length)) /** * Show a toast message * @param message: the message and shown * @param length: the duration of the toast. Defaults to short */ -fun Fragment.showToast(message: String, length: Int = Toast.LENGTH_SHORT) { - Toast.makeText(requireContext(), message, length).show() +fun Context.showToast(message: String, length: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, message, length).show() } /** @@ -83,7 +85,8 @@ fun Fragment.openExternalWebPage(url: String, showErrorToast: Boolean = true): B return true } else if (showErrorToast) - showToast(R.string.invalid_url) + context?.showToast(R.string.invalid_url) + return false } @@ -107,4 +110,50 @@ fun Activity.getUpdatedLocaleContext(context: Context, language: String): Contex Locale.setDefault(locale) configuration.setLocale(locale) return context.createConfigurationContext(configuration) +} + +fun LiveData.combineWith( + liveData: LiveData, + block: (T?, K?) -> R +): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + result.value = block(this.value, liveData.value) + } + result.addSource(liveData) { + result.value = block(this.value, liveData.value) + } + return result +} + +fun zipLiveData(t: LiveData, k: LiveData): LiveData> { + return MediatorLiveData>().apply { + var lastT: T? = null + var lastK: K? = null + + fun update() { + val localLastT = lastT + val localLastK = lastK + if (localLastT != null && localLastK != null) + this.value = Pair(localLastT, localLastK) + } + + addSource(t) { + lastT = it + update() + } + addSource(k) { + lastK = it + update() + } + } +} + +fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { + observe(lifecycleOwner, object : Observer { + override fun onChanged(t: T?) { + observer.onChanged(t) + removeObserver(this) + } + }) } \ No newline at end of file diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/StringExtension.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/StringExtension.kt index d357ba3c2..a55611ce1 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/StringExtension.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/utilities/extension/StringExtension.kt @@ -5,6 +5,7 @@ import android.icu.text.DateFormat import android.icu.text.SimpleDateFormat import android.util.Patterns import com.github.livingwithhippos.unchained.utilities.MAGNET_PATTERN +import com.github.livingwithhippos.unchained.utilities.TORRENT_PATTERN import java.util.* import java.util.regex.Matcher import java.util.regex.Pattern @@ -18,11 +19,23 @@ fun String.isWebUrl(): Boolean = /** * check if a String is a magnet link */ -fun String.isMagnet(): Boolean { +fun String?.isMagnet(): Boolean { + if (this == null) + return false val m: Matcher = Pattern.compile(MAGNET_PATTERN).matcher(this) return m.lookingAt() } +/** + * check if a String is a torrent link + */ +fun String?.isTorrent(): Boolean { + if (this == null) + return false + val m: Matcher = Pattern.compile(TORRENT_PATTERN).matcher(this) + return m.matches() +} + /** * parse a string from a custom date pattern to the current Locale pattern * @return the parsed String diff --git a/app/app/src/main/res/layout/fragment_torrent_details.xml b/app/app/src/main/res/layout/fragment_torrent_details.xml index d5cd5fc58..3c214eb93 100644 --- a/app/app/src/main/res/layout/fragment_torrent_details.xml +++ b/app/app/src/main/res/layout/fragment_torrent_details.xml @@ -53,7 +53,8 @@