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 {
|
defaultConfig {
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
versionCode 33
|
versionCode 34
|
||||||
versionName "1.0.beta" + versionCode
|
versionName "1.0.beta" + versionCode
|
||||||
|
|
||||||
//TODO add resConfigs("en", "fr", "ja",...) ?
|
//TODO add resConfigs("en", "fr", "ja",...) ?
|
||||||
|
@ -164,15 +164,17 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
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'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AndroidX dependencies:
|
* 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-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.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||||
|
@ -182,23 +184,23 @@ dependencies {
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
implementation 'androidx.paging:paging-runtime-ktx:3.3.2'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
|
implementation "androidx.lifecycle:lifecycle-common-java8:2.8.4"
|
||||||
implementation "androidx.annotation:annotation:1.7.1"
|
implementation "androidx.annotation:annotation:1.8.2"
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
implementation "androidx.activity:activity-ktx:1.9.1"
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
implementation 'androidx.fragment:fragment-ktx:1.8.2'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
implementation 'androidx.work:work-runtime-ktx:2.9.1'
|
||||||
implementation 'androidx.media2:media2-widget:1.3.0'
|
implementation 'androidx.media2:media2-widget:1.3.0'
|
||||||
implementation 'androidx.media2:media2-player:1.3.0'
|
implementation 'androidx.media2:media2-player:1.3.0'
|
||||||
|
|
||||||
|
|
||||||
// Use the most recent version of CameraX
|
// 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-core:$cameraX_version"
|
||||||
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
||||||
// CameraX Lifecycle library
|
// CameraX Lifecycle library
|
||||||
|
@ -220,11 +222,11 @@ dependencies {
|
||||||
|
|
||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
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)
|
//Dagger (dependency injection)
|
||||||
implementation 'com.google.dagger:dagger:2.51'
|
implementation 'com.google.dagger:dagger:2.51.1'
|
||||||
ksp 'com.google.dagger:dagger-compiler:2.51'
|
ksp 'com.google.dagger:dagger-compiler:2.51.1'
|
||||||
|
|
||||||
implementation('com.google.dagger:hilt-android:2.51')
|
implementation('com.google.dagger:hilt-android:2.51')
|
||||||
ksp 'com.google.dagger:hilt-compiler:2.51'
|
ksp 'com.google.dagger:hilt-compiler:2.51'
|
||||||
|
@ -237,7 +239,7 @@ dependencies {
|
||||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||||
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
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: ':scrambler')
|
||||||
implementation project(path: ':pixel_common')
|
implementation project(path: ':pixel_common')
|
||||||
|
|
||||||
|
@ -260,12 +262,6 @@ dependencies {
|
||||||
// Add for NavController support
|
// Add for NavController support
|
||||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
|
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 'com.github.ligi:tracedroid:4.1'
|
||||||
|
|
||||||
implementation 'me.relex:circleindicator:2.1.6'
|
implementation 'me.relex:circleindicator:2.1.6'
|
||||||
|
@ -280,7 +276,7 @@ dependencies {
|
||||||
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
|
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
|
||||||
androidTestUtil 'com.linkedin.testbutler:test-butler-app: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.github.tomakehurst:wiremock-jre8:2.34.0'
|
||||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
@ -288,13 +284,13 @@ dependencies {
|
||||||
|
|
||||||
|
|
||||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
|
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
androidTestImplementation 'androidx.test:runner:1.6.1'
|
||||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
androidTestImplementation 'androidx.test:rules:1.6.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1'
|
||||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
|
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
|
||||||
|
|
||||||
|
|
|
@ -53,9 +53,7 @@
|
||||||
android:theme="@style/BaseAppTheme"/>
|
android:theme="@style/BaseAppTheme"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".posts.ReportActivity"
|
android:name=".posts.ReportActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:theme="@style/BaseAppTheme" />
|
||||||
android:theme="@style/BaseAppTheme"
|
|
||||||
tools:ignore="LockedOrientationActivity" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".stories.StoriesActivity" />
|
android:name=".stories.StoriesActivity" />
|
||||||
|
@ -76,39 +74,29 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.FollowsActivity"
|
android:name=".profile.FollowsActivity"
|
||||||
android:theme="@style/BaseAppTheme"
|
android:theme="@style/BaseAppTheme" />
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
tools:ignore="LockedOrientationActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
|
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
|
||||||
android:theme="@style/BaseAppTheme"
|
android:theme="@style/BaseAppTheme" />
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
tools:ignore="LockedOrientationActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".posts.PostActivity"
|
android:name=".posts.PostActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
tools:ignore="LockedOrientationActivity"
|
|
||||||
android:theme="@style/BaseAppTheme" />
|
android:theme="@style/BaseAppTheme" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".profile.ProfileActivity"
|
android:name=".profile.ProfileActivity"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
tools:ignore="LockedOrientationActivity"
|
|
||||||
android:theme="@style/BaseAppTheme"/>
|
android:theme="@style/BaseAppTheme"/>
|
||||||
<activity android:name=".profile.CollectionActivity"
|
<activity android:name=".profile.CollectionActivity"
|
||||||
android:theme="@style/BaseAppTheme"/>
|
android:theme="@style/BaseAppTheme"/>
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/title_activity_settings2"
|
android:label="@string/title_activity_settings2"
|
||||||
android:parentActivityName=".MainActivity"
|
android:parentActivityName=".main.MainActivity"
|
||||||
android:theme="@style/BaseAppTheme" />
|
android:theme="@style/BaseAppTheme" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".main.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.App.Starting"
|
android:theme="@style/Theme.App.Starting"
|
||||||
android:screenOrientation="sensorPortrait"
|
android:windowSoftInputMode="adjustPan">
|
||||||
android:windowSoftInputMode="adjustPan"
|
|
||||||
tools:ignore="LockedOrientationActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
@ -122,12 +110,10 @@
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".LoginActivity"
|
android:name=".login.LoginActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
android:theme="@style/BaseAppTheme"
|
android:theme="@style/BaseAppTheme"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize">
|
||||||
tools:ignore="LockedOrientationActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
@ -143,9 +129,7 @@
|
||||||
android:name=".searchDiscover.SearchActivity"
|
android:name=".searchDiscover.SearchActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/BaseAppTheme"
|
android:theme="@style/BaseAppTheme"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop">
|
||||||
android:screenOrientation="sensorPortrait"
|
|
||||||
tools:ignore="LockedOrientationActivity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -166,6 +150,16 @@
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -16,31 +16,37 @@ import androidx.activity.addCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.GravityCompat
|
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.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.paging.ExperimentalPagingApi
|
import androidx.paging.ExperimentalPagingApi
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||||
import com.bumptech.glide.Glide
|
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.google.android.material.color.DynamicColors
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.google.android.material.navigation.NavigationBarView
|
||||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
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.iconUrl
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||||
|
@ -49,6 +55,8 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.ligi.tracedroid.sending.sendTraceDroidStackTracesIfExist
|
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.databinding.ActivityMainBinding
|
||||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||||
import org.pixeldroid.app.posts.NestedScrollableHost
|
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.NotificationsWorker.Companion.USER_NOTIFICATION_TAG
|
||||||
import org.pixeldroid.app.utils.notificationsWorker.enablePullNotifications
|
import org.pixeldroid.app.utils.notificationsWorker.enablePullNotifications
|
||||||
import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFromAccount
|
import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFromAccount
|
||||||
|
import org.pixeldroid.common.dpToPx
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
|
@ -179,22 +188,53 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupDrawer() {
|
private fun setupDrawer() {
|
||||||
binding.mainDrawerButton.setOnClickListener{
|
binding.mainDrawerButton?.setOnClickListener {
|
||||||
binding.drawerLayout.openDrawer(binding.drawer)
|
binding.drawer?.let { drawer -> binding.drawerLayout.openDrawer(drawer) }
|
||||||
}
|
}
|
||||||
|
|
||||||
header = AccountHeaderView(this).apply {
|
val navigationHeader = binding.navigation?.getHeaderView(0) as? AccountHeaderView
|
||||||
attachToSliderView(binding.drawer)
|
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
|
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
currentHiddenInList = true
|
currentHiddenInList = true
|
||||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean ->
|
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 {
|
addProfile(ProfileSettingDrawerItem().apply {
|
||||||
identifier = ADD_ACCOUNT_IDENTIFIER
|
identifier = ADD_ACCOUNT_IDENTIFIER
|
||||||
nameRes = R.string.add_account_name
|
nameRes = R.string.add_account_name
|
||||||
descriptionRes = R.string.add_account_description
|
descriptionRes = R.string.add_account_description
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
iconRes = R.drawable.add
|
||||||
}, 0)
|
}, 0)
|
||||||
dividerBelowHeader = false
|
dividerBelowHeader = false
|
||||||
closeDrawerOnProfileListClick = true
|
closeDrawerOnProfileListClick = true
|
||||||
|
@ -202,11 +242,11 @@ class MainActivity : BaseActivity() {
|
||||||
|
|
||||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||||
Glide.with(this@MainActivity)
|
Glide.with(this@MainActivity)
|
||||||
.load(uri)
|
.load(uri)
|
||||||
.placeholder(placeholder)
|
.placeholder(placeholder)
|
||||||
.circleCrop()
|
.circleCrop()
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancel(imageView: ImageView) {
|
override fun cancel(imageView: ImageView) {
|
||||||
|
@ -228,22 +268,23 @@ class MainActivity : BaseActivity() {
|
||||||
//with the received one. This happens asynchronously.
|
//with the received one. This happens asynchronously.
|
||||||
getUpdatedAccount()
|
getUpdatedAccount()
|
||||||
|
|
||||||
binding.drawer.itemAdapter.add(
|
binding.drawer?.itemAdapter?.add(
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.menu_account
|
nameRes = R.string.menu_account
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
iconRes = R.drawable.person
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.menu_settings
|
nameRes = R.string.menu_settings
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
iconRes = R.drawable.settings
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.logout
|
nameRes = R.string.logout
|
||||||
iconicsIcon = GoogleMaterial.Icon.gmd_close
|
iconRes = R.drawable.logout
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
|
|
||||||
when (position){
|
binding.drawer?.onDrawerItemClickListener = { v, drawerItem, position ->
|
||||||
|
when (position) {
|
||||||
1 -> launchActivity(ProfileActivity())
|
1 -> launchActivity(ProfileActivity())
|
||||||
2 -> launchActivity(SettingsActivity())
|
2 -> launchActivity(SettingsActivity())
|
||||||
3 -> logOut()
|
3 -> logOut()
|
||||||
|
@ -254,10 +295,9 @@ class MainActivity : BaseActivity() {
|
||||||
// Closes the drawer if it is open, when we press the back button
|
// Closes the drawer if it is open, when we press the back button
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
// Handle the back button event
|
// Handle the back button event
|
||||||
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
|
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
this.isEnabled = false
|
this.isEnabled = false
|
||||||
super.onBackPressedDispatcher.onBackPressed()
|
super.onBackPressedDispatcher.onBackPressed()
|
||||||
}
|
}
|
||||||
|
@ -305,18 +345,19 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
//called when switching profiles, or when clicking on current profile
|
//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){
|
if(current){
|
||||||
launchActivity(ProfileActivity())
|
launchActivity(ProfileActivity())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
//Clicked on add new account
|
//Clicked on add new account
|
||||||
if(profile.identifier == ADD_ACCOUNT_IDENTIFIER){
|
if(id == null || instance == null){
|
||||||
launchActivity(LoginActivity())
|
launchActivity(LoginActivity())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
switchUser(profile.identifier.toString(), profile.tag as String)
|
switchUser(id, instance)
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
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)
|
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>){
|
private fun setupTabs(tab_array: List<() -> Fragment>){
|
||||||
binding.viewPager.reduceDragSensitivity()
|
binding.viewPager.reduceDragSensitivity()
|
||||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||||
|
@ -422,37 +491,49 @@ class MainActivity : BaseActivity() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (selected != 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)
|
super.onPageSelected(position)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fun MenuItem.itemPos(): Int? {
|
|
||||||
return when(itemId){
|
fun MenuItem.buttonPos() {
|
||||||
R.id.page_1 -> 0
|
when(itemId){
|
||||||
R.id.page_2 -> 1
|
R.id.my_profile -> launchActivity(ProfileActivity())
|
||||||
R.id.page_3 -> 2
|
R.id.settings -> launchActivity(SettingsActivity())
|
||||||
R.id.page_4 -> 3
|
R.id.log_out -> logOut()
|
||||||
R.id.page_5 -> 4
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tabs.setOnItemSelectedListener {item ->
|
(binding.tabs as? NavigationBarView)?.setOnItemSelectedListener { item ->
|
||||||
item.itemPos()?.let {
|
item.itemPos()?.let {
|
||||||
binding.viewPager.currentItem = it
|
binding.viewPager.currentItem = it
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tabs.setOnItemReselectedListener { item ->
|
(binding.tabs as? NavigationBarView)?.setOnItemReselectedListener(::reclick)
|
||||||
item.itemPos()?.let { position ->
|
|
||||||
val page =
|
binding.navigation?.setNavigationItemSelectedListener { item ->
|
||||||
//No clue why this works but it does. F to pay respects
|
if (binding.navigation?.menu?.children?.find { it.itemId == item.itemId }?.isChecked == true) {
|
||||||
supportFragmentManager.findFragmentByTag("f$position")
|
reclick(item)
|
||||||
(page as? CachedFeedFragment<*>)?.onTabReClicked()
|
} 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
|
// Fetch one notification to show a badge if there are new notifications
|
||||||
|
@ -479,20 +560,13 @@ class MainActivity : BaseActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setNotificationBadge(show: Boolean, count: Int? = null){
|
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){
|
if(show){
|
||||||
val badge = binding.tabs.getOrCreateBadge(R.id.page_4)
|
val badge = (binding.tabs as? NavigationBarView)?.getOrCreateBadge(R.id.page_4)
|
||||||
if (count != null) badge.number = count
|
if (count != null) badge?.number = count
|
||||||
}
|
}
|
||||||
else binding.tabs.removeBadge(R.id.page_4)
|
else (binding.tabs as? NavigationBarView)?.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,4 +1,4 @@
|
||||||
package org.pixeldroid.app
|
package org.pixeldroid.app.main
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import org.pixeldroid.app.MainActivity
|
import org.pixeldroid.app.main.MainActivity
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||||
|
|
|
@ -3,7 +3,7 @@ package org.pixeldroid.app.postCreation.camera
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import org.pixeldroid.app.MainActivity
|
import org.pixeldroid.app.main.MainActivity
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.databinding.ActivityCameraBinding
|
import org.pixeldroid.app.databinding.ActivityCameraBinding
|
||||||
import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY
|
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.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.pixeldroid.app.R
|
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
|
import org.pixeldroid.app.utils.displayDimensionsInPx
|
||||||
|
|
||||||
class PostActivity : BaseActivity() {
|
class PostActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivityPostBinding
|
lateinit var binding: ActivityPostBinding
|
||||||
|
|
||||||
private lateinit var commentFragment: CommentFragment
|
private lateinit var commentFragment: CommentFragment
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class PostActivity : BaseActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityPostBinding.inflate(layoutInflater)
|
binding = ActivityPostBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
commentFragment = CommentFragment(binding.swipeRefreshLayout)
|
commentFragment = CommentFragment()
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setSupportActionBar(binding.topBar)
|
setSupportActionBar(binding.topBar)
|
||||||
|
@ -48,14 +48,15 @@ class PostActivity : BaseActivity() {
|
||||||
supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName())
|
supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName())
|
||||||
|
|
||||||
val holder = StatusViewHolder(binding.postFragmentSingle)
|
val holder = StatusViewHolder(binding.postFragmentSingle)
|
||||||
|
val (width, height) = displayDimensionsInPx()
|
||||||
|
|
||||||
holder.bind(
|
holder.bind(
|
||||||
status, apiHolder, db, lifecycleScope, displayDimensionsInPx(),
|
status, apiHolder, db, lifecycleScope, Pair((width*.7).toInt(), height),
|
||||||
requestPermissionDownloadPic, isActivity = true
|
requestPermissionDownloadPic, isActivity = true
|
||||||
)
|
)
|
||||||
|
|
||||||
activateCommenter()
|
activateCommenter()
|
||||||
initCommentsFragment(domain = user?.instance_uri.orEmpty())
|
initCommentsFragment(domain = user?.instance_uri.orEmpty(), savedInstanceState)
|
||||||
|
|
||||||
if(viewComments || postComment){
|
if(viewComments || postComment){
|
||||||
//Scroll already down as much as possible (since comments are not loaded yet)
|
//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()
|
val arguments = Bundle()
|
||||||
arguments.putSerializable(COMMENT_STATUS_ID, status.id)
|
arguments.putSerializable(COMMENT_STATUS_ID, status.id)
|
||||||
arguments.putSerializable(COMMENT_DOMAIN, domain)
|
arguments.putSerializable(COMMENT_DOMAIN, domain)
|
||||||
commentFragment.arguments = arguments
|
commentFragment.arguments = arguments
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction()
|
//TODO finish work here! commentFragment needs the swiperefreshlayout.. how??
|
||||||
.add(R.id.commentFragment, commentFragment).commit()
|
//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 {
|
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||||
commentFragment.adapter.refresh()
|
commentFragment.adapter.refresh()
|
||||||
|
|
|
@ -2,15 +2,22 @@ package org.pixeldroid.app.posts
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.databinding.ActivityReportBinding
|
import org.pixeldroid.app.databinding.ActivityReportBinding
|
||||||
|
import org.pixeldroid.app.posts.ReportActivityViewModel.UploadState
|
||||||
import org.pixeldroid.app.utils.BaseActivity
|
import org.pixeldroid.app.utils.BaseActivity
|
||||||
import org.pixeldroid.app.utils.api.objects.Status
|
import org.pixeldroid.app.utils.api.objects.Status
|
||||||
|
|
||||||
class ReportActivity : BaseActivity() {
|
class ReportActivity : BaseActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityReportBinding
|
private lateinit var binding: ActivityReportBinding
|
||||||
|
private val model: ReportActivityViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -24,42 +31,47 @@ class ReportActivity : BaseActivity() {
|
||||||
|
|
||||||
binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct)
|
binding.reportTargetTextview.text = getString(R.string.report_target).format(status?.account?.acct)
|
||||||
|
|
||||||
|
binding.textInputLayout.editText?.text = model.editable
|
||||||
|
|
||||||
binding.reportButton.setOnClickListener{
|
binding.textInputLayout.editText?.doAfterTextChanged { model.textChanged(it) }
|
||||||
binding.reportButton.visibility = View.INVISIBLE
|
|
||||||
binding.reportProgressBar.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
binding.textInputLayout.editText?.isEnabled = false
|
binding.reportButton.setOnClickListener {
|
||||||
|
model.sendReport(status, binding.textInputLayout.editText?.text.toString())
|
||||||
|
}
|
||||||
|
|
||||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
lifecycleScope.launchWhenCreated {
|
model.reportSent.collect {
|
||||||
try {
|
reportStatus(it)
|
||||||
api.report(
|
|
||||||
status?.account?.id!!,
|
|
||||||
listOf(status),
|
|
||||||
binding.textInputLayout.editText?.text.toString()
|
|
||||||
)
|
|
||||||
|
|
||||||
reportStatus(true)
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
reportStatus(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reportStatus(success: Boolean){
|
private fun reportStatus(success: UploadState){
|
||||||
if(success){
|
when (success) {
|
||||||
binding.reportProgressBar.visibility = View.GONE
|
UploadState.initial -> {
|
||||||
binding.reportButton.visibility = View.INVISIBLE
|
binding.reportProgressBar.visibility = View.GONE
|
||||||
binding.reportSuccess.visibility = View.VISIBLE
|
binding.reportButton.visibility = View.VISIBLE
|
||||||
} else {
|
binding.reportSuccess.visibility = View.INVISIBLE
|
||||||
binding.textInputLayout.error = getString(R.string.report_error)
|
}
|
||||||
binding.reportButton.visibility = View.VISIBLE
|
UploadState.success -> {
|
||||||
binding.textInputLayout.editText?.isEnabled = true
|
binding.reportProgressBar.visibility = View.GONE
|
||||||
binding.reportProgressBar.visibility = View.GONE
|
binding.reportButton.visibility = View.INVISIBLE
|
||||||
binding.reportSuccess.visibility = View.GONE
|
binding.reportSuccess.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
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()) {
|
if(!status?.media_attachments.isNullOrEmpty()) {
|
||||||
setupPostPics(binding, request)
|
setupPostPics(binding, request)
|
||||||
} else {
|
} else {
|
||||||
binding.postPager.visibility = View.GONE
|
binding.postConstraint.visibility = View.GONE
|
||||||
binding.postIndicator.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,16 +55,16 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
||||||
//TODO rename function to something that makes sense
|
//TODO rename function to something that makes sense
|
||||||
internal fun initSearch() {
|
internal fun initSearch() {
|
||||||
// Scroll to top when the list is refreshed from network.
|
// Scroll to top when the list is refreshed from network.
|
||||||
lifecycleScope.launchWhenStarted {
|
// lifecycleScope.launchWhenStarted {
|
||||||
adapter.loadStateFlow
|
// adapter.loadStateFlow
|
||||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
// // Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||||
.distinctUntilChangedBy {
|
// .distinctUntilChangedBy {
|
||||||
it.refresh
|
// it.refresh
|
||||||
}
|
// }
|
||||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
// // Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||||
.filter { it.refresh is NotLoading}
|
// .filter { it.refresh is NotLoading}
|
||||||
.collect { binding.list.scrollToPosition(0) }
|
// .collect { binding.list.scrollToPosition(0) }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
|
|
@ -40,8 +40,6 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||||
|
|
||||||
home = requireArguments().getBoolean("home")
|
home = requireArguments().getBoolean("home")
|
||||||
|
|
||||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
if (home){
|
if (home){
|
||||||
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||||
|
@ -61,6 +59,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View? {
|
||||||
|
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||||
|
|
||||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
|
|
|
@ -42,15 +42,15 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun initSearch() {
|
internal fun initSearch() {
|
||||||
// Scroll to top when the list is refreshed from network.
|
// // Scroll to top when the list is refreshed from network.
|
||||||
lifecycleScope.launch {
|
// lifecycleScope.launch {
|
||||||
adapter.loadStateFlow
|
// adapter.loadStateFlow
|
||||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
// // Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||||
.distinctUntilChangedBy { it.refresh }
|
// .distinctUntilChangedBy { it.refresh }
|
||||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
// // Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||||
.filter { it.refresh is LoadState.NotLoading }
|
// .filter { it.refresh is LoadState.NotLoading }
|
||||||
.collect { binding?.list?.scrollToPosition(0) }
|
// .collect { binding?.list?.scrollToPosition(0) }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCreateView(
|
fun onCreateView(
|
||||||
|
|
|
@ -28,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
|
||||||
/**
|
/**
|
||||||
* Fragment to show a list of [Status]s, in form of comments
|
* 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 id: String
|
||||||
private lateinit var domain: String
|
private lateinit var domain: String
|
||||||
|
@ -48,8 +48,10 @@ class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedF
|
||||||
savedInstanceState: Bundle?,
|
savedInstanceState: Bundle?,
|
||||||
): View? {
|
): View? {
|
||||||
|
|
||||||
|
val view = super.onCreateView(
|
||||||
val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
|
inflater, container, savedInstanceState,
|
||||||
|
(activity as? PostActivity)?.binding?.swipeRefreshLayout
|
||||||
|
)
|
||||||
|
|
||||||
// Get the view model
|
// Get the view model
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
|
package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
|
||||||
|
|
||||||
import android.os.Bundle
|
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.R
|
||||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
|
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.BaseActivity
|
||||||
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
|
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
|
||||||
|
|
||||||
|
|
||||||
class HashTagActivity : BaseActivity() {
|
class HashTagActivity : BaseActivity() {
|
||||||
private var tagFragment = UncachedPostsFragment()
|
|
||||||
private lateinit var binding: ActivityFollowersBinding
|
private lateinit var binding: ActivityFollowersBinding
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||||
|
@ -32,10 +34,10 @@ class HashTagActivity : BaseActivity() {
|
||||||
|
|
||||||
val arguments = Bundle()
|
val arguments = Bundle()
|
||||||
arguments.putSerializable(HASHTAG_TAG, tag)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
// Relaunch same activity, to avoid duplicates in history
|
// Relaunch same activity, to avoid duplicates in history
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
finish()
|
finish()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.pixeldroid.app.profile
|
package org.pixeldroid.app.profile
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.commit
|
||||||
|
import androidx.fragment.app.replace
|
||||||
import org.pixeldroid.app.R
|
import org.pixeldroid.app.R
|
||||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
|
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() {
|
class FollowsActivity : BaseActivity() {
|
||||||
private var followsFragment = AccountListFragment()
|
|
||||||
private lateinit var binding: ActivityFollowersBinding
|
private lateinit var binding: ActivityFollowersBinding
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||||
|
@ -47,10 +47,10 @@ class FollowsActivity : BaseActivity() {
|
||||||
val arguments = Bundle()
|
val arguments = Bundle()
|
||||||
arguments.putSerializable(ACCOUNT_ID_TAG, id)
|
arguments.putSerializable(ACCOUNT_ID_TAG, id)
|
||||||
arguments.putSerializable(FOLLOWERS_TAG, followers)
|
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.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
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.R
|
||||||
import org.pixeldroid.app.databinding.SettingsBinding
|
import org.pixeldroid.app.databinding.SettingsBinding
|
||||||
import org.pixeldroid.common.ThemedActivity
|
import org.pixeldroid.common.ThemedActivity
|
||||||
|
|
|
@ -1,13 +1,29 @@
|
||||||
package org.pixeldroid.app.utils
|
package org.pixeldroid.app.utils
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.work.Configuration
|
||||||
import com.google.android.material.color.DynamicColors
|
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.android.HiltAndroidApp
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
import org.ligi.tracedroid.TraceDroid
|
import org.ligi.tracedroid.TraceDroid
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@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() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
|
@ -12,36 +12,41 @@ import android.os.Build
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.work.CoroutineWorker
|
import androidx.work.CoroutineWorker
|
||||||
import androidx.work.WorkerParameters
|
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.R
|
||||||
import org.pixeldroid.app.posts.PostActivity
|
import org.pixeldroid.app.posts.PostActivity
|
||||||
import org.pixeldroid.app.posts.fromHtml
|
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.PixelfedAPI.Companion.apiForUser
|
||||||
import org.pixeldroid.app.utils.api.objects.Notification
|
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.api.objects.Status
|
||||||
import org.pixeldroid.app.utils.db.AppDatabase
|
import org.pixeldroid.app.utils.db.AppDatabase
|
||||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||||
import org.pixeldroid.app.utils.getColorFromAttr
|
import org.pixeldroid.app.utils.getColorFromAttr
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class NotificationsWorker(
|
@HiltWorker
|
||||||
context: Context,
|
class NotificationsWorker @AssistedInject constructor(
|
||||||
params: WorkerParameters
|
@Assisted context: Context,
|
||||||
|
@Assisted params: WorkerParameters,
|
||||||
|
private val db: AppDatabase,
|
||||||
|
private val apiHolder: PixelfedAPIHolder
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var db: AppDatabase
|
|
||||||
@Inject
|
|
||||||
lateinit var apiHolder: PixelfedAPIHolder
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val users: List<UserDatabaseEntity> = db.userDao().getAll()
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".LoginActivity">
|
tools:context=".login.LoginActivity">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -67,30 +67,6 @@
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</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
|
<LinearLayout
|
||||||
android:id="@+id/progressLayout"
|
android:id="@+id/progressLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
android:id="@+id/drawer_layout"
|
android:id="@+id/drawer_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="org.pixeldroid.app.MainActivity">
|
tools:context="org.pixeldroid.app.main.MainActivity">
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
app:elevation="0dp"
|
app:elevation="0dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:menu="@menu/bottom_navigation_main" />
|
app:menu="@menu/navigation_main" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<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,238 +1,238 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:layout_marginBottom="5dp"
|
android:layout_marginBottom="5dp">
|
||||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/profilePic"
|
android:id="@+id/profilePic"
|
||||||
android:layout_width="50dp"
|
android:layout_width="50dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginStart="10dp"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:src="@drawable/ic_default_user"
|
android:contentDescription="@string/profile_picture"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:src="@drawable/ic_default_user"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:contentDescription="@string/profile_picture" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/username"
|
android:id="@+id/username"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
||||||
app:layout_constraintStart_toEndOf="@+id/profilePic"
|
app:layout_constraintStart_toEndOf="@+id/profilePic"
|
||||||
app:layout_constraintTop_toTopOf="@+id/profilePic"
|
app:layout_constraintTop_toTopOf="@+id/profilePic"
|
||||||
tools:text="username" />
|
tools:text="username" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/postDomain"
|
android:id="@+id/postDomain"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:textColor="#b3b3b3"
|
android:textColor="#b3b3b3"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
app:layout_constraintBottom_toBottomOf="@+id/profilePic"
|
||||||
app:layout_constraintStart_toEndOf="@+id/username"
|
app:layout_constraintStart_toEndOf="@+id/username"
|
||||||
app:layout_constraintTop_toTopOf="@+id/profilePic"
|
app:layout_constraintTop_toTopOf="@+id/profilePic"
|
||||||
tools:text="from domain.tld" />
|
tools:text="from domain.tld" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/postConstraint"
|
android:id="@+id/postConstraint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/profilePic">
|
||||||
|
|
||||||
|
<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_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/postPager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="200dp"
|
||||||
android:layout_marginTop="10dp"
|
android:orientation="horizontal"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/profilePic">
|
|
||||||
|
|
||||||
<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_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"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:orientation="horizontal" />
|
|
||||||
|
|
||||||
</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>
|
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/commenter"
|
|
||||||
android:layout_width="30dp"
|
|
||||||
android:layout_height="30dp"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<at.connyduck.sparkbutton.SparkButton
|
|
||||||
android:id="@+id/liker"
|
|
||||||
android:layout_width="30dp"
|
|
||||||
android:layout_height="30dp"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:padding="4dp"
|
|
||||||
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"/>
|
|
||||||
|
|
||||||
<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_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"/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/status_more"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<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_height="wrap_content"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
app:layout_constraintStart_toStartOf="@+id/profilePic"
|
|
||||||
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="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="10dp"
|
|
||||||
android:textColor="#b3b3b3"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/description"
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
tools:text="Yesterday" />
|
|
||||||
|
|
||||||
<TextView
|
</org.pixeldroid.app.posts.NestedScrollableHost>
|
||||||
android:id="@+id/viewComments"
|
|
||||||
android:layout_width="match_parent"
|
<me.relex.circleindicator.CircleIndicator3
|
||||||
android:layout_height="wrap_content"
|
android:id="@+id/postIndicator"
|
||||||
android:layout_marginStart="10dp"
|
android:layout_width="wrap_content"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:layout_height="32dp"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/postDate"
|
app:layout_constraintBottom_toBottomOf="@+id/postPagerHost"
|
||||||
tools:text="3 comments" />
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
<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:layout_marginTop="4dp"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
|
||||||
|
<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/postDomain"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/postDomain" />
|
||||||
|
|
||||||
|
<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"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/profilePic"
|
||||||
|
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="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="10dp"
|
||||||
|
android:textColor="#b3b3b3"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/description"
|
||||||
|
tools:text="Yesterday" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/viewComments"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/postDate"
|
||||||
|
tools:text="3 comments" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -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>
|
<item quantity="other">"%d\nFollowing"</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="edit">Edit</string>
|
<string name="edit">Edit</string>
|
||||||
<string name="save_image_failed">Unable to save image</string>
|
<string name="save_image_failed">Unable to save</string>
|
||||||
<string name="save_image_success">Image successfully saved</string>
|
<string name="save_image_success">Saved successfully</string>
|
||||||
<string name="follow_status_failed">Could not get follow status</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="edit_link_failed">Failed to open edit page</string>
|
||||||
<string name="new_collection_link_failed">Failed to open collection creation page</string>
|
<string name="new_collection_link_failed">Failed to open collection creation page</string>
|
||||||
|
|
|
@ -6,7 +6,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
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"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// 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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
Loading…
Reference in New Issue