diff --git a/.gitignore b/.gitignore index 1ac2501..3ffd84d 100644 --- a/.gitignore +++ b/.gitignore @@ -97,7 +97,8 @@ fabric.properties # *.iml # modules.xml -.idea/misc.xml + .idea/misc.xml + misc.xml # *.ipr diff --git a/app/build.gradle b/app/build.gradle index 4dbb44d..ba64bd2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,7 @@ android { buildConfigField("boolean", "PREPARE_TO_RELEASE", "$prepareToRelease") buildConfigField("String", "MIXPANEL_TOKEN", '"eb99af1dad563cbaaf02f008b28e321f"') buildConfigField("String", "GCM_SENDER", '"597127048287"') + multiDexEnabled true vectorDrawables.useSupportLibrary true } signingConfigs { @@ -76,6 +77,7 @@ configurations.all { dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile(name: 'clansFAB', ext: 'aar') + compile(name: 'googleinapputil', ext: 'aar') compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.anko:anko-common:$anko_version" @@ -117,7 +119,11 @@ dependencies { compile "com.github.emanzanoaxa:RippleEffect:52ea2a0ab6" // compile "com.github.traex.rippleeffect:library:1.3" + compile "com.github.miguelbcr:TableFixHeaders-Wrapper:0.2.0" + compile "com.timehop.stickyheadersrecyclerview:library:0.4.3" compile "jp.wasabeef:recyclerview-animators:2.2.4" + compile "com.kyleduo.switchbutton:library:1.4.4" + compile "org.jsoup:jsoup:1.10.1" testCompile "junit:junit:4.12" testCompile "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion" diff --git a/app/libs/googleinapputil.aar b/app/libs/googleinapputil.aar new file mode 100644 index 0000000..bde8736 Binary files /dev/null and b/app/libs/googleinapputil.aar differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 356cc85..d81db10 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -70,4 +70,9 @@ -dontwarn com.mixpanel.** -keep class **.R$* { ; +} + +## JSoup +-keep public class org.jsoup.** { + public *; } \ No newline at end of file diff --git a/app/src/debug/java/com/ediposouza/teslesgendstracker/util/MetricsManager.kt b/app/src/debug/java/com/ediposouza/teslesgendstracker/util/MetricsManager.kt index 1f0e2c6..eb39381 100644 --- a/app/src/debug/java/com/ediposouza/teslesgendstracker/util/MetricsManager.kt +++ b/app/src/debug/java/com/ediposouza/teslesgendstracker/util/MetricsManager.kt @@ -8,7 +8,7 @@ import timber.log.Timber /** * Created by ediposouza on 08/12/16. */ -@SuppressWarnings("unused") +@Suppress("UNUSED_PARAMETER") object MetricsManager : MetricsConstants() { fun initialize(context: Context) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5adb704..77676b9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ + @@ -32,16 +33,24 @@ + + = object : Parcelable.Creator { @@ -109,7 +112,7 @@ data class Deck( override fun describeContents() = 0 override fun writeToParcel(dest: Parcel?, flags: Int) { - dest?.writeString(id) + dest?.writeString(uuid) dest?.writeString(name) dest?.writeString(owner) dest?.writeInt((if (private) 1 else 0)) @@ -125,4 +128,9 @@ data class Deck( dest?.writeList(updates) dest?.writeList(comments) } + + override fun toString(): String { + return "Deck(id='$uuid', name='$name', owner='$owner', private=$private, type=$type, cls=$cls, cost=$cost, createdAt=$createdAt, updatedAt=$updatedAt, patch='$patch', likes=$likes, views=$views, cards=$cards, updates=$updates, comments=$comments)" + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/General.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/General.kt index 88cf258..4477a61 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/General.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/General.kt @@ -6,6 +6,22 @@ import android.os.Parcelable /** * Created by EdipoSouza on 10/31/16. */ +data class UserInfo( + + val name: String, + val photoUrl: String + +) + +data class CardCollection( + + val cardShortName: String, + val cardAttr: Attribute, + val cardSet: CardSet, + val qtd: Int + +) + data class CardSlot( val card: Card, @@ -35,7 +51,97 @@ data class CardSlot( data class Patch( - val uidDate: String, + val uuidDate: String, val desc: String -) \ No newline at end of file +) + +data class Season( + + val id: Int, + val uuid: String, + val desc: String, + val reward: String + +) + +enum class MatchMode { + + RANKED, + CASUAL, + ARENA + +} + +data class MatchDeck( + + val name: String, + val cls: Class, + val type: DeckType, + val deck: String? = null, + val version: String? = null + +) : Parcelable { + + companion object { + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): MatchDeck = MatchDeck(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + constructor(source: Parcel) : this(source.readString(), Class.values()[source.readInt()], + DeckType.values()[source.readInt()], source.readString(), source.readString()) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel?, flags: Int) { + dest?.writeString(name) + dest?.writeInt(cls.ordinal) + dest?.writeInt(type.ordinal) + dest?.writeString(deck) + dest?.writeString(version) + } +} + +data class Match( + + val uuid: String, + val first: Boolean, + val player: MatchDeck, + val opponent: MatchDeck, + val mode: MatchMode, + val season: String, + val rank: Int, + val legend: Boolean, + val win: Boolean + +) : Parcelable { + + companion object { + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): Match = Match(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + constructor(source: Parcel) : this(source.readString(), 1 == source.readInt(), + source.readParcelable(MatchDeck::class.java.classLoader), + source.readParcelable(MatchDeck::class.java.classLoader), + MatchMode.values()[source.readInt()], source.readString(), source.readInt(), + 1 == source.readInt(), 1 == source.readInt()) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel?, flags: Int) { + dest?.writeString(uuid) + dest?.writeInt((if (first) 1 else 0)) + dest?.writeParcelable(player, 0) + dest?.writeParcelable(opponent, 0) + dest?.writeInt(mode.ordinal) + dest?.writeString(season) + dest?.writeInt(rank) + dest?.writeInt((if (legend) 1 else 0)) + dest?.writeInt((if (win) 1 else 0)) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/User.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/User.kt deleted file mode 100644 index a533e5a..0000000 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/data/User.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.ediposouza.teslesgendstracker.data - -/** - * Created by ediposouza on 10/31/16. - */ -data class User( - - val name: String, - val collection: List, - val collectionPercent: Float, - val decks: List, - val decksFavorites: List, - val matches: List - -) - -data class UserInfo( - - val name: String, - val photoUrl: String - -) - -data class Match( - - val player: MatchDeck, - val opponent: MatchDeck, - val result: Boolean, - val rack: Int, - val legend: Boolean - -) - -data class MatchDeck( - - val name: String?, - val cls: Class?, - val type: DeckType? - -) \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/BaseInteractor.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/BaseInteractor.kt index bfb5639..288a0d4 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/BaseInteractor.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/BaseInteractor.kt @@ -3,9 +3,7 @@ package com.ediposouza.teslesgendstracker.interactor import com.ediposouza.teslesgendstracker.data.Attribute import com.ediposouza.teslesgendstracker.data.CardSet import com.ediposouza.teslesgendstracker.util.ConfigManager -import com.google.firebase.database.DatabaseReference -import com.google.firebase.database.FirebaseDatabase -import com.google.firebase.database.Query +import com.google.firebase.database.* /** * Created by ediposouza on 01/11/16. @@ -16,6 +14,7 @@ open class BaseInteractor { val NODE_CARDS = "cards" val NODE_PATCHES = "patches" + val NODE_SEASONS = "seasons" } @@ -94,4 +93,23 @@ open class BaseInteractor { keepSynced(!ConfigManager.isDBUpdating() && !ConfigManager.isVersionUnsupported()) } + abstract class SimpleChildEventListener : ChildEventListener { + + override fun onChildMoved(snapshot: DataSnapshot?, previousChildName: String?) { + } + + override fun onChildChanged(snapshot: DataSnapshot?, previousChildName: String?) { + } + + override fun onChildAdded(snapshot: DataSnapshot?, previousChildName: String?) { + } + + override fun onChildRemoved(snapshot: DataSnapshot?) { + } + + override fun onCancelled(error: DatabaseError?) { + } + + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/FirebaseParsers.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/FirebaseParsers.kt index e5a18f2..1cfcc36 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/FirebaseParsers.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/FirebaseParsers.kt @@ -65,23 +65,23 @@ abstract class FirebaseParsers { companion object { - private val KEY_DECK_NAME = "owner" - private val KEY_DECK_TYPE = "type" - private val KEY_DECK_CLASS = "cls" - private val KEY_DECK_PATCH = "patch" - private val KEY_DECK_COMMENT_OWNER = "owner" - private val KEY_DECK_COMMENT_MSG = "comment" - private val KEY_DECK_COMMENT_CREATE_AT = "createdAt" + private const val KEY_DECK_NAME = "owner" + private const val KEY_DECK_TYPE = "type" + private const val KEY_DECK_CLASS = "cls" + private const val KEY_DECK_PATCH = "patch" + private const val KEY_DECK_COMMENT_OWNER = "owner" + private const val KEY_DECK_COMMENT_MSG = "comment" + private const val KEY_DECK_COMMENT_CREATE_AT = "createdAt" fun toNewCommentMap(owner: String, comment: String): Map { return mapOf(KEY_DECK_COMMENT_OWNER to owner, KEY_DECK_COMMENT_MSG to comment, - KEY_DECK_COMMENT_CREATE_AT to LocalDateTime.now().toString()) + KEY_DECK_COMMENT_CREATE_AT to LocalDateTime.now().withNano(0).toString()) } } - fun toDeck(id: String, private: Boolean): Deck { - return Deck(id, name, owner, private, DeckType.values()[type], Class.values()[cls], cost, + fun toDeck(uuid: String, private: Boolean): Deck { + return Deck(uuid, name, owner, private, DeckType.values()[type], Class.values()[cls], cost, LocalDateTime.parse(createdAt), LocalDateTime.parse(updatedAt), patch, likes, views, cards, updates.map { DeckUpdate(LocalDateTime.parse(it.key), it.value) }, comments.map { @@ -96,7 +96,7 @@ abstract class FirebaseParsers { deck.createdAt.toString(), deck.updatedAt.toString(), deck.patch, deck.views, deck.likes, deck.cards, deck.updates.map { it.date.toString() to it.changes }.toMap(), deck.comments.map { - it.id to mapOf( + it.uuid to mapOf( KEY_DECK_COMMENT_OWNER to it.owner, KEY_DECK_COMMENT_MSG to it.comment, KEY_DECK_COMMENT_CREATE_AT to it.date.toString()) @@ -107,14 +107,81 @@ abstract class FirebaseParsers { return mapOf(KEY_DECK_NAME to name, KEY_DECK_TYPE to type, KEY_DECK_CLASS to cls, KEY_DECK_PATCH to patch) } + override fun toString(): String { + return "DeckParser(name='$name', type=$type, cls=$cls, cost=$cost, owner='$owner', createdAt='$createdAt', updatedAt='$updatedAt', patch='$patch', views=$views, likes=$likes, cards=$cards, updates=$updates, comments=$comments)" + } + } + class DeckFavoriteParser( + + val name: String = "", + val cls: Int = 0 + + ) + class PatchParser { val desc: String = "" - fun toPatch(uidDate: String): Patch { - return Patch(uidDate, desc) + fun toPatch(uuidDate: String): Patch { + return Patch(uuidDate, desc) + } + + } + + class MatchParser( + + val first: Boolean = false, + val player: Map = mapOf(), + val opponent: Map = mapOf(), + val legend: Boolean = false, + val mode: Int = 0, + val rank: Int = 0, + val season: String = "", + val win: Boolean = false + + ) { + + companion object { + + private const val KEY_MATCH_DECK_CLASS = "cls" + private const val KEY_MATCH_DECK_DECK_UUID = "deck" + private const val KEY_MATCH_DECK_NAME = "name" + private const val KEY_MATCH_DECK_TYPE = "type" + private const val KEY_MATCH_DECK_VERSION = "version" + + } + + fun toMatch(uuid: String): Match { + val playerDeck = MatchDeck(player[KEY_MATCH_DECK_NAME].toString(), + Class.values()[player[KEY_MATCH_DECK_CLASS].toString().toInt()], + DeckType.values()[player[KEY_MATCH_DECK_TYPE].toString().toInt()], + player[KEY_MATCH_DECK_DECK_UUID].toString(), + player[KEY_MATCH_DECK_VERSION].toString()) + val opponentDeck = MatchDeck(opponent[KEY_MATCH_DECK_NAME].toString(), + Class.values()[opponent[KEY_MATCH_DECK_CLASS].toString().toInt()], + DeckType.values()[opponent[KEY_MATCH_DECK_TYPE].toString().toInt()]) + val matchMode = MatchMode.values()[mode] + return Match(uuid, first, playerDeck, opponentDeck, matchMode, season, rank, legend, win) + } + + fun fromMatch(match: Match): MatchParser { + val player = with(match.player) { + mapOf(KEY_MATCH_DECK_NAME to name, KEY_MATCH_DECK_CLASS to cls.ordinal, + KEY_MATCH_DECK_TYPE to type.ordinal, KEY_MATCH_DECK_DECK_UUID to (deck ?: ""), + KEY_MATCH_DECK_VERSION to (version ?: "")) + } + val opponent = with(match.opponent) { + mapOf(KEY_MATCH_DECK_NAME to name, KEY_MATCH_DECK_CLASS to cls.ordinal, + KEY_MATCH_DECK_TYPE to type.ordinal) + } + return MatchParser(match.first, player, opponent, match.legend, match.mode.ordinal, + match.rank, match.season, match.win) + } + + override fun toString(): String { + return "MatchParser(first=$first, player=$player, opponent=$opponent, legend=$legend, mode=$mode, rank=$rank, win=$win)" } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PrivateInteractor.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PrivateInteractor.kt index a1574db..2b8dae2 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PrivateInteractor.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PrivateInteractor.kt @@ -20,6 +20,7 @@ class PrivateInteractor : BaseInteractor() { private val NODE_DECKS_PRIVATE = "private" private val NODE_FAVORITE = "favorite" + private val NODE_MATCHES = "matches" private val KEY_CARD_FAVORITE = "favorite" private val KEY_CARD_QTD = "qtd" @@ -30,6 +31,8 @@ class PrivateInteractor : BaseInteractor() { private val KEY_DECK_UPDATES = "updates" private val KEY_DECK_COMMENTS = "comments" + private val KEY_MATCH_SEASON = "season" + private fun getUserID(): String { return FirebaseAuth.getInstance().currentUser?.uid ?: "" } @@ -61,15 +64,35 @@ class PrivateInteractor : BaseInteractor() { } } - fun setUserCardFavorite(card: Card, favorite: Boolean, onComplete: () -> Unit) { + fun setUserCardFavorite(card: Card, favorite: Boolean, onSuccess: () -> Unit) { dbUserCards(card.set, card.attr)?.apply { - child(card.shortName).child(KEY_CARD_FAVORITE).apply { - if (favorite) { - setValue(true).addOnCompleteListener { onComplete.invoke() } - } else { - removeValue().addOnCompleteListener { onComplete.invoke() } + child(card.shortName).apply { + execSetUserCardFavorite(this, favorite, onSuccess) + } + } + } + + private fun execSetUserCardFavorite(dr: DatabaseReference, favorite: Boolean, onSuccess: () -> Unit) { + val childEventListener = object : SimpleChildEventListener() { + override fun onChildAdded(snapshot: DataSnapshot?, previousChildName: String?) { + Timber.d(snapshot.toString()) + if (snapshot?.key == KEY_CARD_FAVORITE) { + dr.removeEventListener(this) + onSuccess.invoke() } } + + override fun onChildRemoved(snapshot: DataSnapshot?) { + dr.removeEventListener(this) + Timber.d(snapshot.toString()) + onSuccess.invoke() + } + } + dr.addChildEventListener(childEventListener) + if (favorite) { + dr.child(KEY_CARD_FAVORITE).setValue(true).addOnFailureListener { dr.removeEventListener(childEventListener) } + } else { + dr.child(KEY_CARD_FAVORITE).removeValue().addOnFailureListener { dr.removeEventListener(childEventListener) } } } @@ -80,7 +103,7 @@ class PrivateInteractor : BaseInteractor() { @Suppress("UNCHECKED_CAST") override fun onDataChange(ds: DataSnapshot) { val collection = ds.children.flatMap { it.children } - .filter { it.hasChild(KEY_CARD_QTD) } + .filter { it.hasChild(KEY_CARD_QTD) && (it.child(KEY_CARD_QTD).value as Long) > 0 } .map({ it.key to (it.child(KEY_CARD_QTD).value as Long).toInt() }) .toMap() Timber.d(collection.toString()) @@ -122,7 +145,7 @@ class PrivateInteractor : BaseInteractor() { @Suppress("UNCHECKED_CAST") override fun onDataChange(ds: DataSnapshot) { val collection = ds.children - .filter { it.hasChild(KEY_CARD_QTD) } + .filter { it.hasChild(KEY_CARD_QTD) && (it.child(KEY_CARD_QTD).value as Long) > 0 } .map({ it.key to (it.child(KEY_CARD_QTD).value as Long).toInt() }) .toMap() Timber.d(collection.toString()) @@ -137,7 +160,7 @@ class PrivateInteractor : BaseInteractor() { } } - fun getFavoriteCards(set: CardSet?, attr: Attribute, onSuccess: (List) -> Unit) { + fun getUserFavoriteCards(set: CardSet?, attr: Attribute, onSuccess: (List) -> Unit) { getListFromSets(set, attr, onSuccess) { set, attr, onEachSuccess -> dbUserCards(set, attr)?.addListenerForSingleValueEvent(object : ValueEventListener { @@ -157,70 +180,75 @@ class PrivateInteractor : BaseInteractor() { } } - fun getOwnedDecks(cls: Class?, onSuccess: (List) -> Unit) { - getOwnedPublicDecks(cls) { - val decks = it - getOwnedPrivateDecks(cls) { + fun getUserPublicDecksRef() = dbDecks.child(NODE_DECKS_PUBLIC) + .orderByChild(KEY_DECK_OWNER).equalTo(getUserID())?.apply { + keepSynced() + } + + fun getUserPrivateDecksRef() = dbUser()?.child(NODE_DECKS)?.child(NODE_DECKS_PRIVATE) + ?.orderByChild(KEY_DECK_UPDATE_AT)?.apply { + keepSynced() + } + + fun getUserFavoriteDecksRef() = dbUser()?.child(NODE_DECKS)?.child(NODE_FAVORITE)?.apply { + keepSynced() + } + + fun getUserDecks(cls: Class?, onSuccess: (List) -> Unit) { + getUserPublicDecks(cls) { decks -> + getUserPrivateDecks(cls) { onSuccess.invoke(decks.plus(it)) } } } - private fun getOwnedPublicDecks(cls: Class?, onSuccess: (List) -> Unit) { - with(dbDecks.child(NODE_DECKS_PUBLIC).orderByChild(KEY_DECK_OWNER).equalTo(getUserID())) { - keepSynced() - addListenerForSingleValueEvent(object : ValueEventListener { + private fun getUserPublicDecks(cls: Class?, onSuccess: (List) -> Unit) { + getUserPublicDecksRef()?.addListenerForSingleValueEvent(object : ValueEventListener { - override fun onDataChange(ds: DataSnapshot) { - Timber.d(ds.value?.toString()) - val decks = ds.children.mapTo(arrayListOf()) { - it.getValue(FirebaseParsers.DeckParser::class.java).toDeck(it.key, false) - }.filter { cls == null || it.cls == cls } - Timber.d(decks.toString()) - onSuccess.invoke(decks) - } + override fun onDataChange(ds: DataSnapshot) { + Timber.d(ds.value?.toString()) + val decks = ds.children.mapTo(arrayListOf()) { + it.getValue(FirebaseParsers.DeckParser::class.java).toDeck(it.key, false) + }.filter { cls == null || it.cls == cls } + Timber.d(decks.toString()) + onSuccess.invoke(decks) + } - override fun onCancelled(de: DatabaseError) { - Timber.d("Fail: " + de.message) - } + override fun onCancelled(de: DatabaseError) { + Timber.d("Fail: " + de.message) + } - }) - } + }) } - private fun getOwnedPrivateDecks(cls: Class?, onSuccess: (List) -> Unit) { - dbUser()?.child(NODE_DECKS)?.child(NODE_DECKS_PRIVATE)?.orderByChild(KEY_DECK_UPDATE_AT)?.apply { - keepSynced() - addListenerForSingleValueEvent(object : ValueEventListener { + private fun getUserPrivateDecks(cls: Class?, onSuccess: (List) -> Unit) { + getUserPrivateDecksRef()?.addListenerForSingleValueEvent(object : ValueEventListener { - override fun onDataChange(ds: DataSnapshot) { - val decks = ds.children.mapTo(arrayListOf()) { - it.getValue(FirebaseParsers.DeckParser::class.java).toDeck(it.key, true) - }.filter { cls == null || it.cls == cls } - Timber.d(decks.toString()) - onSuccess.invoke(decks) - } + override fun onDataChange(ds: DataSnapshot) { + val decks = ds.children.mapTo(arrayListOf()) { + it.getValue(FirebaseParsers.DeckParser::class.java).toDeck(it.key, true) + }.filter { cls == null || it.cls == cls } + Timber.d(decks.toString()) + onSuccess.invoke(decks) + } - override fun onCancelled(de: DatabaseError) { - Timber.d("Fail: " + de.message) - } + override fun onCancelled(de: DatabaseError) { + Timber.d("Fail: " + de.message) + } - }) - } + }) } - fun getFavoriteDecks(cls: Class?, onSuccess: (List?) -> Unit) { - PublicInteractor().getPublicDecks(cls) { - val publicDecks = it - dbUser()?.child(NODE_DECKS)?.child(NODE_FAVORITE)?.apply { - keepSynced() + fun getUserFavoriteDecks(cls: Class?, onSuccess: (List?) -> Unit) { + PublicInteractor().getPublicDecks(cls) { publicDecks -> + getUserFavoriteDecksRef()?.apply { addListenerForSingleValueEvent(object : ValueEventListener { override fun onDataChange(ds: DataSnapshot) { Timber.d(ds.value?.toString()) val decks = ds.children.map { val deckId = it.key - publicDecks.find { it.id == deckId } ?: Deck() + publicDecks.find { it.uuid == deckId } ?: Deck() }.filter { it.cost > 0 }.filter { cls == null || it.cls == cls } Timber.d(decks.toString()) onSuccess.invoke(decks) @@ -235,7 +263,62 @@ class PrivateInteractor : BaseInteractor() { } } - fun getMissingCards(deck: Deck, onError: ((e: Exception?) -> Unit)? = null, onSuccess: (List) -> Unit) { + fun setUserDeckFavorite(deck: Deck, favorite: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + dbUser()?.child(NODE_DECKS)?.child(NODE_FAVORITE)?.apply { + val childEventListener = object : SimpleChildEventListener() { + override fun onChildAdded(snapshot: DataSnapshot?, previousChildName: String?) { + Timber.d(snapshot.toString()) + if (snapshot?.key == deck.uuid) { + removeEventListener(this) + onSuccess.invoke() + } + } + + override fun onChildRemoved(snapshot: DataSnapshot?) { + removeEventListener(this) + Timber.d(snapshot.toString()) + onSuccess.invoke() + } + } + addChildEventListener(childEventListener) + if (favorite) { + val deckFavorite = FirebaseParsers.DeckFavoriteParser(deck.name, deck.cls.ordinal) + child(deck.uuid)?.setValue(deckFavorite)?.addOnFailureListener { + removeEventListener(childEventListener) + onError?.invoke(it) + } + } else { + child(deck.uuid)?.removeValue()?.addOnFailureListener { + removeEventListener(childEventListener) + onError?.invoke(it) + } + } + } + } + + fun setUserDeckLike(deck: Deck, like: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + dbUser()?.apply { + with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { + val deckLikesUpdated = if (like) deck.likes.plus(getUserID()) else deck.likes.minus(getUserID()) + val childEventListener = object : SimpleChildEventListener() { + override fun onChildChanged(snapshot: DataSnapshot?, previousChildName: String?) { + Timber.d(snapshot.toString()) + if (snapshot?.key == deck.uuid) { + removeEventListener(this) + onSuccess.invoke() + } + } + } + addChildEventListener(childEventListener) + child(deck.uuid).updateChildren(mapOf(KEY_DECK_LIKES to deckLikesUpdated)).addOnFailureListener({ + removeEventListener(childEventListener) + onError?.invoke(it) + }) + } + } + } + + fun getDeckMissingCards(deck: Deck, onError: ((e: Exception?) -> Unit)? = null, onSuccess: (List) -> Unit) { val publicInteractor = PublicInteractor() val attr1 = deck.cls.attr1 val attr2 = deck.cls.attr2 @@ -257,31 +340,27 @@ class PrivateInteractor : BaseInteractor() { private: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: (uid: String) -> Unit) { dbUser()?.apply { with(if (private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { - val deck = Deck(push().key, name, getUserID(), private, type, cls, cost, LocalDateTime.now(), - LocalDateTime.now(), patch, ArrayList(), 0, cards, ArrayList(), ArrayList()) - child(deck.id).setValue(FirebaseParsers.DeckParser().fromDeck(deck)).addOnCompleteListener({ - Timber.d(it.toString()) - if (it.isSuccessful) onSuccess.invoke(deck.id) - else { - val errorMsg = it.exception?.message ?: it.exception.toString() - EventBus.getDefault().post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, errorMsg)) - onError?.invoke(it.exception) + val deck = Deck(push().key, name, getUserID(), private, type, cls, cost, LocalDateTime.now().withNano(0), + LocalDateTime.now().withNano(0), patch, ArrayList(), 0, cards, ArrayList(), ArrayList()) + val childEventListener = object : SimpleChildEventListener() { + override fun onChildAdded(snapshot: DataSnapshot?, previousChildName: String?) { + Timber.d(snapshot.toString()) + if (snapshot?.key == deck.uuid) { + removeEventListener(this) + onSuccess.invoke(deck.uuid) + } } + } + addChildEventListener(childEventListener) + child(deck.uuid).setValue(FirebaseParsers.DeckParser().fromDeck(deck)).addOnFailureListener({ + removeEventListener(childEventListener) + EventBus.getDefault().post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, it.message ?: "")) + onError?.invoke(it) }) } } } - fun setUserDeckFavorite(deck: Deck, favorite: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { - dbUser()?.child(NODE_DECKS)?.child(NODE_FAVORITE)?.apply { - if (favorite) { - child(deck.id)?.setValue(true)?.addOnCompleteListener { onSuccess.invoke() } - } else { - child(deck.id)?.removeValue()?.addOnCompleteListener { onSuccess.invoke() } - } - } - } - /** * Name, Type, Class, Patch, Private */ @@ -289,12 +368,12 @@ class PrivateInteractor : BaseInteractor() { dbUser()?.apply { with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { if (deck.private == oldPrivate) - child(deck.id).updateChildren(FirebaseParsers.DeckParser().fromDeck(deck).toDeckUpdateMap()).addOnCompleteListener({ + child(deck.uuid).updateChildren(FirebaseParsers.DeckParser().fromDeck(deck).toDeckUpdateMap()).addOnCompleteListener({ Timber.d(it.toString()) if (it.isSuccessful) onSuccess.invoke() else onError?.invoke(it.exception) }) else - child(deck.id).setValue(FirebaseParsers.DeckParser().fromDeck(deck)).addOnCompleteListener({ + child(deck.uuid).setValue(FirebaseParsers.DeckParser().fromDeck(deck)).addOnCompleteListener({ Timber.d(it.toString()) deleteDeck(deck, oldPrivate, onError, onSuccess) }) @@ -306,67 +385,136 @@ class PrivateInteractor : BaseInteractor() { onError: ((e: Exception?) -> Unit)? = null) { dbUser()?.apply { with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { - val updateKey = org.threeten.bp.LocalDateTime.now().toString() + val updateKey = LocalDateTime.now().withNano(0).toString() val cardsRem = oldCards.filter { !deck.cards.keys.contains(it.key) }.mapValues { it.key to it.value * -1 } val cardsDiff = deck.cards.mapValues { it.key to it.value.minus(oldCards[it.key] ?: 0) }.plus(cardsRem) - child(deck.id).child(KEY_DECK_UPDATES).child(updateKey).setValue(cardsDiff).addOnCompleteListener({ + child(deck.uuid).child(KEY_DECK_UPDATES).child(updateKey).setValue(cardsDiff).addOnCompleteListener({ Timber.d(it.toString()) if (it.isSuccessful) onSuccess.invoke() else onError?.invoke(it.exception) }) - child(deck.id).child(KEY_DECK_COST).setValue(cost) + child(deck.uuid).child(KEY_DECK_COST).setValue(cost) } } } - fun deleteDeck(deck: Deck, private: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + fun addDeckComment(deck: Deck, msg: String, onError: ((e: Exception?) -> Unit)? = null, + onSuccess: (comment: DeckComment) -> Unit) { dbUser()?.apply { - with(if (private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { - child(deck.id).removeValue().addOnCompleteListener({ - Timber.d(it.toString()) - if (it.isSuccessful) onSuccess.invoke() else onError?.invoke(it.exception) - }) + with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { + val comment = FirebaseParsers.DeckParser.toNewCommentMap(getUserID(), msg) + with(child(deck.uuid).child(KEY_DECK_COMMENTS)) { + val commentKey = push().key + val childEventListener = object : SimpleChildEventListener() { + override fun onChildAdded(snapshot: DataSnapshot?, previousChildName: String?) { + Timber.d(snapshot.toString()) + if (snapshot?.key == commentKey) { + removeEventListener(this) + onSuccess.invoke(DeckComment(commentKey, getUserID(), msg, LocalDateTime.now().withNano(0))) + } + } + } + addChildEventListener(childEventListener) + child(commentKey).setValue(comment).addOnFailureListener({ + removeEventListener(childEventListener) + onError?.invoke(it) + }) + } } } } - fun setUserDeckLike(deck: Deck, like: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + fun remDeckComment(deck: Deck, commentId: String, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { dbUser()?.apply { with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { - val deckLikesUpdated = if (like) deck.likes.plus(getUserID()) else deck.likes.minus(getUserID()) - child(deck.id).updateChildren(mapOf(KEY_DECK_LIKES to deckLikesUpdated)).addOnCompleteListener({ - Timber.d(it.toString()) - if (it.isSuccessful) onSuccess.invoke() else onError?.invoke(it.exception) - }) + child(deck.uuid).child(KEY_DECK_COMMENTS).apply { + val childEventListener = object : SimpleChildEventListener() { + override fun onChildRemoved(snapshot: DataSnapshot?) { + removeEventListener(this) + Timber.d(snapshot.toString()) + onSuccess.invoke() + } + } + addChildEventListener(childEventListener) + child(commentId).removeValue().addOnFailureListener({ + removeEventListener(childEventListener) + onError?.invoke(it) + }) + } } } } - fun addDeckComment(deck: Deck, msg: String, onError: ((e: Exception?) -> Unit)? = null, - onSuccess: (comment: DeckComment) -> Unit) { + fun deleteDeck(deck: Deck, private: Boolean, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { dbUser()?.apply { - with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { - val comment = FirebaseParsers.DeckParser.toNewCommentMap(getUserID(), msg) - with(child(deck.id).child(KEY_DECK_COMMENTS)) { - val commentKey = push().key - child(commentKey).setValue(comment).addOnCompleteListener({ - Timber.d(it.toString()) - if (it.isSuccessful) { - onSuccess.invoke(DeckComment(commentKey, getUserID(), msg, LocalDateTime.now())) - } else - onError?.invoke(it.exception) - }) + with(if (private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { + val childEventListener = object : SimpleChildEventListener() { + override fun onChildRemoved(snapshot: DataSnapshot?) { + removeEventListener(this) + Timber.d(snapshot.toString()) + onSuccess.invoke() + } } + addChildEventListener(childEventListener) + child(deck.uuid).removeValue().addOnFailureListener({ + removeEventListener(childEventListener) + onError?.invoke(it) + }) } } } - fun remDeckComment(deck: Deck, commentId: String, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { - dbUser()?.apply { - with(if (deck.private) child(NODE_DECKS).child(NODE_DECKS_PRIVATE) else dbDecks.child(NODE_DECKS_PUBLIC)) { - child(deck.id).child(KEY_DECK_COMMENTS).child(commentId).removeValue().addOnCompleteListener({ + fun getUserMatchesRef() = dbUser()?.child(NODE_MATCHES)?.apply { + keepSynced() + } + + fun getUserMatches(season: Season?, onSuccess: (List) -> Unit) { + getUserMatchesRef()?.apply { + val query = orderByChild(KEY_MATCH_SEASON).equalTo(season?.uuid) + (if (season != null) query else this).addListenerForSingleValueEvent(object : ValueEventListener { + + override fun onDataChange(ds: DataSnapshot) { + val matches = ds.children.mapTo(arrayListOf()) { + it.getValue(FirebaseParsers.MatchParser::class.java).toMatch(it.key) + } + Timber.d(matches.toString()) + onSuccess.invoke(matches) + } + + override fun onCancelled(de: DatabaseError) { + Timber.d("Fail: " + de.message) + } + + }) + } + } + + fun saveMatch(newMatch: Match, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + getUserMatchesRef()?.apply { + val childEventListener = object : SimpleChildEventListener() { + override fun onChildAdded(snapshot: DataSnapshot?, previousChildName: String?) { + Timber.d(snapshot.toString()) + if (snapshot?.key == newMatch.uuid) { + removeEventListener(this) + onSuccess.invoke() + } + } + } + addChildEventListener(childEventListener) + child(newMatch.uuid).setValue(FirebaseParsers.MatchParser().fromMatch(newMatch)).addOnFailureListener { + removeEventListener(childEventListener) + onError?.invoke(it) + } + } + } + + fun deleteMatch(match: Match, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + getUserMatchesRef()?.apply { + child(match.uuid).removeValue().addOnCompleteListener { + if (it.isSuccessful) { Timber.d(it.toString()) - if (it.isSuccessful) onSuccess.invoke() else onError?.invoke(it.exception) - }) + onSuccess.invoke() + } else + onError?.invoke(it.exception) } } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PublicInteractor.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PublicInteractor.kt index 09f1de3..c4c8360 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PublicInteractor.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/interactor/PublicInteractor.kt @@ -4,7 +4,10 @@ import com.ediposouza.teslesgendstracker.data.* import com.google.firebase.database.DataSnapshot import com.google.firebase.database.DatabaseError import com.google.firebase.database.ValueEventListener +import org.threeten.bp.Month +import org.threeten.bp.format.TextStyle import timber.log.Timber +import java.util.* /** * Created by ediposouza on 01/11/16. @@ -14,6 +17,29 @@ class PublicInteractor : BaseInteractor() { private val KEY_CARD_EVOLVES = "evolves" private val KEY_DECK_VIEWS = "views" + fun getCards(set: CardSet?, onSuccess: (List) -> Unit) { + getListFromSets(set, onSuccess) { set, onEachSuccess -> + database.child(NODE_CARDS).child(set.db).orderByChild(KEY_CARD_COST) + .addListenerForSingleValueEvent(object : ValueEventListener { + + override fun onDataChange(ds: DataSnapshot) { + val cards = ds.children.map { + val attr = Attribute.valueOf(it.key.toUpperCase()) + it.children.map { + it.getValue(FirebaseParsers.CardParser::class.java).toCard(it.key, set, attr) + } + }.flatMap { it } + onEachSuccess.invoke(cards) + } + + override fun onCancelled(de: DatabaseError) { + Timber.d("Fail: " + de.message) + } + + }) + } + } + fun getCards(set: CardSet?, vararg attrs: Attribute, onSuccess: (List) -> Unit) { var attrIndex = 0 val cards = arrayListOf() @@ -104,6 +130,33 @@ class PublicInteractor : BaseInteractor() { } } + fun getPublicDeck(uuid: String, onSuccess: (Deck) -> Unit) { + with(dbDecks.child(NODE_DECKS_PUBLIC).child(uuid)) { + keepSynced() + addListenerForSingleValueEvent(object : ValueEventListener { + + override fun onDataChange(ds: DataSnapshot) { + val value = ds.getValue(FirebaseParsers.DeckParser::class.java) + val deck = value?.toDeck(ds.key, false) + if (deck != null) { + Timber.d(deck.toString()) + onSuccess.invoke(deck) + } + } + + override fun onCancelled(de: DatabaseError) { + Timber.d("Fail: " + de.message) + } + + }) + } + } + + fun getPublicDecksRef() = dbDecks.child(NODE_DECKS_PUBLIC) + .orderByChild(KEY_DECK_UPDATE_AT)?.apply { + keepSynced() + } + fun getPublicDecks(cls: Class?, onSuccess: (List) -> Unit) { val dbPublicDeck = dbDecks.child(NODE_DECKS_PUBLIC) dbPublicDeck.keepSynced() @@ -129,11 +182,12 @@ class PublicInteractor : BaseInteractor() { }) } - fun incDeckView(deck: Deck, onError: ((e: Exception?) -> Unit)? = null, onSuccess: () -> Unit) { + fun incDeckView(deck: Deck, onError: ((e: Exception?) -> Unit)? = null, onSuccess: (Int) -> Unit) { with(dbDecks.child(NODE_DECKS_PUBLIC)) { - child(deck.id).updateChildren(mapOf(KEY_DECK_VIEWS to deck.views.inc())).addOnCompleteListener({ + val views = deck.views.inc() + child(deck.uuid).updateChildren(mapOf(KEY_DECK_VIEWS to views)).addOnCompleteListener({ Timber.d(it.toString()) - if (it.isSuccessful) onSuccess.invoke() else onError?.invoke(it.exception) + if (it.isSuccessful) onSuccess.invoke(views) else onError?.invoke(it.exception) }) } } @@ -149,15 +203,15 @@ class PublicInteractor : BaseInteractor() { } } - fun getUserInfo(uuid: String, onError: ((e: Exception?) -> Unit)? = null, - onSuccess: (UserInfo) -> Unit) { - dbUsers.child(uuid)?.child(NODE_USERS_INFO)?.addListenerForSingleValueEvent(object : ValueEventListener { + fun getPatches(onError: ((e: Exception?) -> Unit)? = null, onSuccess: (List) -> Unit) { + database.child(NODE_PATCHES).addListenerForSingleValueEvent(object : ValueEventListener { override fun onDataChange(ds: DataSnapshot) { - val userInfo = UserInfo(ds.child(KEY_USER_NAME)?.value?.toString() ?: "", - ds.child(KEY_USER_PHOTO)?.value?.toString() ?: "") - Timber.d(userInfo.toString()) - onSuccess.invoke(userInfo) + val patches = ds.children.mapTo(arrayListOf()) { + it.getValue(FirebaseParsers.PatchParser::class.java).toPatch(it.key) + } + Timber.d(patches.toString()) + onSuccess.invoke(patches) } override fun onCancelled(de: DatabaseError) { @@ -168,16 +222,38 @@ class PublicInteractor : BaseInteractor() { }) } - fun getPatches(onError: ((e: Exception?) -> Unit)? = null, - onSuccess: (List) -> Unit) { - database.child(NODE_PATCHES).addListenerForSingleValueEvent(object : ValueEventListener { + fun getSeasons(onError: ((e: Exception?) -> Unit)? = null, onSuccess: (List) -> Unit) { + database.child(NODE_SEASONS).addListenerForSingleValueEvent(object : ValueEventListener { override fun onDataChange(ds: DataSnapshot) { - val patches = ds.children.mapTo(arrayListOf()) { - it.getValue(FirebaseParsers.PatchParser::class.java).toPatch(it.key) + val seasons = ds.children.mapTo(arrayListOf()) { + val id = it.key.replace("_", "").toInt() + val date = it.key.split("_") + val month = Month.of(date[1].toInt()) + val desc = "${month.getDisplayName(TextStyle.FULL, Locale.getDefault())}/${date[0].toInt()}" + Season(id, it.key, desc, it.value.toString()) } - Timber.d(patches.toString()) - onSuccess.invoke(patches) + Timber.d(seasons.toString()) + onSuccess.invoke(seasons) + } + + override fun onCancelled(de: DatabaseError) { + Timber.d("Fail: " + de.message) + onError?.invoke(de.toException()) + } + + }) + } + + fun getUserInfo(uuid: String, onError: ((e: Exception?) -> Unit)? = null, + onSuccess: (UserInfo) -> Unit) { + dbUsers.child(uuid)?.child(NODE_USERS_INFO)?.addListenerForSingleValueEvent(object : ValueEventListener { + + override fun onDataChange(ds: DataSnapshot) { + val userInfo = UserInfo(ds.child(KEY_USER_NAME)?.value?.toString() ?: "", + ds.child(KEY_USER_PHOTO)?.value?.toString() ?: "") + Timber.d(userInfo.toString()) + onSuccess.invoke(userInfo) } override fun onCancelled(de: DatabaseError) { diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DashActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DashActivity.kt index 86358df..23561cb 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DashActivity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DashActivity.kt @@ -1,53 +1,98 @@ package com.ediposouza.teslesgendstracker.ui +import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.preference.PreferenceManager import android.support.design.widget.BottomSheetBehavior import android.support.design.widget.NavigationView import android.support.v4.app.Fragment +import android.support.v4.content.ContextCompat import android.support.v7.app.ActionBarDrawerToggle +import android.support.v7.app.AlertDialog import android.view.Gravity import android.view.MenuItem import android.view.View +import android.webkit.CookieManager +import android.webkit.CookieSyncManager import com.bumptech.glide.Glide -import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.* import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor import com.ediposouza.teslesgendstracker.interactor.PublicInteractor import com.ediposouza.teslesgendstracker.ui.base.BaseFilterActivity -import com.ediposouza.teslesgendstracker.ui.base.CmdShowTabs +import com.ediposouza.teslesgendstracker.ui.base.CmdLoginSuccess +import com.ediposouza.teslesgendstracker.ui.base.CmdShowSnackbarMsg +import com.ediposouza.teslesgendstracker.ui.base.CmdUpdateTitle import com.ediposouza.teslesgendstracker.ui.cards.CardsFragment -import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterMagika -import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterRarity import com.ediposouza.teslesgendstracker.ui.decks.DecksFragment +import com.ediposouza.teslesgendstracker.ui.matches.MatchesFragment import com.ediposouza.teslesgendstracker.ui.util.CircleTransform -import com.ediposouza.teslesgendstracker.ui.widget.CollectionStatistics +import com.ediposouza.teslesgendstracker.util.MetricAction +import com.ediposouza.teslesgendstracker.util.MetricScreen +import com.ediposouza.teslesgendstracker.util.MetricsManager +import com.ediposouza.teslesgendstracker.util.alertThemed import com.google.firebase.auth.FirebaseAuth +import com.google.inapp.util.IabHelper import kotlinx.android.synthetic.main.activity_dash.* +import kotlinx.android.synthetic.main.dialog_about.view.* import kotlinx.android.synthetic.main.navigation_drawer_header.view.* import org.greenrobot.eventbus.Subscribe import org.jetbrains.anko.doAsync import org.jetbrains.anko.itemsSequence +import org.jetbrains.anko.toast import timber.log.Timber + class DashActivity : BaseFilterActivity(), NavigationView.OnNavigationItemSelectedListener { - private val KEY_MENU_ITEM_SELECTED = "menu_index_key" + private val KEY_MENU_ITEM_SELECTED = "menuIndexKey" + private val SKU_TEST = "android.test.purchased" + private val SKU_DONATE_BASIC = "donate_basic" + private val SKU_DONATE_PRO = "donate_pro" + private val RC_DONATE = 221 private var menuItemSelected = 0 private val publicInteractor = PublicInteractor() private val privateInteractor = PrivateInteractor() - private val statisticsSheetBehavior: BottomSheetBehavior by lazy { - BottomSheetBehavior.from(cards_collection_statistics) - } + private var iabHelper: IabHelper? = null + private var iabHelperStarted: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_dash) snackbarNeedMargin = false - decks_fab_add.hide() - filter_rarity.filterClick = { eventBus.post(CmdFilterRarity(it)) } - filter_magika.filterClick = { eventBus.post(CmdFilterMagika(it)) } + with(dash_navigation_view.getHeaderView(0)) { + profile_change_user.setOnClickListener { + showLogin() + } + profile_image.setOnClickListener { + if (!App.hasUserLogged()) { + showLogin() + } + } + } + iabHelper = IabHelper(this, "$PPKA$PPKB$PPKC$PPKD").apply { + enableDebugLogging(BuildConfig.DEBUG) + startSetup { + if (it.isSuccess) { + iabHelperStarted = true + queryInventoryAsync { iabResult, inventory -> + if (inventory != null) { + if (inventory.hasPurchase(SKU_DONATE_BASIC) || inventory.hasPurchase(SKU_DONATE_PRO)) { + handleDonation() + } else { + Timber.d("No donation yet") + } + } + } + } else { + Timber.e("Iab start setup error: ${it.message}") + } + } + } } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -57,23 +102,12 @@ class DashActivity : BaseFilterActivity(), override fun onDrawerOpened(drawerView: View) { super.onDrawerOpened(drawerView) - val user = FirebaseAuth.getInstance().currentUser - dash_navigation_view.menu.findItem(R.id.menu_matches)?.isVisible = user != null - with(dash_navigation_view.getHeaderView(0)) { - profile_name.text = user?.displayName ?: getString(R.string.unknown) - if (user != null) { - Glide.with(this@DashActivity) - .load(user.photoUrl) - .transform(CircleTransform(this@DashActivity)) - .into(profile_image) - updateCollectionStatistics() - } - } - with(statisticsSheetBehavior) { + getStatisticsBottomSheetBehavior()?.apply { if (state == BottomSheetBehavior.STATE_EXPANDED) { state = BottomSheetBehavior.STATE_COLLAPSED } } + updateCollectionStatistics() } } @@ -102,10 +136,41 @@ class DashActivity : BaseFilterActivity(), } } + override fun onResume() { + super.onResume() + updateUserMenuInfo() + } + + override fun onDestroy() { + try { + iabHelper?.disposeWhenFinished() + } catch (e: Exception) { + Timber.d(e) + } + super.onDestroy() + } + + private fun updateUserMenuInfo() { + val user = FirebaseAuth.getInstance().currentUser + dash_navigation_view.menu.findItem(R.id.menu_matches)?.isVisible = App.hasUserLogged() + with(dash_navigation_view.getHeaderView(0)) { + profile_change_user.visibility = if (App.hasUserLogged()) View.VISIBLE else View.GONE + profile_name.text = user?.displayName ?: getString(R.string.unknown) + if (user != null) { + Glide.with(this@DashActivity) + .load(user.photoUrl) + .transform(CircleTransform(this@DashActivity)) + .into(profile_image) + } + } + } + override fun onBackPressed() { - if (statisticsSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { - statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - return + getStatisticsBottomSheetBehavior()?.apply { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + state = BottomSheetBehavior.STATE_COLLAPSED + return + } } if (dash_drawer_layout.isDrawerOpen(Gravity.START)) { dash_drawer_layout.closeDrawer(Gravity.START) @@ -118,6 +183,13 @@ class DashActivity : BaseFilterActivity(), } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (iabHelper?.handleActivityResult(requestCode, resultCode, data) ?: false) { + return + } + super.onActivityResult(requestCode, resultCode, data) + } + override fun onNavigationItemSelected(item: MenuItem): Boolean { menuItemSelected = dash_navigation_view.menu.itemsSequence().indexOf(item) dash_drawer_layout.closeDrawer(Gravity.START) @@ -127,26 +199,123 @@ class DashActivity : BaseFilterActivity(), return when (item.itemId) { R.id.menu_cards -> supportFragmentManager.popBackStackImmediate() R.id.menu_decks -> showFragment(DecksFragment()) - R.id.menu_matches, + R.id.menu_matches -> showFragment(MatchesFragment()) R.id.menu_arena, - R.id.menu_season, - R.id.menu_about -> { + R.id.menu_season -> { true } + R.id.menu_donate -> showDonateDialog() + R.id.menu_about -> showAboutDialog() else -> false } } + private fun showAboutDialog(): Boolean { + val dialogView = View.inflate(this, R.layout.dialog_about, null).apply { + about_dialog_version.text = packageManager.getPackageInfo(packageName, 0).versionName + about_dialog_thanks_cvh_text.setOnClickListener { + val linkUri = Uri.parse(getString(R.string.about_info_thanks_cvh_link)) + startActivity(Intent(Intent.ACTION_VIEW).setData(linkUri)) + MetricsManager.trackAction(MetricAction.ACTION_ABOUT_CVH()) + } + about_dialog_thanks_direwolf_text.setOnClickListener { + val linkUri = Uri.parse(getString(R.string.about_info_thanks_direwolf_link)) + startActivity(Intent(Intent.ACTION_VIEW).setData(linkUri)) + MetricsManager.trackAction(MetricAction.ACTION_ABOUT_DIREWOLF()) + } + } + AlertDialog.Builder(this, R.style.AppDialog) + .setView(dialogView) + .setPositiveButton(R.string.about_rate_app, { di, which -> + startActivity(Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(getString(R.string.playstore_url_format, packageName)))) + MetricsManager.trackAction(MetricAction.ACTION_ABOUT_RATE()) + }) + .show() + MetricsManager.trackScreen(MetricScreen.SCREEN_ABOUT()) + return true + } + + private fun showDonateDialog(): Boolean { + if (iabHelperStarted) { + iabHelper?.queryInventoryAsync(true, listOf(SKU_DONATE_BASIC, SKU_DONATE_PRO), listOf()) { iabResult, inventory -> + if (inventory != null) { + val skuBasic = inventory.getSkuDetails(SKU_DONATE_BASIC) + val skuPro = inventory.getSkuDetails(SKU_DONATE_PRO) + showDonateDialog(skuBasic?.price ?: "", skuPro?.price ?: "") + } else { + eventBus.post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, R.string.app_donate_dialog_payment_error)) + } + } + } else { + eventBus.post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, R.string.app_donate_dialog_payment_error)) + } + return true + } + + private fun showDonateDialog(basicValue: String, proValue: String) { + alertThemed(R.string.app_donate_dialog_text, R.string.menu_donate, R.style.AppDialog) { + positiveButton(getString(R.string.app_donate_dialog_value, proValue), { + processDonate(SKU_DONATE_BASIC) + MetricsManager.trackAction(MetricAction.ACTION_DONATE_BASIC()) + }) + negativeButton(getString(R.string.app_donate_dialog_value, basicValue), { + processDonate(if (BuildConfig.DEBUG) SKU_TEST else SKU_DONATE_PRO) + MetricsManager.trackAction(MetricAction.ACTION_DONATE_PRO()) + }) + neutralButton(R.string.app_donate_dialog_not_now, { + MetricsManager.trackAction(MetricAction.ACTION_DONATE_NOT_NOW()) + }) + }.show() + MetricsManager.trackScreen(MetricScreen.SCREEN_DONATE()) + } + + private fun processDonate(skuItem: String) { + iabHelper?.launchPurchaseFlow(this@DashActivity, skuItem, RC_DONATE) { result, info -> + if (result.isFailure) { + toast(R.string.app_donate_dialog_payment_fail) + return@launchPurchaseFlow + } + if (info.sku == skuItem) { + handleDonation() + toast(R.string.app_donate_dialog_payment_success) + } + } + } + + private fun handleDonation() { + dash_drawer_layout.closeDrawer(Gravity.START) + dash_navigation_view.menu.findItem(R.id.menu_donate)?.apply { + isEnabled = false + title = getString(R.string.menu_donate_done) + icon = ContextCompat.getDrawable(this@DashActivity, R.drawable.ic_no_ads) + } + PreferenceManager.getDefaultSharedPreferences(this@DashActivity).edit() + .putBoolean(PREF_USER_DONATE, true) + .apply() + Timber.d("Donated!") + } + private fun showFragment(frag: Fragment): Boolean { - statisticsSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN - supportFragmentManager.beginTransaction() - .replace(R.id.dash_content, frag) - .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) - .addToBackStack(null) - .commit() + getStatisticsBottomSheetBehavior()?.state = BottomSheetBehavior.STATE_HIDDEN + if (supportFragmentManager.backStackEntryCount > 0) { + supportFragmentManager.popBackStackImmediate() + } + supportFragmentManager.beginTransaction().apply { + replace(R.id.dash_content, frag) + setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out) + addToBackStack(null) + }.commit() return true } + private fun getStatisticsBottomSheetBehavior(): BottomSheetBehavior<*>? { + findViewById(R.id.cards_collection_statistics)?.apply { + return BottomSheetBehavior.from(this) + } + return null + } + private fun updateCollectionStatistics() { var allCardsTotal = 0 var userCardsTotal = 0 @@ -163,7 +332,7 @@ class DashActivity : BaseFilterActivity(), allAttrCards.map { it.shortName }.contains(it.key) }.values.sum() Timber.d("Out: ${it.filter { !allAttrCards.map { it.shortName }.contains(it.key) }}") - val stringPercent = getString(R.string.statistics_percent, + val stringPercent = getString(R.string.collection_statistics_percent, if (allCardsTotal > 0) userCardsTotal.toFloat() / allCardsTotal.toFloat() * 100f else 0f) @@ -180,8 +349,28 @@ class DashActivity : BaseFilterActivity(), } @Subscribe - fun onCmdShowTabs(cmdShowTabs: CmdShowTabs) { - dash_app_bar_layout.setExpanded(true, true) + fun onUpdateTitle(cmdUpdateTitle: CmdUpdateTitle) { + dash_toolbar_title.setText(cmdUpdateTitle.title) + } + + @Subscribe + @Suppress("UNUSED_PARAMETER", "DEPRECATION") + fun onLoginSuccess(cmdLoginSuccess: CmdLoginSuccess) { + updateUserMenuInfo() + updateCollectionStatistics() + dash_navigation_view.getHeaderView(0).profile_clear_cache_webview.clearCache(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().removeAllCookies(null) + CookieManager.getInstance().flush() + } else { + val cookieSyncMngr = CookieSyncManager.createInstance(this) + cookieSyncMngr.startSync() + val cookieManager = CookieManager.getInstance() + cookieManager.removeAllCookie() + cookieManager.removeSessionCookie() + cookieSyncMngr.stopSync() + cookieSyncMngr.sync() + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseActivity.kt index 659ab12..6f9023e 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseActivity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseActivity.kt @@ -21,6 +21,7 @@ import com.ediposouza.teslesgendstracker.interactor.PublicInteractor import com.ediposouza.teslesgendstracker.util.ConfigManager import com.ediposouza.teslesgendstracker.util.MetricAction import com.ediposouza.teslesgendstracker.util.MetricsManager +import com.ediposouza.teslesgendstracker.util.alertThemed import com.google.android.gms.auth.api.Auth import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions @@ -30,7 +31,6 @@ import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.GoogleAuthProvider import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe -import org.jetbrains.anko.alert import org.jetbrains.anko.contentView import org.jetbrains.anko.find import org.jetbrains.anko.toast @@ -93,14 +93,13 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile eventBus.register(this) ConfigManager.updateCaches { if (ConfigManager.isVersionUnsupported()) { - alert(getString(R.string.app_version_unsupported)) { + alertThemed(R.string.app_version_unsupported, theme = R.style.AppDialog) { okButton { MetricsManager.trackAction(MetricAction.ACTION_VERSION_UNSUPPORTED()) startActivity(Intent(Intent.ACTION_VIEW) .setData(Uri.parse(getString(R.string.playstore_url_format, packageName)))) System.exit(0) } - setTheme(R.style.AppDialog) }.show() } } @@ -118,8 +117,7 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile super.onDestroy() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { - super.onActivityResult(requestCode, resultCode, data) + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == RC_SIGN_IN) { val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data) if (result.isSuccess) { @@ -129,6 +127,7 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile hideLoading() } } + super.onActivityResult(requestCode, resultCode, data) } override fun onConnectionFailed(p0: ConnectionResult) { @@ -155,7 +154,25 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile protected fun showErrorUserNotLogged() { eventBus.post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, R.string.error_auth) - .withAction(R.string.action_login, { eventBus.post(CmdShowLogin()) })) + .withAction(R.string.action_login, { showLogin() })) + } + + protected fun showLogin() { + eventBus.post(CmdShowLogin()) + } + + private fun showLoading() { + val progressBar = ProgressBar(this) + progressBar.isIndeterminate = true + val largeMargin = resources.getDimensionPixelSize(R.dimen.large_margin) + progressBar.setPadding(0, largeMargin, 0, largeMargin) + loading = AlertDialog.Builder(this) + .setView(progressBar) + .show() + } + + private fun hideLoading() { + loading?.dismiss() } private fun firebaseAuthWithGoogle(acct: GoogleSignInAccount?) { @@ -174,7 +191,7 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile toast("SignIn with " + currentUser?.displayName) }) PrivateInteractor().setUserInfo() - EventBus.getDefault().post(CmdLoginSuccess()) + eventBus.post(CmdLoginSuccess()) } else { Timber.w("signInWithCredential", task.exception) toast(getString(R.string.error_login)) @@ -184,20 +201,6 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile } } - private fun showLoading() { - val progressBar = ProgressBar(this) - progressBar.isIndeterminate = true - val largeMargin = resources.getDimensionPixelSize(R.dimen.large_margin) - progressBar.setPadding(0, largeMargin, 0, largeMargin) - loading = AlertDialog.Builder(this) - .setView(progressBar) - .show() - } - - private fun hideLoading() { - loading?.dismiss() - } - @SuppressWarnings("ResourceType") @Subscribe fun onCmdShowSnackMsg(cmdShowSnackbarMsg: CmdShowSnackbarMsg) { @@ -219,7 +222,9 @@ open class BaseActivity : AppCompatActivity(), GoogleApiClient.OnConnectionFaile } @Subscribe + @Suppress("UNUSED_PARAMETER") fun onCmdShowLogin(showLogin: CmdShowLogin) { + googleApiClient?.clearDefaultAccountAndReconnect() val signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient) startActivityForResult(signInIntent, RC_SIGN_IN) } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsAdapter.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsAdapter.kt index f3faeb9..3e02ad8 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsAdapter.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsAdapter.kt @@ -23,8 +23,8 @@ abstract class BaseAdsAdapter(val adsEachItems: Int, @LayoutRes val adsLayout: I } - constructor(adsEachItems: Int, layoutManager: GridLayoutManager, - @LayoutRes adsLayout: Int) : this(adsEachItems, adsLayout) { + constructor(adsEachItems: Int, @LayoutRes adsLayout: Int, + layoutManager: GridLayoutManager) : this(adsEachItems, adsLayout) { onRestoreState(layoutManager) } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsFirebaseAdapter.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsFirebaseAdapter.kt new file mode 100644 index 0000000..943ae66 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseAdsFirebaseAdapter.kt @@ -0,0 +1,105 @@ +package com.ediposouza.teslesgendstracker.ui.base + +import android.support.annotation.LayoutRes +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.util.inflate +import com.ediposouza.teslesgendstracker.util.load +import com.google.android.gms.ads.AdView +import com.google.android.gms.ads.NativeExpressAdView +import com.google.firebase.database.Query + +/** + * Created by ediposouza on 08/12/16. + */ +abstract class BaseAdsFirebaseAdapter(model: Class, + ref: () -> Query?, + pageSize: Int, + val adsEachItems: Int, + @LayoutRes val adsLayout: Int, + orderASC: Boolean = false, + filter: ((T) -> Boolean)? = null) : + BaseFirebaseRVAdapter(model, ref, pageSize, orderASC, filter) { + + val VIEW_TYPE_ADS = 3 + + constructor(adsEachItems: Int, + layoutManager: GridLayoutManager, + @LayoutRes adsLayout: Int, + model: Class, + ref: () -> Query?, + pageSize: Int) : this(model, ref, pageSize, adsEachItems, adsLayout) { + onRestoreState(layoutManager) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + if (viewType == VIEW_TYPE_ADS) { + val adsItemView = parent.inflate(adsLayout) + val ads = adsItemView.findViewById(R.id.ads_view) + when (ads) { + is AdView -> ads.load() + is NativeExpressAdView -> ads.load() + } + return object : RecyclerView.ViewHolder(adsItemView) {} + } + return super.onCreateViewHolder(parent, viewType) + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + val adsQtdBeforePosition = getAdsQtdBeforePosition(position) + var itemKey: String? = null + var model: T? = null + val arrayPosition = position - snapShotOffset + if (arrayPosition < getContentCount() + adsQtdBeforePosition && arrayPosition >= 0) { + itemKey = getItemKey(position - adsQtdBeforePosition) + model = getItem(position - adsQtdBeforePosition) + } + populateViewHolder(itemKey, viewHolder, model, position) + } + + @Suppress("UNCHECKED_CAST") + override fun populateViewHolder(itemKey: String?, viewHolder: RecyclerView.ViewHolder, model: T?, position: Int) { + if (getItemViewType(position) != VIEW_TYPE_ADS) { + super.populateViewHolder(itemKey, viewHolder, model) + } + } + + override fun getItemCount(): Int { + val contentItemCount = super.getContentCount() + val adsContentItemCount = contentItemCount + if (adsEachItems > 0) contentItemCount.div(adsEachItems) else 0 + return adsContentItemCount + 2 + } + + override fun getItemViewType(position: Int): Int { + var viewType = super.getItemViewType(position) + if (viewType == VIEW_TYPE_CONTENT) { + val qtdAdsBefore = getAdsQtdBeforePosition(position) + val nextEachItems = adsEachItems * (qtdAdsBefore + 1) + qtdAdsBefore + viewType = if (position == nextEachItems) VIEW_TYPE_ADS else VIEW_TYPE_CONTENT + } + return viewType + } + + private fun getAdsQtdBeforePosition(position: Int): Int { + val qtdAds = position.div(adsEachItems) + return (position - qtdAds).div(adsEachItems) + } + + protected fun getAdsQtdBeforeDefaultPosition(position: Int): Int { + return position.div(adsEachItems) + } + + fun onRestoreState(layoutManager: GridLayoutManager) { + layoutManager.apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + val itemType = getItemViewType(position) + return if (itemType == VIEW_TYPE_ADS) spanCount else 1 + } + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFilterActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFilterActivity.kt index 39a859e..fd8b490 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFilterActivity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFilterActivity.kt @@ -5,16 +5,13 @@ import android.animation.ValueAnimator import android.os.Bundle import android.support.design.widget.CoordinatorLayout import android.text.format.DateUtils +import android.view.View import com.ediposouza.teslesgendstracker.R -import com.ediposouza.teslesgendstracker.ui.widget.filter.FilterMagika -import com.ediposouza.teslesgendstracker.ui.widget.filter.FilterRarity import org.greenrobot.eventbus.Subscribe -import org.jetbrains.anko.find /** * Created by EdipoSouza on 12/29/16. */ - open class BaseFilterActivity : BaseActivity() { private val KEY_FILTERS_GREAT_MARGIN = "filterGreatMarginKey" @@ -22,17 +19,21 @@ open class BaseFilterActivity : BaseActivity() { private val UPDATE_FILTERS_POSITION_DURATION = DateUtils.SECOND_IN_MILLIS private val UPDATE_FILTERS_VISIBILITY_DURATION = UPDATE_FILTERS_POSITION_DURATION / 2 - var filterGreatMargin = false + protected var filterGreatMargin = false - val fab_filter_magika by lazy { find(R.id.filter_magika) } - val fab_filter_rarity by lazy { find(R.id.filter_rarity) } - val filterMagikaLP by lazy { fab_filter_magika.layoutParams as CoordinatorLayout.LayoutParams } - val filterRarityLP by lazy { fab_filter_rarity.layoutParams as CoordinatorLayout.LayoutParams } + private var fab_filter_magika: View? = null + get() = findViewById(R.id.cards_filter_magika) + private var fab_filter_rarity: View? = null + get() = findViewById(R.id.cards_filter_rarity) + private val filterMagikaLP: CoordinatorLayout.LayoutParams? + get() = fab_filter_magika?.layoutParams as? CoordinatorLayout.LayoutParams + private val filterRarityLP: CoordinatorLayout.LayoutParams? + get() = fab_filter_rarity?.layoutParams as? CoordinatorLayout.LayoutParams override fun onSaveInstanceState(outState: Bundle?) { outState?.apply { putBoolean(KEY_FILTERS_GREAT_MARGIN, filterGreatMargin) - putInt(KEY_FILTERS_BOTTOM_MARGIN, filterMagikaLP.bottomMargin) + putInt(KEY_FILTERS_BOTTOM_MARGIN, filterMagikaLP?.bottomMargin ?: 0) } super.onSaveInstanceState(outState) } @@ -49,8 +50,8 @@ open class BaseFilterActivity : BaseActivity() { val margin = if (filterGreatMargin) R.dimen.filter_great_margin_bottom else R.dimen.large_margin val showBottomMargin = resources.getDimensionPixelSize(margin) val hideBottomMargin = -resources.getDimensionPixelSize(R.dimen.filter_hide_height) - if (show && filterMagikaLP.bottomMargin == showBottomMargin || - !show && filterMagikaLP.bottomMargin == hideBottomMargin) { + if (show && filterMagikaLP?.bottomMargin == showBottomMargin || + !show && filterMagikaLP?.bottomMargin == hideBottomMargin) { return } val animFrom = if (show) hideBottomMargin else showBottomMargin @@ -82,7 +83,7 @@ open class BaseFilterActivity : BaseActivity() { fun onCmdUpdateRarityMagikaFiltersPosition(update: CmdUpdateRarityMagikaFiltersPosition) { filterGreatMargin = update.high val endMargin = if (filterGreatMargin) R.dimen.filter_great_margin_bottom else R.dimen.large_margin - with(ValueAnimator.ofInt(filterMagikaLP.bottomMargin, resources.getDimensionPixelSize(endMargin))) { + with(ValueAnimator.ofInt(filterMagikaLP?.bottomMargin ?: 0, resources.getDimensionPixelSize(endMargin))) { duration = UPDATE_FILTERS_POSITION_DURATION addUpdateListener { updateFiltersMargins(it.animatedValue as Int) @@ -92,10 +93,10 @@ open class BaseFilterActivity : BaseActivity() { } private fun updateFiltersMargins(bottomMargin: Int) { - filterRarityLP.bottomMargin = bottomMargin - filterMagikaLP.bottomMargin = bottomMargin - fab_filter_magika.layoutParams = filterMagikaLP - fab_filter_rarity.layoutParams = filterRarityLP + filterRarityLP?.bottomMargin = bottomMargin + filterMagikaLP?.bottomMargin = bottomMargin + fab_filter_magika?.layoutParams = filterMagikaLP + fab_filter_rarity?.layoutParams = filterRarityLP } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFirebaseRVAdapter.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFirebaseRVAdapter.kt new file mode 100644 index 0000000..3a59d9c --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFirebaseRVAdapter.kt @@ -0,0 +1,81 @@ +package com.ediposouza.teslesgendstracker.ui.base + +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.ui.util.firebase.FirebaseRVAdapter +import com.ediposouza.teslesgendstracker.util.inflate +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.Query +import kotlinx.android.synthetic.main.itemlist_loading.view.* +import timber.log.Timber + +/** + * Created by EdipoSouza on 1/2/17. + */ +abstract class BaseFirebaseRVAdapter(model: Class, + ref: () -> Query?, + pageSize: Int, + orderASC: Boolean = false, + filter: ((T) -> Boolean)? = null) : + FirebaseRVAdapter(model, ref, pageSize, orderASC, filter) { + + protected var VIEW_TYPE_HEADER = 0 + protected var VIEW_TYPE_CONTENT = 1 + protected var VIEW_TYPE_LOADING = 2 + + protected var synced: Boolean = false + + override val snapShotOffset: Int = 1 + + abstract fun onCreateDefaultViewHolder(parent: ViewGroup): VH + abstract fun onBindContentHolder(itemKey: String, model: T, viewHolder: VH) + abstract fun onSyncEnd() + + override fun getContentCount(): Int = super.getItemCount() + + override fun getItemCount(): Int = super.getItemCount() + 2 + + override fun getItemViewType(position: Int): Int { + return when (position) { + 0 -> VIEW_TYPE_HEADER + itemCount - 1 -> VIEW_TYPE_LOADING + else -> VIEW_TYPE_CONTENT + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + when (viewType) { + VIEW_TYPE_CONTENT -> return onCreateDefaultViewHolder(parent) + else -> { + val view = if (viewType == VIEW_TYPE_LOADING) parent.inflate(R.layout.itemlist_loading) else + LinearLayout(parent.context).apply { minimumHeight = 1 } + return object : RecyclerView.ViewHolder(view) {} + } + } + } + + @Suppress("UNCHECKED_CAST") + override fun populateViewHolder(itemKey: String?, viewHolder: RecyclerView.ViewHolder, model: T?) { + if (itemKey != null && model != null) { + onBindContentHolder(itemKey, model, viewHolder as VH) + } else { + viewHolder.itemView.loadingBar?.visibility = if (synced) View.GONE else View.VISIBLE + } + } + + override fun onSyncStatusChanged(synced: Boolean) { + this.synced = synced + notifyItemChanged(itemCount - 1) + if (synced) { + onSyncEnd() + } + } + + override fun onArrayError(firebaseError: DatabaseError) { + Timber.d(firebaseError.toException(), firebaseError.toString()) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFragment.kt index b32cbf9..9db9e31 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/BaseFragment.kt @@ -1,12 +1,13 @@ package com.ediposouza.teslesgendstracker.ui.base +import android.os.Bundle import android.support.v4.app.Fragment import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.util.ConfigManager import com.ediposouza.teslesgendstracker.util.MetricAction import com.ediposouza.teslesgendstracker.util.MetricsManager +import com.ediposouza.teslesgendstracker.util.alertThemed import org.greenrobot.eventbus.EventBus -import org.jetbrains.anko.alert import timber.log.Timber /** @@ -15,10 +16,26 @@ import timber.log.Timber open class BaseFragment : Fragment() { + private val KEY_IS_FRAGMENT_SELECTED = "isFragmentSelectedKey" + protected val eventBus: EventBus by lazy { EventBus.getDefault() } protected var isFragmentSelected: Boolean = false + override fun onSaveInstanceState(outState: Bundle?) { + outState?.apply { + putBoolean(KEY_IS_FRAGMENT_SELECTED, isFragmentSelected) + } + super.onSaveInstanceState(outState) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + if (savedInstanceState != null) { + isFragmentSelected = savedInstanceState.getBoolean(KEY_IS_FRAGMENT_SELECTED) + } + } + override fun onStart() { super.onStart() try { @@ -33,12 +50,11 @@ open class BaseFragment : Fragment() { super.onResume() ConfigManager.updateCaches { if (ConfigManager.isDBUpdating()) { - activity.alert(getString(R.string.app_bd_under_updating)) { + context.alertThemed(R.string.app_bd_under_updating, theme = R.style.AppDialog) { okButton { MetricsManager.trackAction(MetricAction.ACTION_NOTIFY_UPDATE()) System.exit(0) } - activity.setTheme(R.style.AppDialog) }.show() } } @@ -63,4 +79,8 @@ open class BaseFragment : Fragment() { isFragmentSelected = menuVisible } + protected fun showLogin() { + eventBus.post(CmdShowLogin()) + } + } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/Commands.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/Commands.kt index 8921cc6..637efa3 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/Commands.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/base/Commands.kt @@ -2,6 +2,7 @@ package com.ediposouza.teslesgendstracker.ui.base import android.R import android.support.annotation.IntDef +import android.support.annotation.IntegerRes import android.support.annotation.StringRes import android.support.design.widget.BaseTransientBottomBar import android.support.design.widget.Snackbar @@ -11,14 +12,14 @@ import com.ediposouza.teslesgendstracker.data.Class /** * Created by EdipoSouza on 11/6/16. */ -class CmdShowTabs - class CmdShowLogin class CmdLoginSuccess class CmdUpdateDeckAndShowDeck +data class CmdUpdateTitle(@IntegerRes val title: Int) + data class CmdShowCardsByAttr(val attr: Attribute) data class CmdShowDecksByClasses(val classes: List) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/CardActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardActivity.kt similarity index 76% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/CardActivity.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardActivity.kt index 855f6aa..4d2bc19 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/CardActivity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardActivity.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui +package com.ediposouza.teslesgendstracker.ui.cards import android.app.Activity import android.content.Context @@ -6,6 +6,7 @@ import android.content.Intent import android.os.Bundle import android.support.design.widget.BottomSheetBehavior import android.support.v4.app.ActivityCompat +import android.support.v7.widget.CardView import android.view.View import com.ediposouza.teslesgendstracker.App import com.ediposouza.teslesgendstracker.R @@ -24,14 +25,19 @@ class CardActivity : BaseActivity() { private val EXTRA_CARD = "cardExtra" private val EXTRA_FAVORITE = "favoriteExtra" - fun newIntent(context: Context, card: Card, favorite: Boolean = false): Intent { + fun newIntent(context: Context, card: Card): Intent { + return context.intentFor(EXTRA_CARD to card) + } + + fun newIntent(context: Context, card: Card, favorite: Boolean): Intent { return context.intentFor(EXTRA_CARD to card, EXTRA_FAVORITE to favorite) } } - val card: Card by lazy { intent.getParcelableExtra(EXTRA_CARD) } - var favorite: Boolean = false + private val card: Card by lazy { intent.getParcelableExtra(EXTRA_CARD) } + private val cardInfoSheetBehavior: BottomSheetBehavior by lazy { BottomSheetBehavior.from(card_bottom_sheet) } + private var favorite: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -39,6 +45,7 @@ class CardActivity : BaseActivity() { snackbarNeedMargin = false favorite = intent.getBooleanExtra(EXTRA_FAVORITE, false) + card_favorite_btn.visibility = if (intent.hasExtra(EXTRA_FAVORITE)) View.VISIBLE else View.GONE card_all_image.setOnClickListener { ActivityCompat.finishAfterTransition(this) MetricsManager.trackAction(MetricAction.ACTION_CARD_DETAILS_CLOSE_TAP()) @@ -53,7 +60,7 @@ class CardActivity : BaseActivity() { super.onPostCreate(savedInstanceState) MetricsManager.trackScreen(MetricScreen.SCREEN_CARD_DETAILS()) MetricsManager.trackCardView(card) - ads_view.load() + card_ads_view.load() } override fun onSaveInstanceState(outState: Bundle?) { @@ -61,9 +68,16 @@ class CardActivity : BaseActivity() { ActivityCompat.finishAfterTransition(this) } + override fun onBackPressed() { + if (cardInfoSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { + cardInfoSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } else { + super.onBackPressed() + } + } + private fun configureBottomSheet() { - val sheetBehavior = BottomSheetBehavior.from(card_bottom_sheet) - sheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + cardInfoSheetBehavior.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(bottomSheet: View, slideOffset: Float) { } @@ -77,7 +91,7 @@ class CardActivity : BaseActivity() { } }) - card_bottom_sheet.setOnClickListener { sheetBehavior.toggleExpanded() } + card_bottom_sheet.setOnClickListener { cardInfoSheetBehavior.toggleExpanded() } } private fun loadCardInfo() { @@ -95,7 +109,7 @@ class CardActivity : BaseActivity() { if (App.hasUserLogged()) { PrivateInteractor().setUserCardFavorite(card, !favorite) { favorite = !favorite - val stringRes = if (favorite) R.string.card_favorited else R.string.card_unfavorited + val stringRes = if (favorite) R.string.action_favorited else R.string.action_unfavorited toast(getString(stringRes, card.name)) loadCardInfo() setResult(Activity.RESULT_OK, Intent()) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardsFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardsFragment.kt index 58b03e6..138033d 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardsFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/CardsFragment.kt @@ -1,6 +1,8 @@ package com.ediposouza.teslesgendstracker.ui.cards import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.os.Handler import android.support.design.widget.BottomSheetBehavior @@ -13,18 +15,20 @@ import android.text.format.DateUtils import android.view.* import android.view.inputmethod.InputMethodManager import com.ediposouza.teslesgendstracker.R -import com.ediposouza.teslesgendstracker.data.Attribute import com.ediposouza.teslesgendstracker.ui.base.* import com.ediposouza.teslesgendstracker.ui.cards.tabs.CardsAllFragment import com.ediposouza.teslesgendstracker.ui.cards.tabs.CardsCollectionFragment import com.ediposouza.teslesgendstracker.ui.cards.tabs.CardsFavoritesFragment -import com.ediposouza.teslesgendstracker.ui.widget.CollectionStatistics -import com.ediposouza.teslesgendstracker.util.MetricScreen -import com.ediposouza.teslesgendstracker.util.MetricsManager -import com.ediposouza.teslesgendstracker.util.inflate -import com.ediposouza.teslesgendstracker.util.toggleExpanded +import com.ediposouza.teslesgendstracker.ui.cards.widget.CollectionStatistics +import com.ediposouza.teslesgendstracker.util.* import kotlinx.android.synthetic.main.activity_dash.* import kotlinx.android.synthetic.main.fragment_cards.* +import kotlinx.android.synthetic.main.include_new_update.* +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import org.jsoup.Jsoup +import timber.log.Timber + /** * Created by EdipoSouza on 10/30/16. @@ -37,9 +41,8 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { private val handler = Handler() private val trackSearch = Runnable { MetricsManager.trackSearch(query ?: "") } - val statisticsSheetBehavior: BottomSheetBehavior by lazy { - BottomSheetBehavior.from(activity.cards_collection_statistics) - } + private val statisticsSheetBehavior: BottomSheetBehavior + get() = BottomSheetBehavior.from(cards_collection_statistics) val pageChange = object : ViewPager.SimpleOnPageChangeListener() { override fun onPageSelected(position: Int) { @@ -47,7 +50,7 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { (cards_view_pager.adapter as CardsPageAdapter).getItem(position).updateCardsList() if (position == 1) { statisticsSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - activity.cards_collection_statistics.updateStatistics() + cards_collection_statistics.updateStatistics() } else { statisticsSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN } @@ -62,11 +65,12 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { } private fun updateActivityTitle(position: Int) { - activity.toolbar_title?.setText(when (position) { + val title = when (position) { 1 -> R.string.title_tab_cards_collection 2 -> R.string.title_tab_cards_favorites else -> R.string.title_tab_cards_all - }) + } + eventBus.post(CmdUpdateTitle(title)) } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -77,26 +81,28 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) activity.dash_navigation_view.setCheckedItem(R.id.menu_cards) - activity.cards_collection_statistics.setOnClickListener { + cards_collection_statistics.setOnClickListener { statisticsSheetBehavior.toggleExpanded() } statisticsSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN cards_view_pager.adapter = CardsPageAdapter(context, childFragmentManager) cards_view_pager.addOnPageChangeListener(pageChange) - attr_filter.filterClick = { + cards_filter_attr.filterClick = { eventBus.post(CmdShowCardsByAttr(it)) - attr_filter.selectAttr(it, true) + cards_filter_attr.selectAttr(it, true) } + cards_filter_rarity.filterClick = { eventBus.post(CmdFilterRarity(it)) } + cards_filter_magika.filterClick = { eventBus.post(CmdFilterMagika(it)) } Handler().postDelayed({ - eventBus.post(CmdShowCardsByAttr(Attribute.STRENGTH)) + eventBus.post(CmdFilterSet(null)) }, DateUtils.SECOND_IN_MILLIS) MetricsManager.trackScreen(MetricScreen.SCREEN_CARDS_ALL()) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - activity.toolbar_title.setText(R.string.app_name_full) - activity.dash_tab_layout.setupWithViewPager(cards_view_pager) + view?.post { updateActivityTitle(cards_view_pager?.currentItem ?: 0) } + cards_tab_layout.setupWithViewPager(cards_view_pager) } override fun onSaveInstanceState(outState: Bundle?) { @@ -113,8 +119,24 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { override fun onResume() { super.onResume() - eventBus.post(CmdShowTabs()) + cards_app_bar_layout.setExpanded(true, true) (activity as BaseFilterActivity).updateRarityMagikaFiltersVisibility(true) + checkLastVersion { + Timber.d("New version $it found!") + new_update_layout.visibility = View.VISIBLE + new_update_later.rippleDuration = 200 + new_update_later.setOnRippleCompleteListener { + new_update_layout.visibility = View.GONE + MetricsManager.trackAction(MetricAction.ACTION_NEW_VERSION_UPDATE_LATER()) + } + new_update_now.rippleDuration = 200 + new_update_now.setOnRippleCompleteListener { + startActivity(Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(getString(R.string.playstore_url_format, context.packageName)))) + MetricsManager.trackAction(MetricAction.ACTION_NEW_VERSION_UPDATE_NOW()) + } + MetricsManager.trackAction(MetricAction.ACTION_NEW_VERSION_DETECTED()) + } } override fun onPause() { @@ -125,9 +147,11 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { menu?.clear() inflater?.inflate(R.menu.menu_search, menu) + inflater?.inflate(R.menu.menu_import, menu) inflater?.inflate(R.menu.menu_sets, menu) + menu?.findItem(R.id.menu_import)?.isVisible = false with(MenuItemCompat.getActionView(menu?.findItem(R.id.menu_search)) as SearchView) { - queryHint = getString(R.string.search_hint) + queryHint = getString(R.string.cards_search_hint) setOnQueryTextListener(this@CardsFragment) } super.onCreateOptionsMenu(menu, inflater) @@ -150,6 +174,33 @@ class CardsFragment : BaseFragment(), SearchView.OnQueryTextListener { return true } + fun checkLastVersion(onNewVersion: (String?) -> Unit) { + doAsync { + try { + val pkg = context.packageName + val newer = Jsoup.connect(getString(R.string.playstore_url_format, pkg)) + .timeout(30000) + .userAgent("Mozilla/5.0 (Windows; U; WindowsNT 5.1; en-US; rv1.8.1.6) Gecko/20070725 Firefox/2.0.0.6") + .referrer("http://www.google.com") + .get() + .select("div[itemprop=softwareVersion]") + .first() + .ownText() + val pInfo = context.packageManager.getPackageInfo(pkg, 0) + val newerVersion = newer.replace(".", "") + val actualVersion = pInfo.versionName.replace(".", "") + Timber.d("Versions - remote: %s, local: %s", newerVersion, actualVersion) + uiThread { + if (Integer.parseInt(newerVersion) > Integer.parseInt(actualVersion)) { + onNewVersion(newer) + } + } + } catch (e: Exception) { + Timber.e(e.message) + } + } + } + class CardsPageAdapter(ctx: Context, fm: FragmentManager) : FragmentStatePagerAdapter(fm) { var titles: Array = ctx.resources.getStringArray(R.array.cards_tabs) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsAllFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsAllFragment.kt index 840ed55..e2b9655 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsAllFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsAllFragment.kt @@ -1,23 +1,18 @@ package com.ediposouza.teslesgendstracker.ui.cards.tabs import android.os.Bundle -import android.support.annotation.DimenRes import android.support.annotation.LayoutRes import android.support.v4.app.ActivityCompat import android.support.v4.app.ActivityOptionsCompat import android.support.v7.util.DiffUtil import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.RecyclerView -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.view.* import com.ediposouza.teslesgendstracker.App import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.data.* import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor import com.ediposouza.teslesgendstracker.interactor.PublicInteractor -import com.ediposouza.teslesgendstracker.ui.CardActivity import com.ediposouza.teslesgendstracker.ui.base.* import com.ediposouza.teslesgendstracker.ui.cards.* import com.ediposouza.teslesgendstracker.ui.util.GridSpacingItemDecoration @@ -29,8 +24,8 @@ import jp.wasabeef.recyclerview.animators.ScaleInAnimator import kotlinx.android.synthetic.main.fragment_cards_list.* import kotlinx.android.synthetic.main.include_login_button.* import kotlinx.android.synthetic.main.itemlist_card.view.* -import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import org.jetbrains.anko.itemsSequence import java.util.* /** @@ -38,6 +33,8 @@ import java.util.* */ open class CardsAllFragment : BaseFragment() { + protected val KEY_CURRENT_ATTR = "currentClassKey" + open val ADS_EACH_ITEMS = 21 //after 7 lines open val CARDS_PER_ROW = 3 @@ -49,15 +46,17 @@ open class CardsAllFragment : BaseFragment() { var classFilter: Class? = null var rarityFilter: CardRarity? = null var searchFilter: String? = null + var menuSets: SubMenu? = null + val publicInteractor: PublicInteractor by lazy { PublicInteractor() } val privateInteractor: PrivateInteractor by lazy { PrivateInteractor() } val transitionName: String by lazy { getString(R.string.card_transition_name) } + val gridLayoutManager by lazy { cards_recycler_view.layoutManager as GridLayoutManager } open protected val isCardsCollection: Boolean = false open val cardsAdapter by lazy { - val gridLayoutManager = cards_recycler_view.layoutManager as GridLayoutManager - CardsAllAdapter(ADS_EACH_ITEMS, gridLayoutManager, R.layout.itemlist_card_ads, R.dimen.card_height, + CardsAllAdapter(ADS_EACH_ITEMS, gridLayoutManager, R.layout.itemlist_card_ads, { view, card -> showCardExpanded(card, view) }) { view: View, card: Card -> showCardExpanded(card, view) @@ -80,20 +79,40 @@ open class CardsAllFragment : BaseFragment() { configRecycleView() } + override fun onSaveInstanceState(outState: Bundle?) { + outState?.apply { + putInt(KEY_CURRENT_ATTR, currentAttr.ordinal) + } + super.onSaveInstanceState(outState) + } + override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) cardsAdapter.onRestoreState(cards_recycler_view.layoutManager as GridLayoutManager) + currentAttr = Attribute.values()[savedInstanceState?.getInt(KEY_CURRENT_ATTR) ?: 0] + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + super.onCreateOptionsMenu(menu, inflater) + menuSets = menu?.findItem(R.id.menu_sets)?.subMenu } override fun onOptionsItemSelected(item: MenuItem?): Boolean { when (item?.itemId) { - R.id.menu_sets_all -> eventBus.post(CmdFilterSet(null)) - R.id.menu_sets_core -> eventBus.post(CmdFilterSet(CardSet.CORE)) - R.id.menu_sets_madhouse -> eventBus.post(CmdFilterSet(CardSet.MADHOUSE)) + R.id.menu_sets_all -> filterSet(item, null) + R.id.menu_sets_core -> filterSet(item, CardSet.CORE) + R.id.menu_sets_madhouse -> filterSet(item, CardSet.MADHOUSE) } return super.onOptionsItemSelected(item) } + private fun filterSet(menuItem: MenuItem?, set: CardSet?) { + menuSets?.itemsSequence()?.forEach { + it.setIcon(if (it.itemId == menuItem?.itemId) R.drawable.ic_checked else 0) + } + eventBus.post(CmdFilterSet(set)) + } + open fun configRecycleView() { cards_recycler_view.layoutManager = object : GridLayoutManager(context, CARDS_PER_ROW) { override fun supportsPredictiveItemAnimations(): Boolean = false @@ -108,7 +127,7 @@ open class CardsAllFragment : BaseFragment() { } fun configLoggedViews() { - signin_button.setOnClickListener { EventBus.getDefault().post(CmdShowLogin()) } + signin_button.setOnClickListener { showLogin() } signin_button.visibility = if (App.hasUserLogged()) View.INVISIBLE else View.VISIBLE cards_recycler_view.visibility = if (App.hasUserLogged()) View.VISIBLE else View.INVISIBLE } @@ -121,6 +140,7 @@ open class CardsAllFragment : BaseFragment() { } @Subscribe + @Suppress("UNUSED_PARAMETER") fun onCmdLoginSuccess(cmdLoginSuccess: CmdLoginSuccess) { configLoggedViews() loadCardsByAttr(currentAttr) @@ -175,11 +195,11 @@ open class CardsAllFragment : BaseFragment() { private fun loadCardsByAttr(attribute: Attribute) { currentAttr = attribute - PublicInteractor().getCards(setFilter, attribute) { + publicInteractor.getCards(setFilter, attribute) { cardsLoaded = it showCards() } - privateInteractor.getFavoriteCards(setFilter, currentAttr) { + privateInteractor.getUserFavoriteCards(setFilter, currentAttr) { userFavorites = it } } @@ -241,14 +261,14 @@ open class CardsAllFragment : BaseFragment() { ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, transitionName).toBundle()) } - class CardsAllAdapter(adsEachItems: Int, layoutManager: GridLayoutManager, @LayoutRes adsLayout: Int, - @DimenRes val cardHeight: Int, val itemClick: (View, Card) -> Unit, - val itemLongClick: (View, Card) -> Boolean) : BaseAdsAdapter(adsEachItems, layoutManager, adsLayout) { + open class CardsAllAdapter(adsEachItems: Int, layoutManager: GridLayoutManager, + @LayoutRes adsLayout: Int, val itemClick: (View, Card) -> Unit, + val itemLongClick: (View, Card) -> Boolean) : BaseAdsAdapter(adsEachItems, adsLayout, layoutManager) { var items: List = ArrayList() override fun onCreateDefaultViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return CardsAllViewHolder(parent.inflate(R.layout.itemlist_card), cardHeight, itemClick, itemLongClick) + return CardsAllViewHolder(parent.inflate(R.layout.itemlist_card), itemClick, itemLongClick) } override fun onBindDefaultViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { @@ -268,14 +288,15 @@ open class CardsAllFragment : BaseFragment() { oldItem.shortName == newItem.shortName }).dispatchUpdatesTo(this) } + } - class CardsAllViewHolder(val view: View, @DimenRes val cardHeight: Int, val itemClick: (View, Card) -> Unit, - val itemLongClick: (View, Card) -> Boolean) : RecyclerView.ViewHolder(view) { + open class CardsAllViewHolder(val view: View, val itemClick: (View, Card) -> Unit, + val itemLongClick: (View, Card) -> Boolean) : RecyclerView.ViewHolder(view) { init { val cardLayoutParams = itemView.card_all_image.layoutParams - cardLayoutParams.height = itemView.context.resources.getDimensionPixelSize(cardHeight) + cardLayoutParams.height = itemView.context.resources.getDimensionPixelSize(R.dimen.card_height) itemView.card_all_image.layoutParams = cardLayoutParams } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsCollectionFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsCollectionFragment.kt index 1ec7344..5feba8d 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsCollectionFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsCollectionFragment.kt @@ -1,31 +1,49 @@ package com.ediposouza.teslesgendstracker.ui.cards.tabs +import android.graphics.Bitmap import android.os.Bundle +import android.os.Handler import android.support.annotation.LayoutRes +import android.support.annotation.StringRes import android.support.design.widget.BottomSheetBehavior import android.support.v4.content.ContextCompat +import android.support.v7.app.AlertDialog import android.support.v7.util.DiffUtil import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup +import android.text.format.DateUtils +import android.view.* +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.webkit.WebViewClient import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.data.Card import com.ediposouza.teslesgendstracker.data.CardSlot import com.ediposouza.teslesgendstracker.ui.base.BaseAdsAdapter +import com.ediposouza.teslesgendstracker.ui.base.CmdShowCardsByAttr +import com.ediposouza.teslesgendstracker.ui.cards.CardActivity +import com.ediposouza.teslesgendstracker.ui.cards.widget.CollectionStatistics import com.ediposouza.teslesgendstracker.ui.util.SimpleDiffCallback -import com.ediposouza.teslesgendstracker.ui.widget.CollectionStatistics import com.ediposouza.teslesgendstracker.util.MetricAction import com.ediposouza.teslesgendstracker.util.MetricScreen import com.ediposouza.teslesgendstracker.util.MetricsManager import com.ediposouza.teslesgendstracker.util.inflate import jp.wasabeef.recyclerview.animators.ScaleInAnimator -import kotlinx.android.synthetic.main.activity_dash.* +import kotlinx.android.synthetic.main.dialog_import.view.* +import kotlinx.android.synthetic.main.dialog_import_result.view.* import kotlinx.android.synthetic.main.fragment_cards_list.* import kotlinx.android.synthetic.main.itemlist_card_collection.view.* +import kotlinx.android.synthetic.main.itemlist_card_imported.view.* +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.find +import org.jetbrains.anko.runOnUiThread +import org.jetbrains.anko.toast +import org.jsoup.Jsoup +import timber.log.Timber import java.util.* + /** * Created by EdipoSouza on 10/30/16. */ @@ -33,13 +51,11 @@ class CardsCollectionFragment : CardsAllFragment() { override val isCardsCollection: Boolean = true - val view_statistics: CollectionStatistics by lazy { activity.cards_collection_statistics } - val statisticsSheetBehavior: BottomSheetBehavior by lazy { - BottomSheetBehavior.from(view_statistics) - } + val view_statistics by lazy { activity.find(R.id.cards_collection_statistics) } + val statisticsSheetBehavior: BottomSheetBehavior + get() = BottomSheetBehavior.from(view_statistics) val cardsCollectionAdapter by lazy { - val gridLayoutManager = cards_recycler_view.layoutManager as GridLayoutManager CardsCollectionAdapter(ADS_EACH_ITEMS, gridLayoutManager, R.layout.itemlist_card_ads, { changeUserCardQtd(it) }) { view, card -> showCardExpanded(card, view) @@ -70,19 +86,85 @@ class CardsCollectionFragment : CardsAllFragment() { } + var importDialog: AlertDialog? = null + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) with(cards_recycler_view) { setPadding(paddingLeft, paddingTop, paddingRight, resources.getDimensionPixelSize(R.dimen.huge_margin)) } statisticsSheetBehavior.setBottomSheetCallback(sheetBehaviorCallback) } + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + menu?.findItem(R.id.menu_import)?.isVisible = true + super.onCreateOptionsMenu(menu, inflater) + } + override fun onOptionsItemSelected(item: MenuItem?): Boolean { BottomSheetBehavior.from(view_statistics).state = BottomSheetBehavior.STATE_COLLAPSED + if (item?.itemId == R.id.menu_import) { + showImportDialog() + return true + } return super.onOptionsItemSelected(item) } + override fun onStop() { + if (importDialog?.isShowing ?: false) { + importDialog?.dismiss() + } + super.onStop() + } + + private fun showImportDialog() { + val dialogView = View.inflate(context, R.layout.dialog_import, null) + importDialog = AlertDialog.Builder(context, R.style.AppDialog) + .setView(dialogView) + .setNegativeButton(android.R.string.cancel, { d, i -> + dialogView.import_dialog_webview.stopLoading() + MetricsManager.trackAction(MetricAction.ACTION_IMPORT_COLLECTION_CANCELLED()) + }) + .create() + importDialog?.setOnShowListener { + dialogView.import_dialog_webview?.apply { + settings.javaScriptEnabled = true + addJavascriptInterface(HTMLViewerInterface(), "HtmlViewer") + loadUrl(getString(R.string.dialog_import_legends_deck_link)) + setWebViewClient(object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + Timber.d("onPageStarted: $url") + val isCollectionPage = url == getString(R.string.dialog_import_legends_deck_link) + settings.loadsImagesAutomatically = !isCollectionPage + dialogView.import_dialog_loading.visibility = if (isCollectionPage) View.VISIBLE else View.GONE + with(dialogView.import_dialog_webview) { + layoutParams = layoutParams.apply { + height = if (isCollectionPage) 1 else ViewGroup.LayoutParams.WRAP_CONTENT + } + } + } + + override fun onPageFinished(view: WebView?, url: String?) { + Timber.d("onPageFinished: $url") + if (url == getString(R.string.dialog_import_legends_deck_link)) { + loadUrl("javascript:HtmlViewer.showHTML" + + "(''+document.getElementsByTagName('html')[0].innerHTML+'');") + } + if (url == getString(R.string.dialog_import_legends_deck_login_done_link)) { + loadUrl(getString(R.string.dialog_import_legends_deck_link)) + } + } + }) + } + } + importDialog?.show() + importDialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + importDialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + importDialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + MetricsManager.trackScreen(MetricScreen.SCREEN_IMPORT_COLLECTION()) + } + override fun configRecycleView() { super.configRecycleView() cards_recycler_view.adapter = cardsCollectionAdapter @@ -112,9 +194,102 @@ class CardsCollectionFragment : CardsAllFragment() { } } + inner class HTMLViewerInterface { + + @JavascriptInterface + fun showHTML(html: String) { + doAsync { + val legendsSlots = Jsoup.parse(html).select("#table_view tr")?.map { + val cardName = it.select(".td_title_card_collection").text() + val cardQtd = it.select(".td_total_card_collection").text().toInt() + val cardShortName = cardName.replace(" ", "").replace("-", "").replace("'", "").toLowerCase() + cardShortName to cardQtd + }?.filter { it.second > 0 }?.toMap() ?: mapOf() + if (legendsSlots.isNotEmpty()) { + importLegendDecksCards(legendsSlots) + } + } + } + + private fun importLegendDecksCards(legendsSlots: Map) { + publicInteractor.getCards(null) { allCards -> + val legendsDecksCards = allCards.filter { legendsSlots.keys.contains(it.shortName) }.toMutableList() + privateInteractor.getUserCollection(null) { userSlots -> + val userCards = allCards.filter { userSlots.keys.contains(it.shortName) }.toMutableList() + val onlyInLegendsDecks = legendsDecksCards.filter { !userSlots.keys.contains(it.shortName) } + val onlyInUserCollection = userCards.filter { !legendsSlots.keys.contains(it.shortName) } + val cardsInCommon = userCards.filter { legendsSlots.keys.contains(it.shortName) } + val userQtdGreater = cardsInCommon.filter { + userSlots[it.shortName] ?: 0 > legendsSlots[it.shortName] ?: 0 + } + val legendsQtdGreater = cardsInCommon.filter { + legendsSlots[it.shortName] ?: 0 > userSlots[it.shortName] ?: 0 + } + onlyInLegendsDecks.forEach { + val qtd = legendsSlots[it.shortName] ?: 0 + privateInteractor.setUserCardQtd(it, qtd) { + Timber.d("$qtd ${it.name} card added") + } + } + legendsQtdGreater.forEach { + val qtd = legendsSlots[it.shortName] ?: 0 + privateInteractor.setUserCardQtd(it, qtd) { + Timber.d("${it.name} qtd updated to $qtd") + } + } + showImportSummary(onlyInLegendsDecks, onlyInUserCollection, legendsQtdGreater, + userQtdGreater, legendsSlots, userSlots) + } + } + context.runOnUiThread { + context.toast("Collection imported!") + importDialog?.dismiss() + } + } + + private fun showImportSummary(onlyInLegendsDecks: List, onlyUserCollection: List, + legendsQtdGreater: List, userQtdGreater: List, + legendsSlots: Map, userSlots: Map) { + val dialogView = View.inflate(context, R.layout.dialog_import_result, null) + with(dialogView.import_dialog_imported_recycler_view) { + layoutManager = LinearLayoutManager(context) + adapter = CardsImportedAdapter(mutableListOf>().apply { + addAll(onlyInLegendsDecks.map { + Pair(CardSlot(it, legendsSlots[it.shortName] ?: 0), R.string.dialog_import_result_added) + }.sortedBy { it.first.card.attr.ordinal }) + addAll(legendsQtdGreater.map { + Pair(CardSlot(it, legendsSlots[it.shortName] ?: 0), R.string.dialog_import_result_updated) + }.sortedBy { it.first.card.attr.ordinal }) + }) + setHasFixedSize(true) + } + with(dialogView.import_dialog_difference_recycler_view) { + layoutManager = LinearLayoutManager(context) + adapter = CardsImportedAdapter(mutableListOf>().apply { + addAll(onlyUserCollection.map { + Pair(CardSlot(it, userSlots[it.shortName] ?: 0), R.string.dialog_import_result_you_have) + }.sortedBy { it.first.card.attr.ordinal }) + addAll(userQtdGreater.map { + Pair(CardSlot(it, userSlots[it.shortName] ?: 0), R.string.dialog_import_result_has_more) + }.sortedBy { it.first.card.attr.ordinal }) + }) + setHasFixedSize(true) + } + AlertDialog.Builder(context, R.style.AppDialog) + .setView(dialogView) + .setPositiveButton(android.R.string.ok, { d, i -> + Handler().postDelayed({ + eventBus.post(CmdShowCardsByAttr(currentAttr)) + }, DateUtils.SECOND_IN_MILLIS / 2) + }) + .show() + MetricsManager.trackAction(MetricAction.ACTION_IMPORT_COLLECTION_FINISH()) + } + } + class CardsCollectionAdapter(adsEachItems: Int, layoutManager: GridLayoutManager, @LayoutRes adsLayout: Int, val itemClick: (CardSlot) -> Unit, - val itemLongClick: (View, Card) -> Boolean) : BaseAdsAdapter(adsEachItems, layoutManager, adsLayout) { + val itemLongClick: (View, Card) -> Boolean) : BaseAdsAdapter(adsEachItems, adsLayout, layoutManager) { var items: ArrayList = ArrayList() @@ -175,4 +350,33 @@ class CardsCollectionFragment : CardsAllFragment() { } + class CardsImportedAdapter(val items: List>) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): CardsImportedViewHolder { + return CardsImportedViewHolder(parent?.inflate(R.layout.itemlist_card_imported)) + } + + override fun onBindViewHolder(holder: CardsImportedViewHolder?, position: Int) { + holder?.bind(items[position].first, items[position].second) + } + + override fun getItemCount() = items.size + } + + class CardsImportedViewHolder(view: View?) : RecyclerView.ViewHolder(view) { + + fun bind(cardSlot: CardSlot, @StringRes status: Int) { + with(itemView) { + card_imported_attr.setImageResource(cardSlot.card.attr.imageRes) + card_imported_name.text = cardSlot.card.name + card_imported_qtd.text = "${cardSlot.qtd}" + card_imported_result.text = context.getString(status) + setOnClickListener { + context.startActivity(CardActivity.newIntent(context, cardSlot.card)) + } + } + } + + } + } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsFavoritesFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsFavoritesFragment.kt index 6eda0cc..1d190b9 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsFavoritesFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/tabs/CardsFavoritesFragment.kt @@ -5,7 +5,7 @@ import android.content.Intent import android.support.v4.app.ActivityOptionsCompat import android.view.View import com.ediposouza.teslesgendstracker.data.Card -import com.ediposouza.teslesgendstracker.ui.CardActivity +import com.ediposouza.teslesgendstracker.ui.cards.CardActivity import kotlinx.android.synthetic.main.fragment_cards_list.* /** @@ -28,7 +28,7 @@ class CardsFavoritesFragment : CardsAllFragment() { } override fun showCards() { - privateInteractor.getFavoriteCards(setFilter, currentAttr) { + privateInteractor.getUserFavoriteCards(setFilter, currentAttr) { userFavorites = it cardsAdapter.showCards(filteredCards().filter { userFavorites.contains(it.shortName) }) cards_recycler_view?.scrollToPosition(0) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/CollectionStatistics.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/CollectionStatistics.kt similarity index 96% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/CollectionStatistics.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/CollectionStatistics.kt index ce24283..ee14e31 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/CollectionStatistics.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/CollectionStatistics.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui.widget +package com.ediposouza.teslesgendstracker.ui.cards.widget import android.content.Context import android.os.Build @@ -26,7 +26,7 @@ class CollectionStatistics(ctx: Context?, attrs: AttributeSet?, defStyleAttr: In init { inflate(context, R.layout.widget_collection_statistics, this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val layoutParams = collection_statistics_container.layoutParams as FrameLayout.LayoutParams + val layoutParams = collection_statistics_container.layoutParams as LayoutParams layoutParams.bottomMargin = resources.getDimensionPixelSize(R.dimen.navigation_bar_height) collection_statistics_container.layoutParams = layoutParams } @@ -77,8 +77,8 @@ class CollectionStatistics(ctx: Context?, attrs: AttributeSet?, defStyleAttr: In rarity_statistics_endurance.soulMissing + rarity_statistics_dual.soulMissing + rarity_statistics_neutral.soulMissing val percent = if (total > 0) owned.toFloat() / total.toFloat() * 100f else 0f - collection_statistics_total.text = context.getString(R.string.statistics_total, owned, total) - collection_statistics_percent.text = context.getString(R.string.statistics_percent, percent) + collection_statistics_total.text = context.getString(R.string.collection_statistics_total, owned, total) + collection_statistics_percent.text = context.getString(R.string.collection_statistics_percent, percent) collection_statistics_soul.text = NumberFormat.getNumberInstance().format(soulMissing) } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/CollectionStatisticsAttr.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/CollectionStatisticsAttr.kt similarity index 84% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/CollectionStatisticsAttr.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/CollectionStatisticsAttr.kt index 9b35308..765c92f 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/CollectionStatisticsAttr.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/CollectionStatisticsAttr.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui.widget +package com.ediposouza.teslesgendstracker.ui.cards.widget import android.content.Context import android.util.AttributeSet @@ -37,25 +37,25 @@ class CollectionStatisticsAttr(ctx: Context?, attrs: AttributeSet?, defStyleAttr constructor(ctx: Context?, attrs: AttributeSet) : this(ctx, attrs, 0) fun setCommon(owned: Int, total: Int) { - attr_statistics_common.text = context.getString(R.string.statistics_rarity, owned, total) + attr_statistics_common.text = context.getString(R.string.collection_statistics_rarity, owned, total) cards[CardRarity.COMMON] = Pair(owned, total) updateTotal() } fun setRare(owned: Int, total: Int) { - attr_statistics_rare.text = context.getString(R.string.statistics_rarity, owned, total) + attr_statistics_rare.text = context.getString(R.string.collection_statistics_rarity, owned, total) cards[CardRarity.RARE] = Pair(owned, total) updateTotal() } fun setEpic(owned: Int, total: Int) { - attr_statistics_epic.text = context.getString(R.string.statistics_rarity, owned, total) + attr_statistics_epic.text = context.getString(R.string.collection_statistics_rarity, owned, total) cards[CardRarity.EPIC] = Pair(owned, total) updateTotal() } fun setLegendary(owned: Int, total: Int) { - attr_statistics_legendary.text = context.getString(R.string.statistics_rarity, owned, total) + attr_statistics_legendary.text = context.getString(R.string.collection_statistics_rarity, owned, total) cards[CardRarity.LEGENDARY] = Pair(owned, total) updateTotal() } @@ -64,7 +64,7 @@ class CollectionStatisticsAttr(ctx: Context?, attrs: AttributeSet?, defStyleAttr owned = cards.map { it.value.first }.sum() total = cards.map { it.value.second }.sum() soulMissing = cards.map { (it.value.second - it.value.first) * it.key.soulCost }.sum() - attr_statistics_total.text = context.getString(R.string.statistics_total, owned, total) + attr_statistics_total.text = context.getString(R.string.collection_statistics_total, owned, total) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterMagika.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/FilterMagika.kt similarity index 97% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterMagika.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/FilterMagika.kt index 8d23cce..7b49e71 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterMagika.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/FilterMagika.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui.widget.filter +package com.ediposouza.teslesgendstracker.ui.cards.widget import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterRarity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/FilterRarity.kt similarity index 98% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterRarity.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/FilterRarity.kt index 2cd33a9..43db3b1 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterRarity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/cards/widget/FilterRarity.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui.widget.filter +package com.ediposouza.teslesgendstracker.ui.cards.widget import android.animation.Animator import android.animation.ValueAnimator diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/Commands.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/Commands.kt index 9b16aa8..534ccdf 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/Commands.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/Commands.kt @@ -2,6 +2,7 @@ package com.ediposouza.teslesgendstracker.ui.decks import com.ediposouza.teslesgendstracker.data.Attribute import com.ediposouza.teslesgendstracker.data.Card +import com.ediposouza.teslesgendstracker.data.CardSlot /** * Created by ediposouza on 06/12/16. @@ -16,4 +17,10 @@ data class CmdRemAttr( val attr: Attribute +) + +data class CmdUpdateCardSlot( + + val cardSlot: CardSlot + ) \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DeckActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DeckActivity.kt similarity index 92% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DeckActivity.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DeckActivity.kt index 1a0b66d..44818bf 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/DeckActivity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DeckActivity.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui +package com.ediposouza.teslesgendstracker.ui.decks import android.app.Activity import android.content.Context @@ -39,7 +39,10 @@ import jp.wasabeef.recyclerview.animators.SlideInLeftAnimator import kotlinx.android.synthetic.main.activity_deck.* import kotlinx.android.synthetic.main.include_deck_info.* import kotlinx.android.synthetic.main.itemlist_deck_comment.view.* -import org.jetbrains.anko.* +import org.jetbrains.anko.contentView +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.intentFor +import org.jetbrains.anko.toast import org.threeten.bp.format.DateTimeFormatter import timber.log.Timber import java.text.NumberFormat @@ -65,7 +68,7 @@ class DeckActivity : BaseActivity() { private val privateInteractor by lazy { PrivateInteractor() } private val keyboardUtil by lazy { KeyboardUtil(this, contentView) } private val deckOwned by lazy { intent.getBooleanExtra(EXTRA_OWNED, false) } - private val deck: Deck by lazy { intent.getParcelableExtra(EXTRA_DECK) } + private val deck by lazy { intent.getParcelableExtra(EXTRA_DECK) } private val numberInstance: NumberFormat by lazy { NumberFormat.getNumberInstance() } private val commentsSheetBehavior: BottomSheetBehavior by lazy { BottomSheetBehavior.from(deck_bottom_sheet) } @@ -96,10 +99,18 @@ class DeckActivity : BaseActivity() { } private fun configViews() { + if (deckOwned) { + deck_fab_favorite.hide() + deck_details_likes.visibility = View.GONE + deck_details_views.visibility = View.GONE + commentsSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } deck_fab_favorite.setOnClickListener { if (App.hasUserLogged()) { privateInteractor.setUserDeckFavorite(deck, !favorite) { favorite = !favorite + val stringRes = if (favorite) R.string.action_favorited else R.string.action_unfavorited + toast(getString(stringRes, deck.name)) updateFavoriteItem() MetricsManager.trackAction(if (favorite) MetricAction.ACTION_DECK_DETAILS_FAVORITE() else MetricAction.ACTION_DECK_DETAILS_UNFAVORITE()) @@ -158,6 +169,10 @@ class DeckActivity : BaseActivity() { CommonUtils.hideKeyboard(this, deck_comment_new) }, DateUtils.SECOND_IN_MILLIS / 2) } + if (ConfigManager.isShowDeckAds()) { + deck_ads_view.visibility = View.VISIBLE + deck_ads_view.load() + } setResult(Activity.RESULT_OK, Intent()) MetricsManager.trackScreen(MetricScreen.SCREEN_DECK_DETAILS()) } @@ -181,7 +196,7 @@ class DeckActivity : BaseActivity() { } override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(if (deckOwned) R.menu.menu_deck_owner else R.menu.menu_deck, menu) + menuInflater.inflate(if (deckOwned) R.menu.menu_delete else R.menu.menu_like, menu) menuLike = menu?.findItem(R.id.menu_like) updateLikeItem() return super.onCreateOptionsMenu(menu) @@ -209,7 +224,7 @@ class DeckActivity : BaseActivity() { return true } R.id.menu_delete -> { - alert(R.string.confirm_message) { + alertThemed(R.string.confirm_message, theme = R.style.AppDialog) { negativeButton(android.R.string.no, {}) positiveButton(android.R.string.yes, { privateInteractor.deleteDeck(deck, deck.private) { @@ -218,7 +233,6 @@ class DeckActivity : BaseActivity() { MetricsManager.trackAction(MetricAction.ACTION_DECK_DETAILS_DELETE()) } }) - setTheme(R.style.AppDialog) }.show() return true } @@ -284,11 +298,13 @@ class DeckActivity : BaseActivity() { private fun loadDeckRemoteInfo() { doAsync { calculateMissingSoul(deck, privateInteractor) - publicInteractor.incDeckView(deck) { - deck_details_views.text = deck.views.inc().toString() + if (!deckOwned) { + publicInteractor.incDeckView(deck) { + deck_details_views.text = it.toString() + } } publicInteractor.getPatches { - val patch = it.find { it.uidDate == deck.patch } + val patch = it.find { it.uuidDate == deck.patch } runOnUiThread { deck_details_patch.text = patch?.desc ?: "" } @@ -310,7 +326,7 @@ class DeckActivity : BaseActivity() { with(deck_details_soul_missing) { visibility = View.INVISIBLE deck_details_soul_missing_loading.visibility = View.VISIBLE - privateInteractor.getMissingCards(deck, { deck_details_soul_missing_loading.visibility = View.VISIBLE }) { + privateInteractor.getDeckMissingCards(deck, { deck_details_soul_missing_loading.visibility = View.VISIBLE }) { deck_details_soul_missing_loading.visibility = View.GONE val missingSoul = it.map { it.qtd * it.rarity.soulCost }.sum() Timber.d("Missing %d", missingSoul) @@ -357,7 +373,7 @@ class DeckActivity : BaseActivity() { } fun rem(commentId: String) { - val deckComment = items.find { it.id == commentId } + val deckComment = items.find { it.uuid == commentId } val deckCommentIndex = items.indexOf(deckComment) (items as ArrayList).remove(deckComment) notifyItemRemoved(deckCommentIndex) @@ -384,7 +400,7 @@ class DeckActivity : BaseActivity() { with(itemView.deck_comment_delete) { val owner = comment.owner == FirebaseAuth.getInstance().currentUser?.uid visibility = if (owner) View.VISIBLE else View.GONE - setOnClickListener { onRemComment(comment.id) } + setOnClickListener { onRemComment(comment.uuid) } } Glide.with(itemView.context) .load(ownerUser.photoUrl) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DecksFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DecksFragment.kt index b293dcc..8a70c46 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DecksFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/DecksFragment.kt @@ -16,10 +16,8 @@ import android.view.* import android.view.inputmethod.InputMethodManager import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.data.Class -import com.ediposouza.teslesgendstracker.ui.DashActivity import com.ediposouza.teslesgendstracker.ui.base.* import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterSearch -import com.ediposouza.teslesgendstracker.ui.decks.new.NewDeckActivity import com.ediposouza.teslesgendstracker.ui.decks.tabs.DecksFavoritedFragment import com.ediposouza.teslesgendstracker.ui.decks.tabs.DecksOwnerFragment import com.ediposouza.teslesgendstracker.ui.decks.tabs.DecksPublicFragment @@ -51,17 +49,17 @@ class DecksFragment : BaseFragment(), SearchView.OnQueryTextListener { 1 -> MetricScreen.SCREEN_DECKS_OWNED() else -> MetricScreen.SCREEN_DECKS_FAVORED() }) - (adapter.getItem(position) as DecksPublicFragment).showDecks() } } private fun updateActivityTitle(position: Int) { - activity.toolbar_title?.setText(when (position) { + val title = CmdUpdateTitle(when (position) { 1 -> R.string.title_tab_decks_owned 2 -> R.string.title_tab_decks_favorites else -> R.string.title_tab_decks_public }) + eventBus.post(title) } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -72,9 +70,7 @@ class DecksFragment : BaseFragment(), SearchView.OnQueryTextListener { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) decks_view_pager.adapter = adapter - activity.dash_tab_layout.setupWithViewPager(decks_view_pager) activity.dash_navigation_view.setCheckedItem(R.id.menu_decks) - activity.toolbar_title?.setText(R.string.title_tab_decks_public) decks_view_pager.addOnPageChangeListener(pageChange) decks_attr_filter.filterClick = { if (decks_attr_filter.isAttrSelected(it)) { @@ -82,16 +78,25 @@ class DecksFragment : BaseFragment(), SearchView.OnQueryTextListener { } else { decks_attr_filter.selectAttr(it, false) } - requestDecks() + eventBus.post(CmdShowDecksByClasses(Class.getClasses(decks_attr_filter.getSelectedAttrs()))) + } + decks_fab_add.setOnClickListener { + val anim = ActivityOptionsCompat.makeCustomAnimation(context, R.anim.slide_up, R.anim.slide_down) + startActivityForResult(context.intentFor(), RC_NEW_DECK, anim.toBundle()) } - Handler().postDelayed({ requestDecks() }, DateUtils.SECOND_IN_MILLIS) MetricsManager.trackScreen(MetricScreen.SCREEN_DECKS_PUBLIC()) } + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + view?.post { updateActivityTitle(decks_view_pager?.currentItem ?: 0) } + decks_tab_layout.setupWithViewPager(decks_view_pager) + } + override fun onSaveInstanceState(outState: Bundle?) { outState?.apply { putInt(KEY_PAGE_VIEW_POSITION, decks_view_pager?.currentItem ?: 0) - putBoolean(KEY_FAB_NEW_DECK, activity?.decks_fab_add?.isShown ?: false) + putBoolean(KEY_FAB_NEW_DECK, decks_fab_add?.isShown ?: false) } super.onSaveInstanceState(outState) } @@ -100,45 +105,19 @@ class DecksFragment : BaseFragment(), SearchView.OnQueryTextListener { super.onViewStateRestored(savedInstanceState) savedInstanceState?.apply { decks_view_pager.currentItem = getInt(KEY_PAGE_VIEW_POSITION) - if (getBoolean(KEY_FAB_NEW_DECK)) { - configFABNewDeck(activity as DashActivity) - } - } - } - - override fun onAttach(context: Context?) { - super.onAttach(context) - configFABNewDeck(context as DashActivity) - } - - private fun configFABNewDeck(dashActivity: DashActivity) { - dashActivity.decks_fab_add?.apply { - setOnClickListener { - val anim = ActivityOptionsCompat.makeCustomAnimation(context, R.anim.slide_up, R.anim.slide_down) - startActivityForResult(context.intentFor(), RC_NEW_DECK, anim.toBundle()) - } - postDelayed({ if (this@DecksFragment.isAdded) show() }, DateUtils.SECOND_IN_MILLIS * 2) } } override fun onResume() { super.onResume() - eventBus.post(CmdShowTabs()) - } - - override fun onDetach() { - super.onDetach() - with(activity.decks_fab_add) { - setOnClickListener { } - hide() - } + decks_app_bar_layout.setExpanded(true, true) } override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { menu?.clear() inflater?.inflate(R.menu.menu_search, menu) with(MenuItemCompat.getActionView(menu?.findItem(R.id.menu_search)) as SearchView) { - queryHint = getString(R.string.search_hint) + queryHint = getString(R.string.decks_search_hint) setOnQueryTextListener(this@DecksFragment) } super.onCreateOptionsMenu(menu, inflater) @@ -165,17 +144,12 @@ class DecksFragment : BaseFragment(), SearchView.OnQueryTextListener { return true } - private fun requestDecks() { - val classesToShow = Class.getClasses(decks_attr_filter.getSelectedAttrs()) - eventBus.post(CmdShowDecksByClasses(classesToShow)) - } - @Subscribe - fun onCmdUpdateRarityMagikaFiltersVisibility(update: CmdUpdateVisibility) { + fun onCmdUpdateVisibility(update: CmdUpdateVisibility) { if (update.show) { - activity.decks_fab_add.show() + decks_fab_add.show() } else { - activity.decks_fab_add.hide() + decks_fab_add.hide() } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/new/NewDeckActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/NewDeckActivity.kt similarity index 82% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/new/NewDeckActivity.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/NewDeckActivity.kt index 4f1f1a1..a953df3 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/new/NewDeckActivity.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/NewDeckActivity.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui.decks.new +package com.ediposouza.teslesgendstracker.ui.decks import android.app.Activity import android.content.Intent @@ -21,8 +21,7 @@ import com.ediposouza.teslesgendstracker.ui.base.CmdShowSnackbarMsg import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterClass import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterMagika import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterRarity -import com.ediposouza.teslesgendstracker.ui.decks.CmdAddCard -import com.ediposouza.teslesgendstracker.ui.decks.CmdRemAttr +import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterSet import com.ediposouza.teslesgendstracker.util.MetricAction import com.ediposouza.teslesgendstracker.util.MetricScreen import com.ediposouza.teslesgendstracker.util.MetricsManager @@ -63,38 +62,20 @@ class NewDeckActivity : BaseFilterActivity() { override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) supportActionBar?.setDisplayHomeAsUpEnabled(true) - toolbar_title.text = getString(R.string.new_deck_title) + new_deck_toolbar_title.text = getString(R.string.new_deck_title) new_deck_cardlist.editMode = true configDeckFilters() - supportFragmentManager.beginTransaction() - .replace(R.id.new_deck_fragment_cards, NewDeckCardsListFragment()) - .commit() + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.new_deck_fragment_cards, NewDeckCardsListFragment()) + .commit() + } handler.postDelayed({ - eventBus.post(CmdShowCardsByAttr(Attribute.STRENGTH)) + eventBus.post(CmdFilterSet(null)) }, DateUtils.SECOND_IN_MILLIS) MetricsManager.trackScreen(MetricScreen.SCREEN_NEW_DECKS()) } - private fun configDeckFilters() { - with(new_deck_attr_filter) { - filterClick = attrFilterClick - onAttrLock = { attr1: Attribute, attr2: Attribute -> - val deckCls = Class.getClasses(listOf(attr1, attr2)).first() - new_deck_class_cover.setImageResource(deckCls.imageRes) - toolbar_title.text = getString(R.string.new_deck_class_title, deckCls.name.toLowerCase().capitalize()) - val outValue = TypedValue() - resources.getValue(R.dimen.deck_class_cover_alpha, outValue, true) - new_deck_class_cover.animate().alpha(outValue.float).setDuration(ANIM_DURATION).start() - } - onAttrUnlock = { - toolbar_title.text = getString(R.string.new_deck_title) - new_deck_class_cover.animate().alpha(0f).setDuration(ANIM_DURATION).start() - } - } - filter_rarity.filterClick = { eventBus.post(CmdFilterRarity(it)) } - filter_magika.filterClick = { eventBus.post(CmdFilterMagika(it)) } - } - override fun onSaveInstanceState(outState: Bundle?) { outState?.apply { val cardsArrayList = ArrayList() @@ -107,7 +88,15 @@ class NewDeckActivity : BaseFilterActivity() { override fun onRestoreInstanceState(savedInstanceState: Bundle?) { super.onRestoreInstanceState(savedInstanceState) savedInstanceState?.apply { - new_deck_cardlist?.addCards(getParcelableArrayList(KEY_DECK_CARDS)) + val deckCardSlots = getParcelableArrayList(KEY_DECK_CARDS) + new_deck_cardlist?.addCards(deckCardSlots) + handler.postDelayed({ + deckCardSlots.forEach { + eventBus.post(CmdUpdateCardSlot(it)) + new_deck_attr_filter.lockAttrs(it.card.dualAttr1, it.card.dualAttr2, false) + } + }, DateUtils.SECOND_IN_MILLIS / 2) + updateDualFilter() } } @@ -148,8 +137,28 @@ class NewDeckActivity : BaseFilterActivity() { return super.onOptionsItemSelected(item) } + private fun configDeckFilters() { + with(new_deck_attr_filter) { + filterClick = attrFilterClick + onAttrLock = { attr1: Attribute, attr2: Attribute -> + val deckCls = Class.getClasses(listOf(attr1, attr2)).first() + new_deck_class_cover.setImageResource(deckCls.imageRes) + new_deck_toolbar_title.text = getString(R.string.new_deck_class_title, deckCls.name.toLowerCase().capitalize()) + val outValue = TypedValue() + resources.getValue(R.dimen.deck_class_cover_alpha, outValue, true) + new_deck_class_cover.animate().alpha(outValue.float).setDuration(ANIM_DURATION).start() + } + onAttrUnlock = { + new_deck_toolbar_title.text = getString(R.string.new_deck_title) + new_deck_class_cover.animate().alpha(0f).setDuration(ANIM_DURATION).start() + } + } + cards_filter_rarity.filterClick = { eventBus.post(CmdFilterRarity(it)) } + cards_filter_magika.filterClick = { eventBus.post(CmdFilterMagika(it)) } + } + private fun showSaveDialog() { - val view = View.inflate(this@NewDeckActivity, R.layout.dialog_new_deck, null) + val view = View.inflate(this, R.layout.dialog_new_deck, null) val deckTypes = DeckType.values().filter { it != DeckType.ARENA }.map { it.name.toLowerCase().capitalize() } view.new_deck_dialog_type_spinner.adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, deckTypes) @@ -177,8 +186,12 @@ class NewDeckActivity : BaseFilterActivity() { val deckPatchSelected = deckPatches.find { it.desc == deckPatchDesc } ?: deckPatches.last() val deckCards = new_deck_cardlist.getCards().map { it.card.shortName to it.qtd }.toMap() val deckPrivate = !view.new_deck_dialog_public.isChecked + if (deckName.length < 5) { + eventBus.post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, R.string.new_match_dialog_start_error_name)) + return + } PrivateInteractor().saveDeck(deckName, deckCls, deckTypeSelected, new_deck_cardlist.getSoulCost(), - deckPatchSelected.uidDate, deckCards, deckPrivate) { + deckPatchSelected.uuidDate, deckCards, deckPrivate) { toast(if (deckPrivate) R.string.new_deck_save_as_private else R.string.new_deck_save_as_public) val data = intentFor(DECK_PRIVATE_EXTRA to deckPrivate) setResult(Activity.RESULT_OK, data) @@ -190,8 +203,8 @@ class NewDeckActivity : BaseFilterActivity() { private fun updateDualFilter() { if (new_deck_attr_filter.lastAttrSelected == Attribute.DUAL) { val cls = Class.getClasses(listOf(new_deck_attr_filter.lockAttr1 ?: Attribute.NEUTRAL, - new_deck_attr_filter.lockAttr2 ?: Attribute.NEUTRAL)).first() - eventBus.post(CmdFilterClass(cls)) + new_deck_attr_filter.lockAttr2 ?: Attribute.NEUTRAL)).filter { it != Class.NEUTRAL } + eventBus.post(CmdFilterClass(cls.firstOrNull())) } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/NewDeckCardsListFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/NewDeckCardsListFragment.kt new file mode 100644 index 0000000..a2aa6a0 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/NewDeckCardsListFragment.kt @@ -0,0 +1,93 @@ +package com.ediposouza.teslesgendstracker.ui.decks + +import android.support.v4.content.ContextCompat +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.View +import android.view.ViewGroup +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.data.Card +import com.ediposouza.teslesgendstracker.data.CardSlot +import com.ediposouza.teslesgendstracker.ui.cards.tabs.CardsAllFragment +import com.ediposouza.teslesgendstracker.ui.util.GridSpacingItemDecoration +import com.ediposouza.teslesgendstracker.util.inflate +import kotlinx.android.synthetic.main.fragment_cards_list.* +import kotlinx.android.synthetic.main.itemlist_card.view.* +import org.greenrobot.eventbus.Subscribe + +class NewDeckCardsListFragment : CardsAllFragment() { + + override val ADS_EACH_ITEMS = 20 //after 10 lines + override val CARDS_PER_ROW = 2 + + private val onItemClick = { view: View, card: Card -> + eventBus.post(CmdAddCard(card)) + } + + override val itemDecoration by lazy { + GridSpacingItemDecoration(CARDS_PER_ROW, + resources.getDimensionPixelSize(R.dimen.deck_new_card_margin), false) + } + + override val cardsAdapter by lazy { + cardsNewDeckAdapter(ADS_EACH_ITEMS, gridLayoutManager, onItemClick, { + view: View, card: Card -> + showCardExpanded(card, view) + true + }) + } + + override fun configRecycleView() { + super.configRecycleView() + cards_recycler_view.setPadding(0, 0, 0, 0) + isFragmentSelected = true + } + + @Subscribe + fun onUpdateCardSlot(cmdUpdateCardSlot: CmdUpdateCardSlot) { + cardsAdapter.updateCardSlot(cmdUpdateCardSlot.cardSlot) + } + + class cardsNewDeckAdapter(adsEachItems: Int, layoutManager: GridLayoutManager, itemClick: (View, Card) -> Unit, + itemLongClick: (View, Card) -> Boolean) : CardsAllAdapter(adsEachItems, + layoutManager, R.layout.itemlist_new_deck_card_ads, itemClick, itemLongClick) { + + var deckCardSlots = arrayListOf() + + override fun onCreateDefaultViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { + return CardsNewDeckViewHolder(parent.inflate(R.layout.itemlist_card), itemClick, itemLongClick) + } + + override fun onBindDefaultViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { + val card = items[position] + val deckQtd = deckCardSlots.find { it.card.shortName == card.shortName }?.qtd ?: 0 + (holder as CardsNewDeckViewHolder).bind(CardSlot(card, deckQtd)) + } + + fun updateCardSlot(cardSlot: CardSlot) { + deckCardSlots.removeIf { it.card.shortName == cardSlot.card.shortName } + deckCardSlots.add(cardSlot) + notifyDataSetChanged() + } + } + + class CardsNewDeckViewHolder(view: View, itemClick: (View, Card) -> Unit, itemLongClick: (View, Card) -> Boolean) : + CardsAllViewHolder(view, itemClick, itemLongClick) { + + fun bind(cardSlot: CardSlot) { + itemView.setOnLongClickListener { itemLongClick(itemView.card_all_image, cardSlot.card) } + itemView.card_all_image.setImageBitmap(cardSlot.card.imageBitmap(itemView.context)) + val isCardUnique = cardSlot.card.unique + if (isCardUnique && cardSlot.qtd == 1 || !isCardUnique && cardSlot.qtd == 3) { + val color = ContextCompat.getColor(itemView.context, R.color.card_zero_qtd) + itemView.card_all_image.setColorFilter(color) + itemView.setOnClickListener { } + } else { + itemView.setOnClickListener { itemClick(itemView.card_all_image, cardSlot.card) } + itemView.card_all_image.clearColorFilter() + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/new/NewDeckCardsListFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/new/NewDeckCardsListFragment.kt deleted file mode 100644 index e47e161..0000000 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/new/NewDeckCardsListFragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.ediposouza.teslesgendstracker.ui.decks.new - -import android.support.v7.widget.GridLayoutManager -import android.view.View -import com.ediposouza.teslesgendstracker.R -import com.ediposouza.teslesgendstracker.data.Card -import com.ediposouza.teslesgendstracker.ui.cards.tabs.CardsAllFragment -import com.ediposouza.teslesgendstracker.ui.decks.CmdAddCard -import com.ediposouza.teslesgendstracker.ui.util.GridSpacingItemDecoration -import kotlinx.android.synthetic.main.fragment_cards_list.* -import org.greenrobot.eventbus.EventBus - -class NewDeckCardsListFragment : CardsAllFragment() { - - override val ADS_EACH_ITEMS = 20 //after 10 lines - override val CARDS_PER_ROW = 2 - - val onItemClick = { view: View, card: Card -> - EventBus.getDefault().post(CmdAddCard(card)) - } - - override val itemDecoration by lazy { - GridSpacingItemDecoration(CARDS_PER_ROW, - resources.getDimensionPixelSize(R.dimen.deck_new_card_margin), false) - } - - override val cardsAdapter by lazy { - val gridLayoutManager = cards_recycler_view.layoutManager as GridLayoutManager - CardsAllAdapter(ADS_EACH_ITEMS, gridLayoutManager, R.layout.itemlist_new_deck_card_ads, - R.dimen.card_height, onItemClick) { - view: View, card: Card -> - showCardExpanded(card, view) - true - } - } - - override fun configRecycleView() { - super.configRecycleView() - isFragmentSelected = true - cards_recycler_view.setPadding(0, 0, 0, 0) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksFavoritedFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksFavoritedFragment.kt index ffc4e9a..6ad8b61 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksFavoritedFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksFavoritedFragment.kt @@ -1,21 +1,51 @@ package com.ediposouza.teslesgendstracker.ui.decks.tabs import android.os.Bundle +import android.text.TextUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.ediposouza.teslesgendstracker.R -import com.ediposouza.teslesgendstracker.data.Class -import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor +import com.ediposouza.teslesgendstracker.interactor.FirebaseParsers +import com.ediposouza.teslesgendstracker.ui.base.BaseAdsFirebaseAdapter import com.ediposouza.teslesgendstracker.util.inflate -import timber.log.Timber +import kotlinx.android.synthetic.main.fragment_decks_list.* /** * Created by EdipoSouza on 11/18/16. */ class DecksFavoritedFragment : DecksPublicFragment() { - private val privateInteractor = PrivateInteractor() + override val dataRef = { + privateInteractor.getUserFavoriteDecksRef() + } + + private val dataFilter: (FirebaseParsers.DeckFavoriteParser) -> Boolean = { + currentClasses.map { it.ordinal }.contains(it.cls) && + it.name.toLowerCase().trim().contains(searchFilter ?: "") + } + + override val decksAdapter: BaseAdsFirebaseAdapter by lazy { + object : BaseAdsFirebaseAdapter( + FirebaseParsers.DeckFavoriteParser::class.java, dataRef, DECK_PAGE_SIZE, ADS_EACH_ITEMS, + R.layout.itemlist_deck_ads, false, dataFilter) { + + override fun onCreateDefaultViewHolder(parent: ViewGroup): DecksAllViewHolder { + return DecksAllViewHolder(parent.inflate(R.layout.itemlist_deck), itemClick, itemLongClick) + } + + override fun onBindContentHolder(itemKey: String, model: FirebaseParsers.DeckFavoriteParser, viewHolder: DecksAllViewHolder) { + if (!TextUtils.isEmpty(itemKey)) { + viewHolder.bind(itemKey, publicInteractor, privateInteractor) + } + } + + override fun onSyncEnd() { + decks_refresh_layout?.isRefreshing = false + } + + } + } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { return container?.inflate(R.layout.fragment_decks_list) @@ -26,11 +56,4 @@ class DecksFavoritedFragment : DecksPublicFragment() { configLoggedViews() } - override fun getDecks(cls: Class?, last: Boolean) { - privateInteractor.getFavoriteDecks(cls, { - it?.forEach { Timber.d("Public: %s", it.toString()) } - decksAdapter.showDecks(it ?: listOf(), last) - }) - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksOwnerFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksOwnerFragment.kt index fc2615a..aa0d7e0 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksOwnerFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksOwnerFragment.kt @@ -2,23 +2,23 @@ package com.ediposouza.teslesgendstracker.ui.decks.tabs import android.os.Bundle import android.view.* -import android.widget.Switch +import android.widget.CompoundButton import com.ediposouza.teslesgendstracker.R -import com.ediposouza.teslesgendstracker.data.Class -import com.ediposouza.teslesgendstracker.data.Deck -import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor import com.ediposouza.teslesgendstracker.util.inflate -import timber.log.Timber /** * Created by EdipoSouza on 11/18/16. */ class DecksOwnerFragment : DecksPublicFragment() { - override val isDeckOwned: Boolean = true + private var onlyPrivate: CompoundButton? = null - private val privateInteractor = PrivateInteractor() - private var onlyPrivate: Switch? = null + override val isDeckPrivate: Boolean + get() = onlyPrivate?.isChecked ?: false + + override val dataRef = { + if (isDeckPrivate) privateInteractor.getUserPrivateDecksRef() else privateInteractor.getUserPublicDecksRef() + } override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { return container?.inflate(R.layout.fragment_decks_list_owner) @@ -32,18 +32,13 @@ class DecksOwnerFragment : DecksPublicFragment() { override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { menu?.clear() - inflater?.inflate(R.menu.menu_decks_owned, menu) - onlyPrivate = menu?.findItem(R.id.menu_only_private)?.actionView as Switch - onlyPrivate?.setOnCheckedChangeListener { button, checked -> showDecks() } + inflater?.inflate(R.menu.menu_private, menu) + inflater?.inflate(R.menu.menu_search, menu) + onlyPrivate = menu?.findItem(R.id.menu_only_private)?.actionView as CompoundButton + onlyPrivate?.setOnCheckedChangeListener { button, checked -> + showDecks() + } super.onCreateOptionsMenu(menu, inflater) } - override fun getDecks(cls: Class?, last: Boolean) { - privateInteractor.getOwnedDecks(cls, { - val decksToShow = if (onlyPrivate?.isChecked ?: false) it.filter(Deck::private) else it - decksToShow.forEach { Timber.d("Decks: %s", it.toString()) } - decksAdapter.showDecks(decksToShow, last) - }) - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksPublicFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksPublicFragment.kt index 1472e11..38da640 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksPublicFragment.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/tabs/DecksPublicFragment.kt @@ -1,114 +1,149 @@ package com.ediposouza.teslesgendstracker.ui.decks.tabs -import android.app.Activity -import android.content.Intent import android.os.Bundle -import android.support.annotation.LayoutRes import android.support.v4.app.ActivityOptionsCompat import android.support.v4.util.Pair -import android.support.v7.util.DiffUtil import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.RelativeLayout import com.ediposouza.teslesgendstracker.App import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.data.Class import com.ediposouza.teslesgendstracker.data.Deck +import com.ediposouza.teslesgendstracker.interactor.FirebaseParsers import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor import com.ediposouza.teslesgendstracker.interactor.PublicInteractor -import com.ediposouza.teslesgendstracker.ui.DeckActivity import com.ediposouza.teslesgendstracker.ui.base.* -import com.ediposouza.teslesgendstracker.ui.util.SimpleDiffCallback +import com.ediposouza.teslesgendstracker.ui.cards.CmdFilterSearch +import com.ediposouza.teslesgendstracker.ui.decks.DeckActivity +import com.ediposouza.teslesgendstracker.ui.util.firebase.OnLinearLayoutItemScrolled import com.ediposouza.teslesgendstracker.util.inflate import com.google.firebase.auth.FirebaseAuth import jp.wasabeef.recyclerview.animators.SlideInLeftAnimator import kotlinx.android.synthetic.main.fragment_decks_list.* import kotlinx.android.synthetic.main.include_login_button.* import kotlinx.android.synthetic.main.itemlist_deck.view.* -import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import timber.log.Timber import java.text.NumberFormat -import java.util.* /** * Created by EdipoSouza on 11/18/16. */ open class DecksPublicFragment : BaseFragment() { - val ADS_EACH_ITEMS = 15 //after 15 lines - val RC_DECK = 123 + val ADS_EACH_ITEMS = 10 //after 10 lines + val DECK_PAGE_SIZE = 8 + protected var searchFilter: String? = null + protected var currentClasses = Class.values() protected val publicInteractor = PublicInteractor() - protected var currentClasses: Array = Class.values() - - val nameTransitionName: String by lazy { getString(R.string.deck_name_transition_name) } - val coverTransitionName: String by lazy { getString(R.string.deck_cover_transition_name) } - val attr1TransitionName: String by lazy { getString(R.string.deck_attr1_transition_name) } - val attr2TransitionName: String by lazy { getString(R.string.deck_attr2_transition_name) } - - open protected val isDeckOwned: Boolean = false - - protected val decksAdapter = DecksAllAdapter(ADS_EACH_ITEMS, R.layout.itemlist_deck_ads, - { view: View, deck: Deck -> - PrivateInteractor().getFavoriteDecks(deck.cls) { - val favorite = it?.filter { it.id == deck.id }?.isNotEmpty() ?: false - val like = deck.likes.contains(FirebaseAuth.getInstance().currentUser?.uid) - startActivityForResult(DeckActivity.newIntent(context, deck, favorite, like, isDeckOwned), - RC_DECK, ActivityOptionsCompat.makeSceneTransitionAnimation(activity, + protected val privateInteractor = PrivateInteractor() + + private val nameTransitionName: String by lazy { getString(R.string.deck_name_transition_name) } + private val coverTransitionName: String by lazy { getString(R.string.deck_cover_transition_name) } + private val attr1TransitionName: String by lazy { getString(R.string.deck_attr1_transition_name) } + private val attr2TransitionName: String by lazy { getString(R.string.deck_attr2_transition_name) } + + open protected val isDeckPrivate: Boolean = false + + open protected val dataRef = { + publicInteractor.getPublicDecksRef() + } + + private val dataFilter: (FirebaseParsers.DeckParser) -> Boolean = { + currentClasses.map { it.ordinal }.contains(it.cls) && + it.name.toLowerCase().trim().contains(searchFilter ?: "") + } + + val itemClick = { view: View, deck: Deck -> + PrivateInteractor().getUserFavoriteDecks(deck.cls) { + val favorite = it?.filter { it.uuid == deck.uuid }?.isNotEmpty() ?: false + val userId = FirebaseAuth.getInstance().currentUser?.uid + val like = deck.likes.contains(userId) + startActivity(DeckActivity.newIntent(context, deck, favorite, like, deck.owner == userId), + ActivityOptionsCompat.makeSceneTransitionAnimation(activity, Pair(view.deck_name as View, nameTransitionName), Pair(view.deck_cover as View, coverTransitionName), Pair(view.deck_attr1 as View, attr1TransitionName), Pair(view.deck_attr2 as View, attr2TransitionName)).toBundle()) - } - }) { + } + } + + val itemLongClick = { view: View, deck: Deck -> true } + open protected val decksAdapter: BaseAdsFirebaseAdapter<*, DecksAllViewHolder> by lazy { + object : BaseAdsFirebaseAdapter( + FirebaseParsers.DeckParser::class.java, dataRef, DECK_PAGE_SIZE, ADS_EACH_ITEMS, + R.layout.itemlist_deck_ads, false, dataFilter) { + + override fun onCreateDefaultViewHolder(parent: ViewGroup): DecksAllViewHolder { + return DecksAllViewHolder(parent.inflate(R.layout.itemlist_deck), itemClick, itemLongClick) + } + + override fun onBindContentHolder(itemKey: String, model: FirebaseParsers.DeckParser, viewHolder: DecksAllViewHolder) { + viewHolder.bind(model.toDeck(itemKey, isDeckPrivate), privateInteractor) + } + + override fun onSyncEnd() { + decks_refresh_layout?.isRefreshing = false + } + + } + } + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { return container?.inflate(R.layout.fragment_decks_list) } override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - decks_recycler_view.adapter = decksAdapter - decks_recycler_view.itemAnimator = SlideInLeftAnimator() - decks_recycler_view.layoutManager = object : LinearLayoutManager(context) { - override fun supportsPredictiveItemAnimations(): Boolean = false + with(decks_recycler_view) { + adapter = decksAdapter + itemAnimator = SlideInLeftAnimator() + layoutManager = object : LinearLayoutManager(context) { + override fun supportsPredictiveItemAnimations(): Boolean = false + } + addOnScrollListener(OnLinearLayoutItemScrolled(decksAdapter.getContentCount() - 3) { + view?.post { decksAdapter.more() } + }) } decks_refresh_layout.setOnRefreshListener { - decks_refresh_layout.isRefreshing = false - showDecks() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == RC_DECK && resultCode == Activity.RESULT_OK) { - showDecks() + decksAdapter.reset() } } fun configLoggedViews() { - signin_button.setOnClickListener { EventBus.getDefault().post(CmdShowLogin()) } + signin_button.setOnClickListener { showLogin() } signin_button.visibility = if (App.hasUserLogged()) View.INVISIBLE else View.VISIBLE decks_recycler_view.visibility = if (App.hasUserLogged()) View.VISIBLE else View.INVISIBLE } @Subscribe + @Suppress("UNUSED_PARAMETER") fun onCmdLoginSuccess(cmdLoginSuccess: CmdLoginSuccess) { configLoggedViews() showDecks() } @Subscribe + @Suppress("UNUSED_PARAMETER") fun onCmdUpdateDeckAndShowDeck(cmdUpdateDeckAndShowDeck: CmdUpdateDeckAndShowDeck) { showDecks() } + @Subscribe + fun onCmdFilterSearch(filterSearch: CmdFilterSearch) { + searchFilter = filterSearch.search?.toLowerCase()?.trim() + decksAdapter.reset() + } + @Subscribe fun onCmdShowDecksByClasses(cmdShowDecksByClasses: CmdShowDecksByClasses) { currentClasses = cmdShowDecksByClasses.classes.toTypedArray() @@ -118,91 +153,68 @@ open class DecksPublicFragment : BaseFragment() { } } - fun showDecks() { - Timber.d("Classes: %s", currentClasses.toSet()) - decksAdapter.clearItems() - for (i in currentClasses.indices) { - getDecks(currentClasses[i], i == currentClasses.size - 1) - } - } - - open fun getDecks(cls: Class?, last: Boolean) { - publicInteractor.getPublicDecks(cls, { - it.forEach { Timber.d("Public: %s", it.toString()) } - decksAdapter.showDecks(it.sortedByDescending(Deck::updatedAt), last) - }) + open fun showDecks() { + decksAdapter.reset() + decksAdapter.notifyDataSetChanged() } - class DecksAllAdapter(adsEachItems: Int, @LayoutRes adsLayout: Int, val itemClick: (View, Deck) -> Unit, - val itemLongClick: (View, Deck) -> Boolean) : BaseAdsAdapter(adsEachItems, adsLayout) { - - val privateInteractor = PrivateInteractor() - - var items: List = listOf() - var newItems: ArrayList = ArrayList() - - override fun onCreateDefaultViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - return DecksAllViewHolder(parent.inflate(R.layout.itemlist_deck), itemClick, itemLongClick) - } - - override fun onBindDefaultViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { - val deck = items[position] - (holder as DecksAllViewHolder).bind(deck, privateInteractor) - } - - override fun getDefaultItemCount(): Int = items.size + class DecksAllViewHolder(view: View, val itemClick: (View, Deck) -> Unit, + val itemLongClick: (View, Deck) -> Boolean) : RecyclerView.ViewHolder(view) { - fun clearItems() { - newItems.clear() - } + constructor(view: View) : this(view, { view, deck -> }, { view, deck -> true }) - fun showDecks(decks: List, last: Boolean) { - newItems.addAll(decks) - if (!last) { - return - } - Collections.sort(newItems, { d1, d2 -> d2.updatedAt.compareTo(d1.updatedAt) }) - val oldItems = items - items = newItems - if (items.isEmpty() || items.minus(oldItems).isEmpty()) { - notifyDataSetChanged() - return + fun bind(itemKey: String, publicInteractor: PublicInteractor, privateInteractor: PrivateInteractor) { + itemView.deck_loading.visibility = View.VISIBLE + itemView.deck_cover.visibility = View.GONE + itemView.deck_info.visibility = View.GONE + publicInteractor.getPublicDeck(itemKey) { + bind(it, privateInteractor) } - DiffUtil.calculateDiff(SimpleDiffCallback(items, oldItems) { oldItem, newItem -> - oldItem.id == newItem.id - }).dispatchUpdatesTo(this) } - } - - class DecksAllViewHolder(val view: View, val itemClick: (View, Deck) -> Unit, - val itemLongClick: (View, Deck) -> Boolean) : RecyclerView.ViewHolder(view) { - fun bind(deck: Deck, privateInteractor: PrivateInteractor) { - itemView.setOnClickListener { itemClick(itemView, deck) } - itemView.setOnLongClickListener { itemLongClick(itemView, deck) } - itemView.deck_cover.setImageResource(deck.cls.imageRes) - itemView.deck_private.layoutParams.width = if (deck.private) ViewGroup.LayoutParams.WRAP_CONTENT else 0 - itemView.deck_name.text = deck.name - itemView.deck_attr1.setImageResource(deck.cls.attr1.imageRes) - itemView.deck_attr2.setImageResource(deck.cls.attr2.imageRes) - itemView.deck_type.text = deck.type.name.toLowerCase().capitalize() - itemView.deck_date.setCompoundDrawablesWithIntrinsicBounds(if (deck.updates.isEmpty()) - R.drawable.ic_create_at else R.drawable.ic_updated_at, 0, 0, 0) - itemView.deck_date.text = deck.updatedAt.toLocalDate().toString() - val numberInstance = NumberFormat.getNumberInstance() - itemView.deck_soul_cost.text = numberInstance.format(deck.cost) - itemView.deck_comments.text = numberInstance.format(deck.comments.size) - itemView.deck_likes.text = numberInstance.format(deck.likes.size) - itemView.deck_views.text = numberInstance.format(deck.views) - calculateMissingSoul(deck, privateInteractor) + with(itemView) { + deck_loading.visibility = View.GONE + deck_cover.visibility = View.VISIBLE + deck_info.visibility = View.VISIBLE + setOnClickListener { itemClick(itemView, deck) } + setOnLongClickListener { itemLongClick(itemView, deck) } + deck_cover.setImageResource(deck.cls.imageRes) + deck_private.layoutParams.width = if (deck.private) ViewGroup.LayoutParams.WRAP_CONTENT else 0 + deck_name.text = deck.name + deck_attr1.setImageResource(deck.cls.attr1.imageRes) + deck_attr2.setImageResource(deck.cls.attr2.imageRes) + deck_type.text = deck.type.name.toLowerCase().capitalize() + deck_date.text = deck.updatedAt.toLocalDate().toString() + (deck_date.layoutParams as RelativeLayout.LayoutParams).apply { + if (deck.private) { + addRule(RelativeLayout.ALIGN_PARENT_END) + removeRule(RelativeLayout.END_OF) + } else { + addRule(RelativeLayout.END_OF, R.id.deck_center) + removeRule(RelativeLayout.ALIGN_PARENT_END) + } + deck_date.layoutParams = this + } + deck_date.setCompoundDrawablesWithIntrinsicBounds(if (deck.updates.isEmpty()) + R.drawable.ic_create_at else R.drawable.ic_updated_at, 0, 0, 0) + val numberInstance = NumberFormat.getNumberInstance() + deck_soul_cost.text = numberInstance.format(deck.cost) + deck_comments.text = numberInstance.format(deck.comments.size) + deck_comments.visibility = if (deck.private) View.INVISIBLE else View.VISIBLE + deck_likes.text = numberInstance.format(deck.likes.size) + deck_likes.visibility = if (deck.private) View.INVISIBLE else View.VISIBLE + deck_views.text = numberInstance.format(deck.views) + deck_views.visibility = if (deck.private) View.INVISIBLE else View.VISIBLE + calculateMissingSoul(deck, privateInteractor) + } } - fun calculateMissingSoul(deck: Deck, interactor: PrivateInteractor) { + fun calculateMissingSoul(deck: Deck, privateInteractor: PrivateInteractor) { with(itemView.deck_soul_missing) { visibility = View.INVISIBLE itemView.deck_soul_missing_loading.visibility = View.VISIBLE - interactor.getMissingCards(deck, { itemView.deck_soul_missing_loading.visibility = View.VISIBLE }) { + privateInteractor.getDeckMissingCards(deck, { itemView.deck_soul_missing_loading.visibility = View.VISIBLE }) { itemView.deck_soul_missing_loading.visibility = View.GONE val missingSoul = it.map { it.qtd * it.rarity.soulCost }.sum() Timber.d("Missing %d", missingSoul) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/DeckList.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/DeckList.kt index e64e580..9ff0ead 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/DeckList.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/DeckList.kt @@ -16,8 +16,9 @@ import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.data.* import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor import com.ediposouza.teslesgendstracker.interactor.PublicInteractor -import com.ediposouza.teslesgendstracker.ui.CardActivity +import com.ediposouza.teslesgendstracker.ui.cards.CardActivity import com.ediposouza.teslesgendstracker.ui.decks.CmdRemAttr +import com.ediposouza.teslesgendstracker.ui.decks.CmdUpdateCardSlot import com.ediposouza.teslesgendstracker.util.inflate import jp.wasabeef.recyclerview.animators.SlideInLeftAnimator import kotlinx.android.synthetic.main.itemlist_decklist_slot.view.* @@ -77,19 +78,23 @@ class DeckList(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : constructor(ctx: Context?, attrs: AttributeSet) : this(ctx, attrs, 0) - fun showDeck(deck: Deck, showSoulCost: Boolean = true) { + fun showDeck(deck: Deck?, showSoulCost: Boolean = true, showMagikaCosts: Boolean = true, showQtd: Boolean = true) { decklist_soul.visibility = if (showSoulCost) View.VISIBLE else View.GONE - doAsync { - PublicInteractor().getDeckCards(deck) { - context.runOnUiThread { - (decklist_recycle_view.adapter as DeckListAdapter).showDeck(it) - onCardListChange() - } - userFavorites.clear() - PrivateInteractor().getFavoriteCards(null, deck.cls.attr1) { - userFavorites.addAll(it) - PrivateInteractor().getFavoriteCards(null, deck.cls.attr2) { + decklist_costs.visibility = if (showMagikaCosts) View.VISIBLE else View.GONE + decklist_qtd.visibility = if (showQtd) View.VISIBLE else View.GONE + if (deck != null) { + doAsync { + PublicInteractor().getDeckCards(deck) { + context.runOnUiThread { + (decklist_recycle_view.adapter as DeckListAdapter).showDeck(it) + onCardListChange() + } + userFavorites.clear() + PrivateInteractor().getUserFavoriteCards(null, deck.cls.attr1) { userFavorites.addAll(it) + PrivateInteractor().getUserFavoriteCards(null, deck.cls.attr2) { + userFavorites.addAll(it) + } } } } @@ -117,12 +122,12 @@ class DeckList(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : fun getCards(): List = deckListAdapter.getCards() - fun getSoulCost(): Int = getCards().sumBy { (it.card.rarity.soulCost * it.qtd).toInt() } + fun getSoulCost(): Int = getCards().sumBy { it.card.rarity.soulCost * it.qtd } private fun onCardListChange() { val cards = getCards() decklist_costs.updateCosts(cards) - decklist_qtd.text = context.getString(R.string.new_deck_card_list_qtd, cards.sumBy { it.qtd.toInt() }) + decklist_qtd.text = context.getString(R.string.new_deck_card_list_qtd, cards.sumBy { it.qtd }) decklist_soul.text = getSoulCost().toString() } @@ -131,6 +136,7 @@ class DeckList(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : private val items = arrayListOf() private var missingCards: List = listOf() + private val eventBus by lazy { EventBus.getDefault() } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): DeckListViewHolder { return DeckListViewHolder(parent?.inflate(R.layout.itemlist_decklist_slot), itemClick, itemLongClick) @@ -171,6 +177,7 @@ class DeckList(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : onAdd(cardIndex) notifyItemChanged(cardIndex) } + eventBus.post(CmdUpdateCardSlot(items.find { it.card == card } ?: CardSlot(card, 0))) } @@ -194,18 +201,25 @@ class DeckList(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : notifyItemChanged(cardIndex) } } + eventBus.post(CmdUpdateCardSlot(items.find { it.card == card } ?: CardSlot(card, 0))) } private fun notifyCardRemoved(card: Card) { when { card.attr == Attribute.DUAL && items.filter { it.card.attr == card.dualAttr1 }.isEmpty() -> { - EventBus.getDefault().post(CmdRemAttr(card.dualAttr1)) + eventBus.post(CmdRemAttr(card.dualAttr1)) + if (items.isEmpty()) { + eventBus.post(CmdRemAttr(card.dualAttr2)) + } } card.attr == Attribute.DUAL && items.filter { it.card.attr == card.dualAttr2 }.isEmpty() -> { - EventBus.getDefault().post(CmdRemAttr(card.dualAttr2)) + eventBus.post(CmdRemAttr(card.dualAttr2)) + if (items.isEmpty()) { + eventBus.post(CmdRemAttr(card.dualAttr1)) + } } items.filter { it.card.dualAttr1 == card.attr || it.card.dualAttr2 == card.attr }.isEmpty() -> { - EventBus.getDefault().post(CmdRemAttr(card.attr)) + eventBus.post(CmdRemAttr(card.attr)) } } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterAttrLockable.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/FilterAttrLockable.kt similarity index 93% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterAttrLockable.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/FilterAttrLockable.kt index 2f3a470..94c24de 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterAttrLockable.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/decks/widget/FilterAttrLockable.kt @@ -1,4 +1,4 @@ -package com.ediposouza.teslesgendstracker.ui.widget.filter +package com.ediposouza.teslesgendstracker.ui.decks.widget import android.content.Context import android.util.AttributeSet @@ -6,6 +6,7 @@ import android.view.View import android.view.animation.AnimationUtils import com.ediposouza.teslesgendstracker.R import com.ediposouza.teslesgendstracker.data.Attribute +import com.ediposouza.teslesgendstracker.ui.widget.FilterAttr import kotlinx.android.synthetic.main.widget_attributes_filter.view.* /** @@ -83,7 +84,7 @@ class FilterAttrLockable(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) } } - fun lockAttrs(dualAttr1: Attribute, dualAttr2: Attribute) { + fun lockAttrs(dualAttr1: Attribute, dualAttr2: Attribute, reselectBasicAttr: Boolean = true) { if (isLocked()) { return } @@ -91,7 +92,7 @@ class FilterAttrLockable(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) lockAttr(dualAttr2) if (isLocked()) { startAnimLock() - if (isAttrBasic(lastAttrSelected)) { + if (reselectBasicAttr && isAttrBasic(lastAttrSelected)) { selectAttr(dualAttr2, true) } onAttrLock?.invoke(lockAttr1!!, lockAttr2!!) @@ -115,6 +116,11 @@ class FilterAttrLockable(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) } private fun startAnimLock() { + if (lockAttr1!!.ordinal > lockAttr2!!.ordinal) { + val lockAttrTmp = lockAttr1 + lockAttr1 = lockAttr2 + lockAttr2 = lockAttrTmp + } val scaleDownAnimation = AnimationUtils.loadAnimation(context, R.anim.fab_scale_down) scaleDownAnimation.fillAfter = true rootView.attr_filter_strength?.startAnimation(scaleDownAnimation) diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/Commands.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/Commands.kt new file mode 100644 index 0000000..737c23a --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/Commands.kt @@ -0,0 +1,14 @@ +package com.ediposouza.teslesgendstracker.ui.matches + +import com.ediposouza.teslesgendstracker.data.MatchMode +import com.ediposouza.teslesgendstracker.data.Season + +/** + * Created by EdipoSouza on 1/8/17. + */ +class CmdUpdateMatches + +data class CmdFilterMode(val mode: MatchMode) + +data class CmdFilterSeason(val season: Season?) + diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/MatchesFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/MatchesFragment.kt new file mode 100644 index 0000000..67330d1 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/MatchesFragment.kt @@ -0,0 +1,298 @@ +package com.ediposouza.teslesgendstracker.ui.matches + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.support.annotation.IntegerRes +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentStatePagerAdapter +import android.support.v4.content.ContextCompat +import android.support.v4.view.ViewPager +import android.support.v7.app.AlertDialog +import android.view.* +import android.widget.AdapterView +import android.widget.ArrayAdapter +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.data.* +import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor +import com.ediposouza.teslesgendstracker.interactor.PublicInteractor +import com.ediposouza.teslesgendstracker.ui.base.BaseFragment +import com.ediposouza.teslesgendstracker.ui.base.CmdShowSnackbarMsg +import com.ediposouza.teslesgendstracker.ui.base.CmdUpdateTitle +import com.ediposouza.teslesgendstracker.ui.base.CmdUpdateVisibility +import com.ediposouza.teslesgendstracker.ui.matches.tabs.MatchesHistoryFragment +import com.ediposouza.teslesgendstracker.ui.matches.tabs.MatchesStatisticsFragment +import com.ediposouza.teslesgendstracker.util.* +import kotlinx.android.synthetic.main.activity_dash.* +import kotlinx.android.synthetic.main.dialog_new_match.view.* +import kotlinx.android.synthetic.main.fragment_matches.* +import kotlinx.android.synthetic.main.itemlist_class.view.* +import org.greenrobot.eventbus.Subscribe +import org.jetbrains.anko.itemsSequence + +/** + * Created by EdipoSouza on 1/3/17. + */ +class MatchesFragment : BaseFragment() { + + private val KEY_PAGE_VIEW_POSITION = "pageViewPositionKey" + private val RC_NEW_MATCHES = 145 + + private var seasons: List = listOf() + private var menuSeasons: SubMenu? = null + + val pageChange = object : ViewPager.SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + updateActivityTitle(position) + if (position == 0) { + matches_fab_add.show() + } + MetricsManager.trackScreen(when (position) { + 0 -> MetricScreen.SCREEN_MATCHES_STATISTICS() + else -> MetricScreen.SCREEN_MATCHES_HISTORY() + }) + } + + } + + private fun updateActivityTitle(position: Int) { + val title = when (position) { + 0 -> R.string.title_tab_matches_statistics + else -> R.string.title_tab_matches_history + } + eventBus.post(CmdUpdateTitle(title)) + } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return container?.inflate(R.layout.fragment_matches) + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + activity.dash_navigation_view.setCheckedItem(R.id.menu_matches) + matches_view_pager.adapter = MatchesPageAdapter(context, childFragmentManager) + matches_view_pager.addOnPageChangeListener(pageChange) + matches_nav_mode.setOnNavigationItemSelectedListener { + when (it.itemId) { + R.id.tab_mode_ranked -> filterMode(MatchMode.RANKED) + R.id.tab_mode_casual -> filterMode(MatchMode.CASUAL) + R.id.tab_mode_arena -> filterMode(MatchMode.ARENA) + } + true + } + matches_fab_add.setOnClickListener { showNewMatchDialog() } + MetricsManager.trackScreen(MetricScreen.SCREEN_MATCHES_STATISTICS()) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + eventBus.post(CmdUpdateTitle(R.string.title_tab_matches_statistics)) + matches_tab_layout.setupWithViewPager(matches_view_pager) + } + + override fun onSaveInstanceState(outState: Bundle?) { + outState?.apply { putInt(KEY_PAGE_VIEW_POSITION, matches_view_pager?.currentItem ?: 0) } + super.onSaveInstanceState(outState) + } + + override fun onViewStateRestored(savedInstanceState: Bundle?) { + super.onViewStateRestored(savedInstanceState) + savedInstanceState?.apply { + matches_view_pager.currentItem = getInt(KEY_PAGE_VIEW_POSITION) + } + } + + override fun onResume() { + super.onResume() + matches_app_bar_layout.setExpanded(true, true) + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + menu?.clear() + inflater?.inflate(R.menu.menu_percent, menu) + inflater?.inflate(R.menu.menu_season, menu) + getSeasons(menu?.findItem(R.id.menu_season)) + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_season_all -> filterSeason(null) + else -> seasons.find { it.id == item?.itemId }?.apply { filterSeason(this) } + } + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == RC_NEW_MATCHES && resultCode == Activity.RESULT_OK) { + eventBus.post(CmdUpdateMatches()) + } + } + + private fun filterMode(mode: MatchMode) { + eventBus.post(CmdFilterMode(mode)) + MetricsManager.trackAction(MetricAction.ACTION_MATCH_STATISTICS_FILTER_MODE(mode)) + } + + private fun filterSeason(season: Season?) { + val seasonId = season?.id ?: R.id.menu_season_all + menuSeasons?.itemsSequence()?.forEach { + it.setIcon(if (it.itemId == seasonId) R.drawable.ic_checked else 0) + } + eventBus.post(CmdFilterSeason(season)) + MetricsManager.trackAction(MetricAction.ACTION_MATCH_STATISTICS_FILTER_SEASON(season)) + } + + private fun getSeasons(menuSeason: MenuItem?) { + menuSeasons = menuSeason?.subMenu + menuSeasons?.apply { + clear() + add(0, R.id.menu_season_all, 0, getString(R.string.matches_seasons_all)).setIcon(R.drawable.ic_checked) + PublicInteractor().getSeasons { + seasons = it.reversed() + seasons.forEach { + add(0, it.id, 0, it.desc) + } + } + } + } + + private fun showNewMatchDialog() { + val dialogView = View.inflate(context, R.layout.dialog_new_match, null) + var decks = listOf(null) + val classClickListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) { + PrivateInteractor().getUserDecks(Class.values()[pos]) { myDecks -> + decks = (myDecks as List).sortedBy { it?.name }.plusElement(null) + dialogView.new_match_dialog_deck_spinner.adapter = ArrayAdapter(context, + android.R.layout.simple_spinner_dropdown_item, + decks.map { it?.name ?: getString(R.string.new_match_dialog_deck_other) }) + } + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + } + } + val deckClickListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { + val lastItem = pos == parent.count - 1 + dialogView.new_match_dialog_deck_info.visibility = if (lastItem) View.VISIBLE else View.GONE + dialogView.new_match_dialog_deck_name.setText(decks[pos]?.name ?: "") + dialogView.new_match_dialog_deck_type_spinner.setSelection(decks[pos]?.type?.ordinal ?: 0) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + } + } + AlertDialog.Builder(context) + .setView(dialogView) + .setPositiveButton(R.string.new_match_dialog_start, { dialog, which -> + val deckPosition = dialogView.new_match_dialog_deck_spinner.selectedItemPosition + val name = dialogView.new_match_dialog_deck_name.text.toString() + val cls = Class.values()[dialogView.new_match_dialog_class_spinner.selectedItemPosition] + val type = DeckType.values()[dialogView.new_match_dialog_deck_type_spinner.selectedItemPosition] + val deck = if (deckPosition >= 0) decks[deckPosition] else null + val mode = MatchMode.values()[dialogView.new_match_dialog_mode_spinner.selectedItemPosition] + if (name.length < 5) { + eventBus.post(CmdShowSnackbarMsg(CmdShowSnackbarMsg.TYPE_ERROR, R.string.new_match_dialog_start_error_name)) + } else { + startActivityForResult(NewMatchesActivity.newIntent(context, name, cls, type, mode, deck), RC_NEW_MATCHES) + } + MetricsManager.trackAction(MetricAction.ACTION_NEW_MATCH_START_WITH(deck)) + }) + .setNegativeButton(android.R.string.cancel, { dialog, which -> }) + .create() + .apply { + setOnShowListener { + dialogView.new_match_dialog_class_spinner.apply { + adapter = ClassAdapter(context) + onItemSelectedListener = classClickListener + limitHeight() + } + dialogView.new_match_dialog_deck_spinner.apply { + onItemSelectedListener = deckClickListener + if (decks.size >= 5) { + limitHeight() + } + } + dialogView.new_match_dialog_deck_type_spinner.apply { + adapter = ArrayAdapter(context, + android.R.layout.simple_spinner_dropdown_item, + DeckType.values().filter { it != DeckType.ARENA } + .map { it.name.toLowerCase().capitalize() }) + limitHeight(4) + } + dialogView.new_match_dialog_mode_spinner.adapter = ArrayAdapter(context, + android.R.layout.simple_spinner_dropdown_item, + MatchMode.values().map { it.name.toLowerCase().capitalize() }) + } + show() + } + } + + @Subscribe + fun onCmdUpdateVisibility(update: CmdUpdateVisibility) { + if (update.show) { + matches_fab_add.show() + } else { + matches_fab_add.hide() + } + } + + class MatchesPageAdapter(ctx: Context, fm: FragmentManager) : FragmentStatePagerAdapter(fm) { + + var titles: Array = ctx.resources.getStringArray(R.array.matches_tabs) + val matchesStatisticsFragment by lazy { MatchesStatisticsFragment() } + val matchesHistoryFragment by lazy { MatchesHistoryFragment() } + + override fun getItem(position: Int): Fragment { + return when (position) { + 0 -> matchesStatisticsFragment + else -> matchesHistoryFragment + } + } + + override fun getCount(): Int { + return titles.size + } + + override fun getPageTitle(position: Int): CharSequence { + return titles[position] + } + + } + + class ClassAdapter(ctx: Context, @IntegerRes layout: Int = R.layout.itemlist_class, + @IntegerRes val textColor: Int = R.color.primary_text_dark) : + ArrayAdapter(ctx, layout, R.id.class_name, Class.values()) { + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup?): View { + return super.getDropDownView(position, convertView, parent).apply { + with(getItem(position)) { + class_attr1.setImageResource(attr1.imageRes) + class_attr2.setImageResource(attr2.imageRes) + class_attr2.visibility = if (attr2 != Attribute.NEUTRAL) View.VISIBLE else View.GONE + class_name.text = name.toLowerCase().capitalize() + } + } + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + return super.getView(position, convertView, parent).apply { + with(getItem(position)) { + class_attr1.setImageResource(attr1.imageRes) + class_attr2.setImageResource(attr2.imageRes) + class_attr2.visibility = if (attr2 != Attribute.NEUTRAL) View.VISIBLE else View.GONE + class_name.text = name.toLowerCase().capitalize() + class_name.setTextColor(ContextCompat.getColor(context, textColor)) + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/MatchesStatisticsClassActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/MatchesStatisticsClassActivity.kt new file mode 100644 index 0000000..cb8efe7 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/MatchesStatisticsClassActivity.kt @@ -0,0 +1,316 @@ +package com.ediposouza.teslesgendstracker.ui.matches + +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.support.design.widget.CollapsingToolbarLayout +import android.support.v4.app.ActivityCompat +import android.view.* +import android.widget.CompoundButton +import android.widget.FrameLayout +import android.widget.RelativeLayout +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.data.* +import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor +import com.ediposouza.teslesgendstracker.interactor.PublicInteractor +import com.ediposouza.teslesgendstracker.ui.base.BaseActivity +import com.ediposouza.teslesgendstracker.util.MetricAction +import com.ediposouza.teslesgendstracker.util.MetricScreen +import com.ediposouza.teslesgendstracker.util.MetricsManager +import com.ediposouza.teslesgendstracker.util.load +import kotlinx.android.synthetic.main.activity_matches_statistics_class.* +import kotlinx.android.synthetic.main.itemcell_class.view.* +import kotlinx.android.synthetic.main.itemcell_text.view.* +import miguelbcr.ui.tableFixHeadesWrapper.TableFixHeaderAdapter +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.intentFor +import org.jetbrains.anko.itemsSequence +import org.jetbrains.anko.uiThread +import java.util.* + +/** + * Created by EdipoSouza on 1/3/17. + */ +class MatchesStatisticsClassActivity : BaseActivity() { + + companion object { + + private val EXTRA_CLASS = "classExtra" + private val EXTRA_SEASON_ID = "SeasonIdExtra" + private val EXTRA_MATCH_MODE = "MatchModeExtra" + + fun newIntent(context: Context, mode: MatchMode, season: Season?, cls: Class): Intent { + return context.intentFor( + EXTRA_MATCH_MODE to mode.ordinal, + EXTRA_SEASON_ID to (season?.id ?: 0), + EXTRA_CLASS to cls.ordinal) + } + + } + + private val HEADER_FIRST by lazy { getString(R.string.match_vs) } + + private val playerClass by lazy { Class.values()[intent.getIntExtra(EXTRA_CLASS, 0)] } + private val matchMode by lazy { MatchMode.values()[intent.getIntExtra(EXTRA_MATCH_MODE, 0)] } + private var seasons = listOf() + private var currentSeason: Season? = null + private var menuSeasons: SubMenu? = null + private var showPercent: CompoundButton? = null + + var statisticsClassTableAdapter: StatisticsTableAdapter? = null + var results: HashMap> = HashMap() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_matches_statistics_class) + val statusBarHeight = resources.getDimensionPixelSize(R.dimen.status_bar_height) + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + val coverLP = statistics_class_cover.layoutParams as RelativeLayout.LayoutParams + coverLP.height = coverLP.height - statusBarHeight + statistics_class_cover.layoutParams = coverLP + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + val layoutParams = toolbar.layoutParams as CollapsingToolbarLayout.LayoutParams + layoutParams.topMargin = statusBarHeight + toolbar.layoutParams = layoutParams + } + } + + @Suppress("unchecked_cast") + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + statisticsClassTableAdapter = StatisticsTableAdapter(this).apply { + setFirstHeader(HEADER_FIRST) + val classTotal: Class? = null + header = Class.values().asList().plus(classTotal) + setFirstBody(Class.values().map { listOf(BodyItem(cls = it)) }) + loadingStatisticsData(this) + setSection(listOf()) + } + matches_statistics_class_table.adapter = statisticsClassTableAdapter + with(playerClass) { + statistics_class_name.text = getString(R.string.matches_class_title, name.toLowerCase().capitalize()) + statistics_class_cover.setImageResource(imageRes) + statistics_class_attr1.setImageResource(attr1.imageRes) + statistics_class_attr2.setImageResource(attr2.imageRes) + } + matches_statistics_class_ads_view.load() + MetricsManager.trackScreen(MetricScreen.SCREEN_MATCHES_STATISTICS_CLASS()) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_percent, menu) + menuInflater?.inflate(R.menu.menu_season, menu) + getSeasons(menu?.findItem(R.id.menu_season)) + val menuPercent = menu?.findItem(R.id.menu_percent) + showPercent = menuPercent?.actionView as CompoundButton + showPercent?.setOnCheckedChangeListener { button, checked -> + updateStatisticsData() + MetricsManager.trackAction(MetricAction.ACTION_MATCH_STATISTICS_CLASS_WIN_RATE(checked)) + } + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + android.R.id.home -> { + ActivityCompat.finishAfterTransition(this) + return true + } + R.id.menu_season_all -> filterSeason(null) + else -> seasons.find { it.id == item?.itemId }?.apply { filterSeason(this) } + } + return super.onOptionsItemSelected(item) + } + + private fun filterSeason(season: Season?) { + currentSeason = season + val seasonId = season?.id ?: R.id.menu_season_all + menuSeasons?.itemsSequence()?.forEach { + it.setIcon(if (it.itemId == seasonId) R.drawable.ic_checked else 0) + } + getMatches() + MetricsManager.trackAction(MetricAction.ACTION_MATCH_STATISTICS_CLASS_FILTER_SEASON(season)) + } + + private fun getSeasons(menuSeason: MenuItem?) { + menuSeasons = menuSeason?.subMenu + menuSeasons?.apply { + clear() + add(0, R.id.menu_season_all, 0, getString(R.string.matches_seasons_all)).setIcon(R.drawable.ic_checked) + PublicInteractor().getSeasons { + seasons = it.reversed() + seasons.forEach { + add(0, it.id, 0, it.desc) + } + currentSeason = seasons.find { it.id == intent.getIntExtra(EXTRA_SEASON_ID, 0) } + filterSeason(currentSeason) + } + } + } + + private fun getMatches() { + loadingStatisticsData() + PrivateInteractor().getUserMatches(currentSeason) { + results.clear() + val classMatches = it.filter { it.player.cls == playerClass } + classMatches.filter { it.mode == matchMode }.groupBy { it.player }.forEach { + results.put(it.key, it.value as ArrayList) + } + updateStatisticsData() + } + } + + private fun loadingStatisticsData(tableAdapter: StatisticsTableAdapter? = statisticsClassTableAdapter) { + tableAdapter?.setFirstBody(Class.values().map { listOf(BodyItem(cls = it)) }) + tableAdapter?.body = mutableListOf>().apply { + Class.values().forEach { myCls -> + add(mutableListOf().apply { + Class.values().forEach { opponentCls -> + add(BodyItem()) + } + add(BodyItem()) + }) + } + } + } + + private fun updateStatisticsData() { + doAsync { + val data = mutableListOf>().apply { + results.forEach { myDeck, matches -> + add(mutableListOf().apply { + Class.values().forEach { opponentCls -> + val matchesVsOpponent = matches.filter { it.opponent.cls == opponentCls } + add(getResultBodyItem(matchesVsOpponent)) + } + add(getResultBodyItem(matches)) + }) + } + } + uiThread { + statisticsClassTableAdapter?.setFirstBody(results.map { listOf(BodyItem(it.key.name)) }) + statisticsClassTableAdapter?.body = data + val allMatches = results.flatMap { it.value } + val wins = allMatches.filter { it.win }.size + val losses = allMatches.filter { !it.win }.size + val winRate = calcWinRate(wins.toFloat(), losses.toFloat()) + matches_statistics_games.text = allMatches.size.toString() + matches_statistics_wins.text = wins.toString() + matches_statistics_losses.text = losses.toString() + matches_statistics_win_rate.text = getString(R.string.match_statistics_percent, + if (winRate < 0) 0f else winRate) + } + } + } + + private fun getResultBodyItem(matches: List): BodyItem { + val result = matches.groupBy { it.win } + val wins = result[true]?.size ?: 0 + val losses = result[false]?.size ?: 0 + return BodyItem(if (!(showPercent?.isChecked ?: false)) "$wins/$losses" else + getString(R.string.match_statistics_percent, calcWinRate(wins.toFloat(), losses.toFloat()))) + } + + private fun calcWinRate(wins: Float, losses: Float): Float { + val total = (wins + losses) + return if (total == 0f) -1f else 100 / total * wins + } + + class BodyItem(val result: String? = null, val cls: Class? = null) + + class StatisticsTableAdapter(val context: Context) : TableFixHeaderAdapter, CellTextCenter, CellTextCenter, CellTextCenter>(context) { + + override fun inflateFirstHeader() = CellTextCenter(context) + + override fun inflateHeader() = CellClass(context) + + override fun inflateFirstBody() = CellTextCenter(context) + + override fun inflateBody() = CellTextCenter(context) + + override fun inflateSection() = CellTextCenter(context) + + override fun getHeaderHeight() = context.resources.getDimensionPixelSize(R.dimen.match_statistics_cell_height) + + override fun getHeaderWidths(): List { + val headerWidth = context.resources.getDimensionPixelSize(R.dimen.match_statistics_class_header_width) + val cellWidth = context.resources.getDimensionPixelSize(R.dimen.match_statistics_cell_width) + val colWidths = mutableListOf(headerWidth) + Class.values().forEach { colWidths.add(cellWidth) } + colWidths.add(headerWidth) + return colWidths + } + + override fun getBodyHeight() = context.resources.getDimensionPixelSize(R.dimen.match_statistics_cell_height) + + override fun isSection(items: List>?, row: Int): Boolean = false + + override fun getSectionHeight() = 0 + + } + + class CellClass(context: Context) : FrameLayout(context), + TableFixHeaderAdapter.HeaderBinder { + + init { + LayoutInflater.from(context).inflate(R.layout.itemcell_class, this, true) + } + + override fun bindHeader(cls: Class?, col: Int) { + bindClass(cls) + } + + private fun bindClass(cls: Class?) { + with(rootView) { + val attr1Visibility = if (cls != null) View.VISIBLE else View.GONE + val attr2Visibility = if (cls?.attr2 != Attribute.NEUTRAL) attr1Visibility else View.GONE + cell_class_attr1.visibility = attr1Visibility + cell_class_attr2.visibility = attr2Visibility + cell_class_attr1.setImageResource(cls?.attr1?.imageRes ?: 0) + cell_class_attr2.setImageResource(cls?.attr2?.imageRes ?: 0) + cell_total.visibility = if (cls == null) View.VISIBLE else View.GONE + } + } + + } + + class CellTextCenter(context: Context) : FrameLayout(context), + TableFixHeaderAdapter.FirstHeaderBinder, + TableFixHeaderAdapter.FirstBodyBinder>, + TableFixHeaderAdapter.BodyBinder>, + TableFixHeaderAdapter.SectionBinder> { + + init { + LayoutInflater.from(context).inflate(R.layout.itemcell_text, this, true) + } + + override fun bindFirstHeader(result: String) { + bindResult(result) + } + + override fun bindFirstBody(bodyItems: List, row: Int) { + bindResult(bodyItems[0].result) + } + + override fun bindBody(bodyItems: List, row: Int, col: Int) { + bindResult(bodyItems[col].result) + } + + private fun bindResult(result: String?) { + with(rootView) { + cell_text.text = if (result == "0/0" || result?.contains("-") ?: false) "-" else result + cell_text.visibility = if (result == null) View.GONE else View.VISIBLE + cell_progress.visibility = if (result == null) View.VISIBLE else View.GONE + } + } + + override fun bindSection(bodyItems: List, row: Int, col: Int) { + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/NewMatchesActivity.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/NewMatchesActivity.kt new file mode 100644 index 0000000..9065830 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/NewMatchesActivity.kt @@ -0,0 +1,205 @@ +package com.ediposouza.teslesgendstracker.ui.matches + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.support.design.widget.CollapsingToolbarLayout +import android.support.v4.app.ActivityCompat +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.RelativeLayout +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.SEASON_UUID_PATTERN +import com.ediposouza.teslesgendstracker.data.* +import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor +import com.ediposouza.teslesgendstracker.ui.base.BaseActivity +import com.ediposouza.teslesgendstracker.ui.matches.tabs.MatchesHistoryFragment +import com.ediposouza.teslesgendstracker.util.* +import jp.wasabeef.recyclerview.animators.SlideInLeftAnimator +import kotlinx.android.synthetic.main.activity_new_matches.* +import kotlinx.android.synthetic.main.include_new_matches.* +import org.jetbrains.anko.intentFor +import org.jetbrains.anko.toast +import org.threeten.bp.LocalDate +import org.threeten.bp.LocalDateTime +import org.threeten.bp.format.DateTimeFormatter + +class NewMatchesActivity : BaseActivity() { + + companion object { + + private val EXTRA_DECK_NAME = "className" + private val EXTRA_DECK_CLASS = "classExtra" + private val EXTRA_DECK_TYPE = "classType" + private val EXTRA_DECK = "deckExtra" + private val EXTRA_MATCH_MODE = "modeExtra" + + fun newIntent(context: Context, deckName: String, deckCls: Class, deckType: DeckType, mode: MatchMode, deck: Deck?): Intent { + return context.intentFor( + EXTRA_DECK_NAME to deckName, + EXTRA_DECK_CLASS to deckCls.ordinal, + EXTRA_DECK_TYPE to deckType.ordinal, + EXTRA_MATCH_MODE to mode.ordinal).apply { + if (deck != null) { + putExtra(EXTRA_DECK, deck) + } + } + } + + } + + private val privateInteractor by lazy { PrivateInteractor() } + private val deckName by lazy { intent.getStringExtra(EXTRA_DECK_NAME) } + private val deckCls by lazy { Class.values()[intent.getIntExtra(EXTRA_DECK_CLASS, 0)] } + private val deckType by lazy { DeckType.values()[intent.getIntExtra(EXTRA_DECK_TYPE, 0)] } + private val mode by lazy { MatchMode.values()[intent.getIntExtra(EXTRA_MATCH_MODE, 0)] } + private val deck: Deck? by lazy { + if (intent.hasExtra(EXTRA_DECK)) intent.getParcelableExtra(EXTRA_DECK) else null + } + + private val matchesAddedAdapter = object : RecyclerView.Adapter() { + + val items = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchesHistoryFragment.MatchViewHolder { + return MatchesHistoryFragment.MatchViewHolder(parent.inflate(R.layout.itemlist_match_history)) + } + + override fun onBindViewHolder(holder: MatchesHistoryFragment.MatchViewHolder?, position: Int) { + holder?.bind(items[position], { + items.removeAt(position) + notifyItemRemoved(position) + }) + } + + override fun getItemCount(): Int = items.size + + fun addMatch(match: Match) { + items.add(match) + notifyItemInserted(itemCount - 1) + new_matches_recycler_view.scrollToPosition(itemCount - 1) + } + + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_new_matches) + val statusBarHeight = resources.getDimensionPixelSize(R.dimen.status_bar_height) + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + val coverLP = new_matches_class_cover.layoutParams as RelativeLayout.LayoutParams + coverLP.height = coverLP.height - statusBarHeight + new_matches_class_cover.layoutParams = coverLP + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + val layoutParams = toolbar.layoutParams as CollapsingToolbarLayout.LayoutParams + layoutParams.topMargin = statusBarHeight + toolbar.layoutParams = layoutParams + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + setResult(Activity.RESULT_CANCELED, Intent()) + + configViews() + MetricsManager.trackScreen(MetricScreen.SCREEN_NEW_MATCHES()) + } + + private fun configViews() { + new_matches_deck_class_name.text = deckName + new_matches_class_cover.setImageResource(deckCls.imageRes) + new_matches_class_attr1.setImageResource(deckCls.attr1.imageRes) + new_matches_class_attr2.setImageResource(deckCls.attr2.imageRes) + new_matches_deck_cardlist.editMode = true + new_matches_deck_cardlist.showDeck(deck, false, false, false) + new_matches_deck_cardlist.visibility = if (deck != null) View.VISIBLE else View.GONE + new_matches_space_start.visibility = if (deck != null) View.GONE else View.VISIBLE + new_matches_space_end.visibility = if (deck != null) View.GONE else View.VISIBLE + new_matches_cards_remains.visibility = if (deck != null) View.VISIBLE else View.GONE + new_matches_legend.visibility = if (mode == MatchMode.RANKED) View.VISIBLE else View.GONE + new_matches_rank_label.visibility = if (mode == MatchMode.RANKED) View.VISIBLE else View.GONE + new_matches_rank.visibility = if (mode == MatchMode.RANKED) View.VISIBLE else View.GONE + new_matches_rank.setText(if (mode == MatchMode.RANKED) "" else "0") + new_matches_opt_type_spinner.adapter = ArrayAdapter(this, + R.layout.widget_spinner_white_text, DeckType.values() + .filter { it != DeckType.ARENA }.map { it.name.toLowerCase().capitalize() }).apply { + setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + } + new_matches_opt_class_spinner.apply { + adapter = MatchesFragment.ClassAdapter(context, R.layout.itemlist_new_match_class, R.color.primary_text) + limitHeight(8) + } + new_matches_recycler_view.apply { + adapter = matchesAddedAdapter + itemAnimator = SlideInLeftAnimator() + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) + setHasFixedSize(true) + } + new_matches_win.rippleDuration = 200 + new_matches_win.setOnRippleCompleteListener { addNewMatch(true) } + new_matches_loss.rippleDuration = 200 + new_matches_loss.setOnRippleCompleteListener { addNewMatch(false) } + new_matches_ads_view.load() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.menu_done, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem?): Boolean { + when (item?.itemId) { + android.R.id.home, + R.id.menu_done -> { + ActivityCompat.finishAfterTransition(this) + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun addNewMatch(win: Boolean) { + val myDeckCls = deck?.cls ?: deckCls + val myDeckType = deck?.type ?: deckType + val myDeckUpdates = deck?.updates ?: listOf() + val myDeckVersion = if (myDeckUpdates.isEmpty()) "v1 (${deck?.createdAt})" else + "v${myDeckUpdates.size + 1} (${myDeckUpdates.last().date.toLocalDate()}" + val optDeckName = new_matches_opt_name.text.toString() + val optDeckCls = Class.values()[new_matches_opt_class_spinner.selectedItemPosition] + val optDeckType = DeckType.values()[new_matches_opt_type_spinner.selectedItemPosition] + val currentSeason = LocalDate.now().format(DateTimeFormatter.ofPattern(SEASON_UUID_PATTERN)) + val currentRank = new_matches_rank.text.toString() + if (currentRank.isEmpty()) { + new_matches_rank.requestFocus() + new_matches_rank.error = getString(R.string.new_match_save_rank_error) + return + } + val legendRank = new_matches_legend.isChecked + val newMatch = Match(LocalDateTime.now().withNano(0).toString(), new_match_first.isChecked, + MatchDeck(deck?.name ?: deckName, myDeckCls, myDeckType, deck?.uuid, myDeckVersion), + MatchDeck(optDeckName, optDeckCls, optDeckType), + mode, currentSeason, currentRank.toInt(), legendRank, win) + privateInteractor.saveMatch(newMatch) { + setResult(Activity.RESULT_OK) + new_matches_opt_name.setText("") + new_matches_opt_class_spinner.setSelection(0) + new_matches_opt_type_spinner.setSelection(0) + new_matches_deck_cardlist.showDeck(deck, false, false, false) + matchesAddedAdapter.addMatch(newMatch) + toast(R.string.new_match_saved) + val deckTrackerUsed = new_matches_deck_cardlist.getCards().size < deck?.cards?.size ?: 0 + MetricsManager.trackAction(MetricAction.ACTION_NEW_MATCH_SAVE(myDeckCls, myDeckType, + optDeckCls, optDeckType, mode, currentSeason, legendRank, deckTrackerUsed)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/tabs/MatchesHistoryFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/tabs/MatchesHistoryFragment.kt new file mode 100644 index 0000000..34c9f12 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/tabs/MatchesHistoryFragment.kt @@ -0,0 +1,194 @@ +package com.ediposouza.teslesgendstracker.ui.matches.tabs + +import android.os.Bundle +import android.support.v4.content.ContextCompat +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.view.* +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.data.Match +import com.ediposouza.teslesgendstracker.data.MatchMode +import com.ediposouza.teslesgendstracker.data.Season +import com.ediposouza.teslesgendstracker.interactor.FirebaseParsers +import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor +import com.ediposouza.teslesgendstracker.ui.base.BaseAdsFirebaseAdapter +import com.ediposouza.teslesgendstracker.ui.base.BaseFragment +import com.ediposouza.teslesgendstracker.ui.matches.CmdFilterMode +import com.ediposouza.teslesgendstracker.ui.matches.CmdFilterSeason +import com.ediposouza.teslesgendstracker.ui.matches.CmdUpdateMatches +import com.ediposouza.teslesgendstracker.ui.util.firebase.OnLinearLayoutItemScrolled +import com.ediposouza.teslesgendstracker.util.alertThemed +import com.ediposouza.teslesgendstracker.util.inflate +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersDecoration +import jp.wasabeef.recyclerview.animators.SlideInLeftAnimator +import kotlinx.android.synthetic.main.fragment_matches_history.* +import kotlinx.android.synthetic.main.itemlist_match_history.view.* +import kotlinx.android.synthetic.main.itemlist_match_history_section.view.* +import org.greenrobot.eventbus.Subscribe +import org.threeten.bp.LocalDate +import org.threeten.bp.LocalDateTime +import org.threeten.bp.format.DateTimeFormatter +import org.threeten.bp.format.FormatStyle + +/** + * Created by EdipoSouza on 1/3/17. + */ +class MatchesHistoryFragment : BaseFragment() { + + val ADS_EACH_ITEMS = 20 //after 10 lines + val MATCH_PAGE_SIZE = 15 + + private var currentMatchMode = MatchMode.RANKED + private var currentSeason: Season? = null + + private val dataFilter: (FirebaseParsers.MatchParser) -> Boolean = { + it.mode == currentMatchMode.ordinal && (it.season == currentSeason?.uuid || currentSeason == null) + } + + private val matchesAdapter: BaseAdsFirebaseAdapter by lazy { + object : BaseAdsFirebaseAdapter( + FirebaseParsers.MatchParser::class.java, { PrivateInteractor().getUserMatchesRef() }, + MATCH_PAGE_SIZE, ADS_EACH_ITEMS, R.layout.itemlist_match_history_ads, false, dataFilter), + StickyRecyclerHeadersAdapter { + + override fun onCreateDefaultViewHolder(parent: ViewGroup): MatchViewHolder { + return MatchViewHolder(parent.inflate(R.layout.itemlist_match_history)) + } + + override fun onBindContentHolder(itemKey: String, model: FirebaseParsers.MatchParser, viewHolder: MatchViewHolder) { + viewHolder.bind(model.toMatch(itemKey), { + reset() + matches_recycler_view?.scrollToPosition(0) + }) + } + + override fun onSyncEnd() { + matches_refresh_layout?.isRefreshing = false + } + + override fun onCreateHeaderViewHolder(parent: ViewGroup): MatchViewHolder { + return MatchViewHolder(parent.inflate(R.layout.itemlist_match_history_section)) + } + + override fun onBindHeaderViewHolder(holder: MatchViewHolder?, position: Int) { + if (position <= getContentCount()) { + holder?.bindSection(LocalDateTime.parse(getItemKey(position)).toLocalDate()) + } + } + + override fun getHeaderId(position: Int): Long { + if (getItemViewType(position) != VIEW_TYPE_CONTENT || position > getContentCount()) { + return -1 + } + val date = LocalDateTime.parse(getItemKey(position)).toLocalDate() + return date.year + date.monthValue + date.dayOfMonth.toLong() + } + + }.apply { + registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() { + override fun onChanged() { + sectionDecoration.invalidateHeaders() + } + }) + } + } + + private val sectionDecoration by lazy { StickyRecyclerHeadersDecoration(matchesAdapter as StickyRecyclerHeadersAdapter<*>) } + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return container?.inflate(R.layout.fragment_matches_history) + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + with(matches_recycler_view) { + adapter = matchesAdapter + itemAnimator = SlideInLeftAnimator() + layoutManager = object : LinearLayoutManager(context) { + override fun supportsPredictiveItemAnimations(): Boolean = false + } + addItemDecoration(sectionDecoration) + addOnScrollListener(OnLinearLayoutItemScrolled(matchesAdapter.getContentCount() - 3) { + view?.post { matchesAdapter.more() } + }) + } + matches_refresh_layout.setOnRefreshListener { + matchesAdapter.reset() + } + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + menu?.findItem(R.id.menu_percent)?.isVisible = false + super.onCreateOptionsMenu(menu, inflater) + } + + @Subscribe + fun onFilterMode(cmdFilterMode: CmdFilterMode) { + currentMatchMode = cmdFilterMode.mode + if (isFragmentSelected) { + matchesAdapter.reset() + matches_recycler_view.scrollToPosition(0) + } + } + + @Subscribe + fun onFilterSeason(cmdFilterSeason: CmdFilterSeason) { + currentSeason = cmdFilterSeason.season + if (isFragmentSelected) { + matchesAdapter.reset() + matches_recycler_view.scrollToPosition(0) + } + } + + @Subscribe + fun onUpdateMatches(cmdUpdateMatches: CmdUpdateMatches) { + if (isFragmentSelected) { + matchesAdapter.reset() + } + } + + class MatchViewHolder(view: View) : RecyclerView.ViewHolder(view) { + + fun bind(match: Match, onDelete: () -> Unit) { + with(itemView) { + match_history_first.visibility = if (match.first) View.VISIBLE else View.INVISIBLE + match_history_player_class_attr1.setImageResource(match.player.cls.attr1.imageRes) + match_history_player_class_attr2.setImageResource(match.player.cls.attr2.imageRes) + match_history_opponent_class_attr1.setImageResource(match.opponent.cls.attr1.imageRes) + match_history_opponent_class_attr2.setImageResource(match.opponent.cls.attr2.imageRes) + val resultColor = if (match.win) R.color.green_200 else R.color.red_100 + val resultText = if (match.win) R.string.match_win else R.string.match_loss + match_history_result.setTextColor(ContextCompat.getColor(context, resultColor)) + match_history_result.text = context.getString(resultText) + match_history_legend.visibility = if (match.legend) View.VISIBLE else View.INVISIBLE + val rankText = if (match.legend) R.string.match_rank_legend else R.string.match_rank_normal + match_history_rank.text = context.getString(rankText, match.rank) + match_history_rank.visibility = if (match.mode == MatchMode.RANKED) View.VISIBLE else View.INVISIBLE + match_history_delete.setOnClickListener { + onHistoryClick(itemView, match, onDelete) + } + } + } + + private fun onHistoryClick(view: View, match: Match, onDelete: () -> Unit) { + val opponentClass = match.opponent.cls.name.toLowerCase().capitalize() + val title = view.context.getString(R.string.match_history_delete, opponentClass) + view.context.alertThemed(title, view.context.getString(R.string.confirm_message), R.style.AppDialog) { + positiveButton(android.R.string.yes, { + PrivateInteractor().deleteMatch(match) { + onDelete.invoke() + } + }) + negativeButton(android.R.string.no, { }) + }.show() + } + + fun bindSection(date: LocalDate) { + itemView.match_history_date.text = date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL)) + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/tabs/MatchesStatisticsFragment.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/tabs/MatchesStatisticsFragment.kt new file mode 100644 index 0000000..9e5ac94 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/matches/tabs/MatchesStatisticsFragment.kt @@ -0,0 +1,300 @@ +package com.ediposouza.teslesgendstracker.ui.matches.tabs + +import android.content.Context +import android.os.Bundle +import android.support.v4.app.ActivityOptionsCompat +import android.support.v4.content.ContextCompat +import android.support.v4.util.Pair +import android.view.* +import android.widget.CompoundButton +import android.widget.FrameLayout +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.data.* +import com.ediposouza.teslesgendstracker.interactor.PrivateInteractor +import com.ediposouza.teslesgendstracker.ui.base.BaseFragment +import com.ediposouza.teslesgendstracker.ui.matches.CmdFilterMode +import com.ediposouza.teslesgendstracker.ui.matches.CmdFilterSeason +import com.ediposouza.teslesgendstracker.ui.matches.CmdUpdateMatches +import com.ediposouza.teslesgendstracker.ui.matches.MatchesStatisticsClassActivity +import com.ediposouza.teslesgendstracker.util.MetricAction +import com.ediposouza.teslesgendstracker.util.MetricsManager +import com.ediposouza.teslesgendstracker.util.inflate +import kotlinx.android.synthetic.main.fragment_matches_statistics.* +import kotlinx.android.synthetic.main.itemcell_class.view.* +import kotlinx.android.synthetic.main.itemcell_text.view.* +import miguelbcr.ui.tableFixHeadesWrapper.TableFixHeaderAdapter +import org.greenrobot.eventbus.Subscribe +import org.jetbrains.anko.childrenSequence +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.util.* + +/** + * Created by EdipoSouza on 1/3/17. + */ +class MatchesStatisticsFragment : BaseFragment() { + + private val HEADER_FIRST by lazy { getString(R.string.match_vs) } + private val attr1TransitionName: String by lazy { getString(R.string.deck_attr1_transition_name) } + private val attr2TransitionName: String by lazy { getString(R.string.deck_attr2_transition_name) } + + private var currentMatchMode = MatchMode.RANKED + private var currentSeason: Season? = null + private var selectedClass: Class? = null + private var showPercent: CompoundButton? = null + + var statisticsTableAdapter: StatisticsTableAdapter? = null + var results: HashMap> = HashMap() + + override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return container?.inflate(R.layout.fragment_matches_statistics) + } + + override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setHasOptionsMenu(true) + statisticsTableAdapter = StatisticsTableAdapter(context).apply { + setFirstHeader(HEADER_FIRST) + val classTotal: Class? = null + header = Class.values().asList().plus(classTotal) + setFirstBody(Class.values().map { listOf(BodyItem(cls = it)) }.plus(listOf(listOf(BodyItem())))) + loadingStatisticsData(this) + setSection(listOf()) + setClickListenerFirstBody { rowItems, view, row, col -> selectRow(row) } + setClickListenerBody { rowItems, view, row, col -> selectRow(row) } + } + matches_statistics_table.adapter = statisticsTableAdapter + getMatches() + } + + override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) { + val menuPercent = menu?.findItem(R.id.menu_percent) + menuPercent?.isVisible = true + showPercent = menuPercent?.actionView as CompoundButton + showPercent?.setOnCheckedChangeListener { button, checked -> + updateStatisticsData() + MetricsManager.trackAction(MetricAction.ACTION_MATCH_STATISTICS_WIN_RATE(checked)) + } + super.onCreateOptionsMenu(menu, inflater) + } + + override fun onResume() { + super.onResume() + selectRow(-1) + } + + private fun selectRow(row: Int) { + selectedClass = if (row >= 0 && row < Class.values().size) Class.values()[row] else null + updateStatisticsData() + statisticsTableAdapter?.setFirstBody(Class.values().map { listOf(BodyItem(null, it, it == selectedClass)) } + .plus(listOf(listOf(BodyItem())))) + if (selectedClass != null) { + val classView = matches_statistics_table.childrenSequence() + .filter { + it.getTag(com.inqbarna.tablefixheaders.R.id.tag_row) == row && + it.getTag(com.inqbarna.tablefixheaders.R.id.tag_type_view) == 2 + }.first() + startActivity(MatchesStatisticsClassActivity.newIntent(context, currentMatchMode, currentSeason, + selectedClass!!), ActivityOptionsCompat.makeSceneTransitionAnimation(activity, + Pair(classView.cell_class_attr1 as View, attr1TransitionName), + Pair(classView.cell_class_attr2 as View, attr2TransitionName)).toBundle()) + MetricsManager.trackAction(MetricAction.ACTION_MATCH_STATISTICS_CLASS(selectedClass!!)) + } + } + + private fun getMatches() { + loadingStatisticsData() + PrivateInteractor().getUserMatches(currentSeason) { + it.filter { it.mode == currentMatchMode }.groupBy { it.player.cls }.forEach { + results[it.key]?.addAll(it.value) + } + updateStatisticsData() + } + } + + private fun loadingStatisticsData(tableAdapter: StatisticsTableAdapter? = statisticsTableAdapter) { + results = HashMap(Class.values().map { it to ArrayList() }.toMap()) + tableAdapter?.body = mutableListOf>().apply { + Class.values().forEach { myCls -> + add(mutableListOf().apply { + Class.values().forEach { opponentCls -> + add(BodyItem()) + } + add(BodyItem()) + }) + } + add(mutableListOf().apply { + Class.values().forEach { + add(BodyItem()) + } + add(BodyItem()) + }) + } + } + + private fun updateStatisticsData() { + doAsync { + val data = mutableListOf>().apply { + Class.values().forEach { myCls -> + add(mutableListOf().apply { + val resByMyCls = results[myCls]!! + Class.values().forEach { opponentCls -> + val matchesVsOpponent = resByMyCls.filter { it.opponent.cls == opponentCls } + add(getResultBodyItem(matchesVsOpponent, myCls == selectedClass)) + } + add(getResultBodyItem(resByMyCls, myCls == selectedClass)) + }) + } + val allMatches = results.flatMap { it.value } + add(mutableListOf().apply { + Class.values().forEach { + val resByOpponent = allMatches.groupBy { it.opponent.cls }[it] ?: listOf() + add(getResultBodyItem(resByOpponent, false)) + } + add(getResultBodyItem(allMatches, false)) + }) + } + uiThread { + statisticsTableAdapter?.body = data + } + } + } + + private fun getResultBodyItem(matches: List, cellSelected: Boolean): BodyItem { + val result = matches.groupBy { it.win } + val wins = result[true]?.size ?: 0 + val losses = result[false]?.size ?: 0 + val resultText = if (!(showPercent?.isChecked ?: false)) "$wins/$losses" else + getString(R.string.match_statistics_percent, calcWinRate(wins.toFloat(), losses.toFloat())) + return BodyItem(resultText, selected = cellSelected) + } + + private fun calcWinRate(wins: Float, losses: Float): Float { + val total = (wins + losses) + return if (total == 0f) -1f else 100 / total * wins + } + + @Subscribe + fun onFilterMode(cmdFilterMode: CmdFilterMode) { + currentMatchMode = cmdFilterMode.mode + if (isFragmentSelected) { + getMatches() + } + } + + @Subscribe + fun onFilterSeason(cmdFilterSeason: CmdFilterSeason) { + currentSeason = cmdFilterSeason.season + if (isFragmentSelected) { + getMatches() + } + } + + @Subscribe + fun onUpdateMatches(cmdUpdateMatches: CmdUpdateMatches) { + if (isFragmentSelected) { + getMatches() + } + } + + class BodyItem(val result: String? = null, val cls: Class? = null, val selected: Boolean = false) + + class StatisticsTableAdapter(val context: Context) : TableFixHeaderAdapter, CellClass, CellTextCenter, CellTextCenter>(context) { + + override fun inflateFirstHeader() = CellTextCenter(context) + + override fun inflateHeader() = CellClass(context) + + override fun inflateFirstBody() = CellClass(context) + + override fun inflateBody() = CellTextCenter(context) + + override fun inflateSection() = CellTextCenter(context) + + override fun getHeaderHeight() = context.resources.getDimensionPixelSize(R.dimen.match_statistics_cell_height) + + override fun getHeaderWidths(): List { + val headerWidth = context.resources.getDimensionPixelSize(R.dimen.match_statistics_header_width) + val cellWidth = context.resources.getDimensionPixelSize(R.dimen.match_statistics_cell_width) + val colWidths = mutableListOf(headerWidth) + Class.values().forEach { colWidths.add(cellWidth) } + colWidths.add(headerWidth) + return colWidths + } + + override fun getBodyHeight() = context.resources.getDimensionPixelSize(R.dimen.match_statistics_cell_height) + + override fun isSection(items: List>?, row: Int): Boolean = false + + override fun getSectionHeight() = 0 + + } + + class CellClass(context: Context) : FrameLayout(context), + TableFixHeaderAdapter.HeaderBinder, + TableFixHeaderAdapter.FirstBodyBinder> { + + init { + LayoutInflater.from(context).inflate(R.layout.itemcell_class, this, true) + } + + override fun bindHeader(cls: Class?, col: Int) { + bindClass(cls, false) + } + + override fun bindFirstBody(bodyItems: List, row: Int) { + val bodyItem = bodyItems[0] + bindClass(bodyItem.cls, bodyItem.selected) + } + + private fun bindClass(cls: Class?, selected: Boolean) { + with(rootView) { + val attr1Visibility = if (cls != null) View.VISIBLE else View.GONE + val attr2Visibility = if (cls?.attr2 != Attribute.NEUTRAL) attr1Visibility else View.GONE + cell_class_attr1.visibility = attr1Visibility + cell_class_attr2.visibility = attr2Visibility + cell_class_attr1.setImageResource(cls?.attr1?.imageRes ?: 0) + cell_class_attr2.setImageResource(cls?.attr2?.imageRes ?: 0) + cell_total.visibility = if (cls == null) View.VISIBLE else View.GONE + val cellColor = if (selected) R.color.colorAccent else android.R.color.transparent + setBackgroundColor(ContextCompat.getColor(context, cellColor)) + } + } + + } + + class CellTextCenter(context: Context) : FrameLayout(context), + TableFixHeaderAdapter.FirstHeaderBinder, + TableFixHeaderAdapter.BodyBinder>, + TableFixHeaderAdapter.SectionBinder> { + + init { + LayoutInflater.from(context).inflate(R.layout.itemcell_text, this, true) + } + + override fun bindFirstHeader(result: String) { + bindResult(result, false) + } + + override fun bindBody(bodyItems: List, row: Int, col: Int) { + val bodyItem = bodyItems[col] + bindResult(bodyItem.result, bodyItem.selected) + } + + private fun bindResult(result: String?, selected: Boolean) { + with(rootView) { + cell_text.text = if (result == "0/0" || result?.contains("-") ?: false) "-" else result + cell_text.visibility = if (result == null) View.GONE else View.VISIBLE + cell_progress.visibility = if (result == null) View.VISIBLE else View.GONE + val cellColor = if (selected) R.color.colorAccent else android.R.color.transparent + setBackgroundColor(ContextCompat.getColor(context, cellColor)) + } + } + + override fun bindSection(bodyItems: List, row: Int, col: Int) { + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/AutoHideBehaviour.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/AutoHideBehaviour.kt index 3a2b57f..cc8bb42 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/AutoHideBehaviour.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/AutoHideBehaviour.kt @@ -7,7 +7,6 @@ import android.util.AttributeSet import android.view.View import com.ediposouza.teslesgendstracker.ui.base.CmdUpdateVisibility import org.greenrobot.eventbus.EventBus -import timber.log.Timber /** @@ -32,11 +31,11 @@ class AutoHideBehaviour(context: Context?, attrs: AttributeSet?) : super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) if (dyConsumed > 0) { // User scrolled down and the FAB is currently visible -> hide the FAB - Timber.d("Scroll Down") +// Timber.d("Scroll Down") eventBus.post(CmdUpdateVisibility(false)) } else if (dyConsumed < 0) { // User scrolled up and the FAB is currently not visible -> show the FAB - Timber.d("Scroll Up") +// Timber.d("Scroll Up") eventBus.post(CmdUpdateVisibility(true)) } } diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/FirebaseArray.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/FirebaseArray.kt new file mode 100644 index 0000000..7ccfb6f --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/FirebaseArray.kt @@ -0,0 +1,229 @@ +/* + * Firebase UI Bindings Android Library + * + * Copyright © 2015 Firebase - All Rights Reserved + * https://www.firebase.com + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binaryform must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.ediposouza.teslesgendstracker.ui.util.firebase + +import com.google.firebase.database.* +import timber.log.Timber +import java.util.* + +/** + * This class implements an array-like collection on top of a Firebase location. + */ +class FirebaseArray(var mModel: Class, val mOriginalQuery: () -> Query?, pageSize: Int = 0, + val mOrderASC: Boolean = true) : ChildEventListener, ValueEventListener { + + enum class EventType { + Added, Changed, Removed, Moved, Reset + } + + enum class SyncType { + UnSynced, Synced + } + + interface OnChangedListener { + fun onChanged(type: EventType, index: Int, oldIndex: Int) + } + + interface OnErrorListener { + fun onError(firebaseError: DatabaseError) + } + + interface OnSyncStatusChangedListener { + fun onChanged(type: SyncType) + } + + private var mQuery: Query? = null + private val mSnapshots: ArrayList> = ArrayList() + private val mPageSize: Int + private var mCurrentSize: Int = 0 + private var isSyncing: Boolean = false + set(syncing) { + if (syncing == isSyncing) { + return + } + field = syncing + if (syncing) { + notifyOnSyncChangedListeners(SyncType.UnSynced) + } else { + notifyOnSyncChangedListeners(SyncType.Synced) + } + } + var mOnChangedListener: OnChangedListener? = null + var mOnErrorListener: OnErrorListener? = null + var mOnSyncStatusListener: OnSyncStatusChangedListener? = null + set(listener) { + field = listener + notifyOnSyncChangedListeners(if (isSyncing) SyncType.UnSynced else SyncType.Synced) + } + + init { + mCurrentSize = Math.abs(pageSize) + mPageSize = mCurrentSize + + setup() + } + + fun reset() { + mCurrentSize = mPageSize + mSnapshots.clear() + setup() + notifyChangedListeners(EventType.Reset, 0) + } + + fun more() { + if (mPageSize > 0 && !isSyncing) { + mCurrentSize += mPageSize + setup() + } + } + + fun cleanup() { + mQuery?.removeEventListener(this as ChildEventListener) + mQuery?.removeEventListener(this as ValueEventListener) + } + + val count: Int + get() = mSnapshots.size + + fun getCount(cond: (T) -> Boolean): Int = mSnapshots.filter { cond.invoke(it.second) }.size + + fun getItem(index: Int, cond: (T) -> Boolean): Pair { + return mSnapshots.filter { cond.invoke(it.second) }[index] + } + + private fun setup() { + if (mQuery != null) { + cleanup() + } + if (mPageSize == 0) { + mQuery = mOriginalQuery() + } else if (mOrderASC) { + mQuery = mOriginalQuery()?.limitToFirst(mCurrentSize) + } else { + mQuery = mOriginalQuery()?.limitToLast(mCurrentSize) + } + isSyncing = true + mQuery?.addChildEventListener(this) + mQuery?.addListenerForSingleValueEvent(this) + } + + private fun getIndexForKey(key: String): Int { + var index = 0 + for (snapshot in mSnapshots) { + if (snapshot.first == key) { + return index + } else { + index++ + } + } + throw IllegalArgumentException("Key not found") + } + + // Start of ChildEventListener and ValueEventListener methods + + override fun onChildAdded(snapshot: DataSnapshot, previousChildKey: String?) { + var index = if (mOrderASC) 0 else count + if (previousChildKey != null) { + if (mOrderASC) { + index = getIndexForKey(previousChildKey) + 1 + } else { + index = getIndexForKey(previousChildKey) + } + } + if (mOrderASC && + index < count && + mSnapshots[index].first == snapshot.key) { + return + } else if (!mOrderASC && + index < count + 1 && + index > 0 && + mSnapshots[index - 1].first == snapshot.key) { + return + } + + try { + mSnapshots.add(index, Pair(snapshot.key, snapshot.getValue(mModel))) + } catch (e: Exception) { + Timber.d(e) + } + notifyChangedListeners(EventType.Added, index) + } + + override fun onChildChanged(snapshot: DataSnapshot, previousChildKey: String?) { + val index = getIndexForKey(snapshot.key) + try { + mSnapshots[index] = Pair(snapshot.key, snapshot.getValue(mModel)) + } catch (e: Exception) { + Timber.d(e) + } + notifyChangedListeners(EventType.Changed, index) + } + + override fun onChildRemoved(snapshot: DataSnapshot) { + val index = getIndexForKey(snapshot.key) + mSnapshots.removeAt(index) + notifyChangedListeners(EventType.Removed, index) + } + + override fun onChildMoved(snapshot: DataSnapshot, previousChildKey: String?) { + val oldIndex = getIndexForKey(snapshot.key) + mSnapshots.removeAt(oldIndex) + val newIndex = if (previousChildKey == null) 0 else getIndexForKey(previousChildKey) + 1 + mSnapshots.add(newIndex, Pair(snapshot.key, snapshot.getValue(mModel))) + notifyChangedListeners(EventType.Moved, newIndex, oldIndex) + } + + override fun onDataChange(dataSnapshot: DataSnapshot) { + isSyncing = false + } + + override fun onCancelled(firebaseError: DatabaseError) { + notifyOnErrorListeners(firebaseError) + } + + // End of ChildEventListener and ValueEventListener methods + + fun notifyChangedListeners(type: EventType, index: Int, oldIndex: Int = -1) { + if (mOnChangedListener != null) { + mOnChangedListener!!.onChanged(type, index, oldIndex) + } + } + + fun notifyOnErrorListeners(firebaseError: DatabaseError) { + if (mOnErrorListener != null) { + mOnErrorListener!!.onError(firebaseError) + } + } + + fun notifyOnSyncChangedListeners(type: SyncType) { + if (mOnSyncStatusListener != null) { + mOnSyncStatusListener!!.onChanged(type) + } + } +} diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/FirebaseRVAdapter.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/FirebaseRVAdapter.kt new file mode 100644 index 0000000..e90dc86 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/FirebaseRVAdapter.kt @@ -0,0 +1,208 @@ +/* + * Firebase UI Bindings Android Library + * + * Copyright © 2015 Firebase - All Rights Reserved + * https://www.firebase.com + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binaryform must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.ediposouza.teslesgendstracker.ui.util.firebase + +import android.support.v7.widget.RecyclerView +import com.google.firebase.database.DatabaseError +import com.google.firebase.database.Query + +/** + * This class is a generic way of backing an RecyclerView with a Firebase location. + * It handles all of the child events at the given Firebase location. It marshals received data into the given + * class type. + * To use this class in your app, subclass it passing in all required parameters and implement the + * populateViewHolder method. + *
+ * {@code
+ *     private static class ChatMessageViewHolder extends RecyclerView.ViewHolder {
+ *         TextView messageText;
+ *         TextView nameText;
+ *
+ *         public ChatMessageViewHolder(View itemView) {
+ *             super(itemView);
+ *             nameText = (TextView)itemView.findViewById(android.R.id.text1);
+ *             messageText = (TextView) itemView.findViewById(android.R.id.text2);
+ *         }
+ *     }
+ *
+ *     FirebaseRVAdapter adapter;
+ *     ref = new Firebase("https://.firebaseio.com");
+ *
+ *     RecyclerView recycler = (RecyclerView) findViewById(R.id.messages_recycler);
+ *     recycler.setHasFixedSize(true);
+ *     recycler.setLayoutManager(new LinearLayoutManager(this));
+ *
+ *     adapter = new FirebaseRVAdapter(ChatMessage.class, android.R.layout.two_line_list_item, ChatMessageViewHolder.class, mRef) {
+ *         public void populateViewHolder(ChatMessageViewHolder chatMessageViewHolder, ChatMessage chatMessage) {
+ *             chatMessageViewHolder.nameText.setText(chatMessage.getName());
+ *             chatMessageViewHolder.messageText.setText(chatMessage.getMessage());
+ *         }
+ *     };
+ *     recycler.setAdapter(mAdapter);
+ * }
+ * 
+ * + * @param The Java class that maps to the type of objects stored in the Firebase location. + * @param The ViewHolder class that contains the Views in the layout that is shown for each object. + * + * + * CONSTRUCTOR + * @param modelLayout This is the layout used to represent a single item in the list. You will be responsible for populating an + * * instance of the corresponding view with the data from an instance of modelClass. + * * + * @param viewHolderClass The class that hold references to all sub-views in an instance modelLayout. + * * + * @param ref The Firebase location to watch for data changes. + * * + * @param pageSize initial page size. set 0 for no limit. + */ +abstract class FirebaseRVAdapter( + var mModel: Class, + ref: () -> Query?, + pageSize: Int = 0, + orderASC: Boolean = false, + val filter: ((T) -> Boolean)? = null) : RecyclerView.Adapter() { + + var mSnapshots = FirebaseArray(mModel, ref, pageSize, orderASC) + + init { + mSnapshots.mOnChangedListener = object : FirebaseArray.OnChangedListener { + override fun onChanged(type: FirebaseArray.EventType, index: Int, oldIndex: Int) { + when (type) { + FirebaseArray.EventType.Added -> notifyItemInserted(index + snapShotOffset) + FirebaseArray.EventType.Changed -> notifyItemChanged(index + snapShotOffset) + FirebaseArray.EventType.Removed -> notifyItemRemoved(index + snapShotOffset) + FirebaseArray.EventType.Moved -> notifyItemMoved(oldIndex + snapShotOffset, index + snapShotOffset) + FirebaseArray.EventType.Reset -> notifyDataSetChanged() + else -> throw IllegalStateException("Incomplete case statement") + } + } + + } + mSnapshots.mOnSyncStatusListener = object : FirebaseArray.OnSyncStatusChangedListener { + override fun onChanged(type: FirebaseArray.SyncType) { + onSyncStatusChanged(type == FirebaseArray.SyncType.Synced) + } + } + mSnapshots.mOnErrorListener = object : FirebaseArray.OnErrorListener { + override fun onError(firebaseError: DatabaseError) { + onArrayError(firebaseError) + } + } + + } + + /** + * Increase the limit of the query by one page. + */ + fun more() = mSnapshots.more() + + /** + * Reset the limit of the query to its original size. + */ + fun reset() = mSnapshots.reset() + + fun cleanup() = mSnapshots.cleanup() + + private fun filterResult(it: T) = filter?.invoke(it) ?: true + + /** + * Override when adding headers. + * @return number of items inserted in front of the FirebaseArray + */ + open val snapShotOffset: Int = 0 + + open fun getContentCount(): Int = mSnapshots.getCount { filterResult(it) } + + /** + * Override when adding headers or footers + * @return number of items including headers and footers. + */ + override fun getItemCount(): Int = mSnapshots.getCount { filterResult(it) } + + open fun getItem(position: Int): T = mSnapshots.getItem(position - snapShotOffset, { filterResult(it) }).second + + fun getItemKey(position: Int): String = mSnapshots.getItem(position - snapShotOffset, { filterResult(it) }).first + + override fun getItemId(position: Int): Long { + if (position < snapShotOffset) + return ("header" + position).hashCode().toLong() + if (position >= snapShotOffset + mSnapshots.getCount { filterResult(it) }) + return ("footer" + (position - (snapShotOffset + mSnapshots.getCount { filterResult(it) }))).hashCode().toLong() + // http://stackoverflow.com/questions/5100071/whats-the-purpose-of-item-ids-in-android-listview-adapter + return mSnapshots.getItem(position, { filterResult(it) }).first.hashCode().toLong() + } + + override fun onBindViewHolder(viewHolder: VH, position: Int) { + var itemKey: String? = null + var model: T? = null + val arrayPosition = position - snapShotOffset + if (arrayPosition < getContentCount() && arrayPosition >= 0) { + itemKey = getItemKey(position) + model = getItem(position) + } + populateViewHolder(itemKey, viewHolder, model, position) + } + + /** + * Each time the data at the given Firebase location changes, this method will be called for each item that needs + * to be displayed. The first two arguments correspond to the mLayout and mModelClass given to the constructor of + * this class. The third argument is the item's position in the list. + *

+ * Your implementation should populate the view using the data contained in the model. + * You should implement either this method or the other FirebaseRVAdapter#populateViewHolder(VH, Object) method + * but not both. + * + * @param viewHolder The view to populate + * * + * @param model The object containing the data used to populate the view + * * + * @param position The position in the list of the view being populated + */ + open protected fun populateViewHolder(itemKey: String?, viewHolder: VH, model: T?, position: Int) { + populateViewHolder(itemKey, viewHolder, model) + } + + /** + * This is a backwards compatible version of populateViewHolder. + * + * + * You should implement either this method or the other FirebaseRVAdapter#populateViewHolder(VH, T, int) method + * but not both. + * see FirebaseListAdapter#populateView(View, Object, int) + * @param viewHolder The view to populate + * * + * @param model The object containing the data used to populate the view + */ + open protected fun populateViewHolder(itemKey: String?, viewHolder: VH, model: T?) { + } + + open protected fun onSyncStatusChanged(synced: Boolean) {} + open protected fun onArrayError(firebaseError: DatabaseError) {} +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/OnLinearLayoutItemScrolled.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/OnLinearLayoutItemScrolled.kt new file mode 100644 index 0000000..faa3cff --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/util/firebase/OnLinearLayoutItemScrolled.kt @@ -0,0 +1,21 @@ +package com.ediposouza.teslesgendstracker.ui.util.firebase + +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView + +/** + * Created by ediposouza on 04/01/17. + */ +class OnLinearLayoutItemScrolled(val itemPosition: Int, val onViewItem: () -> Unit) : RecyclerView.OnScrollListener() { + + override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { + if (dy < 0) { + return + } + val layoutManager = recyclerView?.layoutManager as LinearLayoutManager + if (layoutManager.findLastVisibleItemPosition() >= itemPosition) { + onViewItem() + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterAttr.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/FilterAttr.kt similarity index 76% rename from app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterAttr.kt rename to app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/FilterAttr.kt index 36362e8..dbb3f26 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/filter/FilterAttr.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/FilterAttr.kt @@ -1,6 +1,8 @@ -package com.ediposouza.teslesgendstracker.ui.widget.filter +package com.ediposouza.teslesgendstracker.ui.widget import android.content.Context +import android.os.Parcel +import android.os.Parcelable import android.util.AttributeSet import android.view.View import android.view.ViewGroup @@ -15,11 +17,14 @@ import kotlinx.android.synthetic.main.widget_attributes_filter.view.* open class FilterAttr(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(ctx, attrs, defStyleAttr) { + var filterClick: ((Attribute) -> Unit)? = null + var lastAttrSelected: Attribute = Attribute.STRENGTH + var deckMode: Boolean = false set(value) { field = value if (!value) { - selectAttr(Attribute.STRENGTH, true) + selectAttr(lastAttrSelected, true) } else { rootView.attr_filter_dual.visibility = if (value) View.GONE else View.VISIBLE rootView.attr_filter_dual_indicator.visibility = if (value) View.GONE else View.VISIBLE @@ -28,9 +33,6 @@ open class FilterAttr(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : } } - var filterClick: ((Attribute) -> Unit)? = null - var lastAttrSelected: Attribute = Attribute.STRENGTH - init { inflate(context, R.layout.widget_attributes_filter, rootView as ViewGroup) val a = context.obtainStyledAttributes(attrs, R.styleable.FilterAttr) @@ -55,6 +57,22 @@ open class FilterAttr(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : filterClick?.invoke(attr) } + override fun onSaveInstanceState(): Parcelable { + val superState = super.onSaveInstanceState() + return SavedState(superState, lastAttrSelected) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is SavedState) { + if (!deckMode) { + selectAttr(state.attrSelected, true) + } + super.onRestoreInstanceState(state.superState) + } else { + super.onRestoreInstanceState(state) + } + } + open fun selectAttr(attr: Attribute, only: Boolean) { lastAttrSelected = attr updateVisibility(rootView.attr_filter_strength_indicator, attr == Attribute.STRENGTH, only) @@ -100,4 +118,31 @@ open class FilterAttr(ctx: Context?, attrs: AttributeSet?, defStyleAttr: Int) : v.visibility = if (show) View.VISIBLE else if (only) View.INVISIBLE else v.visibility } + class SavedState : BaseSavedState { + + var attrSelected: Attribute = Attribute.STRENGTH + + constructor(source: Parcel?) : super(source) { + this.attrSelected = Attribute.values()[source?.readInt() ?: 0] + } + + constructor(source: Parcelable, attrSelected: Attribute) : super(source) { + this.attrSelected = attrSelected + } + + companion object { + @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): SavedState = SavedState(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel?, flags: Int) { + super.writeToParcel(dest, flags) + dest?.writeInt(attrSelected.ordinal) + } + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/TableFixHeadersScroll.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/TableFixHeadersScroll.kt new file mode 100644 index 0000000..3a4c661 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/ui/widget/TableFixHeadersScroll.kt @@ -0,0 +1,43 @@ +package com.ediposouza.teslesgendstracker.ui.widget + +import android.content.Context +import android.os.Handler +import android.text.format.DateUtils +import android.util.AttributeSet +import com.ediposouza.teslesgendstracker.ui.base.CmdUpdateVisibility +import com.inqbarna.tablefixheaders.TableFixHeaders +import org.greenrobot.eventbus.EventBus +import timber.log.Timber + +/** + * Created by EdipoSouza on 1/14/17. + */ +class TableFixHeadersScroll(ctx: Context, attrs: AttributeSet?) : TableFixHeaders(ctx, attrs) { + + private val runnableUp = { + Timber.d("Up") + EventBus.getDefault().post(CmdUpdateVisibility(true)) + } + + private val runnableDown = { + Timber.d("Down") + EventBus.getDefault().post(CmdUpdateVisibility(false)) + } + + private val scrollHandler by lazy { Handler() } + + constructor(ctx: Context) : this(ctx, null) + + override fun scrollBy(x: Int, y: Int) { + super.scrollBy(x, y) + with(scrollHandler) { + if (y < 0) { + removeCallbacksAndMessages(null) + postDelayed(runnableUp, DateUtils.SECOND_IN_MILLIS / 2) + } else { + removeCallbacksAndMessages(null) + postDelayed(runnableDown, DateUtils.SECOND_IN_MILLIS / 2) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/AppExtensions.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/AppExtensions.kt index 7b6b29d..cd616bb 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/AppExtensions.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/AppExtensions.kt @@ -1,5 +1,6 @@ package com.ediposouza.teslesgendstracker.util +import android.app.AlertDialog import android.content.Context import android.os.Bundle import android.support.annotation.IntegerRes @@ -7,15 +8,47 @@ import android.support.design.widget.BottomSheetBehavior import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ListPopupWindow +import android.widget.Spinner +import com.ediposouza.teslesgendstracker.App import com.ediposouza.teslesgendstracker.R import com.google.android.gms.ads.AdRequest import com.google.android.gms.ads.AdView import com.google.android.gms.ads.NativeExpressAdView import com.mixpanel.android.mpmetrics.MixpanelAPI +import org.jetbrains.anko.AlertDialogBuilder /** * Created by ediposouza on 01/11/16. */ +fun Context.alertThemed( + message: Int, + title: Int? = null, + theme: Int = 0, + init: (AlertDialogBuilder.() -> Unit)? = null +) = AlertDialogBuilder(this).apply { + val builderField = AlertDialogBuilder::class.java.getDeclaredField("builder") + builderField.isAccessible = true + builderField.set(this, AlertDialog.Builder(ctx, theme)) + if (title != null) title(title) + message(message) + if (init != null) init() +} + +fun Context.alertThemed( + message: String, + title: String? = null, + theme: Int = 0, + init: (AlertDialogBuilder.() -> Unit)? = null +) = AlertDialogBuilder(this).apply { + val builderField = AlertDialogBuilder::class.java.getDeclaredField("builder") + builderField.isAccessible = true + builderField.set(this, AlertDialog.Builder(ctx, theme)) + if (title != null) title(title) + message(message) + if (init != null) init() +} + fun String.toIntSafely(): Int { if (this.trim().isEmpty()) return 0 @@ -36,11 +69,19 @@ fun BottomSheetBehavior<*>.toggleExpanded() { } fun AdView.load() { - this.loadAd(com.ediposouza.teslesgendstracker.util.createAdRequest(context)) + if (App.hasUserDonate()) { + layoutParams = layoutParams.apply { height = 0 } + } else { + loadAd(createAdRequest(context)) + } } fun NativeExpressAdView.load() { - this.loadAd(com.ediposouza.teslesgendstracker.util.createAdRequest(context)) + if (App.hasUserDonate()) { + layoutParams = layoutParams.apply { height = 0 } + } else { + loadAd(createAdRequest(context)) + } } private fun createAdRequest(context: Context): AdRequest { @@ -54,4 +95,19 @@ private fun createAdRequest(context: Context): AdRequest { fun MixpanelAPI.trackBundle(eventName: String, bundle: Bundle) { trackMap(eventName, bundle.keySet().map { it to bundle[it] }.toMap()) -} \ No newline at end of file +} + + +fun Spinner.limitHeight(lines: Int? = null) { + val displayHeight = context.resources.displayMetrics.heightPixels + val itemHeight = context.resources.getDimensionPixelSize(R.dimen.material_design_default_height) + Spinner::class.java.getDeclaredField("mPopup") + ?.apply { isAccessible = true }?.get(this) + ?.apply popup@ { + IntArray(2).apply { + getLocationOnScreen(this) + val listPopupWindow = this@popup as ListPopupWindow + listPopupWindow.height = if (lines != null) lines * itemHeight else displayHeight - get(1) + } + } +} diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/ConfigManager.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/ConfigManager.kt index 7d54b58..fff6f28 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/ConfigManager.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/ConfigManager.kt @@ -12,6 +12,7 @@ import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings object ConfigManager { val DB_UPDATE_CONFIG = "db_update" + val SHOW_DECK_ADS_CONFIG = "showDeckAds" val VERSION_UNSUPPORTED_CONFIG = "version_unsupported" val remoteConfig: FirebaseRemoteConfig by lazy { FirebaseRemoteConfig.getInstance() } @@ -22,6 +23,7 @@ object ConfigManager { .setDeveloperModeEnabled(BuildConfig.DEBUG) .build()) setDefaults(mapOf(DB_UPDATE_CONFIG to false, + SHOW_DECK_ADS_CONFIG to false, VERSION_UNSUPPORTED_CONFIG to "")) } updateCaches {} @@ -40,6 +42,8 @@ object ConfigManager { fun isDBUpdating() = remoteConfig.getBoolean(DB_UPDATE_CONFIG) + fun isShowDeckAds() = remoteConfig.getBoolean(SHOW_DECK_ADS_CONFIG) + fun isVersionUnsupported(): Boolean { val unsupportedVersions = remoteConfig.getString(VERSION_UNSUPPORTED_CONFIG) if (unsupportedVersions.isEmpty()) { diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/MetricsConstants.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/MetricsConstants.kt index 22f7583..d52f745 100644 --- a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/MetricsConstants.kt +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/MetricsConstants.kt @@ -1,8 +1,6 @@ package com.ediposouza.teslesgendstracker.util -import com.ediposouza.teslesgendstracker.data.Attribute -import com.ediposouza.teslesgendstracker.data.CardRarity -import com.ediposouza.teslesgendstracker.data.CardSet +import com.ediposouza.teslesgendstracker.data.* /** * Created by ediposouza on 08/12/16. @@ -35,6 +33,7 @@ abstract class MetricsConstants { sealed class MetricAction(val name: String) { companion object { + const val ALL = "All" const val CLEAR = "Clear" } @@ -77,12 +76,65 @@ sealed class MetricAction(val name: String) { class ACTION_DECK_COMMENTS_COLLAPSE : MetricAction("DeckCommentCollapse") class ACTION_DECK_COMMENTS_SEND : MetricAction("DeckCommentSend") - class ACTION_NEW_DECK_SAVE(val type: String, val patch: String, val private: Boolean) : MetricAction("DeckCommentSend") { + class ACTION_NEW_DECK_SAVE(val type: String, val patch: String, val private: Boolean) : MetricAction("DeckNew") { val PARAM_TYPE = "Type" val PARAM_PATCH = "Patch" val PARAM_PRIVATE = "Private" } + class ACTION_MATCH_STATISTICS_WIN_RATE(val checked: Boolean) : MetricAction("MatchStatisticsWinRate") { + val PARAM_CHECKED = "Checked" + } + + class ACTION_MATCH_STATISTICS_CLASS_WIN_RATE(val checked: Boolean) : MetricAction("MatchStatisticsClassWinRate") { + val PARAM_CHECKED = "Checked" + } + + class ACTION_MATCH_STATISTICS_FILTER_MODE(val mode: MatchMode) : MetricAction("FilterMatchMode") { + val PARAM_MODE = "Mode" + } + + class ACTION_MATCH_STATISTICS_FILTER_SEASON(val season: Season?) : MetricAction("FilterMatchStatisticsSeason") { + val PARAM_SEASON = "Season" + } + + class ACTION_MATCH_STATISTICS_CLASS_FILTER_SEASON(val season: Season?) : MetricAction("FilterMatchStatisticsClassSeason") { + val PARAM_SEASON = "Season" + } + + class ACTION_MATCH_STATISTICS_CLASS(val cls: Class) : MetricAction("MatchStatisticsClass") { + val PARAM_CLASS = "Class" + } + + class ACTION_NEW_MATCH_START_WITH(val deck: Deck?) : MetricAction("MatchStatisticsClass") { + val PARAM_DECK = "Deck" + val PARAM_DECK_VALUE_OTHER = "Other" + } + + class ACTION_NEW_MATCH_SAVE(val myDeckCls: Class, val myDeckType: DeckType, val optDeckCls: Class, + val optDeckType: DeckType, val mode: MatchMode, val season: String?, + val legendRank: Boolean, val deckTrackerUsed: Boolean) : MetricAction("MatchNew") { + val PARAM_MY_CLS = "MyClass" + val PARAM_MY_TYPE = "MyType" + val PARAM_OPT_CLS = "OptClass" + val PARAM_OPT_TYPE = "OptType" + val PARAM_MODE = "Mode" + val PARAM_SEASON = "Season" + val PARAM_LEGEND = "Legend" + } + + class ACTION_DONATE_BASIC : MetricAction("DonateBasic") + class ACTION_DONATE_PRO : MetricAction("DonatePro") + class ACTION_DONATE_NOT_NOW : MetricAction("DonateNotNow") + class ACTION_NEW_VERSION_DETECTED : MetricAction("NewVersionDetected") + class ACTION_NEW_VERSION_UPDATE_NOW : MetricAction("NewVersionUpdateNow") + class ACTION_NEW_VERSION_UPDATE_LATER : MetricAction("NewVersionUpdateLater") + class ACTION_ABOUT_CVH : MetricAction("AboutCVH") + class ACTION_ABOUT_DIREWOLF : MetricAction("AboutDireWolf") + class ACTION_ABOUT_RATE : MetricAction("AboutRate") + class ACTION_IMPORT_COLLECTION_CANCELLED : MetricAction("AboutImportCollectionCancelled") + class ACTION_IMPORT_COLLECTION_FINISH : MetricAction("AboutImportCollectionFinish") + } sealed class MetricScreen(val name: String) { @@ -104,5 +156,12 @@ sealed class MetricScreen(val name: String) { class SCREEN_DECKS_FAVORED : MetricScreen("DecksFavored") class SCREEN_DECK_DETAILS : MetricScreen("DeckDetails") class SCREEN_NEW_DECKS : MetricScreen("NewDeck") + class SCREEN_MATCHES_STATISTICS : MetricScreen("MatchesStatistics") + class SCREEN_MATCHES_STATISTICS_CLASS : MetricScreen("MatchesStatisticsClass") + class SCREEN_MATCHES_HISTORY : MetricScreen("MatchesHistory") + class SCREEN_NEW_MATCHES : MetricScreen("NewMatches") + class SCREEN_DONATE : MetricScreen("Donate") + class SCREEN_ABOUT : MetricScreen("About") + class SCREEN_IMPORT_COLLECTION : MetricScreen("ImportCollection") } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/TestUtils.kt b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/TestUtils.kt new file mode 100644 index 0000000..ffd3ab5 --- /dev/null +++ b/app/src/main/kotlin/com/ediposouza/teslesgendstracker/util/TestUtils.kt @@ -0,0 +1,96 @@ +package com.ediposouza.teslesgendstracker.util + +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup +import com.ediposouza.teslesgendstracker.R +import com.ediposouza.teslesgendstracker.data.* +import com.ediposouza.teslesgendstracker.ui.matches.tabs.MatchesHistoryFragment +import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter +import org.threeten.bp.LocalDateTime +import timber.log.Timber + +/** + * Created by EdipoSouza on 1/5/17. + */ +object TestUtils { + + fun getTestMatches(): List { + val decks = listOf( + MatchDeck("", Class.ARCHER, DeckType.AGGRO, ""), + MatchDeck("", Class.ASSASSIN, DeckType.AGGRO, ""), + MatchDeck("", Class.BATTLEMAGE, DeckType.AGGRO, ""), + MatchDeck("", Class.CRUSADER, DeckType.AGGRO, ""), + MatchDeck("", Class.MAGE, DeckType.COMBO, ""), + MatchDeck("", Class.MONK, DeckType.COMBO, ""), + MatchDeck("", Class.SCOUT, DeckType.COMBO, ""), + MatchDeck("", Class.SORCERER, DeckType.COMBO, ""), + MatchDeck("", Class.SPELLSWORD, DeckType.CONTROL, ""), + MatchDeck("", Class.WARRIOR, DeckType.CONTROL, ""), + MatchDeck("", Class.STRENGTH, DeckType.CONTROL, ""), + MatchDeck("", Class.INTELLIGENCE, DeckType.CONTROL, ""), + MatchDeck("", Class.AGILITY, DeckType.MIDRANGE, ""), + MatchDeck("", Class.WILLPOWER, DeckType.MIDRANGE, ""), + MatchDeck("", Class.ENDURANCE, DeckType.MIDRANGE, ""), + MatchDeck("", Class.NEUTRAL, DeckType.MIDRANGE, "") + ) + return mutableListOf().apply { + for ((indexPlayer, playerDeck) in decks.withIndex()) { + for ((indexOpponent, opponentDeck) in decks.withIndex()) { + addAll(mutableListOf().apply { + for (i in 1..indexPlayer + 1) { + val uuid = LocalDateTime.now().withNano(0).minusDays(playerDeck.type.ordinal.toLong()).toString() + add(Match(uuid, false, playerDeck, opponentDeck, MatchMode.RANKED, "2016_12", 0, false, true)) + } + for (i in 1..indexOpponent + 1) { + val uuid = LocalDateTime.now().withNano(0).minusDays(opponentDeck.type.ordinal.toLong()).toString() + add(Match(uuid, false, playerDeck, opponentDeck, MatchMode.RANKED, "2016_12", 0, false, false)) + } + }) + } + } + } + } + + fun getTestMatchesHistoryAdapter() = TestMatchesHistoryAdapter() + + class TestMatchesHistoryAdapter : RecyclerView.Adapter(), + StickyRecyclerHeadersAdapter { + + val items = TestUtils.getTestMatches() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MatchesHistoryFragment.MatchViewHolder { + return MatchesHistoryFragment.MatchViewHolder(parent.inflate(R.layout.itemlist_match_history)) + } + + override fun onBindViewHolder(holder: MatchesHistoryFragment.MatchViewHolder?, position: Int) { + holder?.bind(items[position], { notifyItemRemoved(position) }) + } + + override fun getItemCount(): Int = items.size + + override fun onCreateHeaderViewHolder(parent: ViewGroup): MatchesHistoryFragment.MatchViewHolder { + return MatchesHistoryFragment.MatchViewHolder(parent.inflate(R.layout.itemlist_match_history_section)) + } + + override fun onBindHeaderViewHolder(holder: MatchesHistoryFragment.MatchViewHolder?, position: Int) { + holder?.bindSection(LocalDateTime.parse(items[position].uuid).toLocalDate()) + } + + override fun getHeaderId(position: Int): Long { + val date = LocalDateTime.parse(items[position].uuid).toLocalDate() + return date.year + date.monthValue + date.dayOfMonth.toLong() + } + + fun getContentCount(): Int = items.size + fun more() { + Timber.d("More") + } + fun reset() { + Timber.d("Reset") + notifyDataSetChanged() + } + + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_rank_legend.png b/app/src/main/res/drawable-hdpi/ic_rank_legend.png new file mode 100644 index 0000000..7f3afef Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_rank_legend.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rank_legend.png b/app/src/main/res/drawable-xhdpi/ic_rank_legend.png new file mode 100644 index 0000000..e35666b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rank_legend.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rank_legend.png b/app/src/main/res/drawable-xxhdpi/ic_rank_legend.png new file mode 100644 index 0000000..a0c11c9 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_rank_legend.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rank_legend.png b/app/src/main/res/drawable-xxxhdpi/ic_rank_legend.png new file mode 100644 index 0000000..b885461 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_rank_legend.png differ diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml new file mode 100644 index 0000000..0f634f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_about.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_about_svg.xml b/app/src/main/res/drawable/ic_about_svg.xml new file mode 100644 index 0000000..77ca1f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_about_svg.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_ads.xml b/app/src/main/res/drawable/ic_ads.xml new file mode 100644 index 0000000..8135d4b --- /dev/null +++ b/app/src/main/res/drawable/ic_ads.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_ads_svg.xml b/app/src/main/res/drawable/ic_ads_svg.xml new file mode 100644 index 0000000..a84bdcc --- /dev/null +++ b/app/src/main/res/drawable/ic_ads_svg.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_cards.xml b/app/src/main/res/drawable/ic_cards.xml new file mode 100644 index 0000000..a58ddf4 --- /dev/null +++ b/app/src/main/res/drawable/ic_cards.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cards_svg.xml b/app/src/main/res/drawable/ic_cards_svg.xml new file mode 100644 index 0000000..8b7c18b --- /dev/null +++ b/app/src/main/res/drawable/ic_cards_svg.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_checked.xml b/app/src/main/res/drawable/ic_checked.xml new file mode 100644 index 0000000..b7684ec --- /dev/null +++ b/app/src/main/res/drawable/ic_checked.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_checked_svg.xml b/app/src/main/res/drawable/ic_checked_svg.xml new file mode 100644 index 0000000..0e3fcf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_checked_svg.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_decks.xml b/app/src/main/res/drawable/ic_decks.xml new file mode 100644 index 0000000..93b30c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_decks.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_decks_svg.xml b/app/src/main/res/drawable/ic_decks_svg.xml new file mode 100644 index 0000000..7199a08 --- /dev/null +++ b/app/src/main/res/drawable/ic_decks_svg.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_first.xml b/app/src/main/res/drawable/ic_first.xml new file mode 100644 index 0000000..7ae97a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_first.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_first_svg.xml b/app/src/main/res/drawable/ic_first_svg.xml new file mode 100644 index 0000000..34037ed --- /dev/null +++ b/app/src/main/res/drawable/ic_first_svg.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml new file mode 100644 index 0000000..b251495 --- /dev/null +++ b/app/src/main/res/drawable/ic_import.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_import_svg.xml b/app/src/main/res/drawable/ic_import_svg.xml new file mode 100644 index 0000000..ebf77c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_import_svg.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_matches.xml b/app/src/main/res/drawable/ic_matches.xml new file mode 100644 index 0000000..d857b4c --- /dev/null +++ b/app/src/main/res/drawable/ic_matches.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_matches_svg.xml b/app/src/main/res/drawable/ic_matches_svg.xml new file mode 100644 index 0000000..a0fb956 --- /dev/null +++ b/app/src/main/res/drawable/ic_matches_svg.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_mode_arena.xml b/app/src/main/res/drawable/ic_mode_arena.xml new file mode 100644 index 0000000..1c41ba8 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_arena.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mode_arena_svg.xml b/app/src/main/res/drawable/ic_mode_arena_svg.xml new file mode 100644 index 0000000..a643075 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_arena_svg.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mode_casual.xml b/app/src/main/res/drawable/ic_mode_casual.xml new file mode 100644 index 0000000..9839b4d --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_casual.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mode_casual_svg.xml b/app/src/main/res/drawable/ic_mode_casual_svg.xml new file mode 100644 index 0000000..4cdaf98 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_casual_svg.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mode_ranked.xml b/app/src/main/res/drawable/ic_mode_ranked.xml new file mode 100644 index 0000000..57d8cc9 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_ranked.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mode_ranked_svg.xml b/app/src/main/res/drawable/ic_mode_ranked_svg.xml new file mode 100644 index 0000000..0f7c1a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_mode_ranked_svg.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_no_ads.xml b/app/src/main/res/drawable/ic_no_ads.xml new file mode 100644 index 0000000..413b601 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_ads.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_no_ads_svg.xml b/app/src/main/res/drawable/ic_no_ads_svg.xml new file mode 100644 index 0000000..3a63e83 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_ads_svg.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_percent_menu.xml b/app/src/main/res/drawable/ic_percent_menu.xml new file mode 100644 index 0000000..b97fb90 --- /dev/null +++ b/app/src/main/res/drawable/ic_percent_menu.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_percent_menu_checked.xml b/app/src/main/res/drawable/ic_percent_menu_checked.xml new file mode 100644 index 0000000..8be09db --- /dev/null +++ b/app/src/main/res/drawable/ic_percent_menu_checked.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_percent_menu_checked_svg.xml b/app/src/main/res/drawable/ic_percent_menu_checked_svg.xml new file mode 100644 index 0000000..251efc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_percent_menu_checked_svg.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_percent_menu_svg.xml b/app/src/main/res/drawable/ic_percent_menu_svg.xml new file mode 100644 index 0000000..fa2be92 --- /dev/null +++ b/app/src/main/res/drawable/ic_percent_menu_svg.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_private_menu_checked_svg.xml b/app/src/main/res/drawable/ic_private_menu_checked_svg.xml index d0ff80e..75e3b51 100644 --- a/app/src/main/res/drawable/ic_private_menu_checked_svg.xml +++ b/app/src/main/res/drawable/ic_private_menu_checked_svg.xml @@ -1,7 +1,7 @@ + + + diff --git a/app/src/main/res/drawable/ic_season_svg.xml b/app/src/main/res/drawable/ic_season_svg.xml new file mode 100644 index 0000000..a1d0663 --- /dev/null +++ b/app/src/main/res/drawable/ic_season_svg.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_bottom_tab_mode.xml b/app/src/main/res/drawable/selector_bottom_tab_mode.xml new file mode 100644 index 0000000..050f7b3 --- /dev/null +++ b/app/src/main/res/drawable/selector_bottom_tab_mode.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_menu_item.xml b/app/src/main/res/drawable/selector_menu_item.xml index f5c81e5..2a7a9db 100644 --- a/app/src/main/res/drawable/selector_menu_item.xml +++ b/app/src/main/res/drawable/selector_menu_item.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/xml_button_accent.xml b/app/src/main/res/drawable/xml_button_accent.xml new file mode 100644 index 0000000..06d574c --- /dev/null +++ b/app/src/main/res/drawable/xml_button_accent.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/xml_button_green.xml b/app/src/main/res/drawable/xml_button_green.xml new file mode 100644 index 0000000..3b91d50 --- /dev/null +++ b/app/src/main/res/drawable/xml_button_green.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/xml_button_red.xml b/app/src/main/res/drawable/xml_button_red.xml new file mode 100644 index 0000000..794d87d --- /dev/null +++ b/app/src/main/res/drawable/xml_button_red.xml @@ -0,0 +1,16 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/xml_percent.xml b/app/src/main/res/drawable/xml_percent.xml new file mode 100644 index 0000000..e47512c --- /dev/null +++ b/app/src/main/res/drawable/xml_percent.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/xml_switch.xml b/app/src/main/res/drawable/xml_switch.xml index 5d0e232..da67b77 100644 --- a/app/src/main/res/drawable/xml_switch.xml +++ b/app/src/main/res/drawable/xml_switch.xml @@ -11,9 +11,7 @@ - + - + diff --git a/app/src/main/res/drawable/xml_update_bg.xml b/app/src/main/res/drawable/xml_update_bg.xml new file mode 100644 index 0000000..4ed8de1 --- /dev/null +++ b/app/src/main/res/drawable/xml_update_bg.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-v21/widget_switch_percent.xml b/app/src/main/res/layout-v21/widget_switch_percent.xml new file mode 100644 index 0000000..b322cc7 --- /dev/null +++ b/app/src/main/res/layout-v21/widget_switch_percent.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout-v21/widget_switch_private.xml b/app/src/main/res/layout-v21/widget_switch_private.xml new file mode 100644 index 0000000..8877808 --- /dev/null +++ b/app/src/main/res/layout-v21/widget_switch_private.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_card.xml b/app/src/main/res/layout/activity_card.xml index 8b3694f..b0b2a3a 100644 --- a/app/src/main/res/layout/activity_card.xml +++ b/app/src/main/res/layout/activity_card.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:animateLayoutChanges="true" - tools:context="com.ediposouza.teslesgendstracker.ui.CardActivity"> + tools:ctx="com.ediposouza.teslesgendstracker.ui.cards.CardActivity"> + tools:ctx="com.ediposouza.teslesgendstracker.ui.DashActivity"> - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" /> @@ -118,6 +58,6 @@ app:itemIconTint="@drawable/selector_menu_item" app:itemTextAppearance="?android:attr/textAppearanceMedium" app:itemTextColor="@drawable/selector_menu_item" - app:menu="@menu/menu_drawer" /> + app:menu="@menu/navigation_drawer" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_deck.xml b/app/src/main/res/layout/activity_deck.xml index 744c3c2..d5cf309 100644 --- a/app/src/main/res/layout/activity_deck.xml +++ b/app/src/main/res/layout/activity_deck.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:animateLayoutChanges="true" - tools:context="com.ediposouza.teslesgendstracker.ui.DeckActivity"> + tools:ctx="com.ediposouza.teslesgendstracker.ui.decks.DeckActivity"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_new_deck.xml b/app/src/main/res/layout/activity_new_deck.xml index 44e782c..59c87d5 100644 --- a/app/src/main/res/layout/activity_new_deck.xml +++ b/app/src/main/res/layout/activity_new_deck.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:animateLayoutChanges="true" - tools:context="com.ediposouza.teslesgendstracker.ui.decks.new.NewDeckActivity"> + tools:ctx="com.ediposouza.teslesgendstracker.ui.decks.NewDeckActivity"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml new file mode 100644 index 0000000..d78142f --- /dev/null +++ b/app/src/main/res/layout/dialog_about.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_import.xml b/app/src/main/res/layout/dialog_import.xml new file mode 100644 index 0000000..e54d4e0 --- /dev/null +++ b/app/src/main/res/layout/dialog_import.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_import_result.xml b/app/src/main/res/layout/dialog_import_result.xml new file mode 100644 index 0000000..37afc00 --- /dev/null +++ b/app/src/main/res/layout/dialog_import_result.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_new_deck.xml b/app/src/main/res/layout/dialog_new_deck.xml index 25b49d4..b3abe05 100644 --- a/app/src/main/res/layout/dialog_new_deck.xml +++ b/app/src/main/res/layout/dialog_new_deck.xml @@ -1,7 +1,9 @@ + android:layout_height="wrap_content" + tools:layout_marginTop="@dimen/status_bar_height"> + android:text="@string/new_deck_save_dialog_type_label" + android:textColor="@color/grey_500" /> + android:text="@string/new_deck_save_dialog_patch_label" + android:textColor="@color/grey_500" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_cards.xml b/app/src/main/res/layout/fragment_cards.xml index 67f48cb..27eb397 100644 --- a/app/src/main/res/layout/fragment_cards.xml +++ b/app/src/main/res/layout/fragment_cards.xml @@ -1,20 +1,82 @@ - + tools:layout_marginBottom="@dimen/navigation_bar_height" + tools:layout_marginTop="@dimen/status_bar_height"> - + android:background="@null"> - + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="match_parent" + android:clickable="true" + android:elevation="@dimen/statistics_elevation" + app:behavior_hideable="true" + app:behavior_peekHeight="@dimen/statistics_bottom_peek_height" + app:layout_behavior="@string/bottom_sheet_behavior" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_decks.xml b/app/src/main/res/layout/fragment_decks.xml index 3a664a7..92f530f 100644 --- a/app/src/main/res/layout/fragment_decks.xml +++ b/app/src/main/res/layout/fragment_decks.xml @@ -1,21 +1,67 @@ - + tools:layout_marginBottom="@dimen/navigation_bar_height" + tools:layout_marginTop="@dimen/status_bar_height"> - + android:background="@null"> - + + + + + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_matches.xml b/app/src/main/res/layout/fragment_matches.xml new file mode 100644 index 0000000..7ac3a22 --- /dev/null +++ b/app/src/main/res/layout/fragment_matches.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_matches_history.xml b/app/src/main/res/layout/fragment_matches_history.xml new file mode 100644 index 0000000..81644a1 --- /dev/null +++ b/app/src/main/res/layout/fragment_matches_history.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_matches_statistics.xml b/app/src/main/res/layout/fragment_matches_statistics.xml new file mode 100644 index 0000000..6429467 --- /dev/null +++ b/app/src/main/res/layout/fragment_matches_statistics.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/include_login_button.xml b/app/src/main/res/layout/include_login_button.xml index 1ad9972..9ea4176 100644 --- a/app/src/main/res/layout/include_login_button.xml +++ b/app/src/main/res/layout/include_login_button.xml @@ -8,7 +8,7 @@ android:layout_marginBottom="@dimen/large_margin" android:background="@drawable/selector_btn_google_signin_light" android:text="@string/sign_in_with_google" - android:textColor="@color/secondary_text" + android:textColor="@color/grey_500" android:textStyle="bold" android:visibility="invisible" tools:showIn="@layout/fragment_cards_list" diff --git a/app/src/main/res/layout/include_new_matches.xml b/app/src/main/res/layout/include_new_matches.xml new file mode 100644 index 0000000..260affe --- /dev/null +++ b/app/src/main/res/layout/include_new_matches.xml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +