Compare commits
75 Commits
1.0.beta26
...
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 | |
Matthieu | 370aeda4a6 | |
Matthieu | eb65e24099 | |
Fred | 1ea4371b3e | |
Matthieu | 2e399884e9 | |
Matthieu | 1dcf605976 | |
Fred | 06478cf8a7 | |
Fred | 580f7ca911 | |
Matthieu | 23fbebfe44 | |
Fred | 6602d912a9 | |
Matthieu | 1f41268d55 | |
Francesco Marinucci | 15efdcabad | |
Matthieu | 97069a76db | |
Francesco Marinucci | 834b3f86bd | |
ButterflyOfFire | adc4ef0199 | |
Matthieu | dafe827a5c | |
Matthieu | 0f8602b3f1 | |
Matthieu | 6edb394ccb | |
Weblate | 13fa45cdb0 | |
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 26
|
||||
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"
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.view.View
|
|||
import android.widget.ImageView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -29,6 +30,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
|||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
|
@ -36,7 +38,12 @@ import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
|||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
|
@ -53,10 +60,10 @@ import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
|
|||
import org.pixeldroid.app.settings.SettingsActivity
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.objects.Notification
|
||||
import org.pixeldroid.app.utils.db.addUser
|
||||
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.hasInternet
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
|
||||
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
|
||||
|
@ -71,6 +78,8 @@ class MainActivity : BaseActivity() {
|
|||
private lateinit var header: AccountHeaderView
|
||||
private var user: UserDatabaseEntity? = null
|
||||
|
||||
private val model: MainActivityViewModel by viewModels()
|
||||
|
||||
companion object {
|
||||
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
|
||||
}
|
||||
|
@ -196,6 +205,7 @@ class MainActivity : BaseActivity() {
|
|||
Glide.with(this@MainActivity)
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.circleCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
|
@ -264,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)
|
||||
}
|
||||
}
|
||||
|
@ -281,16 +291,12 @@ class MainActivity : BaseActivity() {
|
|||
|
||||
lifecycleScope.launchWhenCreated {
|
||||
try {
|
||||
val domain = user?.instance_uri.orEmpty()
|
||||
val accessToken = user?.accessToken.orEmpty()
|
||||
val refreshToken = user?.refreshToken
|
||||
val clientId = user?.clientId.orEmpty()
|
||||
val clientSecret = user?.clientSecret.orEmpty()
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val account = api.verifyCredentials()
|
||||
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
|
||||
fillDrawerAccountInfo(account.id!!)
|
||||
updateUserInfoDb(db, account)
|
||||
|
||||
//No need to update drawer account info here, the ViewModel listens to db updates
|
||||
} catch (exception: Exception) {
|
||||
Log.e("ACCOUNT UPDATE:", exception.toString())
|
||||
}
|
||||
|
@ -322,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 {
|
||||
|
@ -337,35 +345,41 @@ class MainActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun fillDrawerAccountInfo(account: String) {
|
||||
val users = db.userDao().getAll().toMutableList()
|
||||
users.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.users.collect { list ->
|
||||
val users = list.toMutableList()
|
||||
users.sortWith { l, r ->
|
||||
when {
|
||||
l.isActive && !r.isActive -> -1
|
||||
r.isActive && !l.isActive -> 1
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
val profiles: MutableList<IProfile> = users.map { user ->
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = user.isActive
|
||||
nameText = user.display_name
|
||||
iconUrl = user.avatar_static
|
||||
isNameShown = true
|
||||
identifier = user.user_id.toLong()
|
||||
descriptionText = user.fullHandle
|
||||
tag = user.instance_uri
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
header.profiles.orEmpty()
|
||||
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
|
||||
.take(1)
|
||||
.forEach { profiles.add(it) }
|
||||
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(account.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
val profiles: MutableList<IProfile> = users.map { user ->
|
||||
ProfileDrawerItem().apply {
|
||||
isSelected = user.isActive
|
||||
nameText = user.display_name
|
||||
iconUrl = user.avatar_static
|
||||
isNameShown = true
|
||||
identifier = user.user_id.toLong()
|
||||
descriptionText = user.fullHandle
|
||||
tag = user.instance_uri
|
||||
}
|
||||
}.toMutableList()
|
||||
|
||||
// reuse the already existing "add account" item
|
||||
header.profiles.orEmpty()
|
||||
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
|
||||
.take(1)
|
||||
.forEach { profiles.add(it) }
|
||||
|
||||
header.clear()
|
||||
header.profiles = profiles
|
||||
header.setActiveProfile(account.toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package org.pixeldroid.app
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainActivityViewModel @Inject constructor(
|
||||
private val db: AppDatabase
|
||||
): ViewModel() {
|
||||
|
||||
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
|
||||
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
|
||||
|
||||
// Immutable state flow exposed to UI
|
||||
val users = _users.asStateFlow()
|
||||
|
||||
|
||||
init {
|
||||
getUsers()
|
||||
}
|
||||
|
||||
private fun getUsers() {
|
||||
viewModelScope.launch {
|
||||
db.userDao().getAllFlow().flowOn(Dispatchers.IO)
|
||||
.collect { users: List<UserDatabaseEntity> ->
|
||||
_users.update { users }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,43 +1,57 @@
|
|||
package org.pixeldroid.app.postCreation
|
||||
|
||||
import android.os.*
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
|
||||
import org.pixeldroid.app.utils.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
|
||||
|
|
|
@ -73,6 +73,7 @@ internal fun <T: Any> initAdapter(
|
|||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
adapter.refresh()
|
||||
adapter.notifyDataSetChanged()
|
||||
header?.refreshStories()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()){
|
||||
|
@ -51,6 +48,7 @@ class EditProfileActivity : BaseActivity() {
|
|||
}.show()
|
||||
} else {
|
||||
this.isEnabled = false
|
||||
if (model.submittedChanges) setResult(RESULT_OK)
|
||||
super.onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
@ -58,23 +56,24 @@ class EditProfileActivity : BaseActivity() {
|
|||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
model.uiState.collect { uiState ->
|
||||
if(uiState.profileLoaded){
|
||||
binding.bioEditText.setText(uiState.bio)
|
||||
binding.nameEditText.setText(uiState.name)
|
||||
model.changesApplied()
|
||||
}
|
||||
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
|
||||
if(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
|
||||
if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
|
||||
|
||||
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
|
||||
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
|
||||
|
||||
binding.privateSwitch.isChecked = uiState.privateAccount == true
|
||||
Glide.with(binding.profilePic).load(uiState.profilePictureUri)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(binding.profilePic)
|
||||
|
||||
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE
|
||||
else View.VISIBLE
|
||||
binding.savingProgressBar.visibility =
|
||||
if(uiState.error || (uiState.profileSent && !uiState.uploadingPicture)) View.GONE
|
||||
else View.VISIBLE
|
||||
|
||||
if(uiState.profileSent){
|
||||
if(uiState.profileSent && !uiState.uploadingPicture && !uiState.error){
|
||||
binding.progressText.setText(R.string.profile_saved)
|
||||
binding.done.visibility = View.VISIBLE
|
||||
} else {
|
||||
|
@ -112,18 +111,18 @@ class EditProfileActivity : BaseActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// binding.changeImageButton.setOnClickListener {
|
||||
// Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
// type = "*/*"
|
||||
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
|
||||
// action = Intent.ACTION_GET_CONTENT
|
||||
// addCategory(Intent.CATEGORY_OPENABLE)
|
||||
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
// uploadImageResultContract.launch(
|
||||
// Intent.createChooser(this, null)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
binding.profilePic.setOnClickListener {
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
|
||||
action = Intent.ACTION_GET_CONTENT
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
|
||||
uploadImageResultContract.launch(
|
||||
Intent.createChooser(this, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
|
@ -137,10 +136,10 @@ class EditProfileActivity : BaseActivity() {
|
|||
val imageUri: String = clipData.getItemAt(i).uri.toString()
|
||||
images.add(imageUri)
|
||||
}
|
||||
model.uploadImage(images.first())
|
||||
model.updateImage(images.first())
|
||||
} else if (data.data != null) {
|
||||
images.add(data.data!!.toString())
|
||||
model.uploadImage(images.first())
|
||||
model.updateImage(images.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package org.pixeldroid.app.profile
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Editable
|
||||
import android.util.Log
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
|
@ -21,23 +21,33 @@ import kotlinx.coroutines.launch
|
|||
import okhttp3.MultipartBody
|
||||
import org.pixeldroid.app.postCreation.ProgressRequestBody
|
||||
import org.pixeldroid.app.posts.fromHtml
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@HiltViewModel
|
||||
class EditProfileViewModel @Inject constructor(
|
||||
@ApplicationContext private val applicationContext: Context
|
||||
): ViewModel() {
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
|
||||
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
|
||||
|
||||
var oldProfile: Account? = null
|
||||
private var oldProfile: Account? = null
|
||||
|
||||
var submittedChanges = false
|
||||
private set
|
||||
|
||||
init {
|
||||
(application as PixelDroidApplication).getAppComponent().inject(this)
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
|
@ -46,6 +56,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
try {
|
||||
val profile = api.verifyCredentials()
|
||||
updateUserInfoDb(db, profile)
|
||||
if (oldProfile == null) oldProfile = profile
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
|
@ -76,15 +87,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
fun sendProfile() {
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
|
||||
val requestBody =
|
||||
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
|
||||
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
sendingProfile = true,
|
||||
profileSent = false,
|
||||
loadingProfile = false,
|
||||
profileLoaded = false,
|
||||
error = false
|
||||
)
|
||||
}
|
||||
|
@ -97,12 +103,17 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
note = bio,
|
||||
locked = privateAccount,
|
||||
)
|
||||
if (madeChanges()) submittedChanges = true
|
||||
oldProfile = account
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()},
|
||||
bio = account.source?.note
|
||||
?: account.note?.let { fromHtml(it).toString() },
|
||||
name = account.display_name,
|
||||
profilePictureUri = account.anyAvatar()?.toUri(),
|
||||
profilePictureUri = if (profilePictureChanged) profilePictureUri
|
||||
else account.anyAvatar()?.toUri(),
|
||||
uploadProgress = 0,
|
||||
uploadingPicture = profilePictureChanged,
|
||||
privateAccount = account.locked,
|
||||
sendingProfile = false,
|
||||
profileSent = true,
|
||||
|
@ -111,14 +122,13 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
error = false
|
||||
)
|
||||
}
|
||||
if(profilePictureChanged) uploadImage()
|
||||
} catch (exception: Exception) {
|
||||
Log.e("TAG", exception.toString())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
sendingProfile = false,
|
||||
profileSent = false,
|
||||
loadingProfile = false,
|
||||
profileLoaded = false,
|
||||
error = true
|
||||
)
|
||||
}
|
||||
|
@ -145,20 +155,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
}
|
||||
}
|
||||
|
||||
fun changesApplied() {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(profileLoaded = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun madeChanges(): Boolean =
|
||||
with(uiState.value) {
|
||||
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio }
|
||||
// If source note is null, check note
|
||||
val privateChanged = oldProfile?.locked != privateAccount
|
||||
val displayNameChanged = oldProfile?.display_name != name
|
||||
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
|
||||
// If source note is null, check note
|
||||
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
|
||||
?: true
|
||||
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|
||||
|| bioUnchanged
|
||||
|
||||
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
|
||||
}
|
||||
|
||||
fun clickedCard() {
|
||||
|
@ -178,16 +184,27 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
}
|
||||
}
|
||||
|
||||
fun uploadImage(image: String) {
|
||||
//TODO fix
|
||||
fun updateImage(image: String) {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureUri = image.toUri(),
|
||||
profilePictureChanged = true,
|
||||
profileSent = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadImage() {
|
||||
val image = uiState.value.profilePictureUri!!
|
||||
|
||||
val inputStream =
|
||||
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri())
|
||||
applicationContext.contentResolver.openInputStream(image)
|
||||
?: return
|
||||
|
||||
val size: Long =
|
||||
if (image.toUri().scheme == "content") {
|
||||
getApplication<PixelDroidApplication>().contentResolver.query(
|
||||
image.toUri(),
|
||||
if (image.scheme == "content") {
|
||||
applicationContext.contentResolver.query(
|
||||
image,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
@ -203,7 +220,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
cursor.getLong(sizeIndex)
|
||||
} ?: 0
|
||||
} else {
|
||||
image.toUri().toFile().length()
|
||||
image.toFile().length()
|
||||
}
|
||||
|
||||
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
|
||||
|
@ -225,21 +242,32 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
var postSub: Disposable? = null
|
||||
|
||||
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
|
||||
val inter = api.updateProfilePicture(requestBody.parts[0])
|
||||
|
||||
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
|
||||
|
||||
val inter =
|
||||
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
|
||||
else api.updateProfilePictureMastodon(requestBody.parts[0])
|
||||
|
||||
postSub = inter
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ it: Account ->
|
||||
Log.e("qsdfqsdfs", it.toString())
|
||||
|
||||
/* onNext = */ { account: Account ->
|
||||
account.anyAvatar()?.let {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureUri = it.toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ e: Throwable ->
|
||||
/* onError = */ { e: Throwable ->
|
||||
Log.e("error", (e as? HttpException)?.message().orEmpty())
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
uploadProgress = 0,
|
||||
uploadingPicture = true,
|
||||
uploadingPicture = false,
|
||||
error = true
|
||||
)
|
||||
}
|
||||
|
@ -247,9 +275,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
postSub?.dispose()
|
||||
sub.dispose()
|
||||
},
|
||||
{
|
||||
/* onComplete = */ {
|
||||
_uiState.update { currentUiState ->
|
||||
currentUiState.copy(
|
||||
profilePictureChanged = false,
|
||||
uploadProgress = 100,
|
||||
uploadingPicture = false
|
||||
)
|
||||
|
@ -265,7 +294,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
|
|||
data class EditProfileActivityUiState(
|
||||
val name: String? = null,
|
||||
val bio: String? = null,
|
||||
val profilePictureUri: Uri?= null,
|
||||
val profilePictureUri: Uri? = null,
|
||||
val profilePictureChanged: Boolean = false,
|
||||
val privateAccount: Boolean? = null,
|
||||
val loadingProfile: Boolean = true,
|
||||
val profileLoaded: Boolean = false,
|
||||
|
@ -274,10 +304,4 @@ data class EditProfileActivityUiState(
|
|||
val error: Boolean = false,
|
||||
val uploadingPicture: Boolean = false,
|
||||
val uploadProgress: Int = 0,
|
||||
)
|
||||
|
||||
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.getConstructor(Application::class.java).newInstance(application)
|
||||
}
|
||||
}
|
||||
)
|
|
@ -6,21 +6,28 @@ import android.text.method.LinkMovementMethod
|
|||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import kotlinx.coroutines.launch
|
||||
import org.pixeldroid.app.R
|
||||
import org.pixeldroid.app.databinding.ActivityProfileBinding
|
||||
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
|
||||
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
|
||||
import org.pixeldroid.app.posts.parseHTMLText
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI
|
||||
import org.pixeldroid.app.utils.api.objects.Account
|
||||
import org.pixeldroid.app.utils.api.objects.FeedContent
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import org.pixeldroid.app.utils.db.updateUserInfoDb
|
||||
import org.pixeldroid.app.utils.setProfileImageFromURL
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
@ -54,9 +61,32 @@ 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<Fragment>{
|
||||
private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
|
||||
|
||||
val profileFeedFragment = ProfileFeedFragment()
|
||||
profileFeedFragment.arguments = Bundle().apply {
|
||||
|
@ -80,7 +110,7 @@ class ProfileActivity : BaseActivity() {
|
|||
putSerializable(ProfileFeedFragment.COLLECTIONS, true)
|
||||
}
|
||||
|
||||
val returnArray: Array<Fragment> = arrayOf(
|
||||
val returnArray: Array<UncachedFeedFragment<FeedContent>> = arrayOf(
|
||||
profileGridFragment,
|
||||
profileFeedFragment,
|
||||
profileCollectionsFragment
|
||||
|
@ -100,7 +130,7 @@ class ProfileActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun setupTabs(
|
||||
tabs: Array<Fragment>
|
||||
tabs: Array<UncachedFeedFragment<FeedContent>>,
|
||||
){
|
||||
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
|
@ -132,8 +162,15 @@ 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?) {
|
||||
if(account != null) {
|
||||
|
@ -152,6 +189,9 @@ class ProfileActivity : BaseActivity() {
|
|||
).show()
|
||||
return@launchWhenResumed
|
||||
}
|
||||
|
||||
updateUserInfoDb(db, myAccount)
|
||||
|
||||
setViews(myAccount)
|
||||
}
|
||||
}
|
||||
|
@ -217,9 +257,15 @@ class ProfileActivity : BaseActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
private val editResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
// Profile was edited, reload
|
||||
setContent(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickEditButton() {
|
||||
val intent = Intent(this, EditProfileActivity::class.java)
|
||||
ContextCompat.startActivity(this, intent, null)
|
||||
editResult.launch(Intent(this, EditProfileActivity::class.java))
|
||||
}
|
||||
|
||||
private fun onClickFollowers(account: Account?) {
|
||||
|
|
|
@ -101,7 +101,7 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
|
|||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
if(grid || bookmarks || collections || addCollection) {
|
||||
binding.list.layoutManager = GridLayoutManager(context, 3)
|
||||
binding?.list?.layoutManager = GridLayoutManager(context, 3)
|
||||
}
|
||||
|
||||
// Get the view model
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -338,18 +338,31 @@ interface PixelfedAPI {
|
|||
@Header("Authorization") authorization: String? = null
|
||||
): Account
|
||||
|
||||
//@Multipart
|
||||
@PATCH("/api/v1/accounts/update_credentials")
|
||||
suspend fun updateCredentials(
|
||||
@Query(value = "display_name") displayName: String?,
|
||||
@Query(value = "note") note: String?,
|
||||
@Query(value = "locked") locked: Boolean?,
|
||||
// @Part avatar: MultipartBody.Part?,
|
||||
): Account
|
||||
|
||||
/**
|
||||
* Pixelfed uses PHP, multipart uploads don't work through PATCH so we use POST as suggested
|
||||
* here: https://github.com/pixelfed/pixelfed/issues/4250
|
||||
* However, changing to POST breaks the upload on Mastodon.
|
||||
*
|
||||
* To have this work on Pixelfed and Mastodon without special logic to distinguish the two,
|
||||
* we'll have to wait for PHP 8.4 and https://wiki.php.net/rfc/rfc1867-non-post
|
||||
* which should come out end of 2024
|
||||
*/
|
||||
@Multipart
|
||||
@POST("/api/v1/accounts/update_credentials")
|
||||
fun updateProfilePicture(
|
||||
@Part avatar: MultipartBody.Part?
|
||||
): Observable<Account>
|
||||
|
||||
@Multipart
|
||||
@PATCH("/api/v1/accounts/update_credentials")
|
||||
fun updateProfilePicture(
|
||||
fun updateProfilePictureMastodon(
|
||||
@Part avatar: MultipartBody.Part?
|
||||
): Observable<Account>
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
|
|||
PublicFeedStatusDatabaseEntity::class,
|
||||
Notification::class
|
||||
],
|
||||
version = 5
|
||||
version = 6
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
@ -44,4 +44,9 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
|
|||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
||||
val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
|
@ -13,41 +13,58 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
|
|||
import org.pixeldroid.app.utils.normalizeDomain
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
|
||||
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) {
|
||||
suspend fun addUser(
|
||||
db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
|
||||
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
|
||||
) {
|
||||
db.userDao().insertOrUpdate(
|
||||
UserDatabaseEntity(
|
||||
user_id = account.id!!,
|
||||
instance_uri = normalizeDomain(instance_uri),
|
||||
username = account.username!!,
|
||||
display_name = account.getDisplayName(),
|
||||
avatar_static = account.anyAvatar().orEmpty(),
|
||||
isActive = activeUser,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
UserDatabaseEntity(
|
||||
user_id = account.id!!,
|
||||
instance_uri = normalizeDomain(instance_uri),
|
||||
username = account.username!!,
|
||||
display_name = account.getDisplayName(),
|
||||
avatar_static = account.anyAvatar().orEmpty(),
|
||||
isActive = activeUser,
|
||||
accessToken = accessToken,
|
||||
refreshToken = refreshToken,
|
||||
clientId = clientId,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
db.userDao().updateUserAccountDetails(
|
||||
account.username.orEmpty(),
|
||||
account.display_name.orEmpty(),
|
||||
account.anyAvatar().orEmpty(),
|
||||
user.user_id,
|
||||
user.instance_uri
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
|
||||
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(metadata?.config?.site?.url!!),
|
||||
title = metadata.config.site.name!!,
|
||||
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
|
||||
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE,
|
||||
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
|
||||
?: DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Pixelfed doesn't distinguish between max photo and video size
|
||||
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE,
|
||||
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
|
||||
?: DEFAULT_MAX_VIDEO_SIZE,
|
||||
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
|
||||
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED
|
||||
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED,
|
||||
pixelfed = metadata.software?.repo?.contains("pixelfed", ignoreCase = true) == true
|
||||
)
|
||||
} ?: instance?.run {
|
||||
InstanceDatabaseEntity(
|
||||
uri = normalizeDomain(uri.orEmpty()),
|
||||
title = title.orEmpty(),
|
||||
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
|
||||
uri = normalizeDomain(uri.orEmpty()),
|
||||
title = title.orEmpty(),
|
||||
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
|
||||
pixelfed = false
|
||||
)
|
||||
} ?: throw IllegalArgumentException("Cannot store instance where both are null")
|
||||
|
||||
|
|
|
@ -1,27 +1,33 @@
|
|||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface InstanceDao {
|
||||
@Query("SELECT * FROM instances")
|
||||
fun getAll(): List<InstanceDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=:instanceUri")
|
||||
fun getInstance(instanceUri: String): InstanceDatabaseEntity
|
||||
|
||||
|
||||
@Query("SELECT * FROM instances WHERE uri=(SELECT users.instance_uri FROM users WHERE isActive=1)")
|
||||
fun getActiveInstance(): InstanceDatabaseEntity
|
||||
|
||||
/**
|
||||
* Insert an instance, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
suspend fun insertInstance(instance: InstanceDatabaseEntity): Long
|
||||
|
||||
@Update
|
||||
fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
suspend fun updateInstance(instance: InstanceDatabaseEntity)
|
||||
|
||||
@Transaction
|
||||
fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) {
|
||||
if (insertInstance(instance) == -1L) {
|
||||
updateInstance(instance)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
package org.pixeldroid.app.utils.db.dao
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
|
||||
@Dao
|
||||
|
@ -9,17 +15,21 @@ interface UserDao {
|
|||
* Insert a user, if it already exists return -1
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertUser(user: UserDatabaseEntity): Long
|
||||
suspend fun insertUser(user: UserDatabaseEntity): Long
|
||||
|
||||
@Transaction
|
||||
fun insertOrUpdate(user: UserDatabaseEntity) {
|
||||
suspend fun insertOrUpdate(user: UserDatabaseEntity) {
|
||||
if (insertUser(user) == -1L) {
|
||||
updateUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
@Update
|
||||
fun updateUser(user: UserDatabaseEntity)
|
||||
suspend fun updateUser(user: UserDatabaseEntity)
|
||||
|
||||
@Query("UPDATE users SET username = :username, display_name = :displayName, avatar_static = :avatarStatic WHERE user_id = :id and instance_uri = :instanceUri")
|
||||
suspend fun updateUserAccountDetails(username: String, displayName: String, avatarStatic: String, id: String, instanceUri: String)
|
||||
|
||||
|
||||
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
|
||||
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
|
||||
|
@ -27,6 +37,9 @@ interface UserDao {
|
|||
@Query("SELECT * FROM users")
|
||||
fun getAll(): List<UserDatabaseEntity>
|
||||
|
||||
@Query("SELECT * FROM users")
|
||||
fun getAllFlow(): Flow<List<UserDatabaseEntity>>
|
||||
|
||||
@Query("SELECT * FROM users WHERE isActive=1")
|
||||
fun getActiveUser(): UserDatabaseEntity?
|
||||
|
||||
|
|
|
@ -4,20 +4,22 @@ import androidx.room.Entity
|
|||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "instances")
|
||||
data class InstanceDatabaseEntity (
|
||||
@PrimaryKey var uri: String,
|
||||
var title: String,
|
||||
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
|
||||
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
|
||||
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Mastodon has different file limits for videos, default of 40MB
|
||||
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
|
||||
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
|
||||
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
|
||||
// Is video functionality enabled on this instance?
|
||||
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
|
||||
data class InstanceDatabaseEntity(
|
||||
@PrimaryKey var uri: String,
|
||||
var title: String,
|
||||
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
|
||||
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
|
||||
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
|
||||
// Mastodon has different file limits for videos, default of 40MB
|
||||
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
|
||||
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
|
||||
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
|
||||
// Is video functionality enabled on this instance?
|
||||
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
|
||||
// Is this Pixelfed instance?
|
||||
var pixelfed: Boolean = true,
|
||||
) {
|
||||
companion object{
|
||||
companion object {
|
||||
// Default max number of chars for Mastodon: used when their is no other value supplied by
|
||||
// either NodeInfo or the instance endpoint
|
||||
const val DEFAULT_MAX_TOOT_CHARS = 500
|
||||
|
|
|
@ -6,13 +6,16 @@ import org.pixeldroid.app.utils.db.AppDatabase
|
|||
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.*
|
||||
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class APIModule{
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class APIModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
@ -54,7 +57,7 @@ class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase, val
|
|||
client_secret = user.clientSecret
|
||||
)
|
||||
}
|
||||
}catch (e: Exception){
|
||||
} catch (e: Exception){
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import org.pixeldroid.app.utils.BaseActivity
|
||||
import org.pixeldroid.app.utils.PixelDroidApplication
|
||||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import org.pixeldroid.app.utils.BaseFragment
|
||||
import dagger.Component
|
||||
import org.pixeldroid.app.postCreation.PostCreationViewModel
|
||||
import org.pixeldroid.app.profile.EditProfileViewModel
|
||||
import org.pixeldroid.app.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)
|
||||
|
||||
val context: Context?
|
||||
val application: Application?
|
||||
val database: AppDatabase
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package org.pixeldroid.app.utils.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
class ApplicationModule(app: Application) {
|
||||
private val mApplication: Application = app
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideContext(): Context {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideApplication(): Application {
|
||||
return mApplication
|
||||
}
|
||||
|
||||
}
|
|
@ -5,20 +5,27 @@ import androidx.room.Room
|
|||
import org.pixeldroid.app.utils.db.AppDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_3_4
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_4_5
|
||||
import org.pixeldroid.app.utils.db.MIGRATION_5_6
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
class DatabaseModule(private val context: Context) {
|
||||
class DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDatabase(): AppDatabase {
|
||||
fun providesDatabase(
|
||||
@ApplicationContext applicationContext: Context
|
||||
): AppDatabase {
|
||||
return Room.databaseBuilder(
|
||||
context,
|
||||
applicationContext,
|
||||
AppDatabase::class.java, "pixeldroid"
|
||||
).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5)
|
||||
).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
|
||||
.allowMainThreadQueries().build()
|
||||
}
|
||||
}
|
|
@ -32,9 +32,6 @@ import java.io.IOException
|
|||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
||||
|
||||
class NotificationsWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
|
@ -46,9 +43,6 @@ class NotificationsWorker(
|
|||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
|
||||
(applicationContext as PixelDroidApplication).getAppComponent().inject(this)
|
||||
|
||||
val users: List<UserDatabaseEntity> = db.userDao().getAll()
|
||||
|
||||
for (user in users){
|
||||
|
@ -306,8 +300,7 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString())
|
||||
} else {
|
||||
val types: MutableList<Notification.NotificationType?> =
|
||||
Notification.NotificationType.values().toMutableList()
|
||||
val types: MutableList<Notification.NotificationType?> = entries.toMutableList()
|
||||
types += null
|
||||
|
||||
types.forEach {
|
||||
|
|
|
@ -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"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text|textCapSentences|textMultiLine" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submitComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/submit_comment"
|
||||
android:text="@string/comment"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
|
||||
android:background="?attr/colorSecondaryContainer"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintPost"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/top_bar">
|
||||
|
||||
<include
|
||||
android:id="@+id/postFragmentSingle"
|
||||
layout="@layout/post_fragment" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/commentIn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintPost"
|
||||
tools:layout_editor_absoluteX="10dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/textInputLayout2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/submitComment"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editComment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/comment_noun"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="text|textCapSentences|textMultiLine" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/submitComment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/submit_comment"
|
||||
android:text="@string/comment_verb"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
|
||||
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/commentFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="500dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/commentIn" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
android:id="@+id/commentFragment"
|
||||
/>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -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>
|
|
@ -48,7 +48,7 @@
|
|||
<string name="loading_toast">حدث خلل اثناء التحميل</string>
|
||||
<string name="upload_post_error">فشل في تحميل المنشور</string>
|
||||
|
||||
<string name="comment">تعليق</string>
|
||||
<string name="comment_verb">تعليق</string>
|
||||
<string name="comment_posted">التعليق: تم نشر%1$s!</string>
|
||||
<string name="comment_error">خطأ في التعليق!</string>
|
||||
<string name="share_image">مشاركة الصورة</string>
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<string name="follow_error">No s\'ha pogut seguir</string>
|
||||
<string name="follow_button_failed">No s\'ha pogut mostrar el botó de seguir</string>
|
||||
<string name="follow_status_failed">No s\'ha pogut obtenir l\'estat de seguiment</string>
|
||||
<string name="comment">Comentari</string>
|
||||
<string name="comment_verb">Comentari</string>
|
||||
<string name="comment_posted">Comentari: %1$s publicat!</string>
|
||||
<string name="comment_error">Error de comentari!</string>
|
||||
<string name="share_image">Compartir imatge</string>
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
<string name="share_image">Sdílet obrázek</string>
|
||||
<string name="comment_error">Chyba komentáře!</string>
|
||||
<string name="comment_posted">Komentář: %1$s zveřejněn!</string>
|
||||
<string name="comment">Komentář</string>
|
||||
<string name="comment_verb">Komentář</string>
|
||||
<plurals name="number_comments">
|
||||
<item quantity="one">%d komentář</item>
|
||||
<item quantity="few">%d komentáře</item>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<string name="add_account_description">Weiteres Pixelfed-Konto hinzufügen</string>
|
||||
<string name="light_theme">Hell</string>
|
||||
<string name="dark_theme">Dunkel</string>
|
||||
<string name="comment">Kommentieren</string>
|
||||
<string name="comment_verb">Kommentieren</string>
|
||||
<string name="share_image">Bild teilen</string>
|
||||
<string name="empty_comment">Der Kommentar darf nicht leer sein!</string>
|
||||
<string name="no_description">Keine Beschreibung</string>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<string name="follow_error">No pudo seguir</string>
|
||||
<string name="follow_button_failed">No se pudo mostrar el botón de seguimiento</string>
|
||||
<string name="follow_status_failed">No se pudo obtener el estado de seguidores</string>
|
||||
<string name="comment">Comentar</string>
|
||||
<string name="comment_verb">Comentar</string>
|
||||
<string name="comment_posted">¡Comentario: %1$s publicado!</string>
|
||||
<string name="comment_error">¡Error al comentar!</string>
|
||||
<string name="share_image">Compartir imagen</string>
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
<string name="follow_error">Ezin izan da jarraitu</string>
|
||||
<string name="follow_button_failed">Ezin izan da jarraipen-botoia erakutsi</string>
|
||||
<string name="follow_status_failed">Ezin izan da segimendu-egoera lortu</string>
|
||||
<string name="comment">Iruzkin</string>
|
||||
<string name="comment_verb">Iruzkin</string>
|
||||
<string name="posted_on">%1$s(e)n argitaratua</string>
|
||||
<string name="hashtags">TRAOLAK</string>
|
||||
<string name="accounts">KONTUAK</string>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<string name="share_image">همرسانی تصویر</string>
|
||||
<string name="comment_error">خطا در درج نظر!</string>
|
||||
<string name="comment_posted">نظر: %1$s منتشر کرد!</string>
|
||||
<string name="comment">نظر</string>
|
||||
<string name="comment_verb">نظر</string>
|
||||
<string name="follow_status_failed">نتوانستیم وضعیت پیگیری را دریافت کنیم</string>
|
||||
<string name="follow_button_failed">نتوانستیم دکمه پیگیری را نمایش دهیم</string>
|
||||
<string name="follow_error">نتوانستیم پیگیری کنیم</string>
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
<string name="search">Hae</string>
|
||||
<string name="unfollow">Seurataan</string>
|
||||
<string name="follow">Seuraa</string>
|
||||
<string name="comment">Kommentti</string>
|
||||
<string name="comment_verb">Kommentti</string>
|
||||
<string name="image_download_downloading">Ladataan…</string>
|
||||
<string name="gallery_button_alt">Galleria</string>
|
||||
<string name="edit">Muokkaa</string>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<string name="share_image">Partager image</string>
|
||||
<string name="comment_error">Erreur de commentaire !</string>
|
||||
<string name="comment_posted">Commentaire : %1$s publié !</string>
|
||||
<string name="comment">Commenter</string>
|
||||
<string name="comment_verb">Commenter</string>
|
||||
<string name="follow_status_failed">Impossible d\'obtenir l\'état de suivi</string>
|
||||
<string name="follow_button_failed">Impossible d\'afficher le bouton de suivi</string>
|
||||
<string name="follow_error">Impossible de suivre</string>
|
||||
|
@ -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,14 +194,14 @@
|
|||
<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>
|
||||
<string name="file_not_found">Le fichier %1$s n’a pas été trouvé</string>
|
||||
<string name="notifications_settings">Paramètres des notifications</string>
|
||||
<string name="post_is_video">Cette publication est une vidéo</string>
|
||||
<string name="dialog_message_cancel_follow_request">Annuler la demande d’abonnement \?</string>
|
||||
<string name="dialog_message_cancel_follow_request">Annuler la demande de suivi ?</string>
|
||||
<string name="unbookmark">Retirer des signets</string>
|
||||
<string name="delete_collection">Supprimer la collection</string>
|
||||
<string name="your_name">Votre nom</string>
|
||||
|
@ -223,4 +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">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>
|
|
@ -57,7 +57,7 @@
|
|||
<string name="follow_error">Non se puido seguir</string>
|
||||
<string name="follow_button_failed">Non se pode mostrar o botón para seguir</string>
|
||||
<string name="follow_status_failed">Non se obtivo o estado de relacións</string>
|
||||
<string name="comment">Comentar</string>
|
||||
<string name="comment_verb">Comentar</string>
|
||||
<string name="comment_posted">Comentario: %1$s publicado!</string>
|
||||
<string name="comment_error">Fallo ao comentar!</string>
|
||||
<string name="share_image">Compartir Imaxe</string>
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
<item quantity="one">%d hozzászólás</item>
|
||||
<item quantity="other">%d hozzászólás</item>
|
||||
</plurals>
|
||||
<string name="comment">Hozzászólás</string>
|
||||
<string name="comment_verb">Hozzászólás</string>
|
||||
<string name="comment_posted">Hozzászólás: %1$s közzétéve!</string>
|
||||
<string name="comment_error">Hozzászólási hiba!</string>
|
||||
<string name="share_image">Kép megosztása</string>
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
|
@ -38,7 +38,7 @@
|
|||
<string name="empty_comment">Komentar tidak boleh kosong!</string>
|
||||
<string name="share_image">Bagikan Gambar</string>
|
||||
<string name="comment_posted">Komentar: %1$s diposting!</string>
|
||||
<string name="comment">Komentar</string>
|
||||
<string name="comment_verb">Komentar</string>
|
||||
<plurals name="number_comments">
|
||||
<item quantity="other">%d komentar</item>
|
||||
</plurals>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<string name="shared_notification">%1$s ha condiviso il tuo post</string>
|
||||
<string name="followed_notification">%1$s ti ha seguito</string>
|
||||
<string name="mention_notification">%1$s ti ha menzionato</string>
|
||||
<string name="liked_notification">%1$s è piaciuto il tuo post</string>
|
||||
<string name="liked_notification">A %1$s è piaciuto il tuo post</string>
|
||||
<string name="post">Invia</string>
|
||||
<string name="description">Descrizione…</string>
|
||||
<string name="whats_an_instance">Cos\'è un\'istanza\?</string>
|
||||
|
@ -33,7 +33,7 @@
|
|||
<string name="domain_of_your_instance">Dominio della tua istanza</string>
|
||||
<string name="login_connection_required_once">Devi essere online per poter aggiungere il primo account e utilizzare PixelDroid :(</string>
|
||||
<string name="invalid_domain">Dominio non valido</string>
|
||||
<string name="edit">MODIFICA</string>
|
||||
<string name="edit">Modifica</string>
|
||||
<string name="instance_error">Impossibile ottenere informazioni sull\'istanza</string>
|
||||
<string name="save_image_failed">Impossibile salvare l\'immagine</string>
|
||||
<string name="light_theme">Chiaro</string>
|
||||
|
@ -56,7 +56,7 @@
|
|||
<string name="share_image">Condividi immagine</string>
|
||||
<string name="comment_error">Errore nel commento!</string>
|
||||
<string name="comment_posted">Commento: %1$s postato!</string>
|
||||
<string name="comment">Commento</string>
|
||||
<string name="comment_verb">Commento</string>
|
||||
<string name="follow_error">Impossibile seguire</string>
|
||||
<string name="unfollow_error">Impossibile smettere di seguire</string>
|
||||
<string name="access_token_invalid">Il token di accesso non è valido</string>
|
||||
|
@ -85,11 +85,11 @@
|
|||
<string name="instance_not_pixelfed_warning">Questa non sembra essere un\'istanza di Pixelfed, quindi l\'app potrebbe interrompersi in modi inaspettati.</string>
|
||||
<string name="instance_not_pixelfed_cancel">Annullare l\'accesso</string>
|
||||
<string name="instance_not_pixelfed_continue">OK, continua comunque</string>
|
||||
<string name="discover">SCOPRI</string>
|
||||
<string name="discover">Scopri</string>
|
||||
<string name="open_drawer_menu">Apri il menu a scomparsa</string>
|
||||
<string name="profile_picture">Foto profilo</string>
|
||||
<string name="report_error">Impossibile inviare la segnalazione</string>
|
||||
<string name="reported">Segnalato {gmd_check_circle}</string>
|
||||
<string name="reported">Post segnalato</string>
|
||||
<string name="report_target">Segnala @%1$s\'s post</string>
|
||||
<string name="optional_report_comment">Messaggio facoltativo per moderatori/amministratori</string>
|
||||
<string name="share_link">Condividi il Link</string>
|
||||
|
@ -132,25 +132,32 @@
|
|||
<plurals name="nb_followers">
|
||||
<item quantity="one">%d
|
||||
\nSeguace</item>
|
||||
<item quantity="many">%d
|
||||
\nSeguaci</item>
|
||||
<item quantity="other">%d
|
||||
\nSeguaci</item>
|
||||
</plurals>
|
||||
<plurals name="nb_posts">
|
||||
<item quantity="one">%d
|
||||
\nPost</item>
|
||||
<item quantity="many">%d
|
||||
\nPost</item>
|
||||
<item quantity="other">%d
|
||||
\nPost</item>
|
||||
</plurals>
|
||||
<plurals name="number_comments">
|
||||
<item quantity="one">%d commento</item>
|
||||
<item quantity="many">%d commenti</item>
|
||||
<item quantity="other">%d commenti</item>
|
||||
</plurals>
|
||||
<plurals name="shares">
|
||||
<item quantity="one">%d Condivisione</item>
|
||||
<item quantity="many">%d Condivisioni</item>
|
||||
<item quantity="other">%d Condivisioni</item>
|
||||
</plurals>
|
||||
<plurals name="likes">
|
||||
<item quantity="one">%d Mi piace</item>
|
||||
<item quantity="many">%d Mi piace</item>
|
||||
<item quantity="other">%d Mi piace</item>
|
||||
</plurals>
|
||||
<string name="upload_error">Codice di errore restituito dal server: %1$d</string>
|
||||
|
@ -163,11 +170,13 @@
|
|||
<item quantity="other">La descrizione deve contenere al massimo %d caratteri.</item>
|
||||
</plurals>
|
||||
<string name="api_not_enabled_dialog">L\'API non è attivata su questa istanza. Contatta l\'amministratore per chiedergli di attivarlo.</string>
|
||||
<string name="delete_post_failed_io_except">Impossibile eliminare il post, controlla la tua connesione\?</string>
|
||||
<string name="delete_post_failed_io_except">Impossibile eliminare il post, controllare la connessione?</string>
|
||||
<string name="delete_post_failed_error">Impossibile eliminare il post, errore %1$d</string>
|
||||
<plurals name="nb_following">
|
||||
<item quantity="one">%d
|
||||
\nUtente che stai seguendo</item>
|
||||
<item quantity="many">%d
|
||||
\nUtenti che stai seguendo</item>
|
||||
<item quantity="other">%d
|
||||
\nUtenti che stai seguendo</item>
|
||||
</plurals>
|
||||
|
@ -206,4 +215,102 @@
|
|||
<string name="upload_next_step">Prossimo step</string>
|
||||
<string name="add_details">Aggiungi qualche dettaglio</string>
|
||||
<string name="unknown_error_in_error">Errore sconosciuto, controlla se il server è giù: %1$s</string>
|
||||
<string name="feed_view">Vista a elenco</string>
|
||||
<string name="follow_requested">Follow Richiesto</string>
|
||||
<string name="bookmark">Aggiungi ai preferiti</string>
|
||||
<string name="unbookmark">Rimuovi dai preferiti</string>
|
||||
<string name="redraft">Rielabora</string>
|
||||
<string name="from_other_domain">da %1$s</string>
|
||||
<string name="redraft_dialog_launch">Rielaborare questo post ti permetterà di modificare la foto e la sua descrizione, ma eliminerà tutti i commenti e i mi piace attualmente presenti. Continuare?</string>
|
||||
<string name="bookmark_post_failed_io_except">Non è stato possibile aggiungere/rimuovere il post dai preferiti, controllare la connessione?</string>
|
||||
<string name="no_storage_permission">Accesso all\'archiviazione non autorizzato, aggiungi l\'autorizzazione nelle impostazioni se vuoi permettere a PixelDroid di mostrare le miniature</string>
|
||||
<string name="description_template_summary">Inserisci questo nella descrizione dei post</string>
|
||||
<string name="dialog_message_cancel_follow_request">Cancellare la richiesta di follow?</string>
|
||||
<string name="home_feed">Home</string>
|
||||
<string name="search_discover_feed">Cerca</string>
|
||||
<string name="create_feed">Aggiungi</string>
|
||||
<string name="notifications_feed">Aggiornamenti</string>
|
||||
<string name="public_feed">Pubblico</string>
|
||||
<string name="accentColorTitle">Colore secondario</string>
|
||||
<string name="accentColorSummary">Scegli un colore secondario</string>
|
||||
<string name="color_choice_button">Scegli questo colore secondario</string>
|
||||
<string name="color_chosen">Colore secondario scelto</string>
|
||||
<string name="redraft_dialog_cancel">Se elimini questa rielaborazione, il post originale non sarà più sul tuo profilo. Continuare senza ripubblicare?</string>
|
||||
<string name="redraft_post_failed_error">Impossibile rielaborare il post, errore %1$d</string>
|
||||
<string name="redraft_post_failed_io_except">Non è stato possibile rielaborare il post, controllare la connessione?</string>
|
||||
<string name="bookmark_post_failed_error">Non è stato possibile aggiungere/rimuovere il post dai preferiti, errore %1$d</string>
|
||||
<string name="no_camera_permission">Accesso alla fotocamera non autorizzato, aggiungi l\'autorizzazione nelle impostazioni se vuoi permettere a PixelDroid di usare la fotocamera</string>
|
||||
<string name="play_video">Riproduci video</string>
|
||||
<string name="encode_error">Errore durante la codifica</string>
|
||||
<string name="encode_success">Codifica completata!</string>
|
||||
<string name="encode_progress">Codifica %1$d%%</string>
|
||||
<string name="analyzing_stabilization">Analisi per la stabilizzazione %1$d%%</string>
|
||||
<string name="still_encoding">Uno o più video sono ancora in fase di codifica. Aspetta che siano pronti prima di caricare</string>
|
||||
<string name="new_post_shortcut_short">Nuovo post</string>
|
||||
<string name="follow_request">%1$s vorrebbe seguirti</string>
|
||||
<string name="status_notification">%1$s ha aggiunto un post</string>
|
||||
<string name="new_post_shortcut_long">Aggiungi un nuovo post</string>
|
||||
<string name="profile_error">Impossibile caricare il profilo</string>
|
||||
<string name="add_images_error">Errore durante l\'aggiunto delle immagini</string>
|
||||
<string name="notification_thumbnail">Miniatura dell\'immagine nella notifica di questo post</string>
|
||||
<string name="post_preview">Anteprima di un post</string>
|
||||
<string name="description_template">Modello di descrizione</string>
|
||||
<string name="explore_accounts">Scopri profili popolari in questa istanza</string>
|
||||
<string name="popular_accounts">Profili popolari</string>
|
||||
<string name="explore_hashtags">Scopri gli hashtag di tendenza in questa istanza</string>
|
||||
<string name="trending_hashtags">Hashtag di tendenza</string>
|
||||
<string name="daily_trending">Vedi i post di tendenza oggi</string>
|
||||
<string name="trending_posts">Post di tendenza</string>
|
||||
<string name="explore_posts">Scopri post casuali di oggi</string>
|
||||
<string name="grid_view">Vista a griglia</string>
|
||||
<string name="bookmarks">Preferiti</string>
|
||||
<string name="comment_noun">Commento</string>
|
||||
<string name="delete_collection_warning">Vuoi davvero eliminare questa raccolta?</string>
|
||||
<string name="removed_post_from_collection">Post rimosso dalla raccolta</string>
|
||||
<string name="new_collection_link_failed">Impossibile aprire la pagina per aggiungere una raccolta</string>
|
||||
<string name="private_account_explanation">Se il tuo account è privato, solo le persone che approvi possono vedere le tue foto e i tuoi video su Pixelfed. I tuoi seguaci esistenti non subiranno modifiche.</string>
|
||||
<plurals name="replies_count">
|
||||
<item quantity="one">%d risposta</item>
|
||||
<item quantity="many">%d risposte</item>
|
||||
<item quantity="other">%d risposte</item>
|
||||
</plurals>
|
||||
<string name="collection_title">La raccolta di %1$s</string>
|
||||
<string name="story_image">Immagine della storia</string>
|
||||
<string name="replyToStory">Rispondi a %1$s</string>
|
||||
<string name="story_reply_error">Qualcosa è andato storto nell\'invio della risposta</string>
|
||||
<string name="error_fetch_story">Qualcosa è andato storto nel recupero del carosello</string>
|
||||
<string name="sent_reply_story">Risposta inviata</string>
|
||||
<string name="fetching_profile">Recupero del tuo profilo…</string>
|
||||
<string name="collection_remove_post">Rimuovi post</string>
|
||||
<string name="add_to_collection">Scegli un post da aggiungere</string>
|
||||
<string name="delete_from_collection">Scegli un post da rimuovere</string>
|
||||
<string name="added_post_to_collection">Post aggiunto alla raccolta</string>
|
||||
<string name="error_add_post_to_collection">Impossibile aggiungere il post alla raccolta</string>
|
||||
<string name="error_remove_post_from_collection">Impossibile rimuovere il post dalla raccolta</string>
|
||||
<string name="save">Salva</string>
|
||||
<string name="use_dynamic_color">Usa il colore dinamico dal tuo sistema</string>
|
||||
<string name="more_profile_settings">Altre impostazioni del profilo</string>
|
||||
<string name="private_account">Account privato</string>
|
||||
<string name="your_bio">La tua bio</string>
|
||||
<string name="your_name">Il tuo nome</string>
|
||||
<string name="profile_save_changes">Non hai salvato le modifiche. Uscire?</string>
|
||||
<string name="saving_profile">Salvataggio del tuo profilo</string>
|
||||
<string name="profile_saved">Modifiche salvate!</string>
|
||||
<string name="error_profile">Qualcosa è andato storto. Tocca per riprovare</string>
|
||||
<string name="change_profile_picture">Cambia la tua immagine del profilo</string>
|
||||
<string name="contains_nsfw">Contiene media sensibili</string>
|
||||
<string name="switch_accounts">Cambia account</string>
|
||||
<string name="summary_always_show_nsfw">I post con contenuti sensibili non verranno oscurati e saranno mostrati di default.</string>
|
||||
<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>
|
|
@ -64,7 +64,7 @@
|
|||
<string name="follow_error">フォローできませんでした</string>
|
||||
<string name="follow_button_failed">フォローボタンを表示できませんでした</string>
|
||||
<string name="follow_status_failed">フォローステータスを取得できませんでした</string>
|
||||
<string name="comment">コメント</string>
|
||||
<string name="comment_verb">コメント</string>
|
||||
<string name="comment_error">コメントエラー</string>
|
||||
<string name="share_image">画像を共有</string>
|
||||
<string name="no_description">説明はありません</string>
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
<string name="instance_error">Kon instance-informatie niet ophalen</string>
|
||||
<string name="request_format_error">Fout bij het uploaden: slecht verzoekformaat</string>
|
||||
<string name="posted_on">Gepost op %1$s</string>
|
||||
<string name="comment">Commentaar maken</string>
|
||||
<string name="comment_verb">Commentaar maken</string>
|
||||
<string name="comment_posted">Commentaar: %1$s gepost!</string>
|
||||
<string name="comment_error">Fout met commentaar!</string>
|
||||
<string name="share_image">Afbeelding delen</string>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<string name="share_image">Udostępnij</string>
|
||||
<string name="comment_error">Błąd komentarza!</string>
|
||||
<string name="comment_posted">Komentarz: %1$s opublikowany!</string>
|
||||
<string name="comment">Komentarz</string>
|
||||
<string name="comment_verb">Komentarz</string>
|
||||
<string name="menu_account">Mój profil</string>
|
||||
<string name="registration_failed">Nie udało się zarejestrować aplikacji na tym serwerze</string>
|
||||
<string name="instance_error">Nie udało się pobrać informacji o instancji</string>
|
||||
|
|
|
@ -158,7 +158,7 @@
|
|||
<string name="unfollow_error">Não foi possível deixar de seguir</string>
|
||||
<string name="action_not_allowed">Esta ação não é permitida</string>
|
||||
<string name="follow_error">Não foi possível seguir</string>
|
||||
<string name="comment">Comentar</string>
|
||||
<string name="comment_verb">Comentar</string>
|
||||
<plurals name="description_max_characters">
|
||||
<item quantity="one">A descrição precisa ter pelo menos %d carácter.</item>
|
||||
<item quantity="other">A descrição precisa ter pelo menos %d caracteres.</item>
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
<string name="comment_error">Erro ao comentar!</string>
|
||||
<string name="write_permission_download_pic">Precisa adicionar a permissão para transferir imagens!</string>
|
||||
<string name="comment_posted">Comentário: %1$s publicou!</string>
|
||||
<string name="comment">Comentário</string>
|
||||
<string name="comment_verb">Comentário</string>
|
||||
<string name="add_comment">Adicionar um comentário</string>
|
||||
<string name="submit_comment">Submeter comentário</string>
|
||||
<string name="post_is_album">Esta publicação é um álbum</string>
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<string name="add_account_description">Добавить другой аккаунт Pixelfed</string>
|
||||
<string name="add_account_name">Добавить аккаунт</string>
|
||||
<string name="instance_error">Не удалось получить информацию об экземпляре</string>
|
||||
<string name="comment">Комментировать</string>
|
||||
<string name="comment_verb">Комментировать</string>
|
||||
<string name="comment_posted">Комментарий: %1$s опубликован!</string>
|
||||
<string name="comment_error">Ошибка комментирования!</string>
|
||||
<string name="share_image">Поделиться изображением</string>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<string name="follow_error">Kunde inte följa</string>
|
||||
<string name="follow_button_failed">Kunde inte visa följarknapp</string>
|
||||
<string name="follow_status_failed">Kunde inte hämta status för följare</string>
|
||||
<string name="comment">Kommentera</string>
|
||||
<string name="comment_verb">Kommentera</string>
|
||||
<string name="comment_posted">Kommentar: %1$s inlagd!</string>
|
||||
<string name="comment_error">Kommentarsfel!</string>
|
||||
<string name="share_image">Dela bild</string>
|
||||
|
|
|
@ -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>
|
||||
|
@ -80,7 +80,7 @@
|
|||
<string name="instance_not_pixelfed_cancel">Скасувати вхід</string>
|
||||
<string name="request_format_error">Помилка вивантаження: хибний формат запиту</string>
|
||||
<string name="connect_to_pixelfed">З\'єднатися з Pixelfed</string>
|
||||
<string name="comment">Коментар</string>
|
||||
<string name="comment_verb">Коментар</string>
|
||||
<plurals name="nb_posts">
|
||||
<item quantity="one">%d
|
||||
\nдопис</item>
|
||||
|
@ -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>
|
|
@ -60,7 +60,7 @@
|
|||
<string name="follow_error">无法关注</string>
|
||||
<string name="follow_button_failed">无法显示关注按钮</string>
|
||||
<string name="follow_status_failed">无法获得关注状态</string>
|
||||
<string name="comment">评论</string>
|
||||
<string name="comment_verb">评论</string>
|
||||
<string name="comment_posted">评论: %1$s 已发布!</string>
|
||||
<string name="comment_error">评论错误!</string>
|
||||
<string name="share_image">分享图像</string>
|
||||
|
|
|
@ -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>
|
|
@ -153,7 +153,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
|
|||
<string name="share_image">Share Image</string>
|
||||
<string name="comment_error">Comment error!</string>
|
||||
<string name="comment_posted">"Comment: %1$s posted!"</string>
|
||||
<string name="comment">Comment</string>
|
||||
<!-- This is shown on the button, it is a verb and refers to the action of commenting -->
|
||||
<string name="comment_verb">Comment</string>
|
||||
<!-- This is shown in the text field as a hint, it is a noun -->
|
||||
<string name="comment_noun">Comment</string>
|
||||
<plurals name="number_comments">
|
||||
<item quantity="one">%d comment</item>
|
||||
<item quantity="other">%d comments</item>
|
||||
|
|
|
@ -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)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue