Compare commits

...

75 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
Matthieu 05cb615f15 Merge branch 'release28' into 'master'
Release 28

See merge request pixeldroid/PixelDroid!574
2024-01-31 15:45:29 +00:00
Matthieu 9313f321cd Release 28 2024-01-31 16:45:04 +01:00
Matthieu 175438115d Merge branch 'image_editing_improvements' into 'master'
Update dependency on media_editor

See merge request pixeldroid/PixelDroid!566
2024-01-31 11:41:20 +00:00
Matthieu f2600b85e2 Update dependency on media_editor 2024-01-31 11:41:20 +00:00
Matthieu 711a5b310f Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!573
2024-01-31 10:00:49 +00:00
Francesco Marinucci 0192190c6d Translated using Weblate (Italian)
Currently translated at 38.0% (8 of 21 strings)

Translated using Weblate (Italian)

Currently translated at 99.6% (254 of 255 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/it/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2024-01-31 02:10:13 +00:00
Matthieu bb935e73ad Merge branch 'improve_profile_layout' into 'master'
Improve/change profile and post layouts

See merge request pixeldroid/PixelDroid!572
2024-01-30 13:06:44 +00:00
Matthieu ddf1b273de Improve/change profile and post layouts 2024-01-30 13:44:07 +01:00
Matthieu 14dee5463e Merge branch 'fix_crash_cutout' into 'master'
Update dependency to fix crash

See merge request pixeldroid/PixelDroid!571
2024-01-27 14:25:35 +00:00
Matthieu 416d36b1a8 Update dependency to fix crash 2024-01-27 14:30:53 +01:00
Matthieu 370aeda4a6 Merge branch 'edit_profile_picture' into 'master'
Fix profile pic edit

Closes #377

See merge request pixeldroid/PixelDroid!569
2024-01-26 12:22:51 +00:00
Matthieu eb65e24099 Circlecrop directly without RequestOptions 2024-01-26 13:16:33 +01:00
Fred 1ea4371b3e Crop newly changed profile pic to circle 2024-01-24 23:22:58 +01:00
Matthieu 2e399884e9 Fix #377 2024-01-24 22:20:08 +01:00
Matthieu 1dcf605976 Improve consistency of ViewModel and UI 2024-01-23 17:27:45 +01:00
Fred 06478cf8a7 Increment version number 2024-01-22 21:50:04 +00:00
Fred 580f7ca911 Fix profile pic edit
See https://github.com/pixelfed/pixelfed/issues/4250
2024-01-22 21:50:04 +00:00
Matthieu 23fbebfe44 Merge branch 'upgrade_pixel_common' into 'master'
Upgrade common library to latest commit

See merge request pixeldroid/PixelDroid!568
2024-01-22 20:25:43 +00:00
Fred 6602d912a9 Upgrade common library to latest commit
- Solves issue with displaying AboutActivity
2024-01-22 21:03:25 +01:00
Matthieu 1f41268d55 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!567
2024-01-20 08:25:50 +00:00
Francesco Marinucci 15efdcabad Translated using Weblate (Italian)
Currently translated at 95.6% (244 of 255 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translation: PixelDroid/pixeldroid
2024-01-20 00:32:36 +00:00
Matthieu 97069a76db Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!564
2024-01-19 09:02:11 +00:00
Francesco Marinucci 834b3f86bd Translated using Weblate (Italian)
Currently translated at 81.8% (208 of 254 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translation: PixelDroid/pixeldroid
2024-01-19 08:25:31 +00:00
ButterflyOfFire adc4ef0199 Translated using Weblate (French)
Currently translated at 72.4% (184 of 254 strings)

Co-authored-by: ButterflyOfFire <butterflyoffire+pixeldroid@protonmail.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/fr/
Translation: PixelDroid/pixeldroid
2024-01-19 08:25:31 +00:00
Matthieu dafe827a5c Merge branch 'fix_comment_string' into 'master'
Fix comment string being both verb and noun

See merge request pixeldroid/PixelDroid!565
2024-01-19 08:25:29 +00:00
Matthieu 0f8602b3f1 Fix comment string being both verb and noun 2024-01-19 09:04:28 +01:00
Matthieu 6edb394ccb Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!563
2024-01-12 09:11:58 +00:00
Weblate 13fa45cdb0 Added translation using Weblate (Interlingua)
Co-authored-by: Weblate <noreply@weblate.org>
2024-01-12 03:37:15 +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
126 changed files with 2099 additions and 1049 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 26
targetSdkVersion 34
versionCode 33
versionName "1.0.beta" + versionCode
//TODO add resConfigs("en", "fr", "ja",...) ?
@ -77,6 +95,30 @@ android {
proguardFiles 'proguard-rules.pro'
}
}
splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
enable true
// By default all ABIs are included, so use reset() and include to specify that we only
// want APKs for "x86", "x86_64", "arm64-v8a" and "armeabi-v7a".
// Resets the list of ABIs for Gradle to create APKs for to none.
reset()
// Specifies a list of ABIs for Gradle to create APKs for.
//noinspection ChromeOsAbiSupport
include project.ext.abiCodes.keySet() as String[]
// Specifies that we don't want to also generate a universal APK that includes all ABIs.
universalApk false
}
}
/**
* Make a string with the application_id (available in xml etc)
*/
@ -133,30 +175,30 @@ dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation "androidx.browser:browser:1.7.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation "androidx.browser:browser:1.8.0"
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation "androidx.lifecycle:lifecycle-common-java8:2.6.2"
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
implementation "androidx.annotation:annotation:1.7.1"
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.activity:activity-ktx:1.8.2"
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.media2:media2-widget:1.2.1'
implementation 'androidx.media2:media2-player:1.2.1'
implementation 'androidx.media2:media2-widget:1.3.0'
implementation 'androidx.media2:media2-player:1.3.0'
// Use the most recent version of CameraX
def cameraX_version = '1.3.1'
def cameraX_version = '1.3.2'
implementation "androidx.camera:camera-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version"
// CameraX Lifecycle library
@ -181,18 +223,21 @@ dependencies {
implementation 'com.google.android.material:material:1.11.0'
//Dagger (dependency injection)
implementation 'com.google.dagger:dagger:2.48'
ksp 'com.google.dagger:dagger-compiler:2.48'
implementation 'com.google.dagger:dagger:2.51'
ksp 'com.google.dagger:dagger-compiler:2.51'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation('com.google.dagger:hilt-android:2.51')
ksp 'com.google.dagger:hilt-compiler:2.51'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.retrofit2:retrofit:2.10.0'
implementation 'com.squareup.retrofit2:converter-gson:2.10.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.10.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'com.github.connyduck:sparkbutton:4.1.0'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.5'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0'
implementation project(path: ':scrambler')
implementation project(path: ':pixel_common')
@ -200,24 +245,24 @@ dependencies {
exclude group: "com.android.support"
}
implementation 'com.github.bumptech.glide:okhttp3-integration:4.14.2'
implementation('com.github.bumptech.glide:recyclerview-integration:4.14.2') {
implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.16.0') {
// Excludes the support library because it's already included by Glide.
transitive = false
}
implementation 'com.github.bumptech.glide:annotations:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
ksp 'com.github.bumptech.glide:ksp:4.14.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
ksp 'com.github.bumptech.glide:ksp:4.16.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.mikepenz:materialdrawer:9.0.1'
implementation 'com.mikepenz:materialdrawer:9.0.2'
// Add for NavController support
implementation 'com.mikepenz:materialdrawer-nav:9.0.1'
implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
//iconics
implementation 'com.mikepenz:iconics-core:5.4.0'
implementation 'com.mikepenz:materialdrawer-iconics:9.0.1'
implementation 'com.mikepenz:materialdrawer-iconics:9.0.2'
implementation 'com.mikepenz:iconics-views:5.4.0'
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
@ -242,7 +287,7 @@ dependencies {
testImplementation "androidx.room:room-testing:$room_version"
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
@ -251,10 +296,12 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
}
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']

View File

@ -38,7 +38,7 @@
<activity
android:name=".posts.AlbumActivity"
android:exported="false"
android:theme="@style/AppTheme.ActionBar.Transparent"/>
android:theme="@style/TransparentAlbumActivity"/>
<activity
android:name=".profile.EditProfileActivity"
android:exported="false"

View File

@ -14,6 +14,7 @@ import android.view.View
import android.widget.ImageView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@ -29,6 +30,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.color.DynamicColors
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -36,7 +38,12 @@ import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.*
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView
@ -53,10 +60,10 @@ import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
import org.pixeldroid.app.settings.SettingsActivity
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.db.addUser
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.hasInternet
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
@ -71,6 +78,8 @@ class MainActivity : BaseActivity() {
private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null
private val model: MainActivityViewModel by viewModels()
companion object {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
}
@ -196,6 +205,7 @@ class MainActivity : BaseActivity() {
Glide.with(this@MainActivity)
.load(uri)
.placeholder(placeholder)
.circleCrop()
.into(imageView)
}
@ -264,13 +274,13 @@ class MainActivity : BaseActivity() {
val remainingUsers = db.userDao().getAll()
if (remainingUsers.isEmpty()){
//no more users, start first-time login flow
// No more users, start first-time login flow
launchActivity(LoginActivity(), firstTime = true)
} else {
val newActive = remainingUsers.first()
db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
apiHolder.setToCurrentUser()
//relaunch the app
// Relaunch the app
launchActivity(MainActivity(), firstTime = true)
}
}
@ -281,16 +291,12 @@ class MainActivity : BaseActivity() {
lifecycleScope.launchWhenCreated {
try {
val domain = user?.instance_uri.orEmpty()
val accessToken = user?.accessToken.orEmpty()
val refreshToken = user?.refreshToken
val clientId = user?.clientId.orEmpty()
val clientSecret = user?.clientSecret.orEmpty()
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val account = api.verifyCredentials()
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret)
fillDrawerAccountInfo(account.id!!)
updateUserInfoDb(db, account)
//No need to update drawer account info here, the ViewModel listens to db updates
} catch (exception: Exception) {
Log.e("ACCOUNT UPDATE:", exception.toString())
}
@ -322,9 +328,11 @@ class MainActivity : BaseActivity() {
}
private fun switchUser(userId: String, instance_uri: String) {
db.userDao().deActivateActiveUsers()
db.userDao().activateUser(userId, instance_uri)
apiHolder.setToCurrentUser()
db.runInTransaction{
db.userDao().deActivateActiveUsers()
db.userDao().activateUser(userId, instance_uri)
apiHolder.setToCurrentUser()
}
}
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
@ -337,35 +345,41 @@ class MainActivity : BaseActivity() {
}
private fun fillDrawerAccountInfo(account: String) {
val users = db.userDao().getAll().toMutableList()
users.sortWith { l, r ->
when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.users.collect { list ->
val users = list.toMutableList()
users.sortWith { l, r ->
when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
}
}
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = user.fullHandle
tag = user.instance_uri
}
}.toMutableList()
// reuse the already existing "add account" item
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.forEach { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
}
}
}
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = user.fullHandle
tag = user.instance_uri
}
}.toMutableList()
// reuse the already existing "add account" item
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.forEach { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
}
/**

View File

@ -0,0 +1,40 @@
package org.pixeldroid.app
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val db: AppDatabase
): ViewModel() {
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
// Immutable state flow exposed to UI
val users = _users.asStateFlow()
init {
getUsers()
}
private fun getUsers() {
viewModelScope.launch {
db.userDao().getAllFlow().flowOn(Dispatchers.IO)
.collect { users: List<UserDatabaseEntity> ->
_users.update { users }
}
}
}
}

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,29 +33,24 @@ import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.media_editor.common.PICTURE_POSITION
import org.pixeldroid.media_editor.common.PICTURE_URI
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
import java.io.File
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Locale
class PostCreationFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
private lateinit var model: PostCreationViewModel
private val model: PostCreationViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@ -72,30 +67,16 @@ class PostCreationFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
user = db.userDao().getActiveUser()
val user = db.userDao().getActiveUser()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
val instance = user?.run {
db.instanceDao().getInstance(instance_uri)
} ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by activityViewModels {
PostCreationViewModelFactory(
requireActivity().application,
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false),
requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false),
)
}
model = _model
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList<PhotoData>? ->
// update UI
binding.carousel.addData(
newPhotoData.map {
newPhotoData.orEmpty().map {
CarouselItem(
it.imageUri, it.imageDescription, it.video,
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
@ -103,7 +84,7 @@ class PostCreationFragment : BaseFragment() {
)
}
)
binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty()
binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false
}
lifecycleScope.launch {
@ -227,10 +208,9 @@ class PostCreationFragment : BaseFragment() {
}
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) {
result.data?.clipData?.let {
model.setImages(model.addPossibleImages(it))
}
val uris = result.data?.extras?.getParcelableArrayList<Uri>(Intent.EXTRA_STREAM)
if (result.resultCode == Activity.RESULT_OK && uris != null) {
model.setImages(model.addPossibleImages(uris, emptyList()))
} else if (result.resultCode != Activity.RESULT_CANCELED) {
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
}
@ -328,7 +308,7 @@ class PostCreationFragment : BaseFragment() {
ActivityResultContracts.StartActivityForResult()){
result: ActivityResult? ->
if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
val position: Int = result.data!!.getIntExtra(PICTURE_POSITION, 0)
model.modifyAt(position, result.data!!)
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
} else if(result?.resultCode != Activity.RESULT_CANCELED){
@ -341,8 +321,8 @@ class PostCreationFragment : BaseFragment() {
requireActivity(),
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
)
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri)
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
.putExtra(PICTURE_URI, model.getPhotoData().value!![position].imageUri)
.putExtra(PICTURE_POSITION, position)
editResultContract.launch(intent)
}

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,35 +33,25 @@ import kotlinx.parcelize.Parcelize
import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import org.pixeldroid.media_editor.common.PICTURE_URI
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
import retrofit2.HttpException
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.net.URI
import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.MutableList
import kotlin.collections.MutableMap
import kotlin.collections.arrayListOf
import kotlin.collections.forEach
import kotlin.collections.get
import kotlin.collections.getOrNull
import kotlin.collections.indexOfFirst
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.plus
import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.math.ceil
const val TAG = "Post Creation ViewModel"
// Models the UI state for the PostCreationActivity
data class PostCreationActivityUiState(
@ -108,35 +98,56 @@ data class PhotoData(
var videoEncodeError: Boolean = false,
) : Parcelable
class PostCreationViewModel(
application: Application,
clipdata: ClipData? = null,
val instance: InstanceDatabaseEntity? = null,
existingDescription: String? = null,
existingNSFW: Boolean = false,
storyCreation: Boolean = false,
) : AndroidViewModel(application) {
@HiltViewModel
class PostCreationViewModel @Inject constructor(
private val state: SavedStateHandle,
@ApplicationContext private val applicationContext: Context,
db: AppDatabase,
): ViewModel() {
private var storyPhotoDataBackup: MutableList<PhotoData>? = null
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
//FIXME We should be able to access the Intent action somehow, to determine if there are
// 1 or multiple Uris instead of relying on the ClassCastException
// This should not work like this (reading its source code, get() function should return null
// if it's the wrong type but instead throws ClassCastException).
// Lucky for us that it does though: we first try to get a single Uri (which we could be
// getting from a share of a single picture to the app), when the cast to Uri fails
// we try to get a list of Uris instead (casting ourselves from Parcelable as suggested
// in get() documentation)
val uris = try {
val singleUri: Uri? = state[Intent.EXTRA_STREAM]
listOfNotNull(singleUri)
} catch (e: ClassCastException) {
state.get<ArrayList<Parcelable>>(Intent.EXTRA_STREAM)?.map { it as Uri }
}
MutableLiveData<MutableList<PhotoData>>(
addPossibleImages(
uris,
state.get<ArrayList<String>>(PostCreationActivity.PICTURE_DESCRIPTIONS),
previousList = mutableListOf()
)
)
}
private val instance = db.instanceDao().getActiveInstance()
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(application)
PreferenceManager.getDefaultSharedPreferences(applicationContext)
val templateDescription = sharedPreferences.getString("prefill_description", "") ?: ""
val storyCreation: Boolean = state[CameraFragment.CAMERA_ACTIVITY_STORY] ?: false
_uiState = MutableStateFlow(PostCreationActivityUiState(
newPostDescriptionText = existingDescription ?: templateDescription,
nsfw = existingNSFW,
newPostDescriptionText = state[PostCreationActivity.POST_DESCRIPTION] ?: templateDescription,
nsfw = state[PostCreationActivity.POST_NSFW] ?: false,
maxEntries = if(storyCreation) 1 else instance?.albumLimit,
storyCreation = storyCreation
))
@ -161,32 +172,41 @@ class PostCreationViewModel(
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
/**
* Will add as many images as possible to [photoData], from the [clipData], and if
* ([photoData].size + [clipData].itemCount) > uiState.value.maxEntries then it will only add as many images
* Will add as many images as possible to [photoData], from the [uris], and if
* ([photoData].size + [uris].size) > uiState.value.maxEntries then it will only add as many images
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
*/
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
fun addPossibleImages(
uris: List<Uri>?,
descriptions: List<String>?,
previousList: MutableList<PhotoData>? = photoData.value,
): MutableList<PhotoData> {
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
var count = clipData.itemCount
uiState.value.maxEntries?.let {
if(count + (previousList?.size ?: 0) > it){
var count = uris?.size ?: 0
uiState.value.maxEntries?.let { maxEntries ->
if(count + (previousList?.size ?: 0) > maxEntries){
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(it))
currentUiState.copy(userMessage = applicationContext.getString(R.string.total_exceeds_album_limit).format(maxEntries))
}
count = count.coerceAtMost(it - (previousList?.size ?: 0))
count = count.coerceAtMost(maxEntries - (previousList?.size ?: 0))
}
if (count + (previousList?.size ?: 0) >= it) {
if (count + (previousList?.size ?: 0) >= maxEntries) {
// Disable buttons to add more images
_uiState.update { currentUiState ->
currentUiState.copy(addPhotoButtonEnabled = false)
}
}
for (i in 0 until count) {
clipData.getItemAt(i).let {
val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
}
for ((i, uri) in uris.orEmpty().withIndex()) {
val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(
PhotoData(
imageUri = uri,
size = sizeAndVideoPair.first,
video = sizeAndVideoPair.second,
imageDescription = descriptions?.getOrNull(i)
)
)
}
}
@ -204,7 +224,7 @@ class PostCreationViewModel(
private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
val size: Long =
if (uri.scheme =="content") {
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
applicationContext.contentResolver.query(uri, null, null, null, null)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
@ -221,12 +241,12 @@ class PostCreationViewModel(
}
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val type = uri.getMimeType(applicationContext.contentResolver)
val isVideo = type.startsWith("video/")
if (isVideo && !instance!!.videoEnabled) {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
currentUiState.copy(userMessage = applicationContext.getString(R.string.video_not_supported))
}
}
@ -235,7 +255,7 @@ class PostCreationViewModel(
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
userMessage = applicationContext.getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
)
}
}
@ -272,7 +292,7 @@ class PostCreationViewModel(
videoEncodeComplete = false
VideoEditActivity.startEncoding(imageUri, null, it,
context = getApplication<PixelDroidApplication>(),
context = applicationContext,
registerNewFFmpegSession = ::registerNewFFmpegSession,
trackTempFile = ::trackTempFile,
videoEncodeProgress = ::videoEncodeProgress
@ -280,7 +300,7 @@ class PostCreationViewModel(
}
}
} else {
imageUri = data.getStringExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI)!!.toUri()
imageUri = data.getStringExtra(PICTURE_URI)!!.toUri()
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
size = imageSize
video = imageVideo
@ -387,17 +407,17 @@ class PostCreationViewModel(
}
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
val extension = data.imageUri.fileExtension(applicationContext.contentResolver)
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
val strippedImage = File.createTempFile("temp_img", ".$extension", applicationContext.cacheDir)
val imageUri = data.imageUri
val (strippedOrNot, size) = try {
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(
val orientation = ExifInterface(applicationContext.contentResolver.openInputStream(imageUri)!!).getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
stripMetadata(imageUri, strippedImage, applicationContext.contentResolver)
// Restore EXIF orientation
val exifInterface = ExifInterface(strippedImage)
@ -409,11 +429,11 @@ class PostCreationViewModel(
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
val imageInputStream = try {
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
applicationContext.contentResolver.openInputStream(imageUri)!!
} catch (e: FileNotFoundException){
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
userMessage = applicationContext.getString(R.string.file_not_found,
data.imageUri)
)
}
@ -425,14 +445,14 @@ class PostCreationViewModel(
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
userMessage = applicationContext.getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val type = data.imageUri.getMimeType(applicationContext.contentResolver)
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
@ -482,7 +502,7 @@ class PostCreationViewModel(
currentUiState.copy(
uploadErrorVisible = true,
uploadErrorExplanationText = if(e is HttpException){
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
applicationContext.getString(R.string.upload_error, e.code())
} else "",
uploadErrorExplanationVisible = e is HttpException,
)
@ -548,14 +568,14 @@ class PostCreationViewModel(
sensitive = nsfw
)
}
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java)
val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
//TODO make the activity launch this instead (and surrounding toasts too)
getApplication<PixelDroidApplication>().startActivity(intent)
applicationContext.startActivity(intent)
} catch (exception: IOException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString())
_uiState.update { currentUiState ->
@ -564,7 +584,7 @@ class PostCreationViewModel(
)
}
} catch (exception: HttpException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.response().toString() + exception.message().toString())
_uiState.update { currentUiState ->
@ -609,7 +629,7 @@ class PostCreationViewModel(
//Show message saying extraneous pictures were removed but can be restored
newUiState = newUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.extraneous_pictures_stories)
userMessage = applicationContext.getString(R.string.extraneous_pictures_stories)
)
}
// Restore if backup not null and first value is unchanged
@ -629,10 +649,4 @@ class PostCreationViewModel(
fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } }
fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } }
}
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation)
}
}
}

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
@ -39,7 +38,6 @@ import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.FragmentCameraBinding
import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.bindingLifecycleAware
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@ -60,7 +58,7 @@ class CameraFragment : BaseFragment() {
private val cameraLifecycleOwner = CameraLifecycleOwner()
private var binding: FragmentCameraBinding by bindingLifecycleAware()
private lateinit var binding: FragmentCameraBinding
private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
@ -327,7 +325,7 @@ class CameraFragment : BaseFragment() {
}
private fun setupUploadImage() {
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled
val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled
var mimeTypes: Array<String> = arrayOf("image/*")
if(videoEnabled) mimeTypes += "video/*"
@ -450,21 +448,7 @@ class CameraFragment : BaseFragment() {
private fun startAlbumCreation(uris: ArrayList<String>) {
val intent = Intent(requireActivity(), PostCreationActivity::class.java)
.apply {
uris.forEach{
//Why are we using ClipData here? Because the FLAG_GRANT_READ_URI_PERMISSION
//needs to be applied to the URIs, and this flag only applies to the
//Intent's data and any URIs specified in its ClipData.
if(clipData == null){
clipData = ClipData("", emptyArray(), ClipData.Item(it.toUri()))
} else {
clipData!!.addItem(ClipData.Item(it.toUri()))
}
}
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
val intent = PostCreationActivity.intentForUris(requireContext(), uris.map { it.toUri() })
if(inActivity && !addToStory){
requireActivity().setResult(Activity.RESULT_OK, intent)

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

@ -6,6 +6,7 @@ import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R
@ -24,13 +25,16 @@ import org.pixeldroid.app.utils.displayDimensionsInPx
class PostActivity : BaseActivity() {
private lateinit var binding: ActivityPostBinding
private var commentFragment = CommentFragment()
private lateinit var commentFragment: CommentFragment
private lateinit var status: Status
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPostBinding.inflate(layoutInflater)
commentFragment = CommentFragment(binding.swipeRefreshLayout)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -105,6 +109,11 @@ class PostActivity : BaseActivity() {
supportFragmentManager.beginTransaction()
.add(R.id.commentFragment, commentFragment).commit()
binding.swipeRefreshLayout.setOnRefreshListener {
commentFragment.adapter.refresh()
commentFragment.adapter.notifyDataSetChanged()
}
}
private suspend fun postComment(

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

@ -73,6 +73,7 @@ internal fun <T: Any> initAdapter(
swipeRefreshLayout.setOnRefreshListener {
adapter.refresh()
adapter.notifyDataSetChanged()
header?.refreshStories()
}

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

@ -11,6 +11,7 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadState
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter
@ -20,6 +21,7 @@ import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.posts.feeds.launch
import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
/**
@ -30,8 +32,7 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
internal lateinit var viewModel: FeedViewModel<T>
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
lateinit var binding: FragmentFeedBinding
var binding: FragmentFeedBinding? = null
private var job: Job? = null
@ -48,25 +49,35 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
.distinctUntilChangedBy { it.refresh }
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter { it.refresh is LoadState.NotLoading }
.collect { binding.list.scrollToPosition(0) }
.collect { binding?.list?.scrollToPosition(0) }
}
}
override fun onCreateView(
fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
savedInstanceState: Bundle?, swipeRefreshLayout: SwipeRefreshLayout?
): View {
super.onCreateView(inflater, container, savedInstanceState)
binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(
binding.progressBar, binding.swipeRefreshLayout, binding.list,
binding.motionLayout, binding.errorLayout, adapter
)
binding!!.let {
initAdapter(
it.progressBar, swipeRefreshLayout ?: it.swipeRefreshLayout, it.list,
it.motionLayout, it.errorLayout, adapter
)
return binding.root
}
return binding!!.root
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return onCreateView(inflater, container, savedInstanceState, null)
}
fun onTabReClicked() {
binding?.list?.limitedLengthSmoothScrollToPosition(0)
}
}

View File

@ -5,12 +5,15 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingChildHelper
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.CommentBinding
import org.pixeldroid.app.posts.PostActivity
@ -25,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
/**
* Fragment to show a list of [Status]s, in form of comments
*/
class CommentFragment : UncachedFeedFragment<Status>() {
class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment<Status>() {
private lateinit var id: String
private lateinit var domain: String
@ -42,11 +45,11 @@ class CommentFragment : UncachedFeedFragment<Status>() {
@OptIn(ExperimentalPagingApi::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? {
val view = super.onCreateView(inflater, container, savedInstanceState)
val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
// Get the view model
@Suppress("UNCHECKED_CAST")
@ -62,6 +65,7 @@ class CommentFragment : UncachedFeedFragment<Status>() {
launch()
initSearch()
binding?.swipeRefreshLayout?.isEnabled = false
return view
}
companion object {

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()){
@ -51,6 +48,7 @@ class EditProfileActivity : BaseActivity() {
}.show()
} else {
this.isEnabled = false
if (model.submittedChanges) setResult(RESULT_OK)
super.onBackPressedDispatcher.onBackPressed()
}
}
@ -58,23 +56,24 @@ class EditProfileActivity : BaseActivity() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
if(uiState.profileLoaded){
binding.bioEditText.setText(uiState.bio)
binding.nameEditText.setText(uiState.name)
model.changesApplied()
}
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
if(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
binding.privateSwitch.isChecked = uiState.privateAccount == true
Glide.with(binding.profilePic).load(uiState.profilePictureUri)
.apply(RequestOptions.circleCropTransform())
.into(binding.profilePic)
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE
else View.VISIBLE
binding.savingProgressBar.visibility =
if(uiState.error || (uiState.profileSent && !uiState.uploadingPicture)) View.GONE
else View.VISIBLE
if(uiState.profileSent){
if(uiState.profileSent && !uiState.uploadingPicture && !uiState.error){
binding.progressText.setText(R.string.profile_saved)
binding.done.visibility = View.VISIBLE
} else {
@ -112,18 +111,18 @@ class EditProfileActivity : BaseActivity() {
}
}
// binding.changeImageButton.setOnClickListener {
// Intent(Intent.ACTION_GET_CONTENT).apply {
// type = "*/*"
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
// action = Intent.ACTION_GET_CONTENT
// addCategory(Intent.CATEGORY_OPENABLE)
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
// uploadImageResultContract.launch(
// Intent.createChooser(this, null)
// )
// }
// }
binding.profilePic.setOnClickListener {
Intent(Intent.ACTION_GET_CONTENT).apply {
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
uploadImageResultContract.launch(
Intent.createChooser(this, null)
)
}
}
}
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -137,10 +136,10 @@ class EditProfileActivity : BaseActivity() {
val imageUri: String = clipData.getItemAt(i).uri.toString()
images.add(imageUri)
}
model.uploadImage(images.first())
model.updateImage(images.first())
} else if (data.data != null) {
images.add(data.data!!.toString())
model.uploadImage(images.first())
model.updateImage(images.first())
}
}
}

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,23 +21,33 @@ import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.pixeldroid.app.postCreation.ProgressRequestBody
import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import javax.inject.Inject
class EditProfileViewModel(application: Application) : AndroidViewModel(application) {
@HiltViewModel
class EditProfileViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Context
): ViewModel() {
@Inject
lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private val _uiState = MutableStateFlow(EditProfileActivityUiState())
val uiState: StateFlow<EditProfileActivityUiState> = _uiState
var oldProfile: Account? = null
private var oldProfile: Account? = null
var submittedChanges = false
private set
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
loadProfile()
}
@ -46,6 +56,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try {
val profile = api.verifyCredentials()
updateUserInfoDb(db, profile)
if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState ->
currentUiState.copy(
@ -76,15 +87,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
fun sendProfile() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val requestBody =
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = true,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = false
)
}
@ -97,12 +103,17 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
note = bio,
locked = privateAccount,
)
if (madeChanges()) submittedChanges = true
oldProfile = account
_uiState.update { currentUiState ->
currentUiState.copy(
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()},
bio = account.source?.note
?: account.note?.let { fromHtml(it).toString() },
name = account.display_name,
profilePictureUri = account.anyAvatar()?.toUri(),
profilePictureUri = if (profilePictureChanged) profilePictureUri
else account.anyAvatar()?.toUri(),
uploadProgress = 0,
uploadingPicture = profilePictureChanged,
privateAccount = account.locked,
sendingProfile = false,
profileSent = true,
@ -111,14 +122,13 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
error = false
)
}
if(profilePictureChanged) uploadImage()
} catch (exception: Exception) {
Log.e("TAG", exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
sendingProfile = false,
profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true
)
}
@ -145,20 +155,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
fun changesApplied() {
_uiState.update { currentUiState ->
currentUiState.copy(profileLoaded = false)
}
}
fun madeChanges(): Boolean =
with(uiState.value) {
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio }
// If source note is null, check note
val privateChanged = oldProfile?.locked != privateAccount
val displayNameChanged = oldProfile?.display_name != name
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
// If source note is null, check note
?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
?: true
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|| bioUnchanged
profilePictureChanged || privateChanged || displayNameChanged || bioChanged
}
fun clickedCard() {
@ -178,16 +184,27 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
}
}
fun uploadImage(image: String) {
//TODO fix
fun updateImage(image: String) {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = image.toUri(),
profilePictureChanged = true,
profileSent = false
)
}
}
private fun uploadImage() {
val image = uiState.value.profilePictureUri!!
val inputStream =
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri())
applicationContext.contentResolver.openInputStream(image)
?: return
val size: Long =
if (image.toUri().scheme == "content") {
getApplication<PixelDroidApplication>().contentResolver.query(
image.toUri(),
if (image.scheme == "content") {
applicationContext.contentResolver.query(
image,
null,
null,
null,
@ -203,7 +220,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
cursor.getLong(sizeIndex)
} ?: 0
} else {
image.toUri().toFile().length()
image.toFile().length()
}
val imagePart = ProgressRequestBody(inputStream, size, "image/*")
@ -225,21 +242,32 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
var postSub: Disposable? = null
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.updateProfilePicture(requestBody.parts[0])
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
val inter =
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
else api.updateProfilePictureMastodon(requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ it: Account ->
Log.e("qsdfqsdfs", it.toString())
/* onNext = */ { account: Account ->
account.anyAvatar()?.let {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = it.toUri()
)
}
}
},
{ e: Throwable ->
/* onError = */ { e: Throwable ->
Log.e("error", (e as? HttpException)?.message().orEmpty())
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = 0,
uploadingPicture = true,
uploadingPicture = false,
error = true
)
}
@ -247,9 +275,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
postSub?.dispose()
sub.dispose()
},
{
/* onComplete = */ {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureChanged = false,
uploadProgress = 100,
uploadingPicture = false
)
@ -265,7 +294,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
data class EditProfileActivityUiState(
val name: String? = null,
val bio: String? = null,
val profilePictureUri: Uri?= null,
val profilePictureUri: Uri? = null,
val profilePictureChanged: Boolean = false,
val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false,
@ -274,10 +304,4 @@ data class EditProfileActivityUiState(
val error: Boolean = false,
val uploadingPicture: Boolean = false,
val uploadProgress: Int = 0,
)
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}
)

View File

@ -6,21 +6,28 @@ import android.text.method.LinkMovementMethod
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
import org.pixeldroid.app.posts.parseHTMLText
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.setProfileImageFromURL
import retrofit2.HttpException
import java.io.IOException
@ -54,9 +61,32 @@ class ProfileActivity : BaseActivity() {
val tabs = createProfileTabs(account)
setupTabs(tabs)
setContent(account)
binding.profileMotion.setTransitionListener(
object : MotionLayout.TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?, startId: Int, endId: Int,
) {}
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {}
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
if (currentId == R.id.hideProfile && motionLayout?.startState == R.id.start) {
// If the 1st transition has been made go to the second one
motionLayout.setTransition(R.id.second)
} else if(currentId == R.id.hideProfile && motionLayout?.startState == R.id.hideProfile){
motionLayout.setTransition(R.id.first)
}
}
override fun onTransitionTrigger(
motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float,
) {}
}
)
}
private fun createProfileTabs(account: Account?): Array<Fragment>{
private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
val profileFeedFragment = ProfileFeedFragment()
profileFeedFragment.arguments = Bundle().apply {
@ -80,7 +110,7 @@ class ProfileActivity : BaseActivity() {
putSerializable(ProfileFeedFragment.COLLECTIONS, true)
}
val returnArray: Array<Fragment> = arrayOf(
val returnArray: Array<UncachedFeedFragment<FeedContent>> = arrayOf(
profileGridFragment,
profileFeedFragment,
profileCollectionsFragment
@ -100,7 +130,7 @@ class ProfileActivity : BaseActivity() {
}
private fun setupTabs(
tabs: Array<Fragment>
tabs: Array<UncachedFeedFragment<FeedContent>>,
){
binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment {
@ -132,8 +162,15 @@ class ProfileActivity : BaseActivity() {
}
}
}.attach()
}
binding.profileTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {
tabs[tab.position].onTabReClicked()
}
})
}
private fun setContent(account: Account?) {
if(account != null) {
@ -152,6 +189,9 @@ class ProfileActivity : BaseActivity() {
).show()
return@launchWhenResumed
}
updateUserInfoDb(db, myAccount)
setViews(myAccount)
}
}
@ -217,9 +257,15 @@ class ProfileActivity : BaseActivity() {
)
}
private val editResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
// Profile was edited, reload
setContent(null)
}
}
private fun onClickEditButton() {
val intent = Intent(this, EditProfileActivity::class.java)
ContextCompat.startActivity(this, intent, null)
editResult.launch(Intent(this, EditProfileActivity::class.java))
}
private fun onClickFollowers(account: Account?) {

View File

@ -101,7 +101,7 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
val view = super.onCreateView(inflater, container, savedInstanceState)
if(grid || bookmarks || collections || addCollection) {
binding.list.layoutManager = GridLayoutManager(context, 3)
binding?.list?.layoutManager = GridLayoutManager(context, 3)
}
// Get the view model
@ -191,8 +191,11 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
val url = "$domain/i/collections/create"
if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) {
Snackbar.make(binding.root, getString(R.string.new_collection_link_failed),
Snackbar.LENGTH_LONG).show()
binding?.let { binding ->
Snackbar.make(
binding.root, getString(R.string.new_collection_link_failed),
Snackbar.LENGTH_LONG).show()
}
}
}

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

@ -1,28 +1,25 @@
package org.pixeldroid.app.utils
import android.content.*
import android.content.ActivityNotFoundException
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.ImageDecoder
import android.graphics.Matrix
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.DisplayMetrics
import android.view.WindowManager
import android.webkit.MimeTypeMap
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
@ -34,7 +31,7 @@ import okhttp3.HttpUrl
import org.pixeldroid.app.R
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.Locale
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@ -338,18 +338,31 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String? = null
): Account
//@Multipart
@PATCH("/api/v1/accounts/update_credentials")
suspend fun updateCredentials(
@Query(value = "display_name") displayName: String?,
@Query(value = "note") note: String?,
@Query(value = "locked") locked: Boolean?,
// @Part avatar: MultipartBody.Part?,
): Account
/**
* Pixelfed uses PHP, multipart uploads don't work through PATCH so we use POST as suggested
* here: https://github.com/pixelfed/pixelfed/issues/4250
* However, changing to POST breaks the upload on Mastodon.
*
* To have this work on Pixelfed and Mastodon without special logic to distinguish the two,
* we'll have to wait for PHP 8.4 and https://wiki.php.net/rfc/rfc1867-non-post
* which should come out end of 2024
*/
@Multipart
@POST("/api/v1/accounts/update_credentials")
fun updateProfilePicture(
@Part avatar: MultipartBody.Part?
): Observable<Account>
@Multipart
@PATCH("/api/v1/accounts/update_credentials")
fun updateProfilePicture(
fun updateProfilePictureMastodon(
@Part avatar: MultipartBody.Part?
): Observable<Account>

View File

@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
PublicFeedStatusDatabaseEntity::class,
Notification::class
],
version = 5
version = 6
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@ -44,4 +44,9 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
}
}
val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1")
}
}

View File

@ -13,41 +13,58 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
import org.pixeldroid.app.utils.normalizeDomain
import java.lang.IllegalArgumentException
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) {
suspend fun addUser(
db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
) {
db.userDao().insertOrUpdate(
UserDatabaseEntity(
user_id = account.id!!,
instance_uri = normalizeDomain(instance_uri),
username = account.username!!,
display_name = account.getDisplayName(),
avatar_static = account.anyAvatar().orEmpty(),
isActive = activeUser,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
UserDatabaseEntity(
user_id = account.id!!,
instance_uri = normalizeDomain(instance_uri),
username = account.username!!,
display_name = account.getDisplayName(),
avatar_static = account.anyAvatar().orEmpty(),
isActive = activeUser,
accessToken = accessToken,
refreshToken = refreshToken,
clientId = clientId,
clientSecret = clientSecret
)
)
}
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
val user = db.userDao().getActiveUser()!!
db.userDao().updateUserAccountDetails(
account.username.orEmpty(),
account.display_name.orEmpty(),
account.anyAvatar().orEmpty(),
user.user_id,
user.instance_uri
)
}
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
InstanceDatabaseEntity(
uri = normalizeDomain(metadata?.config?.site?.url!!),
title = metadata.config.site.name!!,
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE,
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
?: DEFAULT_MAX_PHOTO_SIZE,
// Pixelfed doesn't distinguish between max photo and video size
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE,
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
?: DEFAULT_MAX_VIDEO_SIZE,
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED,
pixelfed = metadata.software?.repo?.contains("pixelfed", ignoreCase = true) == true
)
} ?: instance?.run {
InstanceDatabaseEntity(
uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
pixelfed = false
)
} ?: throw IllegalArgumentException("Cannot store instance where both are null")

View File

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

View File

@ -1,6 +1,12 @@
package org.pixeldroid.app.utils.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@Dao
@ -9,17 +15,21 @@ interface UserDao {
* Insert a user, if it already exists return -1
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertUser(user: UserDatabaseEntity): Long
suspend fun insertUser(user: UserDatabaseEntity): Long
@Transaction
fun insertOrUpdate(user: UserDatabaseEntity) {
suspend fun insertOrUpdate(user: UserDatabaseEntity) {
if (insertUser(user) == -1L) {
updateUser(user)
}
}
@Update
fun updateUser(user: UserDatabaseEntity)
suspend fun updateUser(user: UserDatabaseEntity)
@Query("UPDATE users SET username = :username, display_name = :displayName, avatar_static = :avatarStatic WHERE user_id = :id and instance_uri = :instanceUri")
suspend fun updateUserAccountDetails(username: String, displayName: String, avatarStatic: String, id: String, instanceUri: String)
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
@ -27,6 +37,9 @@ interface UserDao {
@Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity>
@Query("SELECT * FROM users")
fun getAllFlow(): Flow<List<UserDatabaseEntity>>
@Query("SELECT * FROM users WHERE isActive=1")
fun getActiveUser(): UserDatabaseEntity?

View File

@ -4,20 +4,22 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "instances")
data class InstanceDatabaseEntity (
@PrimaryKey var uri: String,
var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance?
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
data class InstanceDatabaseEntity(
@PrimaryKey var uri: String,
var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance?
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
// Is this Pixelfed instance?
var pixelfed: Boolean = true,
) {
companion object{
companion object {
// Default max number of chars for Mastodon: used when their is no other value supplied by
// either NodeInfo or the instance endpoint
const val DEFAULT_MAX_TOOT_CHARS = 500

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

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

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

@ -3,88 +3,104 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scrollview"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
tools:context=".posts.PostActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"/>
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:background="?attr/colorSurface"
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/postFragmentSingle"
layout="@layout/post_fragment" />
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/postFragmentSingle"
tools:layout_editor_absoluteX="10dp">
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"
android:layout_width="0dp"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/submitComment"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/comment"
android:importantForAutofill="no"
android:inputType="text|textCapSentences|textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/submit_comment"
android:text="@string/comment"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
android:background="?attr/colorSecondaryContainer"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintPost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar">
<include
android:id="@+id/postFragmentSingle"
layout="@layout/post_fragment" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/constraintPost"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/submitComment"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/comment_noun"
android:importantForAutofill="no"
android:inputType="text|textCapSentences|textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/submit_comment"
android:text="@string/comment_verb"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/commentFragment"
android:layout_width="match_parent"
android:layout_height="500dp"
android:fillViewport="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/commentIn" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/commentFragment"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -3,154 +3,155 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".profile.ProfileActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/profileMotion"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/colorSecondaryContainer"
app:layoutDescription="@xml/collapsing_motion_layout_scene">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:fitsSystemWindows="true"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/profile"
android:elevation="-1dp"
android:background="?attr/colorSurface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar">
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="88dp"
android:layout_height="88dp"
android:clickable="false"
android:layout_marginStart="20dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/nbPostsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"/>
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:gravity="center"
android:clickable="false"
android:text="@string/default_nposts"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
android:background="?attr/colorSurface"
<TextView
android:id="@+id/nbFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowers"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
<TextView
android:id="@+id/nbFollowingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowing"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
<TextView
android:id="@+id/accountNameTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
android:clickable="false"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:text="@string/no_username"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="12dp"
android:visibility="visible"
app:layout_collapseMode="parallax"
app:layout_constraintTop_toBottomOf="@id/nbFollowersTextView"
tools:visibility="visible">
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:clickable="false"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginStart="20dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<Button
android:id="@+id/followButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:text="@string/follow"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<TextView
android:id="@+id/nbPostsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="@string/default_nposts"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
<Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit_profile"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/nbFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowers"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
<TextView
android:id="@+id/nbFollowingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowing"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
<TextView
android:id="@+id/accountNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:text="@string/no_username"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
<Button
android:id="@+id/followButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:text="@string/follow"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit_profile"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/profileTabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/profileTabs"
android:layout_width="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:background="?attr/colorSurface"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/profileTabs"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -16,6 +16,8 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
app:titleTextColor="?attr/colorOnSecondaryContainer"
android:background="?attr/colorSecondaryContainer"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
@ -38,7 +40,6 @@
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -8,14 +8,6 @@
android:layout_marginBottom="5dp"
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clipChildren="false"
android:clipToPadding="false">
<ImageView
android:id="@+id/profilePic"
android:layout_width="50dp"
@ -243,6 +235,4 @@
app:layout_constraintTop_toBottomOf="@+id/postDate"
tools:text="3 comments" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -48,7 +48,7 @@
<string name="loading_toast">حدث خلل اثناء التحميل</string>
<string name="upload_post_error">فشل في تحميل المنشور</string>
<string name="comment">تعليق</string>
<string name="comment_verb">تعليق</string>
<string name="comment_posted">التعليق: تم نشر%1$s!</string>
<string name="comment_error">خطأ في التعليق!</string>
<string name="share_image">مشاركة الصورة</string>

View File

@ -56,7 +56,7 @@
<string name="follow_error">No s\'ha pogut seguir</string>
<string name="follow_button_failed">No s\'ha pogut mostrar el botó de seguir</string>
<string name="follow_status_failed">No s\'ha pogut obtenir l\'estat de seguiment</string>
<string name="comment">Comentari</string>
<string name="comment_verb">Comentari</string>
<string name="comment_posted">Comentari: %1$s publicat!</string>
<string name="comment_error">Error de comentari!</string>
<string name="share_image">Compartir imatge</string>

View File

@ -72,7 +72,7 @@
<string name="share_image">Sdílet obrázek</string>
<string name="comment_error">Chyba komentáře!</string>
<string name="comment_posted">Komentář: %1$s zveřejněn!</string>
<string name="comment">Komentář</string>
<string name="comment_verb">Komentář</string>
<plurals name="number_comments">
<item quantity="one">%d komentář</item>
<item quantity="few">%d komentáře</item>

View File

@ -45,7 +45,7 @@
<string name="add_account_description">Weiteres Pixelfed-Konto hinzufügen</string>
<string name="light_theme">Hell</string>
<string name="dark_theme">Dunkel</string>
<string name="comment">Kommentieren</string>
<string name="comment_verb">Kommentieren</string>
<string name="share_image">Bild teilen</string>
<string name="empty_comment">Der Kommentar darf nicht leer sein!</string>
<string name="no_description">Keine Beschreibung</string>

View File

@ -60,7 +60,7 @@
<string name="follow_error">No pudo seguir</string>
<string name="follow_button_failed">No se pudo mostrar el botón de seguimiento</string>
<string name="follow_status_failed">No se pudo obtener el estado de seguidores</string>
<string name="comment">Comentar</string>
<string name="comment_verb">Comentar</string>
<string name="comment_posted">¡Comentario: %1$s publicado!</string>
<string name="comment_error">¡Error al comentar!</string>
<string name="share_image">Compartir imagen</string>

View File

@ -75,7 +75,7 @@
<string name="follow_error">Ezin izan da jarraitu</string>
<string name="follow_button_failed">Ezin izan da jarraipen-botoia erakutsi</string>
<string name="follow_status_failed">Ezin izan da segimendu-egoera lortu</string>
<string name="comment">Iruzkin</string>
<string name="comment_verb">Iruzkin</string>
<string name="posted_on">%1$s(e)n argitaratua</string>
<string name="hashtags">TRAOLAK</string>
<string name="accounts">KONTUAK</string>

View File

@ -59,7 +59,7 @@
<string name="share_image">هم‌رسانی تصویر</string>
<string name="comment_error">خطا در درج نظر!</string>
<string name="comment_posted">نظر: %1$s منتشر کرد!</string>
<string name="comment">نظر</string>
<string name="comment_verb">نظر</string>
<string name="follow_status_failed">نتوانستیم وضعیت پی‌گیری را دریافت کنیم</string>
<string name="follow_button_failed">نتوانستیم دکمه پی‌گیری را نمایش دهیم</string>
<string name="follow_error">نتوانستیم پی‌گیری کنیم</string>

View File

@ -108,7 +108,7 @@
<string name="search">Hae</string>
<string name="unfollow">Seurataan</string>
<string name="follow">Seuraa</string>
<string name="comment">Kommentti</string>
<string name="comment_verb">Kommentti</string>
<string name="image_download_downloading">Ladataan…</string>
<string name="gallery_button_alt">Galleria</string>
<string name="edit">Muokkaa</string>

View File

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

View File

@ -57,7 +57,7 @@
<string name="follow_error">Non se puido seguir</string>
<string name="follow_button_failed">Non se pode mostrar o botón para seguir</string>
<string name="follow_status_failed">Non se obtivo o estado de relacións</string>
<string name="comment">Comentar</string>
<string name="comment_verb">Comentar</string>
<string name="comment_posted">Comentario: %1$s publicado!</string>
<string name="comment_error">Fallo ao comentar!</string>
<string name="share_image">Compartir Imaxe</string>

View File

@ -102,7 +102,7 @@
<item quantity="one">%d hozzászólás</item>
<item quantity="other">%d hozzászólás</item>
</plurals>
<string name="comment">Hozzászólás</string>
<string name="comment_verb">Hozzászólás</string>
<string name="comment_posted">Hozzászólás: %1$s közzétéve!</string>
<string name="comment_error">Hozzászólási hiba!</string>
<string name="share_image">Kép megosztása</string>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -38,7 +38,7 @@
<string name="empty_comment">Komentar tidak boleh kosong!</string>
<string name="share_image">Bagikan Gambar</string>
<string name="comment_posted">Komentar: %1$s diposting!</string>
<string name="comment">Komentar</string>
<string name="comment_verb">Komentar</string>
<plurals name="number_comments">
<item quantity="other">%d komentar</item>
</plurals>

View File

@ -12,7 +12,7 @@
<string name="shared_notification">%1$s ha condiviso il tuo post</string>
<string name="followed_notification">%1$s ti ha seguito</string>
<string name="mention_notification">%1$s ti ha menzionato</string>
<string name="liked_notification">%1$s è piaciuto il tuo post</string>
<string name="liked_notification">A %1$s è piaciuto il tuo post</string>
<string name="post">Invia</string>
<string name="description">Descrizione…</string>
<string name="whats_an_instance">Cos\'è un\'istanza\?</string>
@ -33,7 +33,7 @@
<string name="domain_of_your_instance">Dominio della tua istanza</string>
<string name="login_connection_required_once">Devi essere online per poter aggiungere il primo account e utilizzare PixelDroid :(</string>
<string name="invalid_domain">Dominio non valido</string>
<string name="edit">MODIFICA</string>
<string name="edit">Modifica</string>
<string name="instance_error">Impossibile ottenere informazioni sull\'istanza</string>
<string name="save_image_failed">Impossibile salvare l\'immagine</string>
<string name="light_theme">Chiaro</string>
@ -56,7 +56,7 @@
<string name="share_image">Condividi immagine</string>
<string name="comment_error">Errore nel commento!</string>
<string name="comment_posted">Commento: %1$s postato!</string>
<string name="comment">Commento</string>
<string name="comment_verb">Commento</string>
<string name="follow_error">Impossibile seguire</string>
<string name="unfollow_error">Impossibile smettere di seguire</string>
<string name="access_token_invalid">Il token di accesso non è valido</string>
@ -85,11 +85,11 @@
<string name="instance_not_pixelfed_warning">Questa non sembra essere un\'istanza di Pixelfed, quindi l\'app potrebbe interrompersi in modi inaspettati.</string>
<string name="instance_not_pixelfed_cancel">Annullare l\'accesso</string>
<string name="instance_not_pixelfed_continue">OK, continua comunque</string>
<string name="discover">SCOPRI</string>
<string name="discover">Scopri</string>
<string name="open_drawer_menu">Apri il menu a scomparsa</string>
<string name="profile_picture">Foto profilo</string>
<string name="report_error">Impossibile inviare la segnalazione</string>
<string name="reported">Segnalato {gmd_check_circle}</string>
<string name="reported">Post segnalato</string>
<string name="report_target">Segnala @%1$s\'s post</string>
<string name="optional_report_comment">Messaggio facoltativo per moderatori/amministratori</string>
<string name="share_link">Condividi il Link</string>
@ -132,25 +132,32 @@
<plurals name="nb_followers">
<item quantity="one">%d
\nSeguace</item>
<item quantity="many">%d
\nSeguaci</item>
<item quantity="other">%d
\nSeguaci</item>
</plurals>
<plurals name="nb_posts">
<item quantity="one">%d
\nPost</item>
<item quantity="many">%d
\nPost</item>
<item quantity="other">%d
\nPost</item>
</plurals>
<plurals name="number_comments">
<item quantity="one">%d commento</item>
<item quantity="many">%d commenti</item>
<item quantity="other">%d commenti</item>
</plurals>
<plurals name="shares">
<item quantity="one">%d Condivisione</item>
<item quantity="many">%d Condivisioni</item>
<item quantity="other">%d Condivisioni</item>
</plurals>
<plurals name="likes">
<item quantity="one">%d Mi piace</item>
<item quantity="many">%d Mi piace</item>
<item quantity="other">%d Mi piace</item>
</plurals>
<string name="upload_error">Codice di errore restituito dal server: %1$d</string>
@ -163,11 +170,13 @@
<item quantity="other">La descrizione deve contenere al massimo %d caratteri.</item>
</plurals>
<string name="api_not_enabled_dialog">L\'API non è attivata su questa istanza. Contatta l\'amministratore per chiedergli di attivarlo.</string>
<string name="delete_post_failed_io_except">Impossibile eliminare il post, controlla la tua connesione\?</string>
<string name="delete_post_failed_io_except">Impossibile eliminare il post, controllare la connessione?</string>
<string name="delete_post_failed_error">Impossibile eliminare il post, errore %1$d</string>
<plurals name="nb_following">
<item quantity="one">%d
\nUtente che stai seguendo</item>
<item quantity="many">%d
\nUtenti che stai seguendo</item>
<item quantity="other">%d
\nUtenti che stai seguendo</item>
</plurals>
@ -206,4 +215,102 @@
<string name="upload_next_step">Prossimo step</string>
<string name="add_details">Aggiungi qualche dettaglio</string>
<string name="unknown_error_in_error">Errore sconosciuto, controlla se il server è giù: %1$s</string>
<string name="feed_view">Vista a elenco</string>
<string name="follow_requested">Follow Richiesto</string>
<string name="bookmark">Aggiungi ai preferiti</string>
<string name="unbookmark">Rimuovi dai preferiti</string>
<string name="redraft">Rielabora</string>
<string name="from_other_domain">da %1$s</string>
<string name="redraft_dialog_launch">Rielaborare questo post ti permetterà di modificare la foto e la sua descrizione, ma eliminerà tutti i commenti e i mi piace attualmente presenti. Continuare?</string>
<string name="bookmark_post_failed_io_except">Non è stato possibile aggiungere/rimuovere il post dai preferiti, controllare la connessione?</string>
<string name="no_storage_permission">Accesso all\'archiviazione non autorizzato, aggiungi l\'autorizzazione nelle impostazioni se vuoi permettere a PixelDroid di mostrare le miniature</string>
<string name="description_template_summary">Inserisci questo nella descrizione dei post</string>
<string name="dialog_message_cancel_follow_request">Cancellare la richiesta di follow?</string>
<string name="home_feed">Home</string>
<string name="search_discover_feed">Cerca</string>
<string name="create_feed">Aggiungi</string>
<string name="notifications_feed">Aggiornamenti</string>
<string name="public_feed">Pubblico</string>
<string name="accentColorTitle">Colore secondario</string>
<string name="accentColorSummary">Scegli un colore secondario</string>
<string name="color_choice_button">Scegli questo colore secondario</string>
<string name="color_chosen">Colore secondario scelto</string>
<string name="redraft_dialog_cancel">Se elimini questa rielaborazione, il post originale non sarà più sul tuo profilo. Continuare senza ripubblicare?</string>
<string name="redraft_post_failed_error">Impossibile rielaborare il post, errore %1$d</string>
<string name="redraft_post_failed_io_except">Non è stato possibile rielaborare il post, controllare la connessione?</string>
<string name="bookmark_post_failed_error">Non è stato possibile aggiungere/rimuovere il post dai preferiti, errore %1$d</string>
<string name="no_camera_permission">Accesso alla fotocamera non autorizzato, aggiungi l\'autorizzazione nelle impostazioni se vuoi permettere a PixelDroid di usare la fotocamera</string>
<string name="play_video">Riproduci video</string>
<string name="encode_error">Errore durante la codifica</string>
<string name="encode_success">Codifica completata!</string>
<string name="encode_progress">Codifica %1$d%%</string>
<string name="analyzing_stabilization">Analisi per la stabilizzazione %1$d%%</string>
<string name="still_encoding">Uno o più video sono ancora in fase di codifica. Aspetta che siano pronti prima di caricare</string>
<string name="new_post_shortcut_short">Nuovo post</string>
<string name="follow_request">%1$s vorrebbe seguirti</string>
<string name="status_notification">%1$s ha aggiunto un post</string>
<string name="new_post_shortcut_long">Aggiungi un nuovo post</string>
<string name="profile_error">Impossibile caricare il profilo</string>
<string name="add_images_error">Errore durante l\'aggiunto delle immagini</string>
<string name="notification_thumbnail">Miniatura dell\'immagine nella notifica di questo post</string>
<string name="post_preview">Anteprima di un post</string>
<string name="description_template">Modello di descrizione</string>
<string name="explore_accounts">Scopri profili popolari in questa istanza</string>
<string name="popular_accounts">Profili popolari</string>
<string name="explore_hashtags">Scopri gli hashtag di tendenza in questa istanza</string>
<string name="trending_hashtags">Hashtag di tendenza</string>
<string name="daily_trending">Vedi i post di tendenza oggi</string>
<string name="trending_posts">Post di tendenza</string>
<string name="explore_posts">Scopri post casuali di oggi</string>
<string name="grid_view">Vista a griglia</string>
<string name="bookmarks">Preferiti</string>
<string name="comment_noun">Commento</string>
<string name="delete_collection_warning">Vuoi davvero eliminare questa raccolta?</string>
<string name="removed_post_from_collection">Post rimosso dalla raccolta</string>
<string name="new_collection_link_failed">Impossibile aprire la pagina per aggiungere una raccolta</string>
<string name="private_account_explanation">Se il tuo account è privato, solo le persone che approvi possono vedere le tue foto e i tuoi video su Pixelfed. I tuoi seguaci esistenti non subiranno modifiche.</string>
<plurals name="replies_count">
<item quantity="one">%d risposta</item>
<item quantity="many">%d risposte</item>
<item quantity="other">%d risposte</item>
</plurals>
<string name="collection_title">La raccolta di %1$s</string>
<string name="story_image">Immagine della storia</string>
<string name="replyToStory">Rispondi a %1$s</string>
<string name="story_reply_error">Qualcosa è andato storto nell\'invio della risposta</string>
<string name="error_fetch_story">Qualcosa è andato storto nel recupero del carosello</string>
<string name="sent_reply_story">Risposta inviata</string>
<string name="fetching_profile">Recupero del tuo profilo…</string>
<string name="collection_remove_post">Rimuovi post</string>
<string name="add_to_collection">Scegli un post da aggiungere</string>
<string name="delete_from_collection">Scegli un post da rimuovere</string>
<string name="added_post_to_collection">Post aggiunto alla raccolta</string>
<string name="error_add_post_to_collection">Impossibile aggiungere il post alla raccolta</string>
<string name="error_remove_post_from_collection">Impossibile rimuovere il post dalla raccolta</string>
<string name="save">Salva</string>
<string name="use_dynamic_color">Usa il colore dinamico dal tuo sistema</string>
<string name="more_profile_settings">Altre impostazioni del profilo</string>
<string name="private_account">Account privato</string>
<string name="your_bio">La tua bio</string>
<string name="your_name">Il tuo nome</string>
<string name="profile_save_changes">Non hai salvato le modifiche. Uscire?</string>
<string name="saving_profile">Salvataggio del tuo profilo</string>
<string name="profile_saved">Modifiche salvate!</string>
<string name="error_profile">Qualcosa è andato storto. Tocca per riprovare</string>
<string name="change_profile_picture">Cambia la tua immagine del profilo</string>
<string name="contains_nsfw">Contiene media sensibili</string>
<string name="switch_accounts">Cambia account</string>
<string name="summary_always_show_nsfw">I post con contenuti sensibili non verranno oscurati e saranno mostrati di default.</string>
<string name="collection_add_post">Aggiungi post</string>
<string name="collections">Raccolte</string>
<string name="delete_collection">Elimina raccolta</string>
<string name="add_story">Aggiungi Storia</string>
<string name="story_pause">Riproduci o ferma le storie</string>
<string name="story_could_not_see">Errore: impossibile segnare la storia come visualizzata</string>
<string name="my_story">La mia storia</string>
<string name="type_story">Storia</string>
<string name="type_post">Post</string>
<string name="continue_post_creation">Continua</string>
<string name="extraneous_pictures_stories">Le immagini successive alla prima sono state rimosse, ma possono essere ripristinate tornando indietro alla creazione di un Post</string>
<string name="story_duration">Durata della Storia</string>
</resources>

View File

@ -64,7 +64,7 @@
<string name="follow_error">フォローできませんでした</string>
<string name="follow_button_failed">フォローボタンを表示できませんでした</string>
<string name="follow_status_failed">フォローステータスを取得できませんでした</string>
<string name="comment">コメント</string>
<string name="comment_verb">コメント</string>
<string name="comment_error">コメントエラー</string>
<string name="share_image">画像を共有</string>
<string name="no_description">説明はありません</string>

View File

@ -66,7 +66,7 @@
<string name="instance_error">Kon instance-informatie niet ophalen</string>
<string name="request_format_error">Fout bij het uploaden: slecht verzoekformaat</string>
<string name="posted_on">Gepost op %1$s</string>
<string name="comment">Commentaar maken</string>
<string name="comment_verb">Commentaar maken</string>
<string name="comment_posted">Commentaar: %1$s gepost!</string>
<string name="comment_error">Fout met commentaar!</string>
<string name="share_image">Afbeelding delen</string>

View File

@ -43,7 +43,7 @@
<string name="share_image">Udostępnij</string>
<string name="comment_error">Błąd komentarza!</string>
<string name="comment_posted">Komentarz: %1$s opublikowany!</string>
<string name="comment">Komentarz</string>
<string name="comment_verb">Komentarz</string>
<string name="menu_account">Mój profil</string>
<string name="registration_failed">Nie udało się zarejestrować aplikacji na tym serwerze</string>
<string name="instance_error">Nie udało się pobrać informacji o instancji</string>

View File

@ -158,7 +158,7 @@
<string name="unfollow_error">Não foi possível deixar de seguir</string>
<string name="action_not_allowed">Esta ação não é permitida</string>
<string name="follow_error">Não foi possível seguir</string>
<string name="comment">Comentar</string>
<string name="comment_verb">Comentar</string>
<plurals name="description_max_characters">
<item quantity="one">A descrição precisa ter pelo menos %d carácter.</item>
<item quantity="other">A descrição precisa ter pelo menos %d caracteres.</item>

View File

@ -85,7 +85,7 @@
<string name="comment_error">Erro ao comentar!</string>
<string name="write_permission_download_pic">Precisa adicionar a permissão para transferir imagens!</string>
<string name="comment_posted">Comentário: %1$s publicou!</string>
<string name="comment">Comentário</string>
<string name="comment_verb">Comentário</string>
<string name="add_comment">Adicionar um comentário</string>
<string name="submit_comment">Submeter comentário</string>
<string name="post_is_album">Esta publicação é um álbum</string>

View File

@ -37,7 +37,7 @@
<string name="add_account_description">Добавить другой аккаунт Pixelfed</string>
<string name="add_account_name">Добавить аккаунт</string>
<string name="instance_error">Не удалось получить информацию об экземпляре</string>
<string name="comment">Комментировать</string>
<string name="comment_verb">Комментировать</string>
<string name="comment_posted">Комментарий: %1$s опубликован!</string>
<string name="comment_error">Ошибка комментирования!</string>
<string name="share_image">Поделиться изображением</string>

View File

@ -60,7 +60,7 @@
<string name="follow_error">Kunde inte följa</string>
<string name="follow_button_failed">Kunde inte visa följarknapp</string>
<string name="follow_status_failed">Kunde inte hämta status för följare</string>
<string name="comment">Kommentera</string>
<string name="comment_verb">Kommentera</string>
<string name="comment_posted">Kommentar: %1$s inlagd!</string>
<string name="comment_error">Kommentarsfel!</string>
<string name="share_image">Dela bild</string>

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

View File

@ -60,7 +60,7 @@
<string name="follow_error">无法关注</string>
<string name="follow_button_failed">无法显示关注按钮</string>
<string name="follow_status_failed">无法获得关注状态</string>
<string name="comment">评论</string>
<string name="comment_verb">评论</string>
<string name="comment_posted">评论: %1$s 已发布!</string>
<string name="comment_error">评论错误!</string>
<string name="share_image">分享图像</string>

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

@ -153,7 +153,10 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="share_image">Share Image</string>
<string name="comment_error">Comment error!</string>
<string name="comment_posted">"Comment: %1$s posted!"</string>
<string name="comment">Comment</string>
<!-- This is shown on the button, it is a verb and refers to the action of commenting -->
<string name="comment_verb">Comment</string>
<!-- This is shown in the text field as a hint, it is a noun -->
<string name="comment_noun">Comment</string>
<plurals name="number_comments">
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<Transition
android:id="@+id/first"
app:constraintSetStart="@id/start"
app:constraintSetEnd="@id/hideProfile">
<OnSwipe
app:touchAnchorId="@+id/profile"
app:touchAnchorSide="top"
app:dragDirection="dragUp" />
</Transition>
<Transition
android:id="@+id/second"
app:constraintSetStart="@id/hideProfile"
app:constraintSetEnd="@id/hideBars">
<OnSwipe
app:touchAnchorId="@+id/profileTabs"
app:touchAnchorSide="top"
app:dragDirection="dragUp" />
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/hideProfile">
<Constraint android:id="@id/profile"
android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="@id/top_bar"
app:layout_constraintStart_toStartOf="parent">
</Constraint>
<Constraint android:id="@id/profileTabs"
android:layout_width="match_parent"
app:layout_constraintTop_toBottomOf="@id/top_bar"
app:layout_constraintStart_toStartOf="parent">
</Constraint>
<Constraint android:id="@id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/hideBars">
<Constraint android:id="@id/profile"
android:layout_width="match_parent"
app:layout_constraintBottom_toBottomOf="@id/top_bar"
app:layout_constraintStart_toStartOf="parent">
</Constraint>
<Constraint android:id="@id/profileTabs"
android:layout_width="match_parent"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent">
</Constraint>
<Constraint android:id="@id/top_bar"
android:layout_width="match_parent"
app:layout_constraintBottom_toTopOf="@id/profileTabs"
app:layout_constraintStart_toStartOf="parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent">
</Constraint>
</ConstraintSet>
</MotionScene>

View File

@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0'
classpath 'com.android.tools.build:gradle:8.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@ -16,6 +16,7 @@ buildscript {
plugins {
id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false
id("com.google.dagger.hilt.android") version "2.50" apply false
}
allprojects {
@ -23,15 +24,6 @@ allprojects {
google()
mavenCentral()
maven { url "https://jitpack.io" }
//noinspection JcenterRepositoryObsolete
jcenter {
content {
// info.androidhive:imagefilters is only available in JCenter
//TODO remove JCenter repo:
// see issue https://gitlab.shinice.net/pixeldroid/PixelDroid/-/issues/278
includeGroup("info.androidhive")
}
}
}
}

View File

@ -0,0 +1,7 @@
Bug fix for opening album full-screen crashing
Use hardware acceleration (OpenGL! Shaders!) for image editing. Currently just trying to mimic the previous implementation, but this will provide building blocks for much cooler things later on :)
Tell us if something doesn't work right! (email, Mastodon)
There are also translation updates in this release :)

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)

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