Compare commits
57 Commits
1.0.beta27
...
master
Author | SHA1 | Date |
---|---|---|
Matthieu | 187c29a751 | |
Balaraz | f9a07a5dd0 | |
Alexandre NICOLADIE | fb9296187a | |
Matthieu | 04324577ea | |
Matthieu | 73f08e5a5f | |
Matthieu | a7feab380b | |
Matthieu | 1a4e023091 | |
Regu_Miabyss | 2fb4c91ffd | |
Matthieu | 720c099f0a | |
Matthieu | 869479d53f | |
Matthieu | 018f893388 | |
Matthieu | f27ae611be | |
Matthieu | 234be72f59 | |
Matthieu | 7eaac2e903 | |
Matthieu | a9849c13e6 | |
Matthieu | d1562f18e9 | |
Matthieu | 4a1248bcab | |
Regu_Miabyss | e9122e6d72 | |
Matthieu | 76eac62d73 | |
Matthieu | dd27555d83 | |
Matthieu | c0feb8a37d | |
Matthieu | 7acd4cface | |
Matthieu | 10e93c90b7 | |
Matthieu | 2311627473 | |
Regu_Miabyss | 6c7ab2333e | |
Matthieu | 54711d4e81 | |
Matthieu | 908d1a54c9 | |
Matthieu | 2b91543137 | |
Matthieu | 0688dd4d02 | |
Matthieu | 319da7c11c | |
Matthieu | 7b327fc0d6 | |
Matthieu | 64ab2c2ac5 | |
Matthieu | 37b83f5ae2 | |
Matthieu | 1516452ab5 | |
Matthieu | cb50db7730 | |
Matthieu | afe6f71152 | |
Matthieu | d66c365934 | |
Matthieu | 7815ecba08 | |
Matthieu | b4533014b3 | |
Matthieu | 905c1c2d66 | |
Matthieu | 46ee92a19f | |
Matthieu | ed7ff877fb | |
Francesco Marinucci | 5c3b231e16 | |
Matthieu | 80e021e1a2 | |
Matthieu | 0aa3d86c11 | |
Matthieu | 05cb615f15 | |
Matthieu | 9313f321cd | |
Matthieu | 175438115d | |
Matthieu | f2600b85e2 | |
Matthieu | 711a5b310f | |
Francesco Marinucci | 0192190c6d | |
Matthieu | bb935e73ad | |
Matthieu | ddf1b273de | |
Matthieu | 14dee5463e | |
Matthieu | 416d36b1a8 | |
Fred | f15c23cceb | |
Fred | d9da842df7 |
|
@ -3,4 +3,4 @@
|
|||
url = https://gitlab.com/artectrex/scrambler.git
|
||||
[submodule "pixel_common"]
|
||||
path = pixel_common
|
||||
url = git@gitlab.shinice.net:pixeldroid/pixel_common.git
|
||||
url = https://gitlab.shinice.net/pixeldroid/pixel_common.git
|
||||
|
|
|
@ -9,13 +9,16 @@ Free (as in freedom) Android client for Pixelfed, the federated image sharing pl
|
|||
<img src="https://pixeldroid.org/badge-fdroid.png" alt="Get it on F-Droid" width="206">
|
||||
</a>
|
||||
|
||||
Come talk to us on Matrix, at <a href="https://matrix.to/#/#pixeldroid:gnugen.ch">#pixeldroid:gnugen.ch</a> !
|
||||
|
||||
## 🔧 Compiling the code yourself
|
||||
If you want to try out PixelDroid on your own device, you can compile the source code yourself. To do that you can install [Android Studio](https://developer.android.com/studio/).
|
||||
|
||||
## 🎨 Art attribution
|
||||
Our mascot was commissioned using funds from NLnet. The original file is `pixeldroid_mascot.svg` and it is adapted to work as an Android Drawable. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA).
|
||||
|
||||
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ). In addition, a drawing of a red panda is used for some error messages ( https://thenounproject.com/search/?q=red+panda&i=2877785 ).
|
||||
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ).
|
||||
|
||||
## 🤝 Contribute
|
||||
If you want to contribute, you can check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or [TRANSLATION.md](TRANSLATION.md)
|
||||
|
||||
|
|
121
app/build.gradle
121
app/build.gradle
|
@ -1,10 +1,28 @@
|
|||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'jacoco'
|
||||
apply plugin: "kotlin-parcelize"
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-android")
|
||||
id("jacoco")
|
||||
id("kotlin-parcelize")
|
||||
id("com.google.devtools.ksp")
|
||||
}
|
||||
// Map for the version code that gives each ABI a value.
|
||||
ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3, x86_64: 4]
|
||||
|
||||
//Different version codes per architecture (for F-Droid support)
|
||||
android.applicationVariants.configureEach { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
|
||||
if (baseAbiVersionCode != null) {
|
||||
output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode
|
||||
} else {
|
||||
output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
|
@ -27,8 +45,8 @@ android {
|
|||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
versionCode 27
|
||||
targetSdkVersion 34
|
||||
versionCode 33
|
||||
versionName "1.0.beta" + versionCode
|
||||
|
||||
//TODO add resConfigs("en", "fr", "ja",...) ?
|
||||
|
@ -77,6 +95,30 @@ android {
|
|||
proguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
// Configures multiple APKs based on ABI.
|
||||
abi {
|
||||
|
||||
// Enables building multiple APKs per ABI.
|
||||
enable true
|
||||
|
||||
// By default all ABIs are included, so use reset() and include to specify that we only
|
||||
// want APKs for "x86", "x86_64", "arm64-v8a" and "armeabi-v7a".
|
||||
|
||||
// Resets the list of ABIs for Gradle to create APKs for to none.
|
||||
reset()
|
||||
|
||||
// Specifies a list of ABIs for Gradle to create APKs for.
|
||||
//noinspection ChromeOsAbiSupport
|
||||
include project.ext.abiCodes.keySet() as String[]
|
||||
|
||||
// Specifies that we don't want to also generate a universal APK that includes all ABIs.
|
||||
universalApk false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a string with the application_id (available in xml etc)
|
||||
*/
|
||||
|
@ -133,30 +175,30 @@ dependencies {
|
|||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation "androidx.browser:browser:1.7.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||
implementation "androidx.browser:browser:1.8.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2'
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.6.2"
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
|
||||
implementation "androidx.annotation:annotation:1.7.1"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.8.2"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.9.0'
|
||||
implementation 'androidx.media2:media2-widget:1.2.1'
|
||||
implementation 'androidx.media2:media2-player:1.2.1'
|
||||
implementation 'androidx.media2:media2-widget:1.3.0'
|
||||
implementation 'androidx.media2:media2-player:1.3.0'
|
||||
|
||||
|
||||
// Use the most recent version of CameraX
|
||||
def cameraX_version = '1.3.1'
|
||||
def cameraX_version = '1.3.2'
|
||||
implementation "androidx.camera:camera-core:$cameraX_version"
|
||||
implementation "androidx.camera:camera-camera2:$cameraX_version"
|
||||
// CameraX Lifecycle library
|
||||
|
@ -181,18 +223,21 @@ dependencies {
|
|||
implementation 'com.google.android.material:material:1.11.0'
|
||||
|
||||
//Dagger (dependency injection)
|
||||
implementation 'com.google.dagger:dagger:2.48'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.48'
|
||||
implementation 'com.google.dagger:dagger:2.51'
|
||||
ksp 'com.google.dagger:dagger-compiler:2.51'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation('com.google.dagger:hilt-android:2.51')
|
||||
ksp 'com.google.dagger:hilt-compiler:2.51'
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.10.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.10.0'
|
||||
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.10.0'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'com.github.connyduck:sparkbutton:4.1.0'
|
||||
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.5'
|
||||
implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0'
|
||||
implementation project(path: ':scrambler')
|
||||
implementation project(path: ':pixel_common')
|
||||
|
||||
|
@ -200,24 +245,24 @@ dependencies {
|
|||
exclude group: "com.android.support"
|
||||
}
|
||||
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.14.2'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.14.2') {
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.16.0') {
|
||||
// Excludes the support library because it's already included by Glide.
|
||||
transitive = false
|
||||
}
|
||||
implementation 'com.github.bumptech.glide:annotations:4.16.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
|
||||
ksp 'com.github.bumptech.glide:ksp:4.14.2'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
ksp 'com.github.bumptech.glide:ksp:4.16.0'
|
||||
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
|
||||
implementation 'com.mikepenz:materialdrawer:9.0.1'
|
||||
implementation 'com.mikepenz:materialdrawer:9.0.2'
|
||||
// Add for NavController support
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.1'
|
||||
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
|
||||
|
||||
//iconics
|
||||
implementation 'com.mikepenz:iconics-core:5.4.0'
|
||||
implementation 'com.mikepenz:materialdrawer-iconics:9.0.1'
|
||||
implementation 'com.mikepenz:materialdrawer-iconics:9.0.2'
|
||||
implementation 'com.mikepenz:iconics-views:5.4.0'
|
||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||
|
||||
|
@ -242,7 +287,7 @@ dependencies {
|
|||
testImplementation "androidx.room:room-testing:$room_version"
|
||||
|
||||
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
|
||||
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
@ -251,10 +296,12 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
|
||||
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
tasks.withType(Test).configureEach {
|
||||
jacoco.includeNoLocationClasses = true
|
||||
jacoco.excludes = ['jdk.internal.*']
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<activity
|
||||
android:name=".posts.AlbumActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme.ActionBar.Transparent"/>
|
||||
android:theme="@style/TransparentAlbumActivity"/>
|
||||
<activity
|
||||
android:name=".profile.EditProfileActivity"
|
||||
android:exported="false"
|
||||
|
|
|
@ -78,7 +78,7 @@ class MainActivity : BaseActivity() {
|
|||
private lateinit var header: AccountHeaderView
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
||||
private lateinit var model: MainActivityViewModel
|
||||
private val model: MainActivityViewModel by viewModels()
|
||||
|
||||
companion object {
|
||||
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
|
||||
|
@ -111,12 +111,6 @@ class MainActivity : BaseActivity() {
|
|||
} else {
|
||||
sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this)
|
||||
|
||||
val _model: MainActivityViewModel by viewModels {
|
||||
MainActivityViewModelFactory(application)
|
||||
}
|
||||
model = _model
|
||||
|
||||
|
||||
setupDrawer()
|
||||
val tabs: List<() -> Fragment> = listOf(
|
||||
{
|
||||
|
@ -280,13 +274,13 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
val remainingUsers = db.userDao().getAll()
|
||||
if (remainingUsers.isEmpty()){
|
||||
//no more users, start first-time login flow
|
||||
// No more users, start first-time login flow
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
val newActive = remainingUsers.first()
|
||||
db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
//relaunch the app
|
||||
// Relaunch the app
|
||||
launchActivity(MainActivity(), firstTime = true)
|
||||
}
|
||||
}
|
||||
|
@ -334,9 +328,11 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun switchUser(userId: String, instance_uri: String) {
|
||||
db.userDao().deActivateActiveUsers()
|
||||
db.userDao().activateUser(userId, instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
db.runInTransaction{
|
||||
db.userDao().deActivateActiveUsers()
|
||||
db.userDao().activateUser(userId, instance_uri)
|
||||
apiHolder.setToCurrentUser()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
package org.pixeldroid.app
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivityViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
private val db: AppDatabase
|
||||
): ViewModel() {
|
||||
|
||||
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
|
||||
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
|
||||
|
@ -29,7 +26,6 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica
|
|||
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
getUsers()
|
||||
}
|
||||
|
||||
|
@ -41,12 +37,4 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MainActivityViewModelFactory(
|
||||
val application: Application,
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java).newInstance(application)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +1,57 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.os.*
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
const val TAG = "Post Creation Activity"
|
||||
|
||||
class PostCreationActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
internal const val PICTURE_DESCRIPTION = "picture_description"
|
||||
internal const val POST_DESCRIPTION = "post_description"
|
||||
internal const val PICTURE_DESCRIPTIONS = "picture_descriptions"
|
||||
internal const val POST_REDRAFT = "post_redraft"
|
||||
internal const val POST_NSFW = "post_nsfw"
|
||||
internal const val TEMP_FILES = "temp_files"
|
||||
|
||||
fun intentForUris(context: Context, uris: List<Uri>) =
|
||||
Intent(Intent.ACTION_SEND_MULTIPLE).apply {
|
||||
// Pass downloaded images to new post creation activity
|
||||
putParcelableArrayListExtra(
|
||||
Intent.EXTRA_STREAM, ArrayList(uris)
|
||||
)
|
||||
|
||||
uris.forEach {
|
||||
// Why are we using ClipData in addition to parcelableArrayListExtra here?
|
||||
// Because the FLAG_GRANT_READ_URI_PERMISSION needs to be applied to the URIs, and
|
||||
// for some reason it doesn't get applied to all of them when not using ClipData
|
||||
if (clipData == null) {
|
||||
clipData = ClipData("", emptyArray(), ClipData.Item(it))
|
||||
} else {
|
||||
clipData!!.addItem(ClipData.Item(it))
|
||||
}
|
||||
}
|
||||
|
||||
setClass(context, PostCreationActivity::class.java)
|
||||
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private lateinit var binding: ActivityPostCreationBinding
|
||||
|
||||
private lateinit var navController: NavController
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
binding = ActivityPostCreationBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val navHostFragment =
|
||||
|
@ -46,8 +60,6 @@ class PostCreationActivity : BaseActivity() {
|
|||
navController.setGraph(R.navigation.post_creation_graph)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
return navController.navigateUp() || super.onSupportNavigateUp()
|
||||
}
|
||||
override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp()
|
||||
|
||||
}
|
|
@ -33,29 +33,24 @@ import kotlinx.coroutines.launch
|
|||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
|
||||
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.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.common.PICTURE_POSITION
|
||||
import org.pixeldroid.media_editor.common.PICTURE_URI
|
||||
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class PostCreationFragment : BaseFragment() {
|
||||
|
||||
private var user: UserDatabaseEntity? = null
|
||||
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
|
||||
|
||||
private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
|
||||
private lateinit var model: PostCreationViewModel
|
||||
private val model: PostCreationViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
|
@ -72,30 +67,16 @@ class PostCreationFragment : BaseFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
user = db.userDao().getActiveUser()
|
||||
val user = db.userDao().getActiveUser()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
val instance = user?.run {
|
||||
db.instanceDao().getInstance(instance_uri)
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
|
||||
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false),
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
|
||||
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList<PhotoData>? ->
|
||||
// update UI
|
||||
binding.carousel.addData(
|
||||
newPhotoData.map {
|
||||
newPhotoData.orEmpty().map {
|
||||
CarouselItem(
|
||||
it.imageUri, it.imageDescription, it.video,
|
||||
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
|
||||
|
@ -103,7 +84,7 @@ class PostCreationFragment : BaseFragment() {
|
|||
)
|
||||
}
|
||||
)
|
||||
binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty()
|
||||
binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
|
@ -227,10 +208,9 @@ class PostCreationFragment : BaseFragment() {
|
|||
}
|
||||
|
||||
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
|
||||
result.data?.clipData?.let {
|
||||
model.setImages(model.addPossibleImages(it))
|
||||
}
|
||||
val uris = result.data?.extras?.getParcelableArrayList<Uri>(Intent.EXTRA_STREAM)
|
||||
if (result.resultCode == Activity.RESULT_OK && uris != null) {
|
||||
model.setImages(model.addPossibleImages(uris, emptyList()))
|
||||
} else if (result.resultCode != Activity.RESULT_CANCELED) {
|
||||
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
@ -328,7 +308,7 @@ class PostCreationFragment : BaseFragment() {
|
|||
ActivityResultContracts.StartActivityForResult()){
|
||||
result: ActivityResult? ->
|
||||
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
|
||||
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
|
||||
val position: Int = result.data!!.getIntExtra(PICTURE_POSITION, 0)
|
||||
model.modifyAt(position, result.data!!)
|
||||
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
|
||||
} else if(result?.resultCode != Activity.RESULT_CANCELED){
|
||||
|
@ -341,8 +321,8 @@ class PostCreationFragment : BaseFragment() {
|
|||
requireActivity(),
|
||||
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
|
||||
)
|
||||
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
|
||||
.putExtra(PICTURE_URI, model.getPhotoData().value!![position].imageUri)
|
||||
.putExtra(PICTURE_POSITION, position)
|
||||
|
||||
editResultContract.launch(intent)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
|
@ -12,15 +11,16 @@ import android.widget.Toast
|
|||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
|
||||
import com.jarsilio.android.scrambler.stripMetadata
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
|
@ -33,35 +33,25 @@ import kotlinx.parcelize.Parcelize
|
|||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.MainActivity
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.postCreation.camera.CameraFragment
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.fileExtension
|
||||
import org.pixeldroid.app.utils.getMimeType
|
||||
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
|
||||
import org.pixeldroid.media_editor.common.PICTURE_URI
|
||||
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.MutableList
|
||||
import kotlin.collections.MutableMap
|
||||
import kotlin.collections.arrayListOf
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.get
|
||||
import kotlin.collections.getOrNull
|
||||
import kotlin.collections.indexOfFirst
|
||||
import kotlin.collections.mutableListOf
|
||||
import kotlin.collections.mutableMapOf
|
||||
import kotlin.collections.plus
|
||||
import kotlin.collections.set
|
||||
import kotlin.collections.toMutableList
|
||||
import kotlin.math.ceil
|
||||
|
||||
const val TAG = "Post Creation ViewModel"
|
||||
|
||||
// Models the UI state for the PostCreationActivity
|
||||
data class PostCreationActivityUiState(
|
||||
|
@ -108,35 +98,56 @@ data class PhotoData(
|
|||
var videoEncodeError: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
class PostCreationViewModel(
|
||||
application: Application,
|
||||
clipdata: ClipData? = null,
|
||||
val instance: InstanceDatabaseEntity? = null,
|
||||
existingDescription: String? = null,
|
||||
existingNSFW: Boolean = false,
|
||||
storyCreation: Boolean = false,
|
||||
) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class PostCreationViewModel @Inject constructor(
|
||||
private val state: SavedStateHandle,
|
||||
@ApplicationContext private val applicationContext: Context,
|
||||
db: AppDatabase,
|
||||
): ViewModel() {
|
||||
private var storyPhotoDataBackup: MutableList<PhotoData>? = null
|
||||
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
|
||||
MutableLiveData<MutableList<PhotoData>>().also {
|
||||
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
|
||||
//FIXME We should be able to access the Intent action somehow, to determine if there are
|
||||
// 1 or multiple Uris instead of relying on the ClassCastException
|
||||
|
||||
// This should not work like this (reading its source code, get() function should return null
|
||||
// if it's the wrong type but instead throws ClassCastException).
|
||||
// Lucky for us that it does though: we first try to get a single Uri (which we could be
|
||||
// getting from a share of a single picture to the app), when the cast to Uri fails
|
||||
// we try to get a list of Uris instead (casting ourselves from Parcelable as suggested
|
||||
// in get() documentation)
|
||||
val uris = try {
|
||||
val singleUri: Uri? = state[Intent.EXTRA_STREAM]
|
||||
listOfNotNull(singleUri)
|
||||
} catch (e: ClassCastException) {
|
||||
state.get<ArrayList<Parcelable>>(Intent.EXTRA_STREAM)?.map { it as Uri }
|
||||
}
|
||||
|
||||
MutableLiveData<MutableList<PhotoData>>(
|
||||
addPossibleImages(
|
||||
uris,
|
||||
state.get<ArrayList<String>>(PostCreationActivity.PICTURE_DESCRIPTIONS),
|
||||
previousList = mutableListOf()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val instance = db.instanceDao().getActiveInstance()
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
val sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(application)
|
||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
val templateDescription = sharedPreferences.getString("prefill_description", "") ?: ""
|
||||
|
||||
val storyCreation: Boolean = state[CameraFragment.CAMERA_ACTIVITY_STORY] ?: false
|
||||
|
||||
_uiState = MutableStateFlow(PostCreationActivityUiState(
|
||||
newPostDescriptionText = existingDescription ?: templateDescription,
|
||||
nsfw = existingNSFW,
|
||||
newPostDescriptionText = state[PostCreationActivity.POST_DESCRIPTION] ?: templateDescription,
|
||||
nsfw = state[PostCreationActivity.POST_NSFW] ?: false,
|
||||
maxEntries = if(storyCreation) 1 else instance?.albumLimit,
|
||||
storyCreation = storyCreation
|
||||
))
|
||||
|
@ -161,32 +172,41 @@ class PostCreationViewModel(
|
|||
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
|
||||
|
||||
/**
|
||||
* Will add as many images as possible to [photoData], from the [clipData], and if
|
||||
* ([photoData].size + [clipData].itemCount) > uiState.value.maxEntries then it will only add as many images
|
||||
* Will add as many images as possible to [photoData], from the [uris], and if
|
||||
* ([photoData].size + [uris].size) > uiState.value.maxEntries then it will only add as many images
|
||||
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
|
||||
*/
|
||||
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
|
||||
fun addPossibleImages(
|
||||
uris: List<Uri>?,
|
||||
descriptions: List<String>?,
|
||||
previousList: MutableList<PhotoData>? = photoData.value,
|
||||
): MutableList<PhotoData> {
|
||||
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
|
||||
var count = clipData.itemCount
|
||||
uiState.value.maxEntries?.let {
|
||||
if(count + (previousList?.size ?: 0) > it){
|
||||
var count = uris?.size ?: 0
|
||||
uiState.value.maxEntries?.let { maxEntries ->
|
||||
if(count + (previousList?.size ?: 0) > maxEntries){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(it))
|
||||
currentUiState.copy(userMessage = applicationContext.getString(R.string.total_exceeds_album_limit).format(maxEntries))
|
||||
}
|
||||
count = count.coerceAtMost(it - (previousList?.size ?: 0))
|
||||
count = count.coerceAtMost(maxEntries - (previousList?.size ?: 0))
|
||||
}
|
||||
if (count + (previousList?.size ?: 0) >= it) {
|
||||
if (count + (previousList?.size ?: 0) >= maxEntries) {
|
||||
// Disable buttons to add more images
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(addPhotoButtonEnabled = false)
|
||||
}
|
||||
}
|
||||
for (i in 0 until count) {
|
||||
clipData.getItemAt(i).let {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> =
|
||||
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
|
||||
}
|
||||
for ((i, uri) in uris.orEmpty().withIndex()) {
|
||||
val sizeAndVideoPair: Pair<Long, Boolean> =
|
||||
getSizeAndVideoValidate(uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
|
||||
dataToAdd.add(
|
||||
PhotoData(
|
||||
imageUri = uri,
|
||||
size = sizeAndVideoPair.first,
|
||||
video = sizeAndVideoPair.second,
|
||||
imageDescription = descriptions?.getOrNull(i)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,7 +224,7 @@ class PostCreationViewModel(
|
|||
private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
|
||||
val size: Long =
|
||||
if (uri.scheme =="content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
|
||||
applicationContext.contentResolver.query(uri, null, null, null, null)
|
||||
?.use { cursor ->
|
||||
/* Get the column indexes of the data in the Cursor,
|
||||
* move to the first row in the Cursor, get the data,
|
||||
|
@ -221,12 +241,12 @@ class PostCreationViewModel(
|
|||
}
|
||||
|
||||
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
|
||||
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val type = uri.getMimeType(applicationContext.contentResolver)
|
||||
val isVideo = type.startsWith("video/")
|
||||
|
||||
if (isVideo && !instance!!.videoEnabled) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
|
||||
currentUiState.copy(userMessage = applicationContext.getString(R.string.video_not_supported))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,7 +255,7 @@ class PostCreationViewModel(
|
|||
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
userMessage = applicationContext.getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +292,7 @@ class PostCreationViewModel(
|
|||
videoEncodeComplete = false
|
||||
|
||||
VideoEditActivity.startEncoding(imageUri, null, it,
|
||||
context = getApplication<PixelDroidApplication>(),
|
||||
context = applicationContext,
|
||||
registerNewFFmpegSession = ::registerNewFFmpegSession,
|
||||
trackTempFile = ::trackTempFile,
|
||||
videoEncodeProgress = ::videoEncodeProgress
|
||||
|
@ -280,7 +300,7 @@ class PostCreationViewModel(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
imageUri = data.getStringExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI)!!.toUri()
|
||||
imageUri = data.getStringExtra(PICTURE_URI)!!.toUri()
|
||||
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
|
||||
size = imageSize
|
||||
video = imageVideo
|
||||
|
@ -387,17 +407,17 @@ class PostCreationViewModel(
|
|||
}
|
||||
|
||||
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
|
||||
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val extension = data.imageUri.fileExtension(applicationContext.contentResolver)
|
||||
|
||||
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
|
||||
val strippedImage = File.createTempFile("temp_img", ".$extension", applicationContext.cacheDir)
|
||||
|
||||
val imageUri = data.imageUri
|
||||
|
||||
val (strippedOrNot, size) = try {
|
||||
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||
val orientation = ExifInterface(applicationContext.contentResolver.openInputStream(imageUri)!!).getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
|
||||
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
|
||||
stripMetadata(imageUri, strippedImage, applicationContext.contentResolver)
|
||||
|
||||
// Restore EXIF orientation
|
||||
val exifInterface = ExifInterface(strippedImage)
|
||||
|
@ -409,11 +429,11 @@ class PostCreationViewModel(
|
|||
strippedImage.delete()
|
||||
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||
val imageInputStream = try {
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
|
||||
applicationContext.contentResolver.openInputStream(imageUri)!!
|
||||
} catch (e: FileNotFoundException){
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
userMessage = applicationContext.getString(R.string.file_not_found,
|
||||
data.imageUri)
|
||||
)
|
||||
}
|
||||
|
@ -425,14 +445,14 @@ class PostCreationViewModel(
|
|||
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
|
||||
userMessage = applicationContext.getString(R.string.file_not_found,
|
||||
data.imageUri)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
|
||||
val type = data.imageUri.getMimeType(applicationContext.contentResolver)
|
||||
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
|
||||
val requestBody = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
|
@ -482,7 +502,7 @@ class PostCreationViewModel(
|
|||
currentUiState.copy(
|
||||
uploadErrorVisible = true,
|
||||
uploadErrorExplanationText = if(e is HttpException){
|
||||
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
|
||||
applicationContext.getString(R.string.upload_error, e.code())
|
||||
} else "",
|
||||
uploadErrorExplanationVisible = e is HttpException,
|
||||
)
|
||||
|
@ -548,14 +568,14 @@ class PostCreationViewModel(
|
|||
sensitive = nsfw
|
||||
)
|
||||
}
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_success),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
val intent = Intent(getApplication(), MainActivity::class.java)
|
||||
val intent = Intent(applicationContext, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
//TODO make the activity launch this instead (and surrounding toasts too)
|
||||
getApplication<PixelDroidApplication>().startActivity(intent)
|
||||
applicationContext.startActivity(intent)
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
|
@ -564,7 +584,7 @@ class PostCreationViewModel(
|
|||
)
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
|
||||
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_failed),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e(TAG, exception.response().toString() + exception.message().toString())
|
||||
_uiState.update { currentUiState ->
|
||||
|
@ -609,7 +629,7 @@ class PostCreationViewModel(
|
|||
|
||||
//Show message saying extraneous pictures were removed but can be restored
|
||||
newUiState = newUiState.copy(
|
||||
userMessage = getApplication<PixelDroidApplication>().getString(R.string.extraneous_pictures_stories)
|
||||
userMessage = applicationContext.getString(R.string.extraneous_pictures_stories)
|
||||
)
|
||||
}
|
||||
// Restore if backup not null and first value is unchanged
|
||||
|
@ -629,10 +649,4 @@ class PostCreationViewModel(
|
|||
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, val storyCreation: Boolean) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,7 @@ class PostSubmissionFragment : BaseFragment() {
|
|||
private lateinit var instance: InstanceDatabaseEntity
|
||||
|
||||
private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware()
|
||||
private lateinit var model: PostCreationViewModel
|
||||
private val model: PostCreationViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
|
@ -60,23 +60,9 @@ class PostSubmissionFragment : BaseFragment() {
|
|||
accounts = db.userDao().getAll()
|
||||
|
||||
instance = user?.run {
|
||||
db.instanceDao().getAll().first { instanceDatabaseEntity ->
|
||||
instanceDatabaseEntity.uri.contains(instance_uri)
|
||||
}
|
||||
db.instanceDao().getInstance(instance_uri)
|
||||
} ?: InstanceDatabaseEntity("", "")
|
||||
|
||||
val _model: PostCreationViewModel by activityViewModels {
|
||||
PostCreationViewModelFactory(
|
||||
requireActivity().application,
|
||||
requireActivity().intent.clipData!!,
|
||||
instance,
|
||||
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
|
||||
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
|
||||
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false)
|
||||
)
|
||||
}
|
||||
model = _model
|
||||
|
||||
// Display the values from the view model
|
||||
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
|
||||
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
|
||||
|
|
|
@ -32,7 +32,7 @@ class CameraActivity : BaseActivity() {
|
|||
// If this CameraActivity wasn't started from the shortcut,
|
||||
// tell the fragment it's in an activity (so that it sends back the result instead of
|
||||
// starting a new post creation process)
|
||||
if (intent.action != "android.intent.action.VIEW") {
|
||||
if (intent.action != Intent.ACTION_VIEW) {
|
||||
val arguments = Bundle()
|
||||
arguments.putBoolean(CAMERA_ACTIVITY, true)
|
||||
arguments.putBoolean(CAMERA_ACTIVITY_STORY, story)
|
||||
|
@ -47,7 +47,7 @@ class CameraActivity : BaseActivity() {
|
|||
|
||||
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)
|
||||
if (intent.action != Intent.ACTION_VIEW) return super.onOptionsItemSelected(item)
|
||||
|
||||
// Else, start a new MainActivity when "going back" on this activity
|
||||
when (item.itemId) {
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.pixeldroid.app.postCreation.camera
|
|||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ContentUris
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
|
@ -39,7 +38,6 @@ import kotlinx.coroutines.launch
|
|||
import org.pixeldroid.app.databinding.FragmentCameraBinding
|
||||
import org.pixeldroid.app.postCreation.PostCreationActivity
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.bindingLifecycleAware
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
@ -60,7 +58,7 @@ class CameraFragment : BaseFragment() {
|
|||
|
||||
private val cameraLifecycleOwner = CameraLifecycleOwner()
|
||||
|
||||
private var binding: FragmentCameraBinding by bindingLifecycleAware()
|
||||
private lateinit var binding: FragmentCameraBinding
|
||||
|
||||
private var displayId: Int = -1
|
||||
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
|
||||
|
@ -327,7 +325,7 @@ class CameraFragment : BaseFragment() {
|
|||
}
|
||||
|
||||
private fun setupUploadImage() {
|
||||
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled
|
||||
val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled
|
||||
var mimeTypes: Array<String> = arrayOf("image/*")
|
||||
if(videoEnabled) mimeTypes += "video/*"
|
||||
|
||||
|
@ -450,21 +448,7 @@ class CameraFragment : BaseFragment() {
|
|||
|
||||
private fun startAlbumCreation(uris: ArrayList<String>) {
|
||||
|
||||
val intent = Intent(requireActivity(), PostCreationActivity::class.java)
|
||||
.apply {
|
||||
uris.forEach{
|
||||
//Why are we using ClipData here? Because the FLAG_GRANT_READ_URI_PERMISSION
|
||||
//needs to be applied to the URIs, and this flag only applies to the
|
||||
//Intent's data and any URIs specified in its ClipData.
|
||||
if(clipData == null){
|
||||
clipData = ClipData("", emptyArray(), ClipData.Item(it.toUri()))
|
||||
} else {
|
||||
clipData!!.addItem(ClipData.Item(it.toUri()))
|
||||
}
|
||||
}
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
val intent = PostCreationActivity.intentForUris(requireContext(), uris.map { it.toUri() })
|
||||
|
||||
if(inActivity && !addToStory){
|
||||
requireActivity().setResult(Activity.RESULT_OK, intent)
|
||||
|
|
|
@ -3,40 +3,99 @@ package org.pixeldroid.app.posts
|
|||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.databinding.ActivityAlbumBinding
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
|
||||
|
||||
class AlbumActivity : AppCompatActivity() {
|
||||
private val model: AlbumViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityAlbumBinding.inflate(layoutInflater)
|
||||
|
||||
val binding = ActivityAlbumBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val mediaAttachments = intent.getSerializableExtra("images") as ArrayList<Attachment>
|
||||
val index = intent.getIntExtra("index", 0)
|
||||
binding.albumPager.adapter = AlbumViewPagerAdapter(mediaAttachments,
|
||||
|
||||
binding.albumPager.adapter = AlbumViewPagerAdapter(
|
||||
model.uiState.value.mediaAttachments,
|
||||
sensitive = false,
|
||||
opened = true,
|
||||
//In the activity, we assume we want to show everything
|
||||
alwaysShowNsfw = true
|
||||
alwaysShowNsfw = true,
|
||||
clickCallback = ::clickCallback
|
||||
)
|
||||
binding.albumPager.currentItem = index
|
||||
|
||||
if(mediaAttachments.size == 1){
|
||||
binding.albumPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) { model.positionSelected(position) }
|
||||
})
|
||||
|
||||
if (model.uiState.value.mediaAttachments.size == 1) {
|
||||
binding.albumPager.isUserInputEnabled = false
|
||||
}
|
||||
else if((mediaAttachments.size) > 1) {
|
||||
} else if ((model.uiState.value.mediaAttachments.size) > 1) {
|
||||
binding.postIndicator.setViewPager(binding.albumPager)
|
||||
binding.postIndicator.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Not really necessary because the ViewPager saves its state in onSaveInstanceState, but
|
||||
// it's good to stay consistent in case something gets out of sync
|
||||
binding.albumPager.setCurrentItem(model.uiState.value.index, false)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
supportActionBar?.setBackgroundDrawable(null)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
binding.albumPager.currentItem = uiState.index
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.isActionBarHidden.collect { isActionBarHidden ->
|
||||
val windowInsetsController =
|
||||
WindowCompat.getInsetsController(this@AlbumActivity.window, binding.albumPager)
|
||||
if (isActionBarHidden) {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
binding.postIndicator.visibility = View.GONE
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Show both the status bar and the navigation bar
|
||||
supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
if ((model.uiState.value.mediaAttachments.size) > 1) {
|
||||
binding.postIndicator.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback passed to the AlbumViewPagerAdapter to signal a single click on the image
|
||||
*/
|
||||
private fun clickCallback(){
|
||||
model.barHide()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package org.pixeldroid.app.posts
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.pixeldroid.app.utils.api.objects.Attachment
|
||||
import javax.inject.Inject
|
||||
|
||||
data class AlbumUiState(
|
||||
val mediaAttachments: ArrayList<Attachment> = arrayListOf(),
|
||||
val index: Int = 0,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class AlbumViewModel @Inject constructor(state: SavedStateHandle) : ViewModel() {
|
||||
companion object {
|
||||
const val ALBUM_IMAGES = "AlbumViewImages"
|
||||
const val ALBUM_INDEX = "AlbumViewIndex"
|
||||
}
|
||||
|
||||
private val _uiState: MutableStateFlow<AlbumUiState>
|
||||
private val _isActionBarHidden: MutableStateFlow<Boolean>
|
||||
|
||||
init {
|
||||
_uiState = MutableStateFlow(AlbumUiState(
|
||||
mediaAttachments = state[ALBUM_IMAGES] ?: ArrayList(),
|
||||
index = state[ALBUM_INDEX] ?: 0
|
||||
))
|
||||
_isActionBarHidden = MutableStateFlow(false)
|
||||
}
|
||||
|
||||
val uiState: StateFlow<AlbumUiState> = _uiState.asStateFlow()
|
||||
val isActionBarHidden: StateFlow<Boolean> = _isActionBarHidden
|
||||
|
||||
fun barHide() {
|
||||
_isActionBarHidden.update { !it }
|
||||
}
|
||||
|
||||
fun positionSelected(position: Int) {
|
||||
_uiState.update { it.copy(index = position) }
|
||||
}
|
||||
}
|
|
@ -88,8 +88,8 @@ class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
|
|||
}
|
||||
val intent = Intent(context, AlbumActivity::class.java)
|
||||
|
||||
intent.putExtra("images", images)
|
||||
intent.putExtra("index", (child as ViewPager2).currentItem)
|
||||
intent.putExtra(AlbumViewModel.ALBUM_IMAGES, images)
|
||||
intent.putExtra(AlbumViewModel.ALBUM_INDEX, (child as ViewPager2).currentItem)
|
||||
|
||||
context.startActivity(intent)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.view.View
|
|||
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.pixeldroid.app.R
|
||||
|
@ -24,13 +25,16 @@ import org.pixeldroid.app.utils.displayDimensionsInPx
|
|||
class PostActivity : BaseActivity() {
|
||||
private lateinit var binding: ActivityPostBinding
|
||||
|
||||
private var commentFragment = CommentFragment()
|
||||
private lateinit var commentFragment: CommentFragment
|
||||
|
||||
private lateinit var status: Status
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityPostBinding.inflate(layoutInflater)
|
||||
|
||||
commentFragment = CommentFragment(binding.swipeRefreshLayout)
|
||||
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
@ -105,6 +109,11 @@ class PostActivity : BaseActivity() {
|
|||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.commentFragment, commentFragment).commit()
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
commentFragment.adapter.refresh()
|
||||
commentFragment.adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun postComment(
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
package org.pixeldroid.app.posts
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_DENIED
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
|
@ -20,11 +17,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -77,7 +70,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
fun bind(
|
||||
status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>, isActivity: Boolean = false
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>, isActivity: Boolean = false,
|
||||
) {
|
||||
|
||||
this.itemView.visibility = View.VISIBLE
|
||||
|
@ -371,7 +364,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
apiHolder: PixelfedAPIHolder,
|
||||
db: AppDatabase,
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>
|
||||
requestPermissionDownloadPic: ActivityResultLauncher<String>,
|
||||
){
|
||||
var bookmarked: Boolean? = null
|
||||
binding.statusMore.setOnClickListener {
|
||||
|
@ -449,178 +442,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_redraft -> {
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(R.string.redraft_dialog_launch)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Create new post creation activity
|
||||
val intent =
|
||||
Intent(context, PostCreationActivity::class.java)
|
||||
|
||||
// Get descriptions and images from original post
|
||||
val postDescription = status?.content ?: ""
|
||||
val postAttachments =
|
||||
status?.media_attachments!! // Catch possible exception from !! (?)
|
||||
val postNSFW = status?.sensitive
|
||||
|
||||
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||
postAttachment.url ?: ""
|
||||
}
|
||||
val imageNames = imageUriStrings.map { imageUriString ->
|
||||
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||
}
|
||||
val downloadedFiles = imageNames.map { imageName ->
|
||||
File(context.cacheDir, imageName)
|
||||
}
|
||||
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||
Uri.fromFile(downloadedFile)
|
||||
}
|
||||
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||
fromHtml(postAttachment.description ?: "").toString()
|
||||
}
|
||||
val downloadRequests: List<Request> = imageUriStrings.map { imageUriString ->
|
||||
Request.Builder().url(imageUriString).build()
|
||||
}
|
||||
|
||||
val counter = AtomicInteger(0)
|
||||
|
||||
// Define callback function for after downloading the images
|
||||
fun continuation() {
|
||||
// Wait for all outstanding downloads to finish
|
||||
if (counter.incrementAndGet() == imageUris.size) {
|
||||
if (allFilesExist(imageNames)) {
|
||||
// Delete original post
|
||||
lifecycleScope.launch {
|
||||
deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
|
||||
}
|
||||
|
||||
val counterInt = counter.get()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.resources.getQuantityString(
|
||||
R.plurals.items_load_success,
|
||||
counterInt,
|
||||
counterInt
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
// Pass downloaded images to new post creation activity
|
||||
intent.apply {
|
||||
imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) ->
|
||||
ClipData.Item(imageDescription, null, imageUri)
|
||||
}.forEach { imageItem ->
|
||||
if (clipData == null) {
|
||||
clipData = ClipData(
|
||||
"",
|
||||
emptyArray(),
|
||||
imageItem
|
||||
)
|
||||
} else {
|
||||
clipData!!.addItem(imageItem)
|
||||
}
|
||||
}
|
||||
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
// Pass post description of existing post to new post creation activity
|
||||
intent.putExtra(
|
||||
PostCreationActivity.PICTURE_DESCRIPTION,
|
||||
fromHtml(postDescription).toString()
|
||||
)
|
||||
if (imageNames.isNotEmpty()) {
|
||||
intent.putExtra(
|
||||
PostCreationActivity.TEMP_FILES,
|
||||
imageNames.toTypedArray()
|
||||
)
|
||||
}
|
||||
intent.putExtra(
|
||||
PostCreationActivity.POST_REDRAFT,
|
||||
true
|
||||
)
|
||||
intent.putExtra(
|
||||
PostCreationActivity.POST_NSFW,
|
||||
postNSFW
|
||||
)
|
||||
|
||||
// Launch post creation activity
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFilesExist(imageNames)) {
|
||||
// Track download progress
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.image_download_downloading),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Iterate through all pictures of the original post
|
||||
downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) ->
|
||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||
if (!downloadedFile.exists()) {
|
||||
OkHttpClient().newCall(downloadRequest)
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException
|
||||
) {
|
||||
Looper.prepare()
|
||||
downloadedFile.delete()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response
|
||||
) {
|
||||
val sink: BufferedSink =
|
||||
downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
Looper.prepare()
|
||||
continuation()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
continuation()
|
||||
}
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(
|
||||
R.string.redraft_post_failed_error,
|
||||
exception.code()
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_redraft -> launchRedraftDialog(lifecycleScope, apiHolder, db)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -645,6 +467,165 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
}
|
||||
}
|
||||
|
||||
private fun launchRedraftDialog(
|
||||
lifecycleScope: LifecycleCoroutineScope,
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
db: AppDatabase
|
||||
): Boolean {
|
||||
MaterialAlertDialogBuilder(binding.root.context).apply {
|
||||
setMessage(R.string.redraft_dialog_launch)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
// Get descriptions and images from original post
|
||||
val postDescription = status?.content ?: ""
|
||||
val postAttachments =
|
||||
status?.media_attachments!! // TODO Catch possible exception from !! (?)
|
||||
val postNSFW = status?.sensitive
|
||||
|
||||
val imageUriStrings = postAttachments.map { postAttachment ->
|
||||
postAttachment.url ?: ""
|
||||
}
|
||||
val imageNames = imageUriStrings.map { imageUriString ->
|
||||
Uri.parse(imageUriString).lastPathSegment.toString()
|
||||
}
|
||||
val downloadedFiles = imageNames.map { imageName ->
|
||||
File(context.cacheDir, imageName)
|
||||
}
|
||||
val imageDescriptions = postAttachments.map { postAttachment ->
|
||||
fromHtml(
|
||||
postAttachment.description ?: ""
|
||||
).toString()
|
||||
}
|
||||
val downloadRequests: List<Request> =
|
||||
imageUriStrings.map { imageUriString ->
|
||||
Request.Builder().url(imageUriString).build()
|
||||
}
|
||||
|
||||
val imageUris = downloadedFiles.map { downloadedFile ->
|
||||
Uri.fromFile(downloadedFile)
|
||||
}
|
||||
|
||||
val counter = AtomicInteger(0)
|
||||
|
||||
// Define callback function for after downloading the images
|
||||
fun continuation() {
|
||||
// Wait for all outstanding downloads to finish
|
||||
if (counter.incrementAndGet() == imageUris.size) {
|
||||
if (allFilesExist(imageNames)) {
|
||||
// Delete original post
|
||||
lifecycleScope.launch {
|
||||
deletePost(
|
||||
apiHolder.api ?: apiHolder.setToCurrentUser(), db
|
||||
)
|
||||
}
|
||||
|
||||
val counterInt = counter.get()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.resources.getQuantityString(
|
||||
R.plurals.items_load_success, counterInt, counterInt
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
// Create new post creation activity
|
||||
val intent = PostCreationActivity.intentForUris(context, imageUris).apply {
|
||||
putExtra(
|
||||
PostCreationActivity.PICTURE_DESCRIPTIONS,
|
||||
ArrayList(imageDescriptions)
|
||||
)
|
||||
// Pass post description of existing post to new post creation activity
|
||||
putExtra(
|
||||
PostCreationActivity.POST_DESCRIPTION,
|
||||
fromHtml(postDescription).toString()
|
||||
)
|
||||
if (imageNames.isNotEmpty()) {
|
||||
putExtra(
|
||||
PostCreationActivity.TEMP_FILES,
|
||||
imageNames.toTypedArray()
|
||||
)
|
||||
}
|
||||
putExtra(PostCreationActivity.POST_REDRAFT, true)
|
||||
putExtra(PostCreationActivity.POST_NSFW, postNSFW)
|
||||
}
|
||||
|
||||
// Launch post creation activity
|
||||
binding.root.context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allFilesExist(imageNames)) {
|
||||
// Track download progress
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.image_download_downloading),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// Iterate through all pictures of the original post
|
||||
downloadRequests.zip(downloadedFiles)
|
||||
.forEach { (downloadRequest, downloadedFile) ->
|
||||
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
|
||||
if (!downloadedFile.exists()) {
|
||||
OkHttpClient().newCall(downloadRequest)
|
||||
.enqueue(object : Callback {
|
||||
override fun onFailure(
|
||||
call: Call,
|
||||
e: IOException,
|
||||
) {
|
||||
Looper.prepare()
|
||||
downloadedFile.delete()
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(
|
||||
R.string.redraft_post_failed_io_except
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onResponse(
|
||||
call: Call,
|
||||
response: Response,
|
||||
) {
|
||||
val sink: BufferedSink =
|
||||
downloadedFile.sink().buffer()
|
||||
sink.writeAll(response.body!!.source())
|
||||
sink.close()
|
||||
Looper.prepare()
|
||||
continuation()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
continuation()
|
||||
}
|
||||
}
|
||||
} catch (exception: HttpException) {
|
||||
Toast.makeText(
|
||||
binding.root.context, binding.root.context.getString(
|
||||
R.string.redraft_post_failed_error, exception.code()
|
||||
), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} catch (exception: IOException) {
|
||||
Toast.makeText(
|
||||
binding.root.context,
|
||||
binding.root.context.getString(R.string.redraft_post_failed_io_except),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun activateLiker(
|
||||
apiHolder: PixelfedAPIHolder,
|
||||
isLiked: Boolean,
|
||||
|
@ -820,17 +801,15 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
|
|||
class AlbumViewPagerAdapter(
|
||||
private val media_attachments: List<Attachment>, private var sensitive: Boolean?,
|
||||
private val opened: Boolean, private val alwaysShowNsfw: Boolean,
|
||||
) :
|
||||
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
private var isActionBarHidden: Boolean = false
|
||||
private val clickCallback: (() -> Unit)? = null
|
||||
) : RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return if(!opened) ViewHolderClosed(AlbumImageViewBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)) else ViewHolderOpen(OpenedAlbumBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
))
|
||||
), clickCallback!!)
|
||||
}
|
||||
|
||||
override fun getItemCount() = media_attachments.size
|
||||
|
@ -861,24 +840,6 @@ class AlbumViewPagerAdapter(
|
|||
setDoubleTapZoomDpi(240)
|
||||
resetScaleAndCenter()
|
||||
}
|
||||
holder.image.setOnClickListener {
|
||||
val windowInsetsController = WindowCompat.getInsetsController((it.context as Activity).window, it)
|
||||
// Configure the behavior of the hidden system bars
|
||||
if (isActionBarHidden) {
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
(it.context as AppCompatActivity).supportActionBar?.show()
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
isActionBarHidden = false
|
||||
} else {
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
(it.context as AppCompatActivity).supportActionBar?.hide()
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
isActionBarHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else Glide.with(holder.binding.root)
|
||||
.asDrawable().fitCenter()
|
||||
|
@ -924,9 +885,13 @@ class AlbumViewPagerAdapter(
|
|||
abstract val videoPlayButton: ImageView
|
||||
}
|
||||
|
||||
class ViewHolderOpen(override val binding: OpenedAlbumBinding) : ViewHolder(binding) {
|
||||
class ViewHolderOpen(override val binding: OpenedAlbumBinding, clickCallback: () -> Unit) : ViewHolder(binding) {
|
||||
override val image: SubsamplingScaleImageView = binding.imageImageView
|
||||
override val videoPlayButton: ImageView = binding.videoPlayButton
|
||||
|
||||
init {
|
||||
image.setOnClickListener { clickCallback() }
|
||||
}
|
||||
}
|
||||
class ViewHolderClosed(override val binding: AlbumImageViewBinding) : ViewHolder(binding) {
|
||||
override val image: ImageView = binding.imageImageView
|
||||
|
|
|
@ -16,18 +16,20 @@
|
|||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.RemoteMediator
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository class that works with local and remote data sources.
|
||||
*/
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi
|
||||
@Inject constructor(
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi constructor(
|
||||
private val db: AppDatabase,
|
||||
private val dao: FeedContentDao<T>,
|
||||
private val mediator: RemoteMediator<Int, T>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
|
||||
|
||||
/**
|
||||
|
@ -17,7 +19,7 @@ import javax.inject.Inject
|
|||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class HomeFeedRemoteMediator @Inject constructor(
|
||||
class HomeFeedRemoteMediator(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
) : RemoteMediator<Int, HomeStatusDatabaseEntity>() {
|
||||
|
|
|
@ -16,13 +16,15 @@
|
|||
|
||||
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.room.withTransaction
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import java.lang.NullPointerException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for the public feed.
|
||||
|
@ -32,7 +34,7 @@ import javax.inject.Inject
|
|||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class PublicFeedRemoteMediator @Inject constructor(
|
||||
class PublicFeedRemoteMediator(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, PublicFeedStatusDatabaseEntity>() {
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.paging.ExperimentalPagingApi
|
|||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
@ -20,6 +21,7 @@ import org.pixeldroid.app.posts.feeds.initAdapter
|
|||
import org.pixeldroid.app.posts.feeds.launch
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
|
||||
|
||||
|
||||
/**
|
||||
|
@ -30,8 +32,7 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
|||
internal lateinit var viewModel: FeedViewModel<T>
|
||||
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
|
||||
|
||||
lateinit var binding: FragmentFeedBinding
|
||||
|
||||
var binding: FragmentFeedBinding? = null
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
|
@ -48,25 +49,35 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
|||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
.collect { binding?.list?.scrollToPosition(0) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
savedInstanceState: Bundle?, swipeRefreshLayout: SwipeRefreshLayout?
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
initAdapter(
|
||||
binding.progressBar, binding.swipeRefreshLayout, binding.list,
|
||||
binding.motionLayout, binding.errorLayout, adapter
|
||||
)
|
||||
binding!!.let {
|
||||
initAdapter(
|
||||
it.progressBar, swipeRefreshLayout ?: it.swipeRefreshLayout, it.list,
|
||||
it.motionLayout, it.errorLayout, adapter
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
return binding!!.root
|
||||
}
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return onCreateView(inflater, container, savedInstanceState, null)
|
||||
}
|
||||
fun onTabReClicked() {
|
||||
binding?.list?.limitedLengthSmoothScrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,15 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.NestedScrollingChild
|
||||
import androidx.core.view.NestedScrollingChildHelper
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.CommentBinding
|
||||
import org.pixeldroid.app.posts.PostActivity
|
||||
|
@ -25,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
|
|||
/**
|
||||
* Fragment to show a list of [Status]s, in form of comments
|
||||
*/
|
||||
class CommentFragment : UncachedFeedFragment<Status>() {
|
||||
class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment<Status>() {
|
||||
|
||||
private lateinit var id: String
|
||||
private lateinit var domain: String
|
||||
|
@ -42,11 +45,11 @@ class CommentFragment : UncachedFeedFragment<Status>() {
|
|||
@OptIn(ExperimentalPagingApi::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
|
||||
|
||||
// Get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -62,6 +65,7 @@ class CommentFragment : UncachedFeedFragment<Status>() {
|
|||
launch()
|
||||
initSearch()
|
||||
|
||||
binding?.swipeRefreshLayout?.isEnabled = false
|
||||
return view
|
||||
}
|
||||
companion object {
|
||||
|
|
|
@ -25,7 +25,7 @@ import org.pixeldroid.app.utils.openUrl
|
|||
|
||||
class EditProfileActivity : BaseActivity() {
|
||||
|
||||
private lateinit var model: EditProfileViewModel
|
||||
private val model: EditProfileViewModel by viewModels()
|
||||
private lateinit var binding: ActivityEditProfileBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -35,9 +35,6 @@ class EditProfileActivity : BaseActivity() {
|
|||
setSupportActionBar(binding.topBar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) }
|
||||
model = _model
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
// Handle the back button event
|
||||
if(model.madeChanges()){
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package org.pixeldroid.app.profile
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.launch
|
|||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.postCreation.ProgressRequestBody
|
||||
import org.pixeldroid.app.posts.fromHtml
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
|
@ -29,7 +28,10 @@ import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
|||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context
|
||||
): ViewModel() {
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
@ -46,7 +48,6 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
private set
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
|
@ -197,12 +198,12 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
val image = uiState.value.profilePictureUri!!
|
||||
|
||||
val inputStream =
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image)
|
||||
applicationContext.contentResolver.openInputStream(image)
|
||||
?: return
|
||||
|
||||
val size: Long =
|
||||
if (image.scheme == "content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(
|
||||
applicationContext.contentResolver.query(
|
||||
image,
|
||||
null,
|
||||
null,
|
||||
|
@ -303,10 +304,4 @@ data class EditProfileActivityUiState(
|
|||
val error: Boolean = false,
|
||||
val uploadingPicture: Boolean = false,
|
||||
val uploadProgress: Int = 0,
|
||||
)
|
||||
|
||||
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java).newInstance(application)
|
||||
}
|
||||
}
|
||||
)
|
|
@ -7,16 +7,19 @@ import android.util.Log
|
|||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityProfileBinding
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
|
@ -58,6 +61,29 @@ class ProfileActivity : BaseActivity() {
|
|||
val tabs = createProfileTabs(account)
|
||||
setupTabs(tabs)
|
||||
setContent(account)
|
||||
|
||||
binding.profileMotion.setTransitionListener(
|
||||
object : MotionLayout.TransitionListener {
|
||||
override fun onTransitionStarted(
|
||||
motionLayout: MotionLayout?, startId: Int, endId: Int,
|
||||
) {}
|
||||
|
||||
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {}
|
||||
|
||||
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
|
||||
if (currentId == R.id.hideProfile && motionLayout?.startState == R.id.start) {
|
||||
// If the 1st transition has been made go to the second one
|
||||
motionLayout.setTransition(R.id.second)
|
||||
} else if(currentId == R.id.hideProfile && motionLayout?.startState == R.id.hideProfile){
|
||||
motionLayout.setTransition(R.id.first)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransitionTrigger(
|
||||
motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float,
|
||||
) {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
|
||||
|
@ -104,7 +130,7 @@ class ProfileActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun setupTabs(
|
||||
tabs: Array<UncachedFeedFragment<FeedContent>>
|
||||
tabs: Array<UncachedFeedFragment<FeedContent>>,
|
||||
){
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
|
@ -136,6 +162,14 @@ class ProfileActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
}.attach()
|
||||
|
||||
binding.profileTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {}
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {}
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
tabs[tab.position].onTabReClicked()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setContent(account: Account?) {
|
||||
|
|
|
@ -101,7 +101,7 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
|||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
if(grid || bookmarks || collections || addCollection) {
|
||||
binding.list.layoutManager = GridLayoutManager(context, 3)
|
||||
binding?.list?.layoutManager = GridLayoutManager(context, 3)
|
||||
}
|
||||
|
||||
// Get the view model
|
||||
|
@ -191,8 +191,11 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
|||
val url = "$domain/i/collections/create"
|
||||
|
||||
if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) {
|
||||
Snackbar.make(binding.root, getString(R.string.new_collection_link_failed),
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
binding?.let { binding ->
|
||||
Snackbar.make(
|
||||
binding.root, getString(R.string.new_collection_link_failed),
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,9 +25,6 @@ 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() {
|
||||
|
||||
|
@ -37,12 +34,11 @@ class StoriesActivity: BaseActivity() {
|
|||
const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
|
||||
}
|
||||
|
||||
|
||||
private lateinit var binding: ActivityStoriesBinding
|
||||
|
||||
private lateinit var storyProgress: StoryProgress
|
||||
|
||||
private lateinit var model: StoriesViewModel
|
||||
private val model: StoriesViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
//force night mode always
|
||||
|
@ -50,18 +46,9 @@ class StoriesActivity: BaseActivity() {
|
|||
|
||||
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)
|
||||
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
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.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.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
|
||||
|
@ -37,18 +35,13 @@ data class StoriesUiState(
|
|||
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
|
||||
@HiltViewModel
|
||||
class StoriesViewModel @Inject constructor(state: SavedStateHandle,
|
||||
db: AppDatabase,
|
||||
private val apiHolder: PixelfedAPIHolder) : ViewModel() {
|
||||
private val carousel: StoryCarousel? = state[StoriesActivity.STORY_CAROUSEL]
|
||||
private val userId: String? = state[StoriesActivity.STORY_CAROUSEL_USER_ID]
|
||||
private val selfCarousel: Array<Story>? = state[StoriesActivity.STORY_CAROUSEL_SELF]
|
||||
|
||||
private var currentAccount: CarouselUserContainer?
|
||||
|
||||
|
@ -61,10 +54,9 @@ class StoriesViewModel(
|
|||
private var timer: CountDownTimer? = null
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
currentAccount =
|
||||
if (selfCarousel != null) {
|
||||
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) }
|
||||
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel.toList()) }
|
||||
} else carousel?.nodes?.firstOrNull { it?.user?.id == userId }
|
||||
|
||||
_uiState = MutableStateFlow(newUiStateFromCurrentAccount())
|
||||
|
@ -216,14 +208,3 @@ class StoriesViewModel(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
|
||||
|
||||
@Inject
|
||||
|
@ -12,11 +13,6 @@ open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
|
|||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(this.application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
|
@ -12,6 +12,7 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Base Fragment, for dependency injection and other things common to a lot of the fragments
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
open class BaseFragment: Fragment() {
|
||||
|
||||
@Inject
|
||||
|
@ -20,11 +21,6 @@ open class BaseFragment: Fragment() {
|
|||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
internal val requestPermissionDownloadPic =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
|
|
|
@ -3,14 +3,12 @@ package org.pixeldroid.app.utils
|
|||
import android.app.Application
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import org.ligi.tracedroid.TraceDroid
|
||||
import org.pixeldroid.app.utils.di.*
|
||||
|
||||
|
||||
@HiltAndroidApp
|
||||
class PixelDroidApplication: Application() {
|
||||
|
||||
private lateinit var mApplicationComponent: ApplicationComponent
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
@ -19,18 +17,7 @@ class PixelDroidApplication: Application() {
|
|||
val sharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
setThemeFromPreferences(sharedPreferences, resources)
|
||||
mApplicationComponent = DaggerApplicationComponent
|
||||
.builder()
|
||||
.applicationModule(ApplicationModule(this))
|
||||
.databaseModule(DatabaseModule(applicationContext))
|
||||
.aPIModule(APIModule())
|
||||
.build()
|
||||
mApplicationComponent.inject(this)
|
||||
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
fun getAppComponent(): ApplicationComponent {
|
||||
return mApplicationComponent
|
||||
}
|
||||
}
|
|
@ -1,28 +1,25 @@
|
|||
package org.pixeldroid.app.utils
|
||||
|
||||
import android.content.*
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Matrix
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
|
@ -34,7 +31,7 @@ import okhttp3.HttpUrl
|
|||
import org.pixeldroid.app.R
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
|
|||
)
|
||||
}
|
||||
|
||||
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(metadata?.config?.site?.url!!),
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface InstanceDao {
|
||||
@Query("SELECT * FROM instances")
|
||||
fun getAll(): List<InstanceDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=:instanceUri")
|
||||
fun getInstance(instanceUri: String): InstanceDatabaseEntity
|
||||
|
||||
|
@ -19,13 +21,13 @@ interface InstanceDao {
|
|||
* Insert an instance, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
suspend fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
|
||||
@Update
|
||||
fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
suspend fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
|
||||
@Transaction
|
||||
fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
if (insertInstance(instance) == -1L) {
|
||||
updateInstance(instance)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
|
|
|
@ -6,13 +6,16 @@ import org.pixeldroid.app.utils.db.AppDatabase
|
|||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.*
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class APIModule{
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class APIModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
@ -54,7 +57,7 @@ class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase, val
|
|||
client_secret = user.clientSecret
|
||||
)
|
||||
}
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception){
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import dagger.Component
|
||||
import org.pixeldroid.app.MainActivityViewModel
|
||||
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
||||
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 javax.inject.Singleton
|
||||
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [ApplicationModule::class, DatabaseModule::class, APIModule::class])
|
||||
interface ApplicationComponent {
|
||||
fun inject(application: PixelDroidApplication?)
|
||||
fun inject(activity: BaseActivity?)
|
||||
fun inject(feedFragment: BaseFragment)
|
||||
fun inject(notificationsWorker: NotificationsWorker)
|
||||
fun inject(postCreationViewModel: PostCreationViewModel)
|
||||
fun inject(editProfileViewModel: EditProfileViewModel)
|
||||
fun inject(storiesViewModel: StoriesViewModel)
|
||||
fun inject(mainActivityViewModel: MainActivityViewModel)
|
||||
|
||||
val context: Context?
|
||||
val application: Application?
|
||||
val database: AppDatabase
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
class ApplicationModule(app: Application) {
|
||||
private val mApplication: Application = app
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideContext(): Context {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideApplication(): Application {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
}
|
|
@ -5,19 +5,25 @@ import androidx.room.Room
|
|||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_3_4
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_4_5
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_5_6
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DatabaseModule(private val context: Context) {
|
||||
class DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDatabase(): AppDatabase {
|
||||
fun providesDatabase(
|
||||
@ApplicationContext applicationContext: Context
|
||||
): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
applicationContext,
|
||||
AppDatabase::class.java, "pixeldroid"
|
||||
).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
|
||||
.allowMainThreadQueries().build()
|
||||
|
|
|
@ -32,9 +32,6 @@ import java.io.IOException
|
|||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
||||
|
||||
class NotificationsWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
|
@ -46,9 +43,6 @@ class NotificationsWorker(
|
|||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
||||
(applicationContext as PixelDroidApplication).getAppComponent().inject(this)
|
||||
|
||||
val users: List<UserDatabaseEntity> = db.userDao().getAll()
|
||||
|
||||
for (user in users){
|
||||
|
@ -306,8 +300,7 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString())
|
||||
} else {
|
||||
val types: MutableList<Notification.NotificationType?> =
|
||||
Notification.NotificationType.values().toMutableList()
|
||||
val types: MutableList<Notification.NotificationType?> = entries.toMutableList()
|
||||
types += null
|
||||
|
||||
types.forEach {
|
||||
|
|
|
@ -3,88 +3,104 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/scrollview"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".posts.PostActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways"/>
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:background="?attr/colorSurface"
|
||||
android:id="@+id/collapsing_toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/postFragmentSingle"
|
||||
layout="@layout/post_fragment" />
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/commentIn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/postFragmentSingle"
|
||||
tools:layout_editor_absoluteX="10dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout2"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/submitComment"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editComment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/comment_noun"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text|textCapSentences|textMultiLine" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submitComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/submit_comment"
|
||||
android:text="@string/comment_verb"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintPost"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar">
|
||||
|
||||
<include
|
||||
android:id="@+id/postFragmentSingle"
|
||||
layout="@layout/post_fragment" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/commentIn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintPost"
|
||||
tools:layout_editor_absoluteX="10dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/submitComment"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editComment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/comment_noun"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text|textCapSentences|textMultiLine" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submitComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/submit_comment"
|
||||
android:text="@string/comment_verb"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/commentFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/commentIn" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id="@+id/commentFragment"
|
||||
/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -3,154 +3,155 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".profile.ProfileActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
android:id="@+id/profileMotion"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
app:layoutDescription="@xml/collapsing_motion_layout_scene">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:fitsSystemWindows="true"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/profile"
|
||||
android:elevation="-1dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePictureImageView"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:clickable="false"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbPostsTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways"/>
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:text="@string/default_nposts"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/collapsing_toolbar_layout"
|
||||
android:background="?attr/colorSurface"
|
||||
<TextView
|
||||
android:id="@+id/nbFollowersTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowers"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
|
||||
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowingTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowing"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountNameTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed">
|
||||
android:clickable="false"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/no_username"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="visible"
|
||||
app:layout_collapseMode="parallax"
|
||||
app:layout_constraintTop_toBottomOf="@id/nbFollowersTextView"
|
||||
tools:visibility="visible">
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:clickable="false"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginRight="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePictureImageView"
|
||||
android:layout_width="88dp"
|
||||
android:layout_height="88dp"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:contentDescription="@string/profile_picture"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
<Button
|
||||
android:id="@+id/followButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/follow"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbPostsTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nposts"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/edit_profile"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowersTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowers"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
|
||||
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nbFollowingTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text="@string/default_nfollowing"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountNameTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/no_username"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/descriptionTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="20dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginRight="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/followButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:text="@string/follow"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/editButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/edit_profile"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/profileTabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nbPostsTextView"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/profileTabs"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/profile"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
android:background="?attr/colorSurface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/profileTabs"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -16,6 +16,8 @@
|
|||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
app:titleTextColor="?attr/colorOnSecondaryContainer"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -38,7 +40,6 @@
|
|||
android:id="@+id/refreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -8,14 +8,6 @@
|
|||
android:layout_marginBottom="5dp"
|
||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/profilePic"
|
||||
android:layout_width="50dp"
|
||||
|
@ -243,6 +235,4 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/postDate"
|
||||
tools:text="3 comments" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -138,35 +138,45 @@
|
|||
<plurals name="nb_following">
|
||||
<item quantity="one">%d
|
||||
\nAbonnement</item>
|
||||
<item quantity="many">%d
|
||||
\nAbonnements</item>
|
||||
<item quantity="other">%d
|
||||
\nAbonnements</item>
|
||||
</plurals>
|
||||
<plurals name="nb_followers">
|
||||
<item quantity="one">%d
|
||||
\nAbonné·e</item>
|
||||
<item quantity="many">%d
|
||||
\nAbonné·e·s</item>
|
||||
<item quantity="other">%d
|
||||
\nAbonné·e·s</item>
|
||||
</plurals>
|
||||
<plurals name="nb_posts">
|
||||
<item quantity="one">%d
|
||||
\nPublication</item>
|
||||
<item quantity="many">%d
|
||||
\nPublications</item>
|
||||
<item quantity="other">%d
|
||||
\nPublications</item>
|
||||
</plurals>
|
||||
<plurals name="number_comments">
|
||||
<item quantity="one">%d commentaire</item>
|
||||
<item quantity="many">%d commentaires</item>
|
||||
<item quantity="other">%d commentaires</item>
|
||||
</plurals>
|
||||
<plurals name="shares">
|
||||
<item quantity="one">%d Partage</item>
|
||||
<item quantity="many">%d Partages</item>
|
||||
<item quantity="other">%d Partages</item>
|
||||
</plurals>
|
||||
<plurals name="likes">
|
||||
<item quantity="one">%d J\'aime</item>
|
||||
<item quantity="many">%d J\'aime</item>
|
||||
<item quantity="other">%d J\'aime</item>
|
||||
</plurals>
|
||||
<plurals name="description_max_characters">
|
||||
<item quantity="one">La description doit contenir au moins %d lettre.</item>
|
||||
<item quantity="many">La description doit contenir au moins %d lettres.</item>
|
||||
<item quantity="other">La description doit contenir au moins %d lettres.</item>
|
||||
</plurals>
|
||||
<string name="delete_post_failed_error">Impossible de supprimer la publication, erreur %1$d</string>
|
||||
|
@ -184,7 +194,7 @@
|
|||
<string name="followed_notification_channel">Nouveaux·elles abonné·e·s</string>
|
||||
<string name="mention_notification_channel">Mentions</string>
|
||||
<string name="shared_notification_channel">Partages</string>
|
||||
<string name="liked_notification_channel">Favoris</string>
|
||||
<string name="liked_notification_channel">J\'aime</string>
|
||||
<string name="comment_notification_channel">Commentaires</string>
|
||||
<string name="poll_notification_channel">Sondages</string>
|
||||
<string name="other_notification_channel">Autre</string>
|
||||
|
@ -223,6 +233,85 @@
|
|||
<string name="profile_saved">Modifications enregistrées !</string>
|
||||
<string name="change_profile_picture">Changer votre image de profil</string>
|
||||
<string name="switch_accounts">Permutation de comptes</string>
|
||||
<string name="edit_link_failed">Échec de l\'ouverture de la page de modifications</string>
|
||||
<string name="edit_link_failed">Impossible d\'ouvrir la page d\'édition</string>
|
||||
<string name="follow_requested">Abonnement demandé</string>
|
||||
<string name="comment_noun">Commentaire</string>
|
||||
<string name="redraft_dialog_launch">La reformulation de cet article vous permettra de modifier la photo et sa description, mais supprimera tous les commentaires et les mentions \"J\'aime\". Poursuivre ?</string>
|
||||
<string name="notification_thumbnail">Vignette de l\'image dans ce message</string>
|
||||
<string name="always_show_nsfw">Toujours afficher les contenus sensibles</string>
|
||||
<string name="post_preview">Aperçu d\'un message</string>
|
||||
<plurals name="notification_title_summary">
|
||||
<item quantity="one">%d nouvelle notification</item>
|
||||
<item quantity="many">%d nouvelles notifications</item>
|
||||
<item quantity="other">%d nouvelles notifications</item>
|
||||
</plurals>
|
||||
<plurals name="items_load_success">
|
||||
<item quantity="one">%d article chargé avec succès</item>
|
||||
<item quantity="many">%d articles chargés avec succès</item>
|
||||
<item quantity="other">%d articles chargés avec succès</item>
|
||||
</plurals>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s et %4$d autres</string>
|
||||
<string name="notification_summary_medium">%1$s, %2$s, et %3$s</string>
|
||||
<string name="video_not_supported">Le serveur que vous utilisez ne prend pas en charge les téléchargements de vidéos, il se peut que vous ne puissiez pas télécharger les vidéos incluses dans cet article</string>
|
||||
<string name="new_collection_link_failed">Échec de l\'ouverture de la page de création d\'une collection</string>
|
||||
<string name="bookmark">Favori</string>
|
||||
<string name="unknown_error_in_error">Erreur inconnue, vérifiez si le serveur est en panne : %1$s</string>
|
||||
<string name="profile_error">Impossible de charger le profil</string>
|
||||
<string name="add_images_error">Erreur lors de l\'ajout des images</string>
|
||||
<string name="description_template_summary">Remplir la description des nouveaux messages avec ceci</string>
|
||||
<string name="description_template">Modèle de description</string>
|
||||
<string name="explore_accounts">Explorer les comptes populaires de cette instance</string>
|
||||
<string name="explore_hashtags">Explorer les hashtags en vogue sur cette instance</string>
|
||||
<string name="daily_trending">Voir les messages populaires de la journée</string>
|
||||
<string name="notification_summary_small">%1$s et %2$s</string>
|
||||
<string name="extraneous_pictures_stories">Les images après la première ont été supprimées mais peuvent être restaurées en revenant à la création d\'un message</string>
|
||||
<string name="summary_always_show_nsfw">Les messages NSFW/CW ne seront pas floutés et seront affichés par défaut.</string>
|
||||
<string name="encode_progress">Encodage de %1$d%%</string>
|
||||
<string name="still_encoding">Une ou plusieurs vidéos sont en cours d\'encodage. Attendez qu\'elles soient terminées avant d\'envoyer</string>
|
||||
<string name="notifications_settings_summary">Gérer les notifications que vous souhaitez recevoir</string>
|
||||
<string name="login_notifications">Impossible de récupérer les dernières notifications</string>
|
||||
<string name="no_camera_permission">L\'autorisation pour l\'appareil photo n\'est pas accordée, accordez cette autorisation dans les paramètres si vous voulez permettre à PixelDroid d\'utiliser l\'appareil photo</string>
|
||||
<string name="play_video">Lire la vidéo</string>
|
||||
<string name="public_feed">Publique</string>
|
||||
<string name="accentColorTitle">Couleur d\'accentuation</string>
|
||||
<string name="accentColorSummary">Choisir une couleur d\'accentuation</string>
|
||||
<string name="color_choice_button">Choisir cette couleur d\'accentuation</string>
|
||||
<string name="explore_posts">Explorer aléatoirement les messages d\'aujourd\'hui</string>
|
||||
<string name="grid_view">Vue en grille</string>
|
||||
<string name="feed_view">Vue du flux</string>
|
||||
<string name="encode_error">Erreur d\'encodage</string>
|
||||
<string name="encode_success">Encodage réussi !</string>
|
||||
<string name="more_profile_settings">Autres paramètres du profil</string>
|
||||
<string name="private_account_explanation">Quand votre compte est privé, seules les personnes que vous autorisez peuvent voir vos photos et vidéos sur PixelFed. Les personnes qui vous suivent déjà ne seront pas affectées.</string>
|
||||
<string name="saving_profile">Sauvegarde de votre profil</string>
|
||||
<string name="use_dynamic_color">Utiliser la couleur dynamique de votre système</string>
|
||||
<string name="type_story">Story</string>
|
||||
<string name="story_image">Image de la Story</string>
|
||||
<string name="replyToStory">Répondre à %1$s</string>
|
||||
<string name="story_reply_error">Un problème s\'est produit lors de l\'envoi de la réponse</string>
|
||||
<string name="error_fetch_story">Un problème s\'est produit lors de la récupération du carrousel</string>
|
||||
<string name="sent_reply_story">Envoyer la réponse</string>
|
||||
<string name="fetching_profile">Recherche de votre profil…</string>
|
||||
<string name="redraft_dialog_cancel">Si vous annulez ce remaniement, le message original ne figurera plus sur votre compte. Poursuivre sans réécrire ?</string>
|
||||
<string name="redraft_post_failed_error">Impossible de réécrire le message, erreur %1$d</string>
|
||||
<string name="redraft_post_failed_io_except">Impossible de réécrire le message, vérifiez votre connexion ?</string>
|
||||
<string name="bookmark_post_failed_error">Impossible de mettre/enlever le message en favoris, erreur %1$d</string>
|
||||
<string name="bookmark_post_failed_io_except">Impossible de mettre/enlever le message des favoris, vérifiez votre connexion ?</string>
|
||||
<string name="no_storage_permission">L\'autorisation pour le stockage n\'est pas accordée, accordez l\'autorisation dans les paramètres si vous voulez permettre à PixelDroid d\'afficher la vignette</string>
|
||||
<string name="analyzing_stabilization">Analyse pour stabiliser %1$d%%</string>
|
||||
<string name="color_chosen">Couleur d\'accentuation choisie</string>
|
||||
<string name="error_profile">Un problème s\'est produit. Appuyez pour réessayer</string>
|
||||
<string name="delete_collection_warning">Êtes-vous sûr de vouloir supprimer cette collection ?</string>
|
||||
<string name="added_post_to_collection">Ajouter le message à la collection</string>
|
||||
<string name="error_add_post_to_collection">Échec de l\'ajout du message à la collection</string>
|
||||
<string name="removed_post_from_collection">Enlever le message de la collection</string>
|
||||
<string name="error_remove_post_from_collection">Échec pour retirer le message de la collection</string>
|
||||
<string name="contains_nsfw">Contient des médias NSFW</string>
|
||||
<string name="add_story">Ajouter une Story</string>
|
||||
<string name="story_could_not_see">Erreur : Impossible de marquer la Story comme vu</string>
|
||||
<string name="story_pause">Démarrer ou interrompre les Stories</string>
|
||||
<string name="my_story">Ma Story</string>
|
||||
<string name="type_post">Message</string>
|
||||
<string name="continue_post_creation">Continuer</string>
|
||||
<string name="story_duration">Durée de la Story</string>
|
||||
</resources>
|
|
@ -304,4 +304,13 @@
|
|||
<string name="collection_add_post">Aggiungi post</string>
|
||||
<string name="collections">Raccolte</string>
|
||||
<string name="delete_collection">Elimina raccolta</string>
|
||||
<string name="add_story">Aggiungi Storia</string>
|
||||
<string name="story_pause">Riproduci o ferma le storie</string>
|
||||
<string name="story_could_not_see">Errore: impossibile segnare la storia come visualizzata</string>
|
||||
<string name="my_story">La mia storia</string>
|
||||
<string name="type_story">Storia</string>
|
||||
<string name="type_post">Post</string>
|
||||
<string name="continue_post_creation">Continua</string>
|
||||
<string name="extraneous_pictures_stories">Le immagini successive alla prima sono state rimosse, ma possono essere ripristinate tornando indietro alla creazione di un Post</string>
|
||||
<string name="story_duration">Durata della Storia</string>
|
||||
</resources>
|
|
@ -9,7 +9,7 @@
|
|||
<string name="theme_header">Тема</string>
|
||||
<string name="mention_notification">%1$s вас згадує</string>
|
||||
<string name="description">Опис…</string>
|
||||
<string name="post">надіслати</string>
|
||||
<string name="post">Оприлюднити</string>
|
||||
<string name="save_to_gallery">Зберегти до галереї…</string>
|
||||
<string name="image_download_downloading">Завантаження…</string>
|
||||
<string name="image_download_success">Зображення успішно завантажено</string>
|
||||
|
@ -146,13 +146,13 @@
|
|||
<string name="default_nfollowing">-
|
||||
\nпідписок</string>
|
||||
<string name="search">Пошук</string>
|
||||
<string name="edit_profile">Редагувати</string>
|
||||
<string name="edit_profile">Редагувати профіль</string>
|
||||
<string name="posts">ДОПИСИ</string>
|
||||
<string name="accounts">ОБЛІКОВІ ЗАПИСИ</string>
|
||||
<string name="license_info">PixelDroid — вільне й відкрите програмне забезпечення, доступне на умовах Загальної громадської ліцензії GNU (версії 3 чи новішої)</string>
|
||||
<string name="about">Про застосунок</string>
|
||||
<string name="post_title">Допис %1$s</string>
|
||||
<string name="reported">Скаргу надіслано {gmd_check_circle}</string>
|
||||
<string name="reported">Скаргу надіслано</string>
|
||||
<string name="profile_picture">Зображення профілю</string>
|
||||
<string name="open_drawer_menu">Відкрити висувне меню</string>
|
||||
<string name="something_went_wrong">Щось пішло не так…</string>
|
||||
|
@ -223,4 +223,108 @@
|
|||
<string name="video_not_supported">Використовуваний сервер не підтримує вивантаження відео. Ймовірно, вивантажити відео цього допису не вдасться</string>
|
||||
<string name="post_is_video">Це відеодопис</string>
|
||||
<string name="play_video">Відтворити відео</string>
|
||||
<string name="bookmarks">Закладки</string>
|
||||
<string name="collections">Колекції</string>
|
||||
<string name="delete_collection">Видалити колекцію</string>
|
||||
<string name="collection_add_post">Додати публікацію</string>
|
||||
<string name="collection_remove_post">Вилучити публікацію</string>
|
||||
<string name="comment_noun">Коментар</string>
|
||||
<string name="add_to_collection">Оберіть публікацію для додавання</string>
|
||||
<plurals name="replies_count">
|
||||
<item quantity="one">%d відповідь</item>
|
||||
<item quantity="few">%d відповіді</item>
|
||||
<item quantity="many">%d відповідей</item>
|
||||
<item quantity="other">%d відповідей</item>
|
||||
</plurals>
|
||||
<string name="delete_from_collection">Оберіть публікацію для вилучення</string>
|
||||
<string name="redraft_dialog_cancel">Якщо ви скасуєте цю переробку, оригінального допису більше не буде у вашому акаунті. Продовжити без перепосту?</string>
|
||||
<string name="accentColorTitle">Акцентний колір</string>
|
||||
<string name="explore_posts">Ознайомтеся з випадковими дописами дня</string>
|
||||
<string name="private_account_explanation">Коли ваш акаунт приватний, тільки люди, яких ви схвалите, зможуть бачити ваші фотографії та відео на pixelfed. На ваших існуючих підписників це не вплине.</string>
|
||||
<string name="always_show_nsfw">Завжди показувати чутливий вміст</string>
|
||||
<string name="profile_error">Не вдалося завантажити профіль</string>
|
||||
<string name="explore_accounts">Ознайомтеся з популярними акаунтами на цьому екземплярі</string>
|
||||
<string name="added_post_to_collection">Публікацію додано в колекцію</string>
|
||||
<string name="summary_always_show_nsfw">Дописи NSFW/CW не будуть розмиті і відображатимуться за замовчуванням.</string>
|
||||
<string name="type_story">Історія</string>
|
||||
<string name="new_collection_link_failed">Не вдалося відкрити сторінку створення колекції</string>
|
||||
<string name="redraft_dialog_launch">При переробці цього допису ви зможете відредагувати фотографію та її опис, але при цьому будуть видалені всі поточні коментарі та вподобання. Продовжити?</string>
|
||||
<string name="notification_thumbnail">Ескіз зображення в дописі цього повідомлення</string>
|
||||
<string name="redraft">Переробити</string>
|
||||
<string name="delete_collection_warning">Ви впевнені що хочете видалити цю колекцію?</string>
|
||||
<string name="error_add_post_to_collection">Не вдалося додати публікацію до колекції</string>
|
||||
<string name="use_dynamic_color">Використовувати динамічні кольори з вашої системи</string>
|
||||
<string name="story_could_not_see">Помилка: не вдалося позначити історію як побачену</string>
|
||||
<string name="encode_error">Помилка кодування</string>
|
||||
<string name="encode_success">Кодування успішно завершено!</string>
|
||||
<string name="encode_progress">Кодувати %1$d%%</string>
|
||||
<string name="still_encoding">Одне або кілька відео все ще кодуються. Зачекайте, поки кодування завершиться, перш ніж завантажувати</string>
|
||||
<string name="new_post_shortcut_short">Нова публікація</string>
|
||||
<string name="follow_request">%1$s надіслав запит на відстеження вас</string>
|
||||
<plurals name="items_load_success">
|
||||
<item quantity="one">%d елемент успішно завантажено</item>
|
||||
<item quantity="few">%d елемента успішно завантажено</item>
|
||||
<item quantity="many">%d елементів успішно завантажено</item>
|
||||
<item quantity="other">%d елементів успішно завантажено</item>
|
||||
</plurals>
|
||||
<string name="home_feed">Домівка</string>
|
||||
<string name="search_discover_feed">Пошук</string>
|
||||
<string name="story_image">Зображення історії</string>
|
||||
<string name="replyToStory">Відповісти на %1$s</string>
|
||||
<string name="fetching_profile">Отримання вашого профілю…</string>
|
||||
<string name="continue_post_creation">Продовжити</string>
|
||||
<string name="extraneous_pictures_stories">Зображення після першого були вилучені, але їх можна відновити, повернувшись до створення публікації</string>
|
||||
<string name="upload_next_step">Наступний крок</string>
|
||||
<string name="add_details">Додати деякі деталі</string>
|
||||
<string name="unknown_error_in_error">Невідома помилка, перевірте, чи не працює сервер: %1$s</string>
|
||||
<string name="collection_title">%1$s колекція</string>
|
||||
<string name="bookmark">Закладка</string>
|
||||
<string name="unbookmark">Видалити закладку</string>
|
||||
<string name="feed_view">Вигляд стрічки</string>
|
||||
<string name="redraft_post_failed_error">Не вдалося переробити пост, помилка %1$d</string>
|
||||
<string name="redraft_post_failed_io_except">Не вдалося переписати пост, перевірити ваше зʼєднання?</string>
|
||||
<string name="bookmark_post_failed_error">Не вдалося додати/видалити закладку, помилка %1$d</string>
|
||||
<string name="bookmark_post_failed_io_except">Не вдалося додати/видалити закладку, перевірити зʼєднання?</string>
|
||||
<string name="analyzing_stabilization">Аналіз для стабілізації %1$d%%</string>
|
||||
<string name="new_post_shortcut_long">Створити нову публікацію</string>
|
||||
<string name="status_notification">%1$s створив публікацію</string>
|
||||
<string name="create_feed">Створити</string>
|
||||
<string name="notifications_feed">Оновлення</string>
|
||||
<string name="public_feed">Публічний</string>
|
||||
<string name="accentColorSummary">Виберіть акцентний колір</string>
|
||||
<string name="color_choice_button">Вибрати цей колір як акцентний</string>
|
||||
<string name="color_chosen">Обрано акцентний колір</string>
|
||||
<string name="from_other_domain">з %1$s</string>
|
||||
<string name="add_images_error">Помилка під час додавання зображень</string>
|
||||
<string name="post_preview">Попередній перегляд публікації</string>
|
||||
<string name="description_template_summary">Доповніть опис нових дописів цим</string>
|
||||
<string name="description_template">Шаблон опису</string>
|
||||
<string name="popular_accounts">Популярні акаунти</string>
|
||||
<string name="explore_hashtags">Дослідіть популярні хештеги на цьому екземплярі</string>
|
||||
<string name="trending_hashtags">Популярні хештеги</string>
|
||||
<string name="daily_trending">Переглядайте щоденні популярні дописи</string>
|
||||
<string name="trending_posts">Популярні публікації</string>
|
||||
<string name="grid_view">Вигляд сітки</string>
|
||||
<string name="error_remove_post_from_collection">Не вдалося вилучити публікацію з колекції</string>
|
||||
<string name="removed_post_from_collection">Публікацію вилучено з колекції</string>
|
||||
<string name="save">Зберегти</string>
|
||||
<string name="more_profile_settings">Більше налаштувань профілю</string>
|
||||
<string name="private_account">Приватний обліковий запис</string>
|
||||
<string name="your_bio">Ваша біографія</string>
|
||||
<string name="your_name">Ваге імʼя</string>
|
||||
<string name="profile_save_changes">Ви не зберегли зміни. Вийти?</string>
|
||||
<string name="saving_profile">Збереження вашого профілю</string>
|
||||
<string name="profile_saved">Зміни збережено!</string>
|
||||
<string name="error_profile">Щось пішло не так. Натисніть, щоб повторити спробу</string>
|
||||
<string name="change_profile_picture">Змінити зображення профілю</string>
|
||||
<string name="contains_nsfw">Містить носії NSFW</string>
|
||||
<string name="switch_accounts">Змінити обліковий запис</string>
|
||||
<string name="story_reply_error">Щось пішло не так під час надсилання відповіді</string>
|
||||
<string name="error_fetch_story">Щось пішло не так з каруселлю</string>
|
||||
<string name="sent_reply_story">Відповідь надіслано</string>
|
||||
<string name="add_story">Додати історію</string>
|
||||
<string name="story_pause">Запустити або призупинити історії</string>
|
||||
<string name="my_story">Моя історія</string>
|
||||
<string name="type_post">Публікація</string>
|
||||
<string name="story_duration">Тривалість історії</string>
|
||||
</resources>
|
|
@ -0,0 +1,291 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="menu_settings">設定</string>
|
||||
<string name="menu_account">關於我</string>
|
||||
<string name="invalid_domain">無效的網域</string>
|
||||
<string name="registration_failed">無法在此伺服器上註冊</string>
|
||||
<string name="auth_failed">無法認證</string>
|
||||
<string name="verify_credentials">無法獲取使用者資訊</string>
|
||||
<string name="token_error">獲取Token的時候發生錯誤</string>
|
||||
<string name="instance_error">無法獲取實例資訊</string>
|
||||
<string name="instance_not_pixelfed_continue">無論如何繼續</string>
|
||||
<string name="instance_not_pixelfed_cancel">取消登入</string>
|
||||
<string name="title_activity_settings2">設定</string>
|
||||
<string name="theme_title">應用主題</string>
|
||||
<string name="theme_header">主題</string>
|
||||
<string name="default_system">默認(跟隨系統)</string>
|
||||
<string name="light_theme">明亮</string>
|
||||
<string name="dark_theme">黑暗</string>
|
||||
<string name="always_show_nsfw">總是展示敏感內容</string>
|
||||
<string name="followed_notification">%1$s 追蹤了你</string>
|
||||
<string name="mention_notification">%1$s 提及了你</string>
|
||||
<string name="shared_notification">%1$s 分享了你的貼文</string>
|
||||
<string name="comment_notification">%1$s 在你的貼文下留言了</string>
|
||||
<string name="poll_notification">%1$s的投票已經結束</string>
|
||||
<string name="other_notification">從%1$s的通知</string>
|
||||
<string name="followed_notification_channel">新的追蹤者</string>
|
||||
<string name="mention_notification_channel">提及</string>
|
||||
<string name="shared_notification_channel">分享</string>
|
||||
<string name="liked_notification_channel">喜歡</string>
|
||||
<string name="comment_notification_channel">留言</string>
|
||||
<string name="poll_notification_channel">投票</string>
|
||||
<string name="other_notification_channel">其他</string>
|
||||
<string name="notification_summary_large">%1$s, %2$s, %3$s 和 %4$d 更多</string>
|
||||
<string name="notification_summary_medium">%1$s, %2$s, 和 %3$s</string>
|
||||
<string name="notification_summary_small">%1$s 和 %2$s</string>
|
||||
<string name="whats_an_instance">「實例」是什麼?</string>
|
||||
<string name="domain_of_your_instance">你所在實例的網域</string>
|
||||
<string name="connect_to_pixelfed">連接至Pixelfed</string>
|
||||
<string name="login_connection_required_once">你需要連上網際網路才能新增第一個賬戶來使用PixelDroid :(</string>
|
||||
<string name="api_not_enabled_dialog">這個實例上的API並沒有被啟用。請與你的管理員聯絡。</string>
|
||||
<string name="logout">登出</string>
|
||||
<plurals name="description_max_characters">
|
||||
<item quantity="other">介紹必須含有最少%d個字元。</item>
|
||||
</plurals>
|
||||
<string name="upload_picture_failed">上載圖片失敗!</string>
|
||||
<string name="picture_format_error">上載失敗:錯誤的圖片格式。</string>
|
||||
<string name="request_format_error">上載失敗:請求格式錯誤</string>
|
||||
<string name="upload_post_failed">貼文上載失敗</string>
|
||||
<string name="upload_post_error">貼文上載錯誤</string>
|
||||
<string name="description">介紹……</string>
|
||||
<string name="post">貼文</string>
|
||||
<string name="upload_next_step">下一步</string>
|
||||
<string name="add_details">新增點細節</string>
|
||||
<string name="add_photo">新增圖片</string>
|
||||
<string name="post_image">本貼文中的一個圖片</string>
|
||||
<string name="switch_to_grid">切換到網格視圖</string>
|
||||
<string name="save_image_description">保存圖片介紹</string>
|
||||
<string name="no_media_description">在這裡新增媒體描述……</string>
|
||||
<string name="size_exceeds_instance_limit">第%1$d個圖片超過了實例所允許的最大大小(%2$dkB,限制為%3$dkB),你可能無法上載。</string>
|
||||
<string name="upload_error">伺服器返回了「%1$d」的錯誤碼</string>
|
||||
<string name="capture_button_alt">拍照</string>
|
||||
<string name="switch_camera_button_alt">切換攝像頭</string>
|
||||
<string name="gallery_button_alt">相簿</string>
|
||||
<string name="loading_toast">載入時發生錯誤</string>
|
||||
<string name="feed_failed">無法獲得Feed</string>
|
||||
<string name="unknown_error_in_error">未知錯誤,看看伺服器是否停運:%1$s</string>
|
||||
<string name="browser_launch_failed">無法拉起瀏覽器,你有安裝嗎?</string>
|
||||
<string name="app_name">PixelDroid</string>
|
||||
<string name="instance_not_pixelfed_warning">這看起來不是個Pixelfed實例,應用可能會以意想不到的方式崩潰。</string>
|
||||
<string name="liked_notification">%1$s 喜歡了你的貼文</string>
|
||||
<string name="upload_post_success">成功上載了貼文</string>
|
||||
<string name="switch_to_carousel">切換到輪播</string>
|
||||
<string name="video_not_supported">你選擇的伺服器不支援上載影片,你可能無法在此貼文中新增影片</string>
|
||||
<plurals name="notification_title_summary">
|
||||
<item quantity="other">%d 則新通知</item>
|
||||
</plurals>
|
||||
<string name="whats_an_instance_explanation">你可能因被要求填入「實例」而困惑。
|
||||
\n
|
||||
\nPixelfed是個聯邦平台,「聯邦宇宙」的一員,可以和其他平台上相同語言的使用者相互交談,就像Mastodon(去https://joinmastodon.org看看)
|
||||
\n
|
||||
\n也就是說你必須選擇一個伺服器,或者是Pixelfed的「實例」來使用。如果沒有的話請訪問https://pixelfed.org/join
|
||||
\n
|
||||
\n欲瞭解Pixelfed的更多資訊,請訪問https://pixelfed.org</string>
|
||||
<string name="add_account_name">新增賬戶</string>
|
||||
<string name="add_account_description">新增另一個Pixelfed賬戶</string>
|
||||
<string name="total_exceeds_album_limit">你可以選擇該伺服器最多允許的圖片數量(%1$s個)。超過限制的會被忽略。</string>
|
||||
<string name="share_picture">分享相片……</string>
|
||||
<string name="save_to_gallery">存儲至相簿……</string>
|
||||
<string name="image_download_failed">下載失敗,請重試</string>
|
||||
<string name="image_download_downloading">下載中……</string>
|
||||
<string name="image_download_success">相片下載成功</string>
|
||||
<plurals name="items_load_success">
|
||||
<item quantity="other">%d個物件已被成功載入</item>
|
||||
</plurals>
|
||||
<string name="no_description">沒有描述</string>
|
||||
<plurals name="likes">
|
||||
<item quantity="other">%d個喜歡</item>
|
||||
</plurals>
|
||||
<plurals name="shares">
|
||||
<item quantity="other">%d個分享</item>
|
||||
</plurals>
|
||||
<string name="posted_on">在%1$s發表</string>
|
||||
<string name="NoCommentsToShow">此貼文下沒有回覆……</string>
|
||||
<string name="empty_comment">回覆内容不能為空!</string>
|
||||
<string name="share_image">分享相片</string>
|
||||
<string name="comment_error">回覆失敗!</string>
|
||||
<string name="comment_posted">回覆「%1$s」已發表!</string>
|
||||
<string name="comment_verb">回覆</string>
|
||||
<string name="comment_noun">回覆</string>
|
||||
<plurals name="number_comments">
|
||||
<item quantity="other">%d則回覆</item>
|
||||
</plurals>
|
||||
<string name="add_comment">發表回覆</string>
|
||||
<string name="submit_comment">發表回覆</string>
|
||||
<string name="post_is_album">這則貼文是一個集合</string>
|
||||
<string name="post_is_video">這則貼文是一支影片</string>
|
||||
<plurals name="nb_posts">
|
||||
<item quantity="other">%d
|
||||
\n貼文</item>
|
||||
</plurals>
|
||||
<plurals name="nb_followers">
|
||||
<item quantity="other">%d
|
||||
\n追蹤者</item>
|
||||
</plurals>
|
||||
<string name="edit">編輯</string>
|
||||
<string name="save_image_failed">無法儲存相片</string>
|
||||
<string name="save_image_success">相片成功存儲</string>
|
||||
<string name="follow_status_failed">無法獲取追蹤狀態</string>
|
||||
<string name="edit_link_failed">無法打開編輯頁面</string>
|
||||
<string name="empty_feed">本來無一物,何處惹塵埃</string>
|
||||
<string name="follow_button_failed">無法顯示追蹤按鈕</string>
|
||||
<string name="follow_error">無法追蹤</string>
|
||||
<string name="action_not_allowed">不允許此操作</string>
|
||||
<string name="unfollow_error">無法取消追蹤</string>
|
||||
<string name="access_token_invalid">這個access token無效</string>
|
||||
<string name="default_nposts">-
|
||||
\n貼文</string>
|
||||
<string name="default_nfollowers">-
|
||||
\n追蹤者</string>
|
||||
<string name="default_nfollowing">-
|
||||
\n追蹤中</string>
|
||||
<string name="no_username">無使用者名稱</string>
|
||||
<string name="follow">追蹤</string>
|
||||
<string name="follow_requested">請求已送出</string>
|
||||
<string name="dialog_message_cancel_follow_request">取消追蹤申請嗎?</string>
|
||||
<string name="edit_profile">編輯個人資料</string>
|
||||
<string name="search">搜尋</string>
|
||||
<string name="posts">貼文</string>
|
||||
<string name="accounts">使用者</string>
|
||||
<string name="hashtags">推標</string>
|
||||
<string name="media_upload_completed">媒體上載成功</string>
|
||||
<string name="media_upload_failed">媒體上載失敗,重新試試,或者檢查下網路</string>
|
||||
<string name="posting_image_accessibility_hint">準備要貼出的相片</string>
|
||||
<string name="retry">重試</string>
|
||||
<string name="nothing_to_see_here">空無一物!</string>
|
||||
<string name="about_pixeldroid">關於PixelDroid</string>
|
||||
<string name="dependencies_licenses">依賴和許可</string>
|
||||
<string name="about">關於</string>
|
||||
<string name="post_title">%1$s的貼文</string>
|
||||
<string name="collection_title">%1$s的合集</string>
|
||||
<string name="followers_title">%1$s的追蹤者</string>
|
||||
<string name="hashtag_title">#%1$s</string>
|
||||
<string name="follows_title">%1$s的追蹤</string>
|
||||
<string name="search_empty_error">搜尋的關鍵詞不能為空</string>
|
||||
<string name="status_more_options">更多</string>
|
||||
<string name="report">回報</string>
|
||||
<string name="bookmark">書簽</string>
|
||||
<string name="unbookmark">從書簽中移除</string>
|
||||
<string name="share_link">分享連結</string>
|
||||
<string name="reported">已報告貼文</string>
|
||||
<string name="profile_picture">個人資料相片</string>
|
||||
<string name="open_drawer_menu">打開抽屜菜單</string>
|
||||
<string name="discover">發現</string>
|
||||
<string name="something_went_wrong">發生錯誤……</string>
|
||||
<string name="panda_pull_to_refresh_to_try_again">這隻熊貓不開心。下拉重載以重試。</string>
|
||||
<string name="redraft">重擬</string>
|
||||
<string name="redraft_dialog_cancel">如果你取消這次重擬,原貼文將會被從你的賬戶中移除。繼續嗎?</string>
|
||||
<string name="delete">刪除</string>
|
||||
<string name="delete_dialog">刪除這則貼文嗎?</string>
|
||||
<string name="language">語言</string>
|
||||
<string name="help_translate">幫助將PixelDroid翻譯成你的語言吧:</string>
|
||||
<string name="issues_contribute">報告問題或是給這個軟體貢獻:</string>
|
||||
<string name="redraft_post_failed_error">無法重擬這則貼文,錯誤%1$d</string>
|
||||
<string name="redraft_post_failed_io_except">無法重擬貼文,檢查你的網路?</string>
|
||||
<string name="delete_post_failed_error">無法刪除這則貼文,錯誤%1$d</string>
|
||||
<string name="delete_post_failed_io_except">無法刪除這則貼文,檢查你的網路?</string>
|
||||
<string name="bookmark_post_failed_error">無法對此貼文進行書簽操作,錯誤%1$d</string>
|
||||
<string name="file_not_found">沒找到檔案%1$s</string>
|
||||
<string name="notifications_settings">通知設定</string>
|
||||
<string name="notifications_settings_summary">管理你想接收到的通知</string>
|
||||
<string name="login_notifications">無法獲取最新通知</string>
|
||||
<string name="no_storage_permission">存儲權限未獲取。如果想查看縮圖的話請在設定中允許該權限</string>
|
||||
<string name="play_video">播放影片</string>
|
||||
<string name="encode_error">編碼錯誤</string>
|
||||
<string name="encode_success">編碼成功!</string>
|
||||
<string name="encode_progress">編碼%1$d%%</string>
|
||||
<string name="analyzing_stabilization">穩定性分析%1$d%%</string>
|
||||
<string name="still_encoding">有影片還在編碼。請在上載前等待編碼完成</string>
|
||||
<string name="new_post_shortcut_long">建立新貼文</string>
|
||||
<string name="new_post_shortcut_short">新貼文</string>
|
||||
<string name="follow_request">%1$s請求追蹤</string>
|
||||
<string name="home_feed">主頁面</string>
|
||||
<string name="search_discover_feed">搜尋</string>
|
||||
<string name="create_feed">建立</string>
|
||||
<string name="notifications_feed">更新</string>
|
||||
<string name="public_feed">公共</string>
|
||||
<string name="accentColorTitle">色調</string>
|
||||
<string name="color_choice_button">選擇這個色調</string>
|
||||
<string name="color_chosen">色調已選擇</string>
|
||||
<string name="profile_error">無法載入資料</string>
|
||||
<string name="from_other_domain">來自%1$s</string>
|
||||
<string name="add_images_error">加入相片時發生錯誤</string>
|
||||
<string name="post_preview">貼文預覽</string>
|
||||
<string name="description_template_summary">在新貼文的描述中預填入</string>
|
||||
<string name="description_template">描述模板</string>
|
||||
<string name="explore_accounts">探索這個實例上熱門的使用者</string>
|
||||
<string name="popular_accounts">熱門使用者</string>
|
||||
<string name="explore_hashtags">探索此實例中急上升的推標</string>
|
||||
<string name="trending_hashtags">急上升推標</string>
|
||||
<string name="daily_trending">瀏覽每日熱門貼文</string>
|
||||
<string name="trending_posts">熱門貼文</string>
|
||||
<string name="grid_view">網格視圖</string>
|
||||
<string name="feed_view">饋送視圖</string>
|
||||
<string name="bookmarks">書簽</string>
|
||||
<string name="collections">合集</string>
|
||||
<string name="delete_collection">刪除合集</string>
|
||||
<string name="collection_add_post">加入貼文</string>
|
||||
<string name="collection_remove_post">移除貼文</string>
|
||||
<string name="add_to_collection">選取貼文以新增</string>
|
||||
<string name="delete_from_collection">選取貼文已移除</string>
|
||||
<string name="added_post_to_collection">已新增貼文到合集中</string>
|
||||
<string name="error_add_post_to_collection">無法將貼文新增至合集中</string>
|
||||
<string name="error_remove_post_from_collection">無法將貼文從合集中移除</string>
|
||||
<string name="removed_post_from_collection">已將貼文從合集中移除</string>
|
||||
<plurals name="replies_count">
|
||||
<item quantity="other">%d則回覆</item>
|
||||
</plurals>
|
||||
<string name="save">保存</string>
|
||||
<string name="more_profile_settings">更多資料設定</string>
|
||||
<string name="private_account">私密使用者</string>
|
||||
<string name="your_bio">你的自述</string>
|
||||
<string name="your_name">你的名字</string>
|
||||
<string name="profile_save_changes">不保存而退出嗎?</string>
|
||||
<string name="fetching_profile">獲取你的資料……</string>
|
||||
<string name="saving_profile">存儲你的資料中</string>
|
||||
<string name="profile_saved">更改已保存!</string>
|
||||
<string name="error_profile">發生錯誤。點按重試</string>
|
||||
<string name="change_profile_picture">修改你的資料相片</string>
|
||||
<string name="switch_accounts">切換使用者</string>
|
||||
<string name="summary_always_show_nsfw">敏感媒體將不會被模糊,默認展示。</string>
|
||||
<string name="contains_nsfw">包含敏感媒體</string>
|
||||
<string name="story_image">故事相片</string>
|
||||
<string name="replyToStory">回覆%1$s</string>
|
||||
<string name="error_fetch_story">獲取旋轉視圖的時候出了點問題</string>
|
||||
<string name="sent_reply_story">傳送了回覆</string>
|
||||
<string name="add_story">新增故事</string>
|
||||
<string name="story_could_not_see">錯誤:不能將故事標注為已看過</string>
|
||||
<string name="story_pause">開始或暫停故事</string>
|
||||
<string name="my_story">我的故事</string>
|
||||
<string name="type_story">故事</string>
|
||||
<string name="type_post">貼文</string>
|
||||
<string name="continue_post_creation">繼續</string>
|
||||
<string name="extraneous_pictures_stories">第一張相片后的相片已刪除,不過可以透過切換回創建貼文來恢復</string>
|
||||
<string name="story_duration">故事時長</string>
|
||||
<string name="write_permission_download_pic">你需要允許存儲空間權限才能保存相片!</string>
|
||||
<string name="unfollow">取消追蹤</string>
|
||||
<string name="cw_nsfw_hidden_media_n_click_to_show">敏感媒體
|
||||
\n (點按閲覽)</string>
|
||||
<string name="project_website">項目網站:https://pixeldroid.org</string>
|
||||
<plurals name="nb_following">
|
||||
<item quantity="other">%d
|
||||
\n追蹤中</item>
|
||||
</plurals>
|
||||
<string name="optional_report_comment">給管理員的附加資訊</string>
|
||||
<string name="new_collection_link_failed">無法打開合集創建頁面</string>
|
||||
<string name="license_info">PixelDroid是一個免費且開源的軟體,由GNU General Public License (version 3或更新版)授權</string>
|
||||
<string name="report_target">報告@%1$s的貼文</string>
|
||||
<string name="report_error">無法傳送報告</string>
|
||||
<string name="redraft_dialog_launch">重擬這則貼文將允許你編輯它的相片和描述,但會刪除現在所有的回覆和按讚。繼續?</string>
|
||||
<string name="notification_thumbnail">在這則貼文通知中的縮圖</string>
|
||||
<string name="private_account_explanation">當你的使用者為私密時,只有你認可的使用者才能看到你在Pixelfed上的相片和影片。現存的追蹤者不受影響。</string>
|
||||
<string name="mascot_description">一個展示了一隻紅熊貓(Pixelfed的吉祥物)用手機的相片</string>
|
||||
<string name="use_dynamic_color">用系統的動態顔色</string>
|
||||
<string name="story_reply_error">發表回覆的時候出了點問題</string>
|
||||
<string name="bookmark_post_failed_io_except">無法對此貼文進行書簽操作,檢查你的網路?</string>
|
||||
<string name="status_notification">%1$s建立了一個貼文</string>
|
||||
<string name="no_camera_permission">相機權限未獲取。如果想用攝影功能的話請在設定中允許該權限</string>
|
||||
<string name="accentColorSummary">選擇一個色調</string>
|
||||
<string name="explore_posts">瀏覽隨機貼文</string>
|
||||
<string name="delete_collection_warning">你確定要刪除此合集嗎?</string>
|
||||
</resources>
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<Transition
|
||||
android:id="@+id/first"
|
||||
app:constraintSetStart="@id/start"
|
||||
app:constraintSetEnd="@id/hideProfile">
|
||||
<OnSwipe
|
||||
app:touchAnchorId="@+id/profile"
|
||||
app:touchAnchorSide="top"
|
||||
app:dragDirection="dragUp" />
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
android:id="@+id/second"
|
||||
app:constraintSetStart="@id/hideProfile"
|
||||
app:constraintSetEnd="@id/hideBars">
|
||||
<OnSwipe
|
||||
app:touchAnchorId="@+id/profileTabs"
|
||||
app:touchAnchorSide="top"
|
||||
app:dragDirection="dragUp" />
|
||||
</Transition>
|
||||
|
||||
<ConstraintSet android:id="@+id/start">
|
||||
<Constraint android:id="@id/top_bar"
|
||||
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">
|
||||
</Constraint>
|
||||
</ConstraintSet>
|
||||
|
||||
<ConstraintSet android:id="@+id/hideProfile">
|
||||
<Constraint android:id="@id/profile"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/top_bar"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/profileTabs"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
</Constraint>
|
||||
|
||||
<Constraint android:id="@id/top_bar"
|
||||
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">
|
||||
</Constraint>
|
||||
</ConstraintSet>
|
||||
|
||||
<ConstraintSet android:id="@+id/hideBars">
|
||||
<Constraint android:id="@id/profile"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintBottom_toBottomOf="@id/top_bar"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
</Constraint>
|
||||
|
||||
<Constraint android:id="@id/profileTabs"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintBottom_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/profileTabs"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
</Constraint>
|
||||
</ConstraintSet>
|
||||
</MotionScene>
|
12
build.gradle
12
build.gradle
|
@ -6,7 +6,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.0'
|
||||
classpath 'com.android.tools.build:gradle:8.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
@ -16,6 +16,7 @@ buildscript {
|
|||
|
||||
plugins {
|
||||
id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false
|
||||
id("com.google.dagger.hilt.android") version "2.50" apply false
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
@ -23,15 +24,6 @@ allprojects {
|
|||
google()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
//noinspection JcenterRepositoryObsolete
|
||||
jcenter {
|
||||
content {
|
||||
// info.androidhive:imagefilters is only available in JCenter
|
||||
//TODO remove JCenter repo:
|
||||
// see issue https://gitlab.shinice.net/pixeldroid/PixelDroid/-/issues/278
|
||||
includeGroup("info.androidhive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Bug fix for opening album full-screen crashing
|
||||
|
||||
Use hardware acceleration (OpenGL! Shaders!) for image editing. Currently just trying to mimic the previous implementation, but this will provide building blocks for much cooler things later on :)
|
||||
|
||||
Tell us if something doesn't work right! (email, Mastodon)
|
||||
|
||||
There are also translation updates in this release :)
|
|
@ -0,0 +1 @@
|
|||
Fix bug that was breaking the image editing feature in release mode
|
|
@ -0,0 +1,3 @@
|
|||
* Fix crash when sharing an image from gallery etc to the app
|
||||
|
||||
* Fix permission issue causing images to throw a permission denied error
|
|
@ -0,0 +1 @@
|
|||
Split APKs per CPU architecture, makes the APKs a lot smaller (100MB to 25MB)
|
|
@ -0,0 +1,3 @@
|
|||
Try again to split the apks
|
||||
|
||||
Update dependencies
|
|
@ -0,0 +1,3 @@
|
|||
* Montage vidéo ! Supprimez le son, découper les vidéos
|
||||
* Ouvrez les images en plein écran et zoomez dessus :)
|
||||
* Mises à jour des traductions
|
|
@ -0,0 +1 @@
|
|||
Correctif pour les plantages dans l'activité d'édition qui ne se produisaient qu'en mode release
|
|
@ -0,0 +1 @@
|
|||
Ajout de la traduction en hongrois (Magyar) à l'application. Merci à Balázs :)
|
|
@ -0,0 +1,4 @@
|
|||
* Ajout de règles gson proguard pour corriger les crashs sur les instances Mastodon
|
||||
* Ajout de thèmes de couleur avec 4 thèmes différents
|
||||
* Passage à Material 3
|
||||
* Améliorer la cohérence de l'interface utilisateur
|
|
@ -0,0 +1,7 @@
|
|||
* Commentaires : leur ouverture permet de voir les réponses et l'avatar de l'auteur, l'aimer, etc
|
||||
* MAJ des traductions. Merci aux traducteurs :) Aidez à la traduction sur weblate.pixeldroid.org
|
||||
* Sécurité : altération des dépendances vérifiée, refus des connexions HTTP
|
||||
* Utilisation du User Agent "PixelDroid"
|
||||
* Suppression des chaînes codées en dur, tout est maintenant traduisible
|
||||
* Quelques améliorations du code
|
||||
* Correction de la sauvegarde des images et d'un crash sur des instances Mastodon
|
|
@ -0,0 +1,11 @@
|
|||
* Ajout d'un graphique d'erreur personnalisé panda rouge
|
||||
|
||||
* Autoriser le recadrage libre dans l'édition d'image
|
||||
|
||||
* Amélioration des métadonnées pour F-Droid
|
||||
|
||||
* Mises à jour des traductions
|
||||
|
||||
* Mise à jour des dépendances
|
||||
|
||||
* Correction de bugs
|
|
@ -0,0 +1,9 @@
|
|||
* Métadonnées des photos supprimées avant de les envoyer
|
||||
* Favoris !
|
||||
* Voir un profil comme un flux ou une grille
|
||||
* Définir un modèle pour vos descriptions
|
||||
* Badge sur l'icône de notification pour indiquer les nouvelles notifications
|
||||
* Fonctionnalités d'édition vidéo : recadrer, changer la vitesse, stabilisation
|
||||
* Implémentation de couleurs dynamiques : PixelDroid peut suive la couleur de votre arrière-plan (Android 12 et plus)
|
||||
* Correction de bugs
|
||||
* Mise à jour des traductions
|
|
@ -0,0 +1,7 @@
|
|||
* Mises à jour des traductions : Merci aux traducteurs ❤️ ! Allez sur notre weblate pour nous aider à la traduction.
|
||||
|
||||
#️⃣ Prise en charge des hashtags. Possibilité de parcourir les hashtags, au lieu d'afficher un "toast" message 😜
|
||||
|
||||
* Cliquez sur l'onglet pour revenir au début. Une fois dans la page, il suffit de cliquer sur l'onglet dans lequel vous vous trouvez pour revenir en haut :)
|
||||
|
||||
* Essai de correction d'un bug qui faisait planter l'application. Merci pour vos rapports de crash ! ❤️
|
|
@ -0,0 +1,4 @@
|
|||
- Suppression et reformulation des messages existants
|
||||
- Les collections d'articles peuvent désormais être visualisées et modifiées.
|
||||
- La création d'un message se fait désormais en deux étapes, avec une nouvelle prise en charge des éléments suivants : sensibilité NSFW, changement de compte, etc
|
||||
- Beaucoup d'autres changements et améliorations :)
|
|
@ -0,0 +1,4 @@
|
|||
* Messages d'avertissement moins agressifs si vous désactivez la caméra ou les permissions de fichiers
|
||||
* Mise à jour des traductions
|
||||
* Correction de l'interruption de l'envoi de vidéos
|
||||
* Amélioration de la gestion des erreurs
|
|
@ -0,0 +1,4 @@
|
|||
* Prise en charge des Stories !
|
||||
* Mises à jour des dépendances
|
||||
* Accélération matériel pour l'encodage vidéo
|
||||
* Beaucoup de travail en coulisse :)
|
|
@ -0,0 +1 @@
|
|||
Corrections de bugs et améliorations ;)
|
|
@ -0,0 +1,7 @@
|
|||
Correction d'un bug lors de l'ouverture d'un album en plein écran.
|
||||
|
||||
Utiliser l'accélération matérielle (OpenGL ! Shaders !) pour l'édition d'images. Pour l'instant, nous essayons juste d'imiter l'implémentation précédente, mais cela fournira des blocs de construction pour des choses bien plus cool par la suite :)
|
||||
|
||||
Dites-nous si quelque chose ne fonctionne pas correctement ! (email, Mastodon)
|
||||
|
||||
Il y a aussi des mises à jour de traduction dans cette version :)
|
|
@ -0,0 +1,7 @@
|
|||
* Ajout d'une langue (Malayalam)
|
||||
|
||||
* Correction de quelques mauvaises réponses de l'API
|
||||
|
||||
* Mise à jour des dépendances
|
||||
|
||||
* Mise à jour des traductions
|
|
@ -0,0 +1 @@
|
|||
Correction d'un bug qui cassait la fonction d'édition d'images en mode release
|
|
@ -0,0 +1,3 @@
|
|||
* Correction d'un crash lors du partage d'une image (de la galerie, etc.) vers l'application
|
||||
|
||||
* Correction d'un problème de permission qui causait de la part des images une erreur "permission refusée"
|
|
@ -0,0 +1 @@
|
|||
Séparation des APKs par architecture CPU, rendant les APKs beaucoup plus petits (passage de 100Mo à 25Mo)
|
|
@ -0,0 +1,3 @@
|
|||
Nouvelle tentative de séparation des apks
|
||||
|
||||
Mise à jour des dépendances
|
|
@ -0,0 +1,7 @@
|
|||
- Support des notifications ! Encore un peu rudimentaire :)
|
||||
- Correction : rotation EXIF ignorée, photos tournées dans le mauvais sens
|
||||
- Correction #300
|
||||
- Correction : navigateurs webview affichant une erreur à la connexion quand l'URL contenait des espaces
|
||||
- Correction : vidage des caches, flux vides aux lancements, performance, photos de profil et appareil photo en erreur après changement d'onglet
|
||||
- Traduction : ajout du Tchèque et mise à jour des autres langues
|
||||
- Dépendances mises à jour
|
|
@ -0,0 +1,5 @@
|
|||
* Mises à jour des traductions
|
||||
* Amélioration de l'affichage des informations sur les licences
|
||||
* Correction des problèmes de permission dans l'onglet caméra
|
||||
* Correction de l'analyseur de liens
|
||||
* Correction de la vue découverte (peut nécessiter des mises à jour de l'instance)
|
|
@ -0,0 +1,2 @@
|
|||
* Mises à jour des traductions
|
||||
* Autoriser l'envoi d'images distantes (ex. Nextcloud)
|
|
@ -0,0 +1,4 @@
|
|||
* Ajout d'un support pour la lecture et l'envoi de vidéos
|
||||
* Amélioration des notifications
|
||||
* Mise à jour des traductions
|
||||
* Mise à jour des dépendances
|
|
@ -0,0 +1,3 @@
|
|||
* Correction d'un crash lors de l'affichage de la liste des licences
|
||||
* Amélioration considérable des performances dans l'activité affichant le profil
|
||||
* Mise à jour des dépendances
|
|
@ -0,0 +1,11 @@
|
|||
PixelDroid est un client Android gratuit, libre et open source pour Pixelfed, la plateforme fédérée de partage d'images.
|
||||
|
||||
Parcourez les flux et les profils, envoyez de nouveaux messages, découvrez des messages, interagissez avec d'autres personnes sur le fediverse.
|
||||
|
||||
- Support multi-compte
|
||||
- Thèmes sombres et clairs
|
||||
- Appliquez des filtres, recadrez, modifiez la luminosité/contraste/saturation
|
||||
- Prend en compte la configuration de votre serveur Pixelfed
|
||||
- Logiciel 100% libre et open source. Aucune dépendances propriétaires.
|
||||
|
||||
Visitez https://pixelfed.org pour en savoir plus sur Pixelfed.
|
|
@ -0,0 +1 @@
|
|||
Client pour Pixelfed, la plateforme fédérée de partage d'images
|
|
@ -0,0 +1 @@
|
|||
PixelDroid
|
|
@ -0,0 +1 @@
|
|||
Correzione per i crash durante l'attività di modifica che si sono verificati solo nella modalità release
|
|
@ -0,0 +1 @@
|
|||
Aggiunta la traduzione Ungherese (Magyar) all'app. Grazie a Balàzs :)
|
|
@ -0,0 +1,4 @@
|
|||
* Aggiunte regole gson proguard per sistemare i crash nelle istanze Mastodon
|
||||
* Aggiunto color theming con 4 temi differenti
|
||||
* Passaggio a Material 3
|
||||
* Migliorata la coerenza dell'UI
|
|
@ -0,0 +1,8 @@
|
|||
* Migliorati i commenti: adesso si può aprire un commento per vedere le risposte, mettere mi piace, i commenti mostrano l'avatar dell'autore, ecc.
|
||||
* Aggiornate le traduzioni. Grazie traduttori :). Aiutaci a tradurre PixelDroid nella tua lingua su weblate.pixeldroid.org
|
||||
* Sicurezza: la verifica delle dipendenze assicura che quelle incluse nell'app non siano state manomesse, da adesso PixelDroid rifiuterà ogni connessione non-HTTPS
|
||||
* PixelDroid adesso usa lo User Agent personalizzato "PixelDroid" al posto di quello della libreria OkHttp
|
||||
* Tutte le stringhe presenti nel codice sono state rese traducibili
|
||||
* Alcuni miglioramenti al codice
|
||||
* Corretto il salvataggio di immagini prese dalla libreria o condivise con l'app
|
||||
* Corretto un altro crash nell'utilizzo delle istanze Mastodon
|
|
@ -0,0 +1,11 @@
|
|||
* Aggiunta grafica di errore personalizzata del panda rosso
|
||||
|
||||
* Supporto per il ritaglio libero nella modifica dell'immagine
|
||||
|
||||
* Migliorati i metadati di F-Droid
|
||||
|
||||
* Aggiornamento delle traduzioni
|
||||
|
||||
* Aggiornamenti delle dipendenze
|
||||
|
||||
* Correzione bug
|
|
@ -1,2 +1,2 @@
|
|||
* Traduzione aggiornata
|
||||
* Consenti il caricamento di immagini da remoto (e.g. Nextcloud)
|
||||
* Aggiornata la traduzione
|
||||
* Consentito il caricamento di immagini da remoto (e.g. Nextcloud)
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
* Редагування відео! Вимкнення звуку, обрізка відео
|
||||
* Відкривайте зображення на весь екран, масштабуйте та переміщуйте їх :)
|
||||
* Оновлення перекладів
|
|
@ -0,0 +1 @@
|
|||
Виправлення збоїв під час редагування, які траплялися лише у режимі релізу
|
|
@ -0,0 +1 @@
|
|||
Додано переклад угорською (мадярською) мовою до додатку. Дякуємо Balazs :)
|
|
@ -0,0 +1,4 @@
|
|||
* Додано правила gson proguard для виправлення збоїв на екземплярах Mastodon
|
||||
* Додано кольорове оформлення з 4 різними темами
|
||||
* Перейшли до Material 3
|
||||
* Покращено узгодженість інтерфейсу користувача
|
|
@ -0,0 +1,8 @@
|
|||
* покращено роботу з коментарями: Тепер ви можете відкрити коментар, щоб побачити відповіді, поставити вподобайку, у коментарях відображається аватар його автора тощо.
|
||||
* Оновлення перекладів. Дякуємо перекладачам :). Допоможіть перекласти PixelDroid на вашу мову на weblate.pixeldroid.org
|
||||
* Безпека: перевірка залежностей гарантує, що залежності, включені в додаток, не були підроблені, PixelDroid тепер відмовлятиметься від будь-яких не-HTTPS з'єднань
|
||||
* PixelDroid тепер використовує власний агент користувача "PixelDroid" замість бібліотечного OkHttp.
|
||||
* Видалено всі жорстко закодовані рядки з програми, тепер все нормально перекладається.
|
||||
* Деякі покращення коду
|
||||
* Виправлено збереження зображень, що надходять до програми з бібліотеки або надаються у спільний доступ
|
||||
* Виправлено ще один збій при використанні екземплярів Mastodon
|
|
@ -0,0 +1 @@
|
|||
Виправлено помилку, яка порушувала роботу функції редагування зображень у режимі релізу
|
|
@ -0,0 +1,3 @@
|
|||
* Виправлено збій під час надання доступу до зображення з галереї тощо у додатку
|
||||
|
||||
* Виправлено проблему з дозволами, через яку зображення видавали помилку відмови в доступі
|
|
@ -0,0 +1 @@
|
|||
Розділення APK-файлів по архітектурах процесорів, що робить APK-файли значно меншими (від 100 МБ до 25 МБ)
|
|
@ -0,0 +1,3 @@
|
|||
Спробуйте ще раз розділити apk файли
|
||||
|
||||
Оновити залежності
|
|
@ -16,7 +16,7 @@ org.gradle.jvmargs=-Xmx1536m
|
|||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=false
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
org.gradle.daemon=true
|
||||
|
@ -26,3 +26,5 @@ kapt.incremental.apt=true
|
|||
android.enableR8.fullMode=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
# We want this to be true but it wasn't to be (About library doesn't like it https://github.com/mikepenz/AboutLibraries/issues/857)
|
||||
org.gradle.configuration-cache=false
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#Fri Oct 14 13:37:44 GMT 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
|
||||
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit c093bde49b53f61628289be08d09f4e9dee4a48f
|
||||
Subproject commit 702d14fe701343958337efa1b4eb31f0250849f6
|
|
@ -1 +1 @@
|
|||
Subproject commit 7c67b911930b4344a2917f2944493e08fdd04b57
|
||||
Subproject commit 23d4d94b45a848f0c64a042985eb03d0acc2f18b
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue