Merge master

This commit is contained in:
MarieJ 2024-01-11 19:55:00 +01:00
commit 0fe2c54940
159 changed files with 13080 additions and 2618 deletions

View File

@ -2,6 +2,7 @@ image: registry.gitlab.com/fdroid/fdroidserver:buildserver-bullseye
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_FORCE_HTTPS: "true"
before_script: before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle - export GRADLE_USER_HOME=`pwd`/.gradle
@ -19,7 +20,7 @@ before_script:
- test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH" - test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH"
- export GRADLE_USER_HOME=$PWD/.gradle - 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 - echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
- apt-get update || apt-get update - apt-get update || apt-get update

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "scrambler"] [submodule "scrambler"]
path = scrambler path = scrambler
url = https://gitlab.com/artectrex/scrambler.git url = https://gitlab.com/artectrex/scrambler.git
[submodule "pixel_common"]
path = pixel_common
url = git@gitlab.shinice.net:pixeldroid/pixel_common.git

View File

@ -1,41 +1,34 @@
import com.android.build.api.dsl.ManagedVirtualDevice import com.android.build.api.dsl.ManagedVirtualDevice
plugins {
id "com.mikepenz.aboutlibraries.plugin" version "10.5.2"
}
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco' apply plugin: 'jacoco'
apply plugin: "kotlin-parcelize" apply plugin: "kotlin-parcelize"
apply plugin: 'com.google.devtools.ksp'
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155
jacoco.toolVersion = "0.8.7"
android { android {
namespace 'org.pixeldroid.app' namespace 'org.pixeldroid.app'
compileSdkVersion 33 compileSdk 34
buildToolsVersion '33.0.0'
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
} }
androidResources {
generateLocaleConfig true
}
kotlin {
jvmToolchain(17)
}
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"] freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
} }
defaultConfig { defaultConfig {
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 33 versionCode 26
versionCode 24 targetSdkVersion 34
versionName "1.0.beta" + versionCode versionName "1.0.beta" + versionCode
//TODO add resConfigs("en", "fr", "ja",...) ? //TODO add resConfigs("en", "fr", "ja",...) ?
@ -87,8 +80,9 @@ android {
/** /**
* Make a string with the application_id (available in xml etc) * 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', 'application_id', variant.applicationId
variant.resValue "string", "versionName", variant.versionName
} }
testOptions { testOptions {
@ -113,11 +107,9 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
dataBinding = true
buildConfig = true buildConfig = true
} }
apply plugin: 'kotlin-kapt'
lint { lint {
//We can't expect translators to always keep up immediately: //We can't expect translators to always keep up immediately:
// don't fail if a a string is untranslated // don't fail if a a string is untranslated
@ -131,40 +123,40 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) 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: * AndroidX dependencies:
*/ */
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-splashscreen:1.0.0' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation "androidx.browser:browser:1.5.0" implementation "androidx.browser:browser:1.7.0"
implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.paging:paging-runtime-ktx:3.1.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1" implementation "androidx.lifecycle:lifecycle-common-java8:2.6.2"
implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.annotation:annotation:1.7.1"
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.activity:activity-ktx:1.8.2"
implementation 'androidx.fragment:fragment-ktx:1.5.6' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.media2:media2-widget:1.2.1' implementation 'androidx.media2:media2-widget:1.2.1'
implementation 'androidx.media2:media2-player:1.2.1' implementation 'androidx.media2:media2-player:1.2.1'
// Use the most recent version of CameraX // 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-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version" implementation "androidx.camera:camera-camera2:$cameraX_version"
// CameraX Lifecycle library // CameraX Lifecycle library
@ -173,9 +165,9 @@ dependencies {
// CameraX View class // CameraX View class
implementation "androidx.camera:camera-view:$cameraX_version" 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" 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-ktx:$room_version"
implementation "androidx.room:room-paging:$room_version" implementation "androidx.room:room-paging:$room_version"
@ -186,16 +178,13 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.google.android.material:material:1.8.0' implementation 'com.google.android.material:material:1.11.0'
//Dagger (dependency injection) //Dagger (dependency injection)
implementation 'com.google.dagger:dagger-android:2.45' implementation 'com.google.dagger:dagger:2.48'
implementation 'com.google.dagger:dagger-android-support:2.44' ksp 'com.google.dagger:dagger-compiler:2.48'
// 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.squareup.okhttp3:okhttp:4.9.3' implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
@ -203,11 +192,11 @@ dependencies {
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'com.github.connyduck:sparkbutton:4.1.0' implementation 'com.github.connyduck:sparkbutton:4.1.0'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.5'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.4'
implementation project(path: ':scrambler') 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" exclude group: "com.android.support"
} }
@ -216,9 +205,9 @@ dependencies {
// Excludes the support library because it's already included by Glide. // Excludes the support library because it's already included by Glide.
transitive = false transitive = false
} }
implementation 'com.github.bumptech.glide:annotations:4.14.2' implementation 'com.github.bumptech.glide:annotations:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
kapt 'com.github.bumptech.glide:compiler:4.14.2' ksp 'com.github.bumptech.glide:ksp:4.14.2'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@ -232,15 +221,10 @@ dependencies {
implementation 'com.mikepenz:iconics-views:5.4.0' implementation 'com.mikepenz:iconics-views:5.4.0'
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar' 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 'com.github.ligi:tracedroid:4.1'
implementation 'me.relex:circleindicator:2.1.6' implementation 'me.relex:circleindicator:2.1.6'
implementation 'com.mikepenz:aboutlibraries-core:10.6.0'
/** /**
* Not in release, so not mentioned in licenses list * Not in release, so not mentioned in licenses list
*/ */
@ -251,7 +235,7 @@ dependencies {
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1' androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1' androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1'
androidTestImplementation 'androidx.work:work-testing:2.8.1' androidTestImplementation 'androidx.work:work-testing:2.9.0'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
@ -271,7 +255,7 @@ dependencies {
} }
tasks.withType(Test) { tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*'] jacoco.excludes = ['jdk.internal.*']
} }

View File

@ -8,7 +8,7 @@
<intent <intent
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:targetPackage="org.pixeldroid.app.debug" 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" /> <categories android:name="android.shortcut.conversation" />
<capability-binding android:key="actions.intent.CREATE_MESSAGE" /> <capability-binding android:key="actions.intent.CREATE_MESSAGE" />
</shortcut> </shortcut>

View File

@ -5,14 +5,13 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <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.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-feature <uses-feature
android:name="android.hardware.camera.any" android:name="android.hardware.camera.any"
android:required="false" /> android:required="false" />
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
@ -47,7 +46,6 @@
android:theme="@style/AppTheme.ActionBar.Transparent" /> android:theme="@style/AppTheme.ActionBar.Transparent" />
<activity <activity
android:name=".profile.EditProfileActivity" android:name=".profile.EditProfileActivity"
android:exported="false" />
<activity <activity
android:name=".posts.MediaViewerActivity" android:name=".posts.MediaViewerActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
@ -69,12 +67,16 @@
<activity <activity
android:name=".posts.ReportActivity" android:name=".posts.ReportActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/BaseAppTheme"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity
android:name=".stories.StoriesActivity" />
<activity <activity
android:name=".postCreation.PostCreationActivity" android:name=".postCreation.PostCreationActivity"
android:exported="true" android:exported="true"
android:theme="@style/BaseAppTheme.NoActionBar" android:windowSoftInputMode="adjustResize"
android:windowSoftInputMode="adjustResize"> android:theme="@style/BaseAppTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@ -87,27 +89,32 @@
</activity> </activity>
<activity <activity
android:name=".profile.FollowsActivity" android:name=".profile.FollowsActivity"
android:theme="@style/BaseAppTheme"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity" android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
android:theme="@style/BaseAppTheme"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".posts.PostActivity" android:name=".posts.PostActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity"
android:theme="@style/BaseAppTheme" />
<activity <activity
android:name=".profile.ProfileActivity" android:name=".profile.ProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity"
<activity android:name=".profile.CollectionActivity" /> android:theme="@style/BaseAppTheme"/>
<activity android:name=".profile.CollectionActivity"
android:theme="@style/BaseAppTheme"/>
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings2" android:label="@string/title_activity_settings2"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:screenOrientation="sensorPortrait" android:theme="@style/BaseAppTheme" />
tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -132,7 +139,7 @@
android:name=".LoginActivity" android:name=".LoginActivity"
android:exported="true" android:exported="true"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/BaseAppTheme.NoActionBar" android:theme="@style/BaseAppTheme"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
<intent-filter> <intent-filter>
@ -149,6 +156,7 @@
<activity <activity
android:name=".searchDiscover.SearchActivity" android:name=".searchDiscover.SearchActivity"
android:exported="true" android:exported="true"
android:theme="@style/BaseAppTheme"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
@ -160,17 +168,8 @@
android:name="android.app.searchable" android:name="android.app.searchable"
android:resource="@xml/searchable" /> android:resource="@xml/searchable" />
</activity> </activity>
<activity android:name=".searchDiscover.TrendingActivity" /> <activity android:name=".searchDiscover.TrendingActivity"
<activity android:theme="@style/BaseAppTheme" />
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" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

@ -1,6 +1,5 @@
package org.pixeldroid.app package org.pixeldroid.app
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -16,7 +15,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.ActivityLoginBinding 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.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Application import org.pixeldroid.app.utils.api.objects.Application
import org.pixeldroid.app.utils.api.objects.Instance 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 { companion object {
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID private const val PACKAGE_ID = BuildConfig.APPLICATION_ID

View File

@ -12,6 +12,7 @@ import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@ -51,7 +52,7 @@ import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment
import org.pixeldroid.app.profile.ProfileActivity import org.pixeldroid.app.profile.ProfileActivity
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
import org.pixeldroid.app.settings.SettingsActivity 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.api.objects.Notification
import org.pixeldroid.app.utils.db.addUser import org.pixeldroid.app.utils.db.addUser
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
@ -66,7 +67,7 @@ import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFr
import java.time.Instant import java.time.Instant
class MainActivity : BaseThemedWithoutBarActivity() { class MainActivity : BaseActivity() {
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
@ -231,10 +232,7 @@ class MainActivity : BaseThemedWithoutBarActivity() {
nameRes = R.string.logout nameRes = R.string.logout
iconicsIcon = GoogleMaterial.Icon.gmd_close iconicsIcon = GoogleMaterial.Icon.gmd_close
}, },
primaryDrawerItem { )
nameRes = R.string.logout
iconicsIcon = GoogleMaterial.Icon.gmd_close
})
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position -> binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
when (position){ when (position){
1 -> launchActivity(ProfileActivity()) 1 -> launchActivity(ProfileActivity())
@ -244,6 +242,18 @@ class MainActivity : BaseThemedWithoutBarActivity() {
} }
false 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(){ private fun logOut(){
@ -486,16 +496,4 @@ class MainActivity : BaseThemedWithoutBarActivity() {
} }
startActivity(intent) 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()
}
}
} }

View File

@ -5,13 +5,13 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
const val TAG = "Post Creation Activity" const val TAG = "Post Creation Activity"
class PostCreationActivity : BaseThemedWithoutBarActivity() { class PostCreationActivity : BaseActivity() {
companion object { companion object {
internal const val PICTURE_DESCRIPTION = "picture_description" internal const val PICTURE_DESCRIPTION = "picture_description"

View File

@ -33,8 +33,10 @@ import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostCreationBinding import org.pixeldroid.app.databinding.FragmentPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseFragment 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.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension import org.pixeldroid.app.utils.fileExtension
@ -52,7 +54,7 @@ class PostCreationFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "") private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
private lateinit var binding: FragmentPostCreationBinding private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
private lateinit var model: PostCreationViewModel private lateinit var model: PostCreationViewModel
override fun onCreateView( override fun onCreateView(
@ -63,6 +65,7 @@ class PostCreationFragment : BaseFragment() {
// Inflate the layout for this fragment // Inflate the layout for this fragment
binding = FragmentPostCreationBinding.inflate(layoutInflater) binding = FragmentPostCreationBinding.inflate(layoutInflater)
return binding.root return binding.root
} }
@ -83,7 +86,8 @@ class PostCreationFragment : BaseFragment() {
requireActivity().intent.clipData!!, requireActivity().intent.clipData!!,
instance, instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION), requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false) requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false),
) )
} }
model = _model model = _model
@ -99,6 +103,7 @@ class PostCreationFragment : BaseFragment() {
) )
} }
) )
binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty()
} }
lifecycleScope.launch { lifecycleScope.launch {
@ -119,13 +124,26 @@ class PostCreationFragment : BaseFragment() {
binding.toolbarPostCreation.visibility = binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel 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 { binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)} layoutCarouselCallback = { model.becameCarousel(it)}
maxEntries = instance.albumLimit maxEntries = if(model.uiState.value.storyCreation) 1 else instance.albumLimit
addPhotoButtonCallback = { addPhotoButtonCallback = {
addPhoto() addPhoto()
} }
@ -133,9 +151,10 @@ class PostCreationFragment : BaseFragment() {
model.updateDescription(position, description) model.updateDescription(position, description)
} }
} }
// get the description and send the post
binding.postCreationSendButton.setOnClickListener { // Validate the post and go to the next step of the post creation process
if (validatePost() && model.isNotEmpty()) { binding.postCreationNextButton.setOnClickListener {
if (validatePost()) {
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment) findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
} }
} }
@ -163,6 +182,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 // Clean up temporary files, if any
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES) val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
tempFiles?.asList()?.forEach { tempFiles?.asList()?.forEach {
@ -275,14 +311,17 @@ class PostCreationFragment : BaseFragment() {
private fun validatePost(): Boolean { private fun validatePost(): Boolean {
if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) { if (model.getPhotoData().value?.none { it.video && it.videoEncodeComplete == false } == true) {
MaterialAlertDialogBuilder(requireActivity()).apply { // Encoding is done, i.e. none of the items are both a video and not done encoding.
setMessage(R.string.still_encoding) // We return true if the post is not empty, false otherwise.
setNegativeButton(android.R.string.ok) { _, _ -> } return model.getPhotoData().value?.isNotEmpty() == true
}.show()
return false
} }
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( private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(

View File

@ -22,6 +22,7 @@ import androidx.preference.PreferenceManager
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
import com.jarsilio.android.scrambler.stripMetadata import com.jarsilio.android.scrambler.stripMetadata
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -54,7 +55,6 @@ import kotlin.collections.forEach
import kotlin.collections.get import kotlin.collections.get
import kotlin.collections.getOrNull import kotlin.collections.getOrNull
import kotlin.collections.indexOfFirst import kotlin.collections.indexOfFirst
import kotlin.collections.isNotEmpty
import kotlin.collections.mutableListOf import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf import kotlin.collections.mutableMapOf
import kotlin.collections.plus import kotlin.collections.plus
@ -70,6 +70,7 @@ data class PostCreationActivityUiState(
val addPhotoButtonEnabled: Boolean = true, val addPhotoButtonEnabled: Boolean = true,
val editPhotoButtonEnabled: Boolean = true, val editPhotoButtonEnabled: Boolean = true,
val removePhotoButtonEnabled: Boolean = true, val removePhotoButtonEnabled: Boolean = true,
val maxEntries: Int?,
val isCarousel: Boolean = true, val isCarousel: Boolean = true,
@ -86,6 +87,11 @@ data class PostCreationActivityUiState(
val uploadErrorVisible: Boolean = false, val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "", val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false, val uploadErrorExplanationVisible: Boolean = false,
val storyCreation: Boolean,
val storyDuration: Int = 10,
val storyReplies: Boolean = true,
val storyReactions: Boolean = true,
) )
@Parcelize @Parcelize
@ -98,7 +104,7 @@ data class PhotoData(
var video: Boolean, var video: Boolean,
var videoEncodeProgress: Int? = null, var videoEncodeProgress: Int? = null,
var videoEncodeStabilizationFirstPass: Boolean? = null, var videoEncodeStabilizationFirstPass: Boolean? = null,
var videoEncodeComplete: Boolean = true, var videoEncodeComplete: Boolean? = null,
var videoEncodeError: Boolean = false, var videoEncodeError: Boolean = false,
) : Parcelable ) : Parcelable
@ -107,8 +113,10 @@ class PostCreationViewModel(
clipdata: ClipData? = null, clipdata: ClipData? = null,
val instance: InstanceDatabaseEntity? = null, val instance: InstanceDatabaseEntity? = null,
existingDescription: String? = null, existingDescription: String? = null,
existingNSFW: Boolean = false existingNSFW: Boolean = false,
storyCreation: Boolean = false,
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
private var storyPhotoDataBackup: MutableList<PhotoData>? = null
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy { private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also { MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) } it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
@ -128,7 +136,9 @@ class PostCreationViewModel(
_uiState = MutableStateFlow(PostCreationActivityUiState( _uiState = MutableStateFlow(PostCreationActivityUiState(
newPostDescriptionText = existingDescription ?: templateDescription, newPostDescriptionText = existingDescription ?: templateDescription,
nsfw = existingNSFW nsfw = existingNSFW,
maxEntries = if(storyCreation) 1 else instance?.albumLimit,
storyCreation = storyCreation
)) ))
} }
@ -145,35 +155,41 @@ class PostCreationViewModel(
} }
} }
/**
* Read-only public view on [photoData]
*/
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
/** /**
* Will add as many images as possible to [photoData], from the [clipData], and if * 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 * ([photoData].size + [clipData].itemCount) > 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. * 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(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
val dataToAdd: ArrayList<PhotoData> = arrayListOf() val dataToAdd: ArrayList<PhotoData> = arrayListOf()
var count = clipData.itemCount var count = clipData.itemCount
if(count + (previousList?.size ?: 0) > instance!!.albumLimit){ uiState.value.maxEntries?.let {
_uiState.update { currentUiState -> if(count + (previousList?.size ?: 0) > it){
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) _uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(it))
}
count = count.coerceAtMost(it - (previousList?.size ?: 0))
} }
count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0)) if (count + (previousList?.size ?: 0) >= it) {
} // Disable buttons to add more images
if (count + (previousList?.size ?: 0) >= instance.albumLimit) { _uiState.update { currentUiState ->
// Disable buttons to add more images currentUiState.copy(addPhotoButtonEnabled = false)
_uiState.update { currentUiState -> }
currentUiState.copy(addPhotoButtonEnabled = false) }
} for (i in 0 until count) {
} clipData.getItemAt(i).let {
for (i in 0 until count) { val sizeAndVideoPair: Pair<Long, Boolean> =
clipData.getItemAt(i).let { getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
val sizeAndVideoPair: Pair<Long, Boolean> = dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
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()))
} }
} }
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf() return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
} }
@ -185,18 +201,20 @@ class PostCreationViewModel(
* Returns the size of the file of the Uri, and whether it is a video, * 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. * 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 = val size: Long =
if (uri.scheme =="content") { if (uri.scheme =="content") {
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null) getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
?.use { cursor -> ?.use { cursor ->
/* Get the column indexes of the data in the Cursor, /* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data, * move to the first row in the Cursor, get the data,
* and display it. * and display it.
*/ */
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst() if(sizeIndex >= 0) {
cursor.getLong(sizeIndex) cursor.moveToFirst()
cursor.getLong(sizeIndex)
} else null
} ?: 0 } ?: 0
} else { } else {
uri.toFile().length() uri.toFile().length()
@ -213,6 +231,7 @@ class PostCreationViewModel(
} }
if ((!isVideo && sizeInkBytes > instance!!.maxPhotoSize) || (isVideo && sizeInkBytes > instance!!.maxVideoSize)) { 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 val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
@ -223,8 +242,6 @@ class PostCreationViewModel(
return Pair(size, isVideo) return Pair(size, isVideo)
} }
fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
fun updateDescription(position: Int, description: String) { fun updateDescription(position: Int, description: String) {
photoData.value?.getOrNull(position)?.imageDescription = description photoData.value?.getOrNull(position)?.imageDescription = description
photoData.value = photoData.value photoData.value = photoData.value
@ -234,8 +251,8 @@ class PostCreationViewModel(
photoData.value?.removeAt(currentPosition) photoData.value?.removeAt(currentPosition)
_uiState.update { _uiState.update {
it.copy( it.copy(
addPhotoButtonEnabled = true addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (uiState.value.maxEntries ?: 0),
) )
} }
photoData.value = photoData.value photoData.value = photoData.value
} }
@ -254,7 +271,7 @@ class PostCreationViewModel(
videoEncodeProgress = 0 videoEncodeProgress = 0
videoEncodeComplete = false videoEncodeComplete = false
VideoEditActivity.startEncoding(imageUri, it, VideoEditActivity.startEncoding(imageUri, null, it,
context = getApplication<PixelDroidApplication>(), context = getApplication<PixelDroidApplication>(),
registerNewFFmpegSession = ::registerNewFFmpegSession, registerNewFFmpegSession = ::registerNewFFmpegSession,
trackTempFile = ::trackTempFile, trackTempFile = ::trackTempFile,
@ -442,7 +459,10 @@ class PostCreationViewModel(
apiHolder.setToCurrentUser(it) apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser() } ?: 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 apiHolder.api = null
postSub = inter postSub = inter
@ -451,7 +471,11 @@ class PostCreationViewModel(
.subscribe( .subscribe(
{ attachment: Attachment -> { attachment: Attachment ->
data.progress = 0 data.progress = 0
data.uploadId = attachment.id!! data.uploadId = if(uiState.value.storyCreation){
attachment.media_id!!
} else {
attachment.id!!
}
}, },
{ e: Throwable -> { e: Throwable ->
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
@ -507,11 +531,23 @@ class PostCreationViewModel(
apiHolder.setToCurrentUser(it) apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser() } ?: apiHolder.api ?: apiHolder.setToCurrentUser()
api.postStatus( if(uiState.value.storyCreation){
statusText = description, val canReact = if (uiState.value.storyReactions) "1" else "0"
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(), val canReply = if (uiState.value.storyReplies) "1" else "0"
sensitive = nsfw
) 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(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success), Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java) val intent = Intent(getApplication(), MainActivity::class.java)
@ -551,10 +587,52 @@ class PostCreationViewModel(
fun chooseAccount(which: UserDatabaseEntity) { fun chooseAccount(which: UserDatabaseEntity) {
_uiState.update { it.copy(chosenAccount = which) } _uiState.update { it.copy(chosenAccount = which) }
} }
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 = getApplication<PixelDroidApplication>().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) } }
} }
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory { class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { 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) return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation)
} }
} }

View File

@ -20,10 +20,13 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.BaseFragment 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.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.setSquareImageFromURL import org.pixeldroid.app.utils.setSquareImageFromURL
import kotlin.math.roundToInt
class PostSubmissionFragment : BaseFragment() { class PostSubmissionFragment : BaseFragment() {
@ -34,7 +37,7 @@ class PostSubmissionFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: FragmentPostSubmissionBinding private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware()
private lateinit var model: PostCreationViewModel private lateinit var model: PostCreationViewModel
override fun onCreateView( override fun onCreateView(
@ -68,7 +71,8 @@ class PostSubmissionFragment : BaseFragment() {
requireActivity().intent.clipData!!, requireActivity().intent.clipData!!,
instance, instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION), requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false) requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false)
) )
} }
model = _model model = _model
@ -77,6 +81,18 @@ class PostSubmissionFragment : BaseFragment() {
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) 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 { lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState -> model.uiState.collect { uiState ->
@ -114,13 +130,24 @@ class PostSubmissionFragment : BaseFragment() {
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked -> binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateNSFW(isChecked) model.updateNSFW(isChecked)
} }
binding.storyRepliesSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateStoryReplies(isChecked)
}
binding.storyReactionsSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateStoryReactions(isChecked)
}
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars 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) setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview)
// Get the description and send the post // Get the description and send the post
binding.postCreationSendButton.setOnClickListener { binding.postSubmissionSendButton.setOnClickListener {
if (validatePost()) model.upload() if (validatePost()) model.upload()
} }
@ -179,13 +206,13 @@ class PostSubmissionFragment : BaseFragment() {
} }
private fun enableButton(enable: Boolean = true){ private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable binding.postSubmissionSendButton.isEnabled = enable
if(enable){ if(enable){
binding.postingProgressBar.visibility = View.GONE binding.postingProgressBar.visibility = View.GONE
binding.postCreationSendButton.visibility = View.VISIBLE binding.postSubmissionSendButton.visibility = View.VISIBLE
} else { } else {
binding.postingProgressBar.visibility = View.VISIBLE binding.postingProgressBar.visibility = View.VISIBLE
binding.postCreationSendButton.visibility = View.GONE binding.postSubmissionSendButton.visibility = View.GONE
} }
} }

View File

@ -5,47 +5,51 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.add_photo)
val cameraFragment = CameraFragment() val cameraFragment = CameraFragment()
val arguments = Bundle() val story: Boolean = intent.getBooleanExtra(CAMERA_ACTIVITY_STORY, false)
arguments.putBoolean("CameraActivity", true)
cameraFragment.arguments = arguments
supportFragmentManager.beginTransaction() if(story) supportActionBar?.setTitle(R.string.add_story)
.add(R.id.camera_activity_fragment, cameraFragment).commit() else supportActionBar?.setTitle(R.string.add_photo)
}
}
/** // If this CameraActivity wasn't started from the shortcut,
* Launch without arguments so that it will open the // tell the fragment it's in an activity (so that it sends back the result instead of
* [org.pixeldroid.app.postCreation.PostCreationActivity] instead of "returning" to a non-existent // starting a new post creation process)
* [org.pixeldroid.app.postCreation.PostCreationActivity] if (intent.action != "android.intent.action.VIEW") {
*/ val arguments = Bundle()
class CameraActivityShortcut : BaseThemedWithBarActivity() { arguments.putBoolean(CAMERA_ACTIVITY, true)
override fun onCreate(savedInstanceState: Bundle?) { arguments.putBoolean(CAMERA_ACTIVITY_STORY, story)
super.onCreate(savedInstanceState) cameraFragment.arguments = arguments
setContentView(R.layout.activity_camera) } else {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setTitle(R.string.new_post_shortcut_long)
supportActionBar?.setTitle(R.string.new_post_shortcut_long) }
val cameraFragment = CameraFragment()
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.camera_activity_fragment, cameraFragment).commit() .add(R.id.camera_activity_fragment, cameraFragment).commit()
} }
//Start a new MainActivity when "going back" on this activity
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
// If this CameraActivity wasn't started from the shortcut, behave as usual
if (intent.action != "android.intent.action.VIEW") return super.onOptionsItemSelected(item)
// Else, start a new MainActivity when "going back" on this activity
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)

View File

@ -34,13 +34,12 @@ import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentCameraBinding import org.pixeldroid.app.databinding.FragmentCameraBinding
import org.pixeldroid.app.postCreation.PostCreationActivity import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.bindingLifecycleAware
import java.io.File import java.io.File
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -61,7 +60,7 @@ class CameraFragment : BaseFragment() {
private val cameraLifecycleOwner = CameraLifecycleOwner() private val cameraLifecycleOwner = CameraLifecycleOwner()
private lateinit var binding: FragmentCameraBinding private var binding: FragmentCameraBinding by bindingLifecycleAware()
private var displayId: Int = -1 private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
@ -70,6 +69,7 @@ class CameraFragment : BaseFragment() {
private var camera: Camera? = null private var camera: Camera? = null
private var inActivity by Delegates.notNull<Boolean>() private var inActivity by Delegates.notNull<Boolean>()
private var addToStory by Delegates.notNull<Boolean>()
private var filePermissionDialogLaunched: Boolean = false private var filePermissionDialogLaunched: Boolean = false
@ -89,7 +89,8 @@ class CameraFragment : BaseFragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
super.onCreateView(inflater, container, savedInstanceState) 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) binding = FragmentCameraBinding.inflate(layoutInflater)
@ -106,7 +107,7 @@ class CameraFragment : BaseFragment() {
thumbnail.setPadding(10) thumbnail.setPadding(10)
// Load thumbnail into circular button using Glide // Load thumbnail into circular button using Glide
Glide.with(thumbnail) if(activity?.isDestroyed == false) Glide.with(thumbnail)
.load(uri) .load(uri)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.into(thumbnail) .into(thumbnail)
@ -337,7 +338,8 @@ class CameraFragment : BaseFragment() {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Don't allow multiple for story
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !addToStory)
uploadImageResultContract.launch( uploadImageResultContract.launch(
Intent.createChooser(this, null) Intent.createChooser(this, null)
) )
@ -464,15 +466,20 @@ class CameraFragment : BaseFragment() {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
if(inActivity){ if(inActivity && !addToStory){
requireActivity().setResult(Activity.RESULT_OK, intent) requireActivity().setResult(Activity.RESULT_OK, intent)
requireActivity().finish() requireActivity().finish()
} else { } else {
if(addToStory){
intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory)
}
startActivity(intent) startActivity(intent)
} }
} }
companion object { companion object {
const val CAMERA_ACTIVITY = "CameraActivity"
const val CAMERA_ACTIVITY_STORY = "CameraActivityStory"
private const val TAG = "CameraFragment" private const val TAG = "CameraFragment"
private const val RATIO_4_3_VALUE = 4.0 / 3.0 private const val RATIO_4_3_VALUE = 4.0 / 3.0

View File

@ -8,6 +8,6 @@ data class CarouselItem constructor(
val video: Boolean, val video: Boolean,
var encodeProgress: Int?, var encodeProgress: Int?,
var stabilizationFirstPass: Boolean?, var stabilizationFirstPass: Boolean?,
var encodeComplete: Boolean = false, var encodeComplete: Boolean? = null,
var encodeError: Boolean = false, var encodeError: Boolean = false,
) )

View File

@ -18,6 +18,9 @@ import androidx.recyclerview.widget.*
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ImageCarouselBinding import org.pixeldroid.app.databinding.ImageCarouselBinding
import me.relex.circleindicator.CircleIndicator2 import me.relex.circleindicator.CircleIndicator2
import org.pixeldroid.common.dpToPx
import org.pixeldroid.common.getSnapPosition
import org.pixeldroid.common.spToPx
class ImageCarousel( class ImageCarousel(
context: Context, context: Context,
@ -40,7 +43,6 @@ class ImageCarousel(
) )
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var tvCaption: TextView
private var snapHelper: SnapHelper = PagerSnapHelper() private var snapHelper: SnapHelper = PagerSnapHelper()
var indicator: CircleIndicator2? = null var indicator: CircleIndicator2? = null
@ -107,7 +109,7 @@ class ImageCarousel(
set(value) { set(value) {
field = 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) @Dimension(unit = Dimension.PX)
@ -115,7 +117,7 @@ class ImageCarousel(
set(value) { set(value) {
field = value field = value
tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat()) binding.tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
} }
var showIndicator = false var showIndicator = false
@ -245,14 +247,14 @@ class ImageCarousel(
showNavigationButtons = showNavigationButtons showNavigationButtons = showNavigationButtons
binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE 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 { } else {
recyclerView.layoutManager = GridLayoutManager(context, 3) recyclerView.layoutManager = GridLayoutManager(context, 3)
binding.btnNext.visibility = GONE binding.btnNext.visibility = GONE
binding.btnPrevious.visibility = GONE binding.btnPrevious.visibility = GONE
binding.editMediaDescriptionLayout.visibility = INVISIBLE binding.editMediaDescriptionLayout.visibility = INVISIBLE
tvCaption.visibility = INVISIBLE binding.tvCaption.visibility = INVISIBLE
} }
showIndicator = value showIndicator = value
@ -279,8 +281,7 @@ class ImageCarousel(
updateDescriptionCallback?.invoke(currentPosition, description) updateDescriptionCallback?.invoke(currentPosition, description)
} }
binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE 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) { set(value) {
if(!value.isNullOrEmpty()) { if(!value.isNullOrEmpty()) {
field = value field = value
tvCaption.text = value binding.tvCaption.text = value
} else { } else {
field = null 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) binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true)
recyclerView = binding.recyclerView recyclerView = binding.recyclerView
tvCaption = binding.tvCaption
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
// For marquee effect // For marquee effect
tvCaption.isSelected = true binding.tvCaption.isSelected = true
} }
@ -441,7 +441,7 @@ class ImageCarousel(
caption.apply { caption.apply {
if(layoutCarousel){ if(layoutCarousel){
binding.editMediaDescriptionLayout.visibility = INVISIBLE binding.editMediaDescriptionLayout.visibility = INVISIBLE
tvCaption.visibility = VISIBLE showCaption = true
} }
currentDescription = this currentDescription = this
} }
@ -472,7 +472,7 @@ class ImageCarousel(
} }
}) })
tvCaption.setOnClickListener { binding.tvCaption.setOnClickListener {
editingMediaDescription = true editingMediaDescription = true
} }
@ -562,7 +562,7 @@ class ImageCarousel(
binding.encodeInfoText.setText(R.string.encode_error) binding.encodeInfoText.setText(R.string.encode_error)
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error), binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
null, null, null) null, null, null)
} else if(it.encodeComplete){ } else if(it.encodeComplete == true){
binding.encodeInfoCard.visibility = VISIBLE binding.encodeInfoCard.visibility = VISIBLE
binding.encodeProgress.visibility = GONE binding.encodeProgress.visibility = GONE
binding.encodeInfoText.setText(R.string.encode_success) binding.encodeInfoText.setText(R.string.encode_success)

View File

@ -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)
}

View File

@ -1,12 +1,14 @@
package org.pixeldroid.app.posts package org.pixeldroid.app.posts
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity
import org.pixeldroid.app.databinding.ActivityAlbumBinding import org.pixeldroid.app.databinding.ActivityAlbumBinding
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Attachment import org.pixeldroid.app.utils.api.objects.Attachment
class AlbumActivity : BaseActivity() {
class AlbumActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityAlbumBinding.inflate(layoutInflater) val binding = ActivityAlbumBinding.inflate(layoutInflater)
@ -36,4 +38,17 @@ class AlbumActivity : BaseActivity() {
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setBackgroundDrawable(null) supportActionBar?.setBackgroundDrawable(null)
} }
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)
}
}
} }

View File

@ -11,6 +11,7 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId
@ -106,7 +107,7 @@ fun parseHTMLText(
override fun onClick(widget: View) { override fun onClick(widget: View) {
// Retrieve the account for the given profile // Retrieve the account for the given profile
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser() val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
openAccountFromId(accountId, api, context) 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 val now = Date.from(Instant.now()).time
try { try {
@ -140,7 +141,7 @@ fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Bool
android.text.format.DateUtils.SECOND_IN_MILLIS, android.text.format.DateUtils.SECOND_IN_MILLIS,
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString() 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 else formattedDate
} catch (e: ParseException) { } catch (e: ParseException) {

View File

@ -14,9 +14,9 @@ import androidx.media2.common.MediaMetadata
import androidx.media2.common.UriMediaItem import androidx.media2.common.UriMediaItem
import androidx.media2.player.MediaPlayer import androidx.media2.player.MediaPlayer
import org.pixeldroid.app.databinding.ActivityMediaviewerBinding 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 mediaPlayer: MediaPlayer
private lateinit var binding: ActivityMediaviewerBinding private lateinit var binding: ActivityMediaviewerBinding

View File

@ -96,11 +96,12 @@ class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
return super.onSingleTapConfirmed(e) return super.onSingleTapConfirmed(e)
} }
override fun onScroll( override fun onScroll(
e1: MotionEvent, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent,
distanceX: Float, distanceX: Float,
distanceY: Float distanceY: Float
): Boolean { ): Boolean {
if (e1 == null) return false
val orientation = parentViewPager?.orientation ?: return true val orientation = parentViewPager?.orientation ?: return true
val dx = e2.x - e1.x val dx = e2.x - e1.x

View File

@ -5,13 +5,15 @@ import android.util.Log
import android.view.View import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostBinding import org.pixeldroid.app.databinding.ActivityPostBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment 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_DOMAIN
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_STATUS_ID 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.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_COMMENT_TAG import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_COMMENT_TAG
@ -19,7 +21,7 @@ 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.api.objects.Status.Companion.VIEW_COMMENTS_TAG
import org.pixeldroid.app.utils.displayDimensionsInPx import org.pixeldroid.app.utils.displayDimensionsInPx
class PostActivity : BaseThemedWithBarActivity() { class PostActivity : BaseActivity() {
private lateinit var binding: ActivityPostBinding private lateinit var binding: ActivityPostBinding
private var commentFragment = CommentFragment() private var commentFragment = CommentFragment()
@ -30,7 +32,7 @@ class PostActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityPostBinding.inflate(layoutInflater) binding = ActivityPostBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
status = intent.getSerializableExtra(POST_TAG) as Status status = intent.getSerializableExtra(POST_TAG) as Status
@ -43,7 +45,10 @@ class PostActivity : BaseThemedWithBarActivity() {
val holder = StatusViewHolder(binding.postFragmentSingle) 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() activateCommenter()
initCommentsFragment(domain = user?.instance_uri.orEmpty()) initCommentsFragment(domain = user?.instance_uri.orEmpty())
@ -60,6 +65,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() { private fun activateCommenter() {
//Activate commenter //Activate commenter
binding.submitComment.setOnClickListener { binding.submitComment.setOnClickListener {

View File

@ -5,10 +5,10 @@ import android.view.View
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityReportBinding import org.pixeldroid.app.databinding.ActivityReportBinding
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
class ReportActivity : BaseThemedWithBarActivity() { class ReportActivity : BaseActivity() {
private lateinit var binding: ActivityReportBinding private lateinit var binding: ActivityReportBinding
@ -16,9 +16,9 @@ class ReportActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityReportBinding.inflate(layoutInflater) binding = ActivityReportBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.report)
val status = intent.getSerializableExtra(Status.POST_TAG) as Status? val status = intent.getSerializableExtra(Status.POST_TAG) as Status?

View File

@ -1,14 +1,16 @@
package org.pixeldroid.app.posts package org.pixeldroid.app.posts
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Looper import android.os.Looper
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
@ -17,6 +19,7 @@ import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.ActivityResultLauncher
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -36,10 +39,6 @@ import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar 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 kotlinx.coroutines.launch
import okhttp3.* import okhttp3.*
import okio.BufferedSink import okio.BufferedSink
@ -75,7 +74,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
private var status: Status? = null 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.itemView.visibility = View.VISIBLE
this.status = status this.status = status
@ -104,7 +107,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setupPost(picRequest, user.instance_uri, isActivity) setupPost(picRequest, user.instance_uri, isActivity)
activateButtons(pixelfedAPI, db, lifecycleScope, isActivity) activateButtons(pixelfedAPI, db, lifecycleScope, isActivity, requestPermissionDownloadPic)
} }
@ -139,8 +142,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setTextViewFromISO8601( setTextViewFromISO8601(
status?.created_at!!, status?.created_at!!,
binding.postDate, binding.postDate,
isActivity, isActivity
binding.root.context
) )
binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context) binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context)
@ -233,6 +235,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
db: AppDatabase, db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean, isActivity: Boolean,
requestPermissionDownloadPic: ActivityResultLauncher<String>,
){ ){
//Set the special HTML text //Set the special HTML text
setDescription(apiHolder, lifecycleScope) setDescription(apiHolder, lifecycleScope)
@ -262,7 +265,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
showComments(lifecycleScope, isActivity) showComments(lifecycleScope, isActivity)
activateMoreButton(apiHolder, db, lifecycleScope) activateMoreButton(apiHolder, db, lifecycleScope, requestPermissionDownloadPic)
} }
private fun activateReblogger( private fun activateReblogger(
@ -364,7 +367,12 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
return null 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 var bookmarked: Boolean? = null
binding.statusMore.setOnClickListener { binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply { PopupMenu(it.context, it).apply {
@ -402,50 +410,29 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
true true
} }
R.id.post_more_menu_save_to_gallery -> { R.id.post_more_menu_save_to_gallery -> {
Dexter.withContext(binding.root.context) // Check permissions on old Android versions: on new versions it is not
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) // needed when storing a file.
.withListener(object : BasePermissionListener() { if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(binding.root.context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) { requestPermissionDownloadPic.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
Toast.makeText( } else {
binding.root.context, status?.downloadImage(
binding.root.context.getString(R.string.write_permission_download_pic), binding.root.context,
Toast.LENGTH_SHORT status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
).show() ?: "",
} binding.root
)
override fun onPermissionGranted(p0: PermissionGrantedResponse?) { }
status?.downloadImage(
binding.root.context,
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
?: "",
binding.root
)
}
}).check()
true 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?) { R.id.post_more_menu_share_picture -> {
status?.downloadImage( status?.downloadImage(
binding.root.context, binding.root.context,
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
?: "", ?: "",
binding.root, binding.root,
share = true, share = true,
) )
}
}).check()
true true
} }
R.id.post_more_menu_delete -> { R.id.post_more_menu_delete -> {

View File

@ -6,13 +6,16 @@ import android.widget.ProgressBar
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter import androidx.paging.LoadStateAdapter
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,6 +23,7 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ErrorLayoutBinding import org.pixeldroid.app.databinding.ErrorLayoutBinding
import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel 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.FeedContent
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import retrofit2.HttpException import retrofit2.HttpException
@ -48,14 +52,28 @@ private fun showError(
internal fun <T: Any> initAdapter( internal fun <T: Any> initAdapter(
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout, progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout,
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding, 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 { swipeRefreshLayout.setOnRefreshListener {
adapter.refresh() adapter.refresh()
header?.refreshStories()
} }
adapter.addLoadStateListener { loadState -> adapter.addLoadStateListener { loadState ->
@ -80,6 +98,11 @@ internal fun <T: Any> initAdapter(
?: loadState.append as? LoadState.Error ?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error ?: loadState.prepend as? LoadState.Error
?: loadState.refresh as? LoadState.Error ?: loadState.refresh as? LoadState.Error
if(errorState?.error is CancellationException){
return@addLoadStateListener
}
errorState?.let { errorState?.let {
val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s -> val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s ->
try { try {
@ -143,6 +166,8 @@ class ReposLoadStateAdapter(
} }
} }
/** /**
* [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors * [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors
* in the loading of appending values. * in the loading of appending values.

View File

@ -18,8 +18,10 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import org.pixeldroid.app.databinding.FragmentFeedBinding import org.pixeldroid.app.databinding.FragmentFeedBinding
import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase 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.AppDatabase
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition 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 viewModel: FeedViewModel<T>
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder> 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 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() { internal fun initSearch() {
// Scroll to top when the list is refreshed from network. // Scroll to top when the list is refreshed from network.
lifecycleScope.launchWhenStarted { lifecycleScope.launchWhenStarted {
@ -73,7 +77,9 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
binding = FragmentFeedBinding.inflate(layoutInflater) binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(binding.progressBar, binding.swipeRefreshLayout, initAdapter(binding.progressBar, binding.swipeRefreshLayout,
binding.list, binding.motionLayout, binding.errorLayout, adapter) binding.list, binding.motionLayout, binding.errorLayout, adapter,
headerAdapter
)
return binding.root return binding.root
} }

View File

@ -221,8 +221,7 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
setTextViewFromISO8601( setTextViewFromISO8601(
it, it,
notificationTime, notificationTime,
false, false
itemView.context
) )
} }

View File

@ -47,7 +47,7 @@ class HomeFeedRemoteMediator @Inject constructor(
HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it) 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 { db.withTransaction {
// Clear table in the database // Clear table in the database

View File

@ -11,14 +11,14 @@ import androidx.paging.PagingDataAdapter
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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.StatusViewHolder
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment 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.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.FeedContentDatabase
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.displayDimensionsInPx import org.pixeldroid.app.utils.displayDimensionsInPx
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -38,14 +38,18 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = PostsAdapter(requireContext().displayDimensionsInPx()) home = requireArguments().getBoolean("home")
home = requireArguments().get("home") as Boolean adapter = PostsAdapter(requireContext().displayDimensionsInPx())
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
if (home){ if (home){
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T> mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
dao = db.homePostDao() as FeedContentDao<T> dao = db.homePostDao() as FeedContentDao<T>
headerAdapter = StoriesAdapter(lifecycleScope, apiHolder)
headerAdapter?.showStories = false
headerAdapter?.refreshStories()
} }
else { else {
mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T> mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
@ -55,7 +59,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View? { ): View? {
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
@ -70,6 +74,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
return view return view
} }
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>( inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<T>() { object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame (oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id 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) return StatusViewHolder.create(parent)
} }
override fun getItemViewType(position: Int): Int {
return R.layout.post_fragment
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status? holder.itemView.visibility = View.VISIBLE
uiModel?.let { holder.itemView.layoutParams =
(holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx) 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
)
}
} }
} }
} }

View File

@ -62,7 +62,7 @@ class PublicFeedRemoteMediator @Inject constructor(
val dbObjects = apiResponse.map{ val dbObjects = apiResponse.map{
PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it) 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 { db.withTransaction {
// Clear table in the database // Clear table in the database

View File

@ -61,8 +61,10 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
binding = FragmentFeedBinding.inflate(layoutInflater) binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(binding.progressBar, binding.swipeRefreshLayout, binding.list, initAdapter(
binding.motionLayout, binding.errorLayout, adapter) binding.progressBar, binding.swipeRefreshLayout, binding.list,
binding.motionLayout, binding.errorLayout, adapter
)
return binding.root return binding.root
} }

View File

@ -85,7 +85,9 @@ class UncachedPostsFragment : UncachedFeedFragment<Status>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { getItem(position)?.let {
(holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx) (holder as StatusViewHolder).bind(
it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
)
} }
} }
} }

View File

@ -2,17 +2,23 @@ package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
import android.os.Bundle import android.os.Bundle
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
class HashTagActivity : BaseThemedWithBarActivity() { class HashTagActivity : BaseActivity() {
private var tagFragment = UncachedPostsFragment() private var tagFragment = UncachedPostsFragment()
private lateinit var binding: ActivityFollowersBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_followers) binding = ActivityFollowersBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
// Get hashtag tag // Get hashtag tag

View File

@ -13,12 +13,12 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityCollectionBinding import org.pixeldroid.app.databinding.ActivityCollectionBinding
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION_ID 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.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Collection import org.pixeldroid.app.utils.api.objects.Collection
import java.lang.Exception import java.lang.Exception
class CollectionActivity : BaseThemedWithBarActivity() { class CollectionActivity : BaseActivity() {
private lateinit var binding: ActivityCollectionBinding private lateinit var binding: ActivityCollectionBinding
private lateinit var collection: Collection private lateinit var collection: Collection
@ -37,6 +37,7 @@ class CollectionActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityCollectionBinding.inflate(layoutInflater) binding = ActivityCollectionBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@ -19,10 +20,10 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityEditProfileBinding import org.pixeldroid.app.databinding.ActivityEditProfileBinding
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.openUrl import org.pixeldroid.app.utils.openUrl
class EditProfileActivity : BaseThemedWithBarActivity() { class EditProfileActivity : BaseActivity() {
private lateinit var model: EditProfileViewModel private lateinit var model: EditProfileViewModel
private lateinit var binding: ActivityEditProfileBinding private lateinit var binding: ActivityEditProfileBinding
@ -31,12 +32,29 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityEditProfileBinding.inflate(layoutInflater) binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.edit_profile)
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) } val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
model = _model 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
super.onBackPressedDispatcher.onBackPressed()
}
}
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState -> model.uiState.collect { uiState ->
@ -132,18 +150,6 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
return true 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){ when (item.itemId){
R.id.action_apply -> { R.id.action_apply -> {

View File

@ -2,20 +2,25 @@ package org.pixeldroid.app.profile
import android.os.Bundle import android.os.Bundle
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment 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
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG 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.ACCOUNT_TAG
import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG
class FollowsActivity : BaseThemedWithBarActivity() { class FollowsActivity : BaseActivity() {
private var followsFragment = AccountListFragment() private var followsFragment = AccountListFragment()
private lateinit var binding: ActivityFollowersBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_followers) binding = ActivityFollowersBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -17,7 +17,7 @@ import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.posts.parseHTMLText 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.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@ -25,7 +25,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class ProfileActivity : BaseThemedWithBarActivity() { class ProfileActivity : BaseActivity() {
private lateinit var domain : String private lateinit var domain : String
private lateinit var accountId : String private lateinit var accountId : String
@ -36,7 +36,10 @@ class ProfileActivity : BaseThemedWithBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater) binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -178,8 +178,10 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
deleteFromCollection deleteFromCollection
) )
} else { } else {
(holder as StatusViewHolder).bind(it as Status, apiHolder, db, (holder as StatusViewHolder).bind(
lifecycleScope, requireContext().displayDimensionsInPx()) it as Status, apiHolder, db, lifecycleScope,
requireContext().displayDimensionsInPx(), requestPermissionDownloadPic
)
} }
} }

View File

@ -9,17 +9,21 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.pixeldroid.app.R 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.UncachedPostsFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchAccountFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchAccountFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchHashtagFragment 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 import org.pixeldroid.app.utils.api.objects.Results
class SearchActivity : BaseThemedWithBarActivity() { class SearchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) val binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
var query = "" var query = ""

View File

@ -11,22 +11,24 @@ import androidx.core.content.ContextCompat
import org.pixeldroid.app.databinding.FragmentSearchBinding import org.pixeldroid.app.databinding.FragmentSearchBinding
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType 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.BaseFragment
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.bindingLifecycleAware
/** /**
* This fragment lets you search and use Pixelfed's Discover feature * This fragment lets you search and use Pixelfed's Discover feature
*/ */
class SearchDiscoverFragment : BaseFragment() { class SearchDiscoverFragment : BaseFragment() {
private lateinit var api: PixelfedAPI private lateinit var api: PixelfedAPI
var binding: FragmentSearchBinding by bindingLifecycleAware() var binding: FragmentSearchBinding by bindingLifecycleAware()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View { ): View {
binding = FragmentSearchBinding.inflate(inflater, container, false) binding = FragmentSearchBinding.inflate(inflater, container, false)
@ -56,4 +58,5 @@ class SearchDiscoverFragment : BaseFragment() {
intent.putExtra(TRENDING_TAG, type) intent.putExtra(TRENDING_TAG, type)
ContextCompat.startActivity(binding.root.context, intent, null) ContextCompat.startActivity(binding.root.context, intent, null)
} }
} }

View File

@ -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.accountLists.AccountViewHolder
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.HashTagViewHolder import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.HashTagViewHolder
import org.pixeldroid.app.profile.ProfilePostViewHolder 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.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment 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.api.objects.Tag
import org.pixeldroid.app.utils.setSquareImageFromURL import org.pixeldroid.app.utils.setSquareImageFromURL
class TrendingActivity : BaseThemedWithBarActivity() { class TrendingActivity : BaseActivity() {
private lateinit var binding: ActivityTrendingBinding private lateinit var binding: ActivityTrendingBinding
private lateinit var trendingAdapter : TrendingRecyclerViewAdapter private lateinit var trendingAdapter : TrendingRecyclerViewAdapter
@ -33,6 +33,7 @@ class TrendingActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityTrendingBinding.inflate(layoutInflater) binding = ActivityTrendingBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
val recycler = binding.list val recycler = binding.list
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}
}
}

View File

@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.content.res.XmlResourceParser import android.content.res.XmlResourceParser
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
@ -16,23 +17,39 @@ import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R 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 import org.pixeldroid.app.utils.setThemeFromPreferences
class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private var restartMainOnExit = false private var restartMainOnExit = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = SettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
setContentView(R.layout.settings)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.settings, SettingsFragment()) .replace(R.id.settings, SettingsFragment())
.commit() .commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true) 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) restartMainOnExit = intent.getBooleanExtra("restartMain", false)
} }
@ -51,25 +68,17 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
) )
} }
override fun onBackPressed() { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
// If a setting (for example language or theme) was changed, the main activity should be sharedPreferences?.let {
// started without history so that the change is applied to the whole back stack when (key) {
if (restartMainOnExit) { "theme" -> {
val intent = Intent(this, MainActivity::class.java) setThemeFromPreferences(it, resources)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK recreateWithRestartStatus()
super.startActivity(intent) }
} else {
super.onBackPressed() "themeColor" -> {
} recreateWithRestartStatus()
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"theme" -> {
setThemeFromPreferences(sharedPreferences, resources)
recreateWithRestartStatus()
}
"themeColor" -> {
recreateWithRestartStatus()
} }
} }
} }
@ -125,7 +134,8 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
class LanguageSettingFragment : DialogFragment() { class LanguageSettingFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val list: MutableList<String> = mutableListOf() 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 var eventType = it.eventType
while (eventType != XmlResourceParser.END_DOCUMENT) { while (eventType != XmlResourceParser.END_DOCUMENT) {
when (eventType) { when (eventType) {

View File

@ -0,0 +1,228 @@
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
import org.pixeldroid.app.utils.api.objects.Story
import org.pixeldroid.app.utils.api.objects.StoryCarousel
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 lateinit var model: StoriesViewModel
override fun onCreate(savedInstanceState: Bundle?) {
//force night mode always
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
super.onCreate(savedInstanceState)
val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as? StoryCarousel
val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID)
val selfCarousel: Array<Story>? = intent.getSerializableExtra(STORY_CAROUSEL_SELF) as? Array<Story>
binding = ActivityStoriesBinding.inflate(layoutInflater)
setContentView(binding.root)
val _model: StoriesViewModel by viewModels {
StoriesViewModelFactory(application, carousel, userId, selfCarousel?.asList())
}
model = _model
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()
}
}
}

View File

@ -0,0 +1,229 @@
package org.pixeldroid.app.stories
import android.app.Application
import android.os.CountDownTimer
import android.text.Editable
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
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.PixelDroidApplication
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 = ""
)
class StoriesViewModel(
application: Application,
val carousel: StoryCarousel?,
userId: String?,
val selfCarousel: List<Story>?
) : AndroidViewModel(application) {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private var currentAccount: CarouselUserContainer?
private val _uiState: MutableStateFlow<StoriesUiState>
val uiState: StateFlow<StoriesUiState>
val count = MutableLiveData<Float>()
private var timer: CountDownTimer? = null
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
currentAccount =
if (selfCarousel != null) {
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) }
} 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
}
class StoriesViewModelFactory(
val application: Application,
val carousel: StoryCarousel?,
val userId: String?,
val selfCarousel: List<Story>?
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java, List::class.java).newInstance(application, carousel, userId, selfCarousel)
}
}

View File

@ -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!
}
}
}

View File

@ -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?) {}
}

View File

@ -1,12 +1,11 @@
package org.pixeldroid.app.utils package org.pixeldroid.app.utils
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject import javax.inject.Inject
open class BaseActivity : AppCompatActivity() { open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
@Inject @Inject
lateinit var db: AppDatabase lateinit var db: AppDatabase
@ -19,7 +18,7 @@ open class BaseActivity : AppCompatActivity() {
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
} }

View File

@ -1,7 +1,10 @@
package org.pixeldroid.app.utils package org.pixeldroid.app.utils
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject import javax.inject.Inject
@ -22,4 +25,18 @@ open class BaseFragment: Fragment() {
(requireActivity().application as PixelDroidApplication).getAppComponent().inject(this) (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()
}
}
}
} }

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -24,6 +24,7 @@ fun setProfileImageFromURL(view : View, url : String?, image : ImageView) {
* @param image, the imageView into which we will load the image * @param image, the imageView into which we will load the image
*/ */
fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) { 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( Glide.with(view).load(url).placeholder(
blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32, 32) } blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32, 32) }
).apply(RequestOptions().centerCrop()).into(image) ).apply(RequestOptions().centerCrop()).into(image)

View File

@ -161,30 +161,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 @ColorInt
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK) fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)

View File

@ -23,6 +23,7 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.* import retrofit2.http.*
import retrofit2.http.Field import retrofit2.http.Field
import java.time.Instant import java.time.Instant
import java.util.concurrent.TimeUnit
/* /*
@ -51,7 +52,9 @@ interface PixelfedAPI {
.client( .client(
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor) OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS) // 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) .build().create(PixelfedAPI::class.java)
} }
@ -74,6 +77,7 @@ interface PixelfedAPI {
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor) OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS) // Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
.readTimeout(20, TimeUnit.SECONDS)
.authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder)) .authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder))
.addInterceptor { .addInterceptor {
it.request().newBuilder().run { it.request().newBuilder().run {
@ -161,6 +165,7 @@ interface PixelfedAPI {
@Field("poll[expires_in]") poll_expires: List<String>? = null, @Field("poll[expires_in]") poll_expires: List<String>? = null,
@Field("poll[multiple]") poll_multiple: List<String>? = null, @Field("poll[multiple]") poll_multiple: List<String>? = null,
@Field("poll[hide_totals]") poll_hideTotals: 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("sensitive") sensitive: Int? = null,
@Field("spoiler_text") spoiler_text: String? = null, @Field("spoiler_text") spoiler_text: String? = null,
@Field("visibility") visibility: String = "public", @Field("visibility") visibility: String = "public",
@ -231,6 +236,43 @@ interface PixelfedAPI {
@Query("post_id") post_id: String, @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 //Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context") @GET("/api/v1/statuses/{id}/context")
suspend fun statusComments( suspend fun statusComments(

View File

@ -57,11 +57,13 @@ data class Account(
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) { suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) {
val account = try { val account = try {
api.getAccount(id) api.getAccount(id)
} catch (exception: IOException) { } catch (exception: Exception) {
Log.e("GET ACCOUNT ERROR", exception.toString()) val toLog = if (exception is HttpException) {
return exception.code().toString()
} catch (exception: HttpException) { } else {
Log.e("ERROR CODE", exception.code().toString()) exception.toString()
}
Log.e("GET ACCOUNT ERROR", toLog)
return return
} }
//Open the account page in a separate activity //Open the account page in a separate activity

View File

@ -18,6 +18,12 @@ data class Attachment(
//Deprecated attributes //Deprecated attributes
val text_url: String? = null, //URL 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 { ) : Serializable {
enum class AttachmentType: Serializable { enum class AttachmentType: Serializable {
unknown, image, gifv, video, audio unknown, image, gifv, video, audio

View File

@ -1,8 +1,10 @@
package org.pixeldroid.app.utils.api.objects package org.pixeldroid.app.utils.api.objects
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.DownloadManager import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
@ -11,6 +13,7 @@ import androidx.core.net.toUri
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.posts.getDomain import org.pixeldroid.app.posts.getDomain
import org.pixeldroid.app.utils.getMimeType
import java.io.File import java.io.File
import java.io.Serializable import java.io.Serializable
import java.time.Instant import java.time.Instant
@ -148,11 +151,13 @@ open class Status(
) )
val file = path.toUri() val file = path.toUri()
val shareIntent: Intent = Intent.createChooser(Intent().apply { val shareIntent: Intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, file) putExtra(Intent.EXTRA_STREAM, file)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
type = "image/$ext" type = file.getMimeType(context.contentResolver)
}, null) }, null)
context.startActivity(shareIntent) context.startActivity(shareIntent)

View File

@ -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

View File

@ -10,6 +10,8 @@ import dagger.Component
import org.pixeldroid.app.directMessages.ui.main.ConversationsViewModel import org.pixeldroid.app.directMessages.ui.main.ConversationsViewModel
import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.stories.StoriesViewModel
import org.pixeldroid.app.stories.StoryCarouselViewHolder
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton import javax.inject.Singleton
@ -24,6 +26,7 @@ interface ApplicationComponent {
fun inject(postCreationViewModel: PostCreationViewModel) fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel) fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(editProfileViewModel: ConversationsViewModel) fun inject(editProfileViewModel: ConversationsViewModel)
fun inject(storiesViewModel: StoriesViewModel)
val context: Context? val context: Context?
val application: Application? val application: Application?

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -1,8 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" <vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="201.8771" android:viewportWidth="403.75"
android:viewportHeight="218.8104" android:viewportHeight="437.6"
android:width="254dp" android:width="100dp"
android:height="275dp"> android:height="108dp">
<group android:translateX="100"
android:translateY="115">
<group <group
android:translateX="-1.41459" android:translateX="-1.41459"
android:translateY="-24.00768"> android:translateY="-24.00768">
@ -808,4 +811,5 @@
android:strokeColor="#000000" android:strokeColor="#000000"
android:strokeWidth="1.32292" android:strokeWidth="1.32292"
android:strokeLineCap="round" /> android:strokeLineCap="round" />
</group>
</vector> </vector>

File diff suppressed because one or more lines are too long

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -1,13 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.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" xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
tools:context=".postCreation.camera.CameraActivity"> 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 <androidx.fragment.app.FragmentContainerView
android:id="@+id/camera_activity_fragment" android:id="@+id/camera_activity_fragment"
android:layout_width="match_parent" 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>

View File

@ -1,14 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
tools:context=".searchDiscover.TrendingActivity"> 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 <FrameLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id = "@+id/collectionFragment" android:id = "@+id/collectionFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,184 +1,211 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView <com.google.android.material.appbar.AppBarLayout
android:id="@+id/profilePic" android:layout_width="match_parent"
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"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:background="?attr/colorSecondaryContainer"
android:layout_marginTop="8dp" android:fitsSystemWindows="true"
android:layout_marginEnd="8dp" android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profilePic">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/nameEditText" android:id="@+id/top_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/your_name" android:minHeight="?attr/actionBarSize"
android:ems="10" app:title="@string/edit_profile" />
android:imeOptions="actionDone" /> </com.google.android.material.appbar.AppBarLayout>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/textInputLayoutBio" android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:layout_height="match_parent"
android:layout_width="0dp" app:layout_behavior="@string/appbar_scrolling_view_behavior">
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">
<com.google.android.material.textfield.TextInputEditText <ImageView
android:id="@+id/bioEditText" android:id="@+id/profilePic"
android:layout_width="match_parent" 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:layout_height="wrap_content"
android:hint="@string/your_bio" /> android:layout_marginStart="8dp"
</com.google.android.material.textfield.TextInputLayout> 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 <com.google.android.material.textfield.TextInputEditText
android:id="@+id/privateSwitch" android:id="@+id/nameEditText"
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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:ems="10"
app:layout_constraintStart_toStartOf="parent" android:hint="@string/your_name"
app:layout_constraintTop_toTopOf="parent"> android:imeOptions="actionDone" />
<ProgressBar </com.google.android.material.textfield.TextInputLayout>
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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/error" android:id="@+id/textInputLayoutBio"
app:tint="?attr/colorOnSecondaryContainer" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:src="@drawable/error" android:layout_width="0dp"
android:visibility="gone" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_marginStart="8dp"
android:layout_height="wrap_content" android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
android:contentDescription="@string/profile_saved" /> app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
<ImageView <com.google.android.material.textfield.TextInputEditText
android:id="@+id/done" android:id="@+id/bioEditText"
android:src="@drawable/check_circle_24" android:layout_width="match_parent"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:hint="@string/your_bio" />
app:layout_constraintStart_toStartOf="parent" </com.google.android.material.textfield.TextInputLayout>
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/profile_saved" /> <com.google.android.material.materialswitch.MaterialSwitch
</androidx.constraintlayout.widget.ConstraintLayout> 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 <TextView
android:id="@+id/progressText" android:id="@+id/privateTitle"
tools:text="@string/fetching_profile"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:text="@string/private_account"
app:layout_constraintEnd_toEndOf="parent" android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" 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> </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>

View File

@ -1,5 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/followsFragment" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" 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>

View File

@ -21,7 +21,9 @@
<ImageView <ImageView
android:id="@+id/mascotImage" 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_height="wrap_content"
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:contentDescription="@string/mascot_description" android:contentDescription="@string/mascot_description"
@ -30,6 +32,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/login_activity_instance_input_layout" android:id="@+id/login_activity_instance_input_layout"
android:layout_width="250dp" android:layout_width="250dp"
android:layout_marginTop="-130dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:hint="@string/domain_of_your_instance" android:hint="@string/domain_of_your_instance"

View File

@ -27,7 +27,7 @@
android:id="@+id/main_drawer_button" android:id="@+id/main_drawer_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurfaceContainer"
android:contentDescription="@string/open_drawer_menu" android:contentDescription="@string/open_drawer_menu"
android:padding="12dp" android:padding="12dp"
android:src="@drawable/ic_baseline_menu_24" /> android:src="@drawable/ic_baseline_menu_24" />

View File

@ -3,16 +3,27 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollview" android:id="@+id/scrollview"
android:fitsSystemWindows="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".posts.PostActivity"> tools:context=".posts.PostActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout" android:id="@+id/app_bar_layout"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<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_scrollFlags="scroll|enterAlways"/>
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.CollapsingToolbarLayout
android:background="?attr/colorSurface"
android:id="@+id/collapsing_toolbar_layout" android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
tools:context=".postCreation.PostCreationActivity"> tools:context=".postCreation.PostCreationActivity">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
@ -11,6 +13,7 @@
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface"
app:defaultNavHost="true" app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@ -18,4 +21,4 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/post_creation_graph" /> app:navGraph="@navigation/post_creation_graph" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -3,16 +3,27 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".profile.ProfileActivity"> tools:context=".profile.ProfileActivity">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout" android:id="@+id/app_bar_layout"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<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_scrollFlags="scroll|enterAlways"/>
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout" android:id="@+id/collapsing_toolbar_layout"
android:background="?attr/colorSurface"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"> app:layout_scrollFlags="scroll|exitUntilCollapsed">

View File

@ -1,11 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".posts.ReportActivity"> 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 <TextView
android:id="@+id/report_target_textview" android:id="@+id/report_target_textview"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -14,8 +32,8 @@
app:layout_constraintBottom_toTopOf="@+id/textInputLayout" app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@+id/top_bar"
tools:text="Reporting @user's post:" /> tools:text="Report @user's post" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout" android:id="@+id/textInputLayout"
@ -95,4 +113,5 @@
app:layout_constraintTop_toTopOf="@+id/reportButton" /> app:layout_constraintTop_toTopOf="@+id/reportButton" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,13 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:orientation="vertical"> android:orientation="vertical">
@ -26,5 +42,4 @@
</LinearLayout> </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View 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>

View File

@ -1,11 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/colorSecondaryContainer"
tools:context=".searchDiscover.TrendingActivity"> tools:context=".searchDiscover.TrendingActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="?attr/colorSurface"
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" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"
@ -25,7 +42,7 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toBottomOf="@+id/top_bar">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -58,4 +75,5 @@
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/errorLayout" <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:id="@+id/errorLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
tools:visibility="visible"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:showIn="@layout/fragment_feed" tools:showIn="@layout/fragment_feed"
xmlns:tools="http://schemas.android.com/tools"> tools:visibility="visible">
<ImageView <ImageView
android:id="@+id/imageView4" android:id="@+id/imageView4"

View File

@ -11,29 +11,81 @@
android:id="@+id/carousel" android:id="@+id/carousel"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:showCaption="true" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/buttonConstraints" app:layout_constraintTop_toBottomOf="@+id/top_bar"
app:layout_constraintTop_toTopOf="parent"/> app:showCaption="true" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints" android:id="@+id/top_bar"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:minHeight="?attr/actionBarSize"
app:layout_constraintBottom_toBottomOf="parent" android:theme="?attr/actionBarTheme"
app:layout_constraintEnd_toEndOf="parent"> android:background="?attr/colorSecondaryContainer"
app:titleTextColor="?attr/colorOnSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Button <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/post_creation_send_button" android:id="@+id/backbutton"
android:layout_width="match_parent" android:layout_width="48dp"
android:layout_height="match_parent" android:layout_height="48dp"
android:enabled="true" android:layout_marginStart="4dp"
android:text="@string/upload_next_step" android:tint="?attr/colorOnSecondaryContainer"
android:visibility="visible" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@android:string/cancel"
android:src="?attr/homeAsUpIndicator"
android:tooltipText='@android:string/cancel'
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/toggleStoryPost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:checkedButton="@+id/buttonPost"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/post_creation_next_button"
app:layout_constraintStart_toEndOf="@id/backbutton"
app:layout_constraintTop_toTopOf="parent"
app:selectionRequired="true"
app:singleSelection="true">
<Button
android:id="@+id/buttonStory"
style="?attr/materialButtonOutlinedStyle"
app:backgroundTint="@color/selector_story_post"
android:textColor="@color/selector_story_post_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/type_story" />
<Button
android:id="@+id/buttonPost"
style="?attr/materialButtonOutlinedStyle"
app:backgroundTint="@color/selector_story_post"
android:textColor="@color/selector_story_post_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/type_post" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<Button
style="@style/Widget.Material3.Button.TextButton.Icon"
app:icon="@drawable/arrow_forward"
app:iconGravity="end"
android:id="@+id/post_creation_next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="true"
android:text="@string/continue_post_creation"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -44,7 +96,7 @@
android:minHeight="?attr/actionBarSize" android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toBottomOf="@id/top_bar">
<androidx.appcompat.widget.AppCompatImageButton <androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/savePhotoButton" android:id="@+id/savePhotoButton"
@ -53,8 +105,8 @@
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/save_to_gallery" android:contentDescription="@string/save_to_gallery"
android:tooltipText='@string/save_to_gallery'
android:src="@drawable/download_file_30dp" android:src="@drawable/download_file_30dp"
android:tooltipText='@string/save_to_gallery'
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -66,8 +118,8 @@
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/delete" android:contentDescription="@string/delete"
android:tooltipText='@string/delete'
android:src="@drawable/delete_30dp" android:src="@drawable/delete_30dp"
android:tooltipText='@string/delete'
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/savePhotoButton" app:layout_constraintStart_toEndOf="@+id/savePhotoButton"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -79,8 +131,8 @@
android:layout_marginStart="30dp" android:layout_marginStart="30dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/edit" android:contentDescription="@string/edit"
android:tooltipText='@string/edit'
android:src="@drawable/ic_baseline_edit_30" android:src="@drawable/ic_baseline_edit_30"
android:tooltipText='@string/edit'
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/removePhotoButton" app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -92,8 +144,8 @@
android:layout_marginEnd="30dp" android:layout_marginEnd="30dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/add_photo" android:contentDescription="@string/add_photo"
android:tooltipText='@string/add_photo'
android:src="@drawable/add_photo_button" android:src="@drawable/add_photo_button"
android:tooltipText='@string/add_photo'
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View File

@ -12,7 +12,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" android:minHeight="?attr/actionBarSize"
android:background="?attr/colorSecondaryContainer"
android:theme="?attr/actionBarTheme" android:theme="?attr/actionBarTheme"
app:titleTextColor="?attr/colorOnSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -76,6 +78,7 @@
app:layout_constraintHorizontal_bias="0.498" app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view" app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" /> app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
@ -103,7 +106,7 @@
app:layout_constraintEnd_toEndOf="parent"> app:layout_constraintEnd_toEndOf="parent">
<Button <Button
android:id="@+id/post_creation_send_button" android:id="@+id/post_submission_send_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:enabled="true" android:enabled="true"
@ -182,4 +185,84 @@
app:layout_constraintStart_toEndOf="@+id/nsfwSwitch" app:layout_constraintStart_toEndOf="@+id/nsfwSwitch"
app:layout_constraintTop_toTopOf="@+id/nsfwSwitch" /> app:layout_constraintTop_toTopOf="@+id/nsfwSwitch" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/storyOptions"
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"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/story_duration_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
android:text="@string/story_duration"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.slider.Slider
android:id="@+id/story_duration_slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:stepSize="1.0"
android:valueFrom="3.0"
android:valueTo="15.0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/story_duration_title" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/storyRepliesSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/story_duration_slider"
app:layout_constraintTop_toBottomOf="@+id/story_duration_slider" />
<TextView
android:id="@+id/storyRepliesTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Allow replies"
android:textStyle="bold"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/storyRepliesSwitch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/storyRepliesSwitch"
app:layout_constraintTop_toTopOf="@+id/storyRepliesSwitch" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/storyReactionsSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/storyRepliesSwitch"
app:layout_constraintTop_toBottomOf="@+id/storyRepliesSwitch" />
<TextView
android:id="@+id/storyReactionsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Allow reactions"
android:textStyle="bold"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="@+id/storyReactionsSwitch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/storyReactionsSwitch"
app:layout_constraintTop_toTopOf="@+id/storyReactionsSwitch"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:fillViewport="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
android:layout_gravity="center_horizontal"> <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SearchView <androidx.appcompat.widget.SearchView
android:id="@+id/search" android:id="@+id/search"
@ -26,10 +29,10 @@
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/trendingCardView" android:id="@+id/trendingCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer" app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toTopOf="@+id/hashtagsCardView" app:layout_constraintBottom_toTopOf="@+id/hashtagsCardView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -45,28 +48,28 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/trending_posts" android:text="@string/trending_posts"
android:textAppearance="?attr/textAppearanceTitleLarge" android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer" android:textColor="?attr/colorOnSecondaryContainer"
app:drawableLeftCompat="@drawable/baseline_auto_graph_24" /> app:drawableLeftCompat="@drawable/baseline_auto_graph_24" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/daily_trending" android:text="@string/daily_trending"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" /> android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/hashtagsCardView" android:id="@+id/hashtagsCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer" app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toTopOf="@id/accountsCardView" app:layout_constraintBottom_toTopOf="@id/accountsCardView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -82,33 +85,33 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/trending_hashtags" android:text="@string/trending_hashtags"
android:textAppearance="?attr/textAppearanceTitleLarge" android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer" android:textColor="?attr/colorOnSecondaryContainer"
app:drawableStartCompat="@drawable/baseline_tag" /> app:drawableStartCompat="@drawable/baseline_tag" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/explore_hashtags" android:text="@string/explore_hashtags"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" /> android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/accountsCardView" android:id="@+id/accountsCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer" app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintTop_toBottomOf="@id/hashtagsCardView" app:layout_constraintBottom_toTopOf="@id/discoverCardView"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/discoverCardView"> app:layout_constraintTop_toBottomOf="@id/hashtagsCardView">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -119,33 +122,33 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/popular_accounts" android:text="@string/popular_accounts"
android:textAppearance="?attr/textAppearanceTitleLarge" android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer" android:textColor="?attr/colorOnSecondaryContainer"
app:drawableStartCompat="@drawable/baseline_person_add" /> app:drawableStartCompat="@drawable/baseline_person_add" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/explore_accounts" android:text="@string/explore_accounts"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" /> android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/discoverCardView" android:id="@+id/discoverCardView"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp" android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer" app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintTop_toBottomOf="@id/accountsCardView" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintTop_toBottomOf="@id/accountsCardView">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -156,22 +159,23 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawablePadding="4dp"
android:text="@string/discover" android:text="@string/discover"
android:textAppearance="?attr/textAppearanceTitleLarge" android:textAppearance="?attr/textAppearanceTitleLarge"
android:drawablePadding="4dp"
android:textColor="?attr/colorOnSecondaryContainer" android:textColor="?attr/colorOnSecondaryContainer"
app:drawableStartCompat="@drawable/explore_24dp" /> app:drawableStartCompat="@drawable/explore_24dp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/explore_posts" android:text="@string/explore_posts"
android:textColor="?attr/colorOnSecondaryContainer"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
android:layout_marginTop="8dp" /> android:textColor="?attr/colorOnSecondaryContainer" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -1,41 +0,0 @@
<?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="wrap_content"
android:paddingHorizontal="@dimen/spacing_xlarge"
android:paddingBottom="@dimen/spacing_xlarge">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/spacing_xlarge"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="24dp" />
<TextView
android:id="@+id/copyright"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/spacing_large"
android:autoLink="web"
android:linksClickable="true"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:layout_editor_absoluteX="24dp" />
<TextView
android:id="@+id/url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/spacing_large"
android:autoLink="web"
android:linksClickable="true"
app:layout_constraintTop_toBottomOf="@+id/copyright"
tools:layout_editor_absoluteX="24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/open_source_license_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>

View File

@ -1,9 +1,27 @@
<LinearLayout 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_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>
<FrameLayout <FrameLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/settings" android:id="@+id/settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</LinearLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/story_carousel"
android:layout_width="match_parent"
android:paddingVertical="8dp"
android:layout_height="216dp"
tools:listitem="@layout/story_carousel_item"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="horizontal"
app:layoutManager="com.google.android.material.carousel.CarouselLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search" />

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.carousel.MaskableFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/carousel_item_container"
android:layout_width="120dp"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
tools:context="androidx.recyclerview.widget.RecyclerView"
android:layout_marginEnd="4dp"
android:foreground="?attr/selectableItemBackground"
app:shapeAppearance="?attr/shapeAppearanceCornerExtraLarge">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/carousel_image_view"
android:contentDescription="@string/story_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<ImageView
android:id="@+id/storyAuthorProfilePicture"
android:contentDescription="@string/profile_picture"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginBottom="6dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintStart_toStartOf="@id/username"
tools:srcCompat="@tools:sample/avatars[3]" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="pixeldroid" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.carousel.MaskableFrameLayout>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.carousel.MaskableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/carousel_add_story"
android:layout_width="120dp"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="?attr/colorStoryImage"
app:shapeAppearance="?attr/shapeAppearanceCornerExtraLarge"
tools:context="androidx.recyclerview.widget.RecyclerView">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/carousel_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/story_image"
android:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/add_story"
android:foreground="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/my_story"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/carousel_add_story_icon"
android:layout_width="match_parent"
android:layout_height="40dp"
android:contentDescription="@string/add_story"
android:scaleType="fitCenter"
android:src="@drawable/collection_add"
app:layout_constraintBottom_toTopOf="@id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnStoryImage" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_story"
android:textColor="?attr/colorOnStoryImage"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/carousel_add_story_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/my_story"
android:foreground="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_story"
tools:visibility="visible">
<ImageView
android:id="@+id/my_story_icon"
android:layout_width="match_parent"
android:layout_height="40dp"
android:contentDescription="@string/my_story"
android:scaleType="fitCenter"
android:src="@drawable/story_play"
app:layout_constraintBottom_toTopOf="@id/my_story_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnStoryImage" />
<TextView
android:id="@+id/my_story_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/my_story"
android:textColor="?attr/colorOnStoryImage"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/my_story_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.carousel.MaskableFrameLayout>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/mascot" />

View File

@ -0,0 +1 @@
unqualifiedResLocale=en-US

View File

@ -52,7 +52,6 @@
<string name="comment_posted">التعليق: تم نشر%1$s!</string> <string name="comment_posted">التعليق: تم نشر%1$s!</string>
<string name="comment_error">خطأ في التعليق!</string> <string name="comment_error">خطأ في التعليق!</string>
<string name="share_image">مشاركة الصورة</string> <string name="share_image">مشاركة الصورة</string>
<string name="write_permission_share_pic">يجب عليك منح تصريح للكتابة قصد مشاركة الصور!</string>
<string name="write_permission_download_pic">تحتاج إلى منح إذن الكتابة لتنزيل الصور!</string> <string name="write_permission_download_pic">تحتاج إلى منح إذن الكتابة لتنزيل الصور!</string>
<string name="empty_comment">لا يجب ان يكون التعليق فارغًا!</string> <string name="empty_comment">لا يجب ان يكون التعليق فارغًا!</string>
<string name="posted_on">نُشِر في %1$s</string> <string name="posted_on">نُشِر في %1$s</string>

View File

@ -60,7 +60,6 @@
<string name="comment_posted">Comentari: %1$s publicat!</string> <string name="comment_posted">Comentari: %1$s publicat!</string>
<string name="comment_error">Error de comentari!</string> <string name="comment_error">Error de comentari!</string>
<string name="share_image">Compartir imatge</string> <string name="share_image">Compartir imatge</string>
<string name="write_permission_share_pic">Necessites concedir permís descriptura per compartir imatges!</string>
<string name="write_permission_download_pic">Has de concedir permís descriptura per baixar imatges!</string> <string name="write_permission_download_pic">Has de concedir permís descriptura per baixar imatges!</string>
<string name="empty_comment">El comentari no ha de estar buit!</string> <string name="empty_comment">El comentari no ha de estar buit!</string>
<string name="posted_on">Publica\'t el %1$s</string> <string name="posted_on">Publica\'t el %1$s</string>

View File

@ -69,7 +69,6 @@
<string name="posted_on">Zveřejněno na %1$s</string> <string name="posted_on">Zveřejněno na %1$s</string>
<string name="NoCommentsToShow">U tohoto příspěvku nejsou žádné komentáře…</string> <string name="NoCommentsToShow">U tohoto příspěvku nejsou žádné komentáře…</string>
<string name="empty_comment">Komentář nesmí být prázdný!</string> <string name="empty_comment">Komentář nesmí být prázdný!</string>
<string name="write_permission_share_pic">Pro sdílení obrázků musíte udělit práva k zápisu!</string>
<string name="share_image">Sdílet obrázek</string> <string name="share_image">Sdílet obrázek</string>
<string name="comment_error">Chyba komentáře!</string> <string name="comment_error">Chyba komentáře!</string>
<string name="comment_posted">Komentář: %1$s zveřejněn!</string> <string name="comment_posted">Komentář: %1$s zveřejněn!</string>

Some files were not shown because too many files have changed in this diff Show More