Compare commits

...

47 Commits

Author SHA1 Message Date
Matthieu 187c29a751 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!590
2024-04-15 03:09:08 +00:00
Balaraz f9a07a5dd0 Translated using Weblate (Ukrainian)
Currently translated at 73.0% (19 of 26 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.2% (248 of 255 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.2% (248 of 255 strings)

Co-authored-by: Balaraz <balaraz@tuta.io>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/uk/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/uk/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2024-04-08 06:15:38 +00:00
Alexandre NICOLADIE fb9296187a Translated using Weblate (French)
Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (French)

Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (French)

Currently translated at 26.9% (7 of 26 strings)

Translated using Weblate (French)

Currently translated at 80.3% (205 of 255 strings)

Co-authored-by: Alexandre NICOLADIE <github@nicoladie.fr>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/fr/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/fr/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2024-04-08 06:15:37 +00:00
Matthieu 04324577ea Merge branch 'dependencies_upgrade' into 'master'
Dependencies upgrade

See merge request pixeldroid/PixelDroid!589
2024-03-29 08:33:37 +00:00
Matthieu 73f08e5a5f Update dependencies 2024-03-29 09:03:48 +01:00
Matthieu a7feab380b Merge remote-tracking branch 'origin/master' 2024-03-23 15:39:07 +01:00
Matthieu 1a4e023091 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!588
2024-03-22 14:35:59 +00:00
Regu_Miabyss 2fb4c91ffd Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: Regu_Miabyss <Regu_Miabyss@outlook.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/zh_Hant/
Translation: PixelDroid/pixeldroid
2024-03-22 14:34:56 +00:00
Matthieu 720c099f0a Merge branch 'changelog_update' into 'master'
Adapt to multiplied version numbers

See merge request pixeldroid/PixelDroid!587
2024-03-16 09:11:38 +00:00
Matthieu 869479d53f Adapt to multiplied version numbers 2024-03-16 09:11:31 +00:00
Matthieu 018f893388 Adapt to multiplied version numbers 2024-03-16 10:10:39 +01:00
Matthieu f27ae611be Merge branch 'release33_3' into 'master'
Fix build typo

See merge request pixeldroid/PixelDroid!586
2024-03-16 07:53:28 +00:00
Matthieu 234be72f59 Fix build typo 2024-03-16 08:52:58 +01:00
Matthieu 7eaac2e903 Merge branch 'release33_2' into 'master'
Release33 fixup

See merge request pixeldroid/PixelDroid!585
2024-03-16 07:41:21 +00:00
Matthieu a9849c13e6 Switch submodule to http pull 2024-03-16 08:37:43 +01:00
Matthieu d1562f18e9 Fix order of abiCodes 2024-03-16 08:34:55 +01:00
Matthieu 4a1248bcab Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!583
2024-03-15 18:15:14 +00:00
Regu_Miabyss e9122e6d72 Translated using Weblate (Chinese (Traditional))
Currently translated at 29.0% (74 of 255 strings)

Co-authored-by: Regu_Miabyss <Regu_Miabyss@outlook.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/zh_Hant/
Translation: PixelDroid/pixeldroid
2024-03-15 18:07:42 +00:00
Matthieu 76eac62d73 Merge branch 'seperate-apks2' into 'master'
Seperate version codes per architecture

See merge request pixeldroid/PixelDroid!584
2024-03-15 18:07:39 +00:00
Matthieu dd27555d83 Release 33 2024-03-15 18:10:10 +01:00
Matthieu c0feb8a37d Seperate version codes per architecture 2024-03-15 18:01:04 +01:00
Matthieu 7acd4cface Merge branch 'view_models' into 'master'
Use ViewModel in AlbumActivity

See merge request pixeldroid/PixelDroid!558
2024-03-10 10:22:07 +00:00
Matthieu 10e93c90b7 Merge branch 'master' into view_models 2024-03-08 11:06:33 +01:00
Matthieu 2311627473 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!582
2024-03-08 09:04:39 +00:00
Regu_Miabyss 6c7ab2333e Added translation using Weblate (Chinese (Traditional))
Co-authored-by: Regu_Miabyss <Regu_Miabyss@outlook.com>
2024-03-08 09:04:03 +00:00
Matthieu 54711d4e81 Hide dot indicator in fullscreen album 2024-03-07 07:56:30 +01:00
Matthieu 908d1a54c9 Merge branch 'master' into view_models 2024-03-05 10:59:52 +01:00
Matthieu 2b91543137 Merge branch 'seperate-apks' into 'master'
Split apks into ABIs to make them smaller

See merge request pixeldroid/PixelDroid!581
2024-03-01 15:41:30 +00:00
Matthieu 0688dd4d02 Release 32 2024-03-01 16:41:02 +01:00
Matthieu 319da7c11c Split apks into ABIs to make them smaller 2024-03-01 16:36:08 +01:00
Matthieu 7b327fc0d6 Merge branch 'readme-matrix' into 'master'
Update README.md

See merge request pixeldroid/PixelDroid!580
2024-02-27 13:48:08 +00:00
Matthieu 64ab2c2ac5 Update README.md 2024-02-27 13:23:07 +00:00
Matthieu 37b83f5ae2 Merge branch 'fix_uri_mess' into 'master'
Fix crashes due to ClassCastException

See merge request pixeldroid/PixelDroid!579
2024-02-25 11:03:55 +00:00
Matthieu 1516452ab5 Release 31 2024-02-25 12:03:05 +01:00
Matthieu cb50db7730 Fix crashes due to ClassCastException 2024-02-25 11:54:09 +01:00
Matthieu afe6f71152 Merge branch 'release30' into 'master'
Release 30

See merge request pixeldroid/PixelDroid!578
2024-02-14 21:23:13 +00:00
Matthieu d66c365934 Update dependencies 2024-02-14 22:00:48 +01:00
Matthieu 7815ecba08 Release 30 2024-02-14 21:59:49 +01:00
Matthieu b4533014b3 Update media_editor (bugfix) 2024-02-14 21:57:42 +01:00
Matthieu 905c1c2d66 Merge branch 'release29' into 'master'
New release

See merge request pixeldroid/PixelDroid!577
2024-02-10 17:52:11 +00:00
Matthieu 46ee92a19f New release 2024-02-10 18:48:40 +01:00
Matthieu ed7ff877fb Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!576
2024-02-10 17:44:05 +00:00
Francesco Marinucci 5c3b231e16 Translated using Weblate (Italian)
Currently translated at 40.9% (9 of 22 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/it/
Translation: PixelDroid/Fastlane
2024-02-10 17:43:09 +00:00
Matthieu 80e021e1a2 Merge branch 'hilt' into 'master'
Migration from Dagger to Hilt

See merge request pixeldroid/PixelDroid!575
2024-02-10 17:43:07 +00:00
Matthieu 0aa3d86c11 Migration from Dagger to Hilt 2024-02-10 17:43:07 +00:00
Fred f15c23cceb Prepare code for click listener in AlbumActivity 2023-07-30 13:45:33 +02:00
Fred d9da842df7 Create ViewModel for AlbumActivity 2023-07-18 18:22:30 +02:00
82 changed files with 1250 additions and 634 deletions

2
.gitmodules vendored
View File

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

View File

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

View File

@ -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 28
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,13 +175,13 @@ 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.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
@ -156,7 +198,7 @@ dependencies {
// 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.50'
ksp 'com.google.dagger:dagger-compiler:2.50'
implementation 'com.google.dagger:dagger:2.51'
ksp 'com.google.dagger:dagger-compiler:2.51'
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.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.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.6'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0'
implementation project(path: ':scrambler')
implementation project(path: ':pixel_common')
@ -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'
@ -255,6 +300,8 @@ dependencies {
}
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']

View File

@ -78,7 +78,7 @@ class MainActivity : BaseActivity() {
private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null
private lateinit var model: MainActivityViewModel
private val model: MainActivityViewModel by viewModels()
companion object {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
@ -111,12 +111,6 @@ class MainActivity : BaseActivity() {
} else {
sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this)
val _model: MainActivityViewModel by viewModels {
MainActivityViewModelFactory(application)
}
model = _model
setupDrawer()
val tabs: List<() -> Fragment> = listOf(
{
@ -280,13 +274,13 @@ class MainActivity : BaseActivity() {
val remainingUsers = db.userDao().getAll()
if (remainingUsers.isEmpty()){
//no more users, start first-time login flow
// No more users, start first-time login flow
launchActivity(LoginActivity(), firstTime = true)
} else {
val newActive = remainingUsers.first()
db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
apiHolder.setToCurrentUser()
//relaunch the app
// Relaunch the app
launchActivity(MainActivity(), firstTime = true)
}
}
@ -334,9 +328,11 @@ class MainActivity : BaseActivity() {
}
private fun switchUser(userId: String, instance_uri: String) {
db.userDao().deActivateActiveUsers()
db.userDao().activateUser(userId, instance_uri)
apiHolder.setToCurrentUser()
db.runInTransaction{
db.userDao().deActivateActiveUsers()
db.userDao().activateUser(userId, instance_uri)
apiHolder.setToCurrentUser()
}
}
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {

View File

@ -1,25 +1,22 @@
package org.pixeldroid.app
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import javax.inject.Inject
class MainActivityViewModel(application: Application) : AndroidViewModel(application) {
@Inject
lateinit var db: AppDatabase
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val db: AppDatabase
): ViewModel() {
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
@ -29,7 +26,6 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
getUsers()
}
@ -41,12 +37,4 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica
}
}
}
}
class MainActivityViewModelFactory(
val application: Application,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}
}

View File

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

View File

@ -33,14 +33,14 @@ 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.videoEdit.VideoEditActivity
import java.io.File
@ -48,14 +48,9 @@ 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)
}

View File

@ -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,13 +33,14 @@ 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.common.PICTURE_URI
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
import retrofit2.HttpException
import java.io.File
@ -47,21 +48,10 @@ 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)
}
}
}

View File

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

View File

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

View File

@ -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
@ -326,7 +325,7 @@ class CameraFragment : BaseFragment() {
}
private fun setupUploadImage() {
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled
val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled
var mimeTypes: Array<String> = arrayOf("image/*")
if(videoEnabled) mimeTypes += "video/*"
@ -449,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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,16 @@
package org.pixeldroid.app.profile
import android.app.Application
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
@ -21,7 +21,6 @@ import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.pixeldroid.app.postCreation.ProgressRequestBody
import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.updateUserInfoDb
@ -29,7 +28,10 @@ import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import javax.inject.Inject
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class EditProfileViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Context
): ViewModel() {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
@ -46,7 +48,6 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
private set
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
loadProfile()
}
@ -197,12 +198,12 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
val image = uiState.value.profilePictureUri!!
val inputStream =
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image)
applicationContext.contentResolver.openInputStream(image)
?: return
val size: Long =
if (image.scheme == "content") {
getApplication<PixelDroidApplication>().contentResolver.query(
applicationContext.contentResolver.query(
image,
null,
null,
@ -303,10 +304,4 @@ data class EditProfileActivityUiState(
val error: Boolean = false,
val uploadingPicture: Boolean = false,
val uploadProgress: Int = 0,
)
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -44,7 +44,7 @@ suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
)
}
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
InstanceDatabaseEntity(
uri = normalizeDomain(metadata?.config?.site?.url!!),

View File

@ -1,13 +1,15 @@
package org.pixeldroid.app.utils.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
@Dao
interface InstanceDao {
@Query("SELECT * FROM instances")
fun getAll(): List<InstanceDatabaseEntity>
@Query("SELECT * FROM instances WHERE uri=:instanceUri")
fun getInstance(instanceUri: String): InstanceDatabaseEntity
@ -19,13 +21,13 @@ interface InstanceDao {
* Insert an instance, if it already exists return -1
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertInstance(instance: InstanceDatabaseEntity): Long
suspend fun insertInstance(instance: InstanceDatabaseEntity): Long
@Update
fun updateInstance(instance: InstanceDatabaseEntity)
suspend fun updateInstance(instance: InstanceDatabaseEntity)
@Transaction
fun insertOrUpdate(instance: InstanceDatabaseEntity) {
suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) {
if (insertInstance(instance) == -1L) {
updateInstance(instance)
}

View File

@ -1,6 +1,11 @@
package org.pixeldroid.app.utils.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity

View File

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

View File

@ -1,34 +0,0 @@
package org.pixeldroid.app.utils.di
import android.app.Application
import android.content.Context
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.BaseFragment
import dagger.Component
import org.pixeldroid.app.MainActivityViewModel
import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.stories.StoriesViewModel
import org.pixeldroid.app.stories.StoryCarouselViewHolder
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton
@Singleton
@Component(modules = [ApplicationModule::class, DatabaseModule::class, APIModule::class])
interface ApplicationComponent {
fun inject(application: PixelDroidApplication?)
fun inject(activity: BaseActivity?)
fun inject(feedFragment: BaseFragment)
fun inject(notificationsWorker: NotificationsWorker)
fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(storiesViewModel: StoriesViewModel)
fun inject(mainActivityViewModel: MainActivityViewModel)
val context: Context?
val application: Application?
val database: AppDatabase
}

View File

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

View File

@ -5,19 +5,25 @@ import androidx.room.Room
import org.pixeldroid.app.utils.db.AppDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.pixeldroid.app.utils.db.MIGRATION_3_4
import org.pixeldroid.app.utils.db.MIGRATION_4_5
import org.pixeldroid.app.utils.db.MIGRATION_5_6
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class DatabaseModule(private val context: Context) {
class DatabaseModule {
@Provides
@Singleton
fun providesDatabase(): AppDatabase {
fun providesDatabase(
@ApplicationContext applicationContext: Context
): AppDatabase {
return Room.databaseBuilder(
context,
applicationContext,
AppDatabase::class.java, "pixeldroid"
).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.allowMainThreadQueries().build()

View File

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

View File

@ -138,35 +138,45 @@
<plurals name="nb_following">
<item quantity="one">%d
\nAbonnement</item>
<item quantity="many">%d
\nAbonnements</item>
<item quantity="other">%d
\nAbonnements</item>
</plurals>
<plurals name="nb_followers">
<item quantity="one">%d
\nAbonné·e</item>
<item quantity="many">%d
\nAbonné·e·s</item>
<item quantity="other">%d
\nAbonné·e·s</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">%d
\nPublication</item>
<item quantity="many">%d
\nPublications</item>
<item quantity="other">%d
\nPublications</item>
</plurals>
<plurals name="number_comments">
<item quantity="one">%d commentaire</item>
<item quantity="many">%d commentaires</item>
<item quantity="other">%d commentaires</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Partage</item>
<item quantity="many">%d Partages</item>
<item quantity="other">%d Partages</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d J\'aime</item>
<item quantity="many">%d J\'aime</item>
<item quantity="other">%d J\'aime</item>
</plurals>
<plurals name="description_max_characters">
<item quantity="one">La description doit contenir au moins %d lettre.</item>
<item quantity="many">La description doit contenir au moins %d lettres.</item>
<item quantity="other">La description doit contenir au moins %d lettres.</item>
</plurals>
<string name="delete_post_failed_error">Impossible de supprimer la publication, erreur %1$d</string>
@ -184,7 +194,7 @@
<string name="followed_notification_channel">Nouveaux·elles abonné·e·s</string>
<string name="mention_notification_channel">Mentions</string>
<string name="shared_notification_channel">Partages</string>
<string name="liked_notification_channel">Favoris</string>
<string name="liked_notification_channel">J\'aime</string>
<string name="comment_notification_channel">Commentaires</string>
<string name="poll_notification_channel">Sondages</string>
<string name="other_notification_channel">Autre</string>
@ -223,6 +233,85 @@
<string name="profile_saved">Modifications enregistrées!</string>
<string name="change_profile_picture">Changer votre image de profil</string>
<string name="switch_accounts">Permutation de comptes</string>
<string name="edit_link_failed">Échec de l\'ouverture de la page de modifications</string>
<string name="edit_link_failed">Impossible d\'ouvrir la page d\'édition</string>
<string name="follow_requested">Abonnement demandé</string>
<string name="comment_noun">Commentaire</string>
<string name="redraft_dialog_launch">La reformulation de cet article vous permettra de modifier la photo et sa description, mais supprimera tous les commentaires et les mentions \"J\'aime\". Poursuivre ?</string>
<string name="notification_thumbnail">Vignette de l\'image dans ce message</string>
<string name="always_show_nsfw">Toujours afficher les contenus sensibles</string>
<string name="post_preview">Aperçu d\'un message</string>
<plurals name="notification_title_summary">
<item quantity="one">%d nouvelle notification</item>
<item quantity="many">%d nouvelles notifications</item>
<item quantity="other">%d nouvelles notifications</item>
</plurals>
<plurals name="items_load_success">
<item quantity="one">%d article chargé avec succès</item>
<item quantity="many">%d articles chargés avec succès</item>
<item quantity="other">%d articles chargés avec succès</item>
</plurals>
<string name="notification_summary_large">%1$s, %2$s, %3$s et %4$d autres</string>
<string name="notification_summary_medium">%1$s, %2$s, et %3$s</string>
<string name="video_not_supported">Le serveur que vous utilisez ne prend pas en charge les téléchargements de vidéos, il se peut que vous ne puissiez pas télécharger les vidéos incluses dans cet article</string>
<string name="new_collection_link_failed">Échec de l\'ouverture de la page de création d\'une collection</string>
<string name="bookmark">Favori</string>
<string name="unknown_error_in_error">Erreur inconnue, vérifiez si le serveur est en panne : %1$s</string>
<string name="profile_error">Impossible de charger le profil</string>
<string name="add_images_error">Erreur lors de l\'ajout des images</string>
<string name="description_template_summary">Remplir la description des nouveaux messages avec ceci</string>
<string name="description_template">Modèle de description</string>
<string name="explore_accounts">Explorer les comptes populaires de cette instance</string>
<string name="explore_hashtags">Explorer les hashtags en vogue sur cette instance</string>
<string name="daily_trending">Voir les messages populaires de la journée</string>
<string name="notification_summary_small">%1$s et %2$s</string>
<string name="extraneous_pictures_stories">Les images après la première ont été supprimées mais peuvent être restaurées en revenant à la création d\'un message</string>
<string name="summary_always_show_nsfw">Les messages NSFW/CW ne seront pas floutés et seront affichés par défaut.</string>
<string name="encode_progress">Encodage de %1$d%%</string>
<string name="still_encoding">Une ou plusieurs vidéos sont en cours d\'encodage. Attendez qu\'elles soient terminées avant d\'envoyer</string>
<string name="notifications_settings_summary">Gérer les notifications que vous souhaitez recevoir</string>
<string name="login_notifications">Impossible de récupérer les dernières notifications</string>
<string name="no_camera_permission">L\'autorisation pour l\'appareil photo n\'est pas accordée, accordez cette autorisation dans les paramètres si vous voulez permettre à PixelDroid d\'utiliser l\'appareil photo</string>
<string name="play_video">Lire la vidéo</string>
<string name="public_feed">Publique</string>
<string name="accentColorTitle">Couleur d\'accentuation</string>
<string name="accentColorSummary">Choisir une couleur d\'accentuation</string>
<string name="color_choice_button">Choisir cette couleur d\'accentuation</string>
<string name="explore_posts">Explorer aléatoirement les messages d\'aujourd\'hui</string>
<string name="grid_view">Vue en grille</string>
<string name="feed_view">Vue du flux</string>
<string name="encode_error">Erreur d\'encodage</string>
<string name="encode_success">Encodage réussi !</string>
<string name="more_profile_settings">Autres paramètres du profil</string>
<string name="private_account_explanation">Quand votre compte est privé, seules les personnes que vous autorisez peuvent voir vos photos et vidéos sur PixelFed. Les personnes qui vous suivent déjà ne seront pas affectées.</string>
<string name="saving_profile">Sauvegarde de votre profil</string>
<string name="use_dynamic_color">Utiliser la couleur dynamique de votre système</string>
<string name="type_story">Story</string>
<string name="story_image">Image de la Story</string>
<string name="replyToStory">Répondre à %1$s</string>
<string name="story_reply_error">Un problème s\'est produit lors de l\'envoi de la réponse</string>
<string name="error_fetch_story">Un problème s\'est produit lors de la récupération du carrousel</string>
<string name="sent_reply_story">Envoyer la réponse</string>
<string name="fetching_profile">Recherche de votre profil…</string>
<string name="redraft_dialog_cancel">Si vous annulez ce remaniement, le message original ne figurera plus sur votre compte. Poursuivre sans réécrire ?</string>
<string name="redraft_post_failed_error">Impossible de réécrire le message, erreur %1$d</string>
<string name="redraft_post_failed_io_except">Impossible de réécrire le message, vérifiez votre connexion ?</string>
<string name="bookmark_post_failed_error">Impossible de mettre/enlever le message en favoris, erreur %1$d</string>
<string name="bookmark_post_failed_io_except">Impossible de mettre/enlever le message des favoris, vérifiez votre connexion ?</string>
<string name="no_storage_permission">L\'autorisation pour le stockage n\'est pas accordée, accordez l\'autorisation dans les paramètres si vous voulez permettre à PixelDroid d\'afficher la vignette</string>
<string name="analyzing_stabilization">Analyse pour stabiliser %1$d%%</string>
<string name="color_chosen">Couleur d\'accentuation choisie</string>
<string name="error_profile">Un problème s\'est produit. Appuyez pour réessayer</string>
<string name="delete_collection_warning">Êtes-vous sûr de vouloir supprimer cette collection ?</string>
<string name="added_post_to_collection">Ajouter le message à la collection</string>
<string name="error_add_post_to_collection">Échec de l\'ajout du message à la collection</string>
<string name="removed_post_from_collection">Enlever le message de la collection</string>
<string name="error_remove_post_from_collection">Échec pour retirer le message de la collection</string>
<string name="contains_nsfw">Contient des médias NSFW</string>
<string name="add_story">Ajouter une Story</string>
<string name="story_could_not_see">Erreur : Impossible de marquer la Story comme vu</string>
<string name="story_pause">Démarrer ou interrompre les Stories</string>
<string name="my_story">Ma Story</string>
<string name="type_post">Message</string>
<string name="continue_post_creation">Continuer</string>
<string name="story_duration">Durée de la Story</string>
</resources>

View File

@ -9,7 +9,7 @@
<string name="theme_header">Тема</string>
<string name="mention_notification">%1$s вас згадує</string>
<string name="description">Опис…</string>
<string name="post">надіслати</string>
<string name="post">Оприлюднити</string>
<string name="save_to_gallery">Зберегти до галереї…</string>
<string name="image_download_downloading">Завантаження…</string>
<string name="image_download_success">Зображення успішно завантажено</string>
@ -146,13 +146,13 @@
<string name="default_nfollowing">-
\nпідписок</string>
<string name="search">Пошук</string>
<string name="edit_profile">Редагувати</string>
<string name="edit_profile">Редагувати профіль</string>
<string name="posts">ДОПИСИ</string>
<string name="accounts">ОБЛІКОВІ ЗАПИСИ</string>
<string name="license_info">PixelDroid — вільне й відкрите програмне забезпечення, доступне на умовах Загальної громадської ліцензії GNU (версії 3 чи новішої)</string>
<string name="about">Про застосунок</string>
<string name="post_title">Допис %1$s</string>
<string name="reported">Скаргу надіслано {gmd_check_circle}</string>
<string name="reported">Скаргу надіслано</string>
<string name="profile_picture">Зображення профілю</string>
<string name="open_drawer_menu">Відкрити висувне меню</string>
<string name="something_went_wrong">Щось пішло не так…</string>
@ -223,4 +223,108 @@
<string name="video_not_supported">Використовуваний сервер не підтримує вивантаження відео. Ймовірно, вивантажити відео цього допису не вдасться</string>
<string name="post_is_video">Це відеодопис</string>
<string name="play_video">Відтворити відео</string>
<string name="bookmarks">Закладки</string>
<string name="collections">Колекції</string>
<string name="delete_collection">Видалити колекцію</string>
<string name="collection_add_post">Додати публікацію</string>
<string name="collection_remove_post">Вилучити публікацію</string>
<string name="comment_noun">Коментар</string>
<string name="add_to_collection">Оберіть публікацію для додавання</string>
<plurals name="replies_count">
<item quantity="one">%d відповідь</item>
<item quantity="few">%d відповіді</item>
<item quantity="many">%d відповідей</item>
<item quantity="other">%d відповідей</item>
</plurals>
<string name="delete_from_collection">Оберіть публікацію для вилучення</string>
<string name="redraft_dialog_cancel">Якщо ви скасуєте цю переробку, оригінального допису більше не буде у вашому акаунті. Продовжити без перепосту?</string>
<string name="accentColorTitle">Акцентний колір</string>
<string name="explore_posts">Ознайомтеся з випадковими дописами дня</string>
<string name="private_account_explanation">Коли ваш акаунт приватний, тільки люди, яких ви схвалите, зможуть бачити ваші фотографії та відео на pixelfed. На ваших існуючих підписників це не вплине.</string>
<string name="always_show_nsfw">Завжди показувати чутливий вміст</string>
<string name="profile_error">Не вдалося завантажити профіль</string>
<string name="explore_accounts">Ознайомтеся з популярними акаунтами на цьому екземплярі</string>
<string name="added_post_to_collection">Публікацію додано в колекцію</string>
<string name="summary_always_show_nsfw">Дописи NSFW/CW не будуть розмиті і відображатимуться за замовчуванням.</string>
<string name="type_story">Історія</string>
<string name="new_collection_link_failed">Не вдалося відкрити сторінку створення колекції</string>
<string name="redraft_dialog_launch">При переробці цього допису ви зможете відредагувати фотографію та її опис, але при цьому будуть видалені всі поточні коментарі та вподобання. Продовжити?</string>
<string name="notification_thumbnail">Ескіз зображення в дописі цього повідомлення</string>
<string name="redraft">Переробити</string>
<string name="delete_collection_warning">Ви впевнені що хочете видалити цю колекцію?</string>
<string name="error_add_post_to_collection">Не вдалося додати публікацію до колекції</string>
<string name="use_dynamic_color">Використовувати динамічні кольори з вашої системи</string>
<string name="story_could_not_see">Помилка: не вдалося позначити історію як побачену</string>
<string name="encode_error">Помилка кодування</string>
<string name="encode_success">Кодування успішно завершено!</string>
<string name="encode_progress">Кодувати %1$d%%</string>
<string name="still_encoding">Одне або кілька відео все ще кодуються. Зачекайте, поки кодування завершиться, перш ніж завантажувати</string>
<string name="new_post_shortcut_short">Нова публікація</string>
<string name="follow_request">%1$s надіслав запит на відстеження вас</string>
<plurals name="items_load_success">
<item quantity="one">%d елемент успішно завантажено</item>
<item quantity="few">%d елемента успішно завантажено</item>
<item quantity="many">%d елементів успішно завантажено</item>
<item quantity="other">%d елементів успішно завантажено</item>
</plurals>
<string name="home_feed">Домівка</string>
<string name="search_discover_feed">Пошук</string>
<string name="story_image">Зображення історії</string>
<string name="replyToStory">Відповісти на %1$s</string>
<string name="fetching_profile">Отримання вашого профілю…</string>
<string name="continue_post_creation">Продовжити</string>
<string name="extraneous_pictures_stories">Зображення після першого були вилучені, але їх можна відновити, повернувшись до створення публікації</string>
<string name="upload_next_step">Наступний крок</string>
<string name="add_details">Додати деякі деталі</string>
<string name="unknown_error_in_error">Невідома помилка, перевірте, чи не працює сервер: %1$s</string>
<string name="collection_title">%1$s колекція</string>
<string name="bookmark">Закладка</string>
<string name="unbookmark">Видалити закладку</string>
<string name="feed_view">Вигляд стрічки</string>
<string name="redraft_post_failed_error">Не вдалося переробити пост, помилка %1$d</string>
<string name="redraft_post_failed_io_except">Не вдалося переписати пост, перевірити ваше зʼєднання?</string>
<string name="bookmark_post_failed_error">Не вдалося додати/видалити закладку, помилка %1$d</string>
<string name="bookmark_post_failed_io_except">Не вдалося додати/видалити закладку, перевірити зʼєднання?</string>
<string name="analyzing_stabilization">Аналіз для стабілізації %1$d%%</string>
<string name="new_post_shortcut_long">Створити нову публікацію</string>
<string name="status_notification">%1$s створив публікацію</string>
<string name="create_feed">Створити</string>
<string name="notifications_feed">Оновлення</string>
<string name="public_feed">Публічний</string>
<string name="accentColorSummary">Виберіть акцентний колір</string>
<string name="color_choice_button">Вибрати цей колір як акцентний</string>
<string name="color_chosen">Обрано акцентний колір</string>
<string name="from_other_domain">з %1$s</string>
<string name="add_images_error">Помилка під час додавання зображень</string>
<string name="post_preview">Попередній перегляд публікації</string>
<string name="description_template_summary">Доповніть опис нових дописів цим</string>
<string name="description_template">Шаблон опису</string>
<string name="popular_accounts">Популярні акаунти</string>
<string name="explore_hashtags">Дослідіть популярні хештеги на цьому екземплярі</string>
<string name="trending_hashtags">Популярні хештеги</string>
<string name="daily_trending">Переглядайте щоденні популярні дописи</string>
<string name="trending_posts">Популярні публікації</string>
<string name="grid_view">Вигляд сітки</string>
<string name="error_remove_post_from_collection">Не вдалося вилучити публікацію з колекції</string>
<string name="removed_post_from_collection">Публікацію вилучено з колекції</string>
<string name="save">Зберегти</string>
<string name="more_profile_settings">Більше налаштувань профілю</string>
<string name="private_account">Приватний обліковий запис</string>
<string name="your_bio">Ваша біографія</string>
<string name="your_name">Ваге імʼя</string>
<string name="profile_save_changes">Ви не зберегли зміни. Вийти?</string>
<string name="saving_profile">Збереження вашого профілю</string>
<string name="profile_saved">Зміни збережено!</string>
<string name="error_profile">Щось пішло не так. Натисніть, щоб повторити спробу</string>
<string name="change_profile_picture">Змінити зображення профілю</string>
<string name="contains_nsfw">Містить носії NSFW</string>
<string name="switch_accounts">Змінити обліковий запис</string>
<string name="story_reply_error">Щось пішло не так під час надсилання відповіді</string>
<string name="error_fetch_story">Щось пішло не так з каруселлю</string>
<string name="sent_reply_story">Відповідь надіслано</string>
<string name="add_story">Додати історію</string>
<string name="story_pause">Запустити або призупинити історії</string>
<string name="my_story">Моя історія</string>
<string name="type_post">Публікація</string>
<string name="story_duration">Тривалість історії</string>
</resources>

View File

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

View File

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.1'
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 {

View File

@ -0,0 +1 @@
Fix bug that was breaking the image editing feature in release mode

View File

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

View File

@ -0,0 +1 @@
Split APKs per CPU architecture, makes the APKs a lot smaller (100MB to 25MB)

View File

@ -0,0 +1,3 @@
Try again to split the apks
Update dependencies

View File

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

View File

@ -0,0 +1 @@
Correctif pour les plantages dans l'activité d'édition qui ne se produisaient qu'en mode release

View File

@ -0,0 +1 @@
Ajout de la traduction en hongrois (Magyar) à l'application. Merci à Balázs :)

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ! ❤️

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
Corrections de bugs et améliorations ;)

View File

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

View File

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

View File

@ -0,0 +1 @@
Correction d'un bug qui cassait la fonction d'édition d'images en mode release

View File

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

View File

@ -0,0 +1 @@
Séparation des APKs par architecture CPU, rendant les APKs beaucoup plus petits (passage de 100Mo à 25Mo)

View File

@ -0,0 +1,3 @@
Nouvelle tentative de séparation des apks
Mise à jour des dépendances

View File

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

View File

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

View File

@ -0,0 +1,2 @@
* Mises à jour des traductions
* Autoriser l'envoi d'images distantes (ex. Nextcloud)

View File

@ -0,0 +1,4 @@
* Ajout d'un support pour la lecture et l'envoi de vidéos
* Amélioration des notifications
* Mise à jour des traductions
* Mise à jour des dépendances

View File

@ -0,0 +1,3 @@
* Correction d'un crash lors de l'affichage de la liste des licences
* Amélioration considérable des performances dans l'activité affichant le profil
* Mise à jour des dépendances

View File

@ -0,0 +1,11 @@
PixelDroid est un client Android gratuit, libre et open source pour Pixelfed, la plateforme fédérée de partage d'images.
Parcourez les flux et les profils, envoyez de nouveaux messages, découvrez des messages, interagissez avec d'autres personnes sur le fediverse.
- Support multi-compte
- Thèmes sombres et clairs
- Appliquez des filtres, recadrez, modifiez la luminosité/contraste/saturation
- Prend en compte la configuration de votre serveur Pixelfed
- Logiciel 100% libre et open source. Aucune dépendances propriétaires.
Visitez https://pixelfed.org pour en savoir plus sur Pixelfed.

View File

@ -0,0 +1 @@
Client pour Pixelfed, la plateforme fédérée de partage d'images

View File

@ -0,0 +1 @@
PixelDroid

View File

@ -0,0 +1,11 @@
* Aggiunta grafica di errore personalizzata del panda rosso
* Supporto per il ritaglio libero nella modifica dell'immagine
* Migliorati i metadati di F-Droid
* Aggiornamento delle traduzioni
* Aggiornamenti delle dipendenze
* Correzione bug

View File

@ -1,2 +1,2 @@
* Traduzione aggiornata
* Consenti il caricamento di immagini da remoto (e.g. Nextcloud)
* Aggiornata la traduzione
* Consentito il caricamento di immagini da remoto (e.g. Nextcloud)

View File

@ -0,0 +1,3 @@
* Редагування відео! Вимкнення звуку, обрізка відео
* Відкривайте зображення на весь екран, масштабуйте та переміщуйте їх :)
* Оновлення перекладів

View File

@ -0,0 +1 @@
Виправлення збоїв під час редагування, які траплялися лише у режимі релізу

View File

@ -0,0 +1 @@
Додано переклад угорською (мадярською) мовою до додатку. Дякуємо Balazs :)

View File

@ -0,0 +1,4 @@
* Додано правила gson proguard для виправлення збоїв на екземплярах Mastodon
* Додано кольорове оформлення з 4 різними темами
* Перейшли до Material 3
* Покращено узгодженість інтерфейсу користувача

View File

@ -0,0 +1,8 @@
* покращено роботу з коментарями: Тепер ви можете відкрити коментар, щоб побачити відповіді, поставити вподобайку, у коментарях відображається аватар його автора тощо.
* Оновлення перекладів. Дякуємо перекладачам :). Допоможіть перекласти PixelDroid на вашу мову на weblate.pixeldroid.org
* Безпека: перевірка залежностей гарантує, що залежності, включені в додаток, не були підроблені, PixelDroid тепер відмовлятиметься від будь-яких не-HTTPS з'єднань
* PixelDroid тепер використовує власний агент користувача "PixelDroid" замість бібліотечного OkHttp.
* Видалено всі жорстко закодовані рядки з програми, тепер все нормально перекладається.
* Деякі покращення коду
* Виправлено збереження зображень, що надходять до програми з бібліотеки або надаються у спільний доступ
* Виправлено ще один збій при використанні екземплярів Mastodon

View File

@ -0,0 +1 @@
Виправлено помилку, яка порушувала роботу функції редагування зображень у режимі релізу

View File

@ -0,0 +1,3 @@
* Виправлено збій під час надання доступу до зображення з галереї тощо у додатку
* Виправлено проблему з дозволами, через яку зображення видавали помилку відмови в доступі

View File

@ -0,0 +1 @@
Розділення APK-файлів по архітектурах процесорів, що робить APK-файли значно меншими (від 100 МБ до 25 МБ)

View File

@ -0,0 +1,3 @@
Спробуйте ще раз розділити apk файли
Оновити залежності

View File

@ -26,4 +26,5 @@ kapt.incremental.apt=true
android.enableR8.fullMode=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
org.gradle.configuration-cache=true
# We want this to be true but it wasn't to be (About library doesn't like it https://github.com/mikepenz/AboutLibraries/issues/857)
org.gradle.configuration-cache=false

View File

@ -1,7 +1,7 @@
#Fri Oct 14 13:37:44 GMT 2022
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -1 +1 @@
Subproject commit 956bd5f88d6189009f2ba0a8cb2860a1bfee0ee6
Subproject commit 702d14fe701343958337efa1b4eb31f0250849f6

@ -1 +1 @@
Subproject commit 7c67b911930b4344a2917f2944493e08fdd04b57
Subproject commit 23d4d94b45a848f0c64a042985eb03d0acc2f18b