Initial work on SmartSearch
This commit is contained in:
parent
b5263a6968
commit
8934d251d9
@ -253,9 +253,13 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
|
||||
final coroutines_version = '1.2.0'
|
||||
final coroutines_version = '1.3.0-RC'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version"
|
||||
|
||||
// Text distance (EH)
|
||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||
|
||||
// Pin lock view (EH)
|
||||
implementation 'com.andrognito.pinlockview:pinlockview:2.1.0'
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.view.*
|
||||
@ -23,6 +25,8 @@ import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import exh.ui.smartsearch.SmartSearchController
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -34,7 +38,7 @@ import uy.kohesive.injekt.api.get
|
||||
* [CatalogueAdapter.OnBrowseClickListener] call function data on browse item click.
|
||||
* [CatalogueAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
class CatalogueController(bundle: Bundle? = null) : NucleusController<CataloguePresenter>(bundle),
|
||||
SourceLoginDialog.Listener,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
CatalogueAdapter.OnBrowseClickListener,
|
||||
@ -50,12 +54,18 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
*/
|
||||
private var adapter: CatalogueAdapter? = null
|
||||
|
||||
private val smartSearchConfig: SmartSearchConfig? = args.getParcelable(SMART_SEARCH_CONFIG)
|
||||
|
||||
// EXH -->
|
||||
private val mode = if(smartSearchConfig == null) Mode.CATALOGUE else Mode.SMART_SEARCH
|
||||
// EXH <--
|
||||
|
||||
/**
|
||||
* Called when controller is initialized.
|
||||
*/
|
||||
init {
|
||||
// Enable the option menu
|
||||
setHasOptionsMenu(true)
|
||||
setHasOptionsMenu(mode == Mode.CATALOGUE)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,7 +74,10 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
* @return title.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.label_catalogues)
|
||||
return when(mode) {
|
||||
Mode.CATALOGUE -> applicationContext?.getString(R.string.label_catalogues)
|
||||
Mode.SMART_SEARCH -> "Find in another source"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +86,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
* @return instance of [CataloguePresenter]
|
||||
*/
|
||||
override fun createPresenter(): CataloguePresenter {
|
||||
return CataloguePresenter()
|
||||
return CataloguePresenter(controllerMode = mode)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -140,8 +153,16 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
dialog.targetController = this
|
||||
dialog.showDialog(router)
|
||||
} else {
|
||||
// Open the catalogue view.
|
||||
openCatalogue(source, BrowseCatalogueController(source))
|
||||
when(mode) {
|
||||
Mode.CATALOGUE -> {
|
||||
// Open the catalogue view.
|
||||
openCatalogue(source, BrowseCatalogueController(source))
|
||||
}
|
||||
Mode.SMART_SEARCH -> router.pushController(SmartSearchController(Bundle().apply {
|
||||
putLong(SmartSearchController.ARG_SOURCE_ID, source.id)
|
||||
putParcelable(SmartSearchController.ARG_SMART_SEARCH_CONFIG, smartSearchConfig)
|
||||
}).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -233,4 +254,18 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
}
|
||||
|
||||
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
|
||||
|
||||
// EXH -->
|
||||
@Parcelize
|
||||
data class SmartSearchConfig(val title: String) : Parcelable
|
||||
// EXH <--
|
||||
|
||||
enum class Mode {
|
||||
CATALOGUE,
|
||||
SMART_SEARCH
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
||||
}
|
||||
}
|
@ -24,7 +24,8 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
class CataloguePresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
private val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val controllerMode: CatalogueController.Mode
|
||||
) : BasePresenter<CatalogueController>() {
|
||||
|
||||
/**
|
||||
@ -62,7 +63,7 @@ class CataloguePresenter(
|
||||
val byLang = sources.groupByTo(map, { it.lang })
|
||||
val sourceItems = byLang.flatMap {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source -> SourceItem(source, langItem) }
|
||||
it.value.map { source -> SourceItem(source, langItem, controllerMode == CatalogueController.Mode.CATALOGUE) }
|
||||
}
|
||||
|
||||
sourceSubscription = Observable.just(sourceItems)
|
||||
@ -77,7 +78,7 @@ class CataloguePresenter(
|
||||
sharedObs.take(1),
|
||||
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
|
||||
.distinctUntilChanged()
|
||||
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
|
||||
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it, showButtons = controllerMode == CatalogueController.Mode.CATALOGUE) } }
|
||||
.subscribeLatestCache(CatalogueController::setLastUsedSource)
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.util.visible
|
||||
import io.github.mthli.slice.Slice
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
||||
|
||||
class SourceHolder(view: View, override val adapter: CatalogueAdapter) :
|
||||
class SourceHolder(view: View, override val adapter: CatalogueAdapter, val showButtons: Boolean) :
|
||||
BaseFlexibleViewHolder(view, adapter),
|
||||
SlicedHolder {
|
||||
|
||||
@ -30,6 +30,11 @@ class SourceHolder(view: View, override val adapter: CatalogueAdapter) :
|
||||
source_latest.setOnClickListener {
|
||||
adapter.latestClickListener.onLatestClick(adapterPosition)
|
||||
}
|
||||
|
||||
if(!showButtons) {
|
||||
source_browse.gone()
|
||||
source_latest.gone()
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(item: SourceItem) {
|
||||
@ -50,7 +55,7 @@ class SourceHolder(view: View, override val adapter: CatalogueAdapter) :
|
||||
source_latest.gone()
|
||||
} else {
|
||||
source_browse.setText(R.string.browse)
|
||||
if (source.supportsLatest) {
|
||||
if (source.supportsLatest && showButtons) {
|
||||
source_latest.visible()
|
||||
} else {
|
||||
source_latest.gone()
|
||||
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
* @param source Instance of [CatalogueSource] containing source information.
|
||||
* @param header The header for this item.
|
||||
*/
|
||||
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
|
||||
data class SourceItem(val source: CatalogueSource, val header: LangItem? = null, val showButtons: Boolean) :
|
||||
AbstractSectionableItem<SourceHolder, LangItem>(header) {
|
||||
|
||||
/**
|
||||
@ -28,7 +28,7 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null)
|
||||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
|
||||
return SourceHolder(view, adapter as CatalogueAdapter)
|
||||
return SourceHolder(view, adapter as CatalogueAdapter, showButtons)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,6 +38,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
@ -160,6 +161,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
// EXH -->
|
||||
R.id.action_smart_search -> openSmartSearch()
|
||||
// EXH <--
|
||||
R.id.action_open_in_browser -> openInBrowser()
|
||||
R.id.action_open_in_web_view -> openInWebView()
|
||||
R.id.action_share -> shareManga()
|
||||
@ -169,6 +173,17 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// EXH -->
|
||||
private fun openSmartSearch() {
|
||||
val smartSearchConfig = CatalogueController.SmartSearchConfig(presenter.manga.title)
|
||||
|
||||
parentController?.router?.pushController(CatalogueController(Bundle().apply {
|
||||
putParcelable(CatalogueController.SMART_SEARCH_CONFIG, smartSearchConfig)
|
||||
}).withFadeTransaction())
|
||||
}
|
||||
// EXH <--
|
||||
|
||||
/**
|
||||
* Check if manga is initialized.
|
||||
* If true update view with manga information,
|
||||
|
214
app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt
Normal file
214
app/src/main/java/exh/ui/smartsearch/SmartSearchController.kt
Normal file
@ -0,0 +1,214 @@
|
||||
package exh.ui.smartsearch
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import exh.util.await
|
||||
import info.debatty.java.stringsimilarity.NormalizedLevenshtein
|
||||
import kotlinx.android.synthetic.main.eh_smart_search.*
|
||||
import kotlinx.coroutines.*
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.text.StringBuilder
|
||||
|
||||
class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSearchPresenter>() {
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
private val source = sourceManager.get(bundle?.getLong(ARG_SOURCE_ID, -1) ?: -1) as? CatalogueSource
|
||||
private val smartSearchConfig: CatalogueController.SmartSearchConfig? = bundle?.getParcelable(ARG_SMART_SEARCH_CONFIG)
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup) =
|
||||
inflater.inflate(R.layout.eh_smart_search, container, false)!!
|
||||
|
||||
override fun getTitle() = source?.name ?: ""
|
||||
|
||||
override fun createPresenter() = SmartSearchPresenter()
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
appbar.bringToFront()
|
||||
|
||||
if(source == null || smartSearchConfig == null) {
|
||||
router.popCurrentController()
|
||||
applicationContext?.toast("Missing data!")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Error handling
|
||||
|
||||
// TODO Use activity scope
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val resultManga = initiateSmartSearch(source, smartSearchConfig)
|
||||
if(resultManga != null) {
|
||||
val localManga = networkToLocalManga(resultManga, source.id)
|
||||
val transaction = MangaController(localManga, true).withFadeTransaction()
|
||||
withContext(Dispatchers.Main) {
|
||||
router.replaceTopController(transaction)
|
||||
}
|
||||
} else {
|
||||
// TODO Open search
|
||||
router.popCurrentController()
|
||||
}
|
||||
println(resultManga)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun initiateSmartSearch(source: CatalogueSource, config: CatalogueController.SmartSearchConfig): SManga? {
|
||||
val cleanedTitle = cleanSmartSearchTitle(config.title)
|
||||
|
||||
val queries = getSmartSearchQueries(cleanedTitle)
|
||||
|
||||
val eligibleManga = supervisorScope {
|
||||
queries.map { query ->
|
||||
async(Dispatchers.IO) {
|
||||
val searchResults = source.fetchSearchManga(1, query, FilterList()).toSingle().await(Schedulers.io())
|
||||
|
||||
searchResults.mangas.map {
|
||||
val cleanedMangaTitle = cleanSmartSearchTitle(it.title)
|
||||
val normalizedDistance = NormalizedLevenshtein().similarity(cleanedTitle, cleanedMangaTitle)
|
||||
SearchEntry(it, normalizedDistance)
|
||||
}.filter { (_, normalizedDistance) ->
|
||||
normalizedDistance >= MIN_ELIGIBLE_THRESHOLD
|
||||
}
|
||||
}
|
||||
}.flatMap { it.await() }
|
||||
}
|
||||
|
||||
return eligibleManga.maxBy { it.dist }?.manga
|
||||
}
|
||||
|
||||
fun getSmartSearchQueries(cleanedTitle: String): List<String> {
|
||||
val splitCleanedTitle = cleanedTitle.split(" ")
|
||||
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
||||
|
||||
if(splitCleanedTitle.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Search cleaned title
|
||||
// Search two largest words
|
||||
// Search largest word
|
||||
// Search first two words
|
||||
// Search first word
|
||||
|
||||
val searchQueries = listOf(
|
||||
listOf(cleanedTitle),
|
||||
splitSortedByLargest.take(2),
|
||||
splitSortedByLargest.take(1),
|
||||
splitCleanedTitle.take(2),
|
||||
splitCleanedTitle.take(1)
|
||||
)
|
||||
|
||||
return searchQueries.map {
|
||||
it.joinToString().trim()
|
||||
}.distinct()
|
||||
}
|
||||
|
||||
fun cleanSmartSearchTitle(title: String): String {
|
||||
val preTitle = title.toLowerCase()
|
||||
|
||||
// Remove text in brackets
|
||||
var cleanedTitle = removeTextInBrackets(preTitle, true)
|
||||
if(cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards
|
||||
cleanedTitle = removeTextInBrackets(preTitle, false)
|
||||
}
|
||||
|
||||
// Strip non-special characters
|
||||
cleanedTitle = cleanedTitle.replace(titleRegex, " ")
|
||||
|
||||
// Strip splitters and consecutive spaces
|
||||
cleanedTitle = cleanedTitle.trim().replace(" - ", " ").replace(consecutiveSpacesRegex, " ").trim()
|
||||
|
||||
return cleanedTitle
|
||||
}
|
||||
|
||||
private fun removeTextInBrackets(text: String, readForward: Boolean): String {
|
||||
val bracketPairs = listOf(
|
||||
'(' to ')',
|
||||
'[' to ']',
|
||||
'<' to '>',
|
||||
'{' to '}'
|
||||
)
|
||||
var openingBracketPairs = bracketPairs.mapIndexed { index, (opening, _) ->
|
||||
opening to index
|
||||
}.toMap()
|
||||
var closingBracketPairs = bracketPairs.mapIndexed { index, (_, closing) ->
|
||||
closing to index
|
||||
}.toMap()
|
||||
|
||||
// Reverse pairs if reading backwards
|
||||
if(!readForward) {
|
||||
val tmp = openingBracketPairs
|
||||
openingBracketPairs = closingBracketPairs
|
||||
closingBracketPairs = tmp
|
||||
}
|
||||
|
||||
val depthPairs = bracketPairs.map { 0 }.toMutableList()
|
||||
|
||||
val result = StringBuilder()
|
||||
for(c in if(readForward) text else text.reversed()) {
|
||||
val openingBracketDepthIndex = openingBracketPairs[c]
|
||||
if(openingBracketDepthIndex != null) {
|
||||
depthPairs[openingBracketDepthIndex]++
|
||||
} else {
|
||||
val closingBracketDepthIndex = closingBracketPairs[c]
|
||||
if(closingBracketDepthIndex != null) {
|
||||
depthPairs[closingBracketDepthIndex]--
|
||||
} else {
|
||||
if(depthPairs.all { it <= 0 }) {
|
||||
result.append(c)
|
||||
} else {
|
||||
// In brackets, do not append to result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
*
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
newManga.copyFrom(sManga)
|
||||
val result = db.insertManga(newManga).executeAsBlocking()
|
||||
newManga.id = result.insertedId()
|
||||
localManga = newManga
|
||||
}
|
||||
return localManga
|
||||
}
|
||||
|
||||
data class SearchEntry(val manga: SManga, val dist: Double)
|
||||
|
||||
companion object {
|
||||
const val ARG_SOURCE_ID = "SOURCE_ID"
|
||||
const val ARG_SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
||||
const val MIN_ELIGIBLE_THRESHOLD = 0.7
|
||||
|
||||
private val titleRegex = Regex("[^a-zA-Z0-9- ]")
|
||||
private val consecutiveSpacesRegex = Regex(" +")
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package exh.ui.smartsearch
|
||||
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
|
||||
class SmartSearchPresenter: BasePresenter<SmartSearchPresenter>() {
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M11,6c1.38,0 2.63,0.56 3.54,1.46L12,10h6L18,4l-2.05,2.05C14.68,4.78 12.93,4 11,4c-3.53,0 -6.43,2.61 -6.92,6L6.1,10c0.46,-2.28 2.48,-4 4.9,-4zM16.64,15.14c0.66,-0.9 1.12,-1.97 1.28,-3.14L15.9,12c-0.46,2.28 -2.48,4 -4.9,4 -1.38,0 -2.63,-0.56 -3.54,-1.46L10,12L4,12v6l2.05,-2.05C7.32,17.22 9.07,18 11,18c1.55,0 2.98,-0.51 4.14,-1.36L20,21.49 21.49,20l-4.85,-4.86z"/>
|
||||
</vector>
|
@ -20,7 +20,6 @@
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
|
57
app/src/main/res/layout/eh_smart_search.xml
Executable file
57
app/src/main/res/layout/eh_smart_search.xml
Executable file
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:elevation="0dp">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/actionBarTheme" />
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/intercept_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="Searching source..."
|
||||
android:textAppearance="@style/TextAppearance.Medium.Title"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/intercept_progress"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminateTint="@color/white" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
@ -8,6 +8,11 @@
|
||||
android:title="@string/action_share"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item android:id="@+id/action_smart_search"
|
||||
android:icon="@drawable/eh_ic_find_replace_white_24dp"
|
||||
android:title="Find in another source"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item android:id="@+id/action_open_in_browser"
|
||||
android:title="@string/action_open_in_browser"
|
||||
app:showAsAction="never"/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user