Merge branch 'view_model_fixes' into 'master'
Tablet mode Closes #389 See merge request pixeldroid/PixelDroid!593
This commit is contained in:
commit
579c75417d
|
@ -46,7 +46,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 34
|
||||
versionCode 33
|
||||
versionCode 34
|
||||
versionName "1.0.beta" + versionCode
|
||||
|
||||
//TODO add resConfigs("en", "fr", "ja",...) ?
|
||||
|
@ -164,15 +164,17 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
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'
|
||||
|
||||
/**
|
||||
* AndroidX dependencies:
|
||||
*/
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
|
@ -182,23 +184,23 @@ dependencies {
|
|||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.3.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.8.4"
|
||||
implementation "androidx.annotation:annotation:1.8.2"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation "androidx.activity:activity-ktx:1.9.1"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.2'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.1'
|
||||
implementation 'androidx.media2:media2-widget:1.3.0'
|
||||
implementation 'androidx.media2:media2-player:1.3.0'
|
||||
|
||||
|
||||
// Use the most recent version of CameraX
|
||||
def cameraX_version = '1.3.2'
|
||||
def cameraX_version = '1.3.4'
|
||||
implementation "androidx.camera:camera-core:$cameraX_version"
|
||||
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
||||
// CameraX Lifecycle library
|
||||
|
@ -220,11 +222,11 @@ dependencies {
|
|||
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
|
||||
//Dagger (dependency injection)
|
||||
implementation 'com.google.dagger:dagger:2.51'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.51'
|
||||
implementation 'com.google.dagger:dagger:2.51.1'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.51.1'
|
||||
|
||||
implementation('com.google.dagger:hilt-android:2.51')
|
||||
ksp 'com.google.dagger:hilt-compiler:2.51'
|
||||
|
@ -237,7 +239,7 @@ dependencies {
|
|||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
||||
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0'
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:3.1'
|
||||
implementation project(path: ':scrambler')
|
||||
implementation project(path: ':pixel_common')
|
||||
|
||||
|
@ -260,12 +262,6 @@ dependencies {
|
|||
// Add for NavController support
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
|
||||
|
||||
//iconics
|
||||
implementation 'com.mikepenz:iconics-core:5.4.0'
|
||||
implementation 'com.mikepenz:materialdrawer-iconics:9.0.2'
|
||||
implementation 'com.mikepenz:iconics-views:5.4.0'
|
||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||
|
||||
implementation 'com.github.ligi:tracedroid:4.1'
|
||||
|
||||
implementation 'me.relex:circleindicator:2.1.6'
|
||||
|
@ -280,7 +276,7 @@ dependencies {
|
|||
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
|
||||
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1'
|
||||
|
||||
androidTestImplementation 'androidx.work:work-testing:2.9.0'
|
||||
androidTestImplementation 'androidx.work:work-testing:2.9.1'
|
||||
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
@ -288,13 +284,13 @@ dependencies {
|
|||
|
||||
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
|
||||
|
||||
|
|
|
@ -53,9 +53,7 @@
|
|||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity
|
||||
android:name=".posts.ReportActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.StoriesActivity" />
|
||||
|
@ -76,39 +74,29 @@
|
|||
</activity>
|
||||
<activity
|
||||
android:name=".profile.FollowsActivity"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity
|
||||
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity
|
||||
android:name=".posts.PostActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity android:name=".profile.CollectionActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings2"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:parentActivityName=".main.MainActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".main.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.App.Starting"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -122,12 +110,10 @@
|
|||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:name=".login.LoginActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
@ -143,9 +129,7 @@
|
|||
android:name=".searchDiscover.SearchActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
|
@ -166,6 +150,16 @@
|
|||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,345 +0,0 @@
|
|||
package org.pixeldroid.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.ActivityLoginBinding
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Application
|
||||
import org.pixeldroid.app.utils.api.objects.Instance
|
||||
import org.pixeldroid.app.utils.api.objects.NodeInfo
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.storeInstance
|
||||
import org.pixeldroid.app.utils.hasInternet
|
||||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeNotificationChannels
|
||||
import org.pixeldroid.app.utils.openUrl
|
||||
import org.pixeldroid.app.utils.validDomain
|
||||
|
||||
/**
|
||||
Overview of the flow of the login process: (boxes are requests done in parallel,
|
||||
since they do not depend on each other)
|
||||
|
||||
_________________________________
|
||||
|[PixelfedAPI.registerApplication]|
|
||||
|[PixelfedAPI.wellKnownNodeInfo] |
|
||||
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅
|
||||
+----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed)
|
||||
+----> [promptOAuth]
|
||||
+----> [PixelfedAPI.obtainToken]
|
||||
+----> [PixelfedAPI.verifyCredentials]
|
||||
|
||||
*/
|
||||
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref"
|
||||
private const val SCOPE = "read write follow"
|
||||
}
|
||||
|
||||
private lateinit var oauthScheme: String
|
||||
private lateinit var appName: String
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
private lateinit var pixelfedAPI: PixelfedAPI
|
||||
private var inputVisibility: Int = View.GONE
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
loadingAnimation(true)
|
||||
appName = getString(R.string.app_name)
|
||||
oauthScheme = getString(R.string.auth_scheme)
|
||||
preferences = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
if (hasInternet(applicationContext)) {
|
||||
binding.connectInstanceButton.setOnClickListener {
|
||||
registerAppToServer(normalizeDomain(binding.editText.text.toString()))
|
||||
}
|
||||
binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
|
||||
inputVisibility = View.VISIBLE
|
||||
} else {
|
||||
binding.loginActivityConnectionRequired.visibility = View.VISIBLE
|
||||
binding.loginActivityConnectionRequiredButton.setOnClickListener {
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
loadingAnimation(false)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val url: Uri? = intent.data
|
||||
|
||||
//Check if the activity was started after the authentication
|
||||
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
|
||||
loadingAnimation(true)
|
||||
|
||||
val code = url.getQueryParameter("code")
|
||||
authenticate(code)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
loadingAnimation(false)
|
||||
}
|
||||
|
||||
|
||||
private fun whatsAnInstance() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
// Create the AlertDialog
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
val view = currentFocus
|
||||
if (view != null) {
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerAppToServer(normalizedDomain: String) {
|
||||
|
||||
if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain))
|
||||
|
||||
hideKeyboard()
|
||||
loadingAnimation(true)
|
||||
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val credentialsDeferred: Deferred<Application?> = async {
|
||||
try {
|
||||
pixelfedAPI.registerApplication(
|
||||
appName, "$oauthScheme://$PACKAGE_ID", SCOPE, "https://pixeldroid.org"
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
return@async null
|
||||
}
|
||||
}
|
||||
|
||||
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
|
||||
|
||||
val credentials = credentialsDeferred.await()
|
||||
|
||||
val clientId = credentials?.client_id ?: return@launch failedRegistration()
|
||||
preferences.edit()
|
||||
.putString("clientID", clientId)
|
||||
.putString("clientSecret", credentials.client_secret)
|
||||
.apply()
|
||||
|
||||
|
||||
// c.f. https://nodeinfo.diaspora.software/protocol.html for more info
|
||||
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
|
||||
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
}?.href ?: return@launch failedRegistration(getString(R.string.instance_error))
|
||||
|
||||
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun nodeInfoSchema(
|
||||
normalizedDomain: String,
|
||||
clientId: String,
|
||||
nodeInfoSchemaUrl: String
|
||||
) = coroutineScope {
|
||||
|
||||
val nodeInfo: NodeInfo = try {
|
||||
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(getString(R.string.instance_error))
|
||||
}
|
||||
val domain: String = try {
|
||||
if (nodeInfo.hasInstanceEndpointInfo()) {
|
||||
preferences.edit().putString("nodeInfo", Gson().toJson(nodeInfo)).remove("instance").apply()
|
||||
nodeInfo.metadata?.config?.site?.url
|
||||
} else {
|
||||
val instance: Instance = try {
|
||||
pixelfedAPI.instance()
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(getString(R.string.instance_error))
|
||||
}
|
||||
preferences.edit().putString("instance", Gson().toJson(instance)).remove("nodeInfo").apply()
|
||||
instance.uri
|
||||
}
|
||||
} catch (e: IllegalArgumentException){ null }
|
||||
?: return@coroutineScope failedRegistration(getString(R.string.instance_error))
|
||||
|
||||
preferences.edit()
|
||||
.putString("domain", normalizeDomain(domain))
|
||||
.apply()
|
||||
|
||||
|
||||
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
|
||||
MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.instance_not_pixelfed_warning)
|
||||
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
|
||||
promptOAuth(normalizedDomain, clientId)
|
||||
}
|
||||
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
|
||||
loadingAnimation(false)
|
||||
wipeSharedSettings()
|
||||
}
|
||||
}.show()
|
||||
} else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) {
|
||||
MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.api_not_enabled_dialog)
|
||||
setNegativeButton(android.R.string.ok) { _, _ ->
|
||||
loadingAnimation(false)
|
||||
wipeSharedSettings()
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
promptOAuth(normalizedDomain, clientId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun promptOAuth(normalizedDomain: String, client_id: String) {
|
||||
|
||||
val url = "$normalizedDomain/oauth/authorize?" +
|
||||
"client_id" + "=" + client_id + "&" +
|
||||
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
|
||||
"response_type=code" + "&" +
|
||||
"scope=${SCOPE.replace(" ", "%20")}"
|
||||
|
||||
if (!openUrl(url)) return failedRegistration(getString(R.string.browser_launch_failed))
|
||||
}
|
||||
|
||||
private fun authenticate(code: String?) {
|
||||
|
||||
// Get previous values from preferences
|
||||
val domain = preferences.getString("domain", "") as String
|
||||
val clientId = preferences.getString("clientID", "") as String
|
||||
val clientSecret = preferences.getString("clientSecret", "") as String
|
||||
|
||||
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
|
||||
return failedRegistration(getString(R.string.auth_failed))
|
||||
}
|
||||
|
||||
//Successful authorization
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
|
||||
val gson = Gson()
|
||||
val nodeInfo: NodeInfo? = gson.fromJson(preferences.getString("nodeInfo", null), NodeInfo::class.java)
|
||||
val instance: Instance? = gson.fromJson(preferences.getString("instance", null), Instance::class.java)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val token = pixelfedAPI.obtainToken(
|
||||
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code,
|
||||
"authorization_code"
|
||||
)
|
||||
if (token.access_token == null) {
|
||||
return@launch failedRegistration(getString(R.string.token_error))
|
||||
}
|
||||
storeInstance(db, nodeInfo, instance)
|
||||
storeUser(
|
||||
token.access_token,
|
||||
token.refresh_token,
|
||||
clientId,
|
||||
clientSecret,
|
||||
domain
|
||||
)
|
||||
wipeSharedSettings()
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration(getString(R.string.token_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun failedRegistration(message: String = getString(R.string.registration_failed)) {
|
||||
loadingAnimation(false)
|
||||
binding.editText.error = message
|
||||
wipeSharedSettings()
|
||||
}
|
||||
|
||||
private fun wipeSharedSettings(){
|
||||
preferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
private fun loadingAnimation(on: Boolean){
|
||||
if(on) {
|
||||
binding.loginActivityInstanceInputLayout.visibility = View.GONE
|
||||
binding.progressLayout.visibility = View.VISIBLE
|
||||
}
|
||||
else {
|
||||
binding.loginActivityInstanceInputLayout.visibility = inputVisibility
|
||||
binding.progressLayout.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
|
||||
try {
|
||||
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
|
||||
db.userDao().deActivateActiveUsers()
|
||||
addUser(
|
||||
db,
|
||||
user,
|
||||
instance,
|
||||
activeUser = true,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
apiHolder.setToCurrentUser()
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(getString(R.string.verify_credentials))
|
||||
}
|
||||
|
||||
fetchNotifications()
|
||||
val intent = Intent(this@LoginActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Fetch the latest notifications of this account, to avoid launching old notifications
|
||||
private suspend fun fetchNotifications() {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
try {
|
||||
val notifications = apiHolder.api!!.notifications()
|
||||
|
||||
notifications.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
||||
db.notificationDao().insertAll(notifications)
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(getString(R.string.login_notifications))
|
||||
}
|
||||
|
||||
makeNotificationChannels(
|
||||
applicationContext,
|
||||
user.fullHandle,
|
||||
makeChannelGroupId(user)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package org.pixeldroid.app.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
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.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.openUrl
|
||||
|
||||
/**
|
||||
Overview of the flow of the login process: (boxes are requests done in parallel,
|
||||
since they do not depend on each other)
|
||||
|
||||
_________________________________
|
||||
|[PixelfedAPI.registerApplication]|
|
||||
|[PixelfedAPI.wellKnownNodeInfo] |
|
||||
̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅̅
|
||||
+----> [PixelfedAPI.nodeInfoSchema] (and then [PixelfedAPI.instance] if needed)
|
||||
+----> [promptOAuth]
|
||||
+----> [PixelfedAPI.obtainToken]
|
||||
+----> [PixelfedAPI.verifyCredentials]
|
||||
|
||||
*/
|
||||
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref"
|
||||
private const val SCOPE = "read write follow"
|
||||
}
|
||||
|
||||
private lateinit var oauthScheme: String
|
||||
private lateinit var preferences: SharedPreferences
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
val model: LoginActivityViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
oauthScheme = getString(R.string.auth_scheme)
|
||||
preferences = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
binding.connectInstanceButton.setOnClickListener {
|
||||
hideKeyboard()
|
||||
model.registerAppToServer(binding.editText.text.toString())
|
||||
}
|
||||
binding.whatsAnInstanceTextView.setOnClickListener{ whatsAnInstance() }
|
||||
|
||||
// Enter button on keyboard should press the connect button
|
||||
binding.editText.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
binding.connectInstanceButton.performClick()
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.promptOauth.collectLatest {
|
||||
it?.let {
|
||||
if (it.launch) promptOAuth(it.normalizedDomain, it.clientId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.loadingState.collectLatest {
|
||||
when(it.loginState){
|
||||
LoginActivityViewModel.LoginState.LoadingState.Resting -> loadingAnimation(false)
|
||||
LoginActivityViewModel.LoginState.LoadingState.Busy -> loadingAnimation(true)
|
||||
LoginActivityViewModel.LoginState.LoadingState.Error -> failedRegistration(it.error!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val url: Uri? = intent.data
|
||||
|
||||
//Check if the activity was started after the authentication
|
||||
if (url == null || !url.toString().startsWith("$oauthScheme://$PACKAGE_ID")) return
|
||||
|
||||
val code = url.getQueryParameter("code")
|
||||
model.authenticate(code)
|
||||
}
|
||||
|
||||
private fun whatsAnInstance() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
// Create the AlertDialog
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
val view = currentFocus
|
||||
if (view != null) {
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken,
|
||||
InputMethodManager.HIDE_NOT_ALWAYS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptOAuth(normalizedDomain: String, client_id: String) {
|
||||
val url = "$normalizedDomain/oauth/authorize?" +
|
||||
"client_id" + "=" + client_id + "&" +
|
||||
"redirect_uri" + "=" + "$oauthScheme://$PACKAGE_ID" + "&" +
|
||||
"response_type=code" + "&" +
|
||||
"scope=${SCOPE.replace(" ", "%20")}"
|
||||
|
||||
if (!openUrl(url)) model.oauthLaunchFailed()
|
||||
else model.oauthLaunched()
|
||||
}
|
||||
|
||||
private fun failedRegistration(@StringRes message: Int = R.string.registration_failed) {
|
||||
when (message) {
|
||||
R.string.instance_not_pixelfed_warning -> MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.instance_not_pixelfed_warning)
|
||||
setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ ->
|
||||
model.dialogAckedContinueAnyways()
|
||||
}
|
||||
setNegativeButton(R.string.instance_not_pixelfed_cancel) { _, _ ->
|
||||
model.dialogNegativeButtonClicked()
|
||||
}
|
||||
}.show()
|
||||
|
||||
R.string.api_not_enabled_dialog -> MaterialAlertDialogBuilder(this@LoginActivity).apply {
|
||||
setMessage(R.string.api_not_enabled_dialog)
|
||||
setNegativeButton(android.R.string.ok) { _, _ ->
|
||||
model.dialogNegativeButtonClicked()
|
||||
}
|
||||
}.show()
|
||||
|
||||
else -> binding.editText.error = getString(message)
|
||||
}
|
||||
loadingAnimation(false)
|
||||
}
|
||||
|
||||
private fun loadingAnimation(on: Boolean){
|
||||
if(on) {
|
||||
binding.loginActivityInstanceInputLayout.visibility = View.GONE
|
||||
binding.progressLayout.visibility = View.VISIBLE
|
||||
}
|
||||
else {
|
||||
binding.loginActivityInstanceInputLayout.visibility = View.VISIBLE
|
||||
binding.progressLayout.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
package org.pixeldroid.app.login
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.Gson
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.BuildConfig
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Application
|
||||
import org.pixeldroid.app.utils.api.objects.Instance
|
||||
import org.pixeldroid.app.utils.api.objects.NodeInfo
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.storeInstance
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
|
||||
import org.pixeldroid.app.utils.notificationsWorker.makeNotificationChannels
|
||||
import org.pixeldroid.app.utils.validDomain
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginActivityViewModel @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
private const val PREFERENCE_NAME = "$PACKAGE_ID.loginPref"
|
||||
private const val SCOPE = "read write follow"
|
||||
}
|
||||
private val oauthScheme = applicationContext.getString(R.string.auth_scheme)
|
||||
|
||||
private lateinit var pixelfedAPI: PixelfedAPI
|
||||
private val preferences: SharedPreferences = applicationContext.getSharedPreferences(
|
||||
PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
private val _loadingState: MutableStateFlow<LoginState> = MutableStateFlow(LoginState(LoginState.LoadingState.Resting))
|
||||
val loadingState = _loadingState.asStateFlow()
|
||||
|
||||
private val _finishedLogin = MutableStateFlow(false)
|
||||
val finishedLogin = _finishedLogin.asStateFlow()
|
||||
|
||||
private val _promptOauth: MutableStateFlow<PromptOAuth?> = MutableStateFlow(null)
|
||||
val promptOauth = _promptOauth.asStateFlow()
|
||||
|
||||
data class PromptOAuth(
|
||||
val launch: Boolean,
|
||||
val normalizedDomain: String,
|
||||
val clientId: String,
|
||||
)
|
||||
|
||||
data class LoginState(
|
||||
val loginState: LoadingState,
|
||||
@StringRes
|
||||
val error: Int? = null,
|
||||
) {
|
||||
init {
|
||||
if (loginState == LoadingState.Error && error == null) throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
enum class LoadingState {
|
||||
Resting, Busy, Error
|
||||
}
|
||||
}
|
||||
|
||||
fun registerAppToServer(rawDomain: String) {
|
||||
val normalizedDomain = normalizeDomain(rawDomain)
|
||||
|
||||
if(!validDomain(normalizedDomain)) return failedRegistration(R.string.invalid_domain)
|
||||
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(normalizedDomain)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val credentialsDeferred: Deferred<Application?> = async {
|
||||
try {
|
||||
pixelfedAPI.registerApplication(
|
||||
applicationContext.getString(R.string.app_name),
|
||||
"$oauthScheme://$PACKAGE_ID", SCOPE, "https://pixeldroid.org"
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
return@async null
|
||||
}
|
||||
}
|
||||
|
||||
val nodeInfoJRD = pixelfedAPI.wellKnownNodeInfo()
|
||||
|
||||
val credentials = credentialsDeferred.await()
|
||||
|
||||
val clientId = credentials?.client_id ?: return@launch failedRegistration()
|
||||
preferences.edit()
|
||||
.putString("clientID", clientId)
|
||||
.putString("clientSecret", credentials.client_secret)
|
||||
.apply()
|
||||
|
||||
|
||||
// c.f. https://nodeinfo.diaspora.software/protocol.html for more info
|
||||
val nodeInfoSchemaUrl = nodeInfoJRD.links.firstOrNull {
|
||||
it.rel == "http://nodeinfo.diaspora.software/ns/schema/2.0"
|
||||
}?.href ?: return@launch failedRegistration(R.string.instance_error)
|
||||
|
||||
nodeInfoSchema(normalizedDomain, clientId, nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun nodeInfoSchema(
|
||||
normalizedDomain: String,
|
||||
clientId: String,
|
||||
nodeInfoSchemaUrl: String
|
||||
) = coroutineScope {
|
||||
|
||||
val nodeInfo: NodeInfo = try {
|
||||
pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl)
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(R.string.instance_error)
|
||||
}
|
||||
val domain: String = try {
|
||||
if (nodeInfo.hasInstanceEndpointInfo()) {
|
||||
preferences.edit().putString("nodeInfo", Gson().toJson(nodeInfo)).remove("instance").apply()
|
||||
nodeInfo.metadata?.config?.site?.url
|
||||
} else {
|
||||
val instance: Instance = try {
|
||||
pixelfedAPI.instance()
|
||||
} catch (exception: Exception) {
|
||||
return@coroutineScope failedRegistration(R.string.instance_error)
|
||||
}
|
||||
preferences.edit().putString("instance", Gson().toJson(instance)).remove("nodeInfo").apply()
|
||||
instance.uri
|
||||
}
|
||||
} catch (e: IllegalArgumentException){ null }
|
||||
?: return@coroutineScope failedRegistration(R.string.instance_error)
|
||||
|
||||
preferences.edit()
|
||||
.putString("domain", normalizeDomain(domain))
|
||||
.apply()
|
||||
|
||||
if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.instance_not_pixelfed_warning)
|
||||
_promptOauth.value = PromptOAuth(false, normalizedDomain, clientId)
|
||||
} else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.api_not_enabled_dialog)
|
||||
} else {
|
||||
_promptOauth.value = PromptOAuth(true, normalizedDomain, clientId)
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
}
|
||||
}
|
||||
|
||||
fun authenticate(code: String?) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
// Get previous values from preferences
|
||||
val domain = preferences.getString("domain", "") as String
|
||||
val clientId = preferences.getString("clientID", "") as String
|
||||
val clientSecret = preferences.getString("clientSecret", "") as String
|
||||
|
||||
if (code.isNullOrBlank() || domain.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
|
||||
return failedRegistration(R.string.auth_failed)
|
||||
}
|
||||
|
||||
//Successful authorization
|
||||
pixelfedAPI = PixelfedAPI.createFromUrl(domain)
|
||||
val gson = Gson()
|
||||
val nodeInfo: NodeInfo? = gson.fromJson(preferences.getString("nodeInfo", null), NodeInfo::class.java)
|
||||
val instance: Instance? = gson.fromJson(preferences.getString("instance", null), Instance::class.java)
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val token = pixelfedAPI.obtainToken(
|
||||
clientId, clientSecret, "$oauthScheme://$PACKAGE_ID",
|
||||
SCOPE, code,
|
||||
"authorization_code"
|
||||
)
|
||||
if (token.access_token == null) {
|
||||
return@launch failedRegistration(R.string.token_error)
|
||||
}
|
||||
storeInstance(db, nodeInfo, instance)
|
||||
storeUser(
|
||||
token.access_token,
|
||||
token.refresh_token,
|
||||
clientId,
|
||||
clientSecret,
|
||||
domain
|
||||
)
|
||||
wipeSharedSettings()
|
||||
} catch (exception: Exception) {
|
||||
return@launch failedRegistration(R.string.token_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun storeUser(accessToken: String, refreshToken: String?, clientId: String, clientSecret: String, instance: String) {
|
||||
try {
|
||||
val user = pixelfedAPI.verifyCredentials("Bearer $accessToken")
|
||||
db.userDao().deActivateActiveUsers()
|
||||
addUser(
|
||||
db,
|
||||
user,
|
||||
instance,
|
||||
activeUser = true,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
apiHolder.setToCurrentUser()
|
||||
} 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
|
||||
private suspend fun fetchNotifications() {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
try {
|
||||
val notifications = apiHolder.api!!.notifications()
|
||||
|
||||
notifications.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
||||
db.notificationDao().insertAll(notifications)
|
||||
} catch (exception: Exception) {
|
||||
return failedRegistration(R.string.login_notifications)
|
||||
}
|
||||
|
||||
makeNotificationChannels(
|
||||
applicationContext,
|
||||
user.fullHandle,
|
||||
makeChannelGroupId(user)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun wipeSharedSettings(){
|
||||
preferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
private fun failedRegistration(@StringRes message: Int = R.string.registration_failed) {
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, message)
|
||||
when (message) {
|
||||
R.string.instance_not_pixelfed_warning, R.string.api_not_enabled_dialog -> return
|
||||
else -> wipeSharedSettings()
|
||||
}
|
||||
}
|
||||
|
||||
fun oauthLaunched() {
|
||||
_promptOauth.value = null
|
||||
}
|
||||
|
||||
fun oauthLaunchFailed() {
|
||||
_promptOauth.value = null
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Error, R.string.browser_launch_failed)
|
||||
}
|
||||
|
||||
fun dialogAckedContinueAnyways() {
|
||||
_promptOauth.value = _promptOauth.value?.copy(launch = true)
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Busy)
|
||||
}
|
||||
|
||||
fun dialogNegativeButtonClicked() {
|
||||
wipeSharedSettings()
|
||||
_loadingState.value = LoginState(LoginState.LoadingState.Resting)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package org.pixeldroid.app.main
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.AccountListItemBinding
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
class AccountListAdapter(
|
||||
private val items: StateFlow<List<UserDatabaseEntity>>,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
private val onClick: (UserDatabaseEntity?) -> Unit
|
||||
) : RecyclerView.Adapter<AccountListAdapter.ViewHolder>() {
|
||||
private val itemsList: MutableList<UserDatabaseEntity> = mutableListOf()
|
||||
|
||||
init {
|
||||
lifecycleScope.launch {
|
||||
items.collect {
|
||||
itemsList.clear()
|
||||
itemsList.addAll(it.filter { !it.isActive })
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = AccountListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.binding.root.setOnClickListener{onClick(itemsList.getOrNull(position))}
|
||||
if (position == itemsList.size) {
|
||||
Glide.with(holder.itemView)
|
||||
.load(R.drawable.add)
|
||||
.into(holder.binding.imageView)
|
||||
holder.binding.accountName.setText(R.string.add_account_name)
|
||||
holder.binding.accountUsername.setText(R.string.add_account_description)
|
||||
return
|
||||
}
|
||||
|
||||
val user = itemsList[position]
|
||||
Glide.with(holder.itemView)
|
||||
.load(user.avatar_static)
|
||||
.placeholder(R.drawable.ic_default_user)
|
||||
.circleCrop()
|
||||
.into(holder.binding.imageView)
|
||||
holder.binding.accountName.text = user.display_name
|
||||
holder.binding.accountUsername.text = user.fullHandle
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = itemsList.size + 1
|
||||
|
||||
class ViewHolder(val binding: AccountListItemBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.pixeldroid.app
|
||||
package org.pixeldroid.app.main
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
|
@ -16,31 +16,37 @@ import androidx.activity.addCallback
|
|||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginEnd
|
||||
import androidx.core.view.marginTop
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||
|
@ -49,6 +55,8 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
|||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import kotlinx.coroutines.launch
|
||||
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
|
||||
import org.pixeldroid.app.login.LoginActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityMainBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.posts.NestedScrollableHost
|
||||
|
@ -70,6 +78,7 @@ import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companio
|
|||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.USER_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.enablePullNotifications
|
||||
import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFromAccount
|
||||
import org.pixeldroid.common.dpToPx
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
|
@ -179,22 +188,53 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun setupDrawer() {
|
||||
binding.mainDrawerButton.setOnClickListener{
|
||||
binding.drawerLayout.openDrawer(binding.drawer)
|
||||
binding.mainDrawerButton?.setOnClickListener {
|
||||
binding.drawer?.let { drawer -> binding.drawerLayout.openDrawer(drawer) }
|
||||
}
|
||||
|
||||
header = AccountHeaderView(this).apply {
|
||||
attachToSliderView(binding.drawer)
|
||||
val navigationHeader = binding.navigation?.getHeaderView(0) as? AccountHeaderView
|
||||
val headerview = navigationHeader ?: AccountHeaderView(this)
|
||||
|
||||
navigationHeader?.onAccountHeaderSelectionViewClickListener = { _: View, _: IProfile ->
|
||||
// update the arrow image within the drawer
|
||||
navigationHeader!!.accountSwitcherArrow.clearAnimation()
|
||||
|
||||
if(binding.accountList?.isVisible == true) {
|
||||
navigationHeader.accountSwitcherArrow.animate().rotation(0f).start()
|
||||
} else {
|
||||
navigationHeader.accountSwitcherArrow.animate().rotation(180f).start()
|
||||
|
||||
fun onAccountClick(user: UserDatabaseEntity?){
|
||||
clickProfile(user?.user_id, user?.instance_uri, false)
|
||||
}
|
||||
val adapter = AccountListAdapter(model.users, lifecycleScope, ::onAccountClick)
|
||||
binding.accountList?.adapter = adapter
|
||||
|
||||
val location = IntArray(2)
|
||||
navigationHeader.getLocationOnScreen(location)
|
||||
|
||||
// Set the position of textView within constraintLayout2
|
||||
val textViewLayoutParams = binding.accountList?.layoutParams as? ConstraintLayout.LayoutParams
|
||||
textViewLayoutParams?.topMargin = location[1] + (navigationHeader as ConstraintLayout).height - 6.dpToPx(this)
|
||||
binding.accountList?.layoutParams = textViewLayoutParams
|
||||
}
|
||||
binding.accountList?.isVisible = !(binding.accountList?.isVisible ?: false)
|
||||
true
|
||||
}
|
||||
|
||||
header = headerview.apply {
|
||||
binding.drawer?.let { attachToSliderView(it) }
|
||||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||
currentHiddenInList = true
|
||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean ->
|
||||
clickProfile(profile, current)
|
||||
val userId: String? = if (profile.identifier == ADD_ACCOUNT_IDENTIFIER) null else profile.identifier.toString()
|
||||
clickProfile(userId, profile.tag?.toString(), current)
|
||||
}
|
||||
addProfile(ProfileSettingDrawerItem().apply {
|
||||
identifier = ADD_ACCOUNT_IDENTIFIER
|
||||
nameRes = R.string.add_account_name
|
||||
descriptionRes = R.string.add_account_description
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
||||
iconRes = R.drawable.add
|
||||
}, 0)
|
||||
dividerBelowHeader = false
|
||||
closeDrawerOnProfileListClick = true
|
||||
|
@ -228,21 +268,22 @@ class MainActivity : BaseActivity() {
|
|||
//with the received one. This happens asynchronously.
|
||||
getUpdatedAccount()
|
||||
|
||||
binding.drawer.itemAdapter.add(
|
||||
binding.drawer?.itemAdapter?.add(
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.menu_account
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
iconRes = R.drawable.person
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.menu_settings
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
iconRes = R.drawable.settings
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.logout
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_close
|
||||
iconRes = R.drawable.logout
|
||||
},
|
||||
)
|
||||
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
|
||||
|
||||
binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position ->
|
||||
when (position) {
|
||||
1 -> launchActivity(ProfileActivity())
|
||||
2 -> launchActivity(SettingsActivity())
|
||||
|
@ -256,8 +297,7 @@ class MainActivity : BaseActivity() {
|
|||
// Handle the back button event
|
||||
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
@ -305,18 +345,19 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
//called when switching profiles, or when clicking on current profile
|
||||
private fun clickProfile(profile: IProfile, current: Boolean): Boolean {
|
||||
@Suppress("SameReturnValue")
|
||||
private fun clickProfile(id: String?, instance: String?, current: Boolean): Boolean {
|
||||
if(current){
|
||||
launchActivity(ProfileActivity())
|
||||
return false
|
||||
}
|
||||
//Clicked on add new account
|
||||
if(profile.identifier == ADD_ACCOUNT_IDENTIFIER){
|
||||
if(id == null || instance == null){
|
||||
launchActivity(LoginActivity())
|
||||
return false
|
||||
}
|
||||
|
||||
switchUser(profile.identifier.toString(), profile.tag as String)
|
||||
switchUser(id, instance)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
@ -396,6 +437,34 @@ class MainActivity : BaseActivity() {
|
|||
touchSlopField.set(recyclerView, touchSlop*NestedScrollableHost.touchSlopModifier)
|
||||
}
|
||||
|
||||
private fun NavigationView.unSelectAll() {
|
||||
for (i in 0 until menu.size()) {
|
||||
val menuItem = menu.getItem(i)
|
||||
menuItem.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun MenuItem.itemPos(): Int? {
|
||||
return when(itemId){
|
||||
R.id.page_1 -> 0
|
||||
R.id.page_2 -> 1
|
||||
R.id.page_3 -> 2
|
||||
R.id.page_4 -> 3
|
||||
R.id.page_5 -> 4
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun reclick(item: MenuItem) {
|
||||
item.itemPos()?.let { position ->
|
||||
val page =
|
||||
//No clue why this works but it does. F to pay respects
|
||||
supportFragmentManager.findFragmentByTag("f$position")
|
||||
(page as? CachedFeedFragment<*>)?.onTabReClicked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTabs(tab_array: List<() -> Fragment>){
|
||||
binding.viewPager.reduceDragSensitivity()
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
|
@ -422,37 +491,49 @@ class MainActivity : BaseActivity() {
|
|||
else -> null
|
||||
}
|
||||
if (selected != null) {
|
||||
binding.tabs.selectedItemId = selected
|
||||
// Disable and re-enable reselected listener so that it's not triggered by this
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(null)
|
||||
(binding.tabs as? NavigationBarView)?.selectedItemId = selected
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick)
|
||||
|
||||
binding.navigation?.unSelectAll()
|
||||
binding.navigation?.menu?.getItem(position)?.setChecked(true)
|
||||
}
|
||||
super.onPageSelected(position)
|
||||
}
|
||||
})
|
||||
|
||||
fun MenuItem.itemPos(): Int? {
|
||||
return when(itemId){
|
||||
R.id.page_1 -> 0
|
||||
R.id.page_2 -> 1
|
||||
R.id.page_3 -> 2
|
||||
R.id.page_4 -> 3
|
||||
R.id.page_5 -> 4
|
||||
else -> null
|
||||
|
||||
fun MenuItem.buttonPos() {
|
||||
when(itemId){
|
||||
R.id.my_profile -> launchActivity(ProfileActivity())
|
||||
R.id.settings -> launchActivity(SettingsActivity())
|
||||
R.id.log_out -> logOut()
|
||||
}
|
||||
}
|
||||
|
||||
binding.tabs.setOnItemSelectedListener {item ->
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemSelectedListener { item ->
|
||||
item.itemPos()?.let {
|
||||
binding.viewPager.currentItem = it
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
binding.tabs.setOnItemReselectedListener { item ->
|
||||
item.itemPos()?.let { position ->
|
||||
val page =
|
||||
//No clue why this works but it does. F to pay respects
|
||||
supportFragmentManager.findFragmentByTag("f$position")
|
||||
(page as? CachedFeedFragment<*>)?.onTabReClicked()
|
||||
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick)
|
||||
|
||||
binding.navigation?.setNavigationItemSelectedListener { item ->
|
||||
if (binding.navigation?.menu?.children?.find { it.itemId == item.itemId }?.isChecked == true) {
|
||||
reclick(item)
|
||||
} else {
|
||||
item.itemPos()?.let {
|
||||
binding.navigation?.unSelectAll()
|
||||
item.isChecked = true
|
||||
binding.viewPager.currentItem = it
|
||||
true
|
||||
} ?: item.buttonPos()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// Fetch one notification to show a badge if there are new notifications
|
||||
|
@ -480,19 +561,12 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun setNotificationBadge(show: Boolean, count: Int? = null) {
|
||||
//TODO add badge to NavigationView... not implemented yet: https://github.com/material-components/material-components-android/issues/2860
|
||||
if(show){
|
||||
val badge = binding.tabs.getOrCreateBadge(R.id.page_4)
|
||||
if (count != null) badge.number = count
|
||||
val badge = (binding.tabs as? NavigationBarView)?.getOrCreateBadge(R.id.page_4)
|
||||
if (count != null) badge?.number = count
|
||||
}
|
||||
else binding.tabs.removeBadge(R.id.page_4)
|
||||
}
|
||||
|
||||
fun BottomNavigationView.uncheckAllItems() {
|
||||
menu.setGroupCheckable(0, true, false)
|
||||
for (i in 0 until menu.size()) {
|
||||
menu.getItem(i).isChecked = false
|
||||
}
|
||||
menu.setGroupCheckable(0, true, true)
|
||||
else (binding.tabs as? NavigationBarView)?.removeBadge(R.id.page_4)
|
||||
}
|
||||
|
||||
/**
|
|
@ -1,4 +1,4 @@
|
|||
package org.pixeldroid.app
|
||||
package org.pixeldroid.app.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
|
|
|
@ -3,7 +3,7 @@ package org.pixeldroid.app.postCreation.camera
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityCameraBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.view.View
|
|||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
|
@ -23,7 +23,7 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
|
|||
import org.pixeldroid.app.utils.displayDimensionsInPx
|
||||
|
||||
class PostActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityPostBinding
|
||||
lateinit var binding: ActivityPostBinding
|
||||
|
||||
private lateinit var commentFragment: CommentFragment
|
||||
|
||||
|
@ -33,7 +33,7 @@ class PostActivity : BaseActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostBinding.inflate(layoutInflater)
|
||||
|
||||
commentFragment = CommentFragment(binding.swipeRefreshLayout)
|
||||
commentFragment = CommentFragment()
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
@ -48,14 +48,15 @@ class PostActivity : BaseActivity() {
|
|||
supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName())
|
||||
|
||||
val holder = StatusViewHolder(binding.postFragmentSingle)
|
||||
val (width, height) = displayDimensionsInPx()
|
||||
|
||||
holder.bind(
|
||||
status, apiHolder, db, lifecycleScope, displayDimensionsInPx(),
|
||||
status, apiHolder, db, lifecycleScope, Pair((width*.7).toInt(), height),
|
||||
requestPermissionDownloadPic, isActivity = true
|
||||
)
|
||||
|
||||
activateCommenter()
|
||||
initCommentsFragment(domain = user?.instance_uri.orEmpty())
|
||||
initCommentsFragment(domain = user?.instance_uri.orEmpty(), savedInstanceState)
|
||||
|
||||
if(viewComments || postComment){
|
||||
//Scroll already down as much as possible (since comments are not loaded yet)
|
||||
|
@ -100,15 +101,21 @@ class PostActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun initCommentsFragment(domain: String) {
|
||||
private fun initCommentsFragment(domain: String, savedInstanceState: Bundle?) {
|
||||
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(COMMENT_STATUS_ID, status.id)
|
||||
arguments.putSerializable(COMMENT_DOMAIN, domain)
|
||||
commentFragment.arguments = arguments
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.commentFragment, commentFragment).commit()
|
||||
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
|
||||
//Maybe read https://archive.ph/G9VHW#selection-1324.2-1322.3 or further research
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace(R.id.commentFragment, commentFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
commentFragment.adapter.refresh()
|
||||
|
|
|
@ -2,15 +2,22 @@ package org.pixeldroid.app.posts
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityReportBinding
|
||||
import org.pixeldroid.app.posts.ReportActivityViewModel.UploadState
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
|
||||
class ReportActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityReportBinding
|
||||
private val model: ReportActivityViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -24,42 +31,47 @@ class ReportActivity : BaseActivity() {
|
|||
|
||||
binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct)
|
||||
|
||||
binding.textInputLayout.editText?.text = model.editable
|
||||
|
||||
binding.textInputLayout.editText?.doAfterTextChanged { model.textChanged(it) }
|
||||
|
||||
binding.reportButton.setOnClickListener {
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportProgressBar.visibility = View.VISIBLE
|
||||
model.sendReport(status, binding.textInputLayout.editText?.text.toString())
|
||||
}
|
||||
|
||||
binding.textInputLayout.editText?.isEnabled = false
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
api.report(
|
||||
status?.account?.id!!,
|
||||
listOf(status),
|
||||
binding.textInputLayout.editText?.text.toString()
|
||||
)
|
||||
|
||||
reportStatus(true)
|
||||
} catch (exception: Exception) {
|
||||
reportStatus(false)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
model.reportSent.collect {
|
||||
reportStatus(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reportStatus(success: Boolean){
|
||||
if(success){
|
||||
private fun reportStatus(success: UploadState){
|
||||
when (success) {
|
||||
UploadState.initial -> {
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportButton.visibility = View.VISIBLE
|
||||
binding.reportSuccess.visibility = View.INVISIBLE
|
||||
}
|
||||
UploadState.success -> {
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportSuccess.visibility = View.VISIBLE
|
||||
} else {
|
||||
}
|
||||
UploadState.failed -> {
|
||||
binding.textInputLayout.error = getString(R.string.report_error)
|
||||
binding.reportButton.visibility = View.VISIBLE
|
||||
binding.textInputLayout.editText?.isEnabled = true
|
||||
binding.reportProgressBar.visibility = View.GONE
|
||||
binding.reportSuccess.visibility = View.GONE
|
||||
}
|
||||
UploadState.inProgress -> {
|
||||
binding.reportButton.visibility = View.INVISIBLE
|
||||
binding.reportProgressBar.visibility = View.VISIBLE
|
||||
binding.textInputLayout.editText?.isEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.text.Editable
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ReportActivityViewModel @Inject constructor(val apiHolder: PixelfedAPIHolder): ViewModel() {
|
||||
var editable: Editable? = null
|
||||
private set
|
||||
|
||||
private val _reportSent: MutableStateFlow<UploadState> = MutableStateFlow(UploadState.initial)
|
||||
val reportSent = _reportSent.asStateFlow()
|
||||
|
||||
enum class UploadState {
|
||||
initial, success, failed, inProgress
|
||||
}
|
||||
fun textChanged(it: Editable?) {
|
||||
editable = it
|
||||
}
|
||||
|
||||
fun sendReport(status: Status?, text: String) {
|
||||
_reportSent.value = UploadState.inProgress
|
||||
viewModelScope.launch {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
try {
|
||||
api.report(
|
||||
status?.account?.id!!,
|
||||
listOf(status),
|
||||
text
|
||||
)
|
||||
|
||||
_reportSent.value = UploadState.success
|
||||
} catch (exception: Exception) {
|
||||
_reportSent.value = UploadState.failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -152,8 +152,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
if(!status?.media_attachments.isNullOrEmpty()) {
|
||||
setupPostPics(binding, request)
|
||||
} else {
|
||||
binding.postPager.visibility = View.GONE
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
binding.postConstraint.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,16 +55,16 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
|||
//TODO rename function to something that makes sense
|
||||
internal fun initSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launchWhenStarted {
|
||||
adapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy {
|
||||
it.refresh
|
||||
}
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is NotLoading}
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
}
|
||||
// lifecycleScope.launchWhenStarted {
|
||||
// adapter.loadStateFlow
|
||||
// // Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
// .distinctUntilChangedBy {
|
||||
// it.refresh
|
||||
// }
|
||||
// // Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
// .filter { it.refresh is NotLoading}
|
||||
// .collect { binding.list.scrollToPosition(0) }
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
|
|
@ -40,8 +40,6 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
|||
|
||||
home = requireArguments().getBoolean("home")
|
||||
|
||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (home){
|
||||
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||
|
@ -61,6 +59,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
|||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
|
|
|
@ -42,15 +42,15 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
|||
}
|
||||
|
||||
internal fun initSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launch {
|
||||
adapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { binding?.list?.scrollToPosition(0) }
|
||||
}
|
||||
// // Scroll to top when the list is refreshed from network.
|
||||
// lifecycleScope.launch {
|
||||
// adapter.loadStateFlow
|
||||
// // Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
// .distinctUntilChangedBy { it.refresh }
|
||||
// // Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
// .filter { it.refresh is LoadState.NotLoading }
|
||||
// .collect { binding?.list?.scrollToPosition(0) }
|
||||
// }
|
||||
}
|
||||
|
||||
fun onCreateView(
|
||||
|
|
|
@ -28,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
|
|||
/**
|
||||
* Fragment to show a list of [Status]s, in form of comments
|
||||
*/
|
||||
class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment<Status>() {
|
||||
class CommentFragment: UncachedFeedFragment<Status>() {
|
||||
|
||||
private lateinit var id: String
|
||||
private lateinit var domain: String
|
||||
|
@ -48,8 +48,10 @@ class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedF
|
|||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
|
||||
val view = super.onCreateView(
|
||||
inflater, container, savedInstanceState,
|
||||
(activity as? PostActivity)?.binding?.swipeRefreshLayout
|
||||
)
|
||||
|
||||
// Get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.add
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.replace
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
|
||||
|
||||
|
||||
class HashTagActivity : BaseActivity() {
|
||||
private var tagFragment = UncachedPostsFragment()
|
||||
private lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
|
@ -32,10 +34,10 @@ class HashTagActivity : BaseActivity() {
|
|||
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(HASHTAG_TAG, tag)
|
||||
tagFragment.arguments = arguments
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.followsFragment, tagFragment).commit()
|
||||
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace<UncachedPostsFragment>(R.id.followsFragment, args = arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ class CollectionActivity : BaseActivity() {
|
|||
return true
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
// Relaunch same activity, to avoid duplicates in history
|
||||
super.onNewIntent(intent)
|
||||
finish()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package org.pixeldroid.app.profile
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.replace
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
|
||||
|
@ -12,10 +14,8 @@ import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG
|
|||
|
||||
|
||||
class FollowsActivity : BaseActivity() {
|
||||
private var followsFragment = AccountListFragment()
|
||||
private lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
|
@ -47,10 +47,10 @@ class FollowsActivity : BaseActivity() {
|
|||
val arguments = Bundle()
|
||||
arguments.putSerializable(ACCOUNT_ID_TAG, id)
|
||||
arguments.putSerializable(FOLLOWERS_TAG, followers)
|
||||
followsFragment.arguments = arguments
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.followsFragment, followsFragment).commit()
|
||||
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
replace<AccountListFragment>(R.id.followsFragment, args = arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import androidx.preference.Preference
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.SettingsBinding
|
||||
import org.pixeldroid.common.ThemedActivity
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Configuration
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.EntryPoints
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.ligi.tracedroid.TraceDroid
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class PixelDroidApplication: Application() {
|
||||
class PixelDroidApplication : Application(), Configuration.Provider {
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface HiltWorkerFactoryEntryPoint {
|
||||
fun workerFactory(): HiltWorkerFactory
|
||||
}
|
||||
override val workManagerConfiguration =
|
||||
Configuration.Builder()
|
||||
.setWorkerFactory(EntryPoints.get(this, HiltWorkerFactoryEntryPoint::class.java).workerFactory()) .build()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
|
|
@ -12,36 +12,41 @@ import android.os.Build
|
|||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.pixeldroid.app.main.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
import org.pixeldroid.app.posts.fromHtml
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.*
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.comment
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.entries
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.favourite
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.follow
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.follow_request
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.mention
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.poll
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.reblog
|
||||
import org.pixeldroid.app.utils.api.objects.Notification.NotificationType.status
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.getColorFromAttr
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
class NotificationsWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
@HiltWorker
|
||||
class NotificationsWorker @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val db: AppDatabase,
|
||||
private val apiHolder: PixelfedAPIHolder
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val users: List<UserDatabaseEntity> = db.userDao().getAll()
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
|
||||
</vector>
|
|
@ -0,0 +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">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
|
||||
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
|
||||
</vector>
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.app.main.MainActivity">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/main_activity_main_linear_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/main_drawer_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:contentDescription="@string/open_drawer_menu"
|
||||
android:src="@drawable/ic_baseline_menu_24" />
|
||||
|
||||
<com.google.android.material.navigationrail.NavigationRailView
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:menu="@menu/navigation_main" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/main_activity_main_linear_layout"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
|
||||
android:id="@+id/drawer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
@ -0,0 +1,258 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePic"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
android:src="@drawable/ic_default_user"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
||||
app:layout_constraintStart_toEndOf="@+id/profilePic"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePic"
|
||||
tools:text="username" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/postDomain"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="#b3b3b3"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
||||
app:layout_constraintStart_toEndOf="@+id/username"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePic"
|
||||
tools:text="from domain.tld" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/status_more"
|
||||
style="?android:attr/actionOverflowButtonStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/status_more_options"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePic" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/postConstraint"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/postDetails"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/profilePic"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.7">
|
||||
|
||||
<org.pixeldroid.app.posts.NestedScrollableHost
|
||||
android:id="@+id/postPagerHost"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/postPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</org.pixeldroid.app.posts.NestedScrollableHost>
|
||||
|
||||
<me.relex.circleindicator.CircleIndicator3
|
||||
android:id="@+id/postIndicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="32dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/like_animation"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:src="@drawable/heart_anim"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/post_fragment_image_popup_menu_anchor"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="1dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="@+id/postPagerHost"
|
||||
app:layout_constraintHorizontal_bias="0.1"
|
||||
app:layout_constraintStart_toStartOf="@+id/postPagerHost"
|
||||
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
|
||||
app:layout_constraintVertical_bias="0.1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sensitiveWarning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/rounded_corner"
|
||||
android:gravity="center|center_horizontal|center_vertical"
|
||||
android:text="@string/cw_nsfw_hidden_media_n_click_to_show"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textColor="@color/ic_launcher_background"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/postPagerHost"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/postDetails"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/postConstraint"
|
||||
app:layout_constraintTop_toTopOf="@id/postConstraint">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/commenter"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:contentDescription="@string/add_comment"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/selector_commenter"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/liker"
|
||||
app:layout_constraintEnd_toStartOf="@id/reblogger"
|
||||
app:layout_constraintStart_toEndOf="@id/liker"
|
||||
app:layout_constraintTop_toTopOf="@id/liker" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
android:id="@+id/liker"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:clipToPadding="false"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/commenter"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.20"
|
||||
sparkbutton:activeImage="@drawable/ic_like_full"
|
||||
sparkbutton:iconSize="28dp"
|
||||
sparkbutton:inactiveImage="@drawable/ic_like_empty"
|
||||
sparkbutton:primaryColor="@color/heart_red"
|
||||
sparkbutton:secondaryColor="@color/black" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
android:id="@+id/reblogger"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:clipToPadding="false"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/commenter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/commenter"
|
||||
app:layout_constraintTop_toTopOf="@+id/commenter"
|
||||
sparkbutton:activeImage="@drawable/ic_reblog_blue"
|
||||
sparkbutton:iconSize="28dp"
|
||||
sparkbutton:inactiveImage="@drawable/ic_reblog"
|
||||
sparkbutton:primaryColor="@color/share_blue"
|
||||
sparkbutton:secondaryColor="@color/black" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nlikes"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="50"
|
||||
app:layout_constraintEnd_toEndOf="@+id/liker"
|
||||
app:layout_constraintStart_toStartOf="@+id/liker"
|
||||
app:layout_constraintTop_toBottomOf="@+id/liker"
|
||||
tools:text="2 Likes" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nshares"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="50"
|
||||
android:gravity="end"
|
||||
app:layout_constraintEnd_toEndOf="@+id/reblogger"
|
||||
app:layout_constraintStart_toStartOf="@+id/reblogger"
|
||||
app:layout_constraintTop_toBottomOf="@+id/reblogger"
|
||||
tools:text="3 Shares" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/usernameDesc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nlikes"
|
||||
tools:text="Account" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hyphenationFrequency="full"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/usernameDesc"
|
||||
app:layout_constraintTop_toBottomOf="@+id/usernameDesc"
|
||||
tools:text="This is a description, describing stuff.\nIt contains multiple lines, and that's okay. It's also got some really long lines, and we love it for it." />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/postDate"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textColor="#b3b3b3"
|
||||
app:layout_constraintStart_toStartOf="@+id/usernameDesc"
|
||||
app:layout_constraintTop_toBottomOf="@+id/description"
|
||||
tools:text="Yesterday" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/viewComments"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="14dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/postDate"
|
||||
app:layout_constraintTop_toBottomOf="@+id/postDate"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="3 comments" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.app.main.MainActivity">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/mainConstraint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/accountList"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/colorSurfaceContainerLow"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
android:elevation="10dp"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/navigation"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/navigation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
app:headerLayout="@layout/header_navigation"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:menu="@menu/navigation_main" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/navigation"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.app.main.MainActivity">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/mainConstraint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/main_activity_main_linear_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/main_drawer_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurfaceContainer"
|
||||
android:contentDescription="@string/open_drawer_menu"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_baseline_menu_24" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/tabs"
|
||||
app:elevation="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/navigation_main" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/main_activity_main_linear_layout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
|
||||
android:id="@+id/drawer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="50dp"
|
||||
android:layout_margin="12dp"
|
||||
android:layout_height="50dp"
|
||||
tools:src="@drawable/ic_launcher_foreground"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Account name"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/accountUsername"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountUsername"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
tools:text="\@account@instance.tld"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imageView"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/accountName" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -4,7 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".LoginActivity">
|
||||
tools:context=".login.LoginActivity">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -67,30 +67,6 @@
|
|||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/login_activity_connection_required"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/login_connection_required_once"
|
||||
android:textAlignment="center"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/login_activity_connection_required_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="15dp"
|
||||
android:text="@string/retry"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/progressLayout"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="org.pixeldroid.app.MainActivity">
|
||||
tools:context="org.pixeldroid.app.main.MainActivity">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
@ -37,7 +37,7 @@
|
|||
app:elevation="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/bottom_navigation_main" />
|
||||
app:menu="@menu/navigation_main" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.mikepenz.materialdrawer.widget.AccountHeaderView android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/headerview"/>
|
|
@ -1,24 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto">
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePic"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
android:src="@drawable/ic_default_user"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:contentDescription="@string/profile_picture" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
|
@ -60,10 +60,10 @@
|
|||
android:id="@+id/postPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:orientation="horizontal" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</org.pixeldroid.app.posts.NestedScrollableHost>
|
||||
|
||||
|
@ -123,13 +123,13 @@
|
|||
android:id="@+id/commenter"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:contentDescription="@string/add_comment"
|
||||
android:padding="4dp"
|
||||
android:src="@drawable/selector_commenter"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/liker"
|
||||
app:layout_constraintEnd_toStartOf="@id/reblogger"
|
||||
app:layout_constraintStart_toEndOf="@id/liker"
|
||||
app:layout_constraintTop_toTopOf="@id/liker"
|
||||
android:contentDescription="@string/add_comment" />
|
||||
app:layout_constraintTop_toTopOf="@id/liker" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
android:id="@+id/liker"
|
||||
|
@ -139,15 +139,15 @@
|
|||
android:layout_marginBottom="4dp"
|
||||
android:clipToPadding="false"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/commenter"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="@id/profilePic"
|
||||
app:layout_constraintTop_toBottomOf="@id/postConstraint"
|
||||
sparkbutton:activeImage="@drawable/ic_like_full"
|
||||
sparkbutton:iconSize="28dp"
|
||||
sparkbutton:inactiveImage="@drawable/ic_like_empty"
|
||||
sparkbutton:primaryColor="@color/heart_red"
|
||||
sparkbutton:secondaryColor="@color/black"
|
||||
app:layout_constraintEnd_toStartOf="@id/commenter"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="@id/profilePic"
|
||||
app:layout_constraintTop_toBottomOf="@id/postConstraint"/>
|
||||
sparkbutton:secondaryColor="@color/black" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
android:id="@+id/reblogger"
|
||||
|
@ -167,12 +167,12 @@
|
|||
|
||||
<ImageButton
|
||||
android:id="@+id/status_more"
|
||||
style="?android:attr/actionOverflowButtonStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/status_more_options"
|
||||
android:padding="4dp"
|
||||
style="?android:attr/actionOverflowButtonStyle"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/postDomain"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/postDomain" />
|
||||
|
@ -200,8 +200,8 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/usernameDesc"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="@+id/profilePic"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nlikes"
|
||||
tools:text="Account" />
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<group android:id="@+id/tabsId">
|
||||
<item
|
||||
android:id="@+id/page_1"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/selector_home_feed"
|
||||
android:title="@string/home_feed"/>
|
||||
<item
|
||||
android:id="@+id/page_2"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_search_white_24dp"
|
||||
android:title="@string/search_discover_feed"/>
|
||||
<item
|
||||
android:id="@+id/page_3"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/selector_camera"
|
||||
android:title="@string/create_feed"/>
|
||||
<item
|
||||
android:id="@+id/page_4"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/selector_notifications"
|
||||
android:title="@string/notifications_feed"/>
|
||||
<item
|
||||
android:id="@+id/page_5"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_filter_black_24dp"
|
||||
android:title="@string/public_feed"/>
|
||||
</group>
|
||||
|
||||
<item
|
||||
android:id="@+id/my_profile"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/person"
|
||||
android:title="@string/menu_account"/>
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/settings"
|
||||
android:title="@string/menu_settings"/>
|
||||
<item
|
||||
android:id="@+id/log_out"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/logout"
|
||||
android:title="@string/logout"/>
|
||||
</menu>
|
|
@ -180,8 +180,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
|||
<item quantity="other">"%d\nFollowing"</item>
|
||||
</plurals>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="save_image_failed">Unable to save image</string>
|
||||
<string name="save_image_success">Image successfully saved</string>
|
||||
<string name="save_image_failed">Unable to save</string>
|
||||
<string name="save_image_success">Saved successfully</string>
|
||||
<string name="follow_status_failed">Could not get follow status</string>
|
||||
<string name="edit_link_failed">Failed to open edit page</string>
|
||||
<string name="new_collection_link_failed">Failed to open collection creation page</string>
|
||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.3.1'
|
||||
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
* Support for tablets, and rotation of all screens. There could still be some issues, please send us a message if there are
|
||||
* Bug fixes: notably, fix notifications which broke a couple releases back.
|
||||
* Translation updates
|
||||
* Update dependencies
|
|
@ -2,6 +2,6 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
Loading…
Reference in New Issue