Merge branch 'tutorial' into 'master'

Tutorials

See merge request pixeldroid/PixelDroid!601
This commit is contained in:
Matthieu 2024-08-30 14:17:55 +00:00
commit 49b4f7e445
17 changed files with 684 additions and 26 deletions

View File

@ -171,7 +171,7 @@ dependencies {
implementation 'androidx.hilt:hilt-common:1.2.0'
implementation 'androidx.hilt:hilt-work:1.2.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.1'
/**
* AndroidX dependencies:
@ -226,6 +226,9 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
//Interactive tutorial
implementation 'com.getkeepsafe.taptargetview:taptargetview:1.14.0'
implementation 'com.google.android.material:material:1.12.0'
//Dagger (dependency injection)

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
@ -20,6 +21,8 @@ import org.pixeldroid.app.BuildConfig
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityLoginBinding
import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.settings.SettingsActivity
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.openUrl
@ -89,11 +92,25 @@ class LoginActivity : BaseActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.finishedLogin.collectLatest {
if (it) {
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
when (it) {
LoginActivityViewModel.FinishedLogin.Finished -> {
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
}
LoginActivityViewModel.FinishedLogin.FinishedFirstTime -> MaterialAlertDialogBuilder(binding.root.context)
.setMessage(R.string.first_time_question)
.setPositiveButton(android.R.string.ok) { _, _ ->
val intent = Intent(this@LoginActivity, SettingsActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(START_TUTORIAL, true)
startActivity(intent)
}
.setNegativeButton(R.string.skip_tutorial) { _, _ -> model.finishLogin()}
.show()
LoginActivityViewModel.FinishedLogin.NotFinished -> {}
}
}
}

View File

@ -50,7 +50,10 @@ class LoginActivityViewModel @Inject constructor(
private val _loadingState: MutableStateFlow<LoginState> = MutableStateFlow(LoginState(LoginState.LoadingState.Resting))
val loadingState = _loadingState.asStateFlow()
private val _finishedLogin = MutableStateFlow(false)
enum class FinishedLogin {
NotFinished, Finished, FinishedFirstTime
}
private val _finishedLogin = MutableStateFlow(FinishedLogin.NotFinished)
val finishedLogin = _finishedLogin.asStateFlow()
private val _promptOauth: MutableStateFlow<PromptOAuth?> = MutableStateFlow(null)
@ -207,6 +210,7 @@ class LoginActivityViewModel @Inject constructor(
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
try {
val firstTime = db.userDao().getActiveUser() == null
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
db.userDao().deActivateActiveUsers()
addUser(
@ -220,12 +224,14 @@ class LoginActivityViewModel @Inject constructor(
clientSecret = clientSecret
)
apiHolder.setToCurrentUser()
fetchNotifications()
_finishedLogin.value = if(firstTime) FinishedLogin.FinishedFirstTime else FinishedLogin.Finished
} catch (exception: Exception) {
return failedRegistration(R.string.verify_credentials)
}
fetchNotifications()
_finishedLogin.value = true
}
// Fetch the latest notifications of this account, to avoid launching old notifications
@ -280,4 +286,8 @@ class LoginActivityViewModel @Inject constructor(
_loadingState.value = LoginState(LoginState.LoadingState.Resting)
}
fun finishLogin() {
_finishedLogin.value = FinishedLogin.Finished
}
}

View File

@ -12,7 +12,10 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
@ -34,6 +37,8 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.google.android.material.color.DynamicColors
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView
@ -50,6 +55,7 @@ import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
import org.pixeldroid.app.R
@ -66,6 +72,8 @@ import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
import org.pixeldroid.app.profile.ProfileActivity
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
import org.pixeldroid.app.settings.SettingsActivity
import org.pixeldroid.app.settings.SettingsActivity.SettingsFragment
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.Tab
import org.pixeldroid.app.utils.api.objects.Notification
@ -87,6 +95,7 @@ import java.time.Instant
class MainActivity : BaseActivity() {
private lateinit var tabStored: List<Tab>
private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null
@ -127,10 +136,14 @@ class MainActivity : BaseActivity() {
setupTabs()
val showNotification: Boolean = intent.getBooleanExtra(SHOW_NOTIFICATION_TAG, false)
val showTutorial: Int = intent.getIntExtra(START_TUTORIAL, -1)
if(showNotification){
binding.viewPager.currentItem = 3
} else if(showTutorial >= 0) {
showTutorial(showTutorial)
}
if (ActivityCompat.checkSelfPermission(applicationContext,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
@ -141,6 +154,220 @@ class MainActivity : BaseActivity() {
}
}
private fun showTutorial(showTutorial: Int) {
when(showTutorial){
0 -> tutorialOnTabs(Tab.HOME_FEED)
1 -> tutorialOnTabs(Tab.CREATE_FEED)
2 -> dmTutorial()
else -> return
}
}
private fun tutorialOnTabs(tab: Tab) {
val target = (binding.tabs as? NavigationBarView)?.let{ findTab(it, tab) } ?: return //TODO tablet landscape not supported
when(tab){
Tab.HOME_FEED -> homeTutorial(target)
Tab.SEARCH_DISCOVER_FEED -> homeTutorialSearch(target)
Tab.PUBLIC_FEED -> homeTutorialPublic(target)
Tab.NOTIFICATIONS_FEED -> homeTutorialNotifications(target)
Tab.CREATE_FEED -> createTutorial(target)
else -> return
}
}
private fun dmTutorial(){
val target = (binding.tabs as? NavigationBarView)?.let{ findTab(it, Tab.DIRECT_MESSAGES) } ?: binding.mainDrawerButton ?: return //TODO tablet landscape not supported
if(target is ImageButton) {
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target, getString(R.string.dm_tutorial_drawer))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
dmTutorial2(null)
}
})
} else dmTutorial2(target)
}
private fun findViewWithText(root: ViewGroup, text: String?): View? {
for (i in 0 until root.childCount) {
val child = root.getChildAt(i)
if (child is TextView) {
if (child.text.toString().contains(text!!)) {
return child
}
}
if (child is ViewGroup) {
val result = findViewWithText(child, text)
if (result != null) {
return result
}
}
}
return null
}
private fun dmTutorial2(target: View?) {
lifecycleScope.launch {
var target = target ?: findViewWithText(binding.drawer as ViewGroup, getString(R.string.direct_messages))
while (target == null) {
target = findViewWithText(binding.drawer as ViewGroup, getString(R.string.direct_messages))
delay(100)
}
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target, getString(R.string.direct_messages),
getString(R.string.dm_tutorial_text))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
//tutorialOnTabs(Tab.NOTIFICATIONS_FEED)
}
})
}
}
private fun homeTutorialPublic(target: View) {
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target, getString(R.string.public_feed),
getString(R.string.public_feed_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
tutorialOnTabs(Tab.NOTIFICATIONS_FEED)
}
})
}
private fun homeTutorialNotifications(target: View) {
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target,
getString(R.string.notifications_tutorial_title),
getString(R.string.notifications_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
}
})
}
private fun homeTutorialSearch(target: View) {
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target,
getString(R.string.discover_tutorial_title),
getString(R.string.discover_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
tutorialOnTabs(Tab.PUBLIC_FEED)
}
})
}
private fun homeTutorial(target: View) {
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target,
getString(R.string.home_feed_tutorial_title),
getString(R.string.home_feed_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
tutorialOnTabs(Tab.SEARCH_DISCOVER_FEED)
}
})
}
private fun createTutorial(target: View) {
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(target, getString(R.string.create_tutorial_title),
getString(R.string.create_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
target.performClick()
lifecycleScope.launch {
var targetCamera = findViewById<View>(R.id.camera_capture_button)
while (targetCamera == null) {
targetCamera = findViewById(R.id.camera_capture_button)
delay(100)
}
TapTargetView.showFor(
this@MainActivity,
TapTarget.forView(targetCamera,
getString(R.string.create_tutorial_title_2),
getString(R.string.create_tutorial_explanation_2))
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
targetCamera.performClick()
}
override fun onTargetCancel(view: TapTargetView?) {
super.onTargetCancel(view)
intent.removeExtra(START_TUTORIAL)
}
})
}
}
})
}
private fun findTab(navBar: NavigationBarView, tab: Tab): View? {
val index = tabStored.indexOf(tab)
for (i in 0 until navBar.childCount) {
val child = navBar.getChildAt(i)
if (child is ViewGroup) {
return child.getChildAt(index)
}
}
return null
}
private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
@ -515,6 +742,8 @@ class MainActivity : BaseActivity() {
)
}
tabStored = tabs
val bottomNavigationMenu: Menu? = (binding.tabs as? NavigationBarView)?.menu?.apply {
clear()
}

View File

@ -5,8 +5,15 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.utils.BaseActivity
@ -58,6 +65,126 @@ class PostCreationActivity : BaseActivity() {
supportFragmentManager.findFragmentById(R.id.postCreationContainer) as NavHostFragment
navController = navHostFragment.navController
navController.setGraph(R.navigation.post_creation_graph)
lifecycleScope.launch {
var targetCamera = findViewById<View>(R.id.toggleStoryPost)
while (targetCamera == null) {
targetCamera = findViewById(R.id.toggleStoryPost)
delay(100)
}
TapTargetView.showFor(
this@PostCreationActivity, // `this` is an Activity
TapTarget.forView(targetCamera,
getString(R.string.story_tutorial_title),
getString(R.string.story_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
findViewById<View>(R.id.buttonStory)?.performClick()
TapTargetView.showFor(
this@PostCreationActivity, // `this` is an Activity
TapTarget.forView(findViewById(R.id.editPhotoButton),
getString(R.string.edit_tutorial_title),
getString(R.string.edit_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
findViewById<View>(R.id.editPhotoButton)?.performClick()
TapTargetView.showFor(
this@PostCreationActivity, // `this` is an Activity
TapTarget.forView(findViewById(R.id.tv_caption),
getString(R.string.media_description_tutorial_title),
getString(R.string.media_description_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
findViewById<View>(R.id.tv_caption)?.performClick()
lifecycleScope.launch {
delay(1000)
var tv_caption = findViewById<View>(R.id.tv_caption)
while (tv_caption == null || tv_caption.visibility != View.VISIBLE) {
tv_caption = findViewById(R.id.tv_caption)
delay(100)
}
TapTargetView.showFor(
this@PostCreationActivity, // `this` is an Activity
TapTarget.forView(findViewById(R.id.post_creation_next_button),
getString(
R.string.picture_tutorial_title
),
getString(R.string.picture_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
findViewById<View>(R.id.post_creation_next_button)?.performClick()
showAccountChooser()
}
})
}
}
})
}
})
}
})
}
}
private fun showAccountChooser() {
lifecycleScope.launch {
var toolbar = findViewById<View>(R.id.top_bar) as? MaterialToolbar
while (toolbar == null) {
toolbar = findViewById(R.id.top_bar) as? MaterialToolbar
delay(100)
}
TapTargetView.showFor(
this@PostCreationActivity, // `this` is an Activity
TapTarget.forToolbarMenuItem(
toolbar,
R.id.action_switch_accounts,
getString(R.string.switch_accounts_tutorial_title),
getString(R.string.switch_accounts_tutorial_explanation)
)
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
showPostButton()
}
})
}
}
private fun showPostButton() {
TapTargetView.showFor(
this@PostCreationActivity, // `this` is an Activity
TapTarget.forView(findViewById(R.id.post_submission_send_button),
getString(R.string.post_button_tutorial_title),
getString(R.string.post_button_tutorial_explanation))
.transparentTarget(true)
.targetRadius(60),
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
findViewById<View>(R.id.post_creation_next_button)?.performClick()
}
})
}
override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp()

View File

@ -37,6 +37,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.FragmentCameraBinding
import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
import org.pixeldroid.app.utils.BaseFragment
import java.io.File
import java.util.concurrent.ExecutorService
@ -68,6 +69,7 @@ class CameraFragment : BaseFragment() {
private var inActivity by Delegates.notNull<Boolean>()
private var addToStory by Delegates.notNull<Boolean>()
private var tutorial by Delegates.notNull<Int>()
private var filePermissionDialogLaunched: Boolean = false
@ -90,6 +92,8 @@ class CameraFragment : BaseFragment() {
inActivity = arguments?.getBoolean(CAMERA_ACTIVITY) ?: false
addToStory = arguments?.getBoolean(CAMERA_ACTIVITY_STORY) ?: false
tutorial = arguments?.getInt(START_TUTORIAL) ?: -1
binding = FragmentCameraBinding.inflate(layoutInflater)
return binding.root
@ -457,6 +461,9 @@ class CameraFragment : BaseFragment() {
if(addToStory){
intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory)
}
if(!inActivity && tutorial != -1){
intent.putExtra(START_TUTORIAL, true)
}
startActivity(intent)
}
}

View File

@ -3,7 +3,6 @@ package org.pixeldroid.app.settings
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
@ -16,13 +15,17 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.LayoutTabsArrangeBinding
import org.pixeldroid.app.utils.Tab
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.TabsDatabaseEntity
@ -31,20 +34,23 @@ import javax.inject.Inject
@AndroidEntryPoint
class ArrangeTabsFragment: DialogFragment() {
private lateinit var binding: LayoutTabsArrangeBinding
@Inject
lateinit var db: AppDatabase
private val model: ArrangeTabsViewModel by viewModels()
var showTutorial = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater: LayoutInflater = requireActivity().layoutInflater
val dialogView: View = inflater.inflate(R.layout.layout_tabs_arrange, null)
binding = LayoutTabsArrangeBinding.inflate(layoutInflater)
val itemCount = model.initTabsChecked()
model.initTabsButtons(itemCount, requireContext())
val listFeed: RecyclerView = dialogView.findViewById(R.id.tabs)
val listFeed: RecyclerView = binding.tabs
val listAdapter = ListViewAdapter(model)
listFeed.adapter = listAdapter
listFeed.layoutManager = LinearLayoutManager(requireActivity())
@ -68,7 +74,7 @@ class ArrangeTabsFragment: DialogFragment() {
val dialog = MaterialAlertDialogBuilder(requireContext()).apply {
setIcon(R.drawable.outline_bottom_navigation)
setTitle(R.string.arrange_tabs_summary)
setView(dialogView)
setView(binding.root)
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.ok) { _, _ ->
// Save values into preferences
@ -81,10 +87,77 @@ class ArrangeTabsFragment: DialogFragment() {
}
}
}.create()
if (showTutorial) showTutorial(dialog)
return dialog
}
private fun showTutorial(dialog: Dialog){
lifecycleScope.launch {
var handle = binding.tabs.findViewHolderForLayoutPosition(0)?.itemView?.findViewById<ImageView>(R.id.dragHandle)
while (handle == null) {
handle = binding.tabs.findViewHolderForLayoutPosition(0)?.itemView?.findViewById(R.id.dragHandle)
delay(100)
}
TapTargetView.showFor(
dialog,
TapTarget.forView(handle, getString(R.string.drag_customtabs_tutorial))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
val checkBox = binding.tabs.findViewHolderForLayoutPosition(0)?.itemView?.findViewById<View>(R.id.checkBox)
TapTargetView.showFor(
dialog,
TapTarget.forView(checkBox,
getString(R.string.de_activate_tabs_tutorial))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
val index = (Tab.defaultTabs + Tab.otherTabs).size - 1
binding.tabs.scrollToPosition(index)
lifecycleScope.launch {
var hashtag =
binding.tabs.findViewHolderForLayoutPosition(index)?.itemView?.findViewById<View>(
R.id.textView
)
while (hashtag == null) {
hashtag =
binding.tabs.findViewHolderForLayoutPosition(index)?.itemView?.findViewById(
R.id.textView
)
delay(100)
}
TapTargetView.showFor(
dialog,
TapTarget.forView(
hashtag,
getString(R.string.custom_feed_tutorial_title),
getString(R.string.custom_feed_tutorial_explanation)
)
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
}
})
}
}
})
}
})
}
}
inner class ListViewAdapter(val model: ArrangeTabsViewModel):
RecyclerView.Adapter<RecyclerView.ViewHolder>() {

View File

@ -4,38 +4,62 @@ import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.SettingsBinding
import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.settings.TutorialSettingsDialog.Companion.START_TUTORIAL
import org.pixeldroid.app.utils.setThemeFromPreferences
import org.pixeldroid.common.ThemedActivity
@AndroidEntryPoint
class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private var restartMainOnExit = false
private lateinit var binding: SettingsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = SettingsBinding.inflate(layoutInflater)
binding = SettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportFragmentManager
.beginTransaction()
.replace(R.id.settings, SettingsFragment())
.replace(R.id.settings, SettingsFragment(), "topsettingsfragment")
.commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val showTutorial = intent.getBooleanExtra(START_TUTORIAL, false)
if(showTutorial){
lifecycleScope.launch {
var target =
(supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(10)
while (target == null) {
target = (supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(10)
delay(100)
}
target.performClick()
}
}
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Handle the back button event
// If a setting (for example language or theme) was changed, the main activity should be
@ -98,6 +122,36 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC
super.startActivity(intent)
}
fun customTabsTutorial(){
lifecycleScope.launch {
var target =
(supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(5)
while (target == null) {
target = (supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment)?.scrollToArrangeTabs(5)
delay(100)
}
TapTargetView.showFor(
this@SettingsActivity,
TapTarget.forView(target, getString(R.string.arrange_tabs_tutorial_title))
.transparentTarget(true)
.targetRadius(60), // Specify the target radius (in dp)
object : TapTargetView.Listener() {
// The listener can listen for regular clicks, long clicks or cancels
override fun onTargetClick(view: TapTargetView?) {
super.onTargetClick(view) // This call is optional
// Perform action for the current target
val dialogFragment = ArrangeTabsFragment().apply { showTutorial = true }
dialogFragment.setTargetFragment(
(supportFragmentManager.findFragmentByTag("topsettingsfragment") as? SettingsFragment),
0
)
dialogFragment.show(supportFragmentManager, "settings_fragment")
}
})
}
}
class SettingsFragment : PreferenceFragmentCompat() {
override fun onDisplayPreferenceDialog(preference: Preference) {
var dialogFragment: DialogFragment? = null
@ -107,6 +161,8 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC
dialogFragment = LanguageSettingFragment()
} else if (preference.key == "arrange_tabs") {
dialogFragment = ArrangeTabsFragment()
} else if (preference.key == "tutorial") {
dialogFragment = TutorialSettingsDialog()
}
if (dialogFragment != null) {
dialogFragment.setTargetFragment(this, 0)
@ -115,6 +171,16 @@ class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceC
super.onDisplayPreferenceDialog(preference)
}
}
fun scrollToArrangeTabs(position: Int): View? {
//Hardcoded positions because it's too annoying to find!
if (listView != null && position != -1) {
listView.post {
listView.smoothScrollToPosition(position)
}
}
return listView.findViewHolderForAdapterPosition(position)?.itemView
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)

View File

@ -0,0 +1,77 @@
package org.pixeldroid.app.settings;
import android.app.Dialog
import android.content.Intent
import android.graphics.Typeface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.fragment.app.DialogFragment
import com.getkeepsafe.taptargetview.TapTarget
import com.getkeepsafe.taptargetview.TapTargetView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R
import org.pixeldroid.app.main.MainActivity
import org.pixeldroid.app.utils.Tab
class TutorialSettingsDialog: DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val items = arrayOf(
Pair(R.string.feeds_tutorial, R.drawable.ic_home_white_24dp),
Pair(R.string.create_tutorial, R.drawable.photo_camera),
Pair(R.string.dm_tutorial, R.drawable.message),
Pair(R.string.custom_tabs_tutorial, R.drawable.outline_bottom_navigation)
)
val adapter = object : ArrayAdapter<Pair<Int, Int>>(requireContext(), android.R.layout.simple_list_item_1, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: TextView = if (convertView == null) {
LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false) as TextView
} else {
convertView as TextView
}
val item = getItem(position)
if (item != null) {
view.setText(item.first)
view.setTypeface(null, Typeface.NORMAL) // Set the typeface to normal
view.setCompoundDrawablesWithIntrinsicBounds(item.second, 0, 0, 0)
view.compoundDrawablePadding = 16 // Add padding between text and drawable
}
view.setPadding(0, 32, 0, 32)
return view
}
}
return MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.tutorial_choice))
.setAdapter(adapter) { _, which ->
if(which == 3){
customTabsTutorial()
return@setAdapter
}
val intent = Intent(requireContext(), MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(START_TUTORIAL, which)
startActivity(intent)
}
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.create()
}
private fun customTabsTutorial() {
(requireActivity() as SettingsActivity).customTabsTutorial()
}
companion object {
const val START_TUTORIAL = "tutorial_start_intent"
}
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?attr/colorOnBackground" android:pathData="M19,2L5,2c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h4l3,3 3,-3h4c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM13,18h-2v-2h2v2zM15.07,10.25l-0.9,0.92C13.45,11.9 13,12.5 13,14h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,8c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
</vector>

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
<path android:fillColor="?attr/colorOnBackground" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -1,5 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
<path android:fillColor="?attr/colorOnBackground" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
<path android:fillColor="?attr/colorOnBackground" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>

View File

@ -1,6 +1,6 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path android:fillColor="#FF000000" android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
<path android:fillColor="?attr/colorOnBackground" android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path android:fillColor="?attr/colorOnBackground" android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector>

View File

@ -361,4 +361,43 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="dm_target">Target username</string>
<string name="new_dm">Message you want to write. Say hello!</string>
<string name="new_dm_error">Error sending your message! Check the username</string>
<string name="tutorial">Tutorial</string>
<string name="tutorial_explanation">Explanations on how to use PixelDroid and Pixelfed</string>
<string name="feeds_tutorial">Feeds, how do they work? Where do they come from?</string>
<string name="create_tutorial">A little walk through creating posts</string>
<string name="tutorial_choice">What could you use some help with?</string>
<string name="dm_tutorial">Direct Messages: keep in touch!</string>
<string name="custom_tabs_tutorial">Customize what tabs show up on the main PixelDroid screen!</string>
<string name="first_time_question">It seems like it might be your first time using PixelDroid. Do you want to open a tutorial? You can always find the tutorials in the settings.</string>
<string name="skip_tutorial">No, continue</string>
<string name="dm_tutorial_text">Send messages to other Pixelfed users: on your instance or on others</string>
<string name="public_feed_tutorial_explanation">This feed contains all the posts on your instance! Maybe you can find some interesting posts here :)</string>
<string name="notifications_tutorial_title">Notifications keep you in the loop</string>
<string name="notifications_tutorial_explanation">PixelDroid will also send you push notifications to make sure you don\'t miss anything!</string>
<string name="home_feed_tutorial_title">This is your home feed</string>
<string name="home_feed_tutorial_explanation">The posts of the people you follow will show up here. No algorithms, just chronological goodness. Only you decide what you want to see!</string>
<string name="create_tutorial_title">This is where everything begins</string>
<string name="create_tutorial_explanation">First, let\'s navigate to the create tab. Click here</string>
<string name="create_tutorial_title_2">Take a picture to share</string>
<string name="create_tutorial_explanation_2">It doesn\'t have to be very good for now</string>
<string name="story_tutorial_title">Story or Post?</string>
<string name="story_tutorial_explanation">Stories are short-lived: engage your followers and keep them coming back for more. Try them out!</string>
<string name="edit_tutorial_title">Edit your picture to make it shine ✨</string>
<string name="edit_tutorial_explanation">You can add filters, draw or add text, edit video, and more! 📷</string>
<string name="media_description_tutorial_title">Don\'t forget to add a media description!</string>
<string name="media_description_tutorial_explanation">This helps make Pixelfed accessible to everyone, and also lets you clarify what we\'re supposed to see in your pretty image ;)</string>
<string name="picture_tutorial_title">Take a picture to share</string>
<string name="picture_tutorial_explanation">It doesn\'t have to be very good for now</string>
<string name="switch_accounts_tutorial_title">Switch accounts!</string>
<string name="switch_accounts_tutorial_explanation">PixelDroid supports using multiple Pixelfed accounts. Make sure you don\'t post those cat pics on the dog-only instance! 😱</string>
<string name="post_button_tutorial_title">Final stretch! Post that picture</string>
<string name="post_button_tutorial_explanation">Have fun sharing your pictures with the world! Click anywhere else to cancel and keep looking around :)</string>
<string name="drag_customtabs_tutorial">Drag this to change the order of the tabs</string>
<string name="de_activate_tabs_tutorial">De-activate tabs you don\'t need</string>
<string name="custom_feed_tutorial_title">Create a custom feed with a hashtag you like</string>
<string name="custom_feed_tutorial_explanation">You really like cats? Try #caturday! Lakes? #lake! Or #hiking? And it will be right there in a tab</string>
<string name="arrange_tabs_tutorial_title">First open the \"Arrange tabs\" settings</string>
<string name="discover_tutorial_title">This tab can get you started finding interesting accounts to follow</string>
<string name="discover_tutorial_explanation">Maybe take a look at the trending posts 📈, or discover some random posts every day to broaden your horizons and find the real gems! 💎</string>
<string name="dm_tutorial_drawer">First open the drawer menu</string>
</resources>

View File

@ -54,6 +54,11 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/about">
<ListPreference
android:key="tutorial"
android:title="@string/tutorial"
android:summary="@string/tutorial_explanation"
android:icon="@drawable/help" />
<Preference android:title="@string/about"
android:key="about"
android:summary="@string/about_pixeldroid"

@ -1 +1 @@
Subproject commit 702d14fe701343958337efa1b4eb31f0250849f6
Subproject commit def947b5b1392b3282174bebd0217037f66d0362