diff --git a/app/app/build.gradle b/app/app/build.gradle index f413907d0..5f6c30eb0 100644 --- a/app/app/build.gradle +++ b/app/app/build.gradle @@ -41,8 +41,8 @@ android { applicationId "com.github.livingwithhippos.unchained" minSdk 22 targetSdk 32 - versionCode 31 - versionName "4.35.0-beta" + versionCode 32 + versionName "4.40.2-beta" // limit resources for a list of locales // resConfigs "en", "it" @@ -191,6 +191,7 @@ dependencies { implementation deps.preference_ktx implementation deps.recyclerview.recyclerview implementation deps.recyclerview.selection + implementation deps.viewpager2 // datastore // implementation deps.datastore.preferences diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index a5581120c..cc271e0a2 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -237,6 +237,406 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + // we probably stopped and restored the app, do the same actions + // in the viewModel.fsmAuthenticationState.observe for these states + + // unlock the bottom menu + enableAllBottomNavItems() + } + else -> { + // todo: decide if we need to check other possible values or reset the fsm to checkCredentials in these states and call startAuthenticationMachine + // start the authentication state machine, the first time it's going to be null + viewModel.startAuthenticationMachine() + } + } // check if the app has been opened by clicking on torrents/magnet on sharing links getIntentData() @@ -301,11 +316,20 @@ class MainActivity : AppCompatActivity() { } } } - viewModel.setupConnectivityCheck(applicationContext) viewModel.clearCache(applicationContext.cacheDir) } + override fun onResume() { + super.onResume() + viewModel.addConnectivityCheck(applicationContext) + } + + override fun onPause() { + super.onPause() + viewModel.removeConnectivityCheck(applicationContext) + } + private fun downloadPlugin(link: String) { val pluginName = link.replace("%2F", "/").split("/").last() val manager = @@ -385,7 +409,7 @@ class MainActivity : AppCompatActivity() { } } SCHEME_HTTP, SCHEME_HTTPS -> { - showToast("You activated the http/s scheme somehow") + processExternalRequestOnAuthentication(data) } } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/DownloadRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/DownloadRepository.kt index 5bea1f93b..95fbd695a 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/DownloadRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/DownloadRepository.kt @@ -15,7 +15,7 @@ class DownloadRepository @Inject constructor(private val downloadApiHelper: Down val downloadResponse = safeApiCall( call = { downloadApiHelper.getDownloads("Bearer $token", offset, page, limit) }, - errorMessage = "Error Fetching User Info" + errorMessage = "Error Fetching Downloads list or list empty" ) return downloadResponse ?: emptyList() diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/TorrentsRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/TorrentsRepository.kt index 15ca41319..d0874939a 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/TorrentsRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/TorrentsRepository.kt @@ -45,7 +45,7 @@ class TorrentsRepository @Inject constructor(private val torrentApiHelper: Torre token: String, binaryTorrent: ByteArray, host: String - ): UploadedTorrent? { + ): EitherResult { val requestBody: RequestBody = binaryTorrent.toRequestBody( "application/octet-stream".toMediaTypeOrNull(), @@ -53,7 +53,7 @@ class TorrentsRepository @Inject constructor(private val torrentApiHelper: Torre binaryTorrent.size ) - val addTorrentResponse = safeApiCall( + val addTorrentResponse = eitherApiResult( call = { torrentApiHelper.addTorrent( token = "Bearer $token", @@ -71,8 +71,8 @@ class TorrentsRepository @Inject constructor(private val torrentApiHelper: Torre token: String, magnet: String, host: String - ): UploadedTorrent? { - val torrentResponse = safeApiCall( + ): EitherResult { + val torrentResponse = eitherApiResult( call = { torrentApiHelper.addMagnet( token = "Bearer $token", @@ -104,7 +104,7 @@ class TorrentsRepository @Inject constructor(private val torrentApiHelper: Torre filter = filter ) }, - errorMessage = "Error Retrieving Torrent Info" + errorMessage = "Error retrieving the torrents List, or list empty" ) return torrentsResponse ?: emptyList() diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt index ea99c6b4d..723ba1c3f 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/data/repository/UnrestrictRepository.kt @@ -101,7 +101,7 @@ class UnrestrictRepository @Inject constructor(private val unrestrictApiHelper: suspend fun uploadContainer( token: String, container: ByteArray - ): List? { + ): EitherResult> { val requestBody: RequestBody = container.toRequestBody( "application/octet-stream".toMediaTypeOrNull(), @@ -109,7 +109,7 @@ class UnrestrictRepository @Inject constructor(private val unrestrictApiHelper: container.size ) - val uploadResponse = safeApiCall( + val uploadResponse = eitherApiResult( call = { unrestrictApiHelper.uploadContainer( token = "Bearer $token", 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 3f7ead9d9..5deb1e3e1 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 @@ -28,6 +28,7 @@ import com.github.livingwithhippos.unchained.databinding.FragmentDownloadDetails import com.github.livingwithhippos.unchained.downloaddetails.model.AlternativeDownloadAdapter import com.github.livingwithhippos.unchained.downloaddetails.viewmodel.DownloadDetailsMessage import com.github.livingwithhippos.unchained.downloaddetails.viewmodel.DownloadDetailsViewModel +import com.github.livingwithhippos.unchained.lists.view.ListState import com.github.livingwithhippos.unchained.lists.view.ListsTabFragment import com.github.livingwithhippos.unchained.utilities.EitherResult import com.github.livingwithhippos.unchained.utilities.EventObserver @@ -139,7 +140,7 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener { activity?.baseContext?.showToast(R.string.download_removed) // if deleted go back activity?.onBackPressed() - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_DOWNLOAD) + activityViewModel.setListState(ListState.UpdateDownload) } ) @@ -322,6 +323,19 @@ class DownloadDetailsFragment : UnchainedFragment(), DownloadDetailsListener { tryStartExternalApp(mxIntent) } } + "web_video_cast" -> { + val wvcIntent = createMediaIntent("com.instantbits.cast.webvideo", url) + tryStartExternalApp(wvcIntent) + } + "custom_player" -> { + val customPlayerPackage = viewModel.getCustomPlayerPreference() + if (customPlayerPackage.isNullOrBlank()) { + context?.showToast(R.string.invalid_package) + } else { + val customIntent = createMediaIntent(customPlayerPackage, url) + tryStartExternalApp(customIntent) + } + } else -> { context?.showToast(R.string.missing_default_player) } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/viewmodel/DownloadDetailsViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/viewmodel/DownloadDetailsViewModel.kt index 7d861e025..8aa68b902 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/viewmodel/DownloadDetailsViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/downloaddetails/viewmodel/DownloadDetailsViewModel.kt @@ -92,6 +92,10 @@ class DownloadDetailsViewModel @Inject constructor( fun getButtonVisibilityPreference(buttonKey: String, default: Boolean = true): Boolean { return preferences.getBoolean(buttonKey, default) } + + fun getCustomPlayerPreference(): String { + return preferences.getString("custom_media_player", "") ?: "" + } } sealed class DownloadDetailsMessage { 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 334cb9c71..ca9cdb3e2 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 @@ -11,8 +11,9 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResultListener -import androidx.fragment.app.viewModels import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -20,6 +21,8 @@ import androidx.paging.PagingData import androidx.recyclerview.selection.SelectionPredicates import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.selection.StorageStrategy +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 import com.github.livingwithhippos.unchained.R import com.github.livingwithhippos.unchained.base.UnchainedFragment import com.github.livingwithhippos.unchained.data.model.APIError @@ -28,7 +31,10 @@ import com.github.livingwithhippos.unchained.data.model.DownloadItem import com.github.livingwithhippos.unchained.data.model.EmptyBodyError import com.github.livingwithhippos.unchained.data.model.NetworkError import com.github.livingwithhippos.unchained.data.model.TorrentItem +import com.github.livingwithhippos.unchained.databinding.FragmentDownloadsListBinding import com.github.livingwithhippos.unchained.databinding.FragmentTabListsBinding +import com.github.livingwithhippos.unchained.databinding.FragmentTorrentsListBinding +import com.github.livingwithhippos.unchained.lists.viewmodel.ListEvent import com.github.livingwithhippos.unchained.lists.viewmodel.ListTabsViewModel import com.github.livingwithhippos.unchained.lists.viewmodel.ListTabsViewModel.Companion.DOWNLOADS_DELETED import com.github.livingwithhippos.unchained.lists.viewmodel.ListTabsViewModel.Companion.DOWNLOADS_DELETED_ALL @@ -40,16 +46,20 @@ import com.github.livingwithhippos.unchained.lists.viewmodel.ListTabsViewModel.C import com.github.livingwithhippos.unchained.lists.viewmodel.ListTabsViewModel.Companion.TORRENT_NOT_DELETED import com.github.livingwithhippos.unchained.statemachine.authentication.FSMAuthenticationEvent import com.github.livingwithhippos.unchained.statemachine.authentication.FSMAuthenticationState +import com.github.livingwithhippos.unchained.utilities.DOWNLOADS_TAB import com.github.livingwithhippos.unchained.utilities.DataBindingDetailsLookup import com.github.livingwithhippos.unchained.utilities.EitherResult import com.github.livingwithhippos.unchained.utilities.EventObserver +import com.github.livingwithhippos.unchained.utilities.TORRENTS_TAB import com.github.livingwithhippos.unchained.utilities.extension.delayedScrolling import com.github.livingwithhippos.unchained.utilities.extension.downloadFile import com.github.livingwithhippos.unchained.utilities.extension.getApiErrorMessage import com.github.livingwithhippos.unchained.utilities.extension.getDownloadedFileUri +import com.github.livingwithhippos.unchained.utilities.extension.getThemedDrawable import com.github.livingwithhippos.unchained.utilities.extension.showToast import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -60,13 +70,9 @@ import kotlinx.coroutines.launch * It is capable of showing a list of both [DownloadItem] and [TorrentItem] switched with a tab layout. */ @AndroidEntryPoint -class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListListener { +class ListsTabFragment : UnchainedFragment() { - enum class ListState { - UPDATE_TORRENT, UPDATE_DOWNLOAD, READY - } - - private val viewModel: ListTabsViewModel by viewModels() + private val viewModel: ListTabsViewModel by activityViewModels() // used to simulate a debounce effect while typing on the search bar var queryJob: Job? = null @@ -79,36 +85,301 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL val binding: FragmentTabListsBinding = FragmentTabListsBinding.inflate(inflater, container, false) - binding.selectedDownloads = 0 - binding.selectedTorrents = 0 - binding.selectedTab = 0 + val listsAdapter = ListsAdapter(this) + binding.listPager.adapter = listsAdapter - val downloadAdapter = DownloadListPagingAdapter(this) - val torrentAdapter = TorrentListPagingAdapter(this) + binding.tabs.addOnTabSelectedListener( + object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + // to restore on resume? + if (tab != null) + viewModel.setSelectedTab(tab.position) + } - binding.rvDownloadList.adapter = downloadAdapter - binding.rvTorrentList.adapter = torrentAdapter + override fun onTabUnselected(tab: TabLayout.Tab?) { + // either do nothing or remove/add observers + } - // torrent list selection tracker - val torrentTracker: SelectionTracker = SelectionTracker.Builder( - "torrentListSelection", - binding.rvTorrentList, - TorrentKeyProvider(torrentAdapter), - DataBindingDetailsLookup(binding.rvTorrentList), - StorageStrategy.createParcelableStorage(TorrentItem::class.java) - ).withSelectionPredicate( - SelectionPredicates.createSelectAnything() - ).build() + override fun onTabReselected(tab: TabLayout.Tab?) { + // either do nothing or refresh + } + }) - torrentAdapter.tracker = torrentTracker + binding.fabNewDownload.setOnClickListener { + val action = ListsTabFragmentDirections.actionListTabsDestToNewDownloadFragment() + findNavController().navigate(action) + } - torrentTracker.addObserver( - object : SelectionTracker.SelectionObserver() { - override fun onSelectionChanged() { - super.onSelectionChanged() - binding.selectedTorrents = torrentTracker.selection.size() + // an external link has been shared with the app + activityViewModel.externalLinkLiveData.observe( + viewLifecycleOwner, + EventObserver { uri -> + val action = + ListsTabFragmentDirections.actionListTabsDestToNewDownloadFragment(externalUri = uri) + findNavController().navigate(action) + } + ) + + // a file has been downloaded, usually a torrent, and needs to be unrestricted + activityViewModel.downloadedFileLiveData.observe( + viewLifecycleOwner, + EventObserver { fileID -> + val uri = requireContext().getDownloadedFileUri(fileID) + // no need to recheck the extension since it was checked on download + // if (uri?.path?.endsWith(".torrent") == true) + if (uri?.path != null) { + val action = ListsTabFragmentDirections.actionListTabsDestToNewDownloadFragment( + externalUri = uri + ) + findNavController().navigate(action) } - }) + } + ) + + // a notification has been clicked + activityViewModel.notificationTorrentLiveData.observe( + viewLifecycleOwner, + EventObserver { torrentID -> + val action = ListsTabFragmentDirections.actionListsTabToTorrentDetails(torrentID) + findNavController().navigate(action) + } + ) + + viewModel.eventLiveData.observe( + viewLifecycleOwner, + EventObserver { event -> + when (event) { + is ListEvent.DownloadItemClick -> { + val action = + ListsTabFragmentDirections.actionListsTabToDownloadDetails(event.item) + var loop = 0 + val controller = findNavController() + lifecycleScope.launch { + while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { + delay(100) + } + if (controller.currentDestination?.id == R.id.list_tabs_dest) + controller.navigate(action) + } + } + is ListEvent.TorrentItemClick -> { + when (event.item.status) { + "downloaded" -> { + if (event.item.links.size > 1) { + val action = + ListsTabFragmentDirections.actionListTabsDestToFolderListFragment2( + folder = null, + torrent = event.item, + linkList = null + ) + findNavController().navigate(action) + } else + viewModel.downloadTorrent(event.item) + } + // open the torrent details fragment + else -> { + val action = + ListsTabFragmentDirections.actionListsTabToTorrentDetails(event.item.id) + var loop = 0 + + val controller = findNavController() + lifecycleScope.launch { + while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { + delay(100) + } + if (controller.currentDestination?.id == R.id.list_tabs_dest) + controller.navigate(action) + } + } + } + } + is ListEvent.OpenTorrent -> { + val action = + ListsTabFragmentDirections.actionListsTabToTorrentDetails(event.id) + + // workaround to avoid issues when the dialog still hasn't been popped from the navigation stack + val controller = findNavController() + var loop = 0 + lifecycleScope.launch { + while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { + delay(100) + } + if (controller.currentDestination?.id == R.id.list_tabs_dest) + controller.navigate(action) + } + } + is ListEvent.SetTab -> { + + if (event.tab == DOWNLOADS_TAB) { + if (binding.listPager.currentItem == TORRENTS_TAB) + binding.listPager.currentItem = DOWNLOADS_TAB + } else { + if (viewModel.getSelectedTab() == DOWNLOADS_TAB) + binding.listPager.currentItem = TORRENTS_TAB + } + } + } + + } + ) + + + viewModel.errorsLiveData.observe( + viewLifecycleOwner, + EventObserver { + for (error in it) { + when (error) { + is APIError -> { + context?.let { c -> + c.showToast(c.getApiErrorMessage(error.errorCode)) + } + when (error.errorCode) { + 8 -> { + // bad token, try refreshing it + if (activityViewModel.getAuthenticationMachineState() is FSMAuthenticationState.AuthenticatedOpenToken) + activityViewModel.transitionAuthenticationMachine( + FSMAuthenticationEvent.OnExpiredOpenToken + ) + context?.showToast(R.string.refreshing_token) + } + } + } + is EmptyBodyError -> { + } + is NetworkError -> { + context?.showToast(R.string.network_error) + } + is ApiConversionError -> { + context?.showToast(R.string.parsing_error) + } + } + } + } + ) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val tabLayout: TabLayout = view.findViewById(R.id.tabs) + val viewPager: ViewPager2 = view.findViewById(R.id.listPager) + TabLayoutMediator(tabLayout, viewPager) { tab, position -> + if (position == DOWNLOADS_TAB) { + tab.text = getString(R.string.downloads) + tab.icon = requireContext().getThemedDrawable(R.drawable.icon_cloud_done) + } else { + tab.text = getString(R.string.torrents) + tab.icon = requireContext().getThemedDrawable(R.drawable.icon_torrent_logo) + } + }.attach() + + super.onViewCreated(view, savedInstanceState) + } + + // menu-related functions + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.lists_bar, menu) + super.onCreateOptionsMenu(menu, inflater) + + val searchItem = menu.findItem(R.id.search) + val searchView = searchItem.actionView as SearchView + // listens to the user typing in the search bar + + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + // since there is a 500ms delay on new queries, this will help if the user types something and press search in less than half sec. May be unnecessary. The value is checked anyway in the ViewModel to avoid reloading with the same query as the last one. + override fun onQueryTextSubmit(query: String?): Boolean { + viewModel.setListFilter(query) + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + // simulate debounce + queryJob?.cancel() + + queryJob = lifecycleScope.launch { + delay(500) + viewModel.setListFilter(newText) + } + return true + } + }) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.search -> { + true + } + R.id.delete_all_downloads -> { + showDeleteAllDialog(DOWNLOADS_TAB) + true + } + R.id.delete_all_torrents -> { + showDeleteAllDialog(TORRENTS_TAB) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun showDeleteAllDialog(selectedTab: Int) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.delete_all)) + .setMessage( + if (selectedTab == DOWNLOADS_TAB) + getString(R.string.delete_all_downloads_message) + else + getString(R.string.delete_all_torrents_message) + ) + .setNegativeButton(getString(R.string.decline)) { _, _ -> + } + .setPositiveButton(getString(R.string.accept)) { _, _ -> + if (selectedTab == DOWNLOADS_TAB) + viewModel.deleteAllDownloads() + else + viewModel.deleteAllTorrents() + } + .show() + } +} + +class ListsAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { + + override fun getItemCount(): Int = 2 + + override fun createFragment(position: Int): Fragment { + return if (position == DOWNLOADS_TAB) { + DownloadsListFragment() + } else { + TorrentsListFragment() + } + } +} + +@AndroidEntryPoint +class DownloadsListFragment : UnchainedFragment(), DownloadListListener { + + private val viewModel: ListTabsViewModel by activityViewModels() + + // used to simulate a debounce effect while typing on the search bar + var queryJob: Job? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentDownloadsListBinding.inflate(inflater, container, false) + + binding.selectedDownloads = 0 + + val downloadAdapter = DownloadListPagingAdapter(this) + binding.rvDownloadList.adapter = downloadAdapter // download list selection tracker val downloadTracker: SelectionTracker = SelectionTracker.Builder( @@ -134,104 +405,71 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL // listener for selection buttons binding.listener = object : SelectedItemsButtonsListener { override fun deleteSelectedItems() { - if (binding.tabs.selectedTabPosition == TAB_DOWNLOADS) { - if (downloadTracker.selection.toList().isNotEmpty()) - viewModel.deleteDownloads(downloadTracker.selection.toList()) - else - context?.showToast(R.string.select_one_item) - } else { - if (torrentTracker.selection.toList().isNotEmpty()) - viewModel.deleteTorrents(torrentTracker.selection.toList()) - else - context?.showToast(R.string.select_one_item) - } + if (downloadTracker.selection.toList().isNotEmpty()) + viewModel.deleteDownloads(downloadTracker.selection.toList()) + else + context?.showToast(R.string.select_one_item) } override fun shareSelectedItems() { - if (binding.tabs.selectedTabPosition == TAB_DOWNLOADS) { - - if (downloadTracker.selection.toList().isNotEmpty()) { - val shareIntent = Intent(Intent.ACTION_SEND) - shareIntent.type = "text/plain" - val shareLinks = - downloadTracker.selection.joinToString("\n") { it.download } - shareIntent.putExtra(Intent.EXTRA_TEXT, shareLinks) - startActivity( - Intent.createChooser( - shareIntent, - getString(R.string.share_with) - ) + if (downloadTracker.selection.toList().isNotEmpty()) { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "text/plain" + val shareLinks = + downloadTracker.selection.joinToString("\n") { it.download } + shareIntent.putExtra(Intent.EXTRA_TEXT, shareLinks) + startActivity( + Intent.createChooser( + shareIntent, + getString(R.string.share_with) ) - } else - context?.showToast(R.string.select_one_item) - } + ) + } else + context?.showToast(R.string.select_one_item) } override fun downloadSelectedItems() { - if (binding.tabs.selectedTabPosition == TAB_DOWNLOADS) { - - if (downloadTracker.selection.toList().isNotEmpty()) { - var downloadStarted = false - val manager = - requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - downloadTracker.selection.forEach { item -> - val queuedDownload = manager.downloadFile( - item.download, - item.filename, - getString(R.string.app_name), - ) - when (queuedDownload) { - is EitherResult.Failure -> { - context?.showToast( - getString( - R.string.download_not_started_format, - item.filename - ) + if (downloadTracker.selection.toList().isNotEmpty()) { + var downloadStarted = false + val manager = + requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + downloadTracker.selection.forEach { item -> + val queuedDownload = manager.downloadFile( + item.download, + item.filename, + getString(R.string.app_name), + ) + when (queuedDownload) { + is EitherResult.Failure -> { + context?.showToast( + getString( + R.string.download_not_started_format, + item.filename ) - } - is EitherResult.Success -> { - downloadStarted = true - } + ) + } + is EitherResult.Success -> { + downloadStarted = true } } - if (downloadStarted) - context?.showToast(R.string.download_started) - } else - context?.showToast(R.string.select_one_item) - } else { - if (torrentTracker.selection.toList().isNotEmpty()) { - viewModel.downloadItems(torrentTracker.selection.toList()) - } else - context?.showToast(R.string.select_one_item) - } + } + if (downloadStarted) + context?.showToast(R.string.download_started) + } else + context?.showToast(R.string.select_one_item) } } binding.cbSelectAll.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - if (binding.tabs.selectedTabPosition == TAB_DOWNLOADS) { - downloadTracker.setItemsSelected(downloadAdapter.snapshot().items, true) - } else { - torrentTracker.setItemsSelected(torrentAdapter.snapshot().items, true) - } + downloadTracker.setItemsSelected(downloadAdapter.snapshot().items, true) } else { - if (binding.tabs.selectedTabPosition == TAB_DOWNLOADS) { - downloadTracker.clearSelection() - } else { - torrentTracker.clearSelection() - } + downloadTracker.clearSelection() } } binding.srLayout.setOnRefreshListener { - when (binding.tabs.selectedTabPosition) { - TAB_DOWNLOADS -> { - downloadAdapter.refresh() - } - TAB_TORRENTS -> { - torrentAdapter.refresh() - } - } + downloadAdapter.refresh() } // observers created to be easily added and removed. Pass the retrieved list to the adapter and removes the loading icon from the swipe layout @@ -252,6 +490,182 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL } } + // checks the authentication state. Needed to avoid automatic API calls before the authentication process is finished + activityViewModel.fsmAuthenticationState.observe( + viewLifecycleOwner + ) { + when (it?.peekContent()) { + FSMAuthenticationState.AuthenticatedOpenToken, FSMAuthenticationState.AuthenticatedPrivateToken -> { + // register observers if not already registered + if (!viewModel.downloadsLiveData.hasActiveObservers()) + viewModel.downloadsLiveData.observe( + viewLifecycleOwner, + downloadObserver + ) + } + else -> { + } + } + } + + viewModel.downloadItemLiveData.observe( + viewLifecycleOwner, + EventObserver { links -> + // todo: if it gets emptied null/empty should be processed too + if (!links.isNullOrEmpty()) { + // simulate list refresh + binding.srLayout.isRefreshing = true + // refresh items, when returned they'll stop the animation + downloadAdapter.refresh() + + viewModel.postEventNotice(ListEvent.SetTab(DOWNLOADS_TAB)) + } + } + ) + + activityViewModel.listStateLiveData.observe( + viewLifecycleOwner, + EventObserver { + when (it) { + ListState.UpdateDownload -> { + lifecycleScope.launch { + delay(300L) + downloadAdapter.refresh() + lifecycleScope.launch { + binding.rvDownloadList.delayedScrolling(requireContext()) + } + } + } + else -> {} + } + } + ) + + setFragmentResultListener("downloadActionKey") { _, bundle -> + bundle.getString("deletedDownloadKey")?.let { + viewModel.deleteDownload(it) + } + bundle.getParcelable("openedDownloadItem")?.let { + onClick(it) + } + } + + viewModel.deletedDownloadLiveData.observe( + viewLifecycleOwner, + EventObserver { + when (it) { + DOWNLOAD_NOT_DELETED -> { + } + DOWNLOAD_DELETED -> { + context?.showToast(R.string.download_removed) + downloadAdapter.refresh() + } + DOWNLOADS_DELETED -> { + context?.showToast(R.string.downloads_removed) + downloadAdapter.refresh() + } + DOWNLOADS_DELETED_ALL -> { + context?.showToast(R.string.downloads_removed) + lifecycleScope.launch { + // if we don't refresh the cached copy of the last result will be restored on the first list redraw + downloadAdapter.refresh() + downloadAdapter.submitData(PagingData.empty()) + } + } + 0 -> { + context?.showToast(R.string.removing_downloads) + } + else -> { + downloadAdapter.refresh() + } + } + } + ) + + // starts the Transformations.switchMap(queryLiveData) which otherwise won't trigger the Paging request + viewModel.setListFilter("") + + return binding.root + } + + override fun onClick(item: DownloadItem) { + viewModel.postEventNotice(ListEvent.DownloadItemClick(item)) + } +} + +@AndroidEntryPoint +class TorrentsListFragment : UnchainedFragment(), TorrentListListener { + + private val viewModel: ListTabsViewModel by activityViewModels() + + // used to simulate a debounce effect while typing on the search bar + var queryJob: Job? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentTorrentsListBinding.inflate(inflater, container, false) + + val torrentAdapter = TorrentListPagingAdapter(this) + binding.rvTorrentList.adapter = torrentAdapter + + // torrent list selection tracker + binding.selectedTorrents = 0 + val torrentTracker: SelectionTracker = SelectionTracker.Builder( + "torrentListSelection", + binding.rvTorrentList, + TorrentKeyProvider(torrentAdapter), + DataBindingDetailsLookup(binding.rvTorrentList), + StorageStrategy.createParcelableStorage(TorrentItem::class.java) + ).withSelectionPredicate( + SelectionPredicates.createSelectAnything() + ).build() + + torrentAdapter.tracker = torrentTracker + + torrentTracker.addObserver( + object : SelectionTracker.SelectionObserver() { + override fun onSelectionChanged() { + super.onSelectionChanged() + binding.selectedTorrents = torrentTracker.selection.size() + } + }) + + // listener for selection buttons + binding.listener = object : SelectedItemsButtonsListener { + override fun deleteSelectedItems() { + if (torrentTracker.selection.toList().isNotEmpty()) + viewModel.deleteTorrents(torrentTracker.selection.toList()) + else + context?.showToast(R.string.select_one_item) + } + + override fun shareSelectedItems() { + // do nothing for torrents + } + + override fun downloadSelectedItems() { + if (torrentTracker.selection.toList().isNotEmpty()) { + viewModel.downloadItems(torrentTracker.selection.toList()) + } else + context?.showToast(R.string.select_one_item) + } + } + + binding.cbSelectAll.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + torrentTracker.setItemsSelected(torrentAdapter.snapshot().items, true) + } else { + torrentTracker.clearSelection() + } + } + + binding.srLayout.setOnRefreshListener { + torrentAdapter.refresh() + } + val torrentObserver = Observer> { lifecycleScope.launch { torrentAdapter.submitData(it) @@ -267,89 +681,19 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL } // checks the authentication state. Needed to avoid automatic API calls before the authentication process is finished - activityViewModel.fsmAuthenticationState.observe(viewLifecycleOwner, { - if (it != null) { - when (it.peekContent()) { - FSMAuthenticationState.AuthenticatedOpenToken, FSMAuthenticationState.AuthenticatedPrivateToken -> { - // register observers if not already registered - if (!viewModel.downloadsLiveData.hasActiveObservers()) - viewModel.downloadsLiveData.observe( - viewLifecycleOwner, - downloadObserver - ) - - if (!viewModel.torrentsLiveData.hasActiveObservers()) - viewModel.torrentsLiveData.observe(viewLifecycleOwner, torrentObserver) - } - else -> { - } - } - } - } - ) - - binding.tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { - - override fun onTabSelected(tab: TabLayout.Tab?) { - tab?.let { - - binding.selectedTab = it.position - - when (it.position) { - TAB_DOWNLOADS -> { - viewModel.setSelectedTab(TAB_DOWNLOADS) - binding.rvTorrentList.visibility = View.GONE - binding.rvDownloadList.visibility = View.VISIBLE - if (!viewModel.downloadsLiveData.hasActiveObservers()) - viewModel.downloadsLiveData.observe( - viewLifecycleOwner, - downloadObserver - ) - } - TAB_TORRENTS -> { - viewModel.setSelectedTab(TAB_TORRENTS) - binding.rvTorrentList.visibility = View.VISIBLE - binding.rvDownloadList.visibility = View.GONE - if (!viewModel.torrentsLiveData.hasActiveObservers()) - viewModel.torrentsLiveData.observe( - viewLifecycleOwner, - torrentObserver - ) - } - } - } - } - - override fun onTabReselected(tab: TabLayout.Tab?) { - // either do nothing or refresh - } - - override fun onTabUnselected(tab: TabLayout.Tab?) { - // remove observer - when (tab?.position) { - TAB_DOWNLOADS -> { - viewModel.downloadsLiveData.removeObserver(downloadObserver) - } - TAB_TORRENTS -> { - viewModel.torrentsLiveData.removeObserver(torrentObserver) - } + activityViewModel.fsmAuthenticationState.observe( + viewLifecycleOwner + ) { + when (it?.peekContent()) { + FSMAuthenticationState.AuthenticatedOpenToken, FSMAuthenticationState.AuthenticatedPrivateToken -> { + // register observers if not already registered + if (!viewModel.torrentsLiveData.hasActiveObservers()) + viewModel.torrentsLiveData.observe(viewLifecycleOwner, torrentObserver) } - } - }) - - viewModel.downloadItemLiveData.observe( - viewLifecycleOwner, - EventObserver { links -> - if (!links.isNullOrEmpty()) { - // switch to download tab - binding.tabs.getTabAt(TAB_DOWNLOADS)?.select() - // simulate list refresh - binding.srLayout.isRefreshing = true - // refresh items, when returned they'll stop the animation - downloadAdapter.refresh() + else -> { } } - ) + } viewModel.deletedTorrentLiveData.observe( viewLifecycleOwner, @@ -387,16 +731,7 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL viewLifecycleOwner, EventObserver { when (it) { - ListState.UPDATE_DOWNLOAD -> { - lifecycleScope.launch { - delay(300L) - downloadAdapter.refresh() - lifecycleScope.launch { - binding.rvDownloadList.delayedScrolling(requireContext()) - } - } - } - ListState.UPDATE_TORRENT -> { + ListState.UpdateTorrent -> { lifecycleScope.launch { delay(300L) torrentAdapter.refresh() @@ -405,271 +740,38 @@ class ListsTabFragment : UnchainedFragment(), DownloadListListener, TorrentListL } } } - ListState.READY -> { + else -> { } } } ) - setFragmentResultListener("downloadActionKey") { _, bundle -> - bundle.getString("deletedDownloadKey")?.let { - viewModel.deleteDownload(it) - } - bundle.getParcelable("openedDownloadItem")?.let { - onClick(it) - } - } - setFragmentResultListener("torrentActionKey") { _, bundle -> bundle.getString("deletedTorrentKey")?.let { viewModel.deleteTorrent(it) } bundle.getString("openedTorrentItem")?.let { - val action = ListsTabFragmentDirections.actionListsTabToTorrentDetails(it) - - // workaround to avoid issues when the dialog still hasn't been popped from the navigation stack - val controller = findNavController() - var loop = 0 - lifecycleScope.launch { - while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { - delay(100) - } - if (controller.currentDestination?.id == R.id.list_tabs_dest) - controller.navigate(action) - } + viewModel.postEventNotice(ListEvent.OpenTorrent(it)) } bundle.getParcelable("downloadedTorrentItem")?.let { onClick(it) } } - viewModel.deletedDownloadLiveData.observe( - viewLifecycleOwner, - EventObserver { - when (it) { - DOWNLOAD_NOT_DELETED -> { - } - DOWNLOAD_DELETED -> { - context?.showToast(R.string.download_removed) - downloadAdapter.refresh() - } - DOWNLOADS_DELETED -> { - context?.showToast(R.string.downloads_removed) - downloadAdapter.refresh() - } - DOWNLOADS_DELETED_ALL -> { - context?.showToast(R.string.downloads_removed) - lifecycleScope.launch { - // if we don't refresh the cached copy of the last result will be restored on the first list redraw - downloadAdapter.refresh() - downloadAdapter.submitData(PagingData.empty()) - } - } - 0 -> { - context?.showToast(R.string.removing_downloads) - } - else -> { - downloadAdapter.refresh() - } - } - } - ) - - viewModel.errorsLiveData.observe( - viewLifecycleOwner, - EventObserver { - for (error in it) { - when (error) { - is APIError -> { - context?.let { c -> - c.showToast(c.getApiErrorMessage(error.errorCode)) - } - when (error.errorCode) { - 8 -> { - // bad token, try refreshing it - if (activityViewModel.getAuthenticationMachineState() is FSMAuthenticationState.AuthenticatedOpenToken) - activityViewModel.transitionAuthenticationMachine( - FSMAuthenticationEvent.OnExpiredOpenToken - ) - context?.showToast(R.string.refreshing_token) - } - } - } - is EmptyBodyError -> { - } - is NetworkError -> { - context?.showToast(R.string.network_error) - } - is ApiConversionError -> { - context?.showToast(R.string.parsing_error) - } - } - } - } - ) - - binding.fabNewDownload.setOnClickListener { - val action = ListsTabFragmentDirections.actionListTabsDestToNewDownloadFragment() - findNavController().navigate(action) - } - - // starts the Transformations.switchMap(queryLiveData) which otherwise won't trigger the Paging request - viewModel.setListFilter("") - - binding.tabs.getTabAt(viewModel.getSelectedTab())?.select() - - // an external link has been shared with the app - activityViewModel.externalLinkLiveData.observe( - viewLifecycleOwner, - EventObserver { uri -> - val action = - ListsTabFragmentDirections.actionListTabsDestToNewDownloadFragment(externalUri = uri) - findNavController().navigate(action) - } - ) - // a file has been downloaded, usually a torrent, and needs to be unrestricted - activityViewModel.downloadedFileLiveData.observe( - viewLifecycleOwner, - EventObserver { fileID -> - val uri = requireContext().getDownloadedFileUri(fileID) - // no need to recheck the extension since it was checked on download - // if (uri?.path?.endsWith(".torrent") == true) - if (uri?.path != null) { - val action = ListsTabFragmentDirections.actionListTabsDestToNewDownloadFragment( - externalUri = uri - ) - findNavController().navigate(action) - } - } - ) - - // a notification has been clicked - activityViewModel.notificationTorrentLiveData.observe( - viewLifecycleOwner, - EventObserver { torrentID -> - val action = ListsTabFragmentDirections.actionListsTabToTorrentDetails(torrentID) - findNavController().navigate(action) - } - ) - return binding.root } - // menu-related functions - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.lists_bar, menu) - super.onCreateOptionsMenu(menu, inflater) - - val searchItem = menu.findItem(R.id.search) - val searchView = searchItem.actionView as SearchView - // listens to the user typing in the search bar - - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { - // since there is a 500ms delay on new queries, this will help if the user types something and press search in less than half sec. May be unnecessary. The value is checked anyway in the ViewModel to avoid reloading with the same query as the last one. - override fun onQueryTextSubmit(query: String?): Boolean { - viewModel.setListFilter(query) - return true - } - - override fun onQueryTextChange(newText: String?): Boolean { - // simulate debounce - queryJob?.cancel() - - queryJob = lifecycleScope.launch { - delay(500) - viewModel.setListFilter(newText) - } - return true - } - }) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.search -> { - true - } - R.id.delete_all -> { - showDeleteAllDialog(viewModel.getSelectedTab()) - true - } - else -> super.onOptionsItemSelected(item) - } - } - - private fun showDeleteAllDialog(selectedTab: Int) { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.delete_all)) - .setMessage( - if (selectedTab == TAB_DOWNLOADS) - getString(R.string.delete_all_downloads_message) - else - getString(R.string.delete_all_torrents_message) - ) - .setNegativeButton(getString(R.string.decline)) { _, _ -> - } - .setPositiveButton(getString(R.string.accept)) { _, _ -> - if (selectedTab == TAB_DOWNLOADS) - viewModel.deleteAllDownloads() - else - viewModel.deleteAllTorrents() - } - .show() - } - - override fun onClick(item: DownloadItem) { - val action = ListsTabFragmentDirections.actionListsTabToDownloadDetails(item) - var loop = 0 - val controller = findNavController() - lifecycleScope.launch { - while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { - delay(100) - } - if (controller.currentDestination?.id == R.id.list_tabs_dest) - controller.navigate(action) - } + override fun onClick(item: TorrentItem) { + viewModel.postEventNotice(ListEvent.TorrentItemClick(item)) } - override fun onClick(item: TorrentItem) { - when (item.status) { - "downloaded" -> { - if (item.links.size > 1) { - val action = - ListsTabFragmentDirections.actionListTabsDestToFolderListFragment2( - folder = null, - torrent = item, - linkList = null - ) - findNavController().navigate(action) - } else - viewModel.downloadTorrent(item) - } - // open the torrent details fragment - else -> { - val action = ListsTabFragmentDirections.actionListsTabToTorrentDetails(item.id) - var loop = 0 +} - val controller = findNavController() - lifecycleScope.launch { - while (loop++ < 20 && controller.currentDestination?.id != R.id.list_tabs_dest) { - delay(100) - } - if (controller.currentDestination?.id == R.id.list_tabs_dest) - controller.navigate(action) - } - } - } - } - companion object { - const val TAB_DOWNLOADS = 0 - const val TAB_TORRENTS = 1 - } +sealed class ListState { + object UpdateTorrent : ListState() + object UpdateDownload : ListState() + object Ready : ListState() } interface SelectedItemsButtonsListener { diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt index 4ff6b1a37..840c6386e 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/lists/viewmodel/ListTabsViewModel.kt @@ -20,7 +20,7 @@ import com.github.livingwithhippos.unchained.data.repository.DownloadRepository import com.github.livingwithhippos.unchained.data.repository.TorrentsRepository import com.github.livingwithhippos.unchained.data.repository.UnrestrictRepository import com.github.livingwithhippos.unchained.lists.model.DownloadPagingSource -import com.github.livingwithhippos.unchained.lists.view.ListsTabFragment.Companion.TAB_DOWNLOADS +import com.github.livingwithhippos.unchained.utilities.DOWNLOADS_TAB import com.github.livingwithhippos.unchained.utilities.EitherResult import com.github.livingwithhippos.unchained.utilities.Event import com.github.livingwithhippos.unchained.utilities.postEvent @@ -66,6 +66,8 @@ class ListTabsViewModel @Inject constructor( val deletedTorrentLiveData = MutableLiveData>() val deletedDownloadLiveData = MutableLiveData>() + val eventLiveData = MutableLiveData>() + fun downloadTorrent(torrent: TorrentItem) { viewModelScope.launch { val token = protoStore.getCredentials().accessToken @@ -130,7 +132,7 @@ class ListTabsViewModel @Inject constructor( } fun getSelectedTab(): Int { - return savedStateHandle.get(KEY_SELECTED_TAB) ?: TAB_DOWNLOADS + return savedStateHandle.get(KEY_SELECTED_TAB) ?: DOWNLOADS_TAB } fun setListFilter(query: String?) { @@ -212,6 +214,10 @@ class ListTabsViewModel @Inject constructor( } } + fun postEventNotice(event: ListEvent) { + eventLiveData.postEvent(event) + } + companion object { const val KEY_SELECTED_TAB = "selected_tab_key" const val TORRENT_DELETED = -1 @@ -224,3 +230,10 @@ class ListTabsViewModel @Inject constructor( const val DOWNLOAD_NOT_DELETED = -4 } } + +sealed class ListEvent { + data class DownloadItemClick(val item: DownloadItem) : ListEvent() + data class TorrentItemClick(val item: TorrentItem) : ListEvent() + data class OpenTorrent(val id: String) : ListEvent() + data class SetTab(val tab: Int) : ListEvent() +} 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 4ebe1ba82..bd66457b3 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 @@ -23,6 +23,7 @@ import com.github.livingwithhippos.unchained.data.model.EmptyBodyError import com.github.livingwithhippos.unchained.data.model.NetworkError import com.github.livingwithhippos.unchained.data.repository.DownloadResult import com.github.livingwithhippos.unchained.databinding.NewDownloadFragmentBinding +import com.github.livingwithhippos.unchained.lists.view.ListState import com.github.livingwithhippos.unchained.lists.view.ListsTabFragment import com.github.livingwithhippos.unchained.newdownload.viewmodel.Link import com.github.livingwithhippos.unchained.newdownload.viewmodel.NewDownloadViewModel @@ -43,6 +44,7 @@ import com.github.livingwithhippos.unchained.utilities.extension.isMagnet import com.github.livingwithhippos.unchained.utilities.extension.isTorrent import com.github.livingwithhippos.unchained.utilities.extension.isWebUrl import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber import java.io.File @@ -83,7 +85,7 @@ class NewDownloadFragment : UnchainedFragment() { viewLifecycleOwner, EventObserver { linkDetails -> // new download item, alert the list fragment that it needs updating - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_DOWNLOAD) + activityViewModel.setListState(ListState.UpdateDownload) val action = NewDownloadFragmentDirections.actionUnrestrictDownloadToDetailsFragment( linkDetails @@ -96,7 +98,7 @@ class NewDownloadFragment : UnchainedFragment() { viewLifecycleOwner, EventObserver { folder -> // new folder list, alert the list fragment that it needs updating - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_DOWNLOAD) + activityViewModel.setListState(ListState.UpdateDownload) val action = NewDownloadFragmentDirections.actionNewDownloadDestToFolderListFragment( folder = folder, @@ -111,7 +113,7 @@ class NewDownloadFragment : UnchainedFragment() { viewLifecycleOwner, EventObserver { torrent -> // new torrent item, alert the list fragment that it needs updating - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_TORRENT) + activityViewModel.setListState(ListState.UpdateTorrent) val action = NewDownloadFragmentDirections.actionNewDownloadDestToTorrentDetailsFragment( torrent.id @@ -126,7 +128,7 @@ class NewDownloadFragment : UnchainedFragment() { when (link) { is Link.Container -> { // new container, alert the list fragment that it needs updating - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_DOWNLOAD) + activityViewModel.setListState(ListState.UpdateDownload) val action = NewDownloadFragmentDirections.actionNewDownloadDestToFolderListFragment( linkList = link.links.toTypedArray(), @@ -185,7 +187,7 @@ class NewDownloadFragment : UnchainedFragment() { else Timber.e("Asked for a refresh while in a wrong state: ${activityViewModel.getAuthenticationMachineState()}") } - in 9..15 -> { + in 10..15 -> { viewModel.postMessage(errorMessage) when (activityViewModel.getAuthenticationMachineState()) { FSMAuthenticationState.AuthenticatedOpenToken, FSMAuthenticationState.AuthenticatedPrivateToken, FSMAuthenticationState.RefreshingOpenToken -> { @@ -198,6 +200,11 @@ class NewDownloadFragment : UnchainedFragment() { } } } + 9 -> { + // todo: check if permission denied (code 9) is related only to asking for magnet without premium or other stuff too + // we use this because permission denied is not clear + viewModel.postMessage(getString(R.string.premium_needed)) + } else -> { viewModel.postMessage(errorMessage) } @@ -216,13 +223,20 @@ class NewDownloadFragment : UnchainedFragment() { @SuppressLint("ShowToast") val currentToast: Toast = Toast.makeText(requireContext(), "", Toast.LENGTH_SHORT) + var lastToastTime = System.currentTimeMillis() viewModel.toastLiveData.observe( viewLifecycleOwner, EventObserver { - currentToast.cancel() - currentToast.setText(it) - currentToast.show() + lifecycleScope.launch { + currentToast.cancel() + // if we call this too soon between toasts we'll miss some + if (System.currentTimeMillis() - lastToastTime < 1000L) + delay(1000) + currentToast.setText(it) + currentToast.show() + lastToastTime = System.currentTimeMillis() + } } ) } @@ -288,7 +302,7 @@ class NewDownloadFragment : UnchainedFragment() { enableButtons(binding, false) // new folder list, alert the list fragment that it needs updating - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_DOWNLOAD) + activityViewModel.setListState(ListState.UpdateDownload) val action = NewDownloadFragmentDirections.actionNewDownloadDestToFolderListFragment( folder = null, diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt index 91ccfa20a..88403ba86 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/newdownload/viewmodel/NewDownloadViewModel.kt @@ -67,11 +67,14 @@ class NewDownloadViewModel @Inject constructor( fun uploadContainer(container: ByteArray) { viewModelScope.launch { val token = getToken() - val fileList = unrestrictRepository.uploadContainer(token, container) - if (fileList != null) - containerLiveData.postEvent(Link.Container(fileList)) - else - containerLiveData.postEvent(Link.RetrievalError) + when (val fileList = unrestrictRepository.uploadContainer(token, container)) { + is EitherResult.Failure -> { + networkExceptionLiveData.postEvent(fileList.failure) + } + is EitherResult.Success -> { + containerLiveData.postEvent(Link.Container(fileList.success)) + } + } } } @@ -95,11 +98,13 @@ class NewDownloadViewModel @Inject constructor( } else { val addedMagnet = torrentsRepository.addMagnet(token, magnet, availableHosts.first().host) - if (addedMagnet != null) { - // todo: add custom selection of files, this queues all the files - // todo: add checks for already chosen torrent/magnet (if possible), otherwise we get multiple downloads - // todo: get file info and check if it has already been downloaded before doing a select files - torrentLiveData.postEvent(addedMagnet) + when (addedMagnet) { + is EitherResult.Failure -> { + networkExceptionLiveData.postEvent(addedMagnet.failure) + } + is EitherResult.Success -> { + torrentLiveData.postEvent(addedMagnet.success) + } } } } @@ -114,9 +119,14 @@ class NewDownloadViewModel @Inject constructor( } else { val uploadedTorrent = torrentsRepository.addTorrent(token, binaryTorrent, availableHosts.first().host) - if (uploadedTorrent != null) { - // todo: add checks for already chosen torrent/magnet (if possible), otherwise we get multiple downloads - torrentLiveData.postEvent(uploadedTorrent) + when (uploadedTorrent) { + is EitherResult.Failure -> { + networkExceptionLiveData.postEvent(uploadedTorrent.failure) + } + is EitherResult.Success -> { + // todo: add checks for already chosen torrent/magnet (if possible), otherwise we get multiple downloads + torrentLiveData.postEvent(uploadedTorrent.success) + } } } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/model/LinkItemAdapter.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/model/LinkItemAdapter.kt index a37c372f9..2d1c6c3e9 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/model/LinkItemAdapter.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/model/LinkItemAdapter.kt @@ -26,5 +26,6 @@ interface LinkItemListener { data class LinkItem( val type: String, + val name: String, val link: String ) diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchFragment.kt index 59d0b5048..b4eb3f0f2 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchFragment.kt @@ -156,6 +156,7 @@ class SearchFragment : UnchainedFragment(), SearchItemListener { pluginPickerView?.setOnItemClickListener { _, _, position, _ -> val selection: String? = pluginAdapter.getItem(position) if (selection != null) { + binding.pluginPicker.hideKeyboard() setupCategory(categoryPickerView, plugins.first { it.name == selection }) viewModel.setLastSelectedPlugin(plugins.first { it.name == selection }.name) } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchItemFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchItemFragment.kt index 61022562b..551d1907e 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchItemFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/search/view/SearchItemFragment.kt @@ -45,13 +45,13 @@ class SearchItemFragment : UnchainedFragment(), LinkItemListener { val links = mutableListOf() item.magnets.forEach { - links.add(LinkItem(getString(R.string.magnet), it)) + links.add(LinkItem(getString(R.string.magnet), it.substringBefore("&"), it)) } item.torrents.forEach { - links.add(LinkItem(getString(R.string.torrent), it)) + links.add(LinkItem(getString(R.string.torrent), it, it)) } item.hosting.forEach { - links.add(LinkItem(getString(R.string.hoster), it)) + links.add(LinkItem(getString(R.string.hoster), it, it)) } adapter.submitList(links) } 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 32837009a..49827cfa9 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 @@ -31,6 +31,7 @@ import com.github.livingwithhippos.unchained.data.repository.PluginRepository import com.github.livingwithhippos.unchained.data.repository.PluginRepository.Companion.TYPE_UNCHAINED import com.github.livingwithhippos.unchained.data.repository.UserRepository import com.github.livingwithhippos.unchained.data.repository.VariousApiRepository +import com.github.livingwithhippos.unchained.lists.view.ListState import com.github.livingwithhippos.unchained.lists.view.ListsTabFragment import com.github.livingwithhippos.unchained.plugins.model.Plugin import com.github.livingwithhippos.unchained.statemachine.authentication.CurrentFSMAuthentication @@ -89,7 +90,7 @@ class MainActivityViewModel @Inject constructor( val notificationTorrentLiveData = MutableLiveData>() - val listStateLiveData = MutableLiveData>() + val listStateLiveData = MutableLiveData>() val connectivityLiveData = MutableLiveData() // val currentNetworkLiveData = MutableLiveData() @@ -127,7 +128,7 @@ class MainActivityViewModel @Inject constructor( on { transitionTo( FSMAuthenticationState.AuthenticatedOpenToken, - FSMAuthenticationSideEffect.PostAuthenticatedPrivate + FSMAuthenticationSideEffect.PostAuthenticatedOpen ) } on { @@ -139,7 +140,7 @@ class MainActivityViewModel @Inject constructor( on { transitionTo( FSMAuthenticationState.AuthenticatedPrivateToken, - FSMAuthenticationSideEffect.PostAuthenticatedOpen + FSMAuthenticationSideEffect.PostAuthenticatedPrivate ) } on { @@ -647,7 +648,7 @@ class MainActivityViewModel @Inject constructor( } // todo: move this stuff to a shared navigationViewModel - fun setListState(state: ListsTabFragment.ListState) { + fun setListState(state: ListState) { listStateLiveData.postEvent(state) } @@ -810,7 +811,7 @@ class MainActivityViewModel @Inject constructor( } } - fun setupConnectivityCheck(context: Context) { + fun addConnectivityCheck(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager @@ -820,6 +821,14 @@ class MainActivityViewModel @Inject constructor( } } + fun removeConnectivityCheck(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } + private fun checkConnectivity(context: Context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { val connectivityManager = @@ -1053,6 +1062,7 @@ class MainActivityViewModel @Inject constructor( const val KEY_PLUGIN_DOWNLOAD_ID = "plugin_download_id_key" const val KEY_LAST_BACK_PRESS = "last_back_press_key" const val KEY_REFRESHING_TOKEN = "refreshing_token_key" + const val KEY_FSM_AUTH_STATE = "fsm_auth_state_key" } } diff --git a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt index 672951d3b..aad6b2d56 100644 --- a/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt +++ b/app/app/src/main/java/com/github/livingwithhippos/unchained/torrentdetails/view/TorrentDetailsFragment.kt @@ -22,6 +22,7 @@ import com.github.livingwithhippos.unchained.data.model.EmptyBodyError import com.github.livingwithhippos.unchained.data.model.NetworkError import com.github.livingwithhippos.unchained.data.model.TorrentItem import com.github.livingwithhippos.unchained.databinding.FragmentTorrentDetailsBinding +import com.github.livingwithhippos.unchained.lists.view.ListState import com.github.livingwithhippos.unchained.lists.view.ListsTabFragment import com.github.livingwithhippos.unchained.torrentdetails.viewmodel.TorrentDetailsViewModel import com.github.livingwithhippos.unchained.utilities.EventObserver @@ -116,7 +117,7 @@ class TorrentDetailsFragment : UnchainedFragment(), TorrentDetailsListener { activity?.baseContext?.showToast(R.string.torrent_removed) // if deleted go back activity?.onBackPressed() - activityViewModel.setListState(ListsTabFragment.ListState.UPDATE_TORRENT) + activityViewModel.setListState(ListState.UpdateTorrent) } ) 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 c9cbe9860..07898bceb 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 @@ -88,3 +88,6 @@ val loadingStatusList = listOf( "compressing", "uploading" ) + +const val DOWNLOADS_TAB = 0 +const val TORRENTS_TAB = 1 diff --git a/app/app/src/main/res/layout/fragment_downloads_list.xml b/app/app/src/main/res/layout/fragment_downloads_list.xml new file mode 100644 index 000000000..a3b24d9b6 --- /dev/null +++ b/app/app/src/main/res/layout/fragment_downloads_list.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/app/src/main/res/layout/fragment_tab_lists.xml b/app/app/src/main/res/layout/fragment_tab_lists.xml index 4828fe8a0..bc0aa34a5 100644 --- a/app/app/src/main/res/layout/fragment_tab_lists.xml +++ b/app/app/src/main/res/layout/fragment_tab_lists.xml @@ -7,22 +7,11 @@ - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/app/src/main/res/layout/item_list_link.xml b/app/app/src/main/res/layout/item_list_link.xml index 10fb44fbe..7c76bae99 100644 --- a/app/app/src/main/res/layout/item_list_link.xml +++ b/app/app/src/main/res/layout/item_list_link.xml @@ -54,8 +54,8 @@ style="@style/TextAppearance.UnchainedTheme.Body1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:maxLines="7" - android:text="@{item.link, default=`https://verylonglinkbecauseihavetotestthislinkinmylayoutpreview.com/randomfileletterstomakeitevenmorelong:234gyu234gyu`}" + android:maxLines="4" + android:text="@{item.name, default=`https://verylonglinkbecauseihavetotestthislinkinmylayoutpreview.com/randomfileletterstomakeitevenmorelong:234gyu234gyu`}" app:layout_constraintStart_toStartOf="@id/type" app:layout_constraintTop_toBottomOf="@id/type" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/app/src/main/res/menu/lists_bar.xml b/app/app/src/main/res/menu/lists_bar.xml index 693d1d532..929d0ab45 100644 --- a/app/app/src/main/res/menu/lists_bar.xml +++ b/app/app/src/main/res/menu/lists_bar.xml @@ -12,10 +12,17 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" /> + + \ No newline at end of file diff --git a/app/app/src/main/res/values-fr/strings.xml b/app/app/src/main/res/values-fr/strings.xml index f391ff916..71c1f3a0f 100644 --- a/app/app/src/main/res/values-fr/strings.xml +++ b/app/app/src/main/res/values-fr/strings.xml @@ -150,8 +150,7 @@ ]]> - Vous devez posséder un compte Premium pour faire ceci - Vous devez posséder un compte Premium pour les magnets et torrents + Abonnement Premium nécessaire Torrent en préparation, veuillez patienter… Veuillez entrer un magnet ou un lien Veuillez vous identifier @@ -372,7 +371,7 @@ Paramètres de l\'IU Afficher le bouton pour envoyer le média vers un lecteur installé Afficher le bouton média - Lecteur multimédia par défaut à ouvrir + Lecteur multimédia par défaut Envoyer au lecteur Définir le lecteur multimédia par défaut dans les paramètres Choisissez comment et si les choses sont montrées @@ -387,4 +386,9 @@ Installer le pack de plugins %d plugins installés. Redémarrez l\'application pour les utiliser. Utilisez le bouton du menu de l\'écran de recherche pour installer tous les plugins en une seule fois. + Lecteur multimédia personnalisé + Utilisez un paquetage de lecteur multimédia personnalisé comme \"org.videolan.vlc\". Il peut prendre en charge le partage direct des fichiers multimédias. N\'oubliez pas de choisir \"Lecteur multimédia personnalisé\" comme préférence de lecteur multimédia par défaut. + Paquet non valide + Supprimer tous les téléchargements + Supprimer tous les torrents \ No newline at end of file diff --git a/app/app/src/main/res/values-it/strings.xml b/app/app/src/main/res/values-it/strings.xml index 7183af1d6..9c2b722ef 100644 --- a/app/app/src/main/res/values-it/strings.xml +++ b/app/app/src/main/res/values-it/strings.xml @@ -150,11 +150,10 @@ ]]> - Per utilizzare questa funzionalità è richiesto un account premium + Account premium richiesto Torrent in preparazione, attendere… Inserisci un magnet o un link Si prega di autenticarsi sull\'account - Per torrent e magnet è richiestoun abbonamento premium Caricando link magnet… Caricando file torrent… Unchained .torrent download @@ -368,7 +367,7 @@ Impostazioni UI Mostra un bottone per inviare media ad un lettore locale Mostra bottone media - Lettore media da aprire di default + Lettore media predefinito Invia a lettore Imposta il lettore di default nell impostazioni Scegli se e come sono mostrate le cose @@ -383,4 +382,9 @@ Installa pacchetto plugins %d plugins installati. Riavvia l\'app per usarli Usa il bottone nel menu della schermata di ricerca per installare il pacchetto completo dei plugins + Lettore media personalizzato + Usa il package di un lettore media personalizzato come \"org.videolan.vlc\". Potrebbe supportare la condivisione diretta di media. Ricorda di impostare \"lettore media personalizzato\" come impostazione di lettore media predefinito + Package non valido + Elimina tutti i download + Elimina tutti i torrent \ No newline at end of file diff --git a/app/app/src/main/res/values/arrays.xml b/app/app/src/main/res/values/arrays.xml index 36811af5e..e087b14dd 100644 --- a/app/app/src/main/res/values/arrays.xml +++ b/app/app/src/main/res/values/arrays.xml @@ -28,6 +28,7 @@ Tropical Sunset MPV + Web Video Cast @string/user @@ -44,10 +45,14 @@ @string/player_vlc @string/player_mx @string/player_mpv + @string/player_web_video_cast + @string/custom_media_player vlc mx_player mpv + web_video_cast + custom_player diff --git a/app/app/src/main/res/values/strings.xml b/app/app/src/main/res/values/strings.xml index f909c558c..454d2276f 100644 --- a/app/app/src/main/res/values/strings.xml +++ b/app/app/src/main/res/values/strings.xml @@ -356,8 +356,7 @@ ]]> - A premium subscription is required to do this - A premium subscription is required for magnet and torrents + Premium subscription required Preparing torrent, please wait… Please insert a magnet or a link Please log in your account @@ -583,7 +582,7 @@ MX Player Show the button to send the media to an installed player Show media button - Default media player to open + Default media player Send to player Set the default media player in settings Choose how and if things are shown @@ -597,4 +596,9 @@ Show stream in browser button Install plugins pack Use the button in the search screen menu to install all the plugins at once + Custom media player + Use a custom media player package like \"org.videolan.vlc\". It may support direct sharing of media files. Remember to pick \"custom player\" as default media player preference + Invalid package + Delete all downloads + Delete all torrents \ No newline at end of file diff --git a/app/app/src/main/res/xml/settings.xml b/app/app/src/main/res/xml/settings.xml index eb143b3ea..91830b5e0 100644 --- a/app/app/src/main/res/xml/settings.xml +++ b/app/app/src/main/res/xml/settings.xml @@ -59,27 +59,49 @@ - - - - - + + + + + + - + - - + app:allowDividerAbove="false" + app:allowDividerBelow="false"> + + + + + + @@ -103,7 +125,6 @@ app:summary="@string/show_folder_filters_summary" app:title="@string/show_folder_filters" /> - diff --git a/app/versions.gradle b/app/versions.gradle index cea28e2f2..cf941d4e2 100644 --- a/app/versions.gradle +++ b/app/versions.gradle @@ -39,6 +39,7 @@ versions.test = "1.4.0" versions.test_junit = "1.1.3" versions.test_orchestrator = "1.4.1" versions.timber = "5.0.1" +versions.viewpager2 = "1.0.0" ext.versions = versions ext.deps = [:] @@ -218,4 +219,6 @@ deps.timber = "com.jakewharton.timber:timber:$versions.timber" deps.transition = "androidx.transition:transition:$versions.transition" +deps.viewpager2 = "androidx.viewpager2:viewpager2:$versions.viewpager2" + ext.deps = deps \ No newline at end of file diff --git a/extra_assets/plugins/max-rls.unchained b/extra_assets/plugins/max-rls.unchained deleted file mode 100644 index 6e2843a79..000000000 --- a/extra_assets/plugins/max-rls.unchained +++ /dev/null @@ -1,96 +0,0 @@ -{ - "engine_version": 2.0, - "version": 1.0, - "url": "http://max-rls.com", - "name": "max-rls", - "description": "Parser for max-rls.com", - "supported_categories": { - "all": "None" - }, - "search": { - "no_category": "${url}/page/${page}/?s=${query}", - "page_start": 1 - }, - "download": { - "internal": { - "link": { - "regex_use": "all", - "regexps": [ - { - "regex": "href=\"(http:\/\/max-rls\\.com\/[^\"]+)\"\\s+title", - "group": 1 - } - ] - } - }, - "regexes": { - "name": { - "regex_use": "first", - "regexps": [ - { - "regex": "([^<]+)", - "group": 1, - "slug_type": "complete" - } - ] - }, - "hosting": { - "regex_use": "all", - "regexps": [ - { - "regex": "href=\"(https?://(www\\.)?rapidgator\\.(net|asia)/file/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?hexupload\\.net/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?uploadgig\\.com/file/download/\\w{16}/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?nitroflare\\.com/view/[\\w]+/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?dropapk\\.to/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?turbobit\\.net/[\\w]+/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?uploaded\\.(to|net)/file/[\\w]+/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?drop\\.download/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?fastclick\\.to/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?katfile\\.com/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?usersdrive\\.com/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?clicknupload\\.cc/[\\w]+/[^\"]+)", - "group": 1 - }, - { - "regex": "href=\"(https?://(www\\.)?mixloads\\.com/[^\"]+)", - "group": 1 - } - ] - } - } - } -} \ No newline at end of file diff --git a/extra_assets/plugins/scene-rls.unchained b/extra_assets/plugins/scene-rls.unchained index deb576ba1..26a6db580 100644 --- a/extra_assets/plugins/scene-rls.unchained +++ b/extra_assets/plugins/scene-rls.unchained @@ -1,7 +1,7 @@ { "engine_version": 2.0, - "version": 1.1, - "url": "http://scene-rls.net", + "version": 1.2, + "url": "https://scene-rls.net", "name": "scene-rls", "description": "Parser for scene-rls.net", "supported_categories": { @@ -17,7 +17,7 @@ "regex_use": "all", "regexps": [ { - "regex": "href=\"(http:\/\/scene-rls\\.net\/[^\"]+)\"\\s+title", + "regex": "href=\"(https?:\/\/scene-rls\\.net\/[^\"]+)\"\\s+title", "group": 1 } ] @@ -93,4 +93,4 @@ } } } -} \ No newline at end of file +} diff --git a/extra_assets/plugins/unchained_plugins_pack.zip b/extra_assets/plugins/unchained_plugins_pack.zip index d87942e26..263b69186 100644 Binary files a/extra_assets/plugins/unchained_plugins_pack.zip and b/extra_assets/plugins/unchained_plugins_pack.zip differ