Merge branch 'master' into view_models
This commit is contained in:
commit
908d1a54c9
@ -2,6 +2,7 @@ image: registry.gitlab.com/fdroid/fdroidserver:buildserver-bullseye
|
||||
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
GIT_SUBMODULE_FORCE_HTTPS: "true"
|
||||
|
||||
before_script:
|
||||
- export GRADLE_USER_HOME=`pwd`/.gradle
|
||||
@ -19,7 +20,7 @@ before_script:
|
||||
- test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH"
|
||||
|
||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
|
||||
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
|
||||
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
|
||||
|
||||
- apt-get update || apt-get update
|
||||
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
||||
[submodule "scrambler"]
|
||||
path = scrambler
|
||||
url = https://gitlab.com/artectrex/scrambler.git
|
||||
[submodule "pixel_common"]
|
||||
path = pixel_common
|
||||
url = git@gitlab.shinice.net:pixeldroid/pixel_common.git
|
||||
|
@ -9,13 +9,16 @@ Free (as in freedom) Android client for Pixelfed, the federated image sharing pl
|
||||
<img src="https://pixeldroid.org/badge-fdroid.png" alt="Get it on F-Droid" width="206">
|
||||
</a>
|
||||
|
||||
Come talk to us on Matrix, at <a href="https://matrix.to/#/#pixeldroid:gnugen.ch">#pixeldroid:gnugen.ch</a> !
|
||||
|
||||
## 🔧 Compiling the code yourself
|
||||
If you want to try out PixelDroid on your own device, you can compile the source code yourself. To do that you can install [Android Studio](https://developer.android.com/studio/).
|
||||
|
||||
## 🎨 Art attribution
|
||||
Our mascot was commissioned using funds from NLnet. The original file is `pixeldroid_mascot.svg` and it is adapted to work as an Android Drawable. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA).
|
||||
|
||||
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ). In addition, a drawing of a red panda is used for some error messages ( https://thenounproject.com/search/?q=red+panda&i=2877785 ).
|
||||
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ).
|
||||
|
||||
## 🤝 Contribute
|
||||
If you want to contribute, you can check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or [TRANSLATION.md](TRANSLATION.md)
|
||||
|
||||
|
144
app/build.gradle
144
app/build.gradle
@ -1,41 +1,37 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
id "com.mikepenz.aboutlibraries.plugin" version "10.5.2"
|
||||
id("com.android.application")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-android")
|
||||
id("jacoco")
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: "kotlin-parcelize"
|
||||
|
||||
|
||||
|
||||
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155
|
||||
jacoco.toolVersion = "0.8.7"
|
||||
|
||||
|
||||
android {
|
||||
|
||||
namespace 'org.pixeldroid.app'
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion '33.0.0'
|
||||
compileSdk 34
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig true
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 33
|
||||
versionCode 24
|
||||
targetSdkVersion 34
|
||||
versionCode 31
|
||||
versionName "1.0.beta" + versionCode
|
||||
|
||||
//TODO add resConfigs("en", "fr", "ja",...) ?
|
||||
@ -87,8 +83,9 @@ android {
|
||||
/**
|
||||
* Make a string with the application_id (available in xml etc)
|
||||
*/
|
||||
android.applicationVariants.all { variant ->
|
||||
android.applicationVariants.configureEach { variant ->
|
||||
variant.resValue 'string', 'application_id', variant.applicationId
|
||||
variant.resValue "string", "versionName", variant.versionName
|
||||
}
|
||||
|
||||
testOptions {
|
||||
@ -113,11 +110,9 @@ android {
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
dataBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
lint {
|
||||
//We can't expect translators to always keep up immediately:
|
||||
// don't fail if a a string is untranslated
|
||||
@ -131,40 +126,40 @@ android {
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
|
||||
|
||||
/**
|
||||
* AndroidX dependencies:
|
||||
*/
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
||||
implementation "androidx.browser:browser:1.5.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||
implementation "androidx.browser:browser:1.7.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.1.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1'
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1"
|
||||
implementation "androidx.annotation:annotation:1.6.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.7.0"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.6'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
implementation 'androidx.media2:media2-widget:1.2.1'
|
||||
implementation 'androidx.media2:media2-player:1.2.1'
|
||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation 'androidx.media2:media2-widget:1.3.0'
|
||||
implementation 'androidx.media2:media2-player:1.3.0'
|
||||
|
||||
|
||||
// Use the most recent version of CameraX
|
||||
def cameraX_version = '1.2.2'
|
||||
def cameraX_version = '1.3.1'
|
||||
implementation "androidx.camera:camera-core:$cameraX_version"
|
||||
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
||||
// CameraX Lifecycle library
|
||||
@ -173,9 +168,9 @@ dependencies {
|
||||
// CameraX View class
|
||||
implementation "androidx.camera:camera-view:$cameraX_version"
|
||||
|
||||
def room_version = "2.5.1"
|
||||
def room_version = "2.6.1"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
ksp "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
implementation "androidx.room:room-paging:$room_version"
|
||||
|
||||
@ -186,61 +181,56 @@ dependencies {
|
||||
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
|
||||
//Dagger (dependency injection)
|
||||
implementation 'com.google.dagger:dagger-android:2.45'
|
||||
implementation 'com.google.dagger:dagger-android-support:2.44'
|
||||
// if you use the support libraries
|
||||
kapt 'com.google.dagger:dagger-android-processor:2.44'
|
||||
kapt 'com.google.dagger:dagger-compiler:2.44'
|
||||
implementation 'com.google.dagger:dagger:2.50'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.50'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
|
||||
implementation("com.google.dagger:hilt-android:2.50")
|
||||
ksp "com.google.dagger:hilt-compiler:2.50"
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
||||
|
||||
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.4'
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.7'
|
||||
implementation project(path: ':scrambler')
|
||||
implementation project(path: ':pixel_common')
|
||||
|
||||
implementation('com.github.bumptech.glide:glide:4.14.2') {
|
||||
implementation('com.github.bumptech.glide:glide:4.16.0') {
|
||||
exclude group: "com.android.support"
|
||||
}
|
||||
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.14.2'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.14.2') {
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.16.0') {
|
||||
// Excludes the support library because it's already included by Glide.
|
||||
transitive = false
|
||||
}
|
||||
implementation 'com.github.bumptech.glide:annotations:4.14.2'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.14.2'
|
||||
implementation 'com.github.bumptech.glide:annotations:4.16.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
ksp 'com.github.bumptech.glide:ksp:4.16.0'
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
|
||||
implementation 'com.mikepenz:materialdrawer:9.0.1'
|
||||
implementation 'com.mikepenz:materialdrawer:9.0.2'
|
||||
// Add for NavController support
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.1'
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
|
||||
|
||||
//iconics
|
||||
implementation 'com.mikepenz:iconics-core:5.4.0'
|
||||
implementation 'com.mikepenz:materialdrawer-iconics:9.0.1'
|
||||
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.karumi:dexter:6.2.3'
|
||||
|
||||
implementation 'com.github.ligi:tracedroid:4.1'
|
||||
|
||||
implementation 'me.relex:circleindicator:2.1.6'
|
||||
|
||||
implementation 'com.mikepenz:aboutlibraries-core:10.6.0'
|
||||
|
||||
/**
|
||||
* Not in release, so not mentioned in licenses list
|
||||
*/
|
||||
@ -251,7 +241,7 @@ dependencies {
|
||||
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
|
||||
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1'
|
||||
|
||||
androidTestImplementation 'androidx.work:work-testing:2.8.1'
|
||||
androidTestImplementation 'androidx.work:work-testing:2.9.0'
|
||||
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
|
||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
@ -267,11 +257,11 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
|
||||
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
tasks.withType(Test).configureEach {
|
||||
jacoco.includeNoLocationClasses = true
|
||||
jacoco.excludes = ['jdk.internal.*']
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="org.pixeldroid.app.debug"
|
||||
android:targetClass="org.pixeldroid.app.postCreation.camera.CameraActivityShortcut" />
|
||||
android:targetClass="org.pixeldroid.app.postCreation.camera.CameraActivity" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
<capability-binding android:key="actions.intent.CREATE_MESSAGE" />
|
||||
</shortcut>
|
||||
|
@ -5,14 +5,13 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera.any"
|
||||
android:required="false" />
|
||||
<uses-feature android:name="android.hardware.location.gps" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false" />
|
||||
@ -26,7 +25,6 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:theme="@style/BaseAppTheme">
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
@ -40,35 +38,32 @@
|
||||
<activity
|
||||
android:name=".posts.AlbumActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
||||
android:theme="@style/TransparentAlbumActivity"/>
|
||||
<activity
|
||||
android:name=".profile.EditProfileActivity"
|
||||
android:exported="false"/>
|
||||
android:exported="false"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".posts.MediaViewerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="false"
|
||||
android:theme="@style/BaseAppTheme.NoActionBar" />
|
||||
<activity android:name=".postCreation.camera.CameraActivity"/>
|
||||
<activity android:name=".postCreation.camera.CameraActivityShortcut"
|
||||
android:exported = "true"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".MainActivity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity android:name=".postCreation.camera.CameraActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity
|
||||
android:name=".posts.ReportActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.StoriesActivity" />
|
||||
<activity
|
||||
android:name=".postCreation.PostCreationActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/BaseAppTheme.NoActionBar">
|
||||
android:theme="@style/BaseAppTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
@ -81,27 +76,32 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".profile.FollowsActivity"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity
|
||||
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity
|
||||
android:name=".posts.PostActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity android:name=".profile.CollectionActivity"/>
|
||||
tools:ignore="LockedOrientationActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity android:name=".profile.CollectionActivity"
|
||||
android:theme="@style/BaseAppTheme"/>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/title_activity_settings2"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -125,7 +125,7 @@
|
||||
android:name=".LoginActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/BaseAppTheme.NoActionBar"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
<intent-filter>
|
||||
@ -142,6 +142,7 @@
|
||||
<activity
|
||||
android:name=".searchDiscover.SearchActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/BaseAppTheme"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
@ -153,17 +154,8 @@
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
<activity android:name=".searchDiscover.TrendingActivity"/>
|
||||
<activity
|
||||
android:name=".settings.AboutActivity"
|
||||
android:parentActivityName=".settings.SettingsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity
|
||||
android:name=".settings.LicenseActivity"
|
||||
android:parentActivityName=".settings.AboutActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
tools:ignore="LockedOrientationActivity" />
|
||||
<activity android:name=".searchDiscover.TrendingActivity"
|
||||
android:theme="@style/BaseAppTheme" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.pixeldroid.app
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
@ -16,7 +15,7 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.ActivityLoginBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
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
|
||||
@ -45,7 +44,7 @@ since they do not depend on each other)
|
||||
|
||||
*/
|
||||
|
||||
class LoginActivity : BaseThemedWithoutBarActivity() {
|
||||
class LoginActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID
|
||||
|
@ -12,7 +12,9 @@ import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@ -28,6 +30,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
@ -35,7 +38,12 @@ import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
@ -50,12 +58,12 @@ import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment
|
||||
import org.pixeldroid.app.profile.ProfileActivity
|
||||
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
|
||||
import org.pixeldroid.app.settings.SettingsActivity
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.hasInternet
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
|
||||
@ -65,11 +73,13 @@ import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFr
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var header: AccountHeaderView
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
||||
private val model: MainActivityViewModel by viewModels()
|
||||
|
||||
companion object {
|
||||
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
|
||||
}
|
||||
@ -195,6 +205,7 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
Glide.with(this@MainActivity)
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
@ -229,7 +240,8 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.logout
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_close
|
||||
})
|
||||
},
|
||||
)
|
||||
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
|
||||
when (position){
|
||||
1 -> launchActivity(ProfileActivity())
|
||||
@ -238,6 +250,18 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Closes the drawer if it is open, when we press the back button
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Handle the back button event
|
||||
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
}
|
||||
else {
|
||||
this.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logOut(){
|
||||
@ -250,13 +274,13 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
|
||||
val remainingUsers = db.userDao().getAll()
|
||||
if (remainingUsers.isEmpty()){
|
||||
//no more users, start first-time login flow
|
||||
// No more users, start first-time login flow
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
val newActive = remainingUsers.first()
|
||||
db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
//relaunch the app
|
||||
// Relaunch the app
|
||||
launchActivity(MainActivity(), firstTime = true)
|
||||
}
|
||||
}
|
||||
@ -267,16 +291,12 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val domain = user?.instance_uri.orEmpty()
|
||||
val accessToken = user?.accessToken.orEmpty()
|
||||
val refreshToken = user?.refreshToken
|
||||
val clientId = user?.clientId.orEmpty()
|
||||
val clientSecret = user?.clientSecret.orEmpty()
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val account = api.verifyCredentials()
|
||||
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
|
||||
fillDrawerAccountInfo(account.id!!)
|
||||
updateUserInfoDb(db, account)
|
||||
|
||||
//No need to update drawer account info here, the ViewModel listens to db updates
|
||||
} catch (exception: Exception) {
|
||||
Log.e("ACCOUNT UPDATE:", exception.toString())
|
||||
}
|
||||
@ -308,9 +328,11 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
}
|
||||
|
||||
private fun switchUser(userId: String, instance_uri: String) {
|
||||
db.userDao().deActivateActiveUsers()
|
||||
db.userDao().activateUser(userId, instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
db.runInTransaction{
|
||||
db.userDao().deActivateActiveUsers()
|
||||
db.userDao().activateUser(userId, instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
|
||||
@ -323,35 +345,41 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
}
|
||||
|
||||
private fun fillDrawerAccountInfo(account: String) {
|
||||
val users = db.userDao().getAll().toMutableList()
|
||||
users.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.users.collect { list ->
|
||||
val users = list.toMutableList()
|
||||
users.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
val profiles: MutableList<IProfile> = users.map { user ->
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = user.isActive
|
||||
nameText = user.display_name
|
||||
iconUrl = user.avatar_static
|
||||
isNameShown = true
|
||||
identifier = user.user_id.toLong()
|
||||
descriptionText = user.fullHandle
|
||||
tag = user.instance_uri
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
header.profiles.orEmpty()
|
||||
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
|
||||
.take(1)
|
||||
.forEach { profiles.add(it) }
|
||||
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(account.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
val profiles: MutableList<IProfile> = users.map { user ->
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = user.isActive
|
||||
nameText = user.display_name
|
||||
iconUrl = user.avatar_static
|
||||
isNameShown = true
|
||||
identifier = user.user_id.toLong()
|
||||
descriptionText = user.fullHandle
|
||||
tag = user.instance_uri
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
header.profiles.orEmpty()
|
||||
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
|
||||
.take(1)
|
||||
.forEach { profiles.add(it) }
|
||||
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(account.toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -480,16 +508,4 @@ class MainActivity : BaseThemedWithoutBarActivity() {
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the drawer if it is open, when we press the back button
|
||||
*/
|
||||
override fun onBackPressed() {
|
||||
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package org.pixeldroid.app
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
private val db: AppDatabase
|
||||
): ViewModel() {
|
||||
|
||||
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
|
||||
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
|
||||
|
||||
// Immutable state flow exposed to UI
|
||||
val users = _users.asStateFlow()
|
||||
|
||||
|
||||
init {
|
||||
getUsers()
|
||||
}
|
||||
|
||||
private fun getUsers() {
|
||||
viewModelScope.launch {
|
||||
db.userDao().getAllFlow().flowOn(Dispatchers.IO)
|
||||
.collect { users: List<UserDatabaseEntity> ->
|
||||
_users.update { users }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +1,57 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.os.*
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
|
||||
const val TAG = "Post Creation Activity"
|
||||
|
||||
class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||
class PostCreationActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||
internal const val POST_DESCRIPTION = "post_description"
|
||||
internal const val PICTURE_DESCRIPTIONS = "picture_descriptions"
|
||||
internal const val POST_REDRAFT = "post_redraft"
|
||||
internal const val POST_NSFW = "post_nsfw"
|
||||
internal const val TEMP_FILES = "temp_files"
|
||||
|
||||
fun intentForUris(context: Context, uris: List<Uri>) =
|
||||
Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||
// Pass downloaded images to new post creation activity
|
||||
putParcelableArrayListExtra(
|
||||
Intent.EXTRA_STREAM, ArrayList(uris)
|
||||
)
|
||||
|
||||
uris.forEach {
|
||||
// Why are we using ClipData in addition to parcelableArrayListExtra here?
|
||||
// Because the FLAG_GRANT_READ_URI_PERMISSION needs to be applied to the URIs, and
|
||||
// for some reason it doesn't get applied to all of them when not using ClipData
|
||||
if (clipData == null) {
|
||||
clipData = ClipData("", emptyArray(), ClipData.Item(it))
|
||||
} else {
|
||||
clipData!!.addItem(ClipData.Item(it))
|
||||
}
|
||||
}
|
||||
|
||||
setClass(context, PostCreationActivity::class.java)
|
||||
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
@ -46,8 +60,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
|
||||
navController.setGraph(R.navigation.post_creation_graph)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||
}
|
||||
override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp()
|
||||
|
||||
}
|
@ -35,25 +35,21 @@ import org.pixeldroid.app.databinding.FragmentPostCreationBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.carousel.CarouselItem
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class PostCreationFragment : BaseFragment() {
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
|
||||
|
||||
private lateinit var binding: FragmentPostCreationBinding
|
||||
private lateinit var model: PostCreationViewModel
|
||||
private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
|
||||
private val model: PostCreationViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
@ -63,35 +59,23 @@ class PostCreationFragment : BaseFragment() {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
binding = FragmentPostCreationBinding.inflate(layoutInflater)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
val user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
val instance = user?.run {
|
||||
db.instanceDao().getInstance(instance_uri)
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList<PhotoData>? ->
|
||||
// update UI
|
||||
binding.carousel.addData(
|
||||
newPhotoData.map {
|
||||
newPhotoData.orEmpty().map {
|
||||
CarouselItem(
|
||||
it.imageUri, it.imageDescription, it.video,
|
||||
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
|
||||
@ -99,6 +83,7 @@ class PostCreationFragment : BaseFragment() {
|
||||
)
|
||||
}
|
||||
)
|
||||
binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
@ -119,13 +104,26 @@ class PostCreationFragment : BaseFragment() {
|
||||
binding.toolbarPostCreation.visibility =
|
||||
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
|
||||
binding.carousel.layoutCarousel = uiState.isCarousel
|
||||
|
||||
if(uiState.storyCreation){
|
||||
binding.toggleStoryPost.check(binding.buttonStory.id)
|
||||
binding.buttonStory.isPressed = true
|
||||
binding.carousel.showLayoutSwitchButton = false
|
||||
binding.carousel.showIndicator = false
|
||||
} else {
|
||||
binding.toggleStoryPost.check(binding.buttonPost.id)
|
||||
binding.carousel.showLayoutSwitchButton = true
|
||||
binding.carousel.showIndicator = true
|
||||
}
|
||||
binding.carousel.maxEntries = uiState.maxEntries
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.carousel.apply {
|
||||
layoutCarouselCallback = { model.becameCarousel(it)}
|
||||
maxEntries = instance.albumLimit
|
||||
maxEntries = if(model.uiState.value.storyCreation) 1 else instance.albumLimit
|
||||
addPhotoButtonCallback = {
|
||||
addPhoto()
|
||||
}
|
||||
@ -133,9 +131,10 @@ class PostCreationFragment : BaseFragment() {
|
||||
model.updateDescription(position, description)
|
||||
}
|
||||
}
|
||||
// get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
if (validatePost() && model.isNotEmpty()) {
|
||||
|
||||
// Validate the post and go to the next step of the post creation process
|
||||
binding.postCreationNextButton.setOnClickListener {
|
||||
if (validatePost()) {
|
||||
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
|
||||
}
|
||||
}
|
||||
@ -163,6 +162,23 @@ class PostCreationFragment : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
binding.toggleStoryPost.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
// Only handle checked events
|
||||
if (!isChecked) return@addOnButtonCheckedListener
|
||||
|
||||
when (checkedId) {
|
||||
R.id.buttonStory -> {
|
||||
model.storyMode(true)
|
||||
}
|
||||
R.id.buttonPost -> {
|
||||
model.storyMode(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
binding.backbutton.setOnClickListener{requireActivity().onBackPressedDispatcher.onBackPressed()}
|
||||
|
||||
// Clean up temporary files, if any
|
||||
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
|
||||
tempFiles?.asList()?.forEach {
|
||||
@ -191,10 +207,9 @@ class PostCreationFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
val uris = result.data?.extras?.getParcelableArrayList<Uri>(Intent.EXTRA_STREAM)
|
||||
if (result.resultCode == Activity.RESULT_OK && uris != null) {
|
||||
model.setImages(model.addPossibleImages(uris, emptyList()))
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -275,14 +290,17 @@ class PostCreationFragment : BaseFragment() {
|
||||
|
||||
|
||||
private fun validatePost(): Boolean {
|
||||
if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) {
|
||||
MaterialAlertDialogBuilder(requireActivity()).apply {
|
||||
setMessage(R.string.still_encoding)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
return false
|
||||
if (model.getPhotoData().value?.none { it.video && it.videoEncodeComplete == false } == true) {
|
||||
// Encoding is done, i.e. none of the items are both a video and not done encoding.
|
||||
// We return true if the post is not empty, false otherwise.
|
||||
return model.getPhotoData().value?.isNotEmpty() == true
|
||||
}
|
||||
return true
|
||||
// Encoding is not done, show a dialog and return false to indicate validation failed
|
||||
MaterialAlertDialogBuilder(requireActivity()).apply {
|
||||
setMessage(R.string.still_encoding)
|
||||
setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
}.show()
|
||||
return false
|
||||
}
|
||||
|
||||
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
@ -12,16 +11,18 @@ import android.widget.Toast
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
||||
import com.jarsilio.android.scrambler.stripMetadata
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -32,36 +33,24 @@ import kotlinx.parcelize.Parcelize
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.MutableList
|
||||
import kotlin.collections.MutableMap
|
||||
import kotlin.collections.arrayListOf
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.get
|
||||
import kotlin.collections.getOrNull
|
||||
import kotlin.collections.indexOfFirst
|
||||
import kotlin.collections.isNotEmpty
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.set
|
||||
import kotlin.collections.toMutableList
|
||||
import kotlin.math.ceil
|
||||
|
||||
const val TAG = "Post Creation ViewModel"
|
||||
|
||||
// Models the UI state for the PostCreationActivity
|
||||
data class PostCreationActivityUiState(
|
||||
@ -70,6 +59,7 @@ data class PostCreationActivityUiState(
|
||||
val addPhotoButtonEnabled: Boolean = true,
|
||||
val editPhotoButtonEnabled: Boolean = true,
|
||||
val removePhotoButtonEnabled: Boolean = true,
|
||||
val maxEntries: Int?,
|
||||
|
||||
val isCarousel: Boolean = true,
|
||||
|
||||
@ -86,6 +76,11 @@ data class PostCreationActivityUiState(
|
||||
val uploadErrorVisible: Boolean = false,
|
||||
val uploadErrorExplanationText: String = "",
|
||||
val uploadErrorExplanationVisible: Boolean = false,
|
||||
|
||||
val storyCreation: Boolean,
|
||||
val storyDuration: Int = 10,
|
||||
val storyReplies: Boolean = true,
|
||||
val storyReactions: Boolean = true,
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@ -98,37 +93,62 @@ data class PhotoData(
|
||||
var video: Boolean,
|
||||
var videoEncodeProgress: Int? = null,
|
||||
var videoEncodeStabilizationFirstPass: Boolean? = null,
|
||||
var videoEncodeComplete: Boolean = true,
|
||||
var videoEncodeComplete: Boolean? = null,
|
||||
var videoEncodeError: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
class PostCreationViewModel(
|
||||
application: Application,
|
||||
clipdata: ClipData? = null,
|
||||
val instance: InstanceDatabaseEntity? = null,
|
||||
existingDescription: String? = null,
|
||||
existingNSFW: Boolean = false
|
||||
) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class PostCreationViewModel @Inject constructor(
|
||||
private val state: SavedStateHandle,
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
db: AppDatabase,
|
||||
): ViewModel() {
|
||||
private var storyPhotoDataBackup: MutableList<PhotoData>? = null
|
||||
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
||||
MutableLiveData<MutableList<PhotoData>>().also {
|
||||
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
|
||||
//FIXME We should be able to access the Intent action somehow, to determine if there are
|
||||
// 1 or multiple Uris instead of relying on the ClassCastException
|
||||
|
||||
// This should not work like this (reading its source code, get() function should return null
|
||||
// if it's the wrong type but instead throws ClassCastException).
|
||||
// Lucky for us that it does though: we first try to get a single Uri (which we could be
|
||||
// getting from a share of a single picture to the app), when the cast to Uri fails
|
||||
// we try to get a list of Uris instead (casting ourselves from Parcelable as suggested
|
||||
// in get() documentation)
|
||||
val uris = try {
|
||||
val singleUri: Uri? = state[Intent.EXTRA_STREAM]
|
||||
listOfNotNull(singleUri)
|
||||
} catch (e: ClassCastException) {
|
||||
state.get<ArrayList<Parcelable>>(Intent.EXTRA_STREAM)?.map { it as Uri }
|
||||
}
|
||||
|
||||
MutableLiveData<MutableList<PhotoData>>(
|
||||
addPossibleImages(
|
||||
uris,
|
||||
state.get<ArrayList<String>>(PostCreationActivity.PICTURE_DESCRIPTIONS),
|
||||
previousList = mutableListOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val instance = db.instanceDao().getActiveInstance()
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
val sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(application)
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
val templateDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||
|
||||
val storyCreation: Boolean = state[CameraFragment.CAMERA_ACTIVITY_STORY] ?: false
|
||||
|
||||
_uiState = MutableStateFlow(PostCreationActivityUiState(
|
||||
newPostDescriptionText = existingDescription ?: templateDescription,
|
||||
nsfw = existingNSFW
|
||||
newPostDescriptionText = state[PostCreationActivity.POST_DESCRIPTION] ?: templateDescription,
|
||||
nsfw = state[PostCreationActivity.POST_NSFW] ?: false,
|
||||
maxEntries = if(storyCreation) 1 else instance?.albumLimit,
|
||||
storyCreation = storyCreation
|
||||
))
|
||||
}
|
||||
|
||||
@ -145,35 +165,50 @@ class PostCreationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only public view on [photoData]
|
||||
*/
|
||||
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
||||
|
||||
/**
|
||||
* Will add as many images as possible to [photoData], from the [clipData], and if
|
||||
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
|
||||
* Will add as many images as possible to [photoData], from the [uris], and if
|
||||
* ([photoData].size + [uris].size) > uiState.value.maxEntries then it will only add as many images
|
||||
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
|
||||
*/
|
||||
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
|
||||
fun addPossibleImages(
|
||||
uris: List<Uri>?,
|
||||
descriptions: List<String>?,
|
||||
previousList: MutableList<PhotoData>? = photoData.value,
|
||||
): MutableList<PhotoData> {
|
||||
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
|
||||
var count = clipData.itemCount
|
||||
if(count + (previousList?.size ?: 0) > instance!!.albumLimit){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
|
||||
var count = uris?.size ?: 0
|
||||
uiState.value.maxEntries?.let { maxEntries ->
|
||||
if(count + (previousList?.size ?: 0) > maxEntries){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = applicationContext.getString(R.string.total_exceeds_album_limit).format(maxEntries))
|
||||
}
|
||||
count = count.coerceAtMost(maxEntries - (previousList?.size ?: 0))
|
||||
}
|
||||
count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0))
|
||||
}
|
||||
if (count + (previousList?.size ?: 0) >= instance.albumLimit) {
|
||||
// Disable buttons to add more images
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(addPhotoButtonEnabled = false)
|
||||
if (count + (previousList?.size ?: 0) >= maxEntries) {
|
||||
// Disable buttons to add more images
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(addPhotoButtonEnabled = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i in 0 until count) {
|
||||
clipData.getItemAt(i).let {
|
||||
for ((i, uri) in uris.orEmpty().withIndex()) {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> =
|
||||
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
|
||||
getSizeAndVideoValidate(uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(
|
||||
PhotoData(
|
||||
imageUri = uri,
|
||||
size = sizeAndVideoPair.first,
|
||||
video = sizeAndVideoPair.second,
|
||||
imageDescription = descriptions?.getOrNull(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
|
||||
}
|
||||
|
||||
@ -185,46 +220,47 @@ class PostCreationViewModel(
|
||||
* Returns the size of the file of the Uri, and whether it is a video,
|
||||
* and opens a dialog in case it is too big or in case the file is unsupported.
|
||||
*/
|
||||
fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
|
||||
private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
|
||||
val size: Long =
|
||||
if (uri.scheme =="content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
|
||||
applicationContext.contentResolver.query(uri, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
/* Get the column indexes of the data in the Cursor,
|
||||
* move to the first row in the Cursor, get the data,
|
||||
* and display it.
|
||||
*/
|
||||
* move to the first row in the Cursor, get the data,
|
||||
* and display it.
|
||||
*/
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
cursor.moveToFirst()
|
||||
cursor.getLong(sizeIndex)
|
||||
if(sizeIndex >= 0) {
|
||||
cursor.moveToFirst()
|
||||
cursor.getLong(sizeIndex)
|
||||
} else null
|
||||
} ?: 0
|
||||
} else {
|
||||
uri.toFile().length()
|
||||
}
|
||||
|
||||
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
|
||||
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val type = uri.getMimeType(applicationContext.contentResolver)
|
||||
val isVideo = type.startsWith("video/")
|
||||
|
||||
if (isVideo && !instance!!.videoEnabled) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
||||
currentUiState.copy(userMessage = applicationContext.getString(R.string.video_not_supported))
|
||||
}
|
||||
}
|
||||
|
||||
if ((!isVideo && sizeInkBytes > instance!!.maxPhotoSize) || (isVideo && sizeInkBytes > instance!!.maxVideoSize)) {
|
||||
//TODO Offer remedy for too big file: re-compress it
|
||||
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
userMessage = applicationContext.getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
return Pair(size, isVideo)
|
||||
}
|
||||
|
||||
fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
|
||||
|
||||
fun updateDescription(position: Int, description: String) {
|
||||
photoData.value?.getOrNull(position)?.imageDescription = description
|
||||
photoData.value = photoData.value
|
||||
@ -234,8 +270,8 @@ class PostCreationViewModel(
|
||||
photoData.value?.removeAt(currentPosition)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
addPhotoButtonEnabled = true
|
||||
)
|
||||
addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (uiState.value.maxEntries ?: 0),
|
||||
)
|
||||
}
|
||||
photoData.value = photoData.value
|
||||
}
|
||||
@ -254,8 +290,8 @@ class PostCreationViewModel(
|
||||
videoEncodeProgress = 0
|
||||
videoEncodeComplete = false
|
||||
|
||||
VideoEditActivity.startEncoding(imageUri, it,
|
||||
context = getApplication<PixelDroidApplication>(),
|
||||
VideoEditActivity.startEncoding(imageUri, null, it,
|
||||
context = applicationContext,
|
||||
registerNewFFmpegSession = ::registerNewFFmpegSession,
|
||||
trackTempFile = ::trackTempFile,
|
||||
videoEncodeProgress = ::videoEncodeProgress
|
||||
@ -370,17 +406,17 @@ class PostCreationViewModel(
|
||||
}
|
||||
|
||||
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val extension = data.imageUri.fileExtension(applicationContext.contentResolver)
|
||||
|
||||
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
|
||||
val strippedImage = File.createTempFile("temp_img", ".$extension", applicationContext.cacheDir)
|
||||
|
||||
val imageUri = data.imageUri
|
||||
|
||||
val (strippedOrNot, size) = try {
|
||||
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||
val orientation = ExifInterface(applicationContext.contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
|
||||
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
|
||||
stripMetadata(imageUri, strippedImage, applicationContext.contentResolver)
|
||||
|
||||
// Restore EXIF orientation
|
||||
val exifInterface = ExifInterface(strippedImage)
|
||||
@ -392,11 +428,11 @@ class PostCreationViewModel(
|
||||
strippedImage.delete()
|
||||
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||
val imageInputStream = try {
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
|
||||
applicationContext.contentResolver.openInputStream(imageUri)!!
|
||||
} catch (e: FileNotFoundException){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
userMessage = applicationContext.getString(R.string.file_not_found,
|
||||
data.imageUri)
|
||||
)
|
||||
}
|
||||
@ -408,14 +444,14 @@ class PostCreationViewModel(
|
||||
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
userMessage = applicationContext.getString(R.string.file_not_found,
|
||||
data.imageUri)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val type = data.imageUri.getMimeType(applicationContext.contentResolver)
|
||||
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
@ -442,7 +478,10 @@ class PostCreationViewModel(
|
||||
apiHolder.setToCurrentUser(it)
|
||||
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val inter = api.mediaUpload(description, requestBody.parts[0])
|
||||
val inter: Observable<Attachment> =
|
||||
//TODO validate that image is correct (?) aspect ratio
|
||||
if (uiState.value.storyCreation) api.storyUpload(requestBody.parts[0])
|
||||
else api.mediaUpload(description, requestBody.parts[0])
|
||||
|
||||
apiHolder.api = null
|
||||
postSub = inter
|
||||
@ -451,14 +490,18 @@ class PostCreationViewModel(
|
||||
.subscribe(
|
||||
{ attachment: Attachment ->
|
||||
data.progress = 0
|
||||
data.uploadId = attachment.id!!
|
||||
data.uploadId = if(uiState.value.storyCreation){
|
||||
attachment.media_id!!
|
||||
} else {
|
||||
attachment.id!!
|
||||
}
|
||||
},
|
||||
{ e: Throwable ->
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadErrorVisible = true,
|
||||
uploadErrorExplanationText = if(e is HttpException){
|
||||
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
|
||||
applicationContext.getString(R.string.upload_error, e.code())
|
||||
} else "",
|
||||
uploadErrorExplanationVisible = e is HttpException,
|
||||
)
|
||||
@ -507,19 +550,31 @@ class PostCreationViewModel(
|
||||
apiHolder.setToCurrentUser(it)
|
||||
} ?: apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
api.postStatus(
|
||||
statusText = description,
|
||||
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
|
||||
sensitive = nsfw
|
||||
)
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
|
||||
if(uiState.value.storyCreation){
|
||||
val canReact = if (uiState.value.storyReactions) "1" else "0"
|
||||
val canReply = if (uiState.value.storyReplies) "1" else "0"
|
||||
|
||||
api.storyPublish(
|
||||
media_id = getPhotoData().value!!.firstNotNullOf { it.uploadId },
|
||||
can_react = canReact,
|
||||
can_reply = canReply,
|
||||
duration = uiState.value.storyDuration
|
||||
)
|
||||
} else {
|
||||
api.postStatus(
|
||||
statusText = description,
|
||||
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
|
||||
sensitive = nsfw
|
||||
)
|
||||
}
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_success),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val intent = Intent(getApplication(), MainActivity::class.java)
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
//TODO make the activity launch this instead (and surrounding toasts too)
|
||||
getApplication<PixelDroidApplication>().startActivity(intent)
|
||||
applicationContext.startActivity(intent)
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
@ -528,7 +583,7 @@ class PostCreationViewModel(
|
||||
)
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_failed),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.response().toString() + exception.message().toString())
|
||||
_uiState.update { currentUiState ->
|
||||
@ -551,10 +606,46 @@ class PostCreationViewModel(
|
||||
fun chooseAccount(which: UserDatabaseEntity) {
|
||||
_uiState.update { it.copy(chosenAccount = which) }
|
||||
}
|
||||
}
|
||||
|
||||
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW)
|
||||
fun storyMode(storyMode: Boolean) {
|
||||
//TODO check ratio of files in story mode? What is acceptable?
|
||||
|
||||
val newMaxEntries = if (storyMode) 1 else instance?.albumLimit
|
||||
var newUiState = _uiState.value.copy(
|
||||
storyCreation = storyMode,
|
||||
maxEntries = newMaxEntries,
|
||||
addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (newMaxEntries ?: 0),
|
||||
)
|
||||
|
||||
// Carousel on if in story mode
|
||||
if (storyMode) newUiState = newUiState.copy(isCarousel = true)
|
||||
|
||||
// If switching to story, and there are too many pictures, keep the first and backup the rest
|
||||
if (storyMode && (photoData.value?.size ?: 0) > 1){
|
||||
storyPhotoDataBackup = photoData.value
|
||||
|
||||
photoData.value = photoData.value?.let { mutableListOf(it.firstOrNull()).filterNotNull().toMutableList() }
|
||||
|
||||
//Show message saying extraneous pictures were removed but can be restored
|
||||
newUiState = newUiState.copy(
|
||||
userMessage = applicationContext.getString(R.string.extraneous_pictures_stories)
|
||||
)
|
||||
}
|
||||
// Restore if backup not null and first value is unchanged
|
||||
else if (storyPhotoDataBackup != null && storyPhotoDataBackup?.firstOrNull() == photoData.value?.firstOrNull()){
|
||||
photoData.value = storyPhotoDataBackup
|
||||
storyPhotoDataBackup = null
|
||||
}
|
||||
_uiState.update { newUiState }
|
||||
}
|
||||
}
|
||||
|
||||
fun storyDuration(value: Int) {
|
||||
_uiState.update {
|
||||
it.copy(storyDuration = value)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } }
|
||||
|
||||
fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } }
|
||||
}
|
@ -20,10 +20,13 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.setSquareImageFromURL
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
class PostSubmissionFragment : BaseFragment() {
|
||||
@ -34,8 +37,8 @@ class PostSubmissionFragment : BaseFragment() {
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private lateinit var binding: FragmentPostSubmissionBinding
|
||||
private lateinit var model: PostCreationViewModel
|
||||
private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware()
|
||||
private val model: PostCreationViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
@ -57,26 +60,25 @@ class PostSubmissionFragment : BaseFragment() {
|
||||
accounts = db.userDao().getAll()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
db.instanceDao().getInstance(instance_uri)
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
// Display the values from the view model
|
||||
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
|
||||
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
|
||||
|
||||
if(model.uiState.value.storyCreation){
|
||||
binding.nsfwSwitch.visibility = View.GONE
|
||||
binding.postTextInputLayout.visibility = View.GONE
|
||||
binding.privateTitle.visibility = View.GONE
|
||||
binding.postPreview.visibility = View.GONE
|
||||
|
||||
binding.storyOptions.visibility = View.VISIBLE
|
||||
binding.storyDurationSlider.value = model.uiState.value.storyDuration.toFloat()
|
||||
binding.storyRepliesSwitch.isChecked = model.uiState.value.storyReplies
|
||||
binding.storyReactionsSwitch.isChecked = model.uiState.value.storyReactions
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
@ -114,13 +116,24 @@ class PostSubmissionFragment : BaseFragment() {
|
||||
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateNSFW(isChecked)
|
||||
}
|
||||
binding.storyRepliesSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateStoryReplies(isChecked)
|
||||
}
|
||||
binding.storyReactionsSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.updateStoryReactions(isChecked)
|
||||
}
|
||||
|
||||
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
|
||||
|
||||
binding.storyDurationSlider.addOnChangeListener { _, value, _ ->
|
||||
// Responds to when slider's value is changed
|
||||
model.storyDuration(value.roundToInt())
|
||||
}
|
||||
|
||||
setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview)
|
||||
|
||||
// Get the description and send the post
|
||||
binding.postCreationSendButton.setOnClickListener {
|
||||
binding.postSubmissionSendButton.setOnClickListener {
|
||||
if (validatePost()) model.upload()
|
||||
}
|
||||
|
||||
@ -179,13 +192,13 @@ class PostSubmissionFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private fun enableButton(enable: Boolean = true){
|
||||
binding.postCreationSendButton.isEnabled = enable
|
||||
binding.postSubmissionSendButton.isEnabled = enable
|
||||
if(enable){
|
||||
binding.postingProgressBar.visibility = View.GONE
|
||||
binding.postCreationSendButton.visibility = View.VISIBLE
|
||||
binding.postSubmissionSendButton.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.postingProgressBar.visibility = View.VISIBLE
|
||||
binding.postCreationSendButton.visibility = View.GONE
|
||||
binding.postSubmissionSendButton.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,47 +5,51 @@ import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
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_STORY
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
|
||||
|
||||
class CameraActivity : BaseThemedWithBarActivity() {
|
||||
class CameraActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityCameraBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_camera)
|
||||
|
||||
binding = ActivityCameraBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.add_photo)
|
||||
|
||||
val cameraFragment = CameraFragment()
|
||||
|
||||
val arguments = Bundle()
|
||||
arguments.putBoolean("CameraActivity", true)
|
||||
cameraFragment.arguments = arguments
|
||||
val story: Boolean = intent.getBooleanExtra(CAMERA_ACTIVITY_STORY, false)
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.camera_activity_fragment, cameraFragment).commit()
|
||||
}
|
||||
}
|
||||
if(story) supportActionBar?.setTitle(R.string.add_story)
|
||||
else supportActionBar?.setTitle(R.string.add_photo)
|
||||
|
||||
/**
|
||||
* Launch without arguments so that it will open the
|
||||
* [org.pixeldroid.app.postCreation.PostCreationActivity] instead of "returning" to a non-existent
|
||||
* [org.pixeldroid.app.postCreation.PostCreationActivity]
|
||||
*/
|
||||
class CameraActivityShortcut : BaseThemedWithBarActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_camera)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.new_post_shortcut_long)
|
||||
|
||||
val cameraFragment = CameraFragment()
|
||||
// If this CameraActivity wasn't started from the shortcut,
|
||||
// tell the fragment it's in an activity (so that it sends back the result instead of
|
||||
// starting a new post creation process)
|
||||
if (intent.action != Intent.ACTION_VIEW) {
|
||||
val arguments = Bundle()
|
||||
arguments.putBoolean(CAMERA_ACTIVITY, true)
|
||||
arguments.putBoolean(CAMERA_ACTIVITY_STORY, story)
|
||||
cameraFragment.arguments = arguments
|
||||
} else {
|
||||
supportActionBar?.setTitle(R.string.new_post_shortcut_long)
|
||||
}
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.camera_activity_fragment, cameraFragment).commit()
|
||||
}
|
||||
|
||||
//Start a new MainActivity when "going back" on this activity
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// If this CameraActivity wasn't started from the shortcut, behave as usual
|
||||
if (intent.action != Intent.ACTION_VIEW) return super.onOptionsItemSelected(item)
|
||||
|
||||
// Else, start a new MainActivity when "going back" on this activity
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
@ -2,7 +2,6 @@ package org.pixeldroid.app.postCreation.camera
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@ -34,10 +33,8 @@ import androidx.core.view.setPadding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentCameraBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
@ -70,6 +67,7 @@ class CameraFragment : BaseFragment() {
|
||||
private var camera: Camera? = null
|
||||
|
||||
private var inActivity by Delegates.notNull<Boolean>()
|
||||
private var addToStory by Delegates.notNull<Boolean>()
|
||||
|
||||
private var filePermissionDialogLaunched: Boolean = false
|
||||
|
||||
@ -89,7 +87,8 @@ class CameraFragment : BaseFragment() {
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
inActivity = arguments?.getBoolean("CameraActivity") ?: false
|
||||
inActivity = arguments?.getBoolean(CAMERA_ACTIVITY) ?: false
|
||||
addToStory = arguments?.getBoolean(CAMERA_ACTIVITY_STORY) ?: false
|
||||
|
||||
binding = FragmentCameraBinding.inflate(layoutInflater)
|
||||
|
||||
@ -106,7 +105,7 @@ class CameraFragment : BaseFragment() {
|
||||
thumbnail.setPadding(10)
|
||||
|
||||
// Load thumbnail into circular button using Glide
|
||||
Glide.with(thumbnail)
|
||||
if(activity?.isDestroyed == false) Glide.with(thumbnail)
|
||||
.load(uri)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(thumbnail)
|
||||
@ -326,7 +325,7 @@ class CameraFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private fun setupUploadImage() {
|
||||
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled
|
||||
val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled
|
||||
var mimeTypes: Array<String> = arrayOf("image/*")
|
||||
if(videoEnabled) mimeTypes += "video/*"
|
||||
|
||||
@ -337,7 +336,8 @@ class CameraFragment : BaseFragment() {
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
// Don't allow multiple for story
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !addToStory)
|
||||
uploadImageResultContract.launch(
|
||||
Intent.createChooser(this, null)
|
||||
)
|
||||
@ -448,31 +448,22 @@ class CameraFragment : BaseFragment() {
|
||||
|
||||
private fun startAlbumCreation(uris: ArrayList<String>) {
|
||||
|
||||
val intent = Intent(requireActivity(), PostCreationActivity::class.java)
|
||||
.apply {
|
||||
uris.forEach{
|
||||
//Why are we using ClipData here? Because the FLAG_GRANT_READ_URI_PERMISSION
|
||||
//needs to be applied to the URIs, and this flag only applies to the
|
||||
//Intent's data and any URIs specified in its ClipData.
|
||||
if(clipData == null){
|
||||
clipData = ClipData("", emptyArray(), ClipData.Item(it.toUri()))
|
||||
} else {
|
||||
clipData!!.addItem(ClipData.Item(it.toUri()))
|
||||
}
|
||||
}
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val intent = PostCreationActivity.intentForUris(requireContext(), uris.map { it.toUri() })
|
||||
|
||||
if(inActivity){
|
||||
if(inActivity && !addToStory){
|
||||
requireActivity().setResult(Activity.RESULT_OK, intent)
|
||||
requireActivity().finish()
|
||||
} else {
|
||||
if(addToStory){
|
||||
intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CAMERA_ACTIVITY = "CameraActivity"
|
||||
const val CAMERA_ACTIVITY_STORY = "CameraActivityStory"
|
||||
|
||||
private const val TAG = "CameraFragment"
|
||||
private const val RATIO_4_3_VALUE = 4.0 / 3.0
|
||||
|
@ -8,6 +8,6 @@ data class CarouselItem constructor(
|
||||
val video: Boolean,
|
||||
var encodeProgress: Int?,
|
||||
var stabilizationFirstPass: Boolean?,
|
||||
var encodeComplete: Boolean = false,
|
||||
var encodeComplete: Boolean? = null,
|
||||
var encodeError: Boolean = false,
|
||||
)
|
@ -18,6 +18,9 @@ import androidx.recyclerview.widget.*
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ImageCarouselBinding
|
||||
import me.relex.circleindicator.CircleIndicator2
|
||||
import org.pixeldroid.common.dpToPx
|
||||
import org.pixeldroid.common.getSnapPosition
|
||||
import org.pixeldroid.common.spToPx
|
||||
|
||||
class ImageCarousel(
|
||||
context: Context,
|
||||
@ -40,7 +43,6 @@ class ImageCarousel(
|
||||
)
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var tvCaption: TextView
|
||||
private var snapHelper: SnapHelper = PagerSnapHelper()
|
||||
|
||||
var indicator: CircleIndicator2? = null
|
||||
@ -107,7 +109,7 @@ class ImageCarousel(
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
|
||||
binding.tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
@Dimension(unit = Dimension.PX)
|
||||
@ -115,7 +117,7 @@ class ImageCarousel(
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
|
||||
binding.tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
|
||||
}
|
||||
|
||||
var showIndicator = false
|
||||
@ -245,14 +247,14 @@ class ImageCarousel(
|
||||
showNavigationButtons = showNavigationButtons
|
||||
|
||||
binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE
|
||||
tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE
|
||||
binding.tvCaption.visibility = if(editingMediaDescription || !showCaption) INVISIBLE else VISIBLE
|
||||
} else {
|
||||
recyclerView.layoutManager = GridLayoutManager(context, 3)
|
||||
binding.btnNext.visibility = GONE
|
||||
binding.btnPrevious.visibility = GONE
|
||||
|
||||
binding.editMediaDescriptionLayout.visibility = INVISIBLE
|
||||
tvCaption.visibility = INVISIBLE
|
||||
binding.tvCaption.visibility = INVISIBLE
|
||||
}
|
||||
showIndicator = value
|
||||
|
||||
@ -279,8 +281,7 @@ class ImageCarousel(
|
||||
updateDescriptionCallback?.invoke(currentPosition, description)
|
||||
}
|
||||
binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE
|
||||
tvCaption.visibility = if(value) INVISIBLE else VISIBLE
|
||||
|
||||
binding.tvCaption.visibility = if(value || !showCaption) INVISIBLE else VISIBLE
|
||||
}
|
||||
|
||||
}
|
||||
@ -289,10 +290,10 @@ class ImageCarousel(
|
||||
set(value) {
|
||||
if(!value.isNullOrEmpty()) {
|
||||
field = value
|
||||
tvCaption.text = value
|
||||
binding.tvCaption.text = value
|
||||
} else {
|
||||
field = null
|
||||
tvCaption.text = context.getText(R.string.no_media_description)
|
||||
binding.tvCaption.text = context.getText(R.string.no_media_description)
|
||||
}
|
||||
|
||||
}
|
||||
@ -317,12 +318,11 @@ class ImageCarousel(
|
||||
binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true)
|
||||
|
||||
recyclerView = binding.recyclerView
|
||||
tvCaption = binding.tvCaption
|
||||
|
||||
recyclerView.setHasFixedSize(true)
|
||||
|
||||
// For marquee effect
|
||||
tvCaption.isSelected = true
|
||||
binding.tvCaption.isSelected = true
|
||||
}
|
||||
|
||||
|
||||
@ -441,7 +441,7 @@ class ImageCarousel(
|
||||
caption.apply {
|
||||
if(layoutCarousel){
|
||||
binding.editMediaDescriptionLayout.visibility = INVISIBLE
|
||||
tvCaption.visibility = VISIBLE
|
||||
showCaption = true
|
||||
}
|
||||
currentDescription = this
|
||||
}
|
||||
@ -472,7 +472,7 @@ class ImageCarousel(
|
||||
}
|
||||
})
|
||||
|
||||
tvCaption.setOnClickListener {
|
||||
binding.tvCaption.setOnClickListener {
|
||||
editingMediaDescription = true
|
||||
}
|
||||
|
||||
@ -562,7 +562,7 @@ class ImageCarousel(
|
||||
binding.encodeInfoText.setText(R.string.encode_error)
|
||||
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
|
||||
null, null, null)
|
||||
} else if(it.encodeComplete){
|
||||
} else if(it.encodeComplete == true){
|
||||
binding.encodeInfoCard.visibility = VISIBLE
|
||||
binding.encodeProgress.visibility = GONE
|
||||
binding.encodeInfoText.setText(R.string.encode_success)
|
||||
|
@ -1,52 +0,0 @@
|
||||
package org.pixeldroid.app.postCreation.carousel
|
||||
|
||||
import android.content.Context
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SnapHelper
|
||||
|
||||
|
||||
/**
|
||||
* This method converts device specific pixels to density independent pixels.
|
||||
*/
|
||||
fun Int.pxToDp(context: Context): Int {
|
||||
return (this / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts dp unit to equivalent pixels, depending on device density.
|
||||
*/
|
||||
fun Int.dpToPx(context: Context): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
this.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts sp unit to equivalent pixels, depending on device density.
|
||||
*/
|
||||
fun Int.spToPx(context: Context): Int {
|
||||
return TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP,
|
||||
this.toFloat(),
|
||||
context.resources.displayMetrics
|
||||
).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current snap item position of a recyclerView.
|
||||
*
|
||||
* @param layoutManager Target recyclerView
|
||||
* @return Position of the item or RecyclerView.NO_POSITION (-1)
|
||||
*/
|
||||
fun SnapHelper.getSnapPosition(layoutManager: RecyclerView.LayoutManager?): Int {
|
||||
if (layoutManager == null) {
|
||||
return RecyclerView.NO_POSITION
|
||||
}
|
||||
val snapView: View = this.findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
|
||||
return layoutManager.getPosition(snapView)
|
||||
}
|
@ -1,32 +1,36 @@
|
||||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.ActivityAlbumBinding
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
|
||||
class AlbumActivity : BaseActivity() {
|
||||
|
||||
private lateinit var model: AlbumViewModel
|
||||
class AlbumActivity : AppCompatActivity() {
|
||||
private val model: AlbumViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val binding = ActivityAlbumBinding.inflate(layoutInflater)
|
||||
|
||||
val _model: AlbumViewModel by viewModels { AlbumViewModelFactory(application, intent) }
|
||||
model = _model
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.albumPager.adapter = AlbumViewPagerAdapter(model.uiState.value.mediaAttachments,
|
||||
sensitive = false,
|
||||
opened = true,
|
||||
//In the activity, we assume we want to show everything
|
||||
alwaysShowNsfw = true
|
||||
alwaysShowNsfw = true,
|
||||
clickCallback = ::clickCallback
|
||||
)
|
||||
binding.albumPager.currentItem = model.uiState.value.index
|
||||
|
||||
@ -44,29 +48,48 @@ class AlbumActivity : BaseActivity() {
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
supportActionBar?.setBackgroundDrawable(null)
|
||||
|
||||
// TODO: Remove from StatusViewHolder (877-893)
|
||||
// TODO: Issue is that albumPager does not listen to the clicks here
|
||||
binding.albumPager.setOnClickListener {
|
||||
val windowInsetsController =
|
||||
WindowCompat.getInsetsController(this.window, it)
|
||||
// Configure the behavior of the hidden system bars
|
||||
if (model.uiState.value.isActionBarHidden) {
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
model.uiState.value.isActionBarHidden = false
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
model.uiState.value.isActionBarHidden = true
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.isActionBarHidden.collect { isActionBarHidden ->
|
||||
val windowInsetsController =
|
||||
WindowCompat.getInsetsController(this@AlbumActivity.window, binding.albumPager)
|
||||
if (isActionBarHidden) {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Show both the status bar and the navigation bar
|
||||
supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback passed to the AlbumViewPagerAdapter to signal a single click on the image
|
||||
*/
|
||||
private fun clickCallback(){
|
||||
model.barHide()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
// Handle up arrow manually,
|
||||
// since "up" isn't defined for this activity
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,43 +1,42 @@
|
||||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import javax.inject.Inject
|
||||
|
||||
data class AlbumUiState(
|
||||
val mediaAttachments: ArrayList<Attachment> = arrayListOf(),
|
||||
val index: Int = 0,
|
||||
var isActionBarHidden: Boolean = false
|
||||
)
|
||||
|
||||
class AlbumViewModel(application: Application, intent: Intent) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class AlbumViewModel @Inject constructor(state: SavedStateHandle) : ViewModel() {
|
||||
fun barHide() {
|
||||
_isActionBarHidden.update { !it }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ALBUM_IMAGES = "AlbumViewImages"
|
||||
const val ALBUM_INDEX = "AlbumViewIndex"
|
||||
}
|
||||
|
||||
private val _uiState: MutableStateFlow<AlbumUiState>
|
||||
private val _isActionBarHidden: MutableStateFlow<Boolean>
|
||||
|
||||
init {
|
||||
_uiState = MutableStateFlow(AlbumUiState(
|
||||
mediaAttachments = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
intent.getSerializableExtra("images", arrayListOf<Attachment>()::class.java)!!
|
||||
} else {
|
||||
intent.getSerializableExtra("images") as ArrayList<Attachment>
|
||||
},
|
||||
index = intent.getIntExtra("index", 0)
|
||||
mediaAttachments = state[ALBUM_IMAGES] ?: ArrayList(),
|
||||
index = state[ALBUM_INDEX] ?: 0
|
||||
))
|
||||
_isActionBarHidden = MutableStateFlow(false)
|
||||
}
|
||||
|
||||
val uiState: StateFlow<AlbumUiState> = _uiState.asStateFlow()
|
||||
|
||||
}
|
||||
|
||||
class AlbumViewModelFactory(val application: Application, val intent: Intent) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java, Intent::class.java).newInstance(application, intent)
|
||||
}
|
||||
val isActionBarHidden: StateFlow<Boolean> = _isActionBarHidden
|
||||
}
|
@ -11,6 +11,7 @@ import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.text.toSpanned
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId
|
||||
@ -106,7 +107,7 @@ fun parseHTMLText(
|
||||
override fun onClick(widget: View) {
|
||||
|
||||
// Retrieve the account for the given profile
|
||||
lifecycleScope.launchWhenCreated {
|
||||
lifecycleScope.launch {
|
||||
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
openAccountFromId(accountId, api, context)
|
||||
}
|
||||
@ -130,7 +131,7 @@ fun parseHTMLText(
|
||||
}
|
||||
|
||||
|
||||
fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean, context: Context) {
|
||||
fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean) {
|
||||
val now = Date.from(Instant.now()).time
|
||||
|
||||
try {
|
||||
@ -140,7 +141,7 @@ fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Bool
|
||||
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
||||
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString()
|
||||
|
||||
textView.text = if(absoluteTime) context.getString(R.string.posted_on).format(date)
|
||||
textView.text = if(absoluteTime) textView.context.getString(R.string.posted_on).format(date)
|
||||
else formattedDate
|
||||
|
||||
} catch (e: ParseException) {
|
||||
|
@ -14,9 +14,9 @@ import androidx.media2.common.MediaMetadata
|
||||
import androidx.media2.common.UriMediaItem
|
||||
import androidx.media2.player.MediaPlayer
|
||||
import org.pixeldroid.app.databinding.ActivityMediaviewerBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
|
||||
class MediaViewerActivity : BaseThemedWithoutBarActivity() {
|
||||
class MediaViewerActivity : BaseActivity() {
|
||||
|
||||
private lateinit var mediaPlayer: MediaPlayer
|
||||
private lateinit var binding: ActivityMediaviewerBinding
|
||||
|
@ -88,19 +88,20 @@ class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
|
||||
}
|
||||
val intent = Intent(context, AlbumActivity::class.java)
|
||||
|
||||
intent.putExtra("images", images)
|
||||
intent.putExtra("index", (child as ViewPager2).currentItem)
|
||||
intent.putExtra(AlbumViewModel.ALBUM_IMAGES, images)
|
||||
intent.putExtra(AlbumViewModel.ALBUM_INDEX, (child as ViewPager2).currentItem)
|
||||
|
||||
context.startActivity(intent)
|
||||
|
||||
return super.onSingleTapConfirmed(e)
|
||||
}
|
||||
override fun onScroll(
|
||||
e1: MotionEvent,
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
if (e1 == null) return false
|
||||
val orientation = parentViewPager?.orientation ?: return true
|
||||
|
||||
val dx = e2.x - e1.x
|
||||
|
@ -5,13 +5,16 @@ import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_DOMAIN
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_STATUS_ID
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_COMMENT_TAG
|
||||
@ -19,18 +22,21 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_TAG
|
||||
import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
|
||||
import org.pixeldroid.app.utils.displayDimensionsInPx
|
||||
|
||||
class PostActivity : BaseThemedWithBarActivity() {
|
||||
class PostActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityPostBinding
|
||||
|
||||
private var commentFragment = CommentFragment()
|
||||
private lateinit var commentFragment: CommentFragment
|
||||
|
||||
private lateinit var status: Status
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
commentFragment = CommentFragment(binding.swipeRefreshLayout)
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
status = intent.getSerializableExtra(POST_TAG) as Status
|
||||
@ -43,7 +49,10 @@ class PostActivity : BaseThemedWithBarActivity() {
|
||||
|
||||
val holder = StatusViewHolder(binding.postFragmentSingle)
|
||||
|
||||
holder.bind(status, apiHolder, db, lifecycleScope, displayDimensionsInPx(), isActivity = true)
|
||||
holder.bind(
|
||||
status, apiHolder, db, lifecycleScope, displayDimensionsInPx(),
|
||||
requestPermissionDownloadPic, isActivity = true
|
||||
)
|
||||
|
||||
activateCommenter()
|
||||
initCommentsFragment(domain = user?.instance_uri.orEmpty())
|
||||
@ -60,6 +69,17 @@ class PostActivity : BaseThemedWithBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val requestPermissionDownloadPic =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
if (!isGranted) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.write_permission_download_pic)
|
||||
.setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
private fun activateCommenter() {
|
||||
//Activate commenter
|
||||
binding.submitComment.setOnClickListener {
|
||||
@ -89,6 +109,11 @@ class PostActivity : BaseThemedWithBarActivity() {
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.commentFragment, commentFragment).commit()
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
commentFragment.adapter.refresh()
|
||||
commentFragment.adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun postComment(
|
||||
|
@ -5,10 +5,10 @@ import android.view.View
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityReportBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
|
||||
class ReportActivity : BaseThemedWithBarActivity() {
|
||||
class ReportActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityReportBinding
|
||||
|
||||
@ -16,9 +16,9 @@ class ReportActivity : BaseThemedWithBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityReportBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.report)
|
||||
|
||||
val status = intent.getSerializableExtra(Status.POST_TAG) as Status?
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_DENIED
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
@ -17,11 +16,8 @@ import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@ -36,10 +32,6 @@ import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse
|
||||
import com.karumi.dexter.listener.PermissionGrantedResponse
|
||||
import com.karumi.dexter.listener.single.BasePermissionListener
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.*
|
||||
import okio.BufferedSink
|
||||
@ -75,7 +67,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
|
||||
private var status: Status? = null
|
||||
|
||||
fun bind(status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>, isActivity: Boolean = false) {
|
||||
fun bind(
|
||||
status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>, isActivity: Boolean = false,
|
||||
) {
|
||||
|
||||
this.itemView.visibility = View.VISIBLE
|
||||
this.status = status
|
||||
@ -104,7 +100,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
|
||||
setupPost(picRequest, user.instance_uri, isActivity)
|
||||
|
||||
activateButtons(pixelfedAPI, db, lifecycleScope, isActivity)
|
||||
activateButtons(pixelfedAPI, db, lifecycleScope, isActivity, requestPermissionDownloadPic)
|
||||
|
||||
}
|
||||
|
||||
@ -139,8 +135,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
setTextViewFromISO8601(
|
||||
status?.created_at!!,
|
||||
binding.postDate,
|
||||
isActivity,
|
||||
binding.root.context
|
||||
isActivity
|
||||
)
|
||||
|
||||
binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context)
|
||||
@ -233,6 +228,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
isActivity: Boolean,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>,
|
||||
){
|
||||
//Set the special HTML text
|
||||
setDescription(apiHolder, lifecycleScope)
|
||||
@ -262,7 +258,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
|
||||
showComments(lifecycleScope, isActivity)
|
||||
|
||||
activateMoreButton(apiHolder, db, lifecycleScope)
|
||||
activateMoreButton(apiHolder, db, lifecycleScope, requestPermissionDownloadPic)
|
||||
}
|
||||
|
||||
private fun activateReblogger(
|
||||
@ -364,7 +360,12 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
return null
|
||||
}
|
||||
|
||||
private fun activateMoreButton(apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){
|
||||
private fun activateMoreButton(
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>,
|
||||
){
|
||||
var bookmarked: Boolean? = null
|
||||
binding.statusMore.setOnClickListener {
|
||||
PopupMenu(it.context, it).apply {
|
||||
@ -402,50 +403,29 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_save_to_gallery -> {
|
||||
Dexter.withContext(binding.root.context)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(object : BasePermissionListener() {
|
||||
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.write_permission_download_pic),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
|
||||
status?.downloadImage(
|
||||
binding.root.context,
|
||||
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
|
||||
?: "",
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
}).check()
|
||||
// Check permissions on old Android versions: on new versions it is not
|
||||
// needed when storing a file.
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(binding.root.context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) {
|
||||
requestPermissionDownloadPic.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
status?.downloadImage(
|
||||
binding.root.context,
|
||||
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
|
||||
?: "",
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_share_picture -> {
|
||||
Dexter.withContext(binding.root.context)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(object : BasePermissionListener() {
|
||||
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.write_permission_share_pic),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
|
||||
status?.downloadImage(
|
||||
binding.root.context,
|
||||
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
|
||||
?: "",
|
||||
binding.root,
|
||||
share = true,
|
||||
)
|
||||
}
|
||||
}).check()
|
||||
R.id.post_more_menu_share_picture -> {
|
||||
status?.downloadImage(
|
||||
binding.root.context,
|
||||
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
|
||||
?: "",
|
||||
binding.root,
|
||||
share = true,
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_delete -> {
|
||||
@ -462,178 +442,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_redraft -> {
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(R.string.redraft_dialog_launch)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Create new post creation activity
|
||||
val intent =
|
||||
Intent(context, PostCreationActivity::class.java)
|
||||
|
||||
// Get descriptions and images from original post
|
||||
val postDescription = status?.content ?: ""
|
||||
val postAttachments =
|
||||
status?.media_attachments!! // Catch possible exception from !! (?)
|
||||
val postNSFW = status?.sensitive
|
||||
|
||||
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||
postAttachment.url ?: ""
|
||||
}
|
||||
val imageNames = imageUriStrings.map { imageUriString ->
|
||||
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||
}
|
||||
val downloadedFiles = imageNames.map { imageName ->
|
||||
File(context.cacheDir, imageName)
|
||||
}
|
||||
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||
Uri.fromFile(downloadedFile)
|
||||
}
|
||||
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||
fromHtml(postAttachment.description ?: "").toString()
|
||||
}
|
||||
val downloadRequests: List<Request> = imageUriStrings.map { imageUriString ->
|
||||
Request.Builder().url(imageUriString).build()
|
||||
}
|
||||
|
||||
val counter = AtomicInteger(0)
|
||||
|
||||
// Define callback function for after downloading the images
|
||||
fun continuation() {
|
||||
// Wait for all outstanding downloads to finish
|
||||
if (counter.incrementAndGet() == imageUris.size) {
|
||||
if (allFilesExist(imageNames)) {
|
||||
// Delete original post
|
||||
lifecycleScope.launch {
|
||||
deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
|
||||
}
|
||||
|
||||
val counterInt = counter.get()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.resources.getQuantityString(
|
||||
R.plurals.items_load_success,
|
||||
counterInt,
|
||||
counterInt
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
// Pass downloaded images to new post creation activity
|
||||
intent.apply {
|
||||
imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) ->
|
||||
ClipData.Item(imageDescription, null, imageUri)
|
||||
}.forEach { imageItem ->
|
||||
if (clipData == null) {
|
||||
clipData = ClipData(
|
||||
"",
|
||||
emptyArray(),
|
||||
imageItem
|
||||
)
|
||||
} else {
|
||||
clipData!!.addItem(imageItem)
|
||||
}
|
||||
}
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
// Pass post description of existing post to new post creation activity
|
||||
intent.putExtra(
|
||||
PostCreationActivity.PICTURE_DESCRIPTION,
|
||||
fromHtml(postDescription).toString()
|
||||
)
|
||||
if (imageNames.isNotEmpty()) {
|
||||
intent.putExtra(
|
||||
PostCreationActivity.TEMP_FILES,
|
||||
imageNames.toTypedArray()
|
||||
)
|
||||
}
|
||||
intent.putExtra(
|
||||
PostCreationActivity.POST_REDRAFT,
|
||||
true
|
||||
)
|
||||
intent.putExtra(
|
||||
PostCreationActivity.POST_NSFW,
|
||||
postNSFW
|
||||
)
|
||||
|
||||
// Launch post creation activity
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFilesExist(imageNames)) {
|
||||
// Track download progress
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.image_download_downloading),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Iterate through all pictures of the original post
|
||||
downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) ->
|
||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||
if (!downloadedFile.exists()) {
|
||||
OkHttpClient().newCall(downloadRequest)
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException
|
||||
) {
|
||||
Looper.prepare()
|
||||
downloadedFile.delete()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response
|
||||
) {
|
||||
val sink: BufferedSink =
|
||||
downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
Looper.prepare()
|
||||
continuation()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
continuation()
|
||||
}
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(
|
||||
R.string.redraft_post_failed_error,
|
||||
exception.code()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_redraft -> launchRedraftDialog(lifecycleScope, apiHolder, db)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@ -658,6 +467,165 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRedraftDialog(
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
db: AppDatabase
|
||||
): Boolean {
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(R.string.redraft_dialog_launch)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Get descriptions and images from original post
|
||||
val postDescription = status?.content ?: ""
|
||||
val postAttachments =
|
||||
status?.media_attachments!! // TODO Catch possible exception from !! (?)
|
||||
val postNSFW = status?.sensitive
|
||||
|
||||
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||
postAttachment.url ?: ""
|
||||
}
|
||||
val imageNames = imageUriStrings.map { imageUriString ->
|
||||
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||
}
|
||||
val downloadedFiles = imageNames.map { imageName ->
|
||||
File(context.cacheDir, imageName)
|
||||
}
|
||||
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||
fromHtml(
|
||||
postAttachment.description ?: ""
|
||||
).toString()
|
||||
}
|
||||
val downloadRequests: List<Request> =
|
||||
imageUriStrings.map { imageUriString ->
|
||||
Request.Builder().url(imageUriString).build()
|
||||
}
|
||||
|
||||
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||
Uri.fromFile(downloadedFile)
|
||||
}
|
||||
|
||||
val counter = AtomicInteger(0)
|
||||
|
||||
// Define callback function for after downloading the images
|
||||
fun continuation() {
|
||||
// Wait for all outstanding downloads to finish
|
||||
if (counter.incrementAndGet() == imageUris.size) {
|
||||
if (allFilesExist(imageNames)) {
|
||||
// Delete original post
|
||||
lifecycleScope.launch {
|
||||
deletePost(
|
||||
apiHolder.api ?: apiHolder.setToCurrentUser(), db
|
||||
)
|
||||
}
|
||||
|
||||
val counterInt = counter.get()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.resources.getQuantityString(
|
||||
R.plurals.items_load_success, counterInt, counterInt
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
// Create new post creation activity
|
||||
val intent = PostCreationActivity.intentForUris(context, imageUris).apply {
|
||||
putExtra(
|
||||
PostCreationActivity.PICTURE_DESCRIPTIONS,
|
||||
ArrayList(imageDescriptions)
|
||||
)
|
||||
// Pass post description of existing post to new post creation activity
|
||||
putExtra(
|
||||
PostCreationActivity.POST_DESCRIPTION,
|
||||
fromHtml(postDescription).toString()
|
||||
)
|
||||
if (imageNames.isNotEmpty()) {
|
||||
putExtra(
|
||||
PostCreationActivity.TEMP_FILES,
|
||||
imageNames.toTypedArray()
|
||||
)
|
||||
}
|
||||
putExtra(PostCreationActivity.POST_REDRAFT, true)
|
||||
putExtra(PostCreationActivity.POST_NSFW, postNSFW)
|
||||
}
|
||||
|
||||
// Launch post creation activity
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFilesExist(imageNames)) {
|
||||
// Track download progress
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.image_download_downloading),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Iterate through all pictures of the original post
|
||||
downloadRequests.zip(downloadedFiles)
|
||||
.forEach { (downloadRequest, downloadedFile) ->
|
||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||
if (!downloadedFile.exists()) {
|
||||
OkHttpClient().newCall(downloadRequest)
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
Looper.prepare()
|
||||
downloadedFile.delete()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(
|
||||
R.string.redraft_post_failed_io_except
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
val sink: BufferedSink =
|
||||
downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
Looper.prepare()
|
||||
continuation()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
continuation()
|
||||
}
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(
|
||||
binding.root.context, binding.root.context.getString(
|
||||
R.string.redraft_post_failed_error, exception.code()
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun activateLiker(
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
isLiked: Boolean,
|
||||
@ -833,17 +801,15 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
||||
class AlbumViewPagerAdapter(
|
||||
private val media_attachments: List<Attachment>, private var sensitive: Boolean?,
|
||||
private val opened: Boolean, private val alwaysShowNsfw: Boolean,
|
||||
) :
|
||||
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
private var isActionBarHidden: Boolean = false
|
||||
private val clickCallback: (() -> Unit)? = null
|
||||
) : RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return if(!opened) ViewHolderClosed(AlbumImageViewBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)) else ViewHolderOpen(OpenedAlbumBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
))
|
||||
), clickCallback!!)
|
||||
}
|
||||
|
||||
override fun getItemCount() = media_attachments.size
|
||||
@ -874,24 +840,6 @@ class AlbumViewPagerAdapter(
|
||||
setDoubleTapZoomDpi(240)
|
||||
resetScaleAndCenter()
|
||||
}
|
||||
holder.image.setOnClickListener {
|
||||
val windowInsetsController = WindowCompat.getInsetsController((it.context as Activity).window, it)
|
||||
// Configure the behavior of the hidden system bars
|
||||
if (isActionBarHidden) {
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
(it.context as AppCompatActivity).supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
isActionBarHidden = false
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
(it.context as AppCompatActivity).supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
isActionBarHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else Glide.with(holder.binding.root)
|
||||
.asDrawable().fitCenter()
|
||||
@ -937,9 +885,13 @@ class AlbumViewPagerAdapter(
|
||||
abstract val videoPlayButton: ImageView
|
||||
}
|
||||
|
||||
class ViewHolderOpen(override val binding: OpenedAlbumBinding) : ViewHolder(binding) {
|
||||
class ViewHolderOpen(override val binding: OpenedAlbumBinding, clickCallback: () -> Unit) : ViewHolder(binding) {
|
||||
override val image: SubsamplingScaleImageView = binding.imageImageView
|
||||
override val videoPlayButton: ImageView = binding.videoPlayButton
|
||||
|
||||
init {
|
||||
image.setOnClickListener { clickCallback() }
|
||||
}
|
||||
}
|
||||
class ViewHolderClosed(override val binding: AlbumImageViewBinding) : ViewHolder(binding) {
|
||||
override val image: ImageView = binding.imageImageView
|
||||
|
@ -6,13 +6,16 @@ import android.widget.ProgressBar
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.LoadStateAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@ -20,6 +23,7 @@ import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ErrorLayoutBinding
|
||||
import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.stories.StoriesAdapter
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import retrofit2.HttpException
|
||||
@ -48,14 +52,29 @@ private fun showError(
|
||||
internal fun <T: Any> initAdapter(
|
||||
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout,
|
||||
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding,
|
||||
adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>) {
|
||||
adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>,
|
||||
header: StoriesAdapter? = null
|
||||
) {
|
||||
|
||||
recyclerView.adapter = adapter.withLoadStateFooter(
|
||||
footer = ReposLoadStateAdapter { adapter.retry() }
|
||||
|
||||
val footer = ReposLoadStateAdapter { adapter.retry() }
|
||||
|
||||
adapter.addLoadStateListener { loadStates: CombinedLoadStates ->
|
||||
footer.loadState = loadStates.append
|
||||
}
|
||||
|
||||
recyclerView.adapter = ConcatAdapter(
|
||||
*listOfNotNull(
|
||||
header, // need to filter it if null
|
||||
adapter,
|
||||
footer
|
||||
).toTypedArray()
|
||||
)
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
adapter.refresh()
|
||||
adapter.notifyDataSetChanged()
|
||||
header?.refreshStories()
|
||||
}
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
@ -80,6 +99,11 @@ internal fun <T: Any> initAdapter(
|
||||
?: loadState.append as? LoadState.Error
|
||||
?: loadState.prepend as? LoadState.Error
|
||||
?: loadState.refresh as? LoadState.Error
|
||||
|
||||
if(errorState?.error is CancellationException){
|
||||
return@addLoadStateListener
|
||||
}
|
||||
|
||||
errorState?.let {
|
||||
val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s ->
|
||||
try {
|
||||
@ -143,6 +167,8 @@ class ReposLoadStateAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors
|
||||
* in the loading of appending values.
|
||||
|
@ -18,8 +18,10 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import org.pixeldroid.app.databinding.FragmentFeedBinding
|
||||
import org.pixeldroid.app.posts.feeds.initAdapter
|
||||
import org.pixeldroid.app.stories.StoriesAdapter
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
|
||||
@ -31,8 +33,9 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
||||
|
||||
internal lateinit var viewModel: FeedViewModel<T>
|
||||
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
|
||||
internal var headerAdapter: StoriesAdapter? = null
|
||||
|
||||
private lateinit var binding: FragmentFeedBinding
|
||||
private var binding: FragmentFeedBinding by bindingLifecycleAware()
|
||||
|
||||
|
||||
private var job: Job? = null
|
||||
@ -49,6 +52,7 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
//TODO rename function to something that makes sense
|
||||
internal fun initSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launchWhenStarted {
|
||||
@ -73,7 +77,9 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
initAdapter(binding.progressBar, binding.swipeRefreshLayout,
|
||||
binding.list, binding.motionLayout, binding.errorLayout, adapter)
|
||||
binding.list, binding.motionLayout, binding.errorLayout, adapter,
|
||||
headerAdapter
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
@ -16,18 +16,20 @@
|
||||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.RemoteMediator
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository class that works with local and remote data sources.
|
||||
*/
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi
|
||||
@Inject constructor(
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi constructor(
|
||||
private val db: AppDatabase,
|
||||
private val dao: FeedContentDao<T>,
|
||||
private val mediator: RemoteMediator<Int, T>
|
||||
|
@ -221,8 +221,7 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
|
||||
setTextViewFromISO8601(
|
||||
it,
|
||||
notificationTime,
|
||||
false,
|
||||
itemView.context
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
|
||||
|
||||
/**
|
||||
@ -17,7 +19,7 @@ import javax.inject.Inject
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class HomeFeedRemoteMediator @Inject constructor(
|
||||
class HomeFeedRemoteMediator(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
) : RemoteMediator<Int, HomeStatusDatabaseEntity>() {
|
||||
@ -47,7 +49,7 @@ class HomeFeedRemoteMediator @Inject constructor(
|
||||
HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it)
|
||||
}
|
||||
|
||||
val endOfPaginationReached = apiResponse.isEmpty()
|
||||
val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id
|
||||
|
||||
db.withTransaction {
|
||||
// Clear table in the database
|
||||
|
@ -11,14 +11,14 @@ import androidx.paging.PagingDataAdapter
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
import org.pixeldroid.app.posts.StatusViewHolder
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
|
||||
import org.pixeldroid.app.stories.StoriesAdapter
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
import org.pixeldroid.app.utils.displayDimensionsInPx
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@ -38,14 +38,18 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||
home = requireArguments().getBoolean("home")
|
||||
|
||||
home = requireArguments().get("home") as Boolean
|
||||
adapter = PostsAdapter(requireContext().displayDimensionsInPx())
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (home){
|
||||
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||
dao = db.homePostDao() as FeedContentDao<T>
|
||||
headerAdapter = StoriesAdapter(lifecycleScope, apiHolder)
|
||||
headerAdapter?.showStories = false
|
||||
|
||||
headerAdapter?.refreshStories()
|
||||
}
|
||||
else {
|
||||
mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||
@ -55,7 +59,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
@ -70,6 +74,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame (oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id
|
||||
@ -81,15 +86,19 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
return StatusViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.post_fragment
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position) as Status?
|
||||
uiModel?.let {
|
||||
(holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx)
|
||||
}
|
||||
holder.itemView.visibility = View.VISIBLE
|
||||
holder.itemView.layoutParams =
|
||||
RecyclerView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
val uiModel = getItem(position) as Status?
|
||||
uiModel?.let {
|
||||
(holder as StatusViewHolder).bind(
|
||||
it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,13 +16,15 @@
|
||||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for the public feed.
|
||||
@ -32,7 +34,7 @@ import javax.inject.Inject
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class PublicFeedRemoteMediator @Inject constructor(
|
||||
class PublicFeedRemoteMediator(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, PublicFeedStatusDatabaseEntity>() {
|
||||
@ -62,7 +64,7 @@ class PublicFeedRemoteMediator @Inject constructor(
|
||||
val dbObjects = apiResponse.map{
|
||||
PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it)
|
||||
}
|
||||
val endOfPaginationReached = apiResponse.isEmpty()
|
||||
val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id
|
||||
|
||||
db.withTransaction {
|
||||
// Clear table in the database
|
||||
|
@ -11,6 +11,7 @@ import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@ -20,6 +21,7 @@ import org.pixeldroid.app.posts.feeds.initAdapter
|
||||
import org.pixeldroid.app.posts.feeds.launch
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
|
||||
|
||||
|
||||
/**
|
||||
@ -30,8 +32,7 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
||||
internal lateinit var viewModel: FeedViewModel<T>
|
||||
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
|
||||
|
||||
lateinit var binding: FragmentFeedBinding
|
||||
|
||||
var binding: FragmentFeedBinding? = null
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
@ -48,23 +49,35 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
||||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
.collect { binding?.list?.scrollToPosition(0) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
savedInstanceState: Bundle?, swipeRefreshLayout: SwipeRefreshLayout?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
initAdapter(binding.progressBar, binding.swipeRefreshLayout, binding.list,
|
||||
binding.motionLayout, binding.errorLayout, adapter)
|
||||
binding!!.let {
|
||||
initAdapter(
|
||||
it.progressBar, swipeRefreshLayout ?: it.swipeRefreshLayout, it.list,
|
||||
it.motionLayout, it.errorLayout, adapter
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
return binding!!.root
|
||||
}
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return onCreateView(inflater, container, savedInstanceState, null)
|
||||
}
|
||||
fun onTabReClicked() {
|
||||
binding?.list?.limitedLengthSmoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,9 @@ class UncachedPostsFragment : UncachedFeedFragment<Status>() {
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
getItem(position)?.let {
|
||||
(holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx)
|
||||
(holder as StatusViewHolder).bind(
|
||||
it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,15 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.NestedScrollingChild
|
||||
import androidx.core.view.NestedScrollingChildHelper
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.CommentBinding
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
@ -25,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
|
||||
/**
|
||||
* Fragment to show a list of [Status]s, in form of comments
|
||||
*/
|
||||
class CommentFragment : UncachedFeedFragment<Status>() {
|
||||
class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment<Status>() {
|
||||
|
||||
private lateinit var id: String
|
||||
private lateinit var domain: String
|
||||
@ -42,11 +45,11 @@ class CommentFragment : UncachedFeedFragment<Status>() {
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
|
||||
|
||||
// Get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -62,6 +65,7 @@ class CommentFragment : UncachedFeedFragment<Status>() {
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
binding?.swipeRefreshLayout?.isEnabled = false
|
||||
return view
|
||||
}
|
||||
companion object {
|
||||
|
@ -2,17 +2,23 @@ package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
|
||||
|
||||
import android.os.Bundle
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
|
||||
|
||||
|
||||
class HashTagActivity : BaseThemedWithBarActivity() {
|
||||
class HashTagActivity : BaseActivity() {
|
||||
private var tagFragment = UncachedPostsFragment()
|
||||
private lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_followers)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
// Get hashtag tag
|
||||
|
@ -13,12 +13,12 @@ import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityCollectionBinding
|
||||
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION
|
||||
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION_ID
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Collection
|
||||
import java.lang.Exception
|
||||
|
||||
class CollectionActivity : BaseThemedWithBarActivity() {
|
||||
class CollectionActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityCollectionBinding
|
||||
|
||||
private lateinit var collection: Collection
|
||||
@ -37,6 +37,7 @@ class CollectionActivity : BaseThemedWithBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityCollectionBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
|
@ -6,6 +6,7 @@ import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
@ -19,44 +20,60 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityEditProfileBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.openUrl
|
||||
|
||||
class EditProfileActivity : BaseThemedWithBarActivity() {
|
||||
class EditProfileActivity : BaseActivity() {
|
||||
|
||||
private lateinit var model: EditProfileViewModel
|
||||
private val model: EditProfileViewModel by viewModels()
|
||||
private lateinit var binding: ActivityEditProfileBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityEditProfileBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.edit_profile)
|
||||
|
||||
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
|
||||
model = _model
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Handle the back button event
|
||||
if(model.madeChanges()){
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(getString(R.string.profile_save_changes))
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
this@addCallback.isEnabled = false
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
this.isEnabled = false
|
||||
if (model.submittedChanges) setResult(RESULT_OK)
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
if(uiState.profileLoaded){
|
||||
binding.bioEditText.setText(uiState.bio)
|
||||
binding.nameEditText.setText(uiState.name)
|
||||
model.changesApplied()
|
||||
}
|
||||
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
|
||||
if(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
|
||||
if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
|
||||
|
||||
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
|
||||
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
|
||||
|
||||
binding.privateSwitch.isChecked = uiState.privateAccount == true
|
||||
Glide.with(binding.profilePic).load(uiState.profilePictureUri)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(binding.profilePic)
|
||||
|
||||
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE
|
||||
else View.VISIBLE
|
||||
binding.savingProgressBar.visibility =
|
||||
if(uiState.error || (uiState.profileSent && !uiState.uploadingPicture)) View.GONE
|
||||
else View.VISIBLE
|
||||
|
||||
if(uiState.profileSent){
|
||||
if(uiState.profileSent && !uiState.uploadingPicture && !uiState.error){
|
||||
binding.progressText.setText(R.string.profile_saved)
|
||||
binding.done.visibility = View.VISIBLE
|
||||
} else {
|
||||
@ -94,18 +111,18 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// binding.changeImageButton.setOnClickListener {
|
||||
// Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
// type = "*/*"
|
||||
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
|
||||
// action = Intent.ACTION_GET_CONTENT
|
||||
// addCategory(Intent.CATEGORY_OPENABLE)
|
||||
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
// uploadImageResultContract.launch(
|
||||
// Intent.createChooser(this, null)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
binding.profilePic.setOnClickListener {
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
uploadImageResultContract.launch(
|
||||
Intent.createChooser(this, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
@ -119,10 +136,10 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
|
||||
val imageUri: String = clipData.getItemAt(i).uri.toString()
|
||||
images.add(imageUri)
|
||||
}
|
||||
model.uploadImage(images.first())
|
||||
model.updateImage(images.first())
|
||||
} else if (data.data != null) {
|
||||
images.add(data.data!!.toString())
|
||||
model.uploadImage(images.first())
|
||||
model.updateImage(images.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,18 +149,6 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
|
||||
return true
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if(model.madeChanges()){
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(getString(R.string.profile_save_changes))
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
setPositiveButton(android.R.string.ok) { _, _ -> super.onBackPressed()}
|
||||
}.show()
|
||||
}
|
||||
else super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId){
|
||||
R.id.action_apply -> {
|
||||
|
@ -1,16 +1,16 @@
|
||||
package org.pixeldroid.app.profile
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
@ -21,23 +21,33 @@ import kotlinx.coroutines.launch
|
||||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.postCreation.ProgressRequestBody
|
||||
import org.pixeldroid.app.posts.fromHtml
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context
|
||||
): ViewModel() {
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
|
||||
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
|
||||
|
||||
var oldProfile: Account? = null
|
||||
private var oldProfile: Account? = null
|
||||
|
||||
var submittedChanges = false
|
||||
private set
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
@ -46,6 +56,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
try {
|
||||
val profile = api.verifyCredentials()
|
||||
updateUserInfoDb(db, profile)
|
||||
if (oldProfile == null) oldProfile = profile
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
@ -76,15 +87,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
fun sendProfile() {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val requestBody =
|
||||
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
|
||||
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
sendingProfile = true,
|
||||
profileSent = false,
|
||||
loadingProfile = false,
|
||||
profileLoaded = false,
|
||||
error = false
|
||||
)
|
||||
}
|
||||
@ -97,12 +103,17 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
note = bio,
|
||||
locked = privateAccount,
|
||||
)
|
||||
if (madeChanges()) submittedChanges = true
|
||||
oldProfile = account
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()},
|
||||
bio = account.source?.note
|
||||
?: account.note?.let { fromHtml(it).toString() },
|
||||
name = account.display_name,
|
||||
profilePictureUri = account.anyAvatar()?.toUri(),
|
||||
profilePictureUri = if (profilePictureChanged) profilePictureUri
|
||||
else account.anyAvatar()?.toUri(),
|
||||
uploadProgress = 0,
|
||||
uploadingPicture = profilePictureChanged,
|
||||
privateAccount = account.locked,
|
||||
sendingProfile = false,
|
||||
profileSent = true,
|
||||
@ -111,14 +122,13 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
error = false
|
||||
)
|
||||
}
|
||||
if(profilePictureChanged) uploadImage()
|
||||
} catch (exception: Exception) {
|
||||
Log.e("TAG", exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
sendingProfile = false,
|
||||
profileSent = false,
|
||||
loadingProfile = false,
|
||||
profileLoaded = false,
|
||||
error = true
|
||||
)
|
||||
}
|
||||
@ -145,20 +155,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
fun changesApplied() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(profileLoaded = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun madeChanges(): Boolean =
|
||||
with(uiState.value) {
|
||||
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio }
|
||||
// If source note is null, check note
|
||||
val privateChanged = oldProfile?.locked != privateAccount
|
||||
val displayNameChanged = oldProfile?.display_name != name
|
||||
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
|
||||
// If source note is null, check note
|
||||
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
|
||||
?: true
|
||||
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|
||||
|| bioUnchanged
|
||||
|
||||
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
|
||||
}
|
||||
|
||||
fun clickedCard() {
|
||||
@ -178,16 +184,27 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadImage(image: String) {
|
||||
//TODO fix
|
||||
fun updateImage(image: String) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureUri = image.toUri(),
|
||||
profilePictureChanged = true,
|
||||
profileSent = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadImage() {
|
||||
val image = uiState.value.profilePictureUri!!
|
||||
|
||||
val inputStream =
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri())
|
||||
applicationContext.contentResolver.openInputStream(image)
|
||||
?: return
|
||||
|
||||
val size: Long =
|
||||
if (image.toUri().scheme == "content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(
|
||||
image.toUri(),
|
||||
if (image.scheme == "content") {
|
||||
applicationContext.contentResolver.query(
|
||||
image,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@ -203,7 +220,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
cursor.getLong(sizeIndex)
|
||||
} ?: 0
|
||||
} else {
|
||||
image.toUri().toFile().length()
|
||||
image.toFile().length()
|
||||
}
|
||||
|
||||
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
|
||||
@ -225,21 +242,32 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
var postSub: Disposable? = null
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val inter = api.updateProfilePicture(requestBody.parts[0])
|
||||
|
||||
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
|
||||
|
||||
val inter =
|
||||
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
|
||||
else api.updateProfilePictureMastodon(requestBody.parts[0])
|
||||
|
||||
postSub = inter
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ it: Account ->
|
||||
Log.e("qsdfqsdfs", it.toString())
|
||||
|
||||
/* onNext = */ { account: Account ->
|
||||
account.anyAvatar()?.let {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureUri = it.toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ e: Throwable ->
|
||||
/* onError = */ { e: Throwable ->
|
||||
Log.e("error", (e as? HttpException)?.message().orEmpty())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadProgress = 0,
|
||||
uploadingPicture = true,
|
||||
uploadingPicture = false,
|
||||
error = true
|
||||
)
|
||||
}
|
||||
@ -247,9 +275,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
},
|
||||
{
|
||||
/* onComplete = */ {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureChanged = false,
|
||||
uploadProgress = 100,
|
||||
uploadingPicture = false
|
||||
)
|
||||
@ -265,7 +294,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
||||
data class EditProfileActivityUiState(
|
||||
val name: String? = null,
|
||||
val bio: String? = null,
|
||||
val profilePictureUri: Uri?= null,
|
||||
val profilePictureUri: Uri? = null,
|
||||
val profilePictureChanged: Boolean = false,
|
||||
val privateAccount: Boolean? = null,
|
||||
val loadingProfile: Boolean = true,
|
||||
val profileLoaded: Boolean = false,
|
||||
@ -274,10 +304,4 @@ data class EditProfileActivityUiState(
|
||||
val error: Boolean = false,
|
||||
val uploadingPicture: Boolean = false,
|
||||
val uploadProgress: Int = 0,
|
||||
)
|
||||
|
||||
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java).newInstance(application)
|
||||
}
|
||||
}
|
||||
)
|
@ -2,20 +2,25 @@ package org.pixeldroid.app.profile
|
||||
|
||||
import android.os.Bundle
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityFollowersBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG
|
||||
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_TAG
|
||||
import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG
|
||||
|
||||
|
||||
class FollowsActivity : BaseThemedWithBarActivity() {
|
||||
class FollowsActivity : BaseActivity() {
|
||||
private var followsFragment = AccountListFragment()
|
||||
private lateinit var binding: ActivityFollowersBinding
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_followers)
|
||||
binding = ActivityFollowersBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
|
||||
|
@ -6,26 +6,33 @@ import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityProfileBinding
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.setProfileImageFromURL
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
class ProfileActivity : BaseActivity() {
|
||||
|
||||
private lateinit var domain : String
|
||||
private lateinit var accountId : String
|
||||
@ -36,7 +43,10 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityProfileBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
@ -51,9 +61,32 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
val tabs = createProfileTabs(account)
|
||||
setupTabs(tabs)
|
||||
setContent(account)
|
||||
|
||||
binding.profileMotion.setTransitionListener(
|
||||
object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionStarted(
|
||||
motionLayout: MotionLayout?, startId: Int, endId: Int,
|
||||
) {}
|
||||
|
||||
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {}
|
||||
|
||||
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
|
||||
if (currentId == R.id.hideProfile && motionLayout?.startState == R.id.start) {
|
||||
// If the 1st transition has been made go to the second one
|
||||
motionLayout.setTransition(R.id.second)
|
||||
} else if(currentId == R.id.hideProfile && motionLayout?.startState == R.id.hideProfile){
|
||||
motionLayout.setTransition(R.id.first)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(
|
||||
motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float,
|
||||
) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun createProfileTabs(account: Account?): Array<Fragment>{
|
||||
private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
|
||||
|
||||
val profileFeedFragment = ProfileFeedFragment()
|
||||
profileFeedFragment.arguments = Bundle().apply {
|
||||
@ -77,7 +110,7 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
putSerializable(ProfileFeedFragment.COLLECTIONS, true)
|
||||
}
|
||||
|
||||
val returnArray: Array<Fragment> = arrayOf(
|
||||
val returnArray: Array<UncachedFeedFragment<FeedContent>> = arrayOf(
|
||||
profileGridFragment,
|
||||
profileFeedFragment,
|
||||
profileCollectionsFragment
|
||||
@ -97,7 +130,7 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
}
|
||||
|
||||
private fun setupTabs(
|
||||
tabs: Array<Fragment>
|
||||
tabs: Array<UncachedFeedFragment<FeedContent>>,
|
||||
){
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
@ -129,8 +162,15 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
}
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
binding.profileTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
tabs[tab.position].onTabReClicked()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setContent(account: Account?) {
|
||||
if(account != null) {
|
||||
@ -149,6 +189,9 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
).show()
|
||||
return@launchWhenResumed
|
||||
}
|
||||
|
||||
updateUserInfoDb(db, myAccount)
|
||||
|
||||
setViews(myAccount)
|
||||
}
|
||||
}
|
||||
@ -214,9 +257,15 @@ class ProfileActivity : BaseThemedWithBarActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private val editResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
// Profile was edited, reload
|
||||
setContent(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickEditButton() {
|
||||
val intent = Intent(this, EditProfileActivity::class.java)
|
||||
ContextCompat.startActivity(this, intent, null)
|
||||
editResult.launch(Intent(this, EditProfileActivity::class.java))
|
||||
}
|
||||
|
||||
private fun onClickFollowers(account: Account?) {
|
||||
|
@ -101,7 +101,7 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
if(grid || bookmarks || collections || addCollection) {
|
||||
binding.list.layoutManager = GridLayoutManager(context, 3)
|
||||
binding?.list?.layoutManager = GridLayoutManager(context, 3)
|
||||
}
|
||||
|
||||
// Get the view model
|
||||
@ -178,8 +178,10 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
||||
deleteFromCollection
|
||||
)
|
||||
} else {
|
||||
(holder as StatusViewHolder).bind(it as Status, apiHolder, db,
|
||||
lifecycleScope, requireContext().displayDimensionsInPx())
|
||||
(holder as StatusViewHolder).bind(
|
||||
it as Status, apiHolder, db, lifecycleScope,
|
||||
requireContext().displayDimensionsInPx(), requestPermissionDownloadPic
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,8 +191,11 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
||||
val url = "$domain/i/collections/create"
|
||||
|
||||
if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) {
|
||||
Snackbar.make(binding.root, getString(R.string.new_collection_link_failed),
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
binding?.let { binding ->
|
||||
Snackbar.make(
|
||||
binding.root, getString(R.string.new_collection_link_failed),
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,17 +9,21 @@ import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivitySearchBinding
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchAccountFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchHashtagFragment
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Results
|
||||
|
||||
class SearchActivity : BaseThemedWithBarActivity() {
|
||||
class SearchActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_search)
|
||||
val binding = ActivitySearchBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
var query = ""
|
||||
|
@ -11,22 +11,24 @@ import androidx.core.content.ContextCompat
|
||||
import org.pixeldroid.app.databinding.FragmentSearchBinding
|
||||
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG
|
||||
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
|
||||
|
||||
/**
|
||||
* This fragment lets you search and use Pixelfed's Discover feature
|
||||
*/
|
||||
|
||||
class SearchDiscoverFragment : BaseFragment() {
|
||||
|
||||
private lateinit var api: PixelfedAPI
|
||||
|
||||
var binding: FragmentSearchBinding by bindingLifecycleAware()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
binding = FragmentSearchBinding.inflate(inflater, container, false)
|
||||
|
||||
@ -56,4 +58,5 @@ class SearchDiscoverFragment : BaseFragment() {
|
||||
intent.putExtra(TRENDING_TAG, type)
|
||||
ContextCompat.startActivity(binding.root.context, intent, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import org.pixeldroid.app.posts.PostActivity
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountViewHolder
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.HashTagViewHolder
|
||||
import org.pixeldroid.app.profile.ProfilePostViewHolder
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
@ -24,7 +24,7 @@ import org.pixeldroid.app.utils.api.objects.Status
|
||||
import org.pixeldroid.app.utils.api.objects.Tag
|
||||
import org.pixeldroid.app.utils.setSquareImageFromURL
|
||||
|
||||
class TrendingActivity : BaseThemedWithBarActivity() {
|
||||
class TrendingActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityTrendingBinding
|
||||
private lateinit var trendingAdapter : TrendingRecyclerViewAdapter
|
||||
@ -33,6 +33,7 @@ class TrendingActivity : BaseThemedWithBarActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityTrendingBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
val recycler = binding.list
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
@ -1,26 +0,0 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import org.pixeldroid.app.BuildConfig
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityAboutBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
|
||||
class AboutActivity : BaseThemedWithBarActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityAboutBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.about_pixeldroid)
|
||||
|
||||
binding.aboutVersionNumber.text = BuildConfig.VERSION_NAME
|
||||
binding.licensesButton.setOnClickListener{
|
||||
val intent = Intent(this, LicenseActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.OpenSourceLicenseBinding
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
|
||||
/**
|
||||
* Displays licenses for all app dependencies. JSON is
|
||||
* generated by the plugin https://github.com/cookpad/LicenseToolsPlugin.
|
||||
*/
|
||||
class LicenseActivity: BaseThemedWithBarActivity() {
|
||||
|
||||
private lateinit var binding: OpenSourceLicenseBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = OpenSourceLicenseBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
supportActionBar?.setTitle(R.string.dependencies_licenses)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setHomeButtonEnabled(true)
|
||||
|
||||
setupRecyclerView()
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
val aboutLibsJson: String = applicationContext.resources.openRawResource(R.raw.aboutlibraries)
|
||||
.bufferedReader().use { it.readText() }
|
||||
|
||||
val libs = Libs.Builder()
|
||||
.withJson(aboutLibsJson)
|
||||
.build()
|
||||
|
||||
val adapter = OpenSourceLicenseAdapter(libs)
|
||||
binding.openSourceLicenseRecyclerView.adapter = adapter
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
package org.pixeldroid.app.settings
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.entity.Library
|
||||
import org.pixeldroid.app.databinding.OpenSourceItemBinding
|
||||
|
||||
class OpenSourceLicenseAdapter(private val openSourceItems: Libs) :
|
||||
RecyclerView.Adapter<OpenSourceLicenseAdapter.OpenSourceLicenceViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OpenSourceLicenceViewHolder
|
||||
{
|
||||
val itemBinding = OpenSourceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return OpenSourceLicenceViewHolder(itemBinding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: OpenSourceLicenceViewHolder, position: Int) {
|
||||
val item = openSourceItems.libraries[position]
|
||||
holder.bind(item)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = openSourceItems.libraries.size
|
||||
|
||||
class OpenSourceLicenceViewHolder(val binding: OpenSourceItemBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(item: Library) {
|
||||
with(binding) {
|
||||
if (item.name.isNotEmpty()) {
|
||||
title.isVisible = true
|
||||
title.text = item.name
|
||||
} else {
|
||||
title.isVisible = false
|
||||
}
|
||||
val license = item.licenses.firstOrNull()
|
||||
val licenseName = license?.name ?: ""
|
||||
val licenseUrl = license?.url?.let { " (${it} )" } ?: ""
|
||||
copyright.isVisible = true
|
||||
copyright.apply {
|
||||
text = "$licenseName$licenseUrl"
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
url.isVisible = true
|
||||
url.apply {
|
||||
text = "${item.developers.firstOrNull()?.name ?: ""} ${item.website}"
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import android.content.SharedPreferences
|
||||
import android.content.res.XmlResourceParser
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
@ -16,23 +17,39 @@ import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
|
||||
import org.pixeldroid.app.databinding.SettingsBinding
|
||||
import org.pixeldroid.common.ThemedActivity
|
||||
import org.pixeldroid.app.utils.setThemeFromPreferences
|
||||
|
||||
|
||||
class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private var restartMainOnExit = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = SettingsBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
|
||||
setContentView(R.layout.settings)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, SettingsFragment())
|
||||
.commit()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.menu_settings)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
|
||||
// Handle the back button event
|
||||
// If a setting (for example language or theme) was changed, the main activity should be
|
||||
// started without history so that the change is applied to the whole back stack
|
||||
if (restartMainOnExit) {
|
||||
val intent = Intent(this@SettingsActivity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
super@SettingsActivity.startActivity(intent)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
restartMainOnExit = intent.getBooleanExtra("restartMain", false)
|
||||
}
|
||||
@ -51,25 +68,17 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
// If a setting (for example language or theme) was changed, the main activity should be
|
||||
// started without history so that the change is applied to the whole back stack
|
||||
if (restartMainOnExit) {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
super.startActivity(intent)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"theme" -> {
|
||||
setThemeFromPreferences(sharedPreferences, resources)
|
||||
recreateWithRestartStatus()
|
||||
}
|
||||
"themeColor" -> {
|
||||
recreateWithRestartStatus()
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
sharedPreferences?.let {
|
||||
when (key) {
|
||||
"theme" -> {
|
||||
setThemeFromPreferences(it, resources)
|
||||
recreateWithRestartStatus()
|
||||
}
|
||||
|
||||
"themeColor" -> {
|
||||
recreateWithRestartStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,7 +134,8 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
|
||||
class LanguageSettingFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val list: MutableList<String> = mutableListOf()
|
||||
resources.getXml(R.xml.locales_config).use {
|
||||
// IDE doesn't find it, but compiling works apparently?
|
||||
resources.getXml(R.xml._generated_res_locale_config).use {
|
||||
var eventType = it.eventType
|
||||
while (eventType != XmlResourceParser.END_DOCUMENT) {
|
||||
when (eventType) {
|
||||
|
215
app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt
Normal file
215
app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt
Normal file
@ -0,0 +1,215 @@
|
||||
package org.pixeldroid.app.stories
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnTouchListener
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityStoriesBinding
|
||||
import org.pixeldroid.app.posts.setTextViewFromISO8601
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
|
||||
class StoriesActivity: BaseActivity() {
|
||||
|
||||
companion object {
|
||||
const val STORY_CAROUSEL = "LaunchStoryCarousel"
|
||||
const val STORY_CAROUSEL_SELF = "LaunchStoryCarouselSelf"
|
||||
const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityStoriesBinding
|
||||
|
||||
private lateinit var storyProgress: StoryProgress
|
||||
|
||||
private val model: StoriesViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
//force night mode always
|
||||
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityStoriesBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
storyProgress = StoryProgress(model.uiState.value.imageList.size)
|
||||
binding.storyProgressImage.setImageDrawable(storyProgress)
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
binding.pause.isSelected = uiState.paused
|
||||
|
||||
uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) }
|
||||
|
||||
if (uiState.errorMessage != null) {
|
||||
binding.storyErrorText.setText(uiState.errorMessage)
|
||||
binding.storyErrorCard.isVisible = true
|
||||
} else binding.storyErrorCard.isVisible = false
|
||||
|
||||
if (uiState.snackBar != null) {
|
||||
Snackbar.make(
|
||||
binding.root, uiState.snackBar,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).setAnchorView(binding.storyReplyField).show()
|
||||
model.shownSnackbar()
|
||||
}
|
||||
|
||||
if (uiState.username != null) {
|
||||
binding.storyReplyField.hint = getString(R.string.replyToStory).format(uiState.username)
|
||||
} else binding.storyReplyField.hint = null
|
||||
|
||||
uiState.profilePicture?.let {
|
||||
Glide.with(binding.storyAuthorProfilePicture)
|
||||
.load(it)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(binding.storyAuthorProfilePicture)
|
||||
}
|
||||
|
||||
binding.storyAuthor.text = uiState.username
|
||||
|
||||
storyProgress.currentStory = uiState.currentImage
|
||||
|
||||
uiState.imageList.getOrNull(uiState.currentImage)?.let {
|
||||
Glide.with(binding.storyImage)
|
||||
.load(it)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean = false
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
Glide.with(binding.storyImage)
|
||||
.load(uiState.imageList.getOrNull(uiState.currentImage + 1))
|
||||
.preload()
|
||||
return false
|
||||
}
|
||||
})
|
||||
.into(binding.storyImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Pause when clicked on text field
|
||||
binding.storyReplyField.editText?.setOnFocusChangeListener { view, isFocused ->
|
||||
if (view.isInTouchMode && isFocused) {
|
||||
view.performClick() // picks up first tap
|
||||
}
|
||||
}
|
||||
binding.storyReplyField.editText?.setOnClickListener {
|
||||
if (!model.uiState.value.paused) {
|
||||
model.pause()
|
||||
}
|
||||
}
|
||||
|
||||
binding.storyReplyField.editText?.doAfterTextChanged {
|
||||
it?.let { text ->
|
||||
val string = text.toString()
|
||||
if(string != model.uiState.value.reply) model.replyChanged(string)
|
||||
}
|
||||
}
|
||||
|
||||
binding.storyReplyField.setEndIconOnClickListener {
|
||||
binding.storyReplyField.editText?.text?.let { text ->
|
||||
model.sendReply(text)
|
||||
}
|
||||
}
|
||||
|
||||
binding.storyErrorCard.setOnClickListener{
|
||||
model.dismissError()
|
||||
}
|
||||
|
||||
model.count.observe(this) { state ->
|
||||
// Render state in UI
|
||||
model.uiState.value.durationList.getOrNull(model.uiState.value.currentImage)?.let {
|
||||
storyProgress.progress = 1 - (state/it.toFloat())
|
||||
binding.storyProgressImage.postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
binding.pause.setOnClickListener {
|
||||
//Set the button's appearance
|
||||
it.isSelected = !it.isSelected
|
||||
model.pause()
|
||||
}
|
||||
|
||||
val authorOnClickListener = OnClickListener {
|
||||
if (!model.uiState.value.paused) {
|
||||
model.pause()
|
||||
}
|
||||
model.currentProfileId()?.let {
|
||||
lifecycleScope.launch {
|
||||
Account.openAccountFromId(
|
||||
it,
|
||||
apiHolder.api ?: apiHolder.setToCurrentUser(),
|
||||
this@StoriesActivity
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.storyAuthorProfilePicture.setOnClickListener(authorOnClickListener)
|
||||
binding.storyAuthor.setOnClickListener(authorOnClickListener)
|
||||
|
||||
val onTouchListener = OnTouchListener { v, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> if (!model.uiState.value.paused) {
|
||||
model.pause()
|
||||
}
|
||||
MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
|
||||
v.performClick()
|
||||
return@OnTouchListener false
|
||||
} else model.pause()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
binding.viewMiddle.setOnTouchListener{ v, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> model.pause()
|
||||
MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
|
||||
v.performClick()
|
||||
return@setOnTouchListener false
|
||||
} else model.pause()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
binding.viewLeft.setOnTouchListener(onTouchListener)
|
||||
binding.viewRight.setOnTouchListener(onTouchListener)
|
||||
|
||||
binding.viewRight.setOnClickListener {
|
||||
model.goToNext()
|
||||
}
|
||||
binding.viewLeft.setOnClickListener {
|
||||
model.goToPrevious()
|
||||
}
|
||||
}
|
||||
}
|
210
app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt
Normal file
210
app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt
Normal file
@ -0,0 +1,210 @@
|
||||
package org.pixeldroid.app.stories
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Editable
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
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.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
|
||||
import org.pixeldroid.app.utils.api.objects.Story
|
||||
import org.pixeldroid.app.utils.api.objects.StoryCarousel
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
data class StoriesUiState(
|
||||
val profilePicture: String? = null,
|
||||
val username: String? = null,
|
||||
val age: Instant? = null,
|
||||
val currentImage: Int = 0,
|
||||
val imageList: List<String> = emptyList(),
|
||||
val durationList: List<Int> = emptyList(),
|
||||
val paused: Boolean = false,
|
||||
@StringRes
|
||||
val errorMessage: Int? = null,
|
||||
@StringRes
|
||||
val snackBar: Int? = null,
|
||||
val reply: String = ""
|
||||
)
|
||||
@HiltViewModel
|
||||
class StoriesViewModel @Inject constructor(state: SavedStateHandle,
|
||||
db: AppDatabase,
|
||||
private val apiHolder: PixelfedAPIHolder) : ViewModel() {
|
||||
private val carousel: StoryCarousel? = state[StoriesActivity.STORY_CAROUSEL]
|
||||
private val userId: String? = state[StoriesActivity.STORY_CAROUSEL_USER_ID]
|
||||
private val selfCarousel: Array<Story>? = state[StoriesActivity.STORY_CAROUSEL_SELF]
|
||||
|
||||
private var currentAccount: CarouselUserContainer?
|
||||
|
||||
private val _uiState: MutableStateFlow<StoriesUiState>
|
||||
|
||||
val uiState: StateFlow<StoriesUiState>
|
||||
|
||||
val count = MutableLiveData<Float>()
|
||||
|
||||
private var timer: CountDownTimer? = null
|
||||
|
||||
init {
|
||||
currentAccount =
|
||||
if (selfCarousel != null) {
|
||||
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel.toList()) }
|
||||
} else carousel?.nodes?.firstOrNull { it?.user?.id == userId }
|
||||
|
||||
_uiState = MutableStateFlow(newUiStateFromCurrentAccount())
|
||||
uiState = _uiState
|
||||
|
||||
startTimerForCurrent()
|
||||
}
|
||||
|
||||
private fun setTimer(timerLength: Float) {
|
||||
count.value = timerLength
|
||||
timer = object: CountDownTimer((timerLength * 1000).toLong(), 50){
|
||||
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
count.value = millisUntilFinished.toFloat() / 1000
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
goToNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newUiStateFromCurrentAccount(): StoriesUiState = StoriesUiState(
|
||||
profilePicture = currentAccount?.user?.avatar,
|
||||
age = currentAccount?.nodes?.getOrNull(0)?.created_at,
|
||||
username = currentAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option?
|
||||
errorMessage = null,
|
||||
currentImage = 0,
|
||||
imageList = currentAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(),
|
||||
durationList = currentAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList()
|
||||
)
|
||||
|
||||
private fun goTo(index: Int){
|
||||
if((0 until uiState.value.imageList.size).contains(index)) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
currentImage = index,
|
||||
age = currentAccount?.nodes?.getOrNull(index)?.created_at,
|
||||
paused = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if(selfCarousel != null) return
|
||||
val currentUserId = currentAccount?.user?.id
|
||||
val currentAccountIndex = carousel?.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return
|
||||
currentAccount = when (index) {
|
||||
uiState.value.imageList.size -> {
|
||||
// Go to next user
|
||||
if(currentAccountIndex + 1 >= carousel.nodes.size) return
|
||||
carousel.nodes.getOrNull(currentAccountIndex + 1)
|
||||
|
||||
}
|
||||
|
||||
-1 -> {
|
||||
// Go to previous user
|
||||
if(currentAccountIndex <= 0) return
|
||||
carousel.nodes.getOrNull(currentAccountIndex - 1)
|
||||
}
|
||||
else -> return // Do nothing, given index does not make sense
|
||||
}
|
||||
_uiState.update { newUiStateFromCurrentAccount() }
|
||||
}
|
||||
|
||||
timer?.cancel()
|
||||
startTimerForCurrent()
|
||||
}
|
||||
|
||||
fun goToNext() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val story = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)
|
||||
|
||||
if (story?.seen == true){
|
||||
//TODO update seen when marked successfully as seen?
|
||||
story.id?.let { api.storySeen(it) }
|
||||
}
|
||||
} catch (exception: Exception){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(errorMessage = R.string.story_could_not_see)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
goTo(uiState.value.currentImage + 1)
|
||||
}
|
||||
|
||||
fun goToPrevious() = goTo(uiState.value.currentImage - 1)
|
||||
|
||||
private fun startTimerForCurrent(){
|
||||
uiState.value.let {
|
||||
it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time ->
|
||||
setTimer(time.toFloat())
|
||||
timer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
if(_uiState.value.paused){
|
||||
timer?.start()
|
||||
} else {
|
||||
timer?.cancel()
|
||||
count.value?.let { setTimer(it) }
|
||||
}
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(paused = !currentUiState.paused)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendReply(text: Editable) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
currentStoryId()?.let { api.storyComment(it, text.toString()) }
|
||||
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(snackBar = R.string.sent_reply_story)
|
||||
}
|
||||
} catch (exception: Exception){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(errorMessage = R.string.story_reply_error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentStoryId(): String? = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id
|
||||
|
||||
fun replyChanged(text: String) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(reply = text)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissError() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(errorMessage = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun shownSnackbar() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(snackBar = null)
|
||||
}
|
||||
}
|
||||
|
||||
fun currentProfileId(): String? = currentAccount?.user?.id
|
||||
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
package org.pixeldroid.app.stories
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.Shader
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.StoryCarouselBinding
|
||||
import org.pixeldroid.app.databinding.StoryCarouselItemBinding
|
||||
import org.pixeldroid.app.databinding.StoryCarouselSelfBinding
|
||||
import org.pixeldroid.app.postCreation.camera.CameraActivity
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
|
||||
import org.pixeldroid.app.utils.api.objects.Story
|
||||
import org.pixeldroid.app.utils.api.objects.StoryCarousel
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
|
||||
|
||||
/**
|
||||
* Adapter that has either 1 or 0 items, to show stories widget or not
|
||||
*/
|
||||
class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder: PixelfedAPIHolder) : RecyclerView.Adapter<StoryCarouselViewHolder>() {
|
||||
var carousel: StoryCarousel? = null
|
||||
|
||||
/**
|
||||
* Whether to show stories or not.
|
||||
*
|
||||
* Changing this property will immediately notify the Adapter to change the item it's
|
||||
* presenting.
|
||||
*/
|
||||
var showStories: Boolean = false
|
||||
set(newValue) {
|
||||
val oldValue = field
|
||||
|
||||
if (oldValue && !newValue) {
|
||||
notifyItemRemoved(0)
|
||||
} else if (newValue && !oldValue) {
|
||||
notifyItemInserted(0)
|
||||
} else if (oldValue && newValue) {
|
||||
notifyItemChanged(0)
|
||||
}
|
||||
field = newValue
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoryCarouselViewHolder {
|
||||
return StoryCarouselViewHolder.create(parent, ::noStories)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: StoryCarouselViewHolder, position: Int) {
|
||||
holder.bind(carousel)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = 0
|
||||
|
||||
override fun getItemCount(): Int = if (showStories) 1 else 0
|
||||
|
||||
private fun noStories(){
|
||||
showStories = false
|
||||
}
|
||||
|
||||
private fun gotStories(newCarousel: StoryCarousel) {
|
||||
carousel = newCarousel
|
||||
showStories = true
|
||||
}
|
||||
|
||||
fun refreshStories(){
|
||||
lifecycleScope.launch {
|
||||
try{
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val carousel = api.carousel()
|
||||
|
||||
// If there are stories from someone else or our stories to show, show them
|
||||
if (carousel.nodes?.isEmpty() == false || carousel.self?.nodes?.isEmpty() == false) {
|
||||
// Pass carousel to adapter
|
||||
gotStories(carousel)
|
||||
} else {
|
||||
noStories()
|
||||
}
|
||||
} catch (exception: Exception){
|
||||
noStories()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class StoryCarouselViewHolder(val binding: StoryCarouselBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(carousel: StoryCarousel?) {
|
||||
val adapter = StoriesListAdapter()
|
||||
binding.storyCarousel.adapter = adapter
|
||||
|
||||
carousel?.let { adapter.initCarousel(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup, noStories: () -> Unit): StoryCarouselViewHolder {
|
||||
val itemBinding = StoryCarouselBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return StoryCarouselViewHolder(itemBinding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class StoriesListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var storyCarousel: StoryCarousel? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if(viewType == R.layout.story_carousel_self){
|
||||
val v = StoryCarouselSelfBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
v.myStory.visibility =
|
||||
if (storyCarousel?.self?.nodes?.isEmpty() == false) View.VISIBLE
|
||||
else View.GONE
|
||||
|
||||
AddViewHolder(v)
|
||||
}
|
||||
else {
|
||||
val v = StoryCarouselItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
ViewHolder(v)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if(position == 0) R.layout.story_carousel_self
|
||||
else R.layout.story_carousel_item
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if(position > 0) {
|
||||
val carouselPosition = position - 1
|
||||
storyCarousel?.nodes?.get(carouselPosition)?.let { (holder as ViewHolder).bindItem(it) }
|
||||
holder.itemView.setOnClickListener {
|
||||
storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId ->
|
||||
val intent = Intent(holder.itemView.context, StoriesActivity::class.java)
|
||||
intent.putExtra(StoriesActivity.STORY_CAROUSEL, storyCarousel)
|
||||
intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId)
|
||||
holder.itemView.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storyCarousel?.self?.nodes?.let { (holder as? AddViewHolder)?.bindItem(it.filterNotNull()) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
// If the storyCarousel is not set, the carousel is not shown, so itemCount of 0
|
||||
return (storyCarousel?.nodes?.size?.plus(1)) ?: 0
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun initCarousel(carousel: StoryCarousel){
|
||||
storyCarousel = carousel
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class AddViewHolder(private val itemBinding: StoryCarouselSelfBinding) : RecyclerView.ViewHolder(itemBinding.root) {
|
||||
fun bindItem(nodes: List<Story>) {
|
||||
itemBinding.addStory.setOnClickListener {
|
||||
val intent = Intent(itemView.context, CameraActivity::class.java)
|
||||
intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true)
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
itemBinding.myStory.setOnClickListener {
|
||||
val intent = Intent(itemView.context, StoriesActivity::class.java)
|
||||
intent.putExtra(StoriesActivity.STORY_CAROUSEL_SELF, nodes.toTypedArray())
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
|
||||
// Only show image on new Android versions, because the transformations need it and the
|
||||
// text is not legible without the transformations
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Glide.with(itemBinding.root).load(nodes.firstOrNull()?.src).into(itemBinding.carouselImageView)
|
||||
val value = 70 * 255 / 100
|
||||
val darkFilterRenderEffect = PorterDuffColorFilter(Color.argb(value, 0, 0, 0), PorterDuff.Mode.SRC_ATOP)
|
||||
val blurRenderEffect =
|
||||
RenderEffect.createBlurEffect(
|
||||
4f, 4f, Shader.TileMode.MIRROR
|
||||
)
|
||||
val combinedEffect = RenderEffect.createColorFilterEffect(darkFilterRenderEffect, blurRenderEffect)
|
||||
itemBinding.carouselImageView.setRenderEffect(combinedEffect)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(private val itemBinding: StoryCarouselItemBinding) :
|
||||
RecyclerView.ViewHolder(itemBinding.root) {
|
||||
fun bindItem(user: CarouselUserContainer) {
|
||||
Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView)
|
||||
Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture)
|
||||
|
||||
itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here!
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package org.pixeldroid.app.stories
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
/**
|
||||
* Copied & adapted from AntennaPod's EchoProgress class because it looked great and is very simple
|
||||
* AntennaPod/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java
|
||||
*/
|
||||
class StoryProgress(private val numStories: Int) : Drawable() {
|
||||
private val paint: Paint = Paint().apply {
|
||||
flags = Paint.ANTI_ALIAS_FLAG
|
||||
style = Paint.Style.STROKE
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
color = -0x1
|
||||
}
|
||||
|
||||
var progress = 0f
|
||||
var currentStory: Int = 0
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
paint.strokeWidth = 0.5f * bounds.height()
|
||||
val y = 0.5f * bounds.height()
|
||||
val sectionWidth = 1.0f * bounds.width() / numStories
|
||||
val sectionPadding = 0.03f * sectionWidth
|
||||
// Iterate over stories
|
||||
for (i in 0 until numStories) {
|
||||
if (i < currentStory) {
|
||||
// If current drawing position is smaller than current story, the paint we will use
|
||||
// should be opaque: this story is already "seen"
|
||||
paint.alpha = 255
|
||||
} else {
|
||||
// Otherwise it should be somewhat transparent, denoting it is not yet seen
|
||||
paint.alpha = 100
|
||||
}
|
||||
// Draw an entire line with the paint, for now ignoring partial progress within the
|
||||
// current story
|
||||
canvas.drawLine(
|
||||
i * sectionWidth + sectionPadding,
|
||||
y,
|
||||
(i + 1) * sectionWidth - sectionPadding,
|
||||
y,
|
||||
paint
|
||||
)
|
||||
// If current position is equal to progress, we are drawing the current story. Thus we
|
||||
// should account for partial progress and paint the beginning of the line opaquely
|
||||
if (i == currentStory) {
|
||||
paint.alpha = 255
|
||||
canvas.drawLine(
|
||||
currentStory * sectionWidth + sectionPadding,
|
||||
y,
|
||||
currentStory * sectionWidth + sectionPadding + progress * (sectionWidth - 2 * sectionPadding),
|
||||
y,
|
||||
paint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {}
|
||||
override fun setColorFilter(cf: ColorFilter?) {}
|
||||
}
|
||||
|
@ -1,25 +1,20 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
@AndroidEntryPoint
|
||||
open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(this.application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
@ -9,6 +12,7 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Base Fragment, for dependency injection and other things common to a lot of the fragments
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
open class BaseFragment: Fragment() {
|
||||
|
||||
@Inject
|
||||
@ -17,9 +21,18 @@ open class BaseFragment: Fragment() {
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
internal val requestPermissionDownloadPic =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
if (!isGranted) {
|
||||
context?.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setMessage(R.string.write_permission_download_pic)
|
||||
.setNegativeButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
open class BaseThemedWithBarActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set theme when we chose one
|
||||
themeActionBar()?.let { setTheme(it) }
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
open class BaseThemedWithoutBarActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Set theme when we chose one
|
||||
themeNoActionBar()?.let { setTheme(it) }
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ fun setProfileImageFromURL(view : View, url : String?, image : ImageView) {
|
||||
* @param image, the imageView into which we will load the image
|
||||
*/
|
||||
fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) {
|
||||
//TODO performance: placeholder here takes a lot of time to compute and this is not async!
|
||||
Glide.with(view).load(url).placeholder(
|
||||
blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32, 32) }
|
||||
).apply(RequestOptions().centerCrop()).into(image)
|
||||
|
@ -3,14 +3,12 @@ package org.pixeldroid.app.utils
|
||||
import android.app.Application
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import org.ligi.tracedroid.TraceDroid
|
||||
import org.pixeldroid.app.utils.di.*
|
||||
|
||||
|
||||
@HiltAndroidApp
|
||||
class PixelDroidApplication: Application() {
|
||||
|
||||
private lateinit var mApplicationComponent: ApplicationComponent
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@ -19,18 +17,7 @@ class PixelDroidApplication: Application() {
|
||||
val sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
setThemeFromPreferences(sharedPreferences, resources)
|
||||
mApplicationComponent = DaggerApplicationComponent
|
||||
.builder()
|
||||
.applicationModule(ApplicationModule(this))
|
||||
.databaseModule(DatabaseModule(applicationContext))
|
||||
.aPIModule(APIModule())
|
||||
.build()
|
||||
mApplicationComponent.inject(this)
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
fun getAppComponent(): ApplicationComponent {
|
||||
return mApplicationComponent
|
||||
}
|
||||
}
|
@ -1,28 +1,25 @@
|
||||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.content.*
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Matrix
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
@ -34,7 +31,7 @@ import okhttp3.HttpUrl
|
||||
import org.pixeldroid.app.R
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@ -161,30 +158,6 @@ fun setThemeFromPreferences(preferences: SharedPreferences, resources: Resources
|
||||
}
|
||||
}
|
||||
|
||||
@StyleRes
|
||||
fun Context.themeNoActionBar(): Int? {
|
||||
return when(PreferenceManager.getDefaultSharedPreferences(this).getInt("themeColor", 0)) {
|
||||
// No theme was chosen: the user wants to use the system dynamic color (from wallpaper for example)
|
||||
-1 -> null
|
||||
1 -> R.style.AppTheme2_NoActionBar
|
||||
2 -> R.style.AppTheme3_NoActionBar
|
||||
3 -> R.style.AppTheme4_NoActionBar
|
||||
else -> R.style.AppTheme5_NoActionBar
|
||||
}
|
||||
}
|
||||
|
||||
@StyleRes
|
||||
fun Context.themeActionBar(): Int? {
|
||||
return when(PreferenceManager.getDefaultSharedPreferences(this).getInt("themeColor", 0)) {
|
||||
// No theme was chosen: the user wants to use the system dynamic color (from wallpaper for example)
|
||||
-1 -> null
|
||||
1 -> R.style.AppTheme2
|
||||
2 -> R.style.AppTheme3
|
||||
3 -> R.style.AppTheme4
|
||||
else -> R.style.AppTheme5
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)
|
||||
|
||||
|
@ -23,6 +23,7 @@ import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.*
|
||||
import retrofit2.http.Field
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/*
|
||||
@ -51,7 +52,9 @@ interface PixelfedAPI {
|
||||
.client(
|
||||
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
|
||||
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
|
||||
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)).build()
|
||||
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.build()
|
||||
)
|
||||
.build().create(PixelfedAPI::class.java)
|
||||
}
|
||||
@ -74,6 +77,7 @@ interface PixelfedAPI {
|
||||
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
|
||||
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
|
||||
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder))
|
||||
.addInterceptor {
|
||||
it.request().newBuilder().run {
|
||||
@ -161,6 +165,7 @@ interface PixelfedAPI {
|
||||
@Field("poll[expires_in]") poll_expires: List<String>? = null,
|
||||
@Field("poll[multiple]") poll_multiple: List<String>? = null,
|
||||
@Field("poll[hide_totals]") poll_hideTotals: List<String>? = null,
|
||||
//FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works
|
||||
@Field("sensitive") sensitive: Int? = null,
|
||||
@Field("spoiler_text") spoiler_text: String? = null,
|
||||
@Field("visibility") visibility: String = "public",
|
||||
@ -231,6 +236,43 @@ interface PixelfedAPI {
|
||||
@Query("post_id") post_id: String,
|
||||
)
|
||||
|
||||
@GET("/api/pixelfed/v1/stories/self-carousel")
|
||||
suspend fun carousel(): StoryCarousel
|
||||
|
||||
@POST("/api/v1.1/stories/seen")
|
||||
suspend fun storySeen(
|
||||
@Query("id") id: String
|
||||
)
|
||||
|
||||
@POST("/api/v1.1/stories/comment")
|
||||
suspend fun storyComment(
|
||||
@Query("sid") sid: String,
|
||||
@Query("caption") caption: String
|
||||
)
|
||||
|
||||
@Multipart
|
||||
@POST("/api/v1.1/stories/add")
|
||||
fun storyUpload(
|
||||
@Part file: MultipartBody.Part,
|
||||
// The API takes this value but then overwrites it in /api/v1.1/stories/publish, so ignore this
|
||||
@Part duration: MultipartBody.Part? = null,
|
||||
): Observable<Attachment>
|
||||
|
||||
@POST("/api/v1.1/stories/publish")
|
||||
suspend fun storyPublish(
|
||||
@Query("media_id") media_id: String,
|
||||
//From 0 to 30, duration in seconds of the story
|
||||
@Query("duration") duration: Int = 10,
|
||||
//FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works. Same issue as sensitive boolean in postStatus
|
||||
@Query("can_reply") can_reply: String,
|
||||
@Query("can_react") can_react: String,
|
||||
)
|
||||
|
||||
@POST("/api/v1.1/stories/self-expire/{id}")
|
||||
suspend fun deleteCarousel(
|
||||
@Path("id") storyId: String
|
||||
)
|
||||
|
||||
//Used in our case to retrieve comments for a given status
|
||||
@GET("/api/v1/statuses/{id}/context")
|
||||
suspend fun statusComments(
|
||||
@ -296,18 +338,31 @@ interface PixelfedAPI {
|
||||
@Header("Authorization") authorization: String? = null
|
||||
): Account
|
||||
|
||||
//@Multipart
|
||||
@PATCH("/api/v1/accounts/update_credentials")
|
||||
suspend fun updateCredentials(
|
||||
@Query(value = "display_name") displayName: String?,
|
||||
@Query(value = "note") note: String?,
|
||||
@Query(value = "locked") locked: Boolean?,
|
||||
// @Part avatar: MultipartBody.Part?,
|
||||
): Account
|
||||
|
||||
/**
|
||||
* Pixelfed uses PHP, multipart uploads don't work through PATCH so we use POST as suggested
|
||||
* here: https://github.com/pixelfed/pixelfed/issues/4250
|
||||
* However, changing to POST breaks the upload on Mastodon.
|
||||
*
|
||||
* To have this work on Pixelfed and Mastodon without special logic to distinguish the two,
|
||||
* we'll have to wait for PHP 8.4 and https://wiki.php.net/rfc/rfc1867-non-post
|
||||
* which should come out end of 2024
|
||||
*/
|
||||
@Multipart
|
||||
@POST("/api/v1/accounts/update_credentials")
|
||||
fun updateProfilePicture(
|
||||
@Part avatar: MultipartBody.Part?
|
||||
): Observable<Account>
|
||||
|
||||
@Multipart
|
||||
@PATCH("/api/v1/accounts/update_credentials")
|
||||
fun updateProfilePicture(
|
||||
fun updateProfilePictureMastodon(
|
||||
@Part avatar: MultipartBody.Part?
|
||||
): Observable<Account>
|
||||
|
||||
|
@ -57,11 +57,13 @@ data class Account(
|
||||
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) {
|
||||
val account = try {
|
||||
api.getAccount(id)
|
||||
} catch (exception: IOException) {
|
||||
Log.e("GET ACCOUNT ERROR", exception.toString())
|
||||
return
|
||||
} catch (exception: HttpException) {
|
||||
Log.e("ERROR CODE", exception.code().toString())
|
||||
} catch (exception: Exception) {
|
||||
val toLog = if (exception is HttpException) {
|
||||
exception.code().toString()
|
||||
} else {
|
||||
exception.toString()
|
||||
}
|
||||
Log.e("GET ACCOUNT ERROR", toLog)
|
||||
return
|
||||
}
|
||||
//Open the account page in a separate activity
|
||||
|
@ -18,6 +18,12 @@ data class Attachment(
|
||||
|
||||
//Deprecated attributes
|
||||
val text_url: String? = null, //URL
|
||||
|
||||
//Pixelfed's Story upload response... TODO make the server return a regular Attachment?
|
||||
val msg: String? = null,
|
||||
val media_id: String? = null,
|
||||
val media_url: String? = null,
|
||||
val media_type: String? = null,
|
||||
) : Serializable {
|
||||
enum class AttachmentType: Serializable {
|
||||
unknown, image, gifv, video, audio
|
||||
|
@ -1,8 +1,10 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
@ -11,6 +13,7 @@ import androidx.core.net.toUri
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.posts.getDomain
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
@ -148,11 +151,13 @@ open class Status(
|
||||
)
|
||||
val file = path.toUri()
|
||||
|
||||
|
||||
|
||||
val shareIntent: Intent = Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, file)
|
||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
type = "image/$ext"
|
||||
type = file.getMimeType(context.contentResolver)
|
||||
}, null)
|
||||
|
||||
context.startActivity(shareIntent)
|
||||
|
@ -0,0 +1,43 @@
|
||||
package org.pixeldroid.app.utils.api.objects
|
||||
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import java.io.Serializable
|
||||
import java.time.Instant
|
||||
|
||||
data class StoryCarousel(
|
||||
val self: CarouselUserContainer?,
|
||||
val nodes: List<CarouselUserContainer?>?
|
||||
): Serializable
|
||||
|
||||
data class CarouselUser(
|
||||
val id: String?,
|
||||
val username: String?,
|
||||
val username_acct: String?,
|
||||
val avatar: String?, // URL to account avatar
|
||||
val local: Boolean?, // Is this story from the local instance?
|
||||
val is_author: Boolean?, // Is this me? (seems redundant with id)
|
||||
): Serializable
|
||||
|
||||
/**
|
||||
* Container with a description of the [user] and a list of stories ([nodes])
|
||||
*/
|
||||
data class CarouselUserContainer(
|
||||
val user: CarouselUser?,
|
||||
val nodes: List<Story?>?,
|
||||
): Serializable {
|
||||
constructor(user: UserDatabaseEntity, nodes: List<Story?>?) : this(
|
||||
CarouselUser(user.user_id, user.username, null, user.avatar_static,
|
||||
local = true,
|
||||
is_author = true
|
||||
), nodes)
|
||||
}
|
||||
|
||||
data class Story(
|
||||
val id: String?,
|
||||
val pid: String?, // id of author
|
||||
val type: String?, //TODO make enum of this? examples: "photo", ???
|
||||
val src: String?, // URL to photo of story
|
||||
val duration: Int?, //Time in seconds that the Story should be shown
|
||||
val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen
|
||||
val created_at: Instant?, //ISO 8601 Datetime
|
||||
): Serializable
|
@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
|
||||
PublicFeedStatusDatabaseEntity::class,
|
||||
Notification::class
|
||||
],
|
||||
version = 5
|
||||
version = 6
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@ -44,4 +44,9 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
||||
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
@ -13,41 +13,58 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
|
||||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
|
||||
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) {
|
||||
suspend fun addUser(
|
||||
db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
|
||||
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
|
||||
) {
|
||||
db.userDao().insertOrUpdate(
|
||||
UserDatabaseEntity(
|
||||
user_id = account.id!!,
|
||||
instance_uri = normalizeDomain(instance_uri),
|
||||
username = account.username!!,
|
||||
display_name = account.getDisplayName(),
|
||||
avatar_static = account.anyAvatar().orEmpty(),
|
||||
isActive = activeUser,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
UserDatabaseEntity(
|
||||
user_id = account.id!!,
|
||||
instance_uri = normalizeDomain(instance_uri),
|
||||
username = account.username!!,
|
||||
display_name = account.getDisplayName(),
|
||||
avatar_static = account.anyAvatar().orEmpty(),
|
||||
isActive = activeUser,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
db.userDao().updateUserAccountDetails(
|
||||
account.username.orEmpty(),
|
||||
account.display_name.orEmpty(),
|
||||
account.anyAvatar().orEmpty(),
|
||||
user.user_id,
|
||||
user.instance_uri
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(metadata?.config?.site?.url!!),
|
||||
title = metadata.config.site.name!!,
|
||||
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
|
||||
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE,
|
||||
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
|
||||
?: DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Pixelfed doesn't distinguish between max photo and video size
|
||||
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE,
|
||||
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
|
||||
?: DEFAULT_MAX_VIDEO_SIZE,
|
||||
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
|
||||
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED
|
||||
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED,
|
||||
pixelfed = metadata.software?.repo?.contains("pixelfed", ignoreCase = true) == true
|
||||
)
|
||||
} ?: instance?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(uri.orEmpty()),
|
||||
title = title.orEmpty(),
|
||||
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
|
||||
uri = normalizeDomain(uri.orEmpty()),
|
||||
title = title.orEmpty(),
|
||||
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
|
||||
pixelfed = false
|
||||
)
|
||||
} ?: throw IllegalArgumentException("Cannot store instance where both are null")
|
||||
|
||||
|
@ -1,27 +1,33 @@
|
||||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface InstanceDao {
|
||||
@Query("SELECT * FROM instances")
|
||||
fun getAll(): List<InstanceDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=:instanceUri")
|
||||
fun getInstance(instanceUri: String): InstanceDatabaseEntity
|
||||
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=(SELECT users.instance_uri FROM users WHERE isActive=1)")
|
||||
fun getActiveInstance(): InstanceDatabaseEntity
|
||||
|
||||
/**
|
||||
* Insert an instance, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
suspend fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
|
||||
@Update
|
||||
fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
suspend fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
|
||||
@Transaction
|
||||
fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
if (insertInstance(instance) == -1L) {
|
||||
updateInstance(instance)
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
@Dao
|
||||
@ -9,17 +15,21 @@ interface UserDao {
|
||||
* Insert a user, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertUser(user: UserDatabaseEntity): Long
|
||||
suspend fun insertUser(user: UserDatabaseEntity): Long
|
||||
|
||||
@Transaction
|
||||
fun insertOrUpdate(user: UserDatabaseEntity) {
|
||||
suspend fun insertOrUpdate(user: UserDatabaseEntity) {
|
||||
if (insertUser(user) == -1L) {
|
||||
updateUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Update
|
||||
fun updateUser(user: UserDatabaseEntity)
|
||||
suspend fun updateUser(user: UserDatabaseEntity)
|
||||
|
||||
@Query("UPDATE users SET username = :username, display_name = :displayName, avatar_static = :avatarStatic WHERE user_id = :id and instance_uri = :instanceUri")
|
||||
suspend fun updateUserAccountDetails(username: String, displayName: String, avatarStatic: String, id: String, instanceUri: String)
|
||||
|
||||
|
||||
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
|
||||
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
|
||||
@ -27,6 +37,9 @@ interface UserDao {
|
||||
@Query("SELECT * FROM users")
|
||||
fun getAll(): List<UserDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM users")
|
||||
fun getAllFlow(): Flow<List<UserDatabaseEntity>>
|
||||
|
||||
@Query("SELECT * FROM users WHERE isActive=1")
|
||||
fun getActiveUser(): UserDatabaseEntity?
|
||||
|
||||
|
@ -4,20 +4,22 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "instances")
|
||||
data class InstanceDatabaseEntity (
|
||||
@PrimaryKey var uri: String,
|
||||
var title: String,
|
||||
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
|
||||
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
|
||||
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Mastodon has different file limits for videos, default of 40MB
|
||||
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
|
||||
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
|
||||
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
|
||||
// Is video functionality enabled on this instance?
|
||||
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
|
||||
data class InstanceDatabaseEntity(
|
||||
@PrimaryKey var uri: String,
|
||||
var title: String,
|
||||
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
|
||||
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
|
||||
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Mastodon has different file limits for videos, default of 40MB
|
||||
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
|
||||
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
|
||||
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
|
||||
// Is video functionality enabled on this instance?
|
||||
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
|
||||
// Is this Pixelfed instance?
|
||||
var pixelfed: Boolean = true,
|
||||
) {
|
||||
companion object{
|
||||
companion object {
|
||||
// Default max number of chars for Mastodon: used when their is no other value supplied by
|
||||
// either NodeInfo or the instance endpoint
|
||||
const val DEFAULT_MAX_TOOT_CHARS = 500
|
||||
|
@ -6,13 +6,16 @@ import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.*
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class APIModule{
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class APIModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@ -54,7 +57,7 @@ class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase, val
|
||||
client_secret = user.clientSecret
|
||||
)
|
||||
}
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception){
|
||||
return null
|
||||
}
|
||||
|
||||
|
@ -1,29 +0,0 @@
|
||||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import dagger.Component
|
||||
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
||||
import org.pixeldroid.app.profile.EditProfileViewModel
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [ApplicationModule::class, DatabaseModule::class, APIModule::class])
|
||||
interface ApplicationComponent {
|
||||
fun inject(application: PixelDroidApplication?)
|
||||
fun inject(activity: BaseActivity?)
|
||||
fun inject(feedFragment: BaseFragment)
|
||||
fun inject(notificationsWorker: NotificationsWorker)
|
||||
fun inject(postCreationViewModel: PostCreationViewModel)
|
||||
fun inject(editProfileViewModel: EditProfileViewModel)
|
||||
|
||||
val context: Context?
|
||||
val application: Application?
|
||||
val database: AppDatabase
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
class ApplicationModule(app: Application) {
|
||||
private val mApplication: Application = app
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideContext(): Context {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideApplication(): Application {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
}
|
@ -5,20 +5,27 @@ import androidx.room.Room
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_3_4
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_4_5
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_5_6
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DatabaseModule(private val context: Context) {
|
||||
class DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDatabase(): AppDatabase {
|
||||
fun providesDatabase(
|
||||
@ApplicationContext applicationContext: Context
|
||||
): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
applicationContext,
|
||||
AppDatabase::class.java, "pixeldroid"
|
||||
).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5)
|
||||
).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
|
||||
.allowMainThreadQueries().build()
|
||||
}
|
||||
}
|
@ -32,9 +32,6 @@ import java.io.IOException
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
||||
|
||||
class NotificationsWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
@ -46,9 +43,6 @@ class NotificationsWorker(
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
||||
(applicationContext as PixelDroidApplication).getAppComponent().inject(this)
|
||||
|
||||
val users: List<UserDatabaseEntity> = db.userDao().getAll()
|
||||
|
||||
for (user in users){
|
||||
@ -306,8 +300,7 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString())
|
||||
} else {
|
||||
val types: MutableList<Notification.NotificationType?> =
|
||||
Notification.NotificationType.values().toMutableList()
|
||||
val types: MutableList<Notification.NotificationType?> = entries.toMutableList()
|
||||
types += null
|
||||
|
||||
types.forEach {
|
||||
|
19
app/src/main/res/color/selector_story_post.xml
Normal file
19
app/src/main/res/color/selector_story_post.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="?attr/colorSecondary"/>
|
||||
</selector>
|
20
app/src/main/res/color/selector_story_post_text.xml
Normal file
20
app/src/main/res/color/selector_story_post_text.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="?attr/colorOnSecondary"/>
|
||||
<item android:state_checked="false" android:color="?attr/colorOnSecondaryContainer"/>
|
||||
</selector>
|
5
app/src/main/res/drawable/arrow_forward.xml
Normal file
5
app/src/main/res/drawable/arrow_forward.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#FFFFFF" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"
|
||||
android:fillColor="?attr/colorOnBackground"/>
|
||||
</vector>
|
@ -1,8 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:viewportWidth="201.8771"
|
||||
android:viewportHeight="218.8104"
|
||||
android:width="254dp"
|
||||
android:height="275dp">
|
||||
android:viewportWidth="403.75"
|
||||
android:viewportHeight="437.6"
|
||||
android:width="100dp"
|
||||
android:height="108dp">
|
||||
<group android:translateX="100"
|
||||
android:translateY="115">
|
||||
|
||||
<group
|
||||
android:translateX="-1.41459"
|
||||
android:translateY="-24.00768">
|
||||
@ -808,4 +811,5 @@
|
||||
android:strokeColor="#000000"
|
||||
android:strokeWidth="1.32292"
|
||||
android:strokeLineCap="round" />
|
||||
</group>
|
||||
</vector>
|
File diff suppressed because one or more lines are too long
5
app/src/main/res/drawable/pause.xml
Normal file
5
app/src/main/res/drawable/pause.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/play.xml
Normal file
5
app/src/main/res/drawable/play.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
|
||||
</vector>
|
8
app/src/main/res/drawable/play_pause.xml
Normal file
8
app/src/main/res/drawable/play_pause.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@drawable/play"
|
||||
android:state_selected="true" />
|
||||
<item
|
||||
android:drawable="@drawable/pause"/>
|
||||
</selector>
|
5
app/src/main/res/drawable/story_play.xml
Normal file
5
app/src/main/res/drawable/story_play.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
|
||||
</vector>
|
@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"
|
||||
android:fillColor="?attr/colorOnBackground"/>
|
||||
</vector>
|
@ -1,136 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<ScrollView 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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fadeScrollbars="false">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:context=".settings.AboutActivity">
|
||||
|
||||
<ImageView
|
||||
android:importantForAccessibility="no"
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/mascot" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aboutAppName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="11dp"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aboutVersionNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aboutAppName"
|
||||
tools:text="v1.0.realversion" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aboutAppDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/license_info"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aboutVersionNumber" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/aboutWebsite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autoLink="web"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/project_website"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aboutAppDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contributeTranslationsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autoLink="web"
|
||||
android:drawablePadding="6dp"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/help_translate"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/aboutWebsite"
|
||||
app:drawableLeftCompat="@drawable/translate_black_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contributeTranslationsUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:textAlignment="center"
|
||||
android:text="https://weblate.pixeldroid.org"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contributeTranslationsText"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contributeForgeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:autoLink="web"
|
||||
android:drawablePadding="6dp"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/issues_contribute"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contributeTranslationsUrl"
|
||||
app:drawableLeftCompat="@drawable/bug_report_black_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contributeForgeUrl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web"
|
||||
android:textAlignment="center"
|
||||
android:text="https://gitlab.shinice.net/pixeldroid/PixelDroid"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contributeForgeText"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/licensesButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:text="@string/dependencies_licenses"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contributeForgeUrl" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
@ -1,13 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
tools:context=".postCreation.camera.CameraActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/camera_activity_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/top_bar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,14 +1,30 @@
|
||||
<?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"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".searchDiscover.TrendingActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id = "@+id/collectionFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,184 +1,211 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePic"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars"
|
||||
android:contentDescription="@string/profile_picture" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayoutName"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/profilePic">
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/nameEditText"
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/your_name"
|
||||
android:ems="10"
|
||||
android:imeOptions="actionDone" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:title="@string/edit_profile" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayoutBio"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bioEditText"
|
||||
android:layout_width="match_parent"
|
||||
<ImageView
|
||||
android:id="@+id/profilePic"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayoutName"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/your_bio" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/profilePic">
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/privateSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/privateText"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutBio" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/privateText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/privateSwitch"
|
||||
app:layout_constraintTop_toTopOf="@+id/privateSwitch"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/privateSwitch">
|
||||
<TextView
|
||||
android:id="@+id/privateTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/private_account"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/private_account_explanation"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/privateTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/privateTitle" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/more_profile_settings"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:icon="@drawable/ic_baseline_open_in_browser_24"
|
||||
app:layout_constraintTop_toBottomOf="@+id/privateText" />
|
||||
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/progressCard"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" >
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_margin="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/progressIcon"
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/nameEditText"
|
||||
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">
|
||||
<ProgressBar
|
||||
android:id="@+id/savingProgressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
android:ems="10"
|
||||
android:hint="@string/your_name"
|
||||
android:imeOptions="actionDone" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/error"
|
||||
app:tint="?attr/colorOnSecondaryContainer"
|
||||
android:src="@drawable/error"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:contentDescription="@string/profile_saved" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayoutBio"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/done"
|
||||
android:src="@drawable/check_circle_24"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bioEditText"
|
||||
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"
|
||||
android:contentDescription="@string/profile_saved" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:hint="@string/your_bio" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/privateSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/privateText"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutBio" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/privateText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/privateSwitch"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/privateSwitch"
|
||||
app:layout_constraintTop_toTopOf="@+id/privateSwitch">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressText"
|
||||
tools:text="@string/fetching_profile"
|
||||
android:id="@+id/privateTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:text="@string/private_account"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/progressIcon"/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/private_account_explanation"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/privateTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/privateTitle" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/more_profile_settings"
|
||||
app:icon="@drawable/ic_baseline_open_in_browser_24"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/privateText" />
|
||||
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/progressCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/progressIcon"
|
||||
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">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/savingProgressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/profile_saved"
|
||||
android:src="@drawable/error"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorOnSecondaryContainer" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/done"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/profile_saved"
|
||||
android:src="@drawable/check_circle_24"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progressText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/progressIcon"
|
||||
tools:text="@string/fetching_profile" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,5 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/followsFragment"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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="match_parent"/>
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/followsFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -21,7 +21,9 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mascotImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="508dp"
|
||||
android:layout_marginTop="-130dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:contentDescription="@string/mascot_description"
|
||||
@ -30,6 +32,7 @@
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/login_activity_instance_input_layout"
|
||||
android:layout_width="250dp"
|
||||
android:layout_marginTop="-130dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:hint="@string/domain_of_your_instance"
|
||||
|
@ -27,7 +27,7 @@
|
||||
android:id="@+id/main_drawer_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:background="?attr/colorSurfaceContainer"
|
||||
android:contentDescription="@string/open_drawer_menu"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_baseline_menu_24" />
|
||||
|
@ -5,75 +5,102 @@
|
||||
android:id="@+id/scrollview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".posts.PostActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsing_toolbar_layout"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/postFragmentSingle"
|
||||
layout="@layout/post_fragment" />
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/commentIn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/postFragmentSingle"
|
||||
tools:layout_editor_absoluteX="10dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout2"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/submitComment"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editComment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/comment"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text|textCapSentences|textMultiLine" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submitComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/submit_comment"
|
||||
android:text="@string/comment"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintPost"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar">
|
||||
|
||||
<include
|
||||
android:id="@+id/postFragmentSingle"
|
||||
layout="@layout/post_fragment" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/commentIn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintPost"
|
||||
tools:layout_editor_absoluteX="10dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/submitComment"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editComment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/comment_noun"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text|textCapSentences|textMultiLine" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submitComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/submit_comment"
|
||||
android:text="@string/comment_verb"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/commentFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/commentIn" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id="@+id/commentFragment"
|
||||
/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,9 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".postCreation.PostCreationActivity">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
@ -11,6 +13,7 @@
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
@ -18,4 +21,4 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/post_creation_graph" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -4,142 +4,154 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".profile.ProfileActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
android:id="@+id/profileMotion"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
app:layoutDescription="@xml/collapsing_motion_layout_scene">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsing_toolbar_layout"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/profile"
|
||||
android:elevation="-1dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePictureImageView"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:clickable="false"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbPostsTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:text="@string/default_nposts"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowersTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowers"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
|
||||
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowingTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowing"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountNameTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
android:clickable="false"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/no_username"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="visible"
|
||||
app:layout_collapseMode="parallax"
|
||||
app:layout_constraintTop_toBottomOf="@id/nbFollowersTextView"
|
||||
tools:visibility="visible">
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:clickable="false"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginRight="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePictureImageView"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
<Button
|
||||
android:id="@+id/followButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/follow"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbPostsTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nposts"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/edit_profile"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowersTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowers"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
|
||||
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowingTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowing"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountNameTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/no_username"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginRight="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/followButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/follow"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/edit_profile"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/profileTabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/profileTabs"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:background="?attr/colorSurface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/profileTabs"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,11 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
tools:context=".posts.ReportActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:background="?attr/colorSurface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".posts.ReportActivity">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:title="@string/report"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/report_target_textview"
|
||||
android:layout_width="wrap_content"
|
||||
@ -14,8 +32,8 @@
|
||||
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Reporting @user's post:" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/top_bar"
|
||||
tools:text="Report @user's post" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout"
|
||||
@ -95,4 +113,5 @@
|
||||
app:layout_constraintTop_toTopOf="@+id/reportButton" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
@ -1,13 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:title="@string/menu_settings" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:orientation="vertical">
|
||||
|
||||
|
||||
@ -26,5 +42,4 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
168
app/src/main/res/layout/activity_stories.xml
Normal file
168
app/src/main/res/layout/activity_stories.xml
Normal file
@ -0,0 +1,168 @@
|
||||
<?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:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/storyErrorCard"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="20dp"
|
||||
android:visibility="invisible"
|
||||
app:cardBackgroundColor="?attr/colorSecondaryContainer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:minHeight="48dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/storyErrorIcon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/error"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tint="?attr/colorOnSecondaryContainer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/storyErrorText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/storyErrorIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/storyErrorIcon"
|
||||
app:layout_constraintTop_toTopOf="@id/storyErrorIcon"
|
||||
tools:text="Something is wrong with stories" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/storyImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/story_image"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/story_progress_image"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
tools:scaleType="centerCrop"
|
||||
tools:srcCompat="@tools:sample/backgrounds/scenic[10]" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/pause"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/story_pause"
|
||||
android:src="@drawable/play_pause"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/storyAuthor"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/storyAge"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/story_progress_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/storyAuthorProfilePicture"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="12dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/story_progress_image"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/storyAuthor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toEndOf="@+id/storyAuthorProfilePicture"
|
||||
app:layout_constraintTop_toTopOf="@+id/storyAuthorProfilePicture"
|
||||
tools:text="username" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/storyAge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/storyAuthor"
|
||||
app:layout_constraintStart_toEndOf="@+id/storyAuthor"
|
||||
app:layout_constraintTop_toTopOf="@+id/storyAuthorProfilePicture"
|
||||
tools:text="48m" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/storyReplyField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:endIconContentDescription="TODO"
|
||||
app:endIconDrawable="@drawable/ic_send_blue"
|
||||
app:endIconMode="custom"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:hint="Reply to PixelDroid">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/viewRight"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/viewMiddle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
|
||||
|
||||
|
||||
<View
|
||||
android:id="@+id/viewMiddle"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
|
||||
app:layout_constraintEnd_toStartOf="@id/viewRight"
|
||||
app:layout_constraintStart_toEndOf="@id/viewLeft"
|
||||
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
|
||||
|
||||
|
||||
<View
|
||||
android:id="@+id/viewLeft"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
|
||||
app:layout_constraintEnd_toStartOf="@id/viewMiddle"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user