feat: add targeting list
This commit is contained in:
parent
be86e353d1
commit
2da9616b2b
|
@ -155,4 +155,4 @@
|
|||
<option name="WRAP_ON_TYPING" value="0" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="1.8" />
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
|
|
|
@ -58,16 +58,16 @@ dependencies {
|
|||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
// default kotlin android stuff
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
|
||||
// preference ui objects
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
// libsodium for android
|
||||
|
@ -100,11 +100,11 @@ dependencies {
|
|||
implementation 'com.leinardi.android:speed-dial:3.1.1'
|
||||
// tests
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
// desugaring
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
// sentry
|
||||
implementation 'io.sentry:sentry-android:4.0.0'
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ import io.annaclemens.xivchat.model.message.ClientCatchUp
|
|||
import io.annaclemens.xivchat.model.message.ClientChannel
|
||||
import io.annaclemens.xivchat.model.message.ClientMessage
|
||||
import io.annaclemens.xivchat.model.message.ClientPlayerList
|
||||
import io.annaclemens.xivchat.model.message.ClientPreference
|
||||
import io.annaclemens.xivchat.model.message.ClientPreferences
|
||||
import io.annaclemens.xivchat.model.message.ClientShutdown
|
||||
import io.annaclemens.xivchat.model.message.InputChannel
|
||||
import io.annaclemens.xivchat.model.message.Ping
|
||||
|
@ -49,6 +51,8 @@ import io.annaclemens.xivchat.model.message.SuccessMessage
|
|||
import io.annaclemens.xivchat.ui.friends.FriendListViewModel
|
||||
import io.annaclemens.xivchat.ui.messages.MessagesViewModel
|
||||
import io.annaclemens.xivchat.ui.servers.ServersViewModel
|
||||
import io.annaclemens.xivchat.ui.targeting.TargetingListViewModel
|
||||
import io.annaclemens.xivchat.ui.targeting.TargetingPlayer
|
||||
import io.annaclemens.xivchat.util.AlertDialogFragment
|
||||
import io.annaclemens.xivchat.util.AppKeyExchange
|
||||
import io.annaclemens.xivchat.util.CharacterSearchResults
|
||||
|
@ -83,6 +87,8 @@ import kotlinx.coroutines.withTimeout
|
|||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
class ConnectionService : Service() {
|
||||
companion object {
|
||||
|
@ -174,7 +180,7 @@ class ConnectionService : Service() {
|
|||
.build()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return this.binder
|
||||
}
|
||||
|
||||
|
@ -317,6 +323,9 @@ class ConnectionService : Service() {
|
|||
this.service.app.findNavController(R.id.nav_host_fragment).navigate(R.id.nav_messages_tabs)
|
||||
}
|
||||
|
||||
// send protocol preferences
|
||||
this.sendPreferences(txStream, tx)
|
||||
|
||||
// request catch-up or backlog
|
||||
this.requestBacklog(txStream, tx)
|
||||
|
||||
|
@ -504,6 +513,13 @@ class ConnectionService : Service() {
|
|||
SecretMessage.writeSecretMessage(this@ConnectionRunner.service.app.sodium, txStream, tx, bytes)
|
||||
}
|
||||
|
||||
private suspend fun sendPreferences(txStream: ByteWriteChannel, tx: Key) {
|
||||
val prefs = ClientPreferences(mapOf(
|
||||
ClientPreference.TargetingListSupport to true,
|
||||
))
|
||||
SecretMessage.writeSecretMessage(this.service.app.sodium, txStream, tx, prefs.encode())
|
||||
}
|
||||
|
||||
private class EventLoop(private val runner: ConnectionRunner) {
|
||||
private var loop = true
|
||||
|
||||
|
@ -652,6 +668,34 @@ class ConnectionService : Service() {
|
|||
vm.friends.value = list.players.toList()
|
||||
}
|
||||
}
|
||||
PlayerListType.Targeting -> {
|
||||
val vm: TargetingListViewModel by this.runner.service.app.viewModels()
|
||||
this.runner.service.handler.post {
|
||||
val now = Date()
|
||||
val data = list.players
|
||||
val oldTargeting = vm.targeting.value.orEmpty()
|
||||
|
||||
val newTargeting = data
|
||||
.asSequence()
|
||||
.filter { new -> oldTargeting.all { current -> current.player.name != new.name && current.player.homeWorld != new.homeWorld } }
|
||||
.map { TargetingPlayer(it, true, now) }
|
||||
val finalTargeting = oldTargeting
|
||||
.asSequence()
|
||||
.map {
|
||||
var timestamp = it.timestamp
|
||||
val current = data.any { dataPlayer -> dataPlayer.name == it.player.name && dataPlayer.homeWorld == it.player.homeWorld }
|
||||
if (current || it.current) {
|
||||
timestamp = now
|
||||
}
|
||||
|
||||
TargetingPlayer(it.player, current, timestamp)
|
||||
}
|
||||
.plus(newTargeting)
|
||||
.sortedByDescending { it.current }
|
||||
.sortedByDescending { it.timestamp }
|
||||
vm.targeting.value = finalTargeting.toList()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// TODO: other player list types
|
||||
}
|
||||
|
|
|
@ -204,6 +204,7 @@ class MainActivity : AppCompatActivity(), AlertDialogFragment.DialogHost, EditTe
|
|||
R.id.nav_servers,
|
||||
R.id.nav_messages_tabs,
|
||||
R.id.nav_friend_list,
|
||||
R.id.nav_targeting_list,
|
||||
R.id.nav_settings,
|
||||
R.id.nav_about,
|
||||
),
|
||||
|
|
|
@ -104,3 +104,33 @@ class ClientChannel(private val channel: InputChannel) {
|
|||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
class ClientPreferences(private val preferences: Map<ClientPreference, Any>) {
|
||||
fun encode(): ByteArray {
|
||||
val packer = MessagePack.newDefaultBufferPacker()
|
||||
packer.packArrayHeader(1)
|
||||
packer.packMapHeader(this.preferences.size)
|
||||
for (entry in this.preferences) {
|
||||
packer.packByte(entry.key.code.toByte())
|
||||
packer.packBoolean(entry.value == true) // TODO: fix
|
||||
}
|
||||
packer.close()
|
||||
|
||||
val bytes = packer.toByteArray()
|
||||
val buf = ByteArray(1 + bytes.size)
|
||||
buf[0] = ClientOperation.Preferences.code
|
||||
bytes.copyInto(buf, 1)
|
||||
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
enum class ClientPreference(val code: UByte) {
|
||||
BacklogNewestMessagesFirst(0u),
|
||||
TargetingListSupport(1u);
|
||||
|
||||
companion object {
|
||||
private val map = values().associateBy(ClientPreference::code)
|
||||
fun fromCode(code: UByte) = this.map[code]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ enum class PlayerListType(val code: Byte) {
|
|||
Party(1),
|
||||
Friend(2),
|
||||
Linkshell(3),
|
||||
CrossLinkshell(4);
|
||||
CrossLinkshell(4),
|
||||
Targeting(5);
|
||||
|
||||
companion object {
|
||||
private val map = values().associateBy(PlayerListType::code)
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package io.annaclemens.xivchat.ui.targeting
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.annaclemens.xivchat.R
|
||||
import io.annaclemens.xivchat.databinding.TargetingListElementBinding
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class TargetingListAdapter(private val context: Context) : RecyclerView.Adapter<TargetingListAdapter.ViewHolder>() {
|
||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val binding = TargetingListElementBinding.bind(itemView)
|
||||
|
||||
val name = this.binding.targetingName
|
||||
val timestamp = this.binding.targetingTime
|
||||
}
|
||||
|
||||
private val inflater = LayoutInflater.from(this.context)
|
||||
private var playerList: List<TargetingPlayer> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val itemView = this.inflater.inflate(R.layout.targeting_list_element, parent, false)
|
||||
return this.ViewHolder(itemView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val targeting = this.playerList[position]
|
||||
|
||||
// TODO: add send tell context menu?
|
||||
|
||||
holder.name.text = targeting.player.name ?: this.context.getString(R.string.friend_list_could_not_retrieve)
|
||||
holder.timestamp.text = if (targeting.current) {
|
||||
this.context.getString(R.string.targeting_now)
|
||||
} else {
|
||||
if (Instant.now().epochSecond - targeting.timestamp.toInstant().epochSecond > 24 * 60 * 60) {
|
||||
SimpleDateFormat("dd/MM", Locale.getDefault()).format(targeting.timestamp)
|
||||
} else {
|
||||
DateFormat.getTimeInstance(DateFormat.SHORT).format(targeting.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
holder.name.isEnabled = targeting.current
|
||||
holder.timestamp.isEnabled = targeting.current
|
||||
}
|
||||
|
||||
override fun getItemCount() = this.playerList.size
|
||||
|
||||
fun setTargetingList(list: List<TargetingPlayer>) {
|
||||
this.playerList = list
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package io.annaclemens.xivchat.ui.targeting
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.annaclemens.xivchat.databinding.FragmentTargetingBinding
|
||||
import io.annaclemens.xivchat.util.getApp
|
||||
|
||||
class TargetingListFragment : Fragment() {
|
||||
private lateinit var viewAdapter: TargetingListAdapter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val vm: TargetingListViewModel by this.getApp().viewModels()
|
||||
val binding = FragmentTargetingBinding.inflate(inflater)
|
||||
|
||||
this.viewAdapter = TargetingListAdapter(this.requireContext())
|
||||
binding.targetingList.apply {
|
||||
this.setHasFixedSize(true)
|
||||
this.layoutManager = LinearLayoutManager(this.context)
|
||||
this.adapter = this@TargetingListFragment.viewAdapter
|
||||
}
|
||||
|
||||
this.updateEmptyText(binding, vm.targeting.value.orEmpty().isEmpty())
|
||||
|
||||
vm.targeting.observe(this.viewLifecycleOwner) {
|
||||
this.updateEmptyText(binding, it.orEmpty().isEmpty())
|
||||
|
||||
this.viewAdapter.setTargetingList(it.orEmpty())
|
||||
this.viewAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun updateEmptyText(binding: FragmentTargetingBinding, visible: Boolean) {
|
||||
binding.textView.visibility = if (visible) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.annaclemens.xivchat.ui.targeting
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class TargetingListViewModel : ViewModel() {
|
||||
val targeting: MutableLiveData<List<TargetingPlayer>?> = MutableLiveData()
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package io.annaclemens.xivchat.ui.targeting
|
||||
|
||||
import io.annaclemens.xivchat.model.message.Player
|
||||
import java.util.*
|
||||
|
||||
data class TargetingPlayer(
|
||||
val player: Player,
|
||||
val current: Boolean,
|
||||
val timestamp: Date,
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M12,6c3.79,0 7.17,2.13 8.82,5.5C19.17,14.87 15.79,17 12,17s-7.17,-2.13 -8.82,-5.5C4.83,8.13 8.21,6 12,6m0,-2C7,4 2.73,7.11 1,11.5 2.73,15.89 7,19 12,19s9.27,-3.11 11,-7.5C21.27,7.11 17,4 12,4zM12,9c1.38,0 2.5,1.12 2.5,2.5S13.38,14 12,14s-2.5,-1.12 -2.5,-2.5S10.62,9 12,9m0,-2c-2.48,0 -4.5,2.02 -4.5,4.5S9.52,16 12,16s4.5,-2.02 4.5,-4.5S14.48,7 12,7z" />
|
||||
</vector>
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/targeting_empty" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/targetingList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/targetingName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||
tools:text="Goat Goatington" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/targetingTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAlignment="textEnd"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||
tools:text="04:45" />
|
||||
</LinearLayout>
|
|
@ -20,6 +20,10 @@
|
|||
android:id="@+id/nav_friend_list"
|
||||
android:icon="@drawable/people_black_24dp"
|
||||
android:title="@string/menu_friend_list" />
|
||||
<item
|
||||
android:id="@+id/nav_targeting_list"
|
||||
android:icon="@drawable/visibility_black_24dp"
|
||||
android:title="@string/menu_targeting" />
|
||||
</group>
|
||||
<group android:id="@+id/menuSettings">
|
||||
<item
|
||||
|
|
|
@ -89,6 +89,13 @@
|
|||
android:name="playerWorld"
|
||||
app:argType="integer" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_targeting_list"
|
||||
android:name="io.annaclemens.xivchat.ui.targeting.TargetingListFragment"
|
||||
android:label="@string/menu_targeting"
|
||||
tools:layout="@layout/fragment_targeting" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_scanned_servers"
|
||||
android:name="io.annaclemens.xivchat.ui.servers.scan.ScannedServersFragment"
|
||||
|
|
|
@ -208,4 +208,7 @@
|
|||
<string name="servers_scan_searching">Searching for servers…</string>
|
||||
<string name="servers_scan_instructions">Tap on a server below to add it.</string>
|
||||
<string name="friend_list_could_not_retrieve">(Could not retrieve)</string>
|
||||
<string name="menu_targeting">Targeting</string>
|
||||
<string name="targeting_now">Now</string>
|
||||
<string name="targeting_empty">No one has targeted you since connecting.</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.4.30"
|
||||
ext.kotlin_version = "1.5.20"
|
||||
ext.ktor_version = '1.4.0'
|
||||
ext.room_version = '2.2.6'
|
||||
ext.room_version = '2.3.0'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.3"
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
|
||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
|
||||
|
|
Loading…
Reference in New Issue